diff --git a/django_project/certification/admin.py b/django_project/certification/admin.py
index e753b304e..5287f084a 100644
--- a/django_project/certification/admin.py
+++ b/django_project/certification/admin.py
@@ -4,6 +4,7 @@
from django.contrib.gis import admin
from simple_history.admin import SimpleHistoryAdmin
from certification.models.certificate import Certificate
+from certification.models.certificate_type import CertificateType
from certification.models.course import Course
from certification.models.training_center import TrainingCenter
from certification.models.course_convener import CourseConvener
@@ -34,6 +35,15 @@ def queryset(self, request):
return query_set
+class CertificateTypeAdmin(admin.ModelAdmin):
+ """CertificateType admin model."""
+
+ list_display = ('name', 'wording', 'order')
+ list_editable = ('order', )
+ search_fields = ('name', 'wording')
+ ordering = ('order', )
+
+
class AttendeeAdmin(admin.ModelAdmin):
"""Attendee admin model."""
list_display = ('firstname', 'surname', 'email', 'certifying_organisation')
@@ -163,6 +173,7 @@ class StatusAdmin(admin.ModelAdmin):
admin.site.register(Certificate, CertificateAdmin)
+admin.site.register(CertificateType, CertificateTypeAdmin)
admin.site.register(Attendee, AttendeeAdmin)
admin.site.register(Course, CourseAdmin)
admin.site.register(CourseType, CourseTypeAdmin)
diff --git a/django_project/certification/forms.py b/django_project/certification/forms.py
index 6c3249e38..b900798b2 100644
--- a/django_project/certification/forms.py
+++ b/django_project/certification/forms.py
@@ -21,6 +21,7 @@
)
from .models import (
CertifyingOrganisation,
+ CertificateType,
CourseConvener,
CourseType,
TrainingCenter,
@@ -305,6 +306,7 @@ class Meta:
'end_date',
'template_certificate',
'certifying_organisation',
+ 'certificate_type',
)
def __init__(self, *args, **kwargs):
@@ -324,6 +326,7 @@ def __init__(self, *args, **kwargs):
Field('start_date', css_class='form-control'),
Field('end_date', css_class='form-control'),
Field('template_certificate', css_class='form-control'),
+ Field('certificate_type', css_class='form-control'),
)
)
self.helper.layout = layout
@@ -345,6 +348,10 @@ def __init__(self, *args, **kwargs):
self.certifying_organisation
self.fields['certifying_organisation'].widget = forms.HiddenInput()
self.helper.add_input(Submit('submit', 'Submit'))
+ self.fields['certificate_type'].queryset = \
+ CertificateType.objects.filter(
+ projectcertificatetype__project=
+ self.certifying_organisation.project)
def save(self, commit=True):
instance = super(CourseForm, self).save(commit=False)
diff --git a/django_project/certification/migrations/0007_certificatetype.py b/django_project/certification/migrations/0007_certificatetype.py
new file mode 100644
index 000000000..9434d8235
--- /dev/null
+++ b/django_project/certification/migrations/0007_certificatetype.py
@@ -0,0 +1,34 @@
+# Generated by Django 2.2.18 on 2021-12-03 00:56
+
+from django.db import migrations, models
+
+
+def add_certification_type_as_existing_value(apps, schema_editor):
+ CertificateType = apps.get_model('certification', 'CertificateType')
+ CertificateType.objects.create(
+ name='attendance and completion',
+ wording='Has attended and completed the course:',
+ order=0
+ )
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('certification', '0006_auto_20210730_0615'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='CertificateType',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(help_text='Certificate type.', max_length=200, unique=True)),
+ ('description', models.TextField(blank=True, help_text='Certificate type description - 1000 characters limit.', max_length=1000, null=True)),
+ ('wording', models.CharField(help_text='Wording that will be placed on certificate. e.g. "Has attended and completed".', max_length=500)),
+ ('order', models.IntegerField(blank=True, null=True, unique=True)),
+ ],
+ ),
+
+ migrations.RunPython(add_certification_type_as_existing_value, reverse_code=migrations.RunPython.noop),
+ ]
diff --git a/django_project/certification/migrations/0008_projectcertificatetype.py b/django_project/certification/migrations/0008_projectcertificatetype.py
new file mode 100644
index 000000000..b13a5b41d
--- /dev/null
+++ b/django_project/certification/migrations/0008_projectcertificatetype.py
@@ -0,0 +1,38 @@
+# Generated by Django 2.2.18 on 2021-12-10 02:23
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+def create_existing_project_certificate_type(apps, schema_editor):
+ CertificateType = apps.get_model('certification', 'CertificateType')
+ ProjectCertificateType = apps.get_model('certification', 'ProjectCertificateType')
+ Project = apps.get_model('base', 'Project')
+ certificate_type = CertificateType.objects.filter(
+ name='attendance and completion').first()
+ projects = Project.objects.all()
+
+ for project in projects:
+ ProjectCertificateType.objects.create(
+ project=project,
+ certificate_type=certificate_type
+ )
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('base', '0006_auto_20210308_0244'),
+ ('certification', '0007_certificatetype'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ProjectCertificateType',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('certificate_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='certification.CertificateType')),
+ ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='base.Project')),
+ ],
+ ),
+
+ migrations.RunPython(create_existing_project_certificate_type, reverse_code=migrations.RunPython.noop),
+ ]
diff --git a/django_project/certification/migrations/0009_course_certificate_type.py b/django_project/certification/migrations/0009_course_certificate_type.py
new file mode 100644
index 000000000..d44f9e233
--- /dev/null
+++ b/django_project/certification/migrations/0009_course_certificate_type.py
@@ -0,0 +1,41 @@
+# Generated by Django 2.2.18 on 2021-12-10 08:31
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+def set_existing_certificate_type_value(apps, shcema_editor):
+ CertificateType = apps.get_model('certification', 'CertificateType')
+ Course = apps.get_model('certification', 'Course')
+ certificate_type = CertificateType.objects.filter(
+ name='attendance and completion').first()
+ courses = Course.objects.all()
+
+ for course in courses:
+ course.certificate_type = certificate_type
+ course.save(update_fields=['certificate_type'])
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('certification', '0008_projectcertificatetype'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='course',
+ name='certificate_type',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='certification.CertificateType'),
+ ),
+
+ migrations.RunPython(set_existing_certificate_type_value, reverse_code=migrations.RunPython.noop),
+
+ migrations.AlterField(
+ model_name='course',
+ name='certificate_type',
+ field=models.ForeignKey(null=False, on_delete=django.db.models.deletion.PROTECT,
+ to='certification.CertificateType'),
+ preserve_default=False,
+ ),
+
+ ]
diff --git a/django_project/certification/migrations/0010_merge_20220212_0417.py b/django_project/certification/migrations/0010_merge_20220212_0417.py
new file mode 100644
index 000000000..3b08d2086
--- /dev/null
+++ b/django_project/certification/migrations/0010_merge_20220212_0417.py
@@ -0,0 +1,14 @@
+# Generated by Django 2.2.18 on 2022-02-12 02:17
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('certification', '0007_courseconvener_is_active'),
+ ('certification', '0009_course_certificate_type'),
+ ]
+
+ operations = [
+ ]
diff --git a/django_project/certification/models/__init__.py b/django_project/certification/models/__init__.py
index 10e9bf2df..79c82dd0c 100644
--- a/django_project/certification/models/__init__.py
+++ b/django_project/certification/models/__init__.py
@@ -10,5 +10,6 @@
from certification.models.course_attendee import *
from certification.models.course_type import *
from certification.models.course_convener import *
+from certification.models.certificate_type import *
from certification.models.certificate import *
from certification.models.organisation_certificate import *
diff --git a/django_project/certification/models/certificate_type.py b/django_project/certification/models/certificate_type.py
new file mode 100644
index 000000000..8647a4014
--- /dev/null
+++ b/django_project/certification/models/certificate_type.py
@@ -0,0 +1,55 @@
+"""Certificate type model for certification app"""
+
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+
+from base.models.project import Project
+
+
+class CertificateType(models.Model):
+ name = models.CharField(
+ help_text=_('Certificate type.'),
+ max_length=200,
+ null=False,
+ blank=False,
+ unique=True,
+ )
+
+ description = models.TextField(
+ help_text=_('Certificate type description - 1000 characters limit.'),
+ max_length=1000,
+ null=True,
+ blank=True,
+ )
+
+ wording = models.CharField(
+ help_text=_(
+ 'Wording that will be placed on certificate. '
+ 'e.g. "Has attended and completed".'
+ ),
+ max_length=500,
+ null=False,
+ blank=False
+ )
+
+ order = models.IntegerField(
+ blank=True,
+ null=True,
+ unique=True
+ )
+
+ def __str__(self):
+ return self.name
+
+
+class ProjectCertificateType(models.Model):
+ """A model to store a certificate type linked to a project"""
+
+ project = models.ForeignKey(
+ Project,
+ on_delete=models.CASCADE
+ )
+ certificate_type = models.ForeignKey(
+ CertificateType,
+ on_delete=models.CASCADE
+ )
diff --git a/django_project/certification/models/course.py b/django_project/certification/models/course.py
index a845538e3..339aa3306 100644
--- a/django_project/certification/models/course.py
+++ b/django_project/certification/models/course.py
@@ -20,6 +20,7 @@
from .course_type import CourseType
from certification.utilities import check_slug
from .training_center import TrainingCenter
+from certification.models.certificate_type import CertificateType
logger = logging.getLogger(__name__)
@@ -86,6 +87,8 @@ class Course(models.Model):
on_delete=models.CASCADE)
certifying_organisation = models.ForeignKey(CertifyingOrganisation,
on_delete=models.CASCADE)
+ certificate_type = models.ForeignKey(
+ CertificateType, on_delete=models.PROTECT, null=True)
author = models.ForeignKey(User, on_delete=models.CASCADE)
objects = models.Manager()
diff --git a/django_project/certification/templates/certificate_type/list.html b/django_project/certification/templates/certificate_type/list.html
new file mode 100644
index 000000000..d742cbea9
--- /dev/null
+++ b/django_project/certification/templates/certificate_type/list.html
@@ -0,0 +1,43 @@
+{% extends "project_base.html" %}
+
+{% block extra_js %}
+{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
+ Certificate Type |
+ Wording |
+ Apply |
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/django_project/certification/templates/course/create.html b/django_project/certification/templates/course/create.html
index bc5db0f90..0fb35efda 100644
--- a/django_project/certification/templates/course/create.html
+++ b/django_project/certification/templates/course/create.html
@@ -99,6 +99,7 @@ New Course for {{ organisation.name }}
+
@@ -130,6 +131,9 @@ New Course for {{ organisation.name }}
}else if($('input[id=id_end_date]').val() === ''){
$('#error-submit').html('Please choose end date.');
return false
+ }else if($('select[id=id_certificate_type]').val() === ''){
+ $('#error-submit').html('Please choose certificate type.');
+ return false
}
$('#preview-form input[name=course_convener]').val($('select[name=course_convener]').val());
@@ -138,6 +142,7 @@ New Course for {{ organisation.name }}
$('#preview-form input[name=start_date]').val($('input[id=id_start_date]').val());
$('#preview-form input[name=end_date]').val($('input[id=id_end_date]').val());
$('#preview-form input[name=trained_competence]').val($('input[id=id_trained_competence]').val());
+ $('#preview-form input[name=certificate_type]').val($('select[id=id_certificate_type]').val());
}
//check if browser supports file api and filereader features
diff --git a/django_project/certification/templates/course/update.html b/django_project/certification/templates/course/update.html
index b83e1ecb4..5cc923f05 100644
--- a/django_project/certification/templates/course/update.html
+++ b/django_project/certification/templates/course/update.html
@@ -106,6 +106,7 @@ Update Course for {{ organisation.name }}
+
@@ -138,6 +139,9 @@ Update Course for {{ organisation.name }}
}else if($('input[id=id_end_date]').val() === ''){
$('#error-submit').html('Please choose end date.');
return false
+ }else if($('select[id=id_certificate_type]').val() === ''){
+ $('#error-submit').html('Please choose certificate type.');
+ return false
}
$('#preview-form input[name=course_convener]').val($('select[name=course_convener]').val());
@@ -146,6 +150,7 @@ Update Course for {{ organisation.name }}
$('#preview-form input[name=start_date]').val($('input[id=id_start_date]').val());
$('#preview-form input[name=end_date]').val($('input[id=id_end_date]').val());
$('#preview-form input[name=trained_competence]').val($('input[id=id_trained_competence]').val());
+ $('#preview-form input[name=certificate_type]').val($('select[id=id_certificate_type]').val());
}
//check if browser supports file api and filereader features
diff --git a/django_project/certification/tests/model_factories.py b/django_project/certification/tests/model_factories.py
index 557b61797..96a480403 100644
--- a/django_project/certification/tests/model_factories.py
+++ b/django_project/certification/tests/model_factories.py
@@ -5,6 +5,8 @@
from certification.models import (
Certificate,
+ CertificateType,
+ ProjectCertificateType,
Attendee,
Course,
CourseType,
@@ -81,6 +83,19 @@ class Meta:
author = factory.SubFactory(UserF)
+class CertificateTypeF(factory.django.DjangoModelFactory):
+ """CertificateType model factory."""
+
+ class Meta:
+ model = CertificateType
+
+ name = factory.sequence(lambda n: 'Test certificate type name %s' % n)
+ description = factory.sequence(
+ lambda n: 'Description certificate type %s' % n)
+ wording = factory.sequence(
+ lambda n: 'Wording certificate type %s' % n)
+
+
class CourseF(factory.django.DjangoModelFactory):
"""Course model factory."""
@@ -97,6 +112,7 @@ class Meta:
course_type = factory.SubFactory(CourseTypeF)
training_center = factory.SubFactory(TrainingCenterF)
author = factory.SubFactory(UserF)
+ certificate_type = factory.SubFactory(CertificateTypeF)
class AttendeeF(factory.django.DjangoModelFactory):
@@ -124,6 +140,16 @@ class Meta:
attendee = factory.SubFactory(AttendeeF)
+class ProjectCertificateTypeF(factory.django.DjangoModelFactory):
+ """ProjectCertificateType model factory."""
+
+ class Meta:
+ model = ProjectCertificateType
+
+ project = factory.SubFactory(ProjectF)
+ certificate_type = factory.SubFactory(CertificateTypeF)
+
+
class CertificateF(factory.django.DjangoModelFactory):
"""Certificate model factory."""
diff --git a/django_project/certification/tests/test_models.py b/django_project/certification/tests/test_models.py
index 938004fcd..f01f3d4e9 100644
--- a/django_project/certification/tests/test_models.py
+++ b/django_project/certification/tests/test_models.py
@@ -1,10 +1,12 @@
# coding=utf-8
"""Test for models."""
+from django.db.utils import IntegrityError
from django.core.exceptions import ValidationError
from django.test import TestCase
from certification.tests.model_factories import (
CertificateF,
+ CertificateTypeF,
AttendeeF,
CourseF,
CourseTypeF,
@@ -14,6 +16,14 @@
CourseAttendeeF,
StatusF
)
+from certification.models.certificate_type import CertificateType
+
+
+class SetUpMixin:
+ def setUp(self):
+ """Set up before each test."""
+ # Delete CertificateType created from migration 0007_certificate_type
+ CertificateType.objects.all().delete()
class TestCertifyingOrganisation(TestCase):
@@ -95,17 +105,11 @@ def test_Certifying_Organisation_update(self):
self.assertEqual(model.__dict__.get(key), val)
-class TestCertificate(TestCase):
+class CertificateSetUp(SetUpMixin, TestCase):
"""Test certificate model."""
- def setUp(self):
- """Set up before test."""
-
- pass
-
def test_Certificate_create(self):
"""Test certificate model creation."""
-
model = CertificateF.create()
# check if PK exists.
@@ -121,6 +125,65 @@ def test_Certificate_delete(self):
self.assertTrue(model.pk is None)
+
+class CertificateTypeSetUp(SetUpMixin, TestCase):
+ """Test Certificate models."""
+
+ def test_CRUD_CertificateType(self):
+ # initial
+ self.assertEqual(CertificateType.objects.all().count(), 0)
+
+ # create model
+ model = CertificateTypeF.create()
+ self.assertEqual(CertificateType.objects.all().count(), 1)
+
+ # read model
+ self.assertIsNotNone(model.id)
+ self.assertIn('Test certificate type name', model.name)
+ self.assertIn('Description certificate type', model.description)
+ self.assertIn('Wording certificate type', model.wording)
+ self.assertEqual(model.__str__(), model.name)
+
+ #
+ model.name = 'Update certificate type name'
+ model.save()
+ self.assertEqual(model.name, 'Update certificate type name')
+
+ model.delete()
+ self.assertIsNone(model.id)
+ self.assertEqual(CertificateType.objects.all().count(), 0)
+
+ def test_name_field_must_be_unique(self):
+ CertificateTypeF.create(name="We are twin")
+ msg = ('duplicate key value violates unique constraint '
+ '"certification_certificatetype_name_key"')
+ with self.assertRaisesMessage(IntegrityError, msg):
+ CertificateTypeF.create(name="We are twin")
+
+ def test_order_field_must_be_unique(self):
+ CertificateTypeF.create(order=1)
+ msg = ('duplicate key value violates unique constraint '
+ '"certification_certificatetype_order_key"')
+ with self.assertRaisesMessage(IntegrityError, msg):
+ CertificateTypeF.create(order=1)
+
+ def test_order_field_can_be_null(self):
+ model_1 = CertificateTypeF.create(order=1)
+ model_2 = CertificateTypeF.create(order=2)
+
+ self.assertEqual(model_1.order, 1)
+ self.assertEqual(model_2.order, 2)
+
+ model_1.order = None
+ model_1.save()
+
+ model_2.order = 1
+ model_2.save()
+
+ self.assertEqual(model_1.order, None)
+ self.assertEqual(model_2.order, 1)
+
+
class TestAttendee(TestCase):
"""Test attendee model."""
diff --git a/django_project/certification/tests/views/test_certificate_previews.py b/django_project/certification/tests/views/test_certificate_previews.py
index 7673b5acd..3faab7345 100644
--- a/django_project/certification/tests/views/test_certificate_previews.py
+++ b/django_project/certification/tests/views/test_certificate_previews.py
@@ -1,4 +1,5 @@
# coding=utf-8
+from unittest.mock import patch
from django.urls import reverse
from django.test import TestCase, override_settings
from django.test.client import Client
@@ -9,7 +10,8 @@
CertifyingOrganisationF,
CourseConvenerF,
TrainingCenterF,
- CourseTypeF
+ CourseTypeF,
+ CertificateTypeF
)
@@ -46,6 +48,7 @@ def setUp(self):
self.convener = CourseConvenerF.create()
self.training_center = TrainingCenterF.create()
self.course_type = CourseTypeF.create()
+ self.certificate_type = CertificateTypeF.create()
@override_settings(VALID_DOMAIN=['testserver', ])
def tearDown(self):
@@ -80,7 +83,8 @@ def test_preview_certificate_no_data_posted(self):
self.assertEqual(response.status_code, 200)
@override_settings(VALID_DOMAIN=['testserver', ])
- def test_preview_certificate_with_posted_data(self):
+ @patch('certification.views.certificate.generate_pdf')
+ def test_preview_certificate_with_posted_data(self, mock_gen_pdf):
client = Client(HTTP_HOST='testserver')
client.login(username='anita', password='password')
post_data = {
@@ -96,3 +100,61 @@ def test_preview_certificate_with_posted_data(self):
'organisation_slug': self.test_certifying_organisation.slug
}), post_data)
self.assertEqual(response.status_code, 200)
+ # Only 6 args in generate_pdf call,
+ # Since there's no CertificateType id in POST body
+ self.assertEqual(len(mock_gen_pdf.call_args[0]), 6)
+ self.assertIn(self.test_project, mock_gen_pdf.call_args[0])
+ self.assertNotIn(
+ self.certificate_type.wording, mock_gen_pdf.call_args[0])
+
+ @override_settings(VALID_DOMAIN=['testserver', ])
+ @patch('certification.views.certificate.generate_pdf')
+ def test_preview_certificate_with_posted_data_and_certificate_type(
+ self, mock_gen_pdf):
+ client = Client(HTTP_HOST='testserver')
+ client.login(username='anita', password='password')
+ post_data = {
+ 'course_convener': self.convener.pk,
+ 'training_center': self.training_center.pk,
+ 'course_type': self.course_type.pk,
+ 'start_date': '2018-01-01',
+ 'end_date': '2018-02-01',
+ 'template_certificate': '',
+ 'certificate_type': self.certificate_type.id
+ }
+ response = client.post(reverse('preview-certificate', kwargs={
+ 'project_slug': self.test_project.slug,
+ 'organisation_slug': self.test_certifying_organisation.slug
+ }), post_data)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(len(mock_gen_pdf.call_args[0]), 7)
+ self.assertIn(self.test_project, mock_gen_pdf.call_args[0])
+ self.assertIn(self.certificate_type.wording, mock_gen_pdf.call_args[0])
+
+
+ @override_settings(VALID_DOMAIN=['testserver', ])
+ @patch('certification.views.certificate.generate_pdf')
+ def test_preview_certificate_with_posted_data_cert_type_not_found(
+ self, mock_gen_pdf):
+ client = Client(HTTP_HOST='testserver')
+ client.login(username='anita', password='password')
+ post_data = {
+ 'course_convener': self.convener.pk,
+ 'training_center': self.training_center.pk,
+ 'course_type': self.course_type.pk,
+ 'start_date': '2018-01-01',
+ 'end_date': '2018-02-01',
+ 'template_certificate': '',
+ 'certificate_type': 99999
+ }
+ response = client.post(reverse('preview-certificate', kwargs={
+ 'project_slug': self.test_project.slug,
+ 'organisation_slug': self.test_certifying_organisation.slug
+ }), post_data)
+ self.assertEqual(response.status_code, 200)
+ # Only 6 args in generate_pdf call,
+ # Since there's the CertificateType id doesn't exist
+ self.assertEqual(len(mock_gen_pdf.call_args[0]), 6)
+ self.assertIn(self.test_project, mock_gen_pdf.call_args[0])
+ self.assertNotIn(
+ self.certificate_type.wording, mock_gen_pdf.call_args[0])
diff --git a/django_project/certification/tests/views/test_certificate_type_view.py b/django_project/certification/tests/views/test_certificate_type_view.py
new file mode 100644
index 000000000..61a32f6d4
--- /dev/null
+++ b/django_project/certification/tests/views/test_certificate_type_view.py
@@ -0,0 +1,70 @@
+from bs4 import BeautifulSoup as Soup
+from django.shortcuts import reverse
+from django.test import TestCase, override_settings
+
+from base.tests.model_factories import ProjectF
+from core.model_factories import UserF
+from certification.tests.model_factories import (
+ CertificateTypeF,
+ ProjectCertificateTypeF
+)
+
+
+class TestCertificateTypesView(TestCase):
+
+ def setUp(self):
+ self.project = ProjectF.create()
+ another_project = ProjectF.create()
+ self.certificate_type_1 = CertificateTypeF.create(name='type-1')
+ self.certificate_type_2 = CertificateTypeF.create(name='type-2')
+ ProjectCertificateTypeF.create(
+ project=self.project, certificate_type=self.certificate_type_1
+ )
+ ProjectCertificateTypeF.create(
+ project=another_project, certificate_type=self.certificate_type_2
+ )
+ self.user = UserF.create(**{
+ 'username': 'tester',
+ 'password': 'password',
+ 'is_staff': True,
+ })
+ self.user.set_password('password')
+ self.user.save()
+
+ @override_settings(VALID_DOMAIN=['testserver', ])
+ def test_certificate_type_view_contains_course_type(self):
+ """Test CertificateType list page."""
+
+ self.client.post('/set_language/', data={'language': 'en'})
+ self.client.login(username='tester', password='password')
+ url = reverse('certificate-type-list', kwargs={
+ 'project_slug': self.project.slug
+ })
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+
+ # all certificate types should be displayed
+ self.assertContains(response, self.certificate_type_1.name)
+ self.assertContains(response, self.certificate_type_2.name)
+
+ # only certificate types related to project in context_object ListView
+ self.assertEqual(len(response.context_data['object_list']), 1)
+ self.assertEqual(
+ response.context_data['object_list'].last().certificate_type,
+ self.certificate_type_1
+ )
+
+ @override_settings(VALID_DOMAIN=['testserver', ])
+ def test_update_project_certificate_view(self):
+ self.client.post('/set_language/', data={'language': 'en'})
+ self.client.login(username='tester', password='password')
+ url = reverse('certificate-type-update', kwargs={
+ 'project_slug': self.project.slug
+ })
+ # choose certificate type-2 only
+ post_data = {'certificate_types': 'type-2'}
+ response = self.client.post(url, data=post_data, follow=True)
+ self.assertEqual(response.status_code, 200)
+ soup = Soup(response.content, "html5lib")
+ self.assertTrue(len(soup.find_all('input', checked=True)) == 1)
+ self.assertEqual(soup.find('input', checked=True)["value"], "type-2")
diff --git a/django_project/certification/tests/views/test_course_view.py b/django_project/certification/tests/views/test_course_view.py
index 274bab933..8c27da104 100644
--- a/django_project/certification/tests/views/test_course_view.py
+++ b/django_project/certification/tests/views/test_course_view.py
@@ -1,5 +1,7 @@
# coding=utf-8
import logging
+from bs4 import BeautifulSoup as Soup
+
from django.test import TestCase, override_settings
from django.test.client import Client
from django.urls import reverse
@@ -7,6 +9,8 @@
ProjectF,
UserF,
CertifyingOrganisationF,
+ CertificateTypeF,
+ ProjectCertificateTypeF,
CourseF,
CourseConvenerF
)
@@ -42,6 +46,11 @@ def setUp(self):
self.course = CourseF.create(
certifying_organisation=self.certifying_organisation
)
+ self.certificate_type = CertificateTypeF.create()
+ self.project_cert_type = ProjectCertificateTypeF.create(
+ project=self.project,
+ certificate_type=self.certificate_type
+ )
@override_settings(VALID_DOMAIN=['testserver', ])
def tearDown(self):
@@ -56,6 +65,24 @@ def tearDown(self):
self.project.delete()
self.user.delete()
+ @override_settings(VALID_DOMAIN=['testserver', ])
+ def test_create_course_must_showing_CertificateTypes(self):
+ self.client.login(username='anita', password='password')
+ response = self.client.get(reverse('course-create', kwargs={
+ 'project_slug': self.project.slug,
+ 'organisation_slug': self.certifying_organisation.slug,
+ }))
+ self.assertEqual(response.status_code, 200)
+ soup = Soup(response.content, "html5lib")
+ cert_type_option = soup.find(
+ 'select',
+ {'id': 'id_certificate_type'}
+ ).find_all('option')
+ self.assertIn(
+ self.certificate_type.name,
+ [cert_type.text for cert_type in cert_type_option]
+ )
+
@override_settings(VALID_DOMAIN=['testserver', ])
def test_detail_view(self):
client = Client()
diff --git a/django_project/certification/urls.py b/django_project/certification/urls.py
index 13d912eb4..f3427961c 100644
--- a/django_project/certification/urls.py
+++ b/django_project/certification/urls.py
@@ -31,6 +31,10 @@
CourseDeleteView,
CourseDetailView,
+ # CourseType
+ ProjectCertificateTypeView,
+ updateProjectCertificateView,
+
# Training Center.
TrainingCenterCreateView,
TrainingCenterDetailView,
@@ -235,6 +239,15 @@
view=OrganisationCertificateDetailView.as_view(),
name='detail-certificate-organisation'),
+ # Certificate Type.
+ url(regex='^(?P[\w-]+)/certificate-types/$',
+ view=ProjectCertificateTypeView.as_view(),
+ name='certificate-type-list'),
+ url(regex='^(?P[\w-]+)/certificate-types/update/$',
+ view=updateProjectCertificateView,
+ name='certificate-type-update'),
+
+
# Certificate.
url(regex='^(?P[\w-]+)/certifyingorganisation/'
'(?P[\w-]+)/course/'
diff --git a/django_project/certification/views/__init__.py b/django_project/certification/views/__init__.py
index 032b91f9c..9c8acf31c 100644
--- a/django_project/certification/views/__init__.py
+++ b/django_project/certification/views/__init__.py
@@ -8,4 +8,5 @@
from .course_attendee import *
from .validate import *
from .certificate import *
+from .certificate_type import *
from .certificate_organisation import *
diff --git a/django_project/certification/views/certificate.py b/django_project/certification/views/certificate.py
index 2c610d5b4..a73cdc1ec 100644
--- a/django_project/certification/views/certificate.py
+++ b/django_project/certification/views/certificate.py
@@ -32,6 +32,7 @@
import djstripe.settings
from ..models import (
Certificate,
+ CertificateType,
Course,
Attendee,
CertifyingOrganisation,
@@ -234,7 +235,8 @@ def get_object(self, queryset=None):
def generate_pdf(
- pathname, project, course, attendee, certificate, current_site):
+ pathname, project, course, attendee, certificate, current_site,
+ wording='Has attended and completed the course:'):
"""Create the PDF object, using the response object as its file."""
# Register new font
@@ -348,7 +350,7 @@ def generate_pdf(
attendee.surname))
page.setFont('Noto-Regular', 16)
page.drawCentredString(
- center, 370, 'Has attended and completed the course:')
+ center, 370, wording)
page.setFont('Noto-Bold', 20)
page.drawCentredString(
center, 335, course.course_type.name)
@@ -456,7 +458,9 @@ def certificate_pdf_view(request, **kwargs):
os.makedirs(makepath)
generate_pdf(
- pathname, project, course, attendee, certificate, current_site)
+ pathname, project, course, attendee, certificate, current_site,
+ course.certificate_type.wording
+ )
try:
return FileResponse(open(pathname, 'rb'),
content_type='application/pdf')
@@ -691,7 +695,9 @@ def regenerate_certificate(request, **kwargs):
current_site = request.META['HTTP_HOST']
generate_pdf(
- pathname, project, course, attendee, certificate, current_site)
+ pathname, project, course, attendee, certificate, current_site,
+ course.certificate_type.wording
+ )
try:
return FileResponse(open(pathname, 'rb'),
content_type='application/pdf')
@@ -843,7 +849,8 @@ def regenerate_all_certificate(request, **kwargs):
'/home/web/media',
'pdf/{}/{}'.format(project_folder, filename))
generate_pdf(
- pathname, project, course, key, value, current_site)
+ pathname, project, course, key, value, current_site,
+ course.certificate_type.wording)
messages.success(request, 'All certificates are updated', 'regenerate')
return HttpResponseRedirect(url)
@@ -914,6 +921,7 @@ def preview_certificate(request, **kwargs):
organisation_slug = kwargs.pop('organisation_slug')
convener_id = request.POST.get('course_convener', None)
+ certificate_type_id = request.POST.get('certificate_type', None)
if convener_id is not None:
# Get all posted data.
course_convener = CourseConvener.objects.get(id=convener_id)
@@ -948,8 +956,21 @@ def preview_certificate(request, **kwargs):
current_site = request.META['HTTP_HOST']
- generate_pdf(
- response, project, course, attendee, certificate, current_site)
+ if certificate_type_id:
+ try:
+ certificate_type = CertificateType.objects.get(
+ id=certificate_type_id)
+ generate_pdf(
+ response, project, course, attendee, certificate,
+ current_site, certificate_type.wording
+ )
+ except CertificateType.DoesNotExist:
+ generate_pdf(
+ response, project, course, attendee, certificate,
+ current_site)
+ else:
+ generate_pdf(
+ response, project, course, attendee, certificate, current_site)
else:
# When preview page is refreshed, the data is gone so user needs to
diff --git a/django_project/certification/views/certificate_type.py b/django_project/certification/views/certificate_type.py
new file mode 100644
index 000000000..ce5bf9955
--- /dev/null
+++ b/django_project/certification/views/certificate_type.py
@@ -0,0 +1,68 @@
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.http import HttpResponseRedirect
+from django.shortcuts import get_object_or_404
+from django.urls import reverse
+from django.views.generic import ListView
+
+from base.models.project import Project
+from certification.models.certificate_type import (
+ CertificateType, ProjectCertificateType
+)
+
+
+class ProjectCertificateTypeView(LoginRequiredMixin, ListView):
+ context_object_name = 'project_certificate_types'
+ template_name = 'certificate_type/list.html'
+ model = ProjectCertificateType
+
+ def get_context_data(self, **kwargs):
+ """Get the context data which is passed to a template."""
+
+ # Navbar data
+ self.project_slug = self.kwargs.get('project_slug', None)
+ context = super(
+ ProjectCertificateTypeView, self).get_context_data(*kwargs)
+ context['project_slug'] = self.project_slug
+ if self.project_slug:
+ context['the_project'] = \
+ Project.objects.get(slug=self.project_slug)
+ context['project'] = context['the_project']
+
+ # certificate types
+ context['certificate_types'] = CertificateType.objects.all().order_by(
+ 'order'
+ )
+ project = get_object_or_404(Project, slug=self.kwargs['project_slug'])
+ context['certificate_types_applied'] = ProjectCertificateType.\
+ objects.filter(project=project).values_list(
+ 'certificate_type', flat=True)
+ return context
+
+ def get_queryset(self):
+ """Return certificate_types for a project."""
+
+ project = get_object_or_404(Project, slug=self.kwargs['project_slug'])
+ return ProjectCertificateType.objects.filter(project=project)
+
+
+def updateProjectCertificateView(request, project_slug):
+ project = get_object_or_404(Project, slug=project_slug)
+ manager = project.certification_managers.all()
+ if request.user.is_staff or request.user in manager:
+ certificate_types = request.POST.getlist('certificate_types', [])
+ for cer in certificate_types:
+ certificate_type = get_object_or_404(CertificateType, name=cer)
+ obj, created = ProjectCertificateType.objects.get_or_create(
+ certificate_type=certificate_type, project=project
+ )
+ # remove certificate_type that is not in the list
+ old_certificate_type = ProjectCertificateType.objects.filter(
+ project=project).select_related('certificate_type').all()
+ for cer in old_certificate_type:
+ if cer.certificate_type.name not in certificate_types:
+ ProjectCertificateType.objects.get(
+ certificate_type=cer.certificate_type, project=project
+ ).delete()
+ return HttpResponseRedirect(
+ reverse('certificate-type-list', kwargs={'project_slug': project_slug})
+ )
diff --git a/django_project/core/base_templates/includes/base-auth-nav-left.html b/django_project/core/base_templates/includes/base-auth-nav-left.html
index 5df1d4d2b..2bd328cbd 100644
--- a/django_project/core/base_templates/includes/base-auth-nav-left.html
+++ b/django_project/core/base_templates/includes/base-auth-nav-left.html
@@ -187,6 +187,9 @@
Rejected Organisations
Verify certificate for Certifying Organisation
Verify certificate for Attendee
+ {% if user.is_staff or user in the_project.certification_managers.all %}
+ Manage Certificate Type
+ {% endif %}