diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 67af6946aa481fcf83833c5d52e7b8f8e1a8fafd..1a3da5544fc87d137165059f1dd8f988935d5093 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -670,6 +670,13 @@ class CourseFields(object): scope=Scope.settings, default="" ) + cert_html_view_overrides = Dict( + # Translators: This field is the container for course-specific certifcate configuration values + display_name=_("Certificate Web/HTML View Overrides"), + # Translators: These overrides allow for an alternative configuration of the certificate web view + help=_("Enter course-specific overrides for the Web/HTML template parameters here (JSON format)"), + scope=Scope.settings, + ) # An extra property is used rather than the wiki_slug/number because # there are courses that change the number for different runs. This allows diff --git a/common/test/acceptance/pages/studio/settings_advanced.py b/common/test/acceptance/pages/studio/settings_advanced.py index 34a615fab2a8ff4bb37739dffa46bded43500175..126971bcd736830ed54c853a5953d2971f4051b3 100644 --- a/common/test/acceptance/pages/studio/settings_advanced.py +++ b/common/test/acceptance/pages/studio/settings_advanced.py @@ -150,6 +150,7 @@ class AdvancedSettingsPage(CoursePage): 'allow_anonymous', 'allow_anonymous_to_peers', 'allow_public_wiki_access', + 'cert_html_view_overrides', 'cert_name_long', 'cert_name_short', 'certificates_display_behavior', diff --git a/lms/djangoapps/certificates/admin.py b/lms/djangoapps/certificates/admin.py index 715c339d1eb30dcc259086da00b08fa7d378f9eb..783e621362b0d4132cb246cd491c2ab2df6e9f68 100644 --- a/lms/djangoapps/certificates/admin.py +++ b/lms/djangoapps/certificates/admin.py @@ -2,7 +2,9 @@ django admin pages for certificates models """ from django.contrib import admin -from certificates.models import CertificateGenerationConfiguration +from config_models.admin import ConfigurationModelAdmin +from certificates.models import CertificateGenerationConfiguration, CertificateHtmlViewConfiguration admin.site.register(CertificateGenerationConfiguration) +admin.site.register(CertificateHtmlViewConfiguration, ConfigurationModelAdmin) diff --git a/lms/djangoapps/certificates/migrations/0019_auto__add_certificatehtmlviewconfiguration.py b/lms/djangoapps/certificates/migrations/0019_auto__add_certificatehtmlviewconfiguration.py new file mode 100644 index 0000000000000000000000000000000000000000..fe5d4b9a4b6f9718e1e12db7e3a8b49a589bdc4a --- /dev/null +++ b/lms/djangoapps/certificates/migrations/0019_auto__add_certificatehtmlviewconfiguration.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'CertificateHtmlViewConfiguration' + db.create_table('certificates_certificatehtmlviewconfiguration', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('change_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('changed_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, on_delete=models.PROTECT)), + ('enabled', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('configuration', self.gf('django.db.models.fields.TextField')()), + )) + db.send_create_signal('certificates', ['CertificateHtmlViewConfiguration']) + + def backwards(self, orm): + # Deleting model 'CertificateHtmlViewConfiguration' + db.delete_table('certificates_certificatehtmlviewconfiguration') + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'certificates.certificategenerationconfiguration': { + 'Meta': {'object_name': 'CertificateGenerationConfiguration'}, + 'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'certificates.certificatehtmlviewconfiguration': { + 'Meta': {'object_name': 'CertificateHtmlViewConfiguration'}, + 'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}), + 'configuration': ('django.db.models.fields.TextField', [], {}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'certificates.certificatewhitelist': { + 'Meta': {'object_name': 'CertificateWhitelist'}, + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'default': 'None', 'max_length': '255', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'whitelist': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'certificates.generatedcertificate': { + 'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'GeneratedCertificate'}, + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'default': 'None', 'max_length': '255', 'blank': 'True'}), + 'created_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now_add': 'True', 'blank': 'True'}), + 'distinction': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'download_url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'blank': 'True'}), + 'download_uuid': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), + 'error_reason': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '512', 'blank': 'True'}), + 'grade': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '5', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), + 'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '32'}), + 'modified_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'unavailable'", 'max_length': '32'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'verify_uuid': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + } + } + + complete_apps = ['certificates'] diff --git a/lms/djangoapps/certificates/migrations/0020_certificatehtmlviewconfiguration_data.py b/lms/djangoapps/certificates/migrations/0020_certificatehtmlviewconfiguration_data.py new file mode 100644 index 0000000000000000000000000000000000000000..abe3a142562028cb3bf982864ccf88b19005fba2 --- /dev/null +++ b/lms/djangoapps/certificates/migrations/0020_certificatehtmlviewconfiguration_data.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + + +class Migration(DataMigration): + + def forwards(self, orm): + """ + Bootstraps the HTML view template with some default configuration parameters + """ + json_config = """{ + "default": { + "accomplishment_class_append": "accomplishment--certificate--honorcode", + "certificate_verify_url_prefix": "https://verify-test.edx.org/cert/", + "certificate_verify_url_suffix": "/verify.html", + "company_about_url": "http://www.edx.org/about-us", + "company_courselist_url": "http://www.edx.org/course-list", + "company_careers_url": "http://www.edx.org/jobs", + "company_contact_url": "http://www.edx.org/contact-us", + "platform_name": "edX", + "company_privacy_url": "http://www.edx.org/edx-privacy-policy", + "company_tos_url": "http://www.edx.org/edx-terms-service", + "company_verified_certificate_url": "http://www.edx.org/verified-certificate", + "document_script_src_modernizr": "https://verify-test.edx.org/v2/static/js/vendor/modernizr-2.6.2.min.js", + "document_stylesheet_url_normalize": "https://verify-test.edx.org/v2/static/css/vendor/normalize.css", + "document_stylesheet_url_fontawesome": "https://verify-test.edx.org/v2/static/css/vendor/font-awesome.css", + "document_stylesheet_url_application": "https://verify-test.edx.org/v2/static/css/style-application.css", + "logo_src": "https://verify-test.edx.org/v2/static/images/logo-edx.svg", + "logo_url": "http://www.edx.org" + }, + "honor": { + "certificate_type": "Honor Code", + "document_body_class_append": "is-honorcode" + }, + "verified": { + "certificate_type": "Verified", + "document_body_class_append": "is-idverified" + }, + "xseries": { + "certificate_type": "XSeries", + "document_body_class_append": "is-xseries", + "document_script_src_modernizr": "https://verify-test.edx.org/xseries/static/js/vendor/modernizr-2.6.2.min.js", + "document_stylesheet_url_normalize": "https://verify-test.edx.org/xseries/static/css/vendor/normalize.css", + "document_stylesheet_url_fontawesome": "https://verify-test.edx.org/xseries/static/css/vendor/font-awesome.css", + "document_stylesheet_url_application": "https://verify-test.edx.org/xseries/static/css/style-application.css", + "logo_src": "https://verify-test.edx.org/xseries/static/images/logo-edx.svg" + } + }""" + orm.CertificateHtmlViewConfiguration.objects.create( + configuration=json_config, + enabled=False, + ) + + def backwards(self, orm): + "Write your backwards methods here." + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'certificates.certificategenerationconfiguration': { + 'Meta': {'object_name': 'CertificateGenerationConfiguration'}, + 'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'certificates.certificategenerationcoursesetting': { + 'Meta': {'object_name': 'CertificateGenerationCourseSetting'}, + 'course_key': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}) + }, + 'certificates.certificatehtmlviewconfiguration': { + 'Meta': {'object_name': 'CertificateHtmlViewConfiguration'}, + 'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}), + 'configuration': ('django.db.models.fields.TextField', [], {}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'certificates.certificatewhitelist': { + 'Meta': {'object_name': 'CertificateWhitelist'}, + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'default': 'None', 'max_length': '255', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'whitelist': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'certificates.examplecertificate': { + 'Meta': {'object_name': 'ExampleCertificate'}, + 'access_key': ('django.db.models.fields.CharField', [], {'default': "'0836d966ec2047e18114969f89ee5270'", 'max_length': '255', 'db_index': 'True'}), + 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'download_url': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True'}), + 'error_reason': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + 'example_cert_set': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['certificates.ExampleCertificateSet']"}), + 'full_name': ('django.db.models.fields.CharField', [], {'default': "u'John Do\\xeb'", 'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'started'", 'max_length': '255'}), + 'template': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'uuid': ('django.db.models.fields.CharField', [], {'default': "'558539cc3a114c48a1cc404a73d4cfcd'", 'unique': 'True', 'max_length': '255', 'db_index': 'True'}) + }, + 'certificates.examplecertificateset': { + 'Meta': {'object_name': 'ExampleCertificateSet'}, + 'course_key': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}) + }, + 'certificates.generatedcertificate': { + 'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'GeneratedCertificate'}, + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'default': 'None', 'max_length': '255', 'blank': 'True'}), + 'created_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now_add': 'True', 'blank': 'True'}), + 'distinction': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'download_url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'blank': 'True'}), + 'download_uuid': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), + 'error_reason': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '512', 'blank': 'True'}), + 'grade': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '5', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), + 'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '32'}), + 'modified_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'unavailable'", 'max_length': '32'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'verify_uuid': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + } + } + + complete_apps = ['certificates'] + symmetrical = True diff --git a/lms/djangoapps/certificates/models.py b/lms/djangoapps/certificates/models.py index 31202a558bf861cece4496f9bfbdf258e6d82354..d5cdd61eaf91aa45f02fcfcedbba8030b3bc7617 100644 --- a/lms/djangoapps/certificates/models.py +++ b/lms/djangoapps/certificates/models.py @@ -46,9 +46,12 @@ Eligibility: unless he has allow_certificate set to False. """ from datetime import datetime +import json import uuid +from django.conf import settings from django.contrib.auth.models import User +from django.core.exceptions import ValidationError from django.db import models, transaction from django.db.models.signals import post_save from django.dispatch import receiver @@ -503,3 +506,44 @@ class CertificateGenerationConfiguration(ConfigurationModel): """ pass + + +class CertificateHtmlViewConfiguration(ConfigurationModel): + """ + Static values for certificate HTML view context parameters. + Default values will be applied across all certificate types (course modes) + Matching 'mode' overrides will be used instead of defaults, where applicable + Example configuration : + { + "default": { + "url": "http://www.edx.org", + "logo_src": "http://www.edx.org/static/images/logo.png", + "logo_alt": "Valid Certificate" + }, + "honor": { + "logo_src": "http://www.edx.org/static/images/honor-logo.png", + "logo_alt": "Honor Certificate" + } + } + """ + configuration = models.TextField( + help_text="Certificate HTML View Parameters (JSON)" + ) + + def clean(self): + """ + Ensures configuration field contains valid JSON. + """ + try: + json.loads(self.configuration) + except ValueError: + raise ValidationError('Must be valid JSON string.') + + @classmethod + def get_config(cls): + """ + Retrieves the configuration field value from the database + """ + instance = cls.current() + json_data = json.loads(instance.configuration) if instance.enabled else {} + return json_data diff --git a/lms/djangoapps/certificates/tests/factories.py b/lms/djangoapps/certificates/tests/factories.py index d800032f669e5efeda3db36ba79a3b951b781ae5..728040503090956a8c129f4a503622aa012413f7 100644 --- a/lms/djangoapps/certificates/tests/factories.py +++ b/lms/djangoapps/certificates/tests/factories.py @@ -2,7 +2,7 @@ from factory.django import DjangoModelFactory from opaque_keys.edx.locations import SlashSeparatedCourseKey -from certificates.models import GeneratedCertificate, CertificateStatuses +from certificates.models import GeneratedCertificate, CertificateStatuses, CertificateHtmlViewConfiguration # Factories are self documenting @@ -15,3 +15,48 @@ class GeneratedCertificateFactory(DjangoModelFactory): status = CertificateStatuses.unavailable mode = GeneratedCertificate.MODES.honor name = '' + + +class CertificateHtmlViewConfigurationFactory(DjangoModelFactory): + + FACTORY_FOR = CertificateHtmlViewConfiguration + + enabled = True + configuration = """{ + "default": { + "accomplishment_class_append": "accomplishment--certificate--honorcode", + "certificate_verify_url_prefix": "https://verify-test.edx.org/cert/", + "certificate_verify_url_suffix": "/verify.html", + "company_about_url": "http://www.edx.org/about-us", + "company_courselist_url": "http://www.edx.org/course-list", + "company_careers_url": "http://www.edx.org/jobs", + "company_contact_url": "http://www.edx.org/contact-us", + "platform_name": "edX", + "company_privacy_url": "http://www.edx.org/edx-privacy-policy", + "company_tos_url": "http://www.edx.org/edx-terms-service", + "company_verified_certificate_url": "http://www.edx.org/verified-certificate", + "document_script_src_modernizr": "https://verify-test.edx.org/v2/static/js/vendor/modernizr-2.6.2.min.js", + "document_stylesheet_url_normalize": "https://verify-test.edx.org/v2/static/css/vendor/normalize.css", + "document_stylesheet_url_fontawesome": "https://verify-test.edx.org/v2/static/css/vendor/font-awesome.css", + "document_stylesheet_url_application": "https://verify-test.edx.org/v2/static/css/style-application.css", + "logo_src": "https://verify-test.edx.org/v2/static/images/logo-edx.svg", + "logo_url": "http://www.edx.org" + }, + "honor": { + "certificate_type": "Honor Code", + "document_body_class_append": "is-honorcode" + }, + "verified": { + "certificate_type": "Verified", + "document_body_class_append": "is-idverified" + }, + "xseries": { + "certificate_type": "XSeries", + "document_body_class_append": "is-xseries", + "document_script_src_modernizr": "https://verify-test.edx.org/xseries/static/js/vendor/modernizr-2.6.2.min.js", + "document_stylesheet_url_normalize": "https://verify-test.edx.org/xseries/static/css/vendor/normalize.css", + "document_stylesheet_url_fontawesome": "https://verify-test.edx.org/xseries/static/css/vendor/font-awesome.css", + "document_stylesheet_url_application": "https://verify-test.edx.org/xseries/static/css/style-application.css", + "logo_src": "https://verify-test.edx.org/xseries/static/images/logo-edx.svg" + } + }""" diff --git a/lms/djangoapps/certificates/tests/test_models.py b/lms/djangoapps/certificates/tests/test_models.py index 261baa57f086ec9d0fc0c588214050ecaf6306b9..e856984193d05072fe668b9fddbd50c7c149c1d5 100644 --- a/lms/djangoapps/certificates/tests/test_models.py +++ b/lms/djangoapps/certificates/tests/test_models.py @@ -1,12 +1,19 @@ """Tests for certificate Django models. """ +from django.conf import settings +from django.core.exceptions import ValidationError from django.test import TestCase +from django.test.utils import override_settings from opaque_keys.edx.locator import CourseLocator from certificates.models import ( ExampleCertificate, - ExampleCertificateSet + ExampleCertificateSet, + CertificateHtmlViewConfiguration ) +FEATURES_INVALID_FILE_PATH = settings.FEATURES.copy() +FEATURES_INVALID_FILE_PATH['CERTS_HTML_VIEW_CONFIG_PATH'] = 'invalid/path/to/config.json' + class ExampleCertificateTest(TestCase): """Tests for the ExampleCertificate model. """ @@ -71,3 +78,73 @@ class ExampleCertificateTest(TestCase): other_course = CourseLocator(org='other', course='other', run='other') result = ExampleCertificateSet.latest_status(other_course) self.assertIs(result, None) + + +class CertificateHtmlViewConfigurationTest(TestCase): + """ + Test the CertificateHtmlViewConfiguration model. + """ + def setUp(self): + super(CertificateHtmlViewConfigurationTest, self).setUp() + self.configuration_string = """{ + "default": { + "url": "http://www.edx.org", + "logo_src": "http://www.edx.org/static/images/logo.png", + "logo_alt": "Valid Certificate" + }, + "honor": { + "logo_src": "http://www.edx.org/static/images/honor-logo.png", + "logo_alt": "Honor Certificate" + } + }""" + self.config = CertificateHtmlViewConfiguration(configuration=self.configuration_string) + + def test_create(self): + """ + Tests creation of configuration. + """ + self.config.save() + self.assertEquals(self.config.configuration, self.configuration_string) + + def test_clean_bad_json(self): + """ + Tests if bad JSON string was given. + """ + self.config = CertificateHtmlViewConfiguration(configuration='{"bad":"test"') + self.assertRaises(ValidationError, self.config.clean) + + def test_get(self): + """ + Tests get configuration from saved string. + """ + self.config.enabled = True + self.config.save() + expected_config = { + "default": { + "url": "http://www.edx.org", + "logo_src": "http://www.edx.org/static/images/logo.png", + "logo_alt": "Valid Certificate" + }, + "honor": { + "logo_src": "http://www.edx.org/static/images/honor-logo.png", + "logo_alt": "Honor Certificate" + } + } + self.assertEquals(self.config.get_config(), expected_config) + + def test_get_not_enabled_returns_blank(self): + """ + Tests get configuration that is not enabled. + """ + self.config.enabled = False + self.config.save() + self.assertEquals(len(self.config.get_config()), 0) + + @override_settings(FEATURES=FEATURES_INVALID_FILE_PATH) + def test_get_no_database_no_file(self): + """ + Tests get configuration that is not enabled. + """ + self.config.configuration = '' + self.config.save() + self.assertEquals(self.config.get_config(), {}) diff --git a/lms/djangoapps/certificates/tests/test_views.py b/lms/djangoapps/certificates/tests/test_views.py index 1e846de556cd1dd83c311dde78670f43c150241b..7e031b912ca4649380e58f2cab9a575f42fc01d0 100644 --- a/lms/djangoapps/certificates/tests/test_views.py +++ b/lms/djangoapps/certificates/tests/test_views.py @@ -2,14 +2,28 @@ import json import ddt +from uuid import uuid4 -from django.test import TestCase -from django.core.urlresolvers import reverse +from django.conf import settings from django.core.cache import cache +from django.core.urlresolvers import reverse +from django.test import TestCase +from django.test.client import Client +from django.test.utils import override_settings from opaque_keys.edx.locator import CourseLocator +from student.tests.factories import UserFactory +from xmodule.modulestore.tests.factories import CourseFactory +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase + +from certificates.models import ExampleCertificateSet, ExampleCertificate, GeneratedCertificate +from certificates.tests.factories import CertificateHtmlViewConfigurationFactory + +FEATURES_WITH_CERTS_ENABLED = settings.FEATURES.copy() +FEATURES_WITH_CERTS_ENABLED['CERTIFICATES_HTML_VIEW'] = True -from certificates.models import ExampleCertificateSet, ExampleCertificate +FEATURES_WITH_CERTS_DISABLED = settings.FEATURES.copy() +FEATURES_WITH_CERTS_DISABLED['CERTIFICATES_HTML_VIEW'] = False @ddt.ddt @@ -151,3 +165,90 @@ class UpdateExampleCertificateViewTest(TestCase): content = json.loads(response.content) self.assertEqual(response.status_code, 200) self.assertEqual(content['return_code'], 0) + + +class CertificatesViewsTests(ModuleStoreTestCase): + """ + Tests for the manual refund page + """ + def setUp(self): + super(CertificatesViewsTests, self).setUp() + self.client = Client() + self.course = CourseFactory.create( + org='testorg', number='run1', display_name='refundable course' + ) + self.course_id = self.course.location.course_key + self.user = UserFactory.create( + email='joe_user@edx.org', + username='joeuser', + password='foo' + ) + self.user.profile.name = "Joe User" + self.user.profile.save() + self.client.login(username=self.user.username, password='foo') + + self.cert = GeneratedCertificate.objects.create( + user=self.user, + course_id=self.course_id, + verify_uuid=uuid4(), + download_uuid=uuid4(), + grade="0.95", + key='the_key', + distinction=True, + status='generated', + mode='honor', + name=self.user.profile.name, + ) + CertificateHtmlViewConfigurationFactory.create() + + @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) + def test_render_html_view_valid_certificate(self): + test_url = '/certificates/html?course={}'.format(unicode(self.course.id)) + response = self.client.get(test_url) + self.assertIn(str(self.cert.verify_uuid), response.content) + + # Hit any "verified" mode-specific branches + self.cert.mode = 'verified' + self.cert.save() + test_url = '/certificates/html?course={}'.format(unicode(self.course.id)) + response = self.client.get(test_url) + self.assertIn(str(self.cert.verify_uuid), response.content) + + # Hit any 'xseries' mode-specific branches + self.cert.mode = 'xseries' + self.cert.save() + test_url = '/certificates/html?course={}'.format(unicode(self.course.id)) + response = self.client.get(test_url) + self.assertIn(str(self.cert.verify_uuid), response.content) + + @override_settings(FEATURES=FEATURES_WITH_CERTS_DISABLED) + def test_render_html_view_invalid_feature_flag(self): + test_url = '/certificates/html?course={}'.format(unicode(self.course.id)) + response = self.client.get(test_url) + self.assertIn('invalid', response.content) + + @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) + def test_render_html_view_missing_course_id(self): + test_url = '/certificates/html' + response = self.client.get(test_url) + self.assertIn('invalid', response.content) + + @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) + def test_render_html_view_invalid_course_id(self): + test_url = '/certificates/html?course=az-23423-4vs' + response = self.client.get(test_url) + self.assertIn('invalid', response.content) + + @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) + def test_render_html_view_invalid_course(self): + test_url = '/certificates/html?course=missing/course/key' + response = self.client.get(test_url) + self.assertIn('invalid', response.content) + + @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) + def test_render_html_view_invalid_certificate(self): + self.cert.delete() + self.assertEqual(len(GeneratedCertificate.objects.all()), 0) + test_url = '/certificates/html?course={}'.format(unicode(self.course.id)) + response = self.client.get(test_url) + self.assertIn('invalid', response.content) diff --git a/lms/djangoapps/certificates/views.py b/lms/djangoapps/certificates/views.py index 09d2fb4ffc75b739e4f604058c12c870185dc0c8..f9f78a3f149710ecf87e775abb17b72247bccd37 100644 --- a/lms/djangoapps/certificates/views.py +++ b/lms/djangoapps/certificates/views.py @@ -1,10 +1,14 @@ """URL handlers related to certificate handling by LMS""" +from datetime import datetime import dogstats_wrapper as dog_stats_api import json import logging +from django.conf import settings +from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User from django.http import HttpResponse, Http404, HttpResponseForbidden +from django.utils.translation import ugettext as _ from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST @@ -13,13 +17,17 @@ from certificates.models import ( certificate_status_for_student, CertificateStatuses, GeneratedCertificate, - ExampleCertificate + ExampleCertificate, + CertificateHtmlViewConfiguration ) from certificates.queue import XQueueCertInterface +from edxmako.shortcuts import render_to_response from xmodule.modulestore.django import modulestore +from opaque_keys import InvalidKeyError +from opaque_keys.edx.keys import CourseKey +from opaque_keys.edx.locations import SlashSeparatedCourseKey from util.json_request import JsonResponse, JsonResponseBadRequest from util.bad_request_rate_limiter import BadRequestRateLimiter -from opaque_keys.edx.locations import SlashSeparatedCourseKey logger = logging.getLogger(__name__) @@ -224,3 +232,215 @@ def update_example_certificate(request): # Let the XQueue know that we handled the response return JsonResponse({'return_code': 0}) + + +# pylint: disable=too-many-statements, bad-continuation +@login_required +def render_html_view(request): + """ + This view generates an HTML representation of the specified student's certificate + If a certificate is not available, we display a "Sorry!" screen instead + """ + invalid_template_path = 'certificates/invalid.html' + + # Feature Flag check + if not settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False): + return render_to_response(invalid_template_path) + + context = {} + course_id = request.GET.get('course', None) + context['course'] = course_id + if not course_id: + return render_to_response(invalid_template_path, context) + + # Course Lookup + try: + course_key = CourseKey.from_string(course_id) + except InvalidKeyError: + return render_to_response(invalid_template_path, context) + course = modulestore().get_course(course_key) + if not course: + return render_to_response(invalid_template_path, context) + + # Certificate Lookup + try: + certificate = GeneratedCertificate.objects.get( + user=request.user, + course_id=course_key + ) + except GeneratedCertificate.DoesNotExist: + return render_to_response(invalid_template_path, context) + + # Load static output values from configuration, + configuration = CertificateHtmlViewConfiguration.get_config() + context = configuration.get('default', {}) + # Override the defaults with any mode-specific static values + context.update(configuration.get(certificate.mode, {})) + # Override further with any course-specific static values + context.update(course.cert_html_view_overrides) + + # Populate dynamic output values using the course/certificate data loaded above + user_fullname = request.user.profile.name + platform_name = context.get('platform_name') + context['accomplishment_copy_name'] = user_fullname + context['accomplishment_copy_course_org'] = course.org + context['accomplishment_copy_course_name'] = course.display_name + context['certificate_id_number'] = certificate.verify_uuid + context['certificate_verify_url'] = "{prefix}{uuid}{suffix}".format( + prefix=context.get('certificate_verify_url_prefix'), + uuid=certificate.verify_uuid, + suffix=context.get('certificate_verify_url_suffix') + ) + context['logo_alt'] = platform_name + + accd_course_org_html = '<span class="detail--xuniversity">{partner_name}</span>'.format(partner_name=course.org) + accd_platform_name_html = '<span class="detail--company">{platform_name}</span>'.format(platform_name=platform_name) + # Translators: This line appears on the certificate after the name of a course, and provides more + # information about the organizations providing the course material to platform users + context['accomplishment_copy_course_description'] = _('a course of study offered by {partner_name}, ' + 'through {platform_name}.').format( + partner_name=accd_course_org_html, + platform_name=accd_platform_name_html + ) + + context['accomplishment_more_title'] = _("More Information About {user_name}'s Certificate:").format( + user_name=user_fullname + ) + + # Translators: This line appears on the page just before the generation date for the certificate + context['certificate_date_issued_title'] = _("Issued On:") + + # Translators: The format of the date includes the full name of the month + context['certificate_date_issued'] = _('{month} {day}, {year}').format( + month=certificate.modified_date.strftime("%B"), + day=certificate.modified_date.day, + year=certificate.modified_date.year + ) + + # Translators: The Certificate ID Number is an alphanumeric value unique to each individual certificate + context['certificate_id_number_title'] = _('Certificate ID Number') + + context['certificate_info_title'] = _('About {platform_name} Certificates').format( + platform_name=platform_name + ) + + # Translators: This text describes the purpose (and therefore, value) of a course certificate + # 'verifying your identity' refers to the process for establishing the authenticity of the student + context['certificate_info_description'] = _("{platform_name} acknowledges achievements through certificates, which " + "are awarded for various activities {platform_name} students complete " + "under the <a href='{tos_url}'>{platform_name} Honor Code</a>. Some " + "certificates require completing additional steps, such as " + "<a href='{verified_cert_url}'> verifying your identity</a>.").format( + platform_name=platform_name, + tos_url=context.get('company_tos_url'), + verified_cert_url=context.get('company_verified_certificate_url') + ) + + # Translators: Certificate Types correspond to the different enrollment options available for a given course + context['certificate_type_title'] = _('{certificate_type} Certfificate').format( + certificate_type=context.get('certificate_type') + ) + + context['certificate_verify_title'] = _("How {platform_name} Validates Student Certificates").format( + platform_name=platform_name + ) + + # Translators: This text describes the validation mechanism for a certificate file (known as GPG security) + context['certificate_verify_description'] = _('Certificates issued by {platform_name} are signed by a gpg key so ' + 'that they can be validated independently by anyone with the ' + '{platform_name} public key. For independent verification, ' + '{platform_name} uses what is called a ' + '"detached signature""".').format(platform_name=platform_name) + + context['certificate_verify_urltext'] = _("Validate this certificate for yourself") + + # Translators: This text describes (at a high level) the mission and charter the edX platform and organization + context['company_about_description'] = _("{platform_name} offers interactive online classes and MOOCs from the " + "world's best universities, including MIT, Harvard, Berkeley, University " + "of Texas, and many others. {platform_name} is a non-profit online " + "initiative created by founding partners Harvard and MIT.").format( + platform_name=platform_name + ) + + context['company_about_title'] = _("About {platform_name}").format(platform_name=platform_name) + + context['company_about_urltext'] = _("Learn more about {platform_name}").format(platform_name=platform_name) + + context['company_courselist_urltext'] = _("Learn with {platform_name}").format(platform_name=platform_name) + + context['company_careers_urltext'] = _("Work at {platform_name}").format(platform_name=platform_name) + + context['company_contact_urltext'] = _("Contact {platform_name}").format(platform_name=platform_name) + + context['company_privacy_urltext'] = _("Privacy Policy") + + context['company_tos_urltext'] = _("Terms of Service & Honor Code") + + # Translators: This text appears near the top of the certficate and describes the guarantee provided by edX + context['document_banner'] = _("{platform_name} acknowledges the following student accomplishment").format( + platform_name=platform_name + ) + + context['logo_subtitle'] = _("Certificate Validation") + + if certificate.mode == 'honor': + # Translators: This text describes the 'Honor' course certificate type. + context['certificate_type_description'] = _("An {cert_type} Certificate signifies that an {platform_name} " + "learner has agreed to abide by {platform_name}'s honor code and " + "completed all of the required tasks for this course under its " + "guidelines.").format( + cert_type=context.get('certificate_type'), + platform_name=platform_name + ) + elif certificate.mode == 'verified': + # Translators: This text describes the 'ID Verified' course certificate type, which is a higher level of + # verification offered by edX. This type of verification is useful for professional education/certifications + context['certificate_type_description'] = _("An {cert_type} Certificate signifies that an {platform_name} " + "learner has agreed to abide by {platform_name}'s honor code and " + "completed all of the required tasks for this course under its " + "guidelines, as well as having their photo ID checked to verify " + "their identity.").format( + cert_type=context.get('certificate_type'), + platform_name=platform_name + ) + elif certificate.mode == 'xseries': + # Translators: This text describes the 'XSeries' course certificate type. An XSeries is a collection of + # courses related to each other in a meaningful way, such as a specific topic or theme, or even an organization + context['certificate_type_description'] = _("An {cert_type} Certificate demonstrates a high level of " + "achievement in a program of study, and includes verification of " + "the student's identity.").format( + cert_type=context.get('certificate_type') + ) + + # Translators: This is the copyright line which appears at the bottom of the certificate page/screen + context['copyright_text'] = _('© {year} {platform_name}. All rights reserved.').format( + year=datetime.now().year, + platform_name=platform_name + ) + + # Translators: This text represents the verification of the certificate + context['document_meta_description'] = _('This is a valid {platform_name} certificate for {user_name}, ' + 'who participated in {partner_name} {course_number}').format( + platform_name=platform_name, + user_name=user_fullname, + partner_name=course.org, + course_number=course.number + ) + + # Translators: This text is bound to the HTML 'title' element of the page and appears in the browser title bar + context['document_title'] = _("Valid {partner_name} {course_number} Certificate | {platform_name}").format( + partner_name=course.org, + course_number=course.number, + platform_name=platform_name + ) + + # Translators: This text fragment appears after the student's name (displayed in a large font) on the certificate + # screen. The text describes the accomplishment represented by the certificate information displayed to the user + context['accomplishment_copy_description_full'] = _("successfully completed, received a passing grade, and was " + "awarded a {platform_name} {certificate_type} " + "Certificate of Completion in ").format( + platform_name=platform_name, + certificate_type=context.get("certificate_type") + ) + + return render_to_response("certificates/valid.html", context) diff --git a/lms/envs/bok_choy.env.json b/lms/envs/bok_choy.env.json index 89223675c7501c3d9341dcb65e0133efaab4013f..e95aeffe1396251382a70c79425ea999b84cd8fc 100644 --- a/lms/envs/bok_choy.env.json +++ b/lms/envs/bok_choy.env.json @@ -65,6 +65,7 @@ "FEATURES": { "AUTH_USE_OPENID_PROVIDER": true, "CERTIFICATES_ENABLED": true, + "CERTIFICATES_HTML_VIEW": true, "MULTIPLE_ENROLLMENT_ROLES": true, "ENABLE_PAYMENT_FAKE": true, "ENABLE_VERIFIED_CERTIFICATES": true, diff --git a/lms/envs/common.py b/lms/envs/common.py index aed43d3bfe7ab670a3a761d9ece22946e20359cd..14d998b0606e11de48a3d7d39a80b68b51eca1c5 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -351,6 +351,9 @@ FEATURES = { # enable beacons for video timing statistics 'ENABLE_VIDEO_BEACON': False, + + # Certificates Web/HTML Views + 'CERTIFICATES_HTML_VIEW': False, } # Ignore static asset files on import which match this pattern diff --git a/lms/envs/devstack.py b/lms/envs/devstack.py index 6cdbe0743b50c27b54b0755ecb4c40565cea32a6..3d5762d47a288be348a17666e526778af566738f 100644 --- a/lms/envs/devstack.py +++ b/lms/envs/devstack.py @@ -123,6 +123,10 @@ FEATURES['ENABLE_COURSEWARE_SEARCH'] = True SEARCH_ENGINE = "search.elastic.ElasticSearchEngine" +########################## Certificates Web/HTML View ####################### +FEATURES['CERTIFICATES_HTML_VIEW'] = True + + ##################################################################### # See if the developer has any local overrides. try: diff --git a/lms/envs/test.py b/lms/envs/test.py index 8b22784449f002d8631cb73c58e45cb7c27254e0..42044969889352c878bca6a80557b631fa446b13 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -466,3 +466,6 @@ SEARCH_ENGINE = "search.tests.mock_search_engine.MockSearchEngine" FACEBOOK_APP_SECRET = "Test" FACEBOOK_APP_ID = "Test" FACEBOOK_API_VERSION = "v2.2" + +# Certificates Views +FEATURES['CERTIFICATES_HTML_VIEW'] = True diff --git a/lms/templates/certificates/invalid.html b/lms/templates/certificates/invalid.html new file mode 100644 index 0000000000000000000000000000000000000000..1e9e50274d0c10251ea33d1e7b3cff85162c4644 --- /dev/null +++ b/lms/templates/certificates/invalid.html @@ -0,0 +1,25 @@ +<%! from django.utils.translation import ugettext as _ %> +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title></title> + <link rel="stylesheet" href="/stylesheets/base.css"> +</head> +<body> + <header> + <h1>edX and MITX</h1> + </header> + + <section> + <header class="invalid"> + <h1>${_("This is an invalid edX certificate number")}</h1> + <p>${_("Please check the number to make sure that it is the exact same as on the certificate.")}</p> + </header> + + <section> + <p>${_("This is an unknown certificate number and therefore is a potential forgery.")}</p> + </section> + </section> +</body> +</html> diff --git a/lms/templates/certificates/valid.html b/lms/templates/certificates/valid.html new file mode 100644 index 0000000000000000000000000000000000000000..1d81015fbb957dc0d08d0a716239f4db37be64d9 --- /dev/null +++ b/lms/templates/certificates/valid.html @@ -0,0 +1,154 @@ +<%! from django.utils.translation import ugettext as _ %> +<%! import mako.runtime %> +<% mako.runtime.UNDEFINED = '' %> + +<!DOCTYPE html> +<!--[if lt IE 7]><html class="no-js lt-ie9 lt-ie8 lt-ie7"><![endif]--> +<!--[if IE 7]><html class="no-js lt-ie10 lt-ie9 lt-ie8"><![endif]--> +<!--[if IE 8]><html class="no-js lt-ie10 lt-ie9"><![endif]--> +<!--[if IE 9]><html class="no-js lt-ie10"><![endif]--> +<!--[if gt IE 9]><!--><html class="no-js"><!--<![endif]--> +<head> + <title>${document_title}</title> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="description" content="${document_meta_description}"> + <link rel="stylesheet" href="${document_stylesheet_url_normalize}" /> + <link rel="stylesheet" href="${document_stylesheet_url_fontawesome}" /> + <link rel="stylesheet" href="${document_stylesheet_url_application}" /> + <script src="${document_script_src_modernizr}"></script> +</head> + +<body class="view--valid-certificate ${document_body_class_append}" data-view="valid-certificate"> + + <!--[if lt IE 9]> + <p class="msg msg--browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p> + <![endif]--> + + <nav class="nav--skip sr"> + <h2>${_("Skip to This Page's Content")}</h2> + <ol> + <li class="nav__item"><a class="action" href="#validation-status">${_("Validation Status")}</a></li> + <li class="nav__item"><a class="action" href="#validation-accomplishment">${_("Student Accomplishment")}</a></li> + <li class="nav__item"><a class="action" href="#validation-info">${_("More Information")}</a></li> + <li class="nav__item"><a class="action" href="#company-info">${_("About")} ${platform_name}</a></li> + </ol> + </nav> + <div class="wrapper--view"> + <div class="wrapper--header"> + <header class="header--app" role="banner"> + <h1 class="title title--logo"> + <span class="logo"> + <a href="${logo_url}"><img class="img--logo" src="" alt="${logo_alt}" /></a> + </span> + <span class="title title--sub">${logo_subtitle}</span> + </h1> + </header> + </div> + <hr class="divider /"> + <div class="wrapper--content"> + <section class="content content--main" role="main"> + <div class="status status--valid" id="validation-status"> + <h2 class="title title--lvl2">${document_banner}<span class="sr">:</span></h2> + </div> + <article class="accomplishment ${accomplishment_class_append}" id="validation-accomplishment"> + <div class="accomplishment__statement"> + <p class="copy"> + <span class="copy__name">${accomplishment_copy_name}</span> + <span class="copy__context">${accomplishment_copy_description_full}</span> + <span class="copy__course"> + <span class="copy__course__org">${accomplishment_copy_course_org}</span> + <span class="copy__course__name">${accomplishment_copy_course_name}</span> + </span> + <span class="copy__context">${accomplishment_copy_course_description}</span> + </p> + </div> + <div class="accomplishment__details"> + <h3 class="title title--lvl2 sr">${accomplishment_more_title}</h3> + <ul class="list list--metadata"> + <li class="item certificate--type"> + <span class="label">_(Certificate Type)</span> + <span class="value"> + ${certificate_type_title} + <span class="explanation">${certificate_type_description} </span> + </span> + </li> + <li class="item certificate--id"> + <span class="label">${certificate_id_number_title}</span> + <span class="value">${certificate_id_number}</span> + </li> + <li class="item certificate--date"> + <span class="label">${certificate_date_issued_title}</span> + <span class="value">${certificate_date_issued}</span> + </li> + </ul> + </div> + </article> + </section> + <hr class="divider /"> + <aside class="content content--supplemental" role="complimentary" id="validation-info"> + <div class="supplemental__about"> + <h2 class="title">${company_about_title}</h2> + <div class="copy"> + <p>${company_about_description}</p> + </div> + <ul class="list list--actions"> + <li class="item item--action"> + <a class="action action--primary action--moreinfo" href="${company_about_url}">${company_about_urltext}</a> + </li> + </ul> + </div> + <div class="supplemental__how"> + <h2 class="title">${certificate_verify_title}</h2> + <div class="copy"> + <p>${certificate_verify_description}</p> + </div> + <ul class="list list--actions"> + <li class="item item--action"> + <a class="action action--moreinfo" href="${certificate_verify_url}">${certificate_verify_urltext}</a> + </li> + </ul> + </div> + <div class="supplemental__certificates"> + <h2 class="title">${certificate_info_title}</h2> + <div class="copy"> + <p>${certificate_info_description}</p> + </div> + </div> + </aside> + </div> + <hr class="divider /"> + <div class="wrapper--footer"> + <footer class="footer--app" role="contentinfo" id="company-info"> + <div class="copyright"> + <p>${copyright_text}</p> + </div> + <nav class="nav--footer"> + <ul class="list list--legal"> + <li class="nav__item"> + <a class="action" href="${company_tos_url}">${company_tos_urltext}</a> + </li> + <li class="nav__item"> + <a class="action" href="${company_privacy_url}">${company_privacy_urltext}</a> + </li> + </ul> + <ul class="list list--actions"> + <li class="nav__item"> + <a class="action" href="${company_courselist_url}">${company_courselist_urltext}</a> + </li> + <li class="nav__item"> + <a class="action" href="${company_careers_url}">${company_careers_urltext}</a> + </li> + <li class="nav__item"> + <a class="action" href="${company_contact_url}">${company_contact_urltext}</a> + </li> + </ul> + </nav> + </footer> + </div> + </div> + <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script> + <script>window.jQuery || document.write('<script src="/v2/static/jsjs/vendor/jquery-1.10.2.min.js"><\/script>')</script> +</body> +</html> diff --git a/lms/urls.py b/lms/urls.py index cf423d1e65d6dd6d4307e693c3330a64e2fee59c..844e0d249dd5489b9807ca4efb5daef0189115ce 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -598,6 +598,11 @@ if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'): url(r'^login_oauth_token/(?P<backend>[^/]+)/$', 'student.views.login_oauth_token'), ) +# Certificates Web/HTML View +if settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False): + urlpatterns += ( + url(r'^certificates/html', 'certificates.views.render_html_view', name='cert_html_view'), + ) urlpatterns = patterns(*urlpatterns)