diff --git a/common/djangoapps/util/organizations_helpers.py b/common/djangoapps/util/organizations_helpers.py index ce28d19396..b3a9301cd6 100644 --- a/common/djangoapps/util/organizations_helpers.py +++ b/common/djangoapps/util/organizations_helpers.py @@ -4,6 +4,7 @@ Utility library for working with the edx-organizations app """ from django.conf import settings +from django.db.utils import DatabaseError def add_organization(organization_data): @@ -43,7 +44,15 @@ def get_organizations(): if not settings.FEATURES.get('ORGANIZATIONS_APP', False): return [] from organizations import api as organizations_api - return organizations_api.get_organizations() + # Due to the way unit tests run for edx-platform, models are not yet available at the time + # of Django admin form instantiation. This unfortunately results in an invocation of the following + # workflow, because the test configuration is (correctly) configured to exercise the application + # The good news is that this case does not manifest in the Real World, because migrations have + # been run ahead of application instantiation and the flag set only when that is truly the case. + try: + return organizations_api.get_organizations() + except DatabaseError: + return [] def get_organization_courses(organization_id): diff --git a/lms/djangoapps/certificates/admin.py b/lms/djangoapps/certificates/admin.py index eb1c29a7f9..46f8b037a0 100644 --- a/lms/djangoapps/certificates/admin.py +++ b/lms/djangoapps/certificates/admin.py @@ -2,12 +2,49 @@ django admin pages for certificates models """ from django.contrib import admin +from django import forms from config_models.admin import ConfigurationModelAdmin +from util.organizations_helpers import get_organizations from certificates.models import ( - CertificateGenerationConfiguration, CertificateHtmlViewConfiguration, BadgeImageConfiguration + CertificateGenerationConfiguration, + CertificateHtmlViewConfiguration, + BadgeImageConfiguration, + CertificateTemplate, + CertificateTemplateAsset, ) +class CertificateTemplateForm(forms.ModelForm): + """ + Django admin form for CertificateTemplate model + """ + organizations = get_organizations() + org_choices = [(org["id"], org["name"]) for org in organizations] + org_choices.insert(0, ('', 'None')) + organization_id = forms.TypedChoiceField(choices=org_choices, required=False, coerce=int, empty_value=None) + + class Meta(object): + """ Meta definitions for CertificateTemplateForm """ + model = CertificateTemplate + + +class CertificateTemplateAdmin(admin.ModelAdmin): + """ + Django admin customizations for CertificateTemplate model + """ + list_display = ('name', 'description', 'organization_id', 'course_key', 'mode', 'is_active') + form = CertificateTemplateForm + + +class CertificateTemplateAssetAdmin(admin.ModelAdmin): + """ + Django admin customizations for CertificateTemplateAsset model + """ + list_display = ('description', '__unicode__') + + admin.site.register(CertificateGenerationConfiguration) admin.site.register(CertificateHtmlViewConfiguration, ConfigurationModelAdmin) admin.site.register(BadgeImageConfiguration) +admin.site.register(CertificateTemplate, CertificateTemplateAdmin) +admin.site.register(CertificateTemplateAsset, CertificateTemplateAssetAdmin) diff --git a/lms/djangoapps/certificates/api.py b/lms/djangoapps/certificates/api.py index 5b786a2f0b..181d808b46 100644 --- a/lms/djangoapps/certificates/api.py +++ b/lms/djangoapps/certificates/api.py @@ -14,6 +14,7 @@ from opaque_keys.edx.keys import CourseKey from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from xmodule.modulestore.django import modulestore +from util.organizations_helpers import get_course_organizations from certificates.models import ( CertificateStatuses, @@ -21,7 +22,8 @@ from certificates.models import ( CertificateGenerationCourseSetting, CertificateGenerationConfiguration, ExampleCertificateSet, - GeneratedCertificate + GeneratedCertificate, + CertificateTemplate, ) from certificates.queue import XQueueCertInterface @@ -373,6 +375,46 @@ def get_active_web_certificate(course, is_preview_mode=None): return None +def get_certificate_template(course_key, mode): + """ + Retrieves the custom certificate template based on course_key and mode. + """ + org_id, template = None, None + # fetch organization of the course + course_organization = get_course_organizations(course_key) + if course_organization: + org_id = course_organization[0]['id'] + + if org_id and mode: + template = CertificateTemplate.objects.filter( + organization_id=org_id, + course_key=course_key, + mode=mode, + is_active=True + ) + # if don't template find by org and mode + if not template and org_id and mode: + template = CertificateTemplate.objects.filter( + organization_id=org_id, + mode=mode, + is_active=True + ) + # if don't template find by only org + if not template and org_id: + template = CertificateTemplate.objects.filter( + organization_id=org_id, + is_active=True + ) + # if we still don't template find by only course mode + if not template and mode: + template = CertificateTemplate.objects.filter( + mode=mode, + is_active=True + ) + + return template[0].template if template else None + + def emit_certificate_event(event_name, user, course_id, course=None, event_data=None): """ Emits certificate event. diff --git a/lms/djangoapps/certificates/migrations/0023_auto__del_unique_badgeassertion_course_id_user__add_unique_badgeassert.py b/lms/djangoapps/certificates/migrations/0023_auto__del_unique_badgeassertion_course_id_user__add_unique_badgeassert.py new file mode 100644 index 0000000000..66035e395c --- /dev/null +++ b/lms/djangoapps/certificates/migrations/0023_auto__del_unique_badgeassertion_course_id_user__add_unique_badgeassert.py @@ -0,0 +1,150 @@ +# -*- 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): + # Removing unique constraint on 'BadgeAssertion', fields ['course_id', 'user'] + db.delete_unique('certificates_badgeassertion', ['course_id', 'user_id']) + + # Adding unique constraint on 'BadgeAssertion', fields ['course_id', 'user', 'mode'] + db.create_unique('certificates_badgeassertion', ['course_id', 'user_id', 'mode']) + + + def backwards(self, orm): + # Removing unique constraint on 'BadgeAssertion', fields ['course_id', 'user', 'mode'] + db.delete_unique('certificates_badgeassertion', ['course_id', 'user_id', 'mode']) + + # Adding unique constraint on 'BadgeAssertion', fields ['course_id', 'user'] + db.create_unique('certificates_badgeassertion', ['course_id', 'user_id']) + + + 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.badgeassertion': { + 'Meta': {'unique_together': "(('course_id', 'user', 'mode'),)", 'object_name': 'BadgeAssertion'}, + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'default': 'None', 'max_length': '255', 'blank': 'True'}), + 'data': ('django.db.models.fields.TextField', [], {'default': "'{}'"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mode': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'certificates.badgeimageconfiguration': { + 'Meta': {'object_name': 'BadgeImageConfiguration'}, + 'default': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'icon': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mode': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '125'}) + }, + 'certificates.certificategenerationconfiguration': { + 'Meta': {'ordering': "('-change_date',)", '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': {'ordering': "('-change_date',)", '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': "'25c5af67da3d47039aa8b00b3a929fa9'", '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': "'88190407a2f14c429a7b5336e3fb0189'", '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'] \ No newline at end of file diff --git a/lms/djangoapps/certificates/migrations/0024_auto__add_certificatetemplate__add_unique_certificatetemplate_organiza.py b/lms/djangoapps/certificates/migrations/0024_auto__add_certificatetemplate__add_unique_certificatetemplate_organiza.py new file mode 100644 index 0000000000..006342f6e7 --- /dev/null +++ b/lms/djangoapps/certificates/migrations/0024_auto__add_certificatetemplate__add_unique_certificatetemplate_organiza.py @@ -0,0 +1,197 @@ +# -*- 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 model 'CertificateTemplate' + db.create_table('certificates_certificatetemplate', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('created', self.gf('model_utils.fields.AutoCreatedField')(default=datetime.datetime.now)), + ('modified', self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('description', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)), + ('template', self.gf('django.db.models.fields.TextField')()), + ('organization_id', self.gf('django.db.models.fields.IntegerField')(db_index=True, null=True, blank=True)), + ('course_key', self.gf('xmodule_django.models.CourseKeyField')(db_index=True, max_length=255, null=True, blank=True)), + ('mode', self.gf('django.db.models.fields.CharField')(default='honor', max_length=125, null=True, blank=True)), + ('is_active', self.gf('django.db.models.fields.BooleanField')(default=False)), + )) + db.send_create_signal('certificates', ['CertificateTemplate']) + + # Adding unique constraint on 'CertificateTemplate', fields ['organization_id', 'course_key', 'mode'] + db.create_unique('certificates_certificatetemplate', ['organization_id', 'course_key', 'mode']) + + # Adding model 'CertificateTemplateAsset' + db.create_table('certificates_certificatetemplateasset', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('created', self.gf('model_utils.fields.AutoCreatedField')(default=datetime.datetime.now)), + ('modified', self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now)), + ('description', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)), + ('asset', self.gf('django.db.models.fields.files.FileField')(max_length=255)), + )) + db.send_create_signal('certificates', ['CertificateTemplateAsset']) + + + def backwards(self, orm): + # Removing unique constraint on 'CertificateTemplate', fields ['organization_id', 'course_key', 'mode'] + db.delete_unique('certificates_certificatetemplate', ['organization_id', 'course_key', 'mode']) + + # Deleting model 'CertificateTemplate' + db.delete_table('certificates_certificatetemplate') + + # Deleting model 'CertificateTemplateAsset' + db.delete_table('certificates_certificatetemplateasset') + + + 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.badgeassertion': { + 'Meta': {'unique_together': "(('course_id', 'user', 'mode'),)", 'object_name': 'BadgeAssertion'}, + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'default': 'None', 'max_length': '255', 'blank': 'True'}), + 'data': ('django.db.models.fields.TextField', [], {'default': "'{}'"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mode': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'certificates.badgeimageconfiguration': { + 'Meta': {'object_name': 'BadgeImageConfiguration'}, + 'default': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'icon': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mode': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '125'}) + }, + 'certificates.certificategenerationconfiguration': { + 'Meta': {'ordering': "('-change_date',)", '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': {'ordering': "('-change_date',)", '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.certificatetemplate': { + 'Meta': {'unique_together': "(('organization_id', 'course_key', 'mode'),)", 'object_name': 'CertificateTemplate'}, + 'course_key': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '125', 'null': 'True', 'blank': 'True'}), + 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'organization_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'template': ('django.db.models.fields.TextField', [], {}) + }, + 'certificates.certificatetemplateasset': { + 'Meta': {'object_name': 'CertificateTemplateAsset'}, + 'asset': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}), + 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}) + }, + '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': "'f14d7721cd154a57a4fb52b9d4b4bc75'", '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': "'789810b9a54b4dd5bae3feec5b4e9fdb'", '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'] \ No newline at end of file diff --git a/lms/djangoapps/certificates/models.py b/lms/djangoapps/certificates/models.py index ab105327c4..b28b2bcef9 100644 --- a/lms/djangoapps/certificates/models.py +++ b/lms/djangoapps/certificates/models.py @@ -674,6 +674,88 @@ class BadgeImageConfiguration(models.Model): return cls.objects.get(default=True).icon +class CertificateTemplate(TimeStampedModel): + """A set of custom web certificate templates. + + Web certificate templates are Django web templates + to replace PDF certificate. + + A particular course may have several kinds of certificate templates + (e.g. honor and verified). + + """ + name = models.CharField( + max_length=255, + help_text=_(u'Name of template.'), + ) + description = models.CharField( + max_length=255, + null=True, + blank=True, + help_text=_(u'Description and/or admin notes.'), + ) + template = models.TextField( + help_text=_(u'Django template HTML.'), + ) + organization_id = models.IntegerField( + null=True, + blank=True, + db_index=True, + help_text=_(u'Organization of template.'), + ) + course_key = CourseKeyField( + max_length=255, + null=True, + blank=True, + db_index=True, + ) + mode = models.CharField( + max_length=125, + choices=GeneratedCertificate.MODES, + default=GeneratedCertificate.MODES.honor, + null=True, + blank=True, + help_text=_(u'The course mode for this template.'), + ) + is_active = models.BooleanField( + help_text=_(u'On/Off switch.'), + default=False, + ) + + def __unicode__(self): + return u'%s' % (self.name, ) + + class Meta(object): # pylint: disable=missing-docstring + get_latest_by = 'created' + unique_together = (('organization_id', 'course_key', 'mode'),) + + +class CertificateTemplateAsset(TimeStampedModel): + """A set of assets to be used in custom web certificate templates. + + This model stores assets used in custom web certificate templates + such as image, css files. + + """ + description = models.CharField( + max_length=255, + null=True, + blank=True, + help_text=_(u'Description of the asset.'), + ) + asset = models.FileField( + max_length=255, + upload_to='certificate_template_assets', + help_text=_(u'Asset file. It could be an image or css file.'), + ) + + def __unicode__(self): + return u'%s' % (self.asset.url, ) # pylint: disable=no-member + + class Meta(object): # pylint: disable=missing-docstring + get_latest_by = 'created' + + @receiver(post_save, sender=GeneratedCertificate) #pylint: disable=unused-argument def create_badge(sender, instance, **kwargs): diff --git a/lms/djangoapps/certificates/tests/test_views.py b/lms/djangoapps/certificates/tests/test_views.py index 46c0a5cc29..d4b56143c5 100644 --- a/lms/djangoapps/certificates/tests/test_views.py +++ b/lms/djangoapps/certificates/tests/test_views.py @@ -30,6 +30,7 @@ from certificates.models import ( CertificateStatuses, CertificateHtmlViewConfiguration, CertificateSocialNetworks, + CertificateTemplate, ) from certificates.tests.factories import ( @@ -45,6 +46,11 @@ FEATURES_WITH_CERTS_ENABLED['CERTIFICATES_HTML_VIEW'] = True FEATURES_WITH_CERTS_DISABLED = settings.FEATURES.copy() FEATURES_WITH_CERTS_DISABLED['CERTIFICATES_HTML_VIEW'] = False +FEATURES_WITH_CUSTOM_CERTS_ENABLED = { + "CUSTOM_CERTIFICATE_TEMPLATES_ENABLED": True +} +FEATURES_WITH_CUSTOM_CERTS_ENABLED.update(FEATURES_WITH_CERTS_ENABLED) + @attr('shard_1') @ddt.ddt @@ -427,6 +433,30 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase): self.course.save() self.store.update_item(self.course, self.user.id) + def _create_custom_template(self, org_id=None, mode=None, course_key=None): + """ + Creates a custom certificate template entry in DB. + """ + template_html = """ + + + lang: ${LANGUAGE_CODE} + course name: ${accomplishment_copy_course_name} + mode: ${course_mode} + ${accomplishment_copy_course_description} + + + """ + template = CertificateTemplate( + name='custom template', + template=template_html, + organization_id=org_id, + course_key=course_key, + mode=mode, + is_active=True + ) + template.save() + @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) def test_rendering_course_organization_data(self): """ @@ -724,6 +754,75 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase): response_json = json.loads(response.content) self.assertEqual(CertificateStatuses.generating, response_json['add_status']) + @override_settings(FEATURES=FEATURES_WITH_CUSTOM_CERTS_ENABLED) + @override_settings(LANGUAGE_CODE='fr') + def test_certificate_custom_template_with_org_mode_course(self): + """ + Tests custom template search and rendering. + """ + self._add_course_certificates(count=1, signatory_count=2) + self._create_custom_template(1, mode='honor', course_key=unicode(self.course.id)) + self._create_custom_template(2, mode='honor') + test_url = get_certificate_url( + user_id=self.user.id, + course_id=unicode(self.course.id) + ) + + with patch('certificates.api.get_course_organizations') as mock_get_orgs: + mock_get_orgs.side_effect = [ + [{"id": 1, "name": "organization name"}], + [{"id": 2, "name": "organization name 2"}], + ] + response = self.client.get(test_url) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'lang: fr') + self.assertContains(response, 'course name: {}'.format(self.course.display_name)) + # test with second organization template + response = self.client.get(test_url) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'lang: fr') + self.assertContains(response, 'course name: {}'.format(self.course.display_name)) + + @override_settings(FEATURES=FEATURES_WITH_CUSTOM_CERTS_ENABLED) + def test_certificate_custom_template_with_org(self): + """ + Tests custom template search if if have a single template for all courses of organization. + """ + self._add_course_certificates(count=1, signatory_count=2) + self._create_custom_template(1) + self._create_custom_template(1, mode='honor') + test_url = get_certificate_url( + user_id=self.user.id, + course_id=unicode(self.course.id) + ) + + with patch('certificates.api.get_course_organizations') as mock_get_orgs: + mock_get_orgs.side_effect = [ + [{"id": 1, "name": "organization name"}], + ] + response = self.client.get(test_url) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'course name: {}'.format(self.course.display_name)) + + @override_settings(FEATURES=FEATURES_WITH_CUSTOM_CERTS_ENABLED) + def test_certificate_custom_template_with_course_mode(self): + """ + Tests custom template search if if have a single template for a course mode. + """ + mode = 'honor' + self._add_course_certificates(count=1, signatory_count=2) + self._create_custom_template(mode=mode) + test_url = get_certificate_url( + user_id=self.user.id, + course_id=unicode(self.course.id) + ) + + with patch('certificates.api.get_course_organizations') as mock_get_orgs: + mock_get_orgs.return_value = [] + response = self.client.get(test_url) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'mode: {}'.format(mode)) + class TrackShareRedirectTest(UrlResetMixin, ModuleStoreTestCase, EventTrackingTestCase): """ diff --git a/lms/djangoapps/certificates/views/webview.py b/lms/djangoapps/certificates/views/webview.py index 01303c5950..5650c686db 100644 --- a/lms/djangoapps/certificates/views/webview.py +++ b/lms/djangoapps/certificates/views/webview.py @@ -7,22 +7,27 @@ import logging from django.conf import settings from django.contrib.auth.models import User +from django.http import HttpResponse +from django.template import RequestContext from django.utils.translation import ugettext as _ +from courseware.courses import course_image_url +from edxmako.shortcuts import render_to_response +from edxmako.template import Template +from eventtracking import tracker +from microsite_configuration import microsite from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey -from microsite_configuration import microsite -from edxmako.shortcuts import render_to_response -from eventtracking import tracker -from xmodule.modulestore.django import modulestore from student.models import LinkedInAddToProfileConfiguration -from courseware.courses import course_image_url from util import organizations_helpers as organization_api +from xmodule.modulestore.django import modulestore + from certificates.api import ( get_active_web_certificate, get_certificate_url, emit_certificate_event, - has_html_certificates_enabled + has_html_certificates_enabled, + get_certificate_template ) from certificates.models import ( GeneratedCertificate, @@ -31,7 +36,6 @@ from certificates.models import ( BadgeAssertion ) - log = logging.getLogger(__name__) @@ -401,4 +405,11 @@ def render_html_view(request, user_id, course_id): context.update(course.cert_html_view_overrides) # FINALLY, generate and send the output the client + if settings.FEATURES.get('CUSTOM_CERTIFICATE_TEMPLATES_ENABLED', False): + custom_template = get_certificate_template(course_key, user_certificate.mode) + if custom_template: + template = Template(custom_template) + context = RequestContext(request, context) + return HttpResponse(template.render(context)) + return render_to_response("certificates/valid.html", context)