diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index 88478ef0c7..31a10d5a39 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -514,6 +514,7 @@ class DashboardTest(ModuleStoreTestCase): expiration_datetime=datetime.now(pytz.UTC) - timedelta(days=1) ) + self.course.certificate_available_date = datetime.now(pytz.UTC) - timedelta(days=1) CourseEnrollment.enroll(self.user, self.course.id, mode='honor') self.course.start = datetime.now(pytz.UTC) - timedelta(days=2) diff --git a/common/lib/xmodule/xmodule/course_metadata_utils.py b/common/lib/xmodule/xmodule/course_metadata_utils.py index 2a93628f08..9dfc499a4b 100644 --- a/common/lib/xmodule/xmodule/course_metadata_utils.py +++ b/common/lib/xmodule/xmodule/course_metadata_utils.py @@ -93,7 +93,12 @@ def course_start_date_is_default(start, advertised_start): return advertised_start is None and start == DEFAULT_START_DATE -def may_certify_for_course(certificates_display_behavior, certificates_show_before_end, has_ended): +def may_certify_for_course( + certificates_display_behavior, + certificates_show_before_end, + has_ended, + certificate_available_date +): """ Returns whether it is acceptable to show the student a certificate download link for a course. @@ -105,12 +110,24 @@ def may_certify_for_course(certificates_display_behavior, certificates_show_befo certificates_show_before_end (bool): whether user can download the course's certificates before the course has ended. has_ended (bool): Whether the course has ended. + certificate_available_date (datetime): the date the certificate is available on for the course. """ show_early = ( certificates_display_behavior in ('early_with_info', 'early_no_info') or certificates_show_before_end ) - return show_early or has_ended + past_availability_date = ( + certificate_available_date + and certificate_available_date < datetime.now(utc) + ) + + if show_early: + return True + if past_availability_date: + return True + if (certificate_available_date is None) and has_ended: + return True + return False def sorting_score(start, advertised_start, announcement): diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 8a3f8e2c92..9f1bba916f 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -1065,7 +1065,8 @@ class CourseDescriptor(CourseFields, SequenceDescriptor, LicenseMixin): return course_metadata_utils.may_certify_for_course( self.certificates_display_behavior, self.certificates_show_before_end, - self.has_ended() + self.has_ended(), + self.certificate_available_date ) def has_started(self): diff --git a/common/lib/xmodule/xmodule/tests/test_course_metadata_utils.py b/common/lib/xmodule/xmodule/tests/test_course_metadata_utils.py index 06014a9bd7..9a3b8fd8c1 100644 --- a/common/lib/xmodule/xmodule/tests/test_course_metadata_utils.py +++ b/common/lib/xmodule/xmodule/tests/test_course_metadata_utils.py @@ -161,11 +161,14 @@ class CourseMetadataUtilsTestCase(TestCase): TestScenario((DEFAULT_START_DATE, None), True), ]), FunctionTest(may_certify_for_course, [ - TestScenario(('early_with_info', True, True), True), - TestScenario(('early_no_info', False, False), True), - TestScenario(('end', True, False), True), - TestScenario(('end', False, True), True), - TestScenario(('end', False, False), False), + TestScenario(('early_with_info', True, True, test_datetime), True), + TestScenario(('early_no_info', False, False, test_datetime), True), + TestScenario(('end', True, False, test_datetime), True), + TestScenario(('end', False, True, test_datetime), True), + TestScenario(('end', False, False, _NEXT_WEEK), False), + TestScenario(('end', False, False, _LAST_WEEK), True), + TestScenario(('end', False, False, None), False), + TestScenario(('early_with_info', False, False, None), True), ]), ] diff --git a/lms/djangoapps/certificates/tests/test_api.py b/lms/djangoapps/certificates/tests/test_api.py index 82a86ae868..aafe6b10f9 100644 --- a/lms/djangoapps/certificates/tests/test_api.py +++ b/lms/djangoapps/certificates/tests/test_api.py @@ -4,6 +4,7 @@ from contextlib import contextmanager from functools import wraps import ddt +from datetime import datetime from config_models.models import cache from django.conf import settings from django.core.urlresolvers import reverse @@ -92,7 +93,8 @@ class CertificateDownloadableStatusTests(WebCertificateTestMixin, ModuleStoreTes self.course = CourseFactory.create( org='edx', number='verified', - display_name='Verified Course' + display_name='Verified Course', + end=datetime.now() ) self.request_factory = RequestFactory() diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 845256f9b2..8e620370d0 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -1216,6 +1216,8 @@ class ProgressPageBaseTests(ModuleStoreTestCase): self.course = CourseFactory.create( start=datetime(2013, 9, 16, 7, 17, 28), grade_cutoffs={u'çü†øƒƒ': 0.75, 'Pass': 0.5}, + end=datetime.now(), + certificate_available_date=datetime.now(), **options ) @@ -2052,6 +2054,7 @@ class GenerateUserCertTests(ModuleStoreTestCase): self.course = CourseFactory.create( org='edx', number='verified', + end=datetime.now(), display_name='Verified Course', grade_cutoffs={'cutoff': 0.75, 'Pass': 0.5} ) diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 1887b586b8..2609d293f4 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -112,7 +112,9 @@ log = logging.getLogger("edx.courseware") # credit and verified modes. REQUIREMENTS_DISPLAY_MODES = CourseMode.CREDIT_MODES + [CourseMode.VERIFIED] -CertData = namedtuple("CertData", ["cert_status", "title", "msg", "download_url", "cert_web_view_url"]) +CertData = namedtuple( + "CertData", ["cert_status", "title", "msg", "download_url", "cert_web_view_url", "may_view_certificate"] +) def user_groups(user): @@ -919,6 +921,7 @@ def _get_cert_data(student, course, course_key, is_active, enrollment_mode): Returns: returns dict if course certificate is available else None. """ + from lms.djangoapps.courseware.courses import get_course_by_id if enrollment_mode == CourseMode.AUDIT: return CertData( @@ -926,24 +929,28 @@ def _get_cert_data(student, course, course_key, is_active, enrollment_mode): _('Your enrollment: Audit track'), _('You are enrolled in the audit track for this course. The audit track does not include a certificate.'), download_url=None, - cert_web_view_url=None + cert_web_view_url=None, + may_view_certificate=None ) - show_generate_cert_btn = ( + show_message = ( is_active and CourseMode.is_eligible_for_certificate(enrollment_mode) and certs_api.cert_generation_enabled(course_key) ) - if not show_generate_cert_btn: + if not show_message: return None + may_view_certificate = course_key and get_course_by_id(course_key).may_certify() + if certs_api.is_certificate_invalid(student, course_key): return CertData( CertificateStatuses.invalidated, _('Your certificate has been invalidated'), _('Please contact your course team if you have any questions.'), download_url=None, - cert_web_view_url=None + cert_web_view_url=None, + may_view_certificate=None ) cert_downloadable_status = certs_api.certificate_downloadable_status(student, course_key) @@ -957,7 +964,12 @@ def _get_cert_data(student, course, course_key, is_active, enrollment_mode): cert_web_view_url = certs_api.get_certificate_url( course_id=course_key, uuid=cert_downloadable_status['uuid'] ) - return CertData(cert_status, title, msg, download_url=None, cert_web_view_url=cert_web_view_url) + return CertData(cert_status, + title, + msg, + download_url=None, + cert_web_view_url=cert_web_view_url, + may_view_certificate=may_view_certificate) else: return CertData( CertificateStatuses.generating, @@ -967,11 +979,17 @@ def _get_cert_data(student, course, course_key, is_active, enrollment_mode): "to it will appear here and on your Dashboard when it is ready." ), download_url=None, - cert_web_view_url=None + cert_web_view_url=None, + may_view_certificate=None ) return CertData( - cert_status, title, msg, download_url=cert_downloadable_status['download_url'], cert_web_view_url=None + cert_status, + title, + msg, + download_url=cert_downloadable_status['download_url'], + cert_web_view_url=None, + may_view_certificate=may_view_certificate ) if cert_downloadable_status['is_generating']: @@ -983,7 +1001,8 @@ def _get_cert_data(student, course, course_key, is_active, enrollment_mode): "it will appear here and on your Dashboard when it is ready." ), download_url=None, - cert_web_view_url=None + cert_web_view_url=None, + may_view_certificate=None ) # If the learner is in verified modes and the student did not have @@ -1001,7 +1020,8 @@ def _get_cert_data(student, course, course_key, is_active, enrollment_mode): 'verified identity.' ).format(platform_name=platform_name), download_url=None, - cert_web_view_url=None + cert_web_view_url=None, + may_view_certificate=None ) return CertData( @@ -1009,7 +1029,8 @@ def _get_cert_data(student, course, course_key, is_active, enrollment_mode): _('Congratulations, you qualified for a certificate!'), _('You can keep working for a higher grade, or request your certificate now.'), download_url=None, - cert_web_view_url=None + cert_web_view_url=None, + may_view_certificate=may_view_certificate ) @@ -1355,7 +1376,7 @@ def _track_successful_certificate_generation(user_id, course_id): # pylint: dis Track a successful certificate generation event. Arguments: - user_id (str): The ID of the user generting the certificate. + user_id (str): The ID of the user generating the certificate. course_id (CourseKey): Identifier for the course. Returns: None diff --git a/lms/djangoapps/mobile_api/testutils.py b/lms/djangoapps/mobile_api/testutils.py index 550d9523e1..85533ef816 100644 --- a/lms/djangoapps/mobile_api/testutils.py +++ b/lms/djangoapps/mobile_api/testutils.py @@ -13,6 +13,7 @@ Test utilities for mobile API tests: from datetime import timedelta import ddt +import datetime from django.conf import settings from django.core.urlresolvers import reverse from django.utils import timezone @@ -39,7 +40,10 @@ class MobileAPITestCase(ModuleStoreTestCase, APITestCase): """ def setUp(self): super(MobileAPITestCase, self).setUp() - self.course = CourseFactory.create(mobile_available=True, static_asset_path="needed_for_split") + self.course = CourseFactory.create( + mobile_available=True, + static_asset_path="needed_for_split", + end=datetime.datetime.now()) self.user = UserFactory.create() self.password = 'test' self.username = self.user.username diff --git a/lms/templates/courseware/progress.html b/lms/templates/courseware/progress.html index ce71201f62..8712eae40d 100644 --- a/lms/templates/courseware/progress.html +++ b/lms/templates/courseware/progress.html @@ -67,6 +67,7 @@ from django.utils.http import urlquote_plus

${certificate_data.title}

${certificate_data.msg}

+ %if certificate_data.may_view_certificate:
%if certificate_data.cert_web_view_url: ${_("View Certificate")} ${_("Opens in a new browser window")} @@ -76,6 +77,7 @@ from django.utils.http import urlquote_plus %endif
+ %endif %endif diff --git a/openedx/core/djangoapps/content/course_overviews/migrations/0014_courseoverview_certificate_available_date.py b/openedx/core/djangoapps/content/course_overviews/migrations/0014_courseoverview_certificate_available_date.py new file mode 100644 index 0000000000..c27795074a --- /dev/null +++ b/openedx/core/djangoapps/content/course_overviews/migrations/0014_courseoverview_certificate_available_date.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('course_overviews', '0013_courseoverview_language'), + ] + + operations = [ + migrations.AddField( + model_name='courseoverview', + name='certificate_available_date', + field=models.DateTimeField(default=None, null=True), + ), + ] diff --git a/openedx/core/djangoapps/content/course_overviews/models.py b/openedx/core/djangoapps/content/course_overviews/models.py index bbedad0168..e1283863a4 100644 --- a/openedx/core/djangoapps/content/course_overviews/models.py +++ b/openedx/core/djangoapps/content/course_overviews/models.py @@ -75,6 +75,7 @@ class CourseOverview(TimeStampedModel): has_any_active_web_certificate = BooleanField(default=False) cert_name_short = TextField() cert_name_long = TextField() + certificate_available_date = DateTimeField(default=None, null=True) # Grading lowest_passing_grade = DecimalField(max_digits=5, decimal_places=2, null=True) @@ -172,6 +173,7 @@ class CourseOverview(TimeStampedModel): course_overview.has_any_active_web_certificate = (get_active_web_certificate(course) is not None) course_overview.cert_name_short = course.cert_name_short course_overview.cert_name_long = course.cert_name_long + course_overview.certificate_available_date = course.certificate_available_date course_overview.lowest_passing_grade = lowest_passing_grade course_overview.end_of_course_survey_url = course.end_of_course_survey_url @@ -476,7 +478,8 @@ class CourseOverview(TimeStampedModel): return course_metadata_utils.may_certify_for_course( self.certificates_display_behavior, self.certificates_show_before_end, - self.has_ended() + self.has_ended(), + self.certificate_available_date ) @property