diff --git a/apps/registration/forms.py b/apps/registration/forms.py index ffe2ac0..731cbd7 100644 --- a/apps/registration/forms.py +++ b/apps/registration/forms.py @@ -1,7 +1,6 @@ # Copyright (C) 2020 by Animath # SPDX-License-Identifier: GPL-3.0-or-later -from address.forms import AddressField -from address.widgets import AddressWidget + from django import forms from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.models import User @@ -69,7 +68,8 @@ class StudentRegistrationForm(forms.ModelForm): class Meta: model = StudentRegistration fields = ('team', 'student_class', 'birth_date', 'address', 'phone_number', - 'school', 'give_contact_to_animath', 'email_confirmed',) + 'school', 'responsible_name', 'responsible_phone', 'responsible_email', + 'give_contact_to_animath', 'email_confirmed',) class PhotoAuthorizationForm(forms.ModelForm): @@ -94,6 +94,50 @@ class PhotoAuthorizationForm(forms.ModelForm): fields = ('photo_authorization',) +class HealthSheetForm(forms.ModelForm): + """ + Form to send a health sheet. + """ + def clean_health_sheet(self): + if "health_sheet" in self.files: + file = self.files["health_sheet"] + if file.size > 2e6: + raise ValidationError(_("The uploaded file size must be under 2 Mo.")) + if file.content_type not in ["application/pdf", "image/png", "image/jpeg"]: + raise ValidationError(_("The uploaded file must be a PDF, PNG of JPEG file.")) + return self.cleaned_data["health_sheet"] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["health_sheet"].widget = FileInput() + + class Meta: + model = StudentRegistration + fields = ('health_sheet',) + + +class ParentalAuthorizationForm(forms.ModelForm): + """ + Form to send a parental authorization. + """ + def clean_parental_authorization(self): + if "parental_authorization" in self.files: + file = self.files["parental_authorization"] + if file.size > 2e6: + raise ValidationError(_("The uploaded file size must be under 2 Mo.")) + if file.content_type not in ["application/pdf", "image/png", "image/jpeg"]: + raise ValidationError(_("The uploaded file must be a PDF, PNG of JPEG file.")) + return self.cleaned_data["parental_authorization"] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["parental_authorization"].widget = FileInput() + + class Meta: + model = StudentRegistration + fields = ('parental_authorization',) + + class CoachRegistrationForm(forms.ModelForm): """ A coach can tell its professional activity. diff --git a/apps/registration/models.py b/apps/registration/models.py index 61d5b0c..ff1cdc4 100644 --- a/apps/registration/models.py +++ b/apps/registration/models.py @@ -1,6 +1,8 @@ # Copyright (C) 2020 by Animath # SPDX-License-Identifier: GPL-3.0-or-later +from datetime import date + from address.models import AddressField from django.contrib.sites.models import Site from django.db import models @@ -73,7 +75,7 @@ class Registration(PolymorphicModel): @property def participates(self): - return isinstance(self, StudentRegistration) or isinstance(self, CoachRegistration) + return isinstance(self, ParticipantRegistration) @property def is_admin(self): @@ -119,7 +121,7 @@ class ParticipantRegistration(Registration): birth_date = models.DateField( verbose_name=_("birth date"), - default=timezone.now, + default=date.today, ) address = AddressField( @@ -147,6 +149,10 @@ class ParticipantRegistration(Registration): default="", ) + @property + def under_18(self): + return (timezone.now().date() - self.birth_date).days < 18 * 365.24 + @property def type(self): # pragma: no cover raise NotImplementedError diff --git a/apps/registration/templates/registration/upload_health_sheet.html b/apps/registration/templates/registration/upload_health_sheet.html new file mode 100644 index 0000000..0c4415b --- /dev/null +++ b/apps/registration/templates/registration/upload_health_sheet.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% load i18n static crispy_forms_filters %} + +{% block content %} + {% trans "Back to the user detail" %} +
+
+
+
+ {% trans "Health sheet template:" %} + {% trans "Download" %} +
+ {% csrf_token %} + {{ form|crispy }} +
+ +
+{% endblock %} diff --git a/apps/registration/templates/registration/upload_parental_authorization.html b/apps/registration/templates/registration/upload_parental_authorization.html new file mode 100644 index 0000000..6bf65f0 --- /dev/null +++ b/apps/registration/templates/registration/upload_parental_authorization.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% load i18n static crispy_forms_filters %} + +{% block content %} + {% trans "Back to the user detail" %} +
+
+
+
+ {% trans "Authorzation template:" %} + {% trans "Download" %} +
+ {% csrf_token %} + {{ form|crispy }} +
+ +
+{% endblock %} diff --git a/apps/registration/templates/registration/upload_photo_authorization.html b/apps/registration/templates/registration/upload_photo_authorization.html index 9a5a1c5..b689f90 100644 --- a/apps/registration/templates/registration/upload_photo_authorization.html +++ b/apps/registration/templates/registration/upload_photo_authorization.html @@ -9,8 +9,8 @@
{% trans "Authorzation templates:" %} - {% trans "Adult" %} — - {% trans "Child" %} + {% trans "Adult" %} — + {% trans "Child" %}
{% csrf_token %} {{ form|crispy }} diff --git a/apps/registration/templates/registration/user_detail.html b/apps/registration/templates/registration/user_detail.html index 710cf48..d3ec639 100644 --- a/apps/registration/templates/registration/user_detail.html +++ b/apps/registration/templates/registration/user_detail.html @@ -21,7 +21,7 @@
{{ user_object.email }} {% if not user_object.registration.email_confirmed %} ({% trans "Not confirmed" %}, {% trans "resend the validation link" %}){% endif %}
- {% if user_object.registration.participates or True %} + {% if user_object.registration.participates %}
{% trans "Team:" %}
{% trans "any" as any %}
@@ -29,14 +29,15 @@ {{ user_object.registration.team|default:any }}
- {% endif %} - {% if user_object.registration.studentregistration %} -
{% trans "Student class:" %}
-
{{ user_object.registration.get_student_class_display }}
+
{% trans "Birth date:" %}
+
{{ user_object.registration.birth_date }}
-
{% trans "School:" %}
-
{{ user_object.registration.school }}
+
{% trans "Address:" %}
+
{{ user_object.registration.address }}
+ +
{% trans "Phone number:" %}
+
{{ user_object.registration.phone_number }}
{% trans "Photo authorization:" %}
@@ -47,6 +48,47 @@ {% endif %}
+ +
{% trans "Health sheet:" %}
+
+ {% if user_object.registration.health_sheet %} + {% trans "Download" %} + {% endif %} + {% if user_object.pk == user.pk %} + + {% endif %} +
+ {% endif %} + + {% if user_object.registration.studentregistration %} + {% if user_object.registration.under_18 %} +
{% trans "Parental authorization:" %}
+
+ {% if user_object.registration.parental_authorization %} + {% trans "Download" %} + {% endif %} + {% if user_object.pk == user.pk %} + + {% endif %} +
+ {% endif %} + +
{% trans "Student class:" %}
+
{{ user_object.registration.get_student_class_display }}
+ +
{% trans "School:" %}
+
{{ user_object.registration.school }}
+ +
{% trans "Responsible name:" %}
+
{{ user_object.registration.responsible_name }}
+ +
{% trans "Responsible phone number:" %}
+
{{ user_object.registration.responsible_phone }}
+ +
{% trans "Responsible email address:" %}
+ {% with user_object.registration.responsible_email as email %} +
{{ email }}
+ {% endwith %} {% elif user_object.registration.coachregistration %}
{% trans "Profesional activity:" %}
{{ user_object.registration.professional_activity }}
@@ -78,6 +120,16 @@ {% trans "Upload" as modal_button %} {% url "registration:upload_user_photo_authorization" pk=user_object.registration.pk as modal_action %} {% include "base_modal.html" with modal_id="uploadPhotoAuthorization" modal_enctype="multipart/form-data" %} + + {% trans "Upload health sheet" as modal_title %} + {% trans "Upload" as modal_button %} + {% url "registration:upload_user_health_sheet" pk=user_object.registration.pk as modal_action %} + {% include "base_modal.html" with modal_id="uploadHealthSheet" modal_enctype="multipart/form-data" %} + + {% trans "Upload parental authorization" as modal_title %} + {% trans "Upload" as modal_button %} + {% url "registration:upload_user_parental_authorization" pk=user_object.registration.pk as modal_action %} + {% include "base_modal.html" with modal_id="uploadParentalAuthorization" modal_enctype="multipart/form-data" %} {% endblock %} {% block extrajavascript %} @@ -93,6 +145,16 @@ if (!modalBody.html().trim()) modalBody.load("{% url "registration:upload_user_photo_authorization" pk=user_object.registration.pk %} #form-content"); }); + $('button[data-target="#uploadHealthSheetModal"]').click(function() { + let modalBody = $("#uploadHealthSheetModal div.modal-body"); + if (!modalBody.html().trim()) + modalBody.load("{% url "registration:upload_user_health_sheet" pk=user_object.registration.pk %} #form-content"); + }); + $('button[data-target="#uploadParentalAuthorizationModal"]').click(function() { + let modalBody = $("#uploadParentalAuthorizationModal div.modal-body"); + if (!modalBody.html().trim()) + modalBody.load("{% url "registration:upload_user_parental_authorization" pk=user_object.registration.pk %} #form-content"); + }); }); {% endblock %} diff --git a/apps/registration/tests.py b/apps/registration/tests.py index 9317ed2..b620b51 100644 --- a/apps/registration/tests.py +++ b/apps/registration/tests.py @@ -268,37 +268,38 @@ class TestRegistration(TestCase): """ Try to upload a photo authorization. """ - response = self.client.get(reverse("registration:upload_user_photo_authorization", - args=(self.student.registration.pk,))) - self.assertEqual(response.status_code, 200) + for auth_type in ["photo_authorization", "health_sheet", "parental_authorization"]: + response = self.client.get(reverse("registration:upload_user_photo_authorization", + args=(self.student.registration.pk,))) + self.assertEqual(response.status_code, 200) - # README is not a valid PDF file - response = self.client.post(reverse("registration:upload_user_photo_authorization", - args=(self.student.registration.pk,)), data=dict( - photo_authorization=open("README.md", "rb"), - )) - self.assertEqual(response.status_code, 200) + # README is not a valid PDF file + response = self.client.post(reverse(f"registration:upload_user_{auth_type}", + args=(self.student.registration.pk,)), data={ + auth_type: open("README.md", "rb"), + }) + self.assertEqual(response.status_code, 200) - # Don't send too large files - response = self.client.post(reverse("registration:upload_user_photo_authorization", - args=(self.student.registration.pk,)), data=dict( - photo_authorization=SimpleUploadedFile("file.pdf", content=int(0).to_bytes(2000001, "big"), - content_type="application/pdf"), - )) - self.assertEqual(response.status_code, 200) + # Don't send too large files + response = self.client.post(reverse(f"registration:upload_user_{auth_type}", + args=(self.student.registration.pk,)), data={ + auth_type: SimpleUploadedFile("file.pdf", content=int(0).to_bytes(2000001, "big"), + content_type="application/pdf"), + }) + self.assertEqual(response.status_code, 200) - response = self.client.post(reverse("registration:upload_user_photo_authorization", - args=(self.student.registration.pk,)), data=dict( - photo_authorization=open("tfjm/static/Fiche_sanitaire.pdf", "rb"), - )) - self.assertRedirects(response, reverse("registration:user_detail", args=(self.student.pk,)), 302, 200) + response = self.client.post(reverse(f"registration:upload_user_{auth_type}", + args=(self.student.registration.pk,)), data={ + auth_type: open("tfjm/static/Fiche_sanitaire.pdf", "rb"), + }) + self.assertRedirects(response, reverse("registration:user_detail", args=(self.student.pk,)), 302, 200) - self.student.registration.refresh_from_db() - self.assertTrue(self.student.registration.photo_authorization) + self.student.registration.refresh_from_db() + self.assertTrue(getattr(self.student.registration, auth_type)) - response = self.client.get(reverse("photo_authorization", - args=(self.student.registration.photo_authorization.name.split('/')[-1],))) - self.assertEqual(response.status_code, 200) + response = self.client.get(reverse( + auth_type, args=(self.student.registration.photo_authorization.name.split('/')[-1],))) + self.assertEqual(response.status_code, 200) from participation.models import Team team = Team.objects.create(name="Test", trigram="TES") diff --git a/apps/registration/urls.py b/apps/registration/urls.py index 7d6bf01..de0fc63 100644 --- a/apps/registration/urls.py +++ b/apps/registration/urls.py @@ -4,7 +4,8 @@ from django.urls import path from .views import MyAccountDetailView, ResetAdminView, SignupView, UserDetailView, UserImpersonateView, \ - UserListView, UserResendValidationEmailView, UserUpdateView, UserUploadPhotoAuthorizationView, UserValidateView, \ + UserListView, UserResendValidationEmailView, UserUpdateView, UserUploadHealthSheetView, \ + UserUploadParentalAuthorizationView, UserUploadPhotoAuthorizationView, UserValidateView, \ UserValidationEmailSentView app_name = "registration" @@ -20,6 +21,10 @@ urlpatterns = [ path("user//update/", UserUpdateView.as_view(), name="update_user"), path("user//upload-photo-authorization/", UserUploadPhotoAuthorizationView.as_view(), name="upload_user_photo_authorization"), + path("user//upload-health_sheet/", UserUploadHealthSheetView.as_view(), + name="upload_user_health_sheet"), + path("user//upload-parental-authorization/", UserUploadParentalAuthorizationView.as_view(), + name="upload_user_parental_authorization"), path("user//impersonate/", UserImpersonateView.as_view(), name="user_impersonate"), path("user/list/", UserListView.as_view(), name="user_list"), path("reset-admin/", ResetAdminView.as_view(), name="reset_admin"), diff --git a/apps/registration/views.py b/apps/registration/views.py index 723ccd0..f36f477 100644 --- a/apps/registration/views.py +++ b/apps/registration/views.py @@ -17,9 +17,10 @@ from django.views.generic import CreateView, DetailView, RedirectView, TemplateV from django_tables2 import SingleTableView from magic import Magic from tfjm.tokens import email_validation_token -from tfjm.views import AdminMixin +from tfjm.views import AdminMixin, UserMixin -from .forms import CoachRegistrationForm, PhotoAuthorizationForm, SignupForm, StudentRegistrationForm, UserForm +from .forms import CoachRegistrationForm, HealthSheetForm, ParentalAuthorizationForm, PhotoAuthorizationForm,\ + SignupForm, StudentRegistrationForm, UserForm from .models import Registration, StudentRegistration from .tables import RegistrationTable @@ -150,7 +151,7 @@ class MyAccountDetailView(LoginRequiredMixin, RedirectView): return reverse_lazy("registration:user_detail", args=(self.request.user.pk,)) -class UserDetailView(LoginRequiredMixin, DetailView): +class UserDetailView(UserMixin, DetailView): """ Display the detail about a user. """ @@ -159,15 +160,6 @@ class UserDetailView(LoginRequiredMixin, DetailView): context_object_name = "user_object" template_name = "registration/user_detail.html" - def dispatch(self, request, *args, **kwargs): - user = request.user - if not user.is_authenticated: - return self.handle_no_permission() - # Only an admin or the concerned user can see the information - if not user.registration.is_admin and user.pk != kwargs["pk"]: - raise PermissionDenied - return super().dispatch(request, *args, **kwargs) - def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["title"] = _("Detail of user {user}").format(user=str(self.object.registration)) @@ -183,7 +175,7 @@ class UserListView(AdminMixin, SingleTableView): template_name = "registration/user_list.html" -class UserUpdateView(LoginRequiredMixin, UpdateView): +class UserUpdateView(UserMixin, UpdateView): """ Update the detail about a user and its registration. """ @@ -191,12 +183,6 @@ class UserUpdateView(LoginRequiredMixin, UpdateView): form_class = UserForm template_name = "registration/update_user.html" - def dispatch(self, request, *args, **kwargs): - user = request.user - if not user.registration.is_admin and user.pk != kwargs["pk"]: - raise PermissionDenied - return super().dispatch(request, *args, **kwargs) - def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) user = self.get_object() @@ -229,7 +215,7 @@ class UserUpdateView(LoginRequiredMixin, UpdateView): return reverse_lazy("registration:user_detail", args=(self.object.pk,)) -class UserUploadPhotoAuthorizationView(LoginRequiredMixin, UpdateView): +class UserUploadPhotoAuthorizationView(UserMixin, UpdateView): """ A participant can send its photo authorization. """ @@ -238,12 +224,6 @@ class UserUploadPhotoAuthorizationView(LoginRequiredMixin, UpdateView): template_name = "registration/upload_photo_authorization.html" extra_context = dict(title=_("Upload photo authorization")) - def dispatch(self, request, *args, **kwargs): - user = request.user - if not user.registration.is_admin and user.registration.pk != kwargs["pk"]: - raise PermissionDenied - return super().dispatch(request, *args, **kwargs) - @transaction.atomic def form_valid(self, form): old_instance = StudentRegistration.objects.get(pk=self.object.pk) @@ -255,6 +235,46 @@ class UserUploadPhotoAuthorizationView(LoginRequiredMixin, UpdateView): return reverse_lazy("registration:user_detail", args=(self.object.user.pk,)) +class UserUploadHealthSheetView(UserMixin, UpdateView): + """ + A participant can send its health sheet. + """ + model = StudentRegistration + form_class = HealthSheetForm + template_name = "registration/upload_health_sheet.html" + extra_context = dict(title=_("Upload health sheet")) + + @transaction.atomic + def form_valid(self, form): + old_instance = StudentRegistration.objects.get(pk=self.object.pk) + if old_instance.health_sheet: + old_instance.health_sheet.delete() + return super().form_valid(form) + + def get_success_url(self): + return reverse_lazy("registration:user_detail", args=(self.object.user.pk,)) + + +class UserUploadParentalAuthorizationView(UserMixin, UpdateView): + """ + A participant can send its parental authorization. + """ + model = StudentRegistration + form_class = ParentalAuthorizationForm + template_name = "registration/upload_parental_authorization.html" + extra_context = dict(title=_("Upload parental authorization")) + + @transaction.atomic + def form_valid(self, form): + old_instance = StudentRegistration.objects.get(pk=self.object.pk) + if old_instance.parental_authorization: + old_instance.parental_authorization.delete() + return super().form_valid(form) + + def get_success_url(self): + return reverse_lazy("registration:user_detail", args=(self.object.user.pk,)) + + class PhotoAuthorizationView(LoginRequiredMixin, View): """ Display the sent photo authorization. diff --git a/tfjm/views.py b/tfjm/views.py index 76a43df..81bdae2 100644 --- a/tfjm/views.py +++ b/tfjm/views.py @@ -8,7 +8,15 @@ from haystack.generic_views import SearchView class AdminMixin(LoginRequiredMixin): def dispatch(self, request, *args, **kwargs): - if not request.user.registration.is_admin: + if request.user.is_authenticated and not request.user.registration.is_admin: + raise PermissionDenied + return super().dispatch(request, *args, **kwargs) + + +class UserMixin(LoginRequiredMixin): + def dispatch(self, request, *args, **kwargs): + user = request.user + if user.is_authenticated and not user.registration.is_admin and user.registration.pk != kwargs["pk"]: raise PermissionDenied return super().dispatch(request, *args, **kwargs)