MA-779 Update student dashboard to use CourseOverview
This commit is contained in:
@@ -23,8 +23,9 @@ VERIFY_STATUS_MISSED_DEADLINE = "verify_missed_deadline"
|
||||
VERIFY_STATUS_NEED_TO_REVERIFY = "verify_need_to_reverify"
|
||||
|
||||
|
||||
def check_verify_status_by_course(user, course_enrollment_pairs, all_course_modes):
|
||||
"""Determine the per-course verification statuses for a given user.
|
||||
def check_verify_status_by_course(user, course_enrollments, all_course_modes):
|
||||
"""
|
||||
Determine the per-course verification statuses for a given user.
|
||||
|
||||
The possible statuses are:
|
||||
* VERIFY_STATUS_NEED_TO_VERIFY: The student has not yet submitted photos for verification.
|
||||
@@ -46,8 +47,7 @@ def check_verify_status_by_course(user, course_enrollment_pairs, all_course_mode
|
||||
|
||||
Arguments:
|
||||
user (User): The currently logged-in user.
|
||||
course_enrollment_pairs (list): The courses the user is enrolled in.
|
||||
The list should contain tuples of `(Course, CourseEnrollment)`.
|
||||
course_enrollments (list[CourseEnrollment]): The courses the user is enrolled in.
|
||||
all_course_modes (list): List of all course modes for the student's enrolled courses,
|
||||
including modes that have expired.
|
||||
|
||||
@@ -75,15 +75,15 @@ def check_verify_status_by_course(user, course_enrollment_pairs, all_course_mode
|
||||
|
||||
recent_verification_datetime = None
|
||||
|
||||
for course, enrollment in course_enrollment_pairs:
|
||||
for enrollment in course_enrollments:
|
||||
|
||||
# Get the verified mode (if any) for this course
|
||||
# We pass in the course modes we have already loaded to avoid
|
||||
# another database hit, as well as to ensure that expired
|
||||
# course modes are included in the search.
|
||||
verified_mode = CourseMode.verified_mode_for_course(
|
||||
course.id,
|
||||
modes=all_course_modes[course.id]
|
||||
enrollment.course_id,
|
||||
modes=all_course_modes[enrollment.course_id]
|
||||
)
|
||||
|
||||
# If no verified mode has ever been offered, or the user hasn't enrolled
|
||||
@@ -156,7 +156,7 @@ def check_verify_status_by_course(user, course_enrollment_pairs, all_course_mode
|
||||
if deadline is not None and deadline > now:
|
||||
days_until_deadline = (deadline - now).days
|
||||
|
||||
status_by_course[course.id] = {
|
||||
status_by_course[enrollment.course_id] = {
|
||||
'status': status,
|
||||
'days_until_deadline': days_until_deadline
|
||||
}
|
||||
|
||||
@@ -850,6 +850,13 @@ class CourseEnrollment(models.Model):
|
||||
unique_together = (('user', 'course_id'),)
|
||||
ordering = ('user', 'course_id')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CourseEnrollment, self).__init__(*args, **kwargs)
|
||||
|
||||
# Private variable for storing course_overview to minimize calls to the database.
|
||||
# When the property .course_overview is accessed for the first time, this variable will be set.
|
||||
self._course_overview = None
|
||||
|
||||
def __unicode__(self):
|
||||
return (
|
||||
"[CourseEnrollment] {}: {} ({}); active: ({})"
|
||||
@@ -1318,10 +1325,21 @@ class CourseEnrollment(models.Model):
|
||||
@property
|
||||
def course_overview(self):
|
||||
"""
|
||||
Return a CourseOverview of this enrollment's course.
|
||||
"""
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
return CourseOverview.get_from_id(self.course_id)
|
||||
Returns a CourseOverview of the course to which this enrollment refers.
|
||||
Returns None if an error occurred while trying to load the course.
|
||||
|
||||
Note:
|
||||
If the course is re-published within the lifetime of this
|
||||
CourseEnrollment object, then the value of this property will
|
||||
become stale.
|
||||
"""
|
||||
if not self._course_overview:
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
try:
|
||||
self._course_overview = CourseOverview.get_from_id(self.course_id)
|
||||
except (CourseOverview.DoesNotExist, IOError):
|
||||
self._course_overview = None
|
||||
return self._course_overview
|
||||
|
||||
def is_verified_enrollment(self):
|
||||
"""
|
||||
|
||||
@@ -14,7 +14,7 @@ from xmodule.modulestore.django import modulestore
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from django.test.client import Client
|
||||
from student.models import CourseEnrollment
|
||||
from student.views import get_course_enrollment_pairs
|
||||
from student.views import get_course_enrollments
|
||||
from util.milestones_helpers import (
|
||||
get_pre_requisite_courses_not_completed,
|
||||
set_prerequisite_courses,
|
||||
@@ -73,13 +73,13 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
self._create_course_with_access_groups(course_location)
|
||||
|
||||
# get dashboard
|
||||
courses_list = list(get_course_enrollment_pairs(self.student, None, []))
|
||||
courses_list = list(get_course_enrollments(self.student, None, []))
|
||||
self.assertEqual(len(courses_list), 1)
|
||||
self.assertEqual(courses_list[0][0].id, course_location)
|
||||
self.assertEqual(courses_list[0].course_id, course_location)
|
||||
|
||||
CourseEnrollment.unenroll(self.student, course_location)
|
||||
# get dashboard
|
||||
courses_list = list(get_course_enrollment_pairs(self.student, None, []))
|
||||
courses_list = list(get_course_enrollments(self.student, None, []))
|
||||
self.assertEqual(len(courses_list), 0)
|
||||
|
||||
def test_errored_course_regular_access(self):
|
||||
@@ -95,7 +95,7 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
self.assertIsInstance(modulestore().get_course(course_key), ErrorDescriptor)
|
||||
|
||||
# get courses through iterating all courses
|
||||
courses_list = list(get_course_enrollment_pairs(self.student, None, []))
|
||||
courses_list = list(get_course_enrollments(self.student, None, []))
|
||||
self.assertEqual(courses_list, [])
|
||||
|
||||
def test_course_listing_errored_deleted_courses(self):
|
||||
@@ -112,9 +112,9 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
self._create_course_with_access_groups(course_location, default_store=ModuleStoreEnum.Type.mongo)
|
||||
mongo_store.delete_course(course_location, ModuleStoreEnum.UserID.test)
|
||||
|
||||
courses_list = list(get_course_enrollment_pairs(self.student, None, []))
|
||||
courses_list = list(get_course_enrollments(self.student, None, []))
|
||||
self.assertEqual(len(courses_list), 1, courses_list)
|
||||
self.assertEqual(courses_list[0][0].id, good_location)
|
||||
self.assertEqual(courses_list[0].course_id, good_location)
|
||||
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
|
||||
def test_course_listing_has_pre_requisite_courses(self):
|
||||
@@ -142,9 +142,11 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
|
||||
set_prerequisite_courses(course_location, pre_requisite_courses)
|
||||
# get dashboard
|
||||
course_enrollment_pairs = list(get_course_enrollment_pairs(self.student, None, []))
|
||||
courses_having_prerequisites = frozenset(course.id for course, _enrollment in course_enrollment_pairs
|
||||
if course.pre_requisite_courses)
|
||||
course_enrollments = list(get_course_enrollments(self.student, None, []))
|
||||
courses_having_prerequisites = frozenset(
|
||||
enrollment.course_id for enrollment in course_enrollments
|
||||
if enrollment.course_overview.pre_requisite_courses
|
||||
)
|
||||
courses_requirements_not_met = get_pre_requisite_courses_not_completed(
|
||||
self.student,
|
||||
courses_having_prerequisites
|
||||
|
||||
@@ -15,7 +15,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from student.models import CourseEnrollment, DashboardConfiguration
|
||||
from student.views import get_course_enrollment_pairs, _get_recently_enrolled_courses
|
||||
from student.views import get_course_enrollments, _get_recently_enrolled_courses # pylint: disable=protected-access
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
@@ -67,7 +67,7 @@ class TestRecentEnrollments(ModuleStoreTestCase):
|
||||
self._configure_message_timeout(60)
|
||||
|
||||
# get courses through iterating all courses
|
||||
courses_list = list(get_course_enrollment_pairs(self.student, None, []))
|
||||
courses_list = list(get_course_enrollments(self.student, None, []))
|
||||
self.assertEqual(len(courses_list), 2)
|
||||
|
||||
recent_course_list = _get_recently_enrolled_courses(courses_list)
|
||||
@@ -78,7 +78,7 @@ class TestRecentEnrollments(ModuleStoreTestCase):
|
||||
Tests that the recent enrollment list is empty if configured to zero seconds.
|
||||
"""
|
||||
self._configure_message_timeout(0)
|
||||
courses_list = list(get_course_enrollment_pairs(self.student, None, []))
|
||||
courses_list = list(get_course_enrollments(self.student, None, []))
|
||||
self.assertEqual(len(courses_list), 2)
|
||||
|
||||
recent_course_list = _get_recently_enrolled_courses(courses_list)
|
||||
@@ -106,16 +106,16 @@ class TestRecentEnrollments(ModuleStoreTestCase):
|
||||
enrollment.save()
|
||||
courses.append(course)
|
||||
|
||||
courses_list = list(get_course_enrollment_pairs(self.student, None, []))
|
||||
courses_list = list(get_course_enrollments(self.student, None, []))
|
||||
self.assertEqual(len(courses_list), 6)
|
||||
|
||||
recent_course_list = _get_recently_enrolled_courses(courses_list)
|
||||
self.assertEqual(len(recent_course_list), 5)
|
||||
|
||||
self.assertEqual(recent_course_list[1][0], courses[0])
|
||||
self.assertEqual(recent_course_list[2][0], courses[1])
|
||||
self.assertEqual(recent_course_list[3][0], courses[2])
|
||||
self.assertEqual(recent_course_list[4][0], courses[3])
|
||||
self.assertEqual(recent_course_list[1].course, courses[0])
|
||||
self.assertEqual(recent_course_list[2].course, courses[1])
|
||||
self.assertEqual(recent_course_list[3].course, courses[2])
|
||||
self.assertEqual(recent_course_list[4].course, courses[3])
|
||||
|
||||
def test_dashboard_rendering(self):
|
||||
"""
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This file demonstrates writing tests using the unittest module. These will pass
|
||||
when you run "manage.py test".
|
||||
|
||||
Replace this with more appropriate tests for your application.
|
||||
Miscellaneous tests for the student app.
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
@@ -28,8 +25,8 @@ from student.views import (process_survey_link, _cert_info,
|
||||
from student.tests.factories import UserFactory, CourseModeFactory
|
||||
from util.testing import EventTestMixin
|
||||
from util.model_utils import USER_SETTINGS_CHANGED_EVENT_NAME
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, ModuleStoreEnum
|
||||
|
||||
# These imports refer to lms djangoapps.
|
||||
# Their testcases are only run under lms.
|
||||
@@ -193,6 +190,7 @@ class CourseEndingTest(TestCase):
|
||||
self.assertIsNone(_cert_info(user, course2, cert_status, course_mode))
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class DashboardTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for dashboard utility functions
|
||||
@@ -487,6 +485,48 @@ class DashboardTest(ModuleStoreTestCase):
|
||||
)
|
||||
self.assertContains(response, expected_url)
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
@ddt.data((ModuleStoreEnum.Type.mongo, 1), (ModuleStoreEnum.Type.split, 3))
|
||||
@ddt.unpack
|
||||
def test_dashboard_metadata_caching(self, modulestore_type, expected_mongo_calls):
|
||||
"""
|
||||
Check that the student dashboard makes use of course metadata caching.
|
||||
|
||||
The first time the student dashboard displays a specific course, it will
|
||||
make a call to the module store. After that first request, though, the
|
||||
course's metadata should be cached as a CourseOverview.
|
||||
|
||||
Arguments:
|
||||
modulestore_type (ModuleStoreEnum.Type): Type of modulestore to create
|
||||
test course in.
|
||||
expected_mongo_calls (int >=0): Number of MongoDB queries expected for
|
||||
a single call to the module store.
|
||||
|
||||
Note to future developers:
|
||||
If you break this test so that the "check_mongo_calls(0)" fails,
|
||||
please do NOT change it to "check_mongo_calls(n>1)". Instead, change
|
||||
your code to not load courses from the module store. This may
|
||||
involve adding fields to CourseOverview so that loading a full
|
||||
CourseDescriptor isn't necessary.
|
||||
"""
|
||||
# Create a course, log in the user, and enroll them in the course.
|
||||
test_course = CourseFactory.create(default_store=modulestore_type)
|
||||
self.client.login(username="jack", password="test")
|
||||
CourseEnrollment.enroll(self.user, test_course.id)
|
||||
|
||||
# The first request will result in a modulestore query.
|
||||
with check_mongo_calls(expected_mongo_calls):
|
||||
response_1 = self.client.get(reverse('dashboard'))
|
||||
self.assertEquals(response_1.status_code, 200)
|
||||
|
||||
# Subsequent requests will only result in SQL queries to load the
|
||||
# CourseOverview object that has been created.
|
||||
with check_mongo_calls(0):
|
||||
response_2 = self.client.get(reverse('dashboard'))
|
||||
self.assertEquals(response_2.status_code, 200)
|
||||
response_3 = self.client.get(reverse('dashboard'))
|
||||
self.assertEquals(response_3.status_code, 200)
|
||||
|
||||
|
||||
class UserSettingsEventTestMixin(EventTestMixin):
|
||||
"""
|
||||
|
||||
@@ -4,7 +4,6 @@ Student Views
|
||||
import datetime
|
||||
import logging
|
||||
import uuid
|
||||
import time
|
||||
import json
|
||||
import warnings
|
||||
from datetime import timedelta
|
||||
@@ -115,7 +114,6 @@ from student.helpers import (
|
||||
)
|
||||
from student.cookies import set_logged_in_cookies, delete_logged_in_cookies
|
||||
from student.models import anonymous_id_for_user
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from shoppingcart.models import DonationConfiguration, CourseRegistrationCode
|
||||
|
||||
from embargo import api as embargo_api
|
||||
@@ -185,33 +183,42 @@ def process_survey_link(survey_link, user):
|
||||
return survey_link.format(UNIQUE_ID=unique_id_for_user(user))
|
||||
|
||||
|
||||
def cert_info(user, course, course_mode):
|
||||
def cert_info(user, course_overview, course_mode):
|
||||
"""
|
||||
Get the certificate info needed to render the dashboard section for the given
|
||||
student and course. Returns a dictionary with keys:
|
||||
student and course.
|
||||
|
||||
'status': one of 'generating', 'ready', 'notpassing', 'processing', 'restricted'
|
||||
'show_download_url': bool
|
||||
'download_url': url, only present if show_download_url is True
|
||||
'show_disabled_download_button': bool -- true if state is 'generating'
|
||||
'show_survey_button': bool
|
||||
'survey_url': url, only if show_survey_button is True
|
||||
'grade': if status is not 'processing'
|
||||
Arguments:
|
||||
user (User): A user.
|
||||
course_overview (CourseOverview): A course.
|
||||
course_mode (str): The enrollment mode (honor, verified, audit, etc.)
|
||||
|
||||
Returns:
|
||||
dict: A dictionary with keys:
|
||||
'status': one of 'generating', 'ready', 'notpassing', 'processing', 'restricted'
|
||||
'show_download_url': bool
|
||||
'download_url': url, only present if show_download_url is True
|
||||
'show_disabled_download_button': bool -- true if state is 'generating'
|
||||
'show_survey_button': bool
|
||||
'survey_url': url, only if show_survey_button is True
|
||||
'grade': if status is not 'processing'
|
||||
"""
|
||||
if not course.may_certify():
|
||||
if not course_overview.may_certify():
|
||||
return {}
|
||||
|
||||
return _cert_info(user, course, certificate_status_for_student(user, course.id), course_mode)
|
||||
return _cert_info(
|
||||
user,
|
||||
course_overview,
|
||||
certificate_status_for_student(user, course_overview.id),
|
||||
course_mode
|
||||
)
|
||||
|
||||
|
||||
def reverification_info(course_enrollment_pairs, user, statuses):
|
||||
def reverification_info(statuses):
|
||||
"""
|
||||
Returns reverification-related information for *all* of user's enrollments whose
|
||||
reverification status is in status_list
|
||||
reverification status is in statuses.
|
||||
|
||||
Args:
|
||||
course_enrollment_pairs (list): list of (course, enrollment) tuples
|
||||
user (User): the user whose information we want
|
||||
statuses (list): a list of reverification statuses we want information for
|
||||
example: ["must_reverify", "denied"]
|
||||
|
||||
@@ -229,39 +236,56 @@ def reverification_info(course_enrollment_pairs, user, statuses):
|
||||
return reverifications
|
||||
|
||||
|
||||
def get_course_enrollment_pairs(user, course_org_filter, org_filter_out_set):
|
||||
def get_course_enrollments(user, org_to_include, orgs_to_exclude):
|
||||
"""
|
||||
Get the relevant set of (Course, CourseEnrollment) pairs to be displayed on
|
||||
a student's dashboard.
|
||||
Given a user, return a filtered set of his or her course enrollments.
|
||||
|
||||
Arguments:
|
||||
user (User): the user in question.
|
||||
org_to_include (str): for use in Microsites. If not None, ONLY courses
|
||||
of this org will be returned.
|
||||
orgs_to_exclude (list[str]): If org_to_include is not None, this
|
||||
argument is ignored. Else, courses of this org will be excluded.
|
||||
|
||||
Returns:
|
||||
generator[CourseEnrollment]: a sequence of enrollments to be displayed
|
||||
on the user's dashboard.
|
||||
"""
|
||||
for enrollment in CourseEnrollment.enrollments_for_user(user):
|
||||
store = modulestore()
|
||||
with store.bulk_operations(enrollment.course_id):
|
||||
course = store.get_course(enrollment.course_id)
|
||||
if course and not isinstance(course, ErrorDescriptor):
|
||||
|
||||
# if we are in a Microsite, then filter out anything that is not
|
||||
# attributed (by ORG) to that Microsite
|
||||
if course_org_filter and course_org_filter != course.location.org:
|
||||
continue
|
||||
# Conversely, if we are not in a Microsite, then let's filter out any enrollments
|
||||
# with courses attributed (by ORG) to Microsites
|
||||
elif course.location.org in org_filter_out_set:
|
||||
continue
|
||||
# If the course is missing or broken, log an error and skip it.
|
||||
course_overview = enrollment.course_overview
|
||||
if not course_overview:
|
||||
log.error(
|
||||
"User %s enrolled in broken or non-existent course %s",
|
||||
user.username,
|
||||
enrollment.course_id
|
||||
)
|
||||
continue
|
||||
|
||||
yield (course, enrollment)
|
||||
else:
|
||||
log.error(
|
||||
u"User %s enrolled in %s course %s",
|
||||
user.username,
|
||||
"broken" if course else "non-existent",
|
||||
enrollment.course_id
|
||||
)
|
||||
# If we are in a Microsite, then filter out anything that is not
|
||||
# attributed (by ORG) to that Microsite.
|
||||
if org_to_include and course_overview.location.org != org_to_include:
|
||||
continue
|
||||
|
||||
# Conversely, if we are not in a Microsite, then filter out any enrollments
|
||||
# with courses attributed (by ORG) to Microsites.
|
||||
elif course_overview.location.org in orgs_to_exclude:
|
||||
continue
|
||||
|
||||
# Else, include the enrollment.
|
||||
else:
|
||||
yield enrollment
|
||||
|
||||
|
||||
def _cert_info(user, course, cert_status, course_mode):
|
||||
def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disable=unused-argument
|
||||
"""
|
||||
Implements the logic for cert_info -- split out for testing.
|
||||
|
||||
Arguments:
|
||||
user (User): A user.
|
||||
course_overview (CourseOverview): A course.
|
||||
course_mode (str): The enrollment mode (honor, verified, audit, etc.)
|
||||
"""
|
||||
# simplify the status for the template using this lookup table
|
||||
template_state = {
|
||||
@@ -285,7 +309,7 @@ def _cert_info(user, course, cert_status, course_mode):
|
||||
|
||||
is_hidden_status = cert_status['status'] in ('unavailable', 'processing', 'generating', 'notpassing')
|
||||
|
||||
if course.certificates_display_behavior == 'early_no_info' and is_hidden_status:
|
||||
if course_overview.certificates_display_behavior == 'early_no_info' and is_hidden_status:
|
||||
return None
|
||||
|
||||
status = template_state.get(cert_status['status'], default_status)
|
||||
@@ -299,20 +323,20 @@ def _cert_info(user, course, cert_status, course_mode):
|
||||
}
|
||||
|
||||
if (status in ('generating', 'ready', 'notpassing', 'restricted') and
|
||||
course.end_of_course_survey_url is not None):
|
||||
course_overview.end_of_course_survey_url is not None):
|
||||
status_dict.update({
|
||||
'show_survey_button': True,
|
||||
'survey_url': process_survey_link(course.end_of_course_survey_url, user)})
|
||||
'survey_url': process_survey_link(course_overview.end_of_course_survey_url, user)})
|
||||
else:
|
||||
status_dict['show_survey_button'] = False
|
||||
|
||||
if status == 'ready':
|
||||
# showing the certificate web view button if certificate is ready state and feature flags are enabled.
|
||||
if has_html_certificates_enabled(course.id, course):
|
||||
if get_active_web_certificate(course) is not None:
|
||||
if has_html_certificates_enabled(course_overview.id, course_overview):
|
||||
if course_overview.has_any_active_web_certificate:
|
||||
certificate_url = get_certificate_url(
|
||||
user_id=user.id,
|
||||
course_id=unicode(course.id)
|
||||
course_id=unicode(course_overview.id),
|
||||
)
|
||||
status_dict.update({
|
||||
'show_cert_web_view': True,
|
||||
@@ -325,7 +349,7 @@ def _cert_info(user, course, cert_status, course_mode):
|
||||
log.warning(
|
||||
u"User %s has a downloadable cert for %s, but no download url",
|
||||
user.username,
|
||||
course.id
|
||||
course_overview.id
|
||||
)
|
||||
return default_info
|
||||
else:
|
||||
@@ -337,8 +361,8 @@ def _cert_info(user, course, cert_status, course_mode):
|
||||
linkedin_config = LinkedInAddToProfileConfiguration.current()
|
||||
if linkedin_config.enabled:
|
||||
status_dict['linked_in_url'] = linkedin_config.add_to_profile_url(
|
||||
course.id,
|
||||
course.display_name,
|
||||
course_overview.id,
|
||||
course_overview.display_name,
|
||||
cert_status.get('mode'),
|
||||
cert_status['download_url']
|
||||
)
|
||||
@@ -506,13 +530,13 @@ def dashboard(request):
|
||||
# Build our (course, enrollment) list for the user, but ignore any courses that no
|
||||
# longer exist (because the course IDs have changed). Still, we don't delete those
|
||||
# enrollments, because it could have been a data push snafu.
|
||||
course_enrollment_pairs = list(get_course_enrollment_pairs(user, course_org_filter, org_filter_out_set))
|
||||
course_enrollments = list(get_course_enrollments(user, course_org_filter, org_filter_out_set))
|
||||
|
||||
# sort the enrollment pairs by the enrollment date
|
||||
course_enrollment_pairs.sort(key=lambda x: x[1].created, reverse=True)
|
||||
course_enrollments.sort(key=lambda x: x.created, reverse=True)
|
||||
|
||||
# Retrieve the course modes for each course
|
||||
enrolled_course_ids = [course.id for course, __ in course_enrollment_pairs]
|
||||
enrolled_course_ids = [enrollment.course_id for enrollment in course_enrollments]
|
||||
all_course_modes, unexpired_course_modes = CourseMode.all_and_unexpired_modes_for_courses(enrolled_course_ids)
|
||||
course_modes_by_course = {
|
||||
course_id: {
|
||||
@@ -525,14 +549,9 @@ def dashboard(request):
|
||||
# Check to see if the student has recently enrolled in a course.
|
||||
# If so, display a notification message confirming the enrollment.
|
||||
enrollment_message = _create_recent_enrollment_message(
|
||||
course_enrollment_pairs, course_modes_by_course
|
||||
course_enrollments, course_modes_by_course
|
||||
)
|
||||
|
||||
# Retrieve the course modes for each course
|
||||
enrolled_courses_dict = {}
|
||||
for course, __ in course_enrollment_pairs:
|
||||
enrolled_courses_dict[unicode(course.id)] = course
|
||||
|
||||
course_optouts = Optout.objects.filter(user=user).values_list('course_id', flat=True)
|
||||
|
||||
message = ""
|
||||
@@ -551,20 +570,20 @@ def dashboard(request):
|
||||
errored_courses = modulestore().get_errored_courses()
|
||||
|
||||
show_courseware_links_for = frozenset(
|
||||
course.id for course, _enrollment in course_enrollment_pairs
|
||||
if has_access(request.user, 'load', course)
|
||||
and has_access(request.user, 'view_courseware_with_prerequisites', course)
|
||||
enrollment.course_id for enrollment in course_enrollments
|
||||
if has_access(request.user, 'load', enrollment.course_overview)
|
||||
and has_access(request.user, 'view_courseware_with_prerequisites', enrollment.course_overview)
|
||||
)
|
||||
|
||||
# Construct a dictionary of course mode information
|
||||
# used to render the course list. We re-use the course modes dict
|
||||
# we loaded earlier to avoid hitting the database.
|
||||
course_mode_info = {
|
||||
course.id: complete_course_mode_info(
|
||||
course.id, enrollment,
|
||||
modes=course_modes_by_course[course.id]
|
||||
enrollment.course_id: complete_course_mode_info(
|
||||
enrollment.course_id, enrollment,
|
||||
modes=course_modes_by_course[enrollment.course_id]
|
||||
)
|
||||
for course, enrollment in course_enrollment_pairs
|
||||
for enrollment in course_enrollments
|
||||
}
|
||||
|
||||
# Determine the per-course verification status
|
||||
@@ -583,20 +602,20 @@ def dashboard(request):
|
||||
# there is no verification messaging to display.
|
||||
verify_status_by_course = check_verify_status_by_course(
|
||||
user,
|
||||
course_enrollment_pairs,
|
||||
course_enrollments,
|
||||
all_course_modes
|
||||
)
|
||||
cert_statuses = {
|
||||
course.id: cert_info(request.user, course, _enrollment.mode)
|
||||
for course, _enrollment in course_enrollment_pairs
|
||||
enrollment.course_id: cert_info(request.user, enrollment.course_overview, enrollment.mode)
|
||||
for enrollment in course_enrollments
|
||||
}
|
||||
|
||||
# only show email settings for Mongo course and when bulk email is turned on
|
||||
show_email_settings_for = frozenset(
|
||||
course.id for course, _enrollment in course_enrollment_pairs if (
|
||||
enrollment.course_id for enrollment in course_enrollments if (
|
||||
settings.FEATURES['ENABLE_INSTRUCTOR_EMAIL'] and
|
||||
modulestore().get_modulestore_type(course.id) != ModuleStoreEnum.Type.xml and
|
||||
CourseAuthorization.instructor_email_enabled(course.id)
|
||||
modulestore().get_modulestore_type(enrollment.course_id) != ModuleStoreEnum.Type.xml and
|
||||
CourseAuthorization.instructor_email_enabled(enrollment.course_id)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -606,16 +625,29 @@ def dashboard(request):
|
||||
|
||||
# Gets data for midcourse reverifications, if any are necessary or have failed
|
||||
statuses = ["approved", "denied", "pending", "must_reverify"]
|
||||
reverifications = reverification_info(course_enrollment_pairs, user, statuses)
|
||||
reverifications = reverification_info(statuses)
|
||||
|
||||
show_refund_option_for = frozenset(course.id for course, _enrollment in course_enrollment_pairs
|
||||
if _enrollment.refundable())
|
||||
show_refund_option_for = frozenset(
|
||||
enrollment.course_id for enrollment in course_enrollments
|
||||
if enrollment.refundable()
|
||||
)
|
||||
|
||||
block_courses = frozenset(course.id for course, enrollment in course_enrollment_pairs
|
||||
if is_course_blocked(request, CourseRegistrationCode.objects.filter(course_id=course.id, registrationcoderedemption__redeemed_by=request.user), course.id))
|
||||
block_courses = frozenset(
|
||||
enrollment.course_id for enrollment in course_enrollments
|
||||
if is_course_blocked(
|
||||
request,
|
||||
CourseRegistrationCode.objects.filter(
|
||||
course_id=enrollment.course_id,
|
||||
registrationcoderedemption__redeemed_by=request.user
|
||||
),
|
||||
enrollment.course_id
|
||||
)
|
||||
)
|
||||
|
||||
enrolled_courses_either_paid = frozenset(course.id for course, _enrollment in course_enrollment_pairs
|
||||
if _enrollment.is_paid_course())
|
||||
enrolled_courses_either_paid = frozenset(
|
||||
enrollment.course_id for enrollment in course_enrollments
|
||||
if enrollment.is_paid_course()
|
||||
)
|
||||
|
||||
# If there are *any* denied reverifications that have not been toggled off,
|
||||
# we'll display the banner
|
||||
@@ -625,8 +657,10 @@ def dashboard(request):
|
||||
order_history_list = order_history(user, course_org_filter=course_org_filter, org_filter_out_set=org_filter_out_set)
|
||||
|
||||
# get list of courses having pre-requisites yet to be completed
|
||||
courses_having_prerequisites = frozenset(course.id for course, _enrollment in course_enrollment_pairs
|
||||
if course.pre_requisite_courses)
|
||||
courses_having_prerequisites = frozenset(
|
||||
enrollment.course_id for enrollment in course_enrollments
|
||||
if enrollment.course_overview.pre_requisite_courses
|
||||
)
|
||||
courses_requirements_not_met = get_pre_requisite_courses_not_completed(user, courses_having_prerequisites)
|
||||
|
||||
ccx_membership_triplets = []
|
||||
@@ -638,7 +672,7 @@ def dashboard(request):
|
||||
|
||||
context = {
|
||||
'enrollment_message': enrollment_message,
|
||||
'course_enrollment_pairs': course_enrollment_pairs,
|
||||
'course_enrollments': course_enrollments,
|
||||
'course_optouts': course_optouts,
|
||||
'message': message,
|
||||
'staff_access': staff_access,
|
||||
@@ -646,7 +680,7 @@ def dashboard(request):
|
||||
'show_courseware_links_for': show_courseware_links_for,
|
||||
'all_course_modes': course_mode_info,
|
||||
'cert_statuses': cert_statuses,
|
||||
'credit_statuses': _credit_statuses(user, course_enrollment_pairs),
|
||||
'credit_statuses': _credit_statuses(user, course_enrollments),
|
||||
'show_email_settings_for': show_email_settings_for,
|
||||
'reverifications': reverifications,
|
||||
'verification_status': verification_status,
|
||||
@@ -669,13 +703,15 @@ def dashboard(request):
|
||||
return render_to_response('dashboard.html', context)
|
||||
|
||||
|
||||
def _create_recent_enrollment_message(course_enrollment_pairs, course_modes):
|
||||
"""Builds a recent course enrollment message
|
||||
def _create_recent_enrollment_message(course_enrollments, course_modes): # pylint: disable=invalid-name
|
||||
"""
|
||||
Builds a recent course enrollment message.
|
||||
|
||||
Constructs a new message template based on any recent course enrollments for the student.
|
||||
Constructs a new message template based on any recent course enrollments
|
||||
for the student.
|
||||
|
||||
Args:
|
||||
course_enrollment_pairs (list): A list of tuples containing courses, and the associated enrollment information.
|
||||
course_enrollments (list[CourseEnrollment]): a list of course enrollments.
|
||||
course_modes (dict): Mapping of course ID's to course mode dictionaries.
|
||||
|
||||
Returns:
|
||||
@@ -683,16 +719,16 @@ def _create_recent_enrollment_message(course_enrollment_pairs, course_modes):
|
||||
None if there are no recently enrolled courses.
|
||||
|
||||
"""
|
||||
recently_enrolled_courses = _get_recently_enrolled_courses(course_enrollment_pairs)
|
||||
recently_enrolled_courses = _get_recently_enrolled_courses(course_enrollments)
|
||||
|
||||
if recently_enrolled_courses:
|
||||
messages = [
|
||||
{
|
||||
"course_id": course.id,
|
||||
"course_name": course.display_name,
|
||||
"allow_donation": _allow_donation(course_modes, course.id, enrollment)
|
||||
"course_id": enrollment.course_overview.id,
|
||||
"course_name": enrollment.course_overview.display_name,
|
||||
"allow_donation": _allow_donation(course_modes, enrollment.course_overview.id, enrollment)
|
||||
}
|
||||
for course, enrollment in recently_enrolled_courses
|
||||
for enrollment in recently_enrolled_courses
|
||||
]
|
||||
|
||||
platform_name = microsite.get_value('platform_name', settings.PLATFORM_NAME)
|
||||
@@ -703,22 +739,20 @@ def _create_recent_enrollment_message(course_enrollment_pairs, course_modes):
|
||||
)
|
||||
|
||||
|
||||
def _get_recently_enrolled_courses(course_enrollment_pairs):
|
||||
"""Checks to see if the student has recently enrolled in courses.
|
||||
|
||||
Checks to see if any of the enrollments in the course_enrollment_pairs have been recently created and activated.
|
||||
def _get_recently_enrolled_courses(course_enrollments):
|
||||
"""
|
||||
Given a list of enrollments, filter out all but recent enrollments.
|
||||
|
||||
Args:
|
||||
course_enrollment_pairs (list): A list of tuples containing courses, and the associated enrollment information.
|
||||
course_enrollments (list[CourseEnrollment]): A list of course enrollments.
|
||||
|
||||
Returns:
|
||||
A list of courses
|
||||
|
||||
list[CourseEnrollment]: A list of recent course enrollments.
|
||||
"""
|
||||
seconds = DashboardConfiguration.current().recent_enrollment_time_delta
|
||||
time_delta = (datetime.datetime.now(UTC) - datetime.timedelta(seconds=seconds))
|
||||
return [
|
||||
(course, enrollment) for course, enrollment in course_enrollment_pairs
|
||||
enrollment for enrollment in course_enrollments
|
||||
# If the enrollment has no created date, we are explicitly excluding the course
|
||||
# from the list of recent enrollments.
|
||||
if enrollment.is_active and enrollment.created > time_delta
|
||||
@@ -752,7 +786,7 @@ def _update_email_opt_in(request, org):
|
||||
preferences_api.update_email_opt_in(request.user, org, email_opt_in_boolean)
|
||||
|
||||
|
||||
def _credit_statuses(user, course_enrollment_pairs):
|
||||
def _credit_statuses(user, course_enrollments):
|
||||
"""
|
||||
Retrieve the status for credit courses.
|
||||
|
||||
@@ -768,7 +802,8 @@ def _credit_statuses(user, course_enrollment_pairs):
|
||||
|
||||
Arguments:
|
||||
user (User): The currently logged-in user.
|
||||
course_enrollment_pairs (list): List of (Course, CourseEnrollment) tuples.
|
||||
course_enrollments (list[CourseEnrollment]): List of enrollments for the
|
||||
user.
|
||||
|
||||
Returns: dict
|
||||
|
||||
@@ -785,7 +820,7 @@ def _credit_statuses(user, course_enrollment_pairs):
|
||||
so the user should contact the support team.
|
||||
|
||||
Example:
|
||||
>>> _credit_statuses(user, course_enrollment_pairs)
|
||||
>>> _credit_statuses(user, course_enrollments)
|
||||
{
|
||||
CourseKey.from_string("edX/DemoX/Demo_Course"): {
|
||||
"course_key": "edX/DemoX/Demo_Course",
|
||||
@@ -812,8 +847,8 @@ def _credit_statuses(user, course_enrollment_pairs):
|
||||
}
|
||||
|
||||
credit_enrollments = {
|
||||
course.id: enrollment
|
||||
for course, enrollment in course_enrollment_pairs
|
||||
enrollment.course_id: enrollment
|
||||
for enrollment in course_enrollments
|
||||
if enrollment.mode == "credit"
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from django.core.urlresolvers import reverse
|
||||
from eventtracking import tracker
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
from certificates.models import (
|
||||
@@ -216,13 +217,18 @@ def generate_example_certificates(course_key):
|
||||
|
||||
def has_html_certificates_enabled(course_key, course=None):
|
||||
"""
|
||||
It determines if course has html certificates enabled
|
||||
Determine if a course has html certificates enabled.
|
||||
|
||||
Arguments:
|
||||
course_key (CourseKey|str): A course key or a string representation
|
||||
of one.
|
||||
course (CourseDescriptor|CourseOverview): A course.
|
||||
"""
|
||||
html_certificates_enabled = False
|
||||
try:
|
||||
if not isinstance(course_key, CourseKey):
|
||||
course_key = CourseKey.from_string(course_key)
|
||||
course = course if course else modulestore().get_course(course_key, depth=0)
|
||||
course = course if course else CourseOverview.get_from_id(course_key)
|
||||
if settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False) and course.cert_html_view_enabled:
|
||||
html_certificates_enabled = True
|
||||
except: # pylint: disable=bare-except
|
||||
|
||||
@@ -152,6 +152,16 @@ def find_file(filesystem, dirs, filename):
|
||||
raise ResourceNotFoundError(u"Could not find {0}".format(filename))
|
||||
|
||||
|
||||
def get_course_university_about_section(course): # pylint: disable=invalid-name
|
||||
"""
|
||||
Returns a snippet of HTML displaying the course's university.
|
||||
|
||||
Arguments:
|
||||
course (CourseDescriptor|CourseOverview): A course.
|
||||
"""
|
||||
return course.display_org_with_default
|
||||
|
||||
|
||||
def get_course_about_section(course, section_key):
|
||||
"""
|
||||
This returns the snippet of html to be rendered on the course about page,
|
||||
@@ -227,7 +237,7 @@ def get_course_about_section(course, section_key):
|
||||
elif section_key == "title":
|
||||
return course.display_name_with_default
|
||||
elif section_key == "university":
|
||||
return course.display_org_with_default
|
||||
return get_course_university_about_section(course)
|
||||
elif section_key == "number":
|
||||
return course.display_number_with_default
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<%page args="ccx, membership, course, show_courseware_link, is_course_blocked" />
|
||||
<%page args="ccx, membership, course_overview, show_courseware_link, is_course_blocked" />
|
||||
|
||||
<%!
|
||||
from django.utils.translation import ugettext as _
|
||||
@@ -7,7 +7,7 @@ from courseware.courses import course_image_url, get_course_about_section
|
||||
from ccx_keys.locator import CCXLocator
|
||||
%>
|
||||
<%
|
||||
ccx_target = reverse('info', args=[CCXLocator.from_course_locator(course.id, ccx.id)])
|
||||
ccx_target = reverse('info', args=[CCXLocator.from_course_locator(course_overview.id, ccx.id)])
|
||||
%>
|
||||
<li class="course-item">
|
||||
<article class="course">
|
||||
@@ -16,16 +16,16 @@ from ccx_keys.locator import CCXLocator
|
||||
% if show_courseware_link:
|
||||
% if not is_course_blocked:
|
||||
<a href="${ccx_target}" class="cover">
|
||||
<img src="${course_image_url(course)}" class="course-image" alt="${_('{course_number} {ccx_name} Cover Image').format(course_number=course.number, ccx_name=ccx.display_name) |h}" />
|
||||
<img src="${course_overview.course_image_url}" class="course-image" alt="${_('{course_number} {ccx_name} Cover Image').format(course_number=course_overview.number, ccx_name=ccx.display_name) |h}" />
|
||||
</a>
|
||||
% else:
|
||||
<a class="fade-cover">
|
||||
<img src="${course_image_url(course)}" class="course-image" alt="${_('{course_number} {ccx_name} Cover Image').format(course_number=course.number, ccx_name=ccx.display_name) |h}" />
|
||||
<img src="${course_overview.course_image_url}" class="course-image" alt="${_('{course_number} {ccx_name} Cover Image').format(course_number=course_overview.number, ccx_name=ccx.display_name) |h}" />
|
||||
</a>
|
||||
% endif
|
||||
% else:
|
||||
<a class="cover">
|
||||
<img src="${course_image_url(course)}" class="course-image" alt="${_('{course_number} {ccx_name} Cover Image').format(course_number=course.number, ccx_name=ccx.display_name) |h}" />
|
||||
<img src="${course_overview.course_image_url}" class="course-image" alt="${_('{course_number} {ccx_name} Cover Image').format(course_number=course_overview.number, ccx_name=ccx.display_name) |h}" />
|
||||
</a>
|
||||
% endif
|
||||
</div>
|
||||
@@ -43,7 +43,7 @@ from ccx_keys.locator import CCXLocator
|
||||
</h3>
|
||||
<div class="course-info">
|
||||
<span class="info-university">${get_course_about_section(course, 'university')} - </span>
|
||||
<span class="info-course-id">${course.display_number_with_default | h}</span>
|
||||
<span class="info-course-id">${course_overview.display_number_with_default | h}</span>
|
||||
<span class="info-date-block" data-tooltip="Hi">
|
||||
% if ccx.has_ended():
|
||||
${_("Ended - {end_date}").format(end_date=ccx.end_datetime_text("SHORT_DATE"))}
|
||||
|
||||
@@ -70,28 +70,28 @@ from django.core.urlresolvers import reverse
|
||||
</header>
|
||||
|
||||
|
||||
% if len(course_enrollment_pairs) > 0:
|
||||
% if len(course_enrollments) > 0:
|
||||
<ul class="listing-courses">
|
||||
<% share_settings = settings.FEATURES.get('SOCIAL_SHARING_SETTINGS', {}) %>
|
||||
% for dashboard_index, (course, enrollment) in enumerate(course_enrollment_pairs):
|
||||
<% show_courseware_link = (course.id in show_courseware_links_for) %>
|
||||
<% cert_status = cert_statuses.get(course.id) %>
|
||||
<% credit_status = credit_statuses.get(course.id) %>
|
||||
<% show_email_settings = (course.id in show_email_settings_for) %>
|
||||
<% course_mode_info = all_course_modes.get(course.id) %>
|
||||
<% show_refund_option = (course.id in show_refund_option_for) %>
|
||||
<% is_paid_course = (course.id in enrolled_courses_either_paid) %>
|
||||
<% is_course_blocked = (course.id in block_courses) %>
|
||||
<% course_verification_status = verification_status_by_course.get(course.id, {}) %>
|
||||
<% course_requirements = courses_requirements_not_met.get(course.id) %>
|
||||
<%include file='dashboard/_dashboard_course_listing.html' args="course=course, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option = show_refund_option, is_paid_course = is_paid_course, is_course_blocked = is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user" />
|
||||
% for dashboard_index, enrollment in enumerate(course_enrollments):
|
||||
<% show_courseware_link = (enrollment.course_id in show_courseware_links_for) %>
|
||||
<% cert_status = cert_statuses.get(enrollment.course_id) %>
|
||||
<% credit_status = credit_statuses.get(enrollment.course_id) %>
|
||||
<% show_email_settings = (enrollment.course_id in show_email_settings_for) %>
|
||||
<% course_mode_info = all_course_modes.get(enrollment.course_id) %>
|
||||
<% show_refund_option = (enrollment.course_id in show_refund_option_for) %>
|
||||
<% is_paid_course = (enrollment.course_id in enrolled_courses_either_paid) %>
|
||||
<% is_course_blocked = (enrollment.course_id in block_courses) %>
|
||||
<% course_verification_status = verification_status_by_course.get(enrollment.course_id, {}) %>
|
||||
<% course_requirements = courses_requirements_not_met.get(enrollment.course_id) %>
|
||||
<%include file='dashboard/_dashboard_course_listing.html' args="course_overview=enrollment.course_overview, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option=show_refund_option, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user" />
|
||||
% endfor
|
||||
|
||||
% if settings.FEATURES.get('CUSTOM_COURSES_EDX', False):
|
||||
% for ccx, membership, course in ccx_membership_triplets:
|
||||
<% show_courseware_link = ccx.has_started() %>
|
||||
<% is_course_blocked = False %>
|
||||
<%include file='ccx/_dashboard_ccx_listing.html' args="ccx=ccx, membership=membership, course=course, show_courseware_link=show_courseware_link, is_course_blocked=is_course_blocked" />
|
||||
<%include file='ccx/_dashboard_ccx_listing.html' args="ccx=ccx, membership=membership, course_overview=enrollment.course_overview, show_courseware_link=show_courseware_link, is_course_blocked=is_course_blocked" />
|
||||
% endfor
|
||||
% endif
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<%page args="cert_status, course, enrollment" />
|
||||
<%page args="cert_status, course_overview, enrollment" />
|
||||
|
||||
<%!
|
||||
from django.utils.translation import ugettext as _
|
||||
@@ -7,11 +7,11 @@ from course_modes.models import CourseMode
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<%
|
||||
cert_name_short = course.cert_name_short
|
||||
cert_name_short = course_overview.cert_name_short
|
||||
if cert_name_short == "":
|
||||
cert_name_short = settings.CERT_NAME_SHORT
|
||||
|
||||
cert_name_long = course.cert_name_long
|
||||
cert_name_long = course_overview.cert_name_long
|
||||
if cert_name_long == "":
|
||||
cert_name_long = settings.CERT_NAME_LONG
|
||||
%>
|
||||
@@ -35,7 +35,7 @@ else:
|
||||
<span class="grade-value">${"{0:.0f}%".format(float(cert_status['grade'])*100)}</span>.
|
||||
% if cert_status['status'] == 'notpassing' and enrollment.mode != 'audit':
|
||||
${_("Grade required for a {cert_name_short}:").format(cert_name_short=cert_name_short)} <span class="grade-value">
|
||||
${"{0:.0f}%".format(float(course.lowest_passing_grade)*100)}</span>.
|
||||
${"{0:.0f}%".format(float(course_overview.lowest_passing_grade)*100)}</span>.
|
||||
% elif cert_status['status'] == 'restricted' and enrollment.mode == 'verified':
|
||||
<p class="message-copy">
|
||||
${_("Your verified {cert_name_long} is being held pending confirmation that the issuance of your {cert_name_short} is in compliance with strict U.S. embargoes on Iran, Cuba, Syria and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {email}. If you would like a refund on your {cert_name_long}, please contact our billing address {billing_email}").format(email='<a class="contact-link" href="mailto:{email}">{email}</a>.'.format(email=settings.CONTACT_EMAIL), billing_email='<a class="contact-link" href="mailto:{email}">{email}</a>'.format(email=settings.PAYMENT_SUPPORT_EMAIL), cert_name_short=cert_name_short, cert_name_long=cert_name_long)}
|
||||
@@ -88,7 +88,7 @@ else:
|
||||
<li class="action action-share">
|
||||
<a class="action-linkedin-profile" target="_blank" href="${cert_status['linked_in_url']}"
|
||||
title="${_('Add Certificate to LinkedIn Profile')}"
|
||||
data-course-id="${unicode(course.id)}"
|
||||
data-course-id="${unicode(course_overview.id)}"
|
||||
data-certificate-mode="${cert_status['mode']}"
|
||||
>
|
||||
<img class="action-linkedin-profile-img"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<%page args="course, enrollment, show_courseware_link, cert_status, credit_status, show_email_settings, course_mode_info, show_refund_option, is_paid_course, is_course_blocked, verification_status, course_requirements, dashboard_index, share_settings" />
|
||||
<%page args="course_overview, enrollment, show_courseware_link, cert_status, credit_status, show_email_settings, course_mode_info, show_refund_option, is_paid_course, is_course_blocked, verification_status, course_requirements, dashboard_index, share_settings" />
|
||||
|
||||
<%!
|
||||
import urllib
|
||||
@@ -7,7 +7,7 @@ from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import ungettext
|
||||
from django.core.urlresolvers import reverse
|
||||
from markupsafe import escape
|
||||
from courseware.courses import course_image_url, get_course_about_section
|
||||
from courseware.courses import get_course_university_about_section
|
||||
from course_modes.models import CourseMode
|
||||
from student.helpers import (
|
||||
VERIFY_STATUS_NEED_TO_VERIFY,
|
||||
@@ -19,11 +19,11 @@ from student.helpers import (
|
||||
%>
|
||||
|
||||
<%
|
||||
cert_name_short = course.cert_name_short
|
||||
cert_name_short = course_overview.cert_name_short
|
||||
if cert_name_short == "":
|
||||
cert_name_short = settings.CERT_NAME_SHORT
|
||||
|
||||
cert_name_long = course.cert_name_long
|
||||
cert_name_long = course_overview.cert_name_long
|
||||
if cert_name_long == "":
|
||||
cert_name_long = settings.CERT_NAME_LONG
|
||||
billing_email = settings.PAYMENT_SUPPORT_EMAIL
|
||||
@@ -44,22 +44,22 @@ from student.helpers import (
|
||||
% endif
|
||||
|
||||
<article class="course${mode_class}">
|
||||
<% course_target = reverse('info', args=[unicode(course.id)]) %>
|
||||
<% course_target = reverse('info', args=[unicode(course_overview.id)]) %>
|
||||
<section class="details">
|
||||
<div class="wrapper-course-image" aria-hidden="true">
|
||||
% if show_courseware_link:
|
||||
% if not is_course_blocked:
|
||||
<a href="${course_target}" class="cover">
|
||||
<img src="${course_image_url(course)}" class="course-image" alt="${_('{course_number} {course_name} Home Page').format(course_number=course.number, course_name=course.display_name_with_default) |h}" />
|
||||
<img src="${course_overview.course_image_url}" class="course-image" alt="${_('{course_number} {course_name} Home Page').format(course_number=course_overview.number, course_name=course_overview.display_name_with_default) |h}" />
|
||||
</a>
|
||||
% else:
|
||||
<a class="fade-cover">
|
||||
<img src="${course_image_url(course)}" class="course-image" alt="${_('{course_number} {course_name} Cover Image').format(course_number=course.number, course_name=course.display_name_with_default) |h}" />
|
||||
<img src="${course_overview.course_image_url}" class="course-image" alt="${_('{course_number} {course_name} Cover Image').format(course_number=course_overview.number, course_name=course_overview.display_name_with_default) |h}" />
|
||||
</a>
|
||||
% endif
|
||||
% else:
|
||||
<a class="cover">
|
||||
<img src="${course_image_url(course)}" class="course-image" alt="${_('{course_number} {course_name} Cover Image').format(course_number=course.number, course_name=course.display_name_with_default) | h}" />
|
||||
<img src="${course_overview.course_image_url}" class="course-image" alt="${_('{course_number} {course_name} Cover Image').format(course_number=course_overview.number, course_name=course_overview.display_name_with_default) | h}" />
|
||||
</a>
|
||||
% endif
|
||||
% if settings.FEATURES.get('ENABLE_VERIFIED_CERTIFICATES'):
|
||||
@@ -76,55 +76,55 @@ from student.helpers import (
|
||||
<h3 class="course-title">
|
||||
% if show_courseware_link:
|
||||
% if not is_course_blocked:
|
||||
<a href="${course_target}">${course.display_name_with_default}</a>
|
||||
<a href="${course_target}">${course_overview.display_name_with_default}</a>
|
||||
% else:
|
||||
<a class="disable-look">${course.display_name_with_default}</a>
|
||||
<a class="disable-look">${course_overview.display_name_with_default}</a>
|
||||
% endif
|
||||
% else:
|
||||
<span>${course.display_name_with_default}</span>
|
||||
<span>${course_overview.display_name_with_default}</span>
|
||||
% endif
|
||||
</h3>
|
||||
<div class="course-info">
|
||||
<span class="info-university">${get_course_about_section(course, 'university')} - </span>
|
||||
<span class="info-course-id">${course.display_number_with_default | h}</span>
|
||||
<span class="info-university">${get_course_university_about_section(course_overview)} - </span>
|
||||
<span class="info-course-id">${course_overview.display_number_with_default | h}</span>
|
||||
<span class="info-date-block" data-tooltip="Hi">
|
||||
% if course.has_ended():
|
||||
${_("Ended - {end_date}").format(end_date=course.end_datetime_text("SHORT_DATE"))}
|
||||
% elif course.has_started():
|
||||
${_("Started - {start_date}").format(start_date=course.start_datetime_text("SHORT_DATE"))}
|
||||
% elif course.start_date_is_still_default: # Course start date TBD
|
||||
% if course_overview.has_ended():
|
||||
${_("Ended - {end_date}").format(end_date=course_overview.end_datetime_text("SHORT_DATE"))}
|
||||
% elif course_overview.has_started():
|
||||
${_("Started - {start_date}").format(start_date=course_overview.start_datetime_text("SHORT_DATE"))}
|
||||
% elif course_overview.start_date_is_still_default: # Course start date TBD
|
||||
${_("Coming Soon")}
|
||||
% else: # hasn't started yet
|
||||
${_("Starts - {start_date}").format(start_date=course.start_datetime_text("SHORT_DATE"))}
|
||||
${_("Starts - {start_date}").format(start_date=course_overview.start_datetime_text("SHORT_DATE"))}
|
||||
% endif
|
||||
</span>
|
||||
</div>
|
||||
<div class="wrapper-course-actions">
|
||||
<div class="course-actions">
|
||||
% if show_courseware_link:
|
||||
% if course.has_ended():
|
||||
% if course_overview.has_ended():
|
||||
% if not is_course_blocked:
|
||||
<a href="${course_target}" class="enter-course archived">${_('View Archived Course')}<span class="sr"> ${course.display_name_with_default}</span></a>
|
||||
<a href="${course_target}" class="enter-course archived">${_('View Archived Course')}<span class="sr"> ${course_overview.display_name_with_default}</span></a>
|
||||
% else:
|
||||
<a class="enter-course-blocked archived">${_('View Archived Course')}<span class="sr"> ${course.display_name_with_default}</span></a>
|
||||
<a class="enter-course-blocked archived">${_('View Archived Course')}<span class="sr"> ${course_overview.display_name_with_default}</span></a>
|
||||
% endif
|
||||
% else:
|
||||
% if not is_course_blocked:
|
||||
<a href="${course_target}" class="enter-course">${_('View Course')}<span class="sr"> ${course.display_name_with_default}</span></a>
|
||||
<a href="${course_target}" class="enter-course">${_('View Course')}<span class="sr"> ${course_overview.display_name_with_default}</span></a>
|
||||
% else:
|
||||
<a class="enter-course-blocked">${_('View Course')}<span class="sr"> ${course.display_name_with_default}</span></a>
|
||||
<a class="enter-course-blocked">${_('View Course')}<span class="sr"> ${course_overview.display_name_with_default}</span></a>
|
||||
% endif
|
||||
% endif
|
||||
|
||||
% if share_settings:
|
||||
<%
|
||||
if share_settings.get("CUSTOM_COURSE_URLS", False):
|
||||
if course.social_sharing_url:
|
||||
share_url = urllib.quote_plus(course.social_sharing_url)
|
||||
if course_overview.social_sharing_url:
|
||||
share_url = urllib.quote_plus(course_overview.social_sharing_url)
|
||||
else:
|
||||
share_url = ''
|
||||
else:
|
||||
share_url = urllib.quote_plus(request.build_absolute_uri(reverse('about_course', args=[unicode(course.id)])))
|
||||
share_url = urllib.quote_plus(request.build_absolute_uri(reverse('about_course', args=[unicode(course_overview.id)])))
|
||||
share_window_name = 'shareWindow'
|
||||
share_window_config = 'toolbar=no, location=no, status=no, menubar=no, scrollbars=yes, resizable=yes, width=640, height=480'
|
||||
%>
|
||||
@@ -163,7 +163,7 @@ from student.helpers import (
|
||||
% endif
|
||||
% endif
|
||||
<div class="wrapper-action-more">
|
||||
<a href="#actions-dropdown-${dashboard_index}" class="action action-more" id="actions-dropdown-link-${dashboard_index}" aria-haspopup="true" aria-expanded="false" data-course-number="${course.number | h}" data-dashboard-index="${dashboard_index}">
|
||||
<a href="#actions-dropdown-${dashboard_index}" class="action action-more" id="actions-dropdown-link-${dashboard_index}" aria-haspopup="true" aria-expanded="false" data-course-number="${course_overview.number | h}" data-dashboard-index="${dashboard_index}">
|
||||
<span class="sr">${_('Course options dropdown')}</span>
|
||||
<i class="fa fa-cog" aria-hidden="true"></i>
|
||||
</a>
|
||||
@@ -173,12 +173,12 @@ from student.helpers import (
|
||||
% if is_paid_course and show_refund_option:
|
||||
## Translators: The course name will be added to the end of this sentence.
|
||||
% if not is_course_blocked:
|
||||
<a href="#unenroll-modal" class="action action-unenroll" rel="leanModal" data-course-id="${course.id | h}" data-course-number="${course.number | h}" data-dashboard-index="${dashboard_index}"
|
||||
<a href="#unenroll-modal" class="action action-unenroll" rel="leanModal" data-course-id="${course_overview.id | h}" data-course_overview-number="${course_overview.number | h}" data-dashboard-index="${dashboard_index}"
|
||||
onclick="set_unenroll_message('${_("Are you sure you want to unenroll from the purchased course %(course_number)s?")}', '${_("You will be refunded the amount you paid.")}')">
|
||||
${_('Unenroll')}
|
||||
</a>
|
||||
% else:
|
||||
<a class="action action-unenroll is-disabled" data-course-id="${course.id | h}" data-course-number="${course.number | h}" data-dashboard-index="${dashboard_index}"
|
||||
<a class="action action-unenroll is-disabled" data-course-id="${course_overview.id | h}" data-course-number="${course_overview.number | h}" data-dashboard-index="${dashboard_index}"
|
||||
onclick="set_unenroll_message('${_("Are you sure you want to unenroll from the purchased course %(course_number)s?")}', '${_("You will be refunded the amount you paid.")}')">
|
||||
${_('Unenroll')}
|
||||
</a>
|
||||
@@ -186,12 +186,12 @@ from student.helpers import (
|
||||
% elif is_paid_course and not show_refund_option:
|
||||
## Translators: The course's name will be added to the end of this sentence.
|
||||
% if not is_course_blocked:
|
||||
<a href="#unenroll-modal" class="action action-unenroll" rel="leanModal" data-course-id="${course.id | h}" data-course-number="${course.number | h}" data-dashboard-index="${dashboard_index}"
|
||||
<a href="#unenroll-modal" class="action action-unenroll" rel="leanModal" data-course-id="${course_overview.id | h}" data-course-number="${course_overview.number | h}" data-dashboard-index="${dashboard_index}"
|
||||
onclick="set_unenroll_message('${_("Are you sure you want to unenroll from the purchased course %(course_number)s?")}', '${_("You will not be refunded the amount you paid.")}')">
|
||||
${_('Unenroll')}
|
||||
</a>
|
||||
% else:
|
||||
<a class="action action-unenroll is-disabled" data-course-id="${course.id | h}" data-course-number="${course.number | h}" data-dashboard-index="${dashboard_index}"
|
||||
<a class="action action-unenroll is-disabled" data-course-id="${course_overview.id | h}" data-course-number="${course_overview.number | h}" data-dashboard-index="${dashboard_index}"
|
||||
onclick="set_unenroll_message('${_("Are you sure you want to unenroll from the purchased course %(course_number)s?")}', '${_("You will not be refunded the amount you paid.")}')">
|
||||
${_('Unenroll')}
|
||||
</a>
|
||||
@@ -199,12 +199,12 @@ from student.helpers import (
|
||||
% elif enrollment.mode != "verified":
|
||||
## Translators: The course's name will be added to the end of this sentence.
|
||||
% if not is_course_blocked:
|
||||
<a href="#unenroll-modal" class="action action-unenroll" rel="leanModal" data-course-id="${course.id | h}" data-course-number="${course.number | h}" data-dashboard-index="${dashboard_index}"
|
||||
<a href="#unenroll-modal" class="action action-unenroll" rel="leanModal" data-course-id="${course_overview.id | h}" data-course-number="${course_overview.number | h}" data-dashboard-index="${dashboard_index}"
|
||||
onclick="set_unenroll_message('${_("Are you sure you want to unenroll from %(course_number)s?")}', '')">
|
||||
${_('Unenroll')}
|
||||
</a>
|
||||
% else:
|
||||
<a class="action action-unenroll is-disabled" data-course-id="${course.id | h}" data-course-number="${course.number | h}" data-dashboard-index="${dashboard_index}"
|
||||
<a class="action action-unenroll is-disabled" data-course-id="${course_overview.id | h}" data-course-number="${course_overview.number | h}" data-dashboard-index="${dashboard_index}"
|
||||
onclick="set_unenroll_message('${_("Are you sure you want to unenroll from %(course_number)s?")}', '')">
|
||||
${_('Unenroll')}
|
||||
</a>
|
||||
@@ -212,7 +212,7 @@ from student.helpers import (
|
||||
% elif show_refund_option:
|
||||
## Translators: The course's name will be added to the end of this sentence.
|
||||
% if not is_course_blocked:
|
||||
<a href="#unenroll-modal" class="action action-unenroll" rel="leanModal" data-course-id="${course.id | h}" data-course-number="${course.number | h}" data-dashboard-index="${dashboard_index}"
|
||||
<a href="#unenroll-modal" class="action action-unenroll" rel="leanModal" data-course-id="${course_overview.id | h}" data-course-number="${course_overview.number | h}" data-dashboard-index="${dashboard_index}"
|
||||
onclick="set_unenroll_message(
|
||||
'${_("Are you sure you want to unenroll from the verified {cert_name_long} track of %(course_number)s?").format(cert_name_long=cert_name_long)}',
|
||||
'${_("You will be refunded the amount you paid.")}'
|
||||
@@ -221,7 +221,7 @@ from student.helpers import (
|
||||
${_('Unenroll')}
|
||||
</a>
|
||||
% else:
|
||||
<a class="action action-unenroll is-disabled" data-course-id="${course.id | h}" data-course-number="${course.number | h}" data-dashboard-index="${dashboard_index}"
|
||||
<a class="action action-unenroll is-disabled" data-course-id="${course_overview.id | h}" data-course-number="${course_overview.number | h}" data-dashboard-index="${dashboard_index}"
|
||||
onclick="set_unenroll_message(
|
||||
'${_("Are you sure you want to unenroll from the verified {cert_name_long} track of %(course_number)s?").format(cert_name_long=cert_name_long)}',
|
||||
'${_("You will be refunded the amount you paid.")}'
|
||||
@@ -233,7 +233,7 @@ from student.helpers import (
|
||||
% else:
|
||||
## Translators: The course's name will be added to the end of this sentence.
|
||||
% if not is_course_blocked:
|
||||
<a href="#unenroll-modal" class="action action-unenroll" rel="leanModal" data-course-id="${course.id | h}" data-course-number="${course.number | h}" data-dashboard-index="${dashboard_index}"
|
||||
<a href="#unenroll-modal" class="action action-unenroll" rel="leanModal" data-course-id="${course_overview.id | h}" data-course-number="${course_overview.number | h}" data-dashboard-index="${dashboard_index}"
|
||||
onclick="set_unenroll_message(
|
||||
'${_("Are you sure you want to unenroll from the verified {cert_name_long} track of %(course_number)s?").format(cert_name_long=cert_name_long)}',
|
||||
'${_("The refund deadline for this course has passed, so you will not receive a refund.")}'
|
||||
@@ -242,7 +242,7 @@ from student.helpers import (
|
||||
${_('Unenroll')}
|
||||
</a>
|
||||
% else:
|
||||
<a class="action action-unenroll is-disabled" data-course-id="${course.id | h}" data-course-number="${course.number | h}" data-dashboard-index="${dashboard_index}"
|
||||
<a class="action action-unenroll is-disabled" data-course-id="${course_overview.id | h}" data-course-number="${course_overview.number | h}" data-dashboard-index="${dashboard_index}"
|
||||
onclick="set_unenroll_message(
|
||||
'${_("Are you sure you want to unenroll from the verified {cert_name_long} track of %(course_number)s?").format(cert_name_long=cert_name_long)}',
|
||||
'${_("The refund deadline for this course has passed, so you will not receive a refund.")}'
|
||||
@@ -256,9 +256,9 @@ from student.helpers import (
|
||||
<li class="actions-item" id="actions-item-email-settings-${dashboard_index}">
|
||||
% if show_email_settings:
|
||||
% if not is_course_blocked:
|
||||
<a href="#email-settings-modal" class="action action-email-settings" rel="leanModal" data-course-id="${course.id | h}" data-course-number="${course.number | h}" data-dashboard-index="${dashboard_index}" data-optout="${unicode(course.id) in course_optouts}">${_('Email Settings')}</a>
|
||||
<a href="#email-settings-modal" class="action action-email-settings" rel="leanModal" data-course-id="${course_overview.id | h}" data-course-number="${course_overview.number | h}" data-dashboard-index="${dashboard_index}" data-optout="${unicode(course_overview.id) in course_optouts}">${_('Email Settings')}</a>
|
||||
% else:
|
||||
<a class="action action-email-settings is-disabled" data-course-id="${course.id| h}" data-course-number="${course.number | h}" data-dashboard-index="${dashboard_index}" data-optout="${unicode(course.id) in course_optouts}">${_('Email Settings')}</a>
|
||||
<a class="action action-email-settings is-disabled" data-course-id="${course_overview.id| h}" data-course-number="${course_overview.number | h}" data-dashboard-index="${dashboard_index}" data-optout="${unicode(course_overview.id) in course_optouts}">${_('Email Settings')}</a>
|
||||
% endif
|
||||
% endif
|
||||
</li>
|
||||
@@ -271,8 +271,8 @@ from student.helpers import (
|
||||
</section>
|
||||
<footer class="wrapper-messages-primary">
|
||||
<ul class="messages-list">
|
||||
% if course.may_certify() and cert_status:
|
||||
<%include file='_dashboard_certificate_information.html' args='cert_status=cert_status,course=course, enrollment=enrollment'/>
|
||||
% if course_overview.may_certify() and cert_status:
|
||||
<%include file='_dashboard_certificate_information.html' args='cert_status=cert_status,course_overview=course_overview, enrollment=enrollment'/>
|
||||
% endif
|
||||
|
||||
% if credit_status is not None:
|
||||
@@ -296,7 +296,7 @@ from student.helpers import (
|
||||
% endif
|
||||
</div>
|
||||
<div class="verification-cta">
|
||||
<a href="${reverse('verify_student_verify_now', kwargs={'course_id': unicode(course.id)})}" class="cta" data-course-id="${course.id | h}">${_('Verify Now')}</a>
|
||||
<a href="${reverse('verify_student_verify_now', kwargs={'course_id': unicode(course_overview.id)})}" class="cta" data-course-id="${course_overview.id | h}">${_('Verify Now')}</a>
|
||||
</div>
|
||||
% elif verification_status['status'] == VERIFY_STATUS_SUBMITTED:
|
||||
<h4 class="message-title">${_('You have already verified your ID!')}</h4>
|
||||
@@ -329,7 +329,7 @@ from student.helpers import (
|
||||
|
||||
<ul class="actions message-actions">
|
||||
<li class="action-item">
|
||||
<a class="action action-upgrade" href="${reverse('verify_student_upgrade_and_verify', kwargs={'course_id': unicode(course.id)})}" data-course-id="${course.id | h}" data-user="${user.username | h}">
|
||||
<a class="action action-upgrade" href="${reverse('verify_student_upgrade_and_verify', kwargs={'course_id': unicode(course_overview.id)})}" data-course-id="${course_overview.id | h}" data-user="${user.username | h}">
|
||||
<img class="deco-graphic" src="${static.url('images/vcert-ribbon-s.png')}" alt="${_("ID Verified Ribbon/Badge")}">
|
||||
<span class="wrapper-copy">
|
||||
<span class="copy" id="upgrade-to-verified">${_("Upgrade to Verified Track")}</span>
|
||||
@@ -354,8 +354,8 @@ from student.helpers import (
|
||||
'<a id="unregister_block_course" rel="leanModal" '
|
||||
'data-course-id="{course_id}" data-course-number="{course_number}" '
|
||||
'href="#unenroll-modal">'.format(
|
||||
course_id=escape(course.id),
|
||||
course_number=escape(course.number),
|
||||
course_id=escape(course_overview.id),
|
||||
course_number=escape(course_overview.number),
|
||||
)
|
||||
),
|
||||
unenroll_link_end="</a>",
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from south.utils import datetime_utils as datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding field 'CourseOverview.cert_html_view_enabled'
|
||||
|
||||
# The default value for the cert_html_view_eanbled column is False.
|
||||
# However, for courses in the table for which cert_html_view_enabled
|
||||
# should be True, this would be invalid. So, we must clear the
|
||||
# table before adding the new column.
|
||||
|
||||
db.clear_table('course_overviews_courseoverview')
|
||||
db.add_column('course_overviews_courseoverview', 'cert_html_view_enabled',
|
||||
self.gf('django.db.models.fields.BooleanField')(default=False),
|
||||
keep_default=False)
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting field 'CourseOverview.cert_html_view_enabled'
|
||||
db.delete_column('course_overviews_courseoverview', 'cert_html_view_enabled')
|
||||
|
||||
|
||||
models = {
|
||||
'course_overviews.courseoverview': {
|
||||
'Meta': {'object_name': 'CourseOverview'},
|
||||
'_location': ('xmodule_django.models.UsageKeyField', [], {'max_length': '255'}),
|
||||
'_pre_requisite_courses_json': ('django.db.models.fields.TextField', [], {}),
|
||||
'advertised_start': ('django.db.models.fields.TextField', [], {'null': 'True'}),
|
||||
'cert_html_view_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'cert_name_long': ('django.db.models.fields.TextField', [], {}),
|
||||
'cert_name_short': ('django.db.models.fields.TextField', [], {}),
|
||||
'certificates_display_behavior': ('django.db.models.fields.TextField', [], {'null': 'True'}),
|
||||
'certificates_show_before_end': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'course_image_url': ('django.db.models.fields.TextField', [], {}),
|
||||
'days_early_for_beta': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
|
||||
'display_name': ('django.db.models.fields.TextField', [], {'null': 'True'}),
|
||||
'display_number_with_default': ('django.db.models.fields.TextField', [], {}),
|
||||
'display_org_with_default': ('django.db.models.fields.TextField', [], {}),
|
||||
'end': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
|
||||
'end_of_course_survey_url': ('django.db.models.fields.TextField', [], {'null': 'True'}),
|
||||
'facebook_url': ('django.db.models.fields.TextField', [], {'null': 'True'}),
|
||||
'has_any_active_web_certificate': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'primary_key': 'True', 'db_index': 'True'}),
|
||||
'lowest_passing_grade': ('django.db.models.fields.DecimalField', [], {'max_digits': '5', 'decimal_places': '2'}),
|
||||
'mobile_available': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'social_sharing_url': ('django.db.models.fields.TextField', [], {'null': 'True'}),
|
||||
'start': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
|
||||
'visible_to_staff_only': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['course_overviews']
|
||||
@@ -10,6 +10,8 @@ from django.utils.translation import ugettext
|
||||
|
||||
from util.date_utils import strftime_localized
|
||||
from xmodule import course_metadata_utils
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule_django.models import CourseKeyField, UsageKeyField
|
||||
|
||||
@@ -44,6 +46,7 @@ class CourseOverview(django.db.models.Model):
|
||||
# Certification data
|
||||
certificates_display_behavior = TextField(null=True)
|
||||
certificates_show_before_end = BooleanField()
|
||||
cert_html_view_enabled = BooleanField()
|
||||
has_any_active_web_certificate = BooleanField()
|
||||
cert_name_short = TextField()
|
||||
cert_name_long = TextField()
|
||||
@@ -91,6 +94,7 @@ class CourseOverview(django.db.models.Model):
|
||||
|
||||
certificates_display_behavior=course.certificates_display_behavior,
|
||||
certificates_show_before_end=course.certificates_show_before_end,
|
||||
cert_html_view_enabled=course.cert_html_view_enabled,
|
||||
has_any_active_web_certificate=(get_active_web_certificate(course) is not None),
|
||||
cert_name_short=course.cert_name_short,
|
||||
cert_name_long=course.cert_name_long,
|
||||
@@ -114,10 +118,17 @@ class CourseOverview(django.db.models.Model):
|
||||
future use.
|
||||
|
||||
Arguments:
|
||||
course_id (CourseKey): the ID of the course overview to be loaded
|
||||
course_id (CourseKey): the ID of the course overview to be loaded.
|
||||
|
||||
Returns:
|
||||
CourseOverview: overview of the requested course
|
||||
CourseOverview: overview of the requested course. If loading course
|
||||
from the module store failed, returns None.
|
||||
|
||||
Raises:
|
||||
- CourseOverview.DoesNotExist if the course specified by course_id
|
||||
was not found.
|
||||
- IOError if some other error occurs while trying to load the
|
||||
course from the module store.
|
||||
"""
|
||||
course_overview = None
|
||||
try:
|
||||
@@ -126,9 +137,17 @@ class CourseOverview(django.db.models.Model):
|
||||
store = modulestore()
|
||||
with store.bulk_operations(course_id):
|
||||
course = store.get_course(course_id)
|
||||
if course:
|
||||
if isinstance(course, CourseDescriptor):
|
||||
course_overview = CourseOverview._create_from_course(course)
|
||||
course_overview.save() # Save new overview to the cache
|
||||
course_overview.save()
|
||||
elif course is not None:
|
||||
raise IOError(
|
||||
"Error while loading course {} from the module store: {}",
|
||||
unicode(course_id),
|
||||
course.error_msg if isinstance(course, ErrorDescriptor) else unicode(course)
|
||||
)
|
||||
else:
|
||||
raise CourseOverview.DoesNotExist()
|
||||
return course_overview
|
||||
|
||||
def clean_id(self, padding_char='='):
|
||||
|
||||
@@ -6,13 +6,16 @@ import ddt
|
||||
import itertools
|
||||
import pytz
|
||||
import math
|
||||
import mock
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from lms.djangoapps.certificates.api import get_active_web_certificate
|
||||
from lms.djangoapps.courseware.courses import course_image_url
|
||||
from xmodule.course_metadata_utils import DEFAULT_START_DATE
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls, check_mongo_calls_range
|
||||
|
||||
@@ -41,12 +44,18 @@ class CourseOverviewTestCase(ModuleStoreTestCase):
|
||||
- the CourseDescriptor itself
|
||||
- a CourseOverview that was newly constructed from _create_from_course
|
||||
- a CourseOverview that was loaded from the MySQL database
|
||||
|
||||
Arguments:
|
||||
course (CourseDescriptor): the course to be checked.
|
||||
"""
|
||||
|
||||
def get_seconds_since_epoch(date_time):
|
||||
"""
|
||||
Returns the number of seconds between the Unix Epoch and the given
|
||||
datetime. If the given datetime is None, return None.
|
||||
|
||||
Arguments:
|
||||
date_time (datetime): the datetime in question.
|
||||
"""
|
||||
if date_time is None:
|
||||
return None
|
||||
@@ -189,18 +198,14 @@ class CourseOverviewTestCase(ModuleStoreTestCase):
|
||||
by comparing pairs of them given a variety of scenarios.
|
||||
|
||||
Arguments:
|
||||
course_kwargs (dict): kwargs to be passed to course constructor
|
||||
modulestore_type (ModuleStoreEnum.Type)
|
||||
is_user_enrolled (bool)
|
||||
course_kwargs (dict): kwargs to be passed to course constructor.
|
||||
modulestore_type (ModuleStoreEnum.Type): type of store to create the
|
||||
course in.
|
||||
"""
|
||||
|
||||
course = CourseFactory.create(
|
||||
course="TEST101",
|
||||
org="edX",
|
||||
run="Run1",
|
||||
default_store=modulestore_type,
|
||||
**course_kwargs
|
||||
)
|
||||
# Note: We specify a value for 'run' here because, for some reason,
|
||||
# .create raises an InvalidKeyError if we don't (even though my
|
||||
# other test functions don't specify a run but work fine).
|
||||
course = CourseFactory.create(default_store=modulestore_type, run="TestRun", **course_kwargs)
|
||||
self.check_course_overview_against_course(course)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@@ -208,17 +213,15 @@ class CourseOverviewTestCase(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests that when a course is published, the corresponding
|
||||
course_overview is removed from the cache.
|
||||
|
||||
Arguments:
|
||||
modulestore_type (ModuleStoreEnum.Type): type of store to create the
|
||||
course in.
|
||||
"""
|
||||
with self.store.default_store(modulestore_type):
|
||||
|
||||
# Create a course where mobile_available is True.
|
||||
course = CourseFactory.create(
|
||||
course="TEST101",
|
||||
org="edX",
|
||||
run="Run1",
|
||||
mobile_available=True,
|
||||
default_store=modulestore_type
|
||||
)
|
||||
course = CourseFactory.create(mobile_available=True, default_store=modulestore_type)
|
||||
course_overview_1 = CourseOverview.get_from_id(course.id)
|
||||
self.assertTrue(course_overview_1.mobile_available)
|
||||
|
||||
@@ -238,14 +241,16 @@ class CourseOverviewTestCase(ModuleStoreTestCase):
|
||||
def test_course_overview_caching(self, modulestore_type, min_mongo_calls, max_mongo_calls):
|
||||
"""
|
||||
Tests that CourseOverview structures are actually getting cached.
|
||||
|
||||
Arguments:
|
||||
modulestore_type (ModuleStoreEnum.Type): type of store to create the
|
||||
course in.
|
||||
min_mongo_calls (int): minimum number of MongoDB queries we expect
|
||||
to be made.
|
||||
max_mongo_calls (int): maximum number of MongoDB queries we expect
|
||||
to be made.
|
||||
"""
|
||||
course = CourseFactory.create(
|
||||
course="TEST101",
|
||||
org="edX",
|
||||
run="Run1",
|
||||
mobile_available=True,
|
||||
default_store=modulestore_type
|
||||
)
|
||||
course = CourseFactory.create(default_store=modulestore_type)
|
||||
|
||||
# The first time we load a CourseOverview, it will be a cache miss, so
|
||||
# we expect the modulestore to be queried.
|
||||
@@ -256,3 +261,36 @@ class CourseOverviewTestCase(ModuleStoreTestCase):
|
||||
# we expect no modulestore queries to be made.
|
||||
with check_mongo_calls(0):
|
||||
_course_overview_2 = CourseOverview.get_from_id(course.id)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo)
|
||||
def test_get_non_existent_course(self, modulestore_type):
|
||||
"""
|
||||
Tests that requesting a non-existent course from get_from_id raises
|
||||
CourseOverview.DoesNotExist.
|
||||
|
||||
Arguments:
|
||||
modulestore_type (ModuleStoreEnum.Type): type of store to create the
|
||||
course in.
|
||||
"""
|
||||
store = modulestore()._get_modulestore_by_type(modulestore_type) # pylint: disable=protected-access
|
||||
with self.assertRaises(CourseOverview.DoesNotExist):
|
||||
CourseOverview.get_from_id(store.make_course_key('Non', 'Existent', 'Course'))
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo)
|
||||
def test_get_errored_course(self, modulestore_type):
|
||||
"""
|
||||
Test that getting an ErrorDescriptor back from the module store causes
|
||||
get_from_id to raise an IOError.
|
||||
|
||||
Arguments:
|
||||
modulestore_type (ModuleStoreEnum.Type): type of store to create the
|
||||
course in.
|
||||
"""
|
||||
course = CourseFactory.create(default_store=modulestore_type)
|
||||
mock_get_course = mock.Mock(return_value=ErrorDescriptor)
|
||||
with mock.patch('xmodule.modulestore.mixed.MixedModuleStore.get_course', mock_get_course):
|
||||
# This mock makes it so when the module store tries to load course data,
|
||||
# an exception is thrown, which causes get_course to return an ErrorDescriptor,
|
||||
# which causes get_from_id to raise an IOError.
|
||||
with self.assertRaises(IOError):
|
||||
CourseOverview.get_from_id(course.id)
|
||||
|
||||
Reference in New Issue
Block a user