Allow anonymous users to perform a payment using a special auth token
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
This commit is contained in:
parent
8d08b18d08
commit
b16b6e422f
|
@ -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 <emmy.danello@animath.fr>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\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}"
|
||||
|
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
||||
|
|
|
@ -79,17 +79,39 @@
|
|||
<div class="card-body">
|
||||
<div class="tab-content" id="payment-form">
|
||||
<div class="tab-pane fade show active" id="credit-card" role="tabpanel" aria-labelledby="credit-card-tab">
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<div class="text-center">
|
||||
<a href="{% url "registration:payment_hello_asso" pk=payment.pk %}" class="btn btn-primary">
|
||||
<i class="fas fa-credit-card"></i> Aller sur la page Hello Asso
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Si un tiers doit payer pour vous (parents, lycée,…), vous pouvez lui transmettre le lien pour
|
||||
payer pour vous :
|
||||
</p>
|
||||
|
||||
<div class="text-center border border-1 my-3 p-2 border-danger bg-body-tertiary shadow-lg rounded">
|
||||
{% url "registration:payment_hello_asso" pk=payment.pk as payment_url %}
|
||||
{{ request.scheme }}://{{ request.site.domain }}{{ payment_url }}?token={{ payment.token }}
|
||||
<a id="copyIcon" href="#"
|
||||
data-bs-title="Copié !"
|
||||
onclick="event.preventDefault();copyToClipboard('{{ request.scheme }}://{{ request.site.domain }}{{ payment_url }}?token={{ payment.token }}')">
|
||||
<i class="fas fa-copy"></i> Copier
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="bank-transfer" role="tabpanel" aria-labelledby="bank-transfer-tab">
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -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,))
|
||||
|
|
Loading…
Reference in New Issue