From b9fc0ced89805f87615f5a5612d3bd41d500684e Mon Sep 17 00:00:00 2001 From: Dimas Ciputra Date: Fri, 19 Aug 2022 18:47:59 +0700 Subject: [PATCH] Make course attendee editable after certificate is issued (#1438) * Make course attendee editable after certificate is issued * Fix lint error * Add test --- django_project/certification/admin.py | 1 + .../migrations/0023_certificate_issue_date.py | 18 +++++++ .../migrations/0024_auto_20220605_0612.py | 19 +++++++ .../certification/models/certificate.py | 6 +++ .../templates/course/detail.html | 4 +- .../tests/views/test_attendee_views.py | 52 +++++++++++++++++++ .../tests/views/test_course_view.py | 32 +++++++++++- .../certification/views/attendee.py | 17 ++++-- django_project/certification/views/course.py | 22 +++++++- 9 files changed, 164 insertions(+), 7 deletions(-) create mode 100755 django_project/certification/migrations/0023_certificate_issue_date.py create mode 100755 django_project/certification/migrations/0024_auto_20220605_0612.py diff --git a/django_project/certification/admin.py b/django_project/certification/admin.py index 9c3933e04..3ffc6fdd4 100644 --- a/django_project/certification/admin.py +++ b/django_project/certification/admin.py @@ -25,6 +25,7 @@ class CertificateAdmin(admin.ModelAdmin): list_display = ('certificateID', 'course') search_fields = ('certificateID', 'course__name',) + readonly_fields = ('issue_date',) def queryset(self, request): """Ensure we use the correct manager. diff --git a/django_project/certification/migrations/0023_certificate_issue_date.py b/django_project/certification/migrations/0023_certificate_issue_date.py new file mode 100755 index 000000000..c307aba64 --- /dev/null +++ b/django_project/certification/migrations/0023_certificate_issue_date.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.13 on 2022-06-05 04:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('certification', '0022_externalreviewer_name'), + ] + + operations = [ + migrations.AddField( + model_name='certificate', + name='issue_date', + field=models.DateField(auto_now_add=True, null=True), + ), + ] diff --git a/django_project/certification/migrations/0024_auto_20220605_0612.py b/django_project/certification/migrations/0024_auto_20220605_0612.py new file mode 100755 index 000000000..5294aa836 --- /dev/null +++ b/django_project/certification/migrations/0024_auto_20220605_0612.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.13 on 2022-06-05 04:12 + +from django.db import migrations + + +def set_issue_date_to_null(apps, schema_editor): + Ceritificate = apps.get_model('certification', 'Certificate') + Ceritificate.objects.all().update(issue_date=None) + + +class Migration(migrations.Migration): + + dependencies = [ + ('certification', '0023_certificate_issue_date'), + ] + + operations = [ + migrations.RunPython(set_issue_date_to_null) + ] diff --git a/django_project/certification/models/certificate.py b/django_project/certification/models/certificate.py index a218731a8..c8277a608 100644 --- a/django_project/certification/models/certificate.py +++ b/django_project/certification/models/certificate.py @@ -51,6 +51,12 @@ class Certificate(models.Model): default=False ) + issue_date = models.DateField( + auto_now_add=True, + blank=True, + null=True + ) + author = models.ForeignKey(User, on_delete=models.CASCADE) course = models.ForeignKey(Course, on_delete=models.CASCADE) attendee = models.ForeignKey(Attendee, on_delete=models.CASCADE) diff --git a/django_project/certification/templates/course/detail.html b/django_project/certification/templates/course/detail.html index 0a48cfd38..2f4e08724 100644 --- a/django_project/certification/templates/course/detail.html +++ b/django_project/certification/templates/course/detail.html @@ -229,7 +229,7 @@

{% if user in course.certifying_organisation.organisation_owners.all or user.is_staff or user == course.course_convener.user or user == project.owner or user in course.certifying_organisation.project.certification_managers.all %}
- {% if course.editable %} + {% if attendee.editable %} @@ -238,7 +238,7 @@

{% else %} + data-title="Edit attendee is only available 7 days after the certificate is issued."> {% endif %} diff --git a/django_project/certification/tests/views/test_attendee_views.py b/django_project/certification/tests/views/test_attendee_views.py index b701395df..927000413 100644 --- a/django_project/certification/tests/views/test_attendee_views.py +++ b/django_project/certification/tests/views/test_attendee_views.py @@ -9,11 +9,63 @@ CourseF, CourseConvenerF, CourseTypeF, + AttendeeF, + CertificateF ) from core.model_factories import UserF import logging +class TestAttendeeView(TestCase): + + @override_settings(VALID_DOMAIN=['testserver', ]) + def setUp(self) -> None: + self.client = Client() + self.client.post( + '/set_language/', data={'language': 'en'}) + self.project = ProjectF.create() + self.certifying_organisation = CertifyingOrganisationF.create( + project=self.project) + self.training_center = TrainingCenterF.create( + certifying_organisation=self.certifying_organisation) + self.course_convener = CourseConvenerF.create( + certifying_organisation=self.certifying_organisation) + self.course_type = CourseTypeF.create( + certifying_organisation=self.certifying_organisation) + self.course = CourseF.create( + certifying_organisation=self.certifying_organisation, + training_center=self.training_center, + course_convener=self.course_convener, + course_type=self.course_type + ) + self.user = UserF.create(**{ + 'username': 'user', + 'password': 'password', + 'is_staff': True + }) + self.user.set_password('password') + self.user.save() + self.attendee = AttendeeF.create( + certifying_organisation=self.certifying_organisation) + self.certificate = CertificateF.create( + course=self.course, + attendee=self.attendee, + author=self.user) + + @override_settings(VALID_DOMAIN=['testserver', ]) + def test_AttendeeUpdateView_with_login(self): + status = self.client.login(username='user', password='password') + self.assertTrue(status) + url = reverse('attendee-update', kwargs={ + 'project_slug': self.project.slug, + 'organisation_slug': self.certifying_organisation.slug, + 'course_slug': self.course.slug, + 'pk': self.attendee.pk + }) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + class TestCourseAttendeeView(TestCase): """Tests that attendee and course attendee view works.""" diff --git a/django_project/certification/tests/views/test_course_view.py b/django_project/certification/tests/views/test_course_view.py index 8c27da104..d92cce42b 100644 --- a/django_project/certification/tests/views/test_course_view.py +++ b/django_project/certification/tests/views/test_course_view.py @@ -1,4 +1,5 @@ # coding=utf-8 +import datetime import logging from bs4 import BeautifulSoup as Soup @@ -12,7 +13,7 @@ CertificateTypeF, ProjectCertificateTypeF, CourseF, - CourseConvenerF + CourseConvenerF, AttendeeF, CertificateF, CourseAttendeeF ) @@ -86,12 +87,41 @@ def test_create_course_must_showing_CertificateTypes(self): @override_settings(VALID_DOMAIN=['testserver', ]) def test_detail_view(self): client = Client() + attendee = AttendeeF.create() + CourseAttendeeF.create( + attendee=attendee, + course=self.course + ) + CertificateF.create( + attendee=attendee, + course=self.course, + issue_date=datetime.datetime.now() + ) + + old_attendee = AttendeeF.create() + CourseAttendeeF.create( + attendee=old_attendee, + course=self.course + ) + cert = CertificateF.create( + attendee=old_attendee, + course=self.course + ) + cert.issue_date = datetime.datetime.now() - datetime.timedelta(days=7) + cert.save() + response = client.get(reverse('course-detail', kwargs={ 'project_slug': self.project.slug, 'organisation_slug': self.certifying_organisation.slug, 'slug': self.course.slug })) self.assertEqual(response.status_code, 200) + course_attendees = response.context_data['attendees'] + for course_attendee in course_attendees: + if course_attendee.attendee_id == attendee.id: + self.assertTrue(course_attendee.editable) + else: + self.assertFalse(course_attendee.editable) @override_settings(VALID_DOMAIN=['testserver', ]) def test_detail_with_duplicates(self): diff --git a/django_project/certification/views/attendee.py b/django_project/certification/views/attendee.py index efa752818..c8ef362e6 100644 --- a/django_project/certification/views/attendee.py +++ b/django_project/certification/views/attendee.py @@ -1,6 +1,8 @@ # coding=utf-8 import io import csv +from datetime import timedelta, datetime + from django.db import transaction from django.http import HttpResponseForbidden from django.urls import reverse @@ -8,7 +10,7 @@ CreateView, FormView, UpdateView) from braces.views import LoginRequiredMixin, FormMessagesMixin from certification.models import ( - Attendee, CertifyingOrganisation, CourseAttendee, Course + Attendee, CertifyingOrganisation, CourseAttendee, Course, Certificate ) from certification.forms import ( AttendeeForm, CsvAttendeeForm, UpdateAttendeeForm) @@ -304,6 +306,15 @@ def get_form_kwargs(self): def get(self, request, *args, **kwargs): self.course_slug = self.kwargs.get('course_slug', None) course = Course.objects.get(slug=self.course_slug) - if not course.editable: - return HttpResponseForbidden('Course is not editable.') + certificate = Certificate.objects.filter( + course=course, + attendee=self.get_object() + ).first() + if certificate: + if ( + not certificate.issue_date or + certificate.issue_date + + timedelta(days=7) <= datetime.today().date() + ): + return HttpResponseForbidden('Course is not editable.') return super(AttendeeUpdateView, self).get(request, *args, **kwargs) diff --git a/django_project/certification/views/course.py b/django_project/certification/views/course.py index 4aecd3a67..6819d5060 100644 --- a/django_project/certification/views/course.py +++ b/django_project/certification/views/course.py @@ -1,4 +1,6 @@ # coding=utf-8 +from datetime import timedelta, datetime + from django.urls import reverse from django.core.exceptions import PermissionDenied from django.http import Http404, HttpResponseRedirect @@ -457,8 +459,26 @@ def get_context_data(self, **kwargs): self.course = Course.objects.get(slug=self.slug) context = super( CourseDetailView, self).get_context_data(**kwargs) - context['attendees'] = \ + + attendees = ( CourseAttendee.objects.filter(course=self.course) + ) + for course_attendee in attendees: + course_attendee.editable = False + certificate = Certificate.objects.filter( + course=self.course, + attendee=course_attendee.attendee + ).first() + if certificate: + course_attendee.editable = ( + certificate.issue_date and + certificate.issue_date + + timedelta(days=7) > datetime.today().date() + ) + else: + course_attendee.editable = True + context['attendees'] = attendees + context['certificates'] = dict( Certificate.objects.filter( course=self.course