diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 39ac97b..e803e18 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: TFJM\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-02-20 22:48+0100\n" +"POT-Creation-Date: 2024-02-21 22:42+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Emmy D'Anello \n" "Language-Team: LANGUAGE \n" @@ -1831,7 +1831,7 @@ msgstr "" msgid "registration" msgstr "inscription" -#: registration/models.py:129 registration/models.py:513 +#: registration/models.py:129 registration/models.py:517 msgid "registrations" msgstr "inscriptions" @@ -2131,11 +2131,11 @@ msgstr "inscription de bénévole" msgid "volunteer registrations" msgstr "inscriptions de bénévoles" -#: registration/models.py:517 +#: registration/models.py:521 msgid "grouped" msgstr "groupé" -#: registration/models.py:519 +#: registration/models.py:523 msgid "" "If set to true, then one payment is made for the full team, for example if " "the school pays for all." @@ -2143,84 +2143,92 @@ msgstr "" "Si vrai, alors un seul paiement est fait pour toute l'équipe, par exemple si " "le lycée paie pour tout le monde." -#: registration/models.py:524 +#: registration/models.py:528 msgid "total amount" msgstr "montant total" -#: registration/models.py:525 +#: registration/models.py:529 msgid "Corresponds to the total required amount to pay, in euros." msgstr "Correspond au montant total à payer, en euros." -#: registration/models.py:530 +#: registration/models.py:534 +msgid "token" +msgstr "jeton" + +#: registration/models.py:537 +msgid "A token to authorize external users to make this payment." +msgstr "Un jeton pour autoriser des utilisateurs externes à faire ce paiement." + +#: registration/models.py:541 msgid "for final tournament" msgstr "pour la finale" -#: registration/models.py:535 +#: registration/models.py:546 msgid "type" msgstr "type" -#: registration/models.py:538 +#: registration/models.py:549 msgid "No payment" msgstr "Pas de paiement" -#: registration/models.py:539 +#: registration/models.py:550 #: registration/templates/registration/payment_form.html:56 msgid "Credit card" msgstr "Carte bancaire" -#: registration/models.py:540 +#: registration/models.py:551 msgid "Scholarship" msgstr "Notification de bourse" -#: registration/models.py:541 +#: registration/models.py:552 #: registration/templates/registration/payment_form.html:61 msgid "Bank transfer" msgstr "Virement bancaire" -#: registration/models.py:542 +#: registration/models.py:553 msgid "Other (please indicate)" msgstr "Autre (veuillez spécifier)" -#: registration/models.py:543 +#: registration/models.py:554 msgid "The tournament is free" msgstr "Le tournoi est gratuit" -#: registration/models.py:550 +#: registration/models.py:561 msgid "Hello Asso checkout intent ID" msgstr "ID de l'intention de paiement Hello Asso" -#: registration/models.py:557 +#: registration/models.py:568 msgid "receipt" msgstr "justificatif" -#: registration/models.py:558 +#: registration/models.py:569 msgid "only if you have a scholarship or if you chose a bank transfer." msgstr "" "Nécessaire seulement si vous déclarez être boursièr⋅e ou si vous payez par " "virement bancaire." -#: registration/models.py:565 +#: registration/models.py:576 msgid "additional information" msgstr "informations additionnelles" -#: registration/models.py:566 +#: registration/models.py:577 msgid "To help us to find your payment." msgstr "Pour nous aider à retrouver votre paiement, si nécessaire." -#: registration/models.py:572 +#: registration/models.py:583 msgid "payment valid" msgstr "paiement valide" -#: registration/models.py:630 +#: registration/models.py:641 #, python-brace-format msgid "Payment of {registrations}" msgstr "Paiements de {registrations}" -#: registration/models.py:633 +#: registration/models.py:644 msgid "payment" msgstr "paiement" -#: registration/models.py:634 +#: registration/models.py:645 msgid "payments" msgstr "paiements" @@ -2656,30 +2664,34 @@ msgstr "Mise à jour de l'utilisateur⋅rice {user}" msgid "This payment is already valid or pending validation." msgstr "Le paiement est déjà validé ou en attente de validation." -#: registration/views.py:563 +#: registration/views.py:570 +msgid "The payment is already valid or pending validation." +msgstr "Le paiement est déjà validé ou en attente de validation." + +#: registration/views.py:584 msgid "The payment is not found or is already validated." msgstr "Le paiement n'est pas trouvé ou déjà validé." -#: registration/views.py:582 +#: registration/views.py:603 #, python-brace-format msgid "An error occurred during the payment: {error}" msgstr "Une erreur est survenue lors du paiement : {error}" -#: registration/views.py:588 +#: registration/views.py:609 msgid "The payment has been refused." msgstr "Le paiement a été refusé." -#: registration/views.py:591 +#: registration/views.py:612 #, python-brace-format msgid "The return code is unknown: {code}" msgstr "Le code de retour est inconnu : {code}" -#: registration/views.py:594 +#: registration/views.py:615 #, python-brace-format msgid "The return type is unknown: {type}" msgstr "Le type de retour est inconnu : {type}" -#: registration/views.py:601 +#: registration/views.py:622 msgid "" "The payment has been successfully validated! Your registration is now " "complete." @@ -2687,7 +2699,7 @@ msgstr "" "Le paiement a été validé avec succès ! Votre inscription est désormais " "complète." -#: registration/views.py:606 +#: registration/views.py:627 msgid "" "Your payment is done! The validation of your payment may takes a few " "minutes, and will be automatically done. If it is not the case, please " @@ -2697,27 +2709,27 @@ msgstr "" "quelques minutes, et sera faite automatiquement. Si ce n'est pas le cas, " "merci de nous contacter." -#: registration/views.py:641 +#: registration/views.py:662 #, python-brace-format msgid "Photo authorization of {student}.{ext}" msgstr "Autorisation de droit à l'image de {student}.{ext}" -#: registration/views.py:665 +#: registration/views.py:686 #, python-brace-format msgid "Health sheet of {student}.{ext}" msgstr "Fiche sanitaire de {student}.{ext}" -#: registration/views.py:689 +#: registration/views.py:710 #, python-brace-format msgid "Vaccine sheet of {student}.{ext}" msgstr "Carnet de vaccination de {student}.{ext}" -#: registration/views.py:713 +#: registration/views.py:734 #, python-brace-format msgid "Parental authorization of {student}.{ext}" msgstr "Autorisation parentale de {student}.{ext}" -#: registration/views.py:736 +#: registration/views.py:757 #, python-brace-format msgid "Payment receipt of {user}.{ext}" msgstr "Justificatif de paiement de {user}.{ext}" diff --git a/registration/migrations/0012_payment_token_alter_payment_type.py b/registration/migrations/0012_payment_token_alter_payment_type.py new file mode 100644 index 0000000..1d05f05 --- /dev/null +++ b/registration/migrations/0012_payment_token_alter_payment_type.py @@ -0,0 +1,41 @@ +# Generated by Django 5.0.1 on 2024-02-20 22:48 + +import registration.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registration", "0011_remove_payment_registration_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="payment", + name="token", + field=models.CharField( + default=registration.models.get_random_token, + help_text="A token to authorize external users to make this payment.", + max_length=32, + verbose_name="token", + ), + ), + migrations.AlterField( + model_name="payment", + name="type", + field=models.CharField( + blank=True, + choices=[ + ("", "No payment"), + ("helloasso", "Credit card"), + ("scholarship", "Scholarship"), + ("bank_transfer", "Bank transfer"), + ("other", "Other (please indicate)"), + ("free", "The tournament is free"), + ], + default="", + max_length=16, + verbose_name="type", + ), + ), + ] diff --git a/registration/models.py b/registration/models.py index b13870a..56422f2 100644 --- a/registration/models.py +++ b/registration/models.py @@ -506,6 +506,10 @@ def get_receipt_filename(instance, filename): return f"authorization/receipt/receipt_{instance.id}" +def get_random_token(): + return get_random_string(32) + + class Payment(models.Model): registrations = models.ManyToManyField( ParticipantRegistration, @@ -526,6 +530,13 @@ class Payment(models.Model): default=0, ) + token = models.CharField( + verbose_name=_("token"), + max_length=32, + default=get_random_token, + help_text=_("A token to authorize external users to make this payment."), + ) + final = models.BooleanField( verbose_name=_("for final tournament"), default=False, @@ -585,13 +596,13 @@ class Payment(models.Model): return Tournament.final_tournament() return self.registrations.first().team.participation.tournament - def get_checkout_intent(self): + def get_checkout_intent(self, none_if_link_disabled=False): if self.checkout_intent_id is None: return None - return helloasso.get_checkout_intent(self.checkout_intent_id) + return helloasso.get_checkout_intent(self.checkout_intent_id, none_if_link_disabled=none_if_link_disabled) def create_checkout_intent(self): - checkout_intent = self.get_checkout_intent() + checkout_intent = self.get_checkout_intent(none_if_link_disabled=True) if checkout_intent is not None: return checkout_intent diff --git a/registration/templates/registration/payment_form.html b/registration/templates/registration/payment_form.html index 327bb2f..d6ef676 100644 --- a/registration/templates/registration/payment_form.html +++ b/registration/templates/registration/payment_form.html @@ -79,17 +79,39 @@
- Le paiement par carte bancaire s'effectue via Hello Asso. Pour cela, vous pouvez cliquer sur - le bouton ci-dessous, qui vous redirigera vers la page de paiement sécurisée de Hello Asso. - La validation du paiement sera ensuite faite automatiquement, sous quelques minutes. - Si un tiers doit payer pour vous (parents, lycée,…), vous pouvez lui transmettre le lien pour - payer pour vous. +

