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 %} + + + + + + + + + + + + + + + {% csrf_token %} + {% for cer_type in certificate_types %} + + + + + + {% endfor %} + +
Certificate TypeWordingApply
{{ cer_type.name }}{{ cer_type.wording }} + +
+ + +{% 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 %}