diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 31afe010d5..2f720a14c2 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -52,6 +52,7 @@ class CourseDetailsTestCase(CourseTestCase): self.assertIsNone(details.intro_video, "intro_video somehow initialized" + str(details.intro_video)) self.assertIsNone(details.effort, "effort somehow initialized" + str(details.effort)) self.assertIsNone(details.language, "language somehow initialized" + str(details.language)) + self.assertIsNone(details.has_cert_config) def test_encoder(self): details = CourseDetails.fetch(self.course.id) @@ -1008,6 +1009,41 @@ class CourseMetadataEditingTest(CourseTestCase): tab_list.append(self.notes_tab) self.assertEqual(tab_list, course.tabs) + @override_settings(FEATURES={'CERTIFICATES_HTML_VIEW': True}) + def test_web_view_certifcate_configuration_settings(self): + """ + Test that has_cert_config is updated based on cert_html_view_enabled setting. + """ + test_model = CourseMetadata.update_from_json( + self.course, + { + "cert_html_view_enabled": {"value": "true"} + }, + user=self.user + ) + self.assertIn('cert_html_view_enabled', test_model) + url = get_url(self.course.id) + response = self.client.get_json(url) + course_detail_json = json.loads(response.content) + self.assertFalse(course_detail_json['has_cert_config']) + + # Now add a certificate configuration + certificates = [ + { + 'id': 1, + 'name': 'Certificate Config Name', + 'course_title': 'Title override', + 'org_logo_path': '/c4x/test/CSS101/asset/org_logo.png', + 'signatories': [], + 'is_active': True + } + ] + self.course.certificates = {'certificates': certificates} + modulestore().update_item(self.course, self.user.id) + response = self.client.get_json(url) + course_detail_json = json.loads(response.content) + self.assertTrue(course_detail_json['has_cert_config']) + class CourseGraderUpdatesTest(CourseTestCase): """ diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 5828d4e761..1b3c1ec580 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -310,3 +310,22 @@ def reverse_usage_url(handler_name, usage_key, kwargs=None): Creates the URL for handlers that use usage_keys as URL parameters. """ return reverse_url(handler_name, 'usage_key_string', usage_key, kwargs) + + +def has_active_web_certificate(course): + """ + Returns True if given course has active web certificate configuration. + If given course has no active web certificate configuration returns False. + Returns None If `CERTIFICATES_HTML_VIEW` is not enabled of course has not enabled + `cert_html_view_enabled` settings. + """ + cert_config = None + if settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False) and course.cert_html_view_enabled: + cert_config = False + certificates = getattr(course, 'certificates', {}) + configurations = certificates.get('certificates', []) + for config in configurations: + if config.get('is_active'): + cert_config = True + break + return cert_config diff --git a/cms/djangoapps/models/settings/course_details.py b/cms/djangoapps/models/settings/course_details.py index 0b81444ab5..4483b1145f 100644 --- a/cms/djangoapps/models/settings/course_details.py +++ b/cms/djangoapps/models/settings/course_details.py @@ -8,7 +8,7 @@ from django.conf import settings from opaque_keys.edx.locations import Location from xmodule.modulestore.exceptions import ItemNotFoundError -from contentstore.utils import course_image_url +from contentstore.utils import course_image_url, has_active_web_certificate from models.settings import course_grading from xmodule.fields import Date from xmodule.modulestore.django import modulestore @@ -52,7 +52,8 @@ class CourseDetails(object): self.entrance_exam_minimum_score_pct = settings.FEATURES.get( 'ENTRANCE_EXAM_MIN_SCORE_PCT', '50' - ) # minimum passing score for entrance exam content module/tree + ) # minimum passing score for entrance exam content module/tree, + self.has_cert_config = None # course has active certificate configuration @classmethod def _fetch_about_attribute(cls, course_key, attribute): @@ -84,6 +85,7 @@ class CourseDetails(object): course_details.language = descriptor.language # Default course license is "All Rights Reserved" course_details.license = getattr(descriptor, "license", "all-rights-reserved") + course_details.has_cert_config = has_active_web_certificate(descriptor) for attribute in ABOUT_ATTRIBUTES: value = cls._fetch_about_attribute(course_key, attribute) diff --git a/cms/static/js/models/settings/course_details.js b/cms/static/js/models/settings/course_details.js index a9f70eac7e..efb3e24dcd 100644 --- a/cms/static/js/models/settings/course_details.js +++ b/cms/static/js/models/settings/course_details.js @@ -1,5 +1,5 @@ -define(["backbone", "underscore", "gettext", "js/models/validation_helpers"], - function(Backbone, _, gettext, ValidationHelpers) { +define(["backbone", "underscore", "gettext", "js/models/validation_helpers", "js/utils/date_utils"], + function(Backbone, _, gettext, ValidationHelpers, DateUtils) { var CourseDetails = Backbone.Model.extend({ defaults: { @@ -28,14 +28,21 @@ var CourseDetails = Backbone.Model.extend({ // Returns either nothing (no return call) so that validate works or an object of {field: errorstring} pairs // A bit funny in that the video key validation is asynchronous; so, it won't stop the validation. var errors = {}; + newattrs = DateUtils.convertDateStringsToObjects( + newattrs, ["start_date", "end_date", "enrollment_start", "enrollment_end"] + ); + if (newattrs.start_date === null) { errors.start_date = gettext("The course must have an assigned start date."); } + if (this.hasChanged("start_date") && this.get("has_cert_config") === false){ + errors.start_date = gettext("The course must have at least one active certificate configuration before it can be started."); + } if (newattrs.start_date && newattrs.end_date && newattrs.start_date >= newattrs.end_date) { - errors.end_date = gettext("The course end date cannot be before the course start date."); + errors.end_date = gettext("The course end date must be later than the course start date."); } if (newattrs.start_date && newattrs.enrollment_start && newattrs.start_date < newattrs.enrollment_start) { - errors.enrollment_start = gettext("The course start date cannot be before the enrollment start date."); + errors.enrollment_start = gettext("The course start date must be later than the enrollment start date."); } if (newattrs.enrollment_start && newattrs.enrollment_end && newattrs.enrollment_start >= newattrs.enrollment_end) { errors.enrollment_end = gettext("The enrollment start date cannot be after the enrollment end date."); diff --git a/cms/static/js/spec/views/settings/main_spec.js b/cms/static/js/spec/views/settings/main_spec.js index f39d669c5d..636ed93c0a 100644 --- a/cms/static/js/spec/views/settings/main_spec.js +++ b/cms/static/js/spec/views/settings/main_spec.js @@ -31,7 +31,8 @@ define([ entrance_exam_enabled : '', entrance_exam_minimum_score_pct: '50', license: null, - language: '' + language: '', + has_cert_config: false }, mockSettingsPage = readFixtures('mock/mock-settings-page.underscore'); @@ -71,6 +72,13 @@ define([ ); }); + it('Changing course start date without active certificate configuration should result in error', function () { + this.view.$el.find('#course-start-date') + .val('10/06/2014') + .trigger('change'); + expect(this.view.$el.find('span.message-error').text()).toContain("course must have at least one active certificate configuration"); + }); + it('Selecting a course in pre-requisite drop down should save it as part of course details', function () { var pre_requisite_courses = ['test/CSS101/2012_T1']; var requests = AjaxHelpers.requests(this), diff --git a/cms/static/js/utils/date_utils.js b/cms/static/js/utils/date_utils.js index 8bb1c20884..c6ae76d00d 100644 --- a/cms/static/js/utils/date_utils.js +++ b/cms/static/js/utils/date_utils.js @@ -35,9 +35,29 @@ define(["jquery", "date", "jquery.ui", "jquery.timepicker"], function($, date) { ); }; + var parseDateFromString = function(stringDate){ + if (stringDate && typeof stringDate === "string"){ + return new Date(stringDate); + } + else { + return stringDate; + } + }; + + var convertDateStringsToObjects = function(obj, dateFields){ + for (var i = 0; i < dateFields.length; i++){ + if (obj[dateFields[i]]){ + obj[dateFields[i]] = parseDateFromString(obj[dateFields[i]]); + } + } + return obj; + }; + return { getDate: getDate, setDate: setDate, - renderDate: renderDate + renderDate: renderDate, + convertDateStringsToObjects: convertDateStringsToObjects, + parseDateFromString: parseDateFromString }; }); diff --git a/common/djangoapps/student/tests/test_certificates.py b/common/djangoapps/student/tests/test_certificates.py index 03d5b66e00..33d89458a1 100644 --- a/common/djangoapps/student/tests/test_certificates.py +++ b/common/djangoapps/student/tests/test_certificates.py @@ -47,9 +47,14 @@ class CertificateDisplayTest(ModuleStoreTestCase): @patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': True}) def test_display_download_certificate_button(self, enrollment_mode): """ - Tests if CERTIFICATES_HTML_VIEW is True and there is no active certificate configuration available + Tests if CERTIFICATES_HTML_VIEW is True + and course has enabled web certificates via cert_html_view_enabled setting + and no active certificate configuration available then any of the Download certificate button should not be visible. """ + self.course.cert_html_view_enabled = True + self.course.save() + self.store.update_item(self.course, self.user.id) self._create_certificate(enrollment_mode) self._check_can_not_download_certificate() @@ -75,6 +80,7 @@ class CertificateDisplayTest(ModuleStoreTestCase): } ] self.course.certificates = {'certificates': certificates} + self.course.cert_html_view_enabled = True self.course.save() # pylint: disable=no-member self.store.update_item(self.course, self.user.id) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 1420aa5432..b46a973168 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -59,7 +59,11 @@ from student.forms import AccountCreationForm, PasswordResetFormNoActive from verify_student.models import SoftwareSecurePhotoVerification # pylint: disable=import-error from certificates.models import CertificateStatuses, certificate_status_for_student -from certificates.api import get_certificate_url, get_active_web_certificate # pylint: disable=import-error +from certificates.api import ( # pylint: disable=import-error + get_certificate_url, + get_active_web_certificate, + has_html_certificates_enabled, +) from dark_lang.models import DarkLangConfig from xmodule.modulestore.django import modulestore @@ -305,7 +309,7 @@ def _cert_info(user, course, cert_status, course_mode): if status == 'ready': # showing the certificate web view button if certificate is ready state and feature flags are enabled. - if settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False): + if has_html_certificates_enabled(course.id, course): if get_active_web_certificate(course) is not None: certificate_url = get_certificate_url( user_id=user.id, diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 5a5066c344..f3d18eb8be 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -712,6 +712,12 @@ class CourseFields(object): scope=Scope.settings, default="" ) + cert_html_view_enabled = Boolean( + display_name=_("Certificate Web/HTML View Enabled"), + help=_("If true, certificate Web/HTML views are enabled for the course."), + scope=Scope.settings, + default=False, + ) cert_html_view_overrides = Dict( # Translators: This field is the container for course-specific certifcate configuration values display_name=_("Certificate Web/HTML View Overrides"), diff --git a/common/test/acceptance/pages/studio/settings_advanced.py b/common/test/acceptance/pages/studio/settings_advanced.py index dc94a08920..d4a02d23d5 100644 --- a/common/test/acceptance/pages/studio/settings_advanced.py +++ b/common/test/acceptance/pages/studio/settings_advanced.py @@ -201,4 +201,5 @@ class AdvancedSettingsPage(CoursePage): 'social_sharing_url', 'teams_configuration', 'video_bumper', + 'cert_html_view_enabled', ] diff --git a/common/test/acceptance/tests/lms/test_certificate_web_view.py b/common/test/acceptance/tests/lms/test_certificate_web_view.py index b069ee654e..495457de7f 100644 --- a/common/test/acceptance/tests/lms/test_certificate_web_view.py +++ b/common/test/acceptance/tests/lms/test_certificate_web_view.py @@ -36,8 +36,11 @@ class CertificateWebViewTest(EventsTestMixin, UniqueCourseTest): self.course_info["display_name"], settings=course_settings ) + self.course_fixture.add_advanced_settings({ + "cert_html_view_enabled": {"value": "true"} + }) self.course_fixture.install() - self.user_id = "99" # we have createad a user with this id in fixture + self.user_id = "99" # we have created a user with this id in fixture self.cert_fixture = CertificateConfigFixture(self.course_id, test_certificate_config) # Load certificate web view page for use by the tests diff --git a/lms/djangoapps/certificates/api.py b/lms/djangoapps/certificates/api.py index 58b2b48a9f..aa9f4c005f 100644 --- a/lms/djangoapps/certificates/api.py +++ b/lms/djangoapps/certificates/api.py @@ -10,6 +10,7 @@ from django.conf import settings from django.core.urlresolvers import reverse from eventtracking import tracker +from opaque_keys.edx.keys import CourseKey from xmodule.modulestore.django import modulestore @@ -211,10 +212,14 @@ def has_html_certificates_enabled(course_key, course=None): It determines if course has html certificates enabled """ html_certificates_enabled = False - if settings.FEATURES.get('CERTIFICATES_HTML_VIEW', 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) - if get_active_web_certificate(course) is not None: + if settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False) and course.cert_html_view_enabled: html_certificates_enabled = True + except: # pylint: disable=bare-except + pass return html_certificates_enabled diff --git a/lms/djangoapps/certificates/tests/test_api.py b/lms/djangoapps/certificates/tests/test_api.py index 043bdf6639..c6fa67aec6 100644 --- a/lms/djangoapps/certificates/tests/test_api.py +++ b/lms/djangoapps/certificates/tests/test_api.py @@ -214,6 +214,7 @@ class GenerateUserCertificatesTest(EventTestMixin, ModuleStoreTestCase): } ] self.course.certificates = {'certificates': certificates} + self.course.cert_html_view_enabled = True self.course.save() self.store.update_item(self.course, self.user.id) diff --git a/lms/djangoapps/certificates/tests/test_views.py b/lms/djangoapps/certificates/tests/test_views.py index e4f3eb7b80..7bc80ffd0d 100644 --- a/lms/djangoapps/certificates/tests/test_views.py +++ b/lms/djangoapps/certificates/tests/test_views.py @@ -267,6 +267,7 @@ class MicrositeCertificatesViewsTests(ModuleStoreTestCase): ] self.course.certificates = {'certificates': certificates} + self.course.cert_html_view_enabled = True self.course.save() self.store.update_item(self.course, self.user.id) @@ -422,6 +423,7 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase): ] self.course.certificates = {'certificates': certificates} + self.course.cert_html_view_enabled = True self.course.save() self.store.update_item(self.course, self.user.id) @@ -483,6 +485,7 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase): } ] self.course.certificates = {'certificates': test_certificates} + self.course.cert_html_view_enabled = True self.course.save() self.store.update_item(self.course, self.user.id) response = self.client.get(test_url) @@ -506,6 +509,7 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase): } ] self.course.certificates = {'certificates': test_certificates} + self.course.cert_html_view_enabled = True self.course.save() self.store.update_item(self.course, self.user.id) response = self.client.get(test_url) @@ -646,6 +650,7 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase): verify_uuid=self.cert.verify_uuid ) test_url = '{}?evidence_visit=1'.format(cert_url) + self._add_course_certificates(count=1, signatory_count=2) self.recreate_tracker() assertion = BadgeAssertion( user=self.user, course_id=self.course_id, mode='honor', diff --git a/lms/djangoapps/certificates/views.py b/lms/djangoapps/certificates/views.py index 7d0c7bf40a..282abc273b 100644 --- a/lms/djangoapps/certificates/views.py +++ b/lms/djangoapps/certificates/views.py @@ -21,7 +21,8 @@ from certificates.api import ( get_active_web_certificate, get_certificate_url, generate_user_certificates, - emit_certificate_event + emit_certificate_event, + has_html_certificates_enabled ) from certificates.models import ( certificate_status_for_student, @@ -504,7 +505,7 @@ def render_html_view(request, user_id, course_id): invalid_template_path = 'certificates/invalid.html' # Kick the user back to the "Invalid" screen if the feature is disabled - if not settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False): + if not has_html_certificates_enabled(course_id): return render_to_response(invalid_template_path, context) # Load the core building blocks for the view context diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 217da31f53..e85e06cfe3 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -853,6 +853,7 @@ class ProgressPageTests(ModuleStoreTestCase): ] self.course.certificates = {'certificates': certificates} + self.course.cert_html_view_enabled = True self.course.save() self.store.update_item(self.course, self.user.id) diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index de5252bbfe..4ccb838fbf 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -1081,7 +1081,7 @@ def _progress(request, course_key, student_id): if show_generate_cert_btn: context.update(certs_api.certificate_downloadable_status(student, course_key)) # showing the certificate web view button if feature flags are enabled. - if settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False): + if certs_api.has_html_certificates_enabled(course_key, course): if certs_api.get_active_web_certificate(course) is not None: context.update({ 'show_cert_web_view': True,