+ Le paiement par carte bancaire s'effectue via Hello Asso. Pour cela, vous pouvez cliquer sur + le bouton ci-dessous, qui vous redirigera vers la page de paiement sécurisée de Hello Asso. + La validation du paiement sera ensuite faite automatiquement, sous quelques minutes. +

+ +

+ Si un tiers doit payer pour vous (parents, lycée,…), vous pouvez lui transmettre le lien pour + payer pour vous : +

+ +
+ {% url "registration:payment_hello_asso" pk=payment.pk as payment_url %} + {{ request.scheme }}://{{ request.site.domain }}{{ payment_url }}?token={{ payment.token }} + + Copier + +
+ +

+ Si tel est le cas et si une facture est nécessaire, merci de contacter les organisateur⋅ices + du tournoi en transmettant le nom de l'équipe, le nombre de participant⋅es, le nom de + l'établissement payeur, l'adresse mail de l'établissement et/ou l'adresse mail du ou de la + gestionnaire de l'établissement. +

@@ -138,5 +160,28 @@ elem => elem.addEventListener( 'click', () => document.location.hash = '#' + elem.getAttribute('aria-controls'))) }) + + function copyToClipboard(text) { + const copyIcon = document.getElementById('copyIcon') + if (navigator.clipboard) { + navigator.clipboard.writeText(text).then(() => { + const tooltip = bootstrap.Tooltip.getOrCreateInstance(copyIcon) + tooltip.setContent('Copied!') + tooltip.show() + } + ) + } else { + const input = document.createElement('input') + input.value = text + document.body.appendChild(input) + input.select() + document.execCommand('copy') + document.body.removeChild(input) + const tooltip = bootstrap.Tooltip.getOrCreateInstance(copyIcon) + tooltip.enable() + tooltip.show() + setTimeout(() => {tooltip.disable(); tooltip.hide()}, 2000) + } + } {% endblock %} diff --git a/registration/views.py b/registration/views.py index 57eefd8..141e1d9 100644 --- a/registration/views.py +++ b/registration/views.py @@ -7,7 +7,7 @@ from tempfile import mkdtemp from django.conf import settings from django.contrib import messages -from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.auth.mixins import AccessMixin, LoginRequiredMixin from django.contrib.auth.models import User from django.contrib.sites.models import Site from django.core.exceptions import PermissionDenied, ValidationError @@ -535,21 +535,41 @@ class PaymentUpdateGroupView(LoginRequiredMixin, DetailView): return redirect(reverse_lazy("registration:update_payment", args=(payment.pk,))) -class PaymenRedirectHelloAssoView(LoginRequiredMixin, DetailView): +class PaymenRedirectHelloAssoView(AccessMixin, DetailView): model = Payment def dispatch(self, request, *args, **kwargs): - if not self.request.user.is_authenticated or \ - not self.request.user.registration.is_admin \ - and (self.request.user.registration not in self.get_object().registrations.all() - or self.get_object().valid is not False): + payment = self.get_object() + + # An external user has the link for the payment + token = request.GET.get('token', "") + if token and token == payment.token: + return super().dispatch(request, *args, **kwargs) + + if not request.user.is_authenticated: return self.handle_no_permission() + + if not request.user.registration.is_admin: + if request.user.registration.is_volunteer \ + and payment.tournament not in request.user.registration.organized_tournaments.all(): + return self.handle_no_permission() + + if request.user.registration.is_student \ + and request.user.registration not in payment.registrations.all(): + return self.handle_no_permission() + + if request.user.registration.is_coach \ + and request.user.registration.team != payment.team: + return self.handle_no_permission() + return super().dispatch(request, *args, **kwargs) def get(self, request, *args, **kwargs): payment = self.get_object() - checkout_intent = payment.create_checkout_intent() + if payment.valid is not False: + raise PermissionDenied(_("The payment is already valid or pending validation.")) + checkout_intent = payment.create_checkout_intent() return redirect(checkout_intent["redirectUrl"]) @@ -558,9 +578,10 @@ class PaymentHelloAssoReturnView(DetailView): def get(self, request, *args, **kwargs): checkout_id = request.GET.get("checkoutIntentId") - payment = Payment.objects.get(checkout_intent_id=checkout_id).exclude(valid=True) - if payment != self.get_object(): - messages.error(request, _("The payment is not found or is already validated.")) + payment = self.get_object() + payment_qs = Payment.objects.exclude(valid=True).filter(checkout_intent_id=checkout_id).filter(pk=payment.pk) + if not payment_qs.exists(): + messages.error(request, _("The payment is not found or is already validated."), "danger") return redirect("index") team = payment.team @@ -580,18 +601,18 @@ class PaymentHelloAssoReturnView(DetailView): return_type = request.GET.get("type") if return_type == "error": messages.error(request, format_lazy(_("An error occurred during the payment: {error}"), - error=request.GET.get("error"))) + error=request.GET.get("error")), "danger") return error_response elif return_type == "return": code = request.GET.get("code") if code == "refused": - messages.error(request, _("The payment has been refused.")) + messages.error(request, _("The payment has been refused."), "danger") return error_response - elif code != "success": - messages.error(request, format_lazy(_("The return code is unknown: {code}"), code=code)) + elif code != "succeeded": + messages.error(request, format_lazy(_("The return code is unknown: {code}"), code=code), "danger") return error_response else: - messages.error(request, format_lazy(_("The return type is unknown: {type}"), type=return_type)) + messages.error(request, format_lazy(_("The return type is unknown: {type}"), type=return_type), "danger") return error_response checkout_intent = payment.get_checkout_intent() @@ -608,7 +629,7 @@ class PaymentHelloAssoReturnView(DetailView): "and will be automatically done. " "If it is not the case, please contact us.")) - if request.user.registration in payment.registrations.all(): + if not request.user.is_anonymous and request.user.registration in payment.registrations.all(): success_response = redirect("registration:user_detail", args=(request.user.pk,)) elif right_to_see: success_response = redirect("participation:team_detail", args=(team.pk,))