Compare commits
17 Commits
2155275627
...
de22a12e85
Author | SHA1 | Date | |
---|---|---|---|
|
de22a12e85 | ||
|
415d83acc7 | ||
|
eb7e7c1579 | ||
|
348004320c | ||
|
9829541289 | ||
|
1e1fef7a7b | ||
|
d0c9256c5b | ||
|
83300ad4b7 | ||
|
92408b359b | ||
|
01ba0a1df9 | ||
|
207af441a0 | ||
|
2a2786ba6d | ||
|
1d01376703 | ||
|
6e35bdc0b3 | ||
|
9380fbaaf7 | ||
|
295717256f | ||
|
87038dd6f4 |
15
.gitignore
vendored
@ -15,16 +15,6 @@ coverage
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# PyCharm project settings
|
||||
.idea
|
||||
|
||||
@ -32,7 +22,7 @@ coverage
|
||||
.vscode
|
||||
|
||||
# Local data
|
||||
secrets.py
|
||||
settings_local.py
|
||||
*.log
|
||||
media/
|
||||
output/
|
||||
@ -42,6 +32,3 @@ output/
|
||||
env/
|
||||
venv/
|
||||
db.sqlite3
|
||||
|
||||
# Don't git index
|
||||
whoosh_index/
|
||||
|
@ -7,7 +7,7 @@ py311:
|
||||
image: python:3.11-alpine
|
||||
before_script:
|
||||
- apk add --no-cache libmagic
|
||||
- apk add --no-cache git # Useful for django-haystack, remove when the newer versions are in PyPI
|
||||
- apk add --no-cache gettext git # Useful for django-haystack, remove when the newer versions are in PyPI
|
||||
- pip install tox --no-cache-dir
|
||||
script: tox -e py311
|
||||
|
||||
@ -16,7 +16,7 @@ py312:
|
||||
image: python:3.12-alpine
|
||||
before_script:
|
||||
- apk add --no-cache libmagic
|
||||
- apk add --no-cache git # Useful for django-haystack, remove when the newer versions are in PyPI
|
||||
- apk add --no-cache gettext git # Useful for django-haystack, remove when the newer versions are in PyPI
|
||||
- pip install tox --no-cache-dir
|
||||
script: tox -e py312
|
||||
|
||||
|
BIN
docs/_static/img/payment_bank_transfer.png
vendored
Normal file
After Width: | Height: | Size: 118 KiB |
BIN
docs/_static/img/payment_grouped.png
vendored
Normal file
After Width: | Height: | Size: 115 KiB |
BIN
docs/_static/img/payment_hello_asso_confirmation.png
vendored
Normal file
After Width: | Height: | Size: 107 KiB |
BIN
docs/_static/img/payment_hello_asso_step_1.png
vendored
Normal file
After Width: | Height: | Size: 101 KiB |
BIN
docs/_static/img/payment_hello_asso_step_2.png
vendored
Normal file
After Width: | Height: | Size: 87 KiB |
BIN
docs/_static/img/payment_index.png
vendored
Normal file
After Width: | Height: | Size: 120 KiB |
BIN
docs/_static/img/payment_scholarship.png
vendored
Normal file
After Width: | Height: | Size: 94 KiB |
@ -29,6 +29,7 @@ author = "Animath"
|
||||
# ones.
|
||||
extensions = [
|
||||
"sphinx_rtd_theme",
|
||||
"sphinx_rtd_dark_mode",
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
@ -58,3 +59,5 @@ html_theme = 'sphinx_rtd_theme'
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
default_dark_mode = True
|
||||
|
@ -1,2 +1,3 @@
|
||||
sphinx>=3.3
|
||||
sphinx-rtd-theme>=0.5
|
||||
sphinx-rtd-theme>=2.0
|
||||
sphinx_rtd_dark_mode>=1.3.0
|
||||
|
117
docs/user.rst
@ -179,16 +179,125 @@ Payer son inscription
|
||||
---------------------
|
||||
|
||||
Une fois votre inscription validée, il vous faudra payer votre inscription. Les frais s'élèvent à
|
||||
23 € par élève, sauf pour les élèves boursièr⋅es qui en sont exonéré⋅es. Les encadrant⋅es n'ont pas
|
||||
à payer.
|
||||
21 € par élève, sauf pour les élèves boursièr⋅es qui en sont exonéré⋅es. Les encadrant⋅es n'ont pas
|
||||
à payer. Pour la finale, les frais sont de 35 € par élève.
|
||||
|
||||
.. note::
|
||||
Ces frais couvrent une partie des frais de restauration et d'hébergement. L'organisation reste
|
||||
bénévole.
|
||||
|
||||
.. TODO
|
||||
Il est possible de payer par carte bancaire ou virement bancaire. Pour d'autres types de paiement,
|
||||
merci de nous contacter.
|
||||
|
||||
Pour payer, si votre équipe est bien validée, vous pouvez vous rendre sur la page de votre compte
|
||||
ou celle de votre équipe, et cliquer sur le bouton « Modifier le paiement », qui devrais désormais
|
||||
apparaître. Vous pouvez également utiliser le lien présent dans le volet « Informations ».
|
||||
|
||||
.. image:: /_static/img/payment_index.png
|
||||
:alt: Page de paiement
|
||||
|
||||
.. note::
|
||||
Cette section sera mise à jour plus tard.
|
||||
|
||||
Vous recevrez un mail de rappel chaque semaine. Le paiement doit être effectué avant le début du
|
||||
tournoi, sans quoi votre participation pourrait être refusée. En cas de difficultés de paiement,
|
||||
merci de nous contacter.
|
||||
|
||||
Carte bancaire
|
||||
""""""""""""""
|
||||
|
||||
La façon la plus simple de payer son inscription est de payer par carte bancaire. Animath utilise
|
||||
`Hello Asso <https://helloasso.com/>`_ en guise de solution de paiements en ligne.
|
||||
|
||||
Il vous suffit de cliquer sur le bouton « Aller à la page Hello Asso ». Vous serez redirigé⋅e ensuite
|
||||
vers la page de paiement.
|
||||
|
||||
.. warning::
|
||||
|
||||
Pour procéder au paiement, si vous êtes mineur⋅e, vous devrez demander à un⋅e adulte de payer à
|
||||
votre place. Il est important dans la suite de bien mettre les coordonnées du payeur ou de la payeuse,
|
||||
majeur⋅e, et non celles de l'élève.
|
||||
|
||||
.. image:: /_static/img/payment_hello_asso_step_1.png
|
||||
:alt: Formulaire de paiement Hello Asso
|
||||
|
||||
La personne qui paie peut rentrer ses informations demandées (nom, prénom, e-mail, date de naissance).
|
||||
|
||||
Notez que, par défaut, Hello Asso ajoute automatiquement une participation à ses frais de fonctionnement,
|
||||
d'environ 15 à 20 % du prix payé. Ces frais ne sont pas obligatoires, ne sont pas versés à Animath et
|
||||
représentent la seule source de revenus à Hello Asso. En effet : Animath ne verse aucune commission lors
|
||||
de ses transactions, et seules les contributions volontaires financent leur service.
|
||||
|
||||
Sur la page suivante, vous pouvez indiquer vos coordonnées bancaires :
|
||||
|
||||
.. image:: /_static/img/payment_hello_asso_step_2.png
|
||||
:alt: Formulaire de paiement Hello Asso - coordonnées bancaires
|
||||
|
||||
Vous devez ensuite éventuellement confirmer votre paiement auprès de votre banque.
|
||||
|
||||
Une fois ceci fait, vous êtes automatiquement redirigé⋅es vers la plateforme du TFJM² :
|
||||
|
||||
.. image:: /_static/img/payment_hello_asso_confirmation.png
|
||||
:alt: Confirmation de paiement Hello Asso
|
||||
|
||||
Il se peut que la validation ne soit pas instantanée. Elle peut prendre au plus quelques minutes.
|
||||
Si le délai est plus long, merci de nous contacter.
|
||||
|
||||
Vous recevrez ensuite un mail de confirmation de la plateforme, ainsi qu'un justificatif de paiement
|
||||
de la part de Hello Asso.
|
||||
|
||||
|
||||
Carte bancaire - paiement par un tiers
|
||||
""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
Il est possible, si nécessaire, de faire payer l'inscription par carte bancaire par un tiers. Pour cela,
|
||||
vous pouvez lui transmettre le lien de paiement qui apparaît au centre de l'écran. Cela est notamment
|
||||
utile pour faire payer l'inscription par un établissement scolaire, ou par des parents.
|
||||
|
||||
L'interface de paiement sera ensuite identique.
|
||||
|
||||
|
||||
Virement bancaire
|
||||
"""""""""""""""""
|
||||
|
||||
Il est possible de payer par virement bancaire. Pour cela, vous pouvez ouvrir l'onglet virement bancaire :
|
||||
|
||||
.. image:: /_static/img/payment_bank_transfer.png
|
||||
:alt: Formulaire de paiement par virement bancaire
|
||||
|
||||
Pour effectuer le virement, merci de mettre en référence du virement « TFJMpu » suivi du nom et du prénom de l'élève.
|
||||
|
||||
Les coordonnées bancaires sont :
|
||||
|
||||
* IBAN : FR76 1027 8065 0000 0206 4290 127
|
||||
* BIC : CMCIFR2A
|
||||
|
||||
Une fois le paiment effectué, vous pouvez envoyer une preuve de virement via le formulaire ci-dessus. Le paiement
|
||||
sera ensuite validé manuellement par les organisateur⋅rices après réception.
|
||||
|
||||
Si vous avez besoin d'une facture, merci de nous contacter.
|
||||
|
||||
|
||||
Exonération - boursièr⋅es
|
||||
"""""""""""""""""""""""""
|
||||
|
||||
Si vous bénéficiez d'une bourse, vous pouvez être exonéré⋅es des frais d'inscription. Pour cela, il vous suffit
|
||||
de nous envoyer une copie de votre notification de bourse, ou tout autre document justifiant de votre situation.
|
||||
Vous pouvez envoyer ce document en vous rendant sur l'onglet dédié :
|
||||
|
||||
.. image:: /_static/img/payment_scholarship.png
|
||||
:alt: Formulaire de soumission de notification de bourse
|
||||
|
||||
|
||||
Paiements groupés
|
||||
"""""""""""""""""
|
||||
|
||||
Il est possible de payer en une seule fois pour toute l'équipe. Cela est notamment utile si l'inscription est
|
||||
payée par l'établissement. Pour cela, il suffit de cliquer sur le bouton « Regrouper les paiements de mon équipe ».
|
||||
Cela a pour effet d'unifier les paiements de l'équipe, et de ne pas demander à chaque membre de payer individuellement.
|
||||
Attention : cette fonction n'est possible que si aucun membre de l'équipe n'a encore payé son inscription.
|
||||
|
||||
.. image:: /_static/img/payment_grouped.png
|
||||
:alt: Page de paiement groupé
|
||||
|
||||
|
||||
Envoyer ses solutions
|
||||
|
@ -12,8 +12,9 @@ class ParticipationConfig(AppConfig):
|
||||
name = 'participation'
|
||||
|
||||
def ready(self):
|
||||
from participation.signals import create_notes, create_team_participation, update_mailing_list
|
||||
pre_save.connect(update_mailing_list, "participation.Team")
|
||||
post_save.connect(create_team_participation, "participation.Team")
|
||||
post_save.connect(create_notes, "participation.Passage")
|
||||
post_save.connect(create_notes, "participation.Pool")
|
||||
from participation import signals
|
||||
pre_save.connect(signals.update_mailing_list, "participation.Team")
|
||||
post_save.connect(signals.create_team_participation, "participation.Team")
|
||||
post_save.connect(signals.create_payments, "participation.Participation")
|
||||
post_save.connect(signals.create_notes, "participation.Passage")
|
||||
post_save.connect(signals.create_notes, "participation.Pool")
|
||||
|
@ -2,7 +2,8 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from typing import Union
|
||||
|
||||
from participation.models import Note, Participation, Passage, Pool, Team
|
||||
from participation.models import Note, Participation, Passage, Pool, Team, Tournament
|
||||
from registration.models import Payment
|
||||
from tfjm.lists import get_sympa_client
|
||||
|
||||
|
||||
@ -36,6 +37,41 @@ def update_mailing_list(instance: Team, raw, **_):
|
||||
f"{coach.user.first_name} {coach.user.last_name}")
|
||||
|
||||
|
||||
def create_payments(instance: Participation, created, raw, **_):
|
||||
"""
|
||||
When a participation got created, create an associated payment.
|
||||
"""
|
||||
if instance.valid and not raw:
|
||||
for student in instance.team.students.all():
|
||||
payment_qs = Payment.objects.filter(registrations=student, final=False)
|
||||
if payment_qs.exists():
|
||||
payment = payment_qs.get()
|
||||
else:
|
||||
payment = Payment.objects.create()
|
||||
payment.registrations.add(student)
|
||||
payment.save()
|
||||
payment.amount = instance.tournament.price
|
||||
if payment.amount == 0:
|
||||
payment.type = "free"
|
||||
payment.valid = True
|
||||
payment.save()
|
||||
|
||||
if instance.final:
|
||||
for student in instance.team.students.all():
|
||||
payment_qs = Payment.objects.filter(registrations=student, final=True)
|
||||
if payment_qs.exists():
|
||||
payment = payment_qs.get()
|
||||
else:
|
||||
payment = Payment.objects.create(final=True)
|
||||
payment.registrations.add(student)
|
||||
payment.save()
|
||||
payment.amount = Tournament.final_tournament().price
|
||||
if payment.amount == 0:
|
||||
payment.type = "free"
|
||||
payment.valid = True
|
||||
payment.save()
|
||||
|
||||
|
||||
def create_notes(instance: Union[Passage, Pool], raw, **_):
|
||||
if not raw:
|
||||
if isinstance(instance, Pool):
|
||||
|
@ -5,16 +5,42 @@
|
||||
<title>Équipe validée – TFJM²</title>
|
||||
</head>
|
||||
<body>
|
||||
Bonjour,<br/>
|
||||
<br/>
|
||||
Félicitations ! Votre équipe « {{ team.name }} » ({{ team.trigram }}) est désormais validée ! Vous êtes désormais apte
|
||||
à travailler sur vos problèmes. Vous pourrez ensuite envoyer vos solutions sur la plateforme.<br>
|
||||
Les organisateurs vous adressent ce message :<br/>
|
||||
<br/>
|
||||
{{ message }}<br />
|
||||
<br/>
|
||||
Cordialement,<br/>
|
||||
<br/>
|
||||
Le comité d'organisation du TFJM²
|
||||
<p>
|
||||
Bonjour {{ registration }},
|
||||
</p>
|
||||
<p>
|
||||
Félicitations ! Votre équipe « {{ team.name }} » ({{ team.trigram }}) est désormais validée ! Vous êtes désormais
|
||||
apte à travailler sur vos problèmes. Vous pourrez ensuite envoyer vos solutions sur la plateforme.
|
||||
</p>
|
||||
|
||||
{% if payment %}
|
||||
<p>
|
||||
Vous devez désormais vous acquitter de vos frais d'inscription, de {{ payment.amount }} € par élève.
|
||||
Vous pouvez payer par carte bancaire ou par virement bancaire. Vous trouverez les informations
|
||||
sur <a href="https://{{ domain }}{% url 'registration:update_payment' pk=payment.pk %}">la page de paiement</a>.
|
||||
Si vous disposez d'une bourse, l'inscription est gratuite, mais vous devez soumettre un justificatif
|
||||
sur la même page.
|
||||
</p>
|
||||
{% elif registration.is_coach and team.participation.tournament.amount %}
|
||||
<p>
|
||||
Votre équipe doit désormais s'acquitter des frais d'inscription de {{ team.participation.tournament.amount }} €
|
||||
par élève (les encadrant⋅es sont exonéré⋅es). Les élèves qui disposent d'une bourse sont exonéré⋅es de ces frais.
|
||||
Vous pouvez suivre l'état des paiements sur
|
||||
<a href="https://{{ domain }}{% url 'participation:team_detail' pk=team.pk %}">la page de votre équipe</a>.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if message %}
|
||||
<p>
|
||||
Les organisateur⋅ices vous adressent ce message :
|
||||
</p>
|
||||
<p>
|
||||
{{ message }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<p>
|
||||
Le comité d'organisation du TFJM²
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,12 +1,23 @@
|
||||
Bonjour,
|
||||
Bonjour {{ registration }},
|
||||
|
||||
Félicitations ! Votre équipe « {{ team.name }} » ({{ team.trigram }}) est désormais validée ! Vous êtes désormais apte
|
||||
à travailler sur vos problèmes. Vous pourrez ensuite envoyer vos solutions sur la plateforme.
|
||||
|
||||
Les organisateurs vous adressent ce message :
|
||||
{% if team.participation.amount %}
|
||||
Vous devez désormais vous acquitter de vos frais d'inscription, de {{ team.participation.amount }} €.
|
||||
Vous pouvez payer par carte bancaire ou par virement bancaire. Vous trouverez les informations
|
||||
sur la page de paiement que vous pouvez retrouver sur votre compte :
|
||||
https://{{ domain }}{% url 'registration:my_account_detail' %}
|
||||
Si vous disposez d'une bourse, l'inscription est gratuite, mais vous devez soumettre un justificatif
|
||||
sur la même page.
|
||||
{% elif registration.is_coach and team.participation.tournament.amount %}
|
||||
Votre équipe doit désormais s'acquitter des frais d'inscription de {{ team.participation.tournament.amount }} €
|
||||
par élève (les encadrant⋅es sont exonéré⋅es). Les élèves qui disposent d'une bourse sont exonéré⋅es de ces frais.
|
||||
Vous pouvez suivre l'état des paiements sur la page de votre équipe :
|
||||
https://{{ domain }}{% url 'participation:team_detail' pk=team.pk %}
|
||||
{% endif %}
|
||||
{% if message %}
|
||||
Les organisateurices vous adressent ce message :
|
||||
|
||||
{{ message }}
|
||||
|
||||
Cordialement,
|
||||
|
||||
{% endif %}
|
||||
Le comité d'organisation du TFJM²
|
||||
|
@ -4,10 +4,12 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core import mail
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
from django.test import LiveServerTestCase, override_settings, TestCase
|
||||
from django.urls import reverse
|
||||
from registration.models import CoachRegistration, StudentRegistration
|
||||
from registration.models import CoachRegistration, Payment, StudentRegistration
|
||||
|
||||
from .models import Participation, Team, Tournament
|
||||
|
||||
@ -515,6 +517,616 @@ class TestStudentParticipation(TestCase):
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
|
||||
|
||||
class TestPayment(TestCase):
|
||||
"""
|
||||
Tests that are relative to a payment
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.superuser = User.objects.create_superuser(
|
||||
username="admin",
|
||||
email="admin@example.com",
|
||||
password="admin",
|
||||
)
|
||||
self.tournament = Tournament.objects.create(
|
||||
name="France",
|
||||
place="Here",
|
||||
price=21,
|
||||
)
|
||||
self.team = Team.objects.create(
|
||||
name="Super team",
|
||||
trigram="AAA",
|
||||
access_code="azerty",
|
||||
)
|
||||
self.user = User.objects.create(
|
||||
first_name="Toto",
|
||||
last_name="Toto",
|
||||
email="toto@example.com",
|
||||
password="toto",
|
||||
)
|
||||
StudentRegistration.objects.create(
|
||||
user=self.user,
|
||||
team=self.team,
|
||||
student_class=12,
|
||||
address="1 Rue de Rivoli",
|
||||
zip_code=75001,
|
||||
city="Paris",
|
||||
school="Earth",
|
||||
give_contact_to_animath=True,
|
||||
email_confirmed=True,
|
||||
)
|
||||
self.second_user = User.objects.create(
|
||||
first_name="Lalala",
|
||||
last_name="Lalala",
|
||||
email="lalala@example.com",
|
||||
password="lalala",
|
||||
)
|
||||
StudentRegistration.objects.create(
|
||||
user=self.second_user,
|
||||
team=self.team,
|
||||
student_class=11,
|
||||
address="1 Rue de Rivoli",
|
||||
zip_code=75001,
|
||||
city="Paris",
|
||||
school="Moon",
|
||||
give_contact_to_animath=True,
|
||||
email_confirmed=True,
|
||||
)
|
||||
self.coach = User.objects.create(
|
||||
first_name="Coach",
|
||||
last_name="Coach",
|
||||
email="coach@example.com",
|
||||
password="coach",
|
||||
)
|
||||
CoachRegistration.objects.create(
|
||||
user=self.coach,
|
||||
team=self.team,
|
||||
address="1 Rue de Rivoli",
|
||||
zip_code=75001,
|
||||
city="Paris",
|
||||
)
|
||||
|
||||
self.team.participation.tournament = self.tournament
|
||||
self.team.participation.valid = True
|
||||
self.team.participation.save()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_check_payments_exists(self):
|
||||
"""
|
||||
Check that users in a validated team have an invalid payment, but not for the final,
|
||||
and that coaches are not concerned.
|
||||
"""
|
||||
self.assertTrue(Payment.objects.filter(final=False, valid=False, type='',
|
||||
registrations=self.user.registration).exists())
|
||||
self.assertTrue(Payment.objects.filter(final=False, valid=False, type='',
|
||||
registrations=self.second_user.registration).exists())
|
||||
self.assertFalse(Payment.objects.filter(final=False, valid=False, type='',
|
||||
registrations=self.coach.registration).exists())
|
||||
|
||||
self.assertFalse(Payment.objects.filter(final=True, valid=False, type='',
|
||||
registrations=self.user.registration).exists())
|
||||
self.assertFalse(Payment.objects.filter(final=True, valid=False, type='',
|
||||
registrations=self.second_user.registration).exists())
|
||||
self.assertFalse(Payment.objects.filter(final=True, valid=False, type='',
|
||||
registrations=self.coach.registration).exists())
|
||||
|
||||
def test_load_payment_page(self):
|
||||
"""
|
||||
Ensure that the payment page loads correctly.
|
||||
"""
|
||||
response = self.client.get(reverse('participation:team_detail', args=(self.team.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse('registration:user_detail', args=(self.user.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse('participation:tournament_payments', args=(self.tournament.pk,)))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
payment = Payment.objects.get(registrations=self.user.registration, final=False)
|
||||
response = self.client.get(reverse('registration:update_payment', args=(payment.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_bank_transfer_payment(self):
|
||||
"""
|
||||
Try to send a bank transfer.
|
||||
"""
|
||||
payment = Payment.objects.get(registrations=self.user.registration, final=False)
|
||||
response = self.client.get(reverse('registration:update_payment', args=(payment.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(reverse('registration:update_payment', args=(payment.pk,)),
|
||||
data={'type': "bank_transfer",
|
||||
'additional_information': "This is a bank transfer"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFormError(response.context['form'], 'receipt',
|
||||
["This field is required.", "You must upload your receipt."])
|
||||
payment.refresh_from_db()
|
||||
self.assertFalse(payment.valid)
|
||||
self.assertEqual(payment.type, "")
|
||||
|
||||
# README is not a valid PDF file
|
||||
response = self.client.post(reverse('registration:update_payment', args=(payment.pk,)),
|
||||
data={'type': "bank_transfer",
|
||||
'additional_information': "This is a bank transfer",
|
||||
'receipt': open("README.md", "rb")})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFormError(response.context['form'], 'receipt',
|
||||
["The uploaded file must be a PDF, PNG of JPEG file."])
|
||||
self.assertFalse(payment.valid)
|
||||
self.assertEqual(payment.type, "")
|
||||
|
||||
# Don't send too large files
|
||||
response = self.client.post(reverse('registration:update_payment', args=(payment.pk,)),
|
||||
data={'type': "bank_transfer",
|
||||
'additional_information': "This is a bank transfer",
|
||||
'receipt': SimpleUploadedFile(
|
||||
"file.pdf",
|
||||
content=int(0).to_bytes(2000001, "big"),
|
||||
content_type="application/pdf"),
|
||||
})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFormError(response.context['form'], 'receipt',
|
||||
["The uploaded file size must be under 2 Mo."])
|
||||
self.assertFalse(payment.valid)
|
||||
self.assertEqual(payment.type, "")
|
||||
|
||||
response = self.client.post(reverse('registration:update_payment', args=(payment.pk,)),
|
||||
data={'type': "bank_transfer",
|
||||
'additional_information': "This is a bank transfer",
|
||||
'receipt': open("tfjm/static/Fiche_sanitaire.pdf", "rb")})
|
||||
self.assertRedirects(response, reverse('participation:team_detail', args=(self.team.pk,)), 302, 200)
|
||||
payment.refresh_from_db()
|
||||
self.assertIsNone(payment.valid)
|
||||
self.assertEqual(payment.type, "bank_transfer")
|
||||
self.assertEqual(payment.additional_information, "This is a bank transfer")
|
||||
self.assertIsNotNone(payment.receipt)
|
||||
|
||||
response = self.client.get(reverse('registration:update_payment', args=(payment.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_scholarship(self):
|
||||
"""
|
||||
Try to don't pay because of a scholarship.
|
||||
"""
|
||||
payment = Payment.objects.get(registrations=self.user.registration, final=False)
|
||||
response = self.client.get(reverse('registration:update_payment', args=(payment.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(reverse('registration:update_payment', args=(payment.pk,)),
|
||||
data={'type': "scholarship",
|
||||
'additional_information': "I don't have to pay because I have a scholarship"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFormError(response.context['form'], 'receipt',
|
||||
["This field is required.", "You must upload your receipt."])
|
||||
payment.refresh_from_db()
|
||||
self.assertFalse(payment.valid)
|
||||
self.assertEqual(payment.type, "")
|
||||
self.assertEqual(payment.amount, self.tournament.price)
|
||||
|
||||
# README is not a valid PDF file
|
||||
response = self.client.post(reverse('registration:update_payment', args=(payment.pk,)),
|
||||
data={'type': "scholarship",
|
||||
'additional_information': "I don't have to pay because I have a scholarship",
|
||||
'receipt': open("README.md", "rb")})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFormError(response.context['form'], 'receipt',
|
||||
["The uploaded file must be a PDF, PNG of JPEG file."])
|
||||
self.assertFalse(payment.valid)
|
||||
self.assertEqual(payment.type, "")
|
||||
self.assertEqual(payment.amount, self.tournament.price)
|
||||
|
||||
# Don't send too large files
|
||||
response = self.client.post(reverse('registration:update_payment', args=(payment.pk,)),
|
||||
data={'type': "scholarship",
|
||||
'additional_information': "I don't have to pay because I have a scholarship",
|
||||
'receipt': SimpleUploadedFile(
|
||||
"file.pdf",
|
||||
content=int(0).to_bytes(2000001, "big"),
|
||||
content_type="application/pdf"),
|
||||
})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFormError(response.context['form'], 'receipt',
|
||||
["The uploaded file size must be under 2 Mo."])
|
||||
self.assertFalse(payment.valid)
|
||||
self.assertEqual(payment.type, "")
|
||||
self.assertEqual(payment.amount, self.tournament.price)
|
||||
|
||||
response = self.client.post(reverse('registration:update_payment', args=(payment.pk,)),
|
||||
data={'type': "scholarship",
|
||||
'additional_information': "I don't have to pay because I have a scholarship",
|
||||
'receipt': open("tfjm/static/Fiche_sanitaire.pdf", "rb")})
|
||||
self.assertRedirects(response, reverse('participation:team_detail', args=(self.team.pk,)), 302, 200)
|
||||
payment.refresh_from_db()
|
||||
self.assertIsNone(payment.valid)
|
||||
self.assertEqual(payment.type, "scholarship")
|
||||
self.assertEqual(payment.additional_information, "I don't have to pay because I have a scholarship")
|
||||
self.assertIsNotNone(payment.receipt)
|
||||
self.assertEqual(payment.amount, 0)
|
||||
|
||||
response = self.client.get(reverse('registration:update_payment', args=(payment.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_other(self):
|
||||
"""
|
||||
Try to send a different type of payment.
|
||||
"""
|
||||
payment = Payment.objects.get(registrations=self.user.registration, final=False)
|
||||
response = self.client.get(reverse('registration:update_payment', args=(payment.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(reverse('registration:update_payment', args=(payment.pk,)),
|
||||
data={'type': "other"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFormError(response.context['form'], 'additional_information',
|
||||
["This field is required."])
|
||||
payment.refresh_from_db()
|
||||
self.assertFalse(payment.valid)
|
||||
self.assertEqual(payment.type, "")
|
||||
self.assertEqual(payment.amount, self.tournament.price)
|
||||
|
||||
response = self.client.post(reverse('registration:update_payment', args=(payment.pk,)),
|
||||
data={'type': "other",
|
||||
'additional_information': "Why should I pay"})
|
||||
self.assertRedirects(response, reverse('participation:team_detail', args=(self.team.pk,)), 302, 200)
|
||||
payment.refresh_from_db()
|
||||
self.assertIsNone(payment.valid)
|
||||
self.assertEqual(payment.type, "other")
|
||||
self.assertEqual(payment.additional_information, "Why should I pay")
|
||||
self.assertIsNotNone(payment.receipt)
|
||||
self.assertEqual(payment.amount, self.tournament.price)
|
||||
|
||||
response = self.client.get(reverse('registration:update_payment', args=(payment.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_group(self):
|
||||
payment = Payment.objects.get(registrations=self.user.registration, final=False)
|
||||
self.assertFalse(payment.grouped)
|
||||
|
||||
response = self.client.get(reverse('registration:update_payment', args=(payment.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse('registration:update_payment_group_mode', args=(payment.pk,)))
|
||||
self.assertRedirects(response, reverse('registration:update_payment', args=(payment.pk,)), 302, 200)
|
||||
payment.refresh_from_db()
|
||||
self.assertTrue(payment.grouped)
|
||||
self.assertEqual(Payment.objects.count(), 1)
|
||||
self.assertIn(self.user.registration, payment.registrations.all())
|
||||
self.assertIn(self.second_user.registration, payment.registrations.all())
|
||||
self.assertEqual(payment.amount, 2 * self.tournament.price)
|
||||
|
||||
def test_ungroup(self):
|
||||
"""
|
||||
Test to ungroup payments
|
||||
"""
|
||||
payment = Payment.objects.get(registrations=self.user.registration, final=False)
|
||||
self.client.get(reverse('registration:update_payment_group_mode', args=(payment.pk,)))
|
||||
payment.refresh_from_db()
|
||||
self.assertTrue(payment.grouped)
|
||||
|
||||
response = self.client.get(reverse('registration:update_payment', args=(payment.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse('registration:update_payment_group_mode', args=(payment.pk,)))
|
||||
self.assertRedirects(response, reverse('registration:update_payment', args=(payment.pk,)), 302, 200)
|
||||
payment.refresh_from_db()
|
||||
self.assertFalse(payment.grouped)
|
||||
self.assertEqual(Payment.objects.count(), 2)
|
||||
self.assertIn(self.user.registration, payment.registrations.all())
|
||||
self.assertNotIn(self.second_user.registration, payment.registrations.all())
|
||||
self.assertEqual(payment.amount, self.tournament.price)
|
||||
|
||||
def test_group_forbidden(self):
|
||||
"""
|
||||
Payment grouping is forbidden if at least one payment is already valid.
|
||||
"""
|
||||
payment = Payment.objects.get(registrations=self.user.registration, final=False)
|
||||
payment.valid = True
|
||||
payment.save()
|
||||
payment2 = Payment.objects.get(registrations=self.second_user.registration, final=False)
|
||||
response = self.client.get(reverse('registration:update_payment_group_mode', args=(payment.pk,)))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
response = self.client.get(reverse('registration:update_payment_group_mode', args=(payment2.pk,)))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_validate_payment(self):
|
||||
"""
|
||||
Try to validate a payment.
|
||||
"""
|
||||
payment = Payment.objects.get(registrations=self.user.registration, final=False)
|
||||
payment.type = "other"
|
||||
payment.valid = None
|
||||
payment.save()
|
||||
|
||||
response = self.client.get(reverse('registration:update_payment', args=(payment.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(reverse('registration:update_payment', args=(payment.pk,)),
|
||||
data={'valid': True})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertFalse(payment.valid)
|
||||
|
||||
self.client.force_login(self.superuser)
|
||||
|
||||
response = self.client.post(reverse('registration:update_payment', args=(payment.pk,)),
|
||||
data={'valid': True})
|
||||
self.assertRedirects(response, reverse('participation:team_detail', args=(self.team.pk,)), 302, 200)
|
||||
payment.refresh_from_db()
|
||||
self.assertTrue(payment.valid)
|
||||
|
||||
def test_invalidate_payment(self):
|
||||
"""
|
||||
Try to invalidate a payment.
|
||||
"""
|
||||
payment = Payment.objects.get(registrations=self.user.registration, final=False)
|
||||
payment.type = "other"
|
||||
payment.valid = None
|
||||
payment.save()
|
||||
|
||||
response = self.client.get(reverse('registration:update_payment', args=(payment.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.client.force_login(self.superuser)
|
||||
|
||||
response = self.client.post(reverse('registration:update_payment', args=(payment.pk,)),
|
||||
data={'valid': False})
|
||||
self.assertRedirects(response, reverse('participation:team_detail', args=(self.team.pk,)), 302, 200)
|
||||
payment.refresh_from_db()
|
||||
self.assertFalse(payment.valid)
|
||||
|
||||
def test_payment_reminder(self):
|
||||
"""
|
||||
Check that the payment reminder command works correctly.
|
||||
"""
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
|
||||
call_command('remind_payments')
|
||||
self.assertEqual(len(mail.outbox), 2)
|
||||
self.assertEqual(mail.outbox[0].subject, "[TFJM²] Rappel pour votre paiement")
|
||||
|
||||
payment = Payment.objects.get(registrations=self.user.registration, final=False)
|
||||
payment2 = Payment.objects.get(registrations=self.second_user.registration, final=False)
|
||||
payment.type = 'other'
|
||||
payment.valid = True
|
||||
payment.save()
|
||||
payment2.type = 'bank_transfer'
|
||||
payment2.valid = None
|
||||
payment2.save()
|
||||
|
||||
mail.outbox = []
|
||||
call_command('remind_payments')
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
|
||||
|
||||
@override_settings(HELLOASSO_TEST_ENDPOINT=True, ROOT_URLCONF="tfjm.helloasso.test_urls")
|
||||
class TestHelloAssoPayment(LiveServerTestCase):
|
||||
"""
|
||||
Tests that are relative to a HelloAsso
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.superuser = User.objects.create_superuser(
|
||||
username="admin",
|
||||
email="admin@example.com",
|
||||
password="admin",
|
||||
)
|
||||
self.tournament = Tournament.objects.create(
|
||||
name="France",
|
||||
place="Here",
|
||||
price=21,
|
||||
)
|
||||
self.team = Team.objects.create(
|
||||
name="Super team",
|
||||
trigram="AAA",
|
||||
access_code="azerty",
|
||||
)
|
||||
self.user = User.objects.create(
|
||||
first_name="Toto",
|
||||
last_name="Toto",
|
||||
email="toto@example.com",
|
||||
password="toto",
|
||||
)
|
||||
StudentRegistration.objects.create(
|
||||
user=self.user,
|
||||
team=self.team,
|
||||
student_class=12,
|
||||
address="1 Rue de Rivoli",
|
||||
zip_code=75001,
|
||||
city="Paris",
|
||||
school="Earth",
|
||||
give_contact_to_animath=True,
|
||||
email_confirmed=True,
|
||||
)
|
||||
self.coach = User.objects.create(
|
||||
first_name="Coach",
|
||||
last_name="Coach",
|
||||
email="coach@example.com",
|
||||
password="coach",
|
||||
)
|
||||
CoachRegistration.objects.create(
|
||||
user=self.coach,
|
||||
team=self.team,
|
||||
address="1 Rue de Rivoli",
|
||||
zip_code=75001,
|
||||
city="Paris",
|
||||
)
|
||||
|
||||
self.team.participation.tournament = self.tournament
|
||||
self.team.participation.valid = True
|
||||
self.team.participation.save()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
Site.objects.update(domain=self.live_server_url.replace("http://", ""))
|
||||
|
||||
def test_create_checkout_intent(self):
|
||||
with self.settings(HELLOASSO_TEST_ENDPOINT_URL=self.live_server_url):
|
||||
payment = Payment.objects.get(registrations=self.user.registration, final=False)
|
||||
checkout_intent = payment.create_checkout_intent()
|
||||
|
||||
self.assertIsNotNone(checkout_intent)
|
||||
self.assertEqual(checkout_intent['metadata'], {
|
||||
'payment_id': payment.pk,
|
||||
'users': [
|
||||
{
|
||||
'user_id': self.user.pk,
|
||||
'first_name': self.user.first_name,
|
||||
'last_name': self.user.last_name,
|
||||
'email': self.user.email,
|
||||
}
|
||||
],
|
||||
'final': False,
|
||||
'tournament_id': self.tournament.pk,
|
||||
})
|
||||
self.assertNotIn('order', checkout_intent)
|
||||
|
||||
checkout_intent_fetched = payment.get_checkout_intent()
|
||||
self.assertEqual(checkout_intent, checkout_intent_fetched)
|
||||
|
||||
# Don't create a new checkout intent if one already exists
|
||||
checkout_intent_new = payment.create_checkout_intent()
|
||||
self.assertEqual(checkout_intent, checkout_intent_new)
|
||||
|
||||
payment.refresh_from_db()
|
||||
self.assertEqual(payment.checkout_intent_id, checkout_intent['id'])
|
||||
self.assertFalse(payment.valid)
|
||||
|
||||
def test_helloasso_payment_success(self):
|
||||
"""
|
||||
Simulates the redirection to Hello Asso and the return for a successful payment.
|
||||
"""
|
||||
with self.settings(HELLOASSO_TEST_ENDPOINT_URL=self.live_server_url):
|
||||
payment = Payment.objects.get(registrations=self.user.registration, final=False)
|
||||
self.assertIsNone(payment.checkout_intent_id)
|
||||
self.assertFalse(payment.valid)
|
||||
|
||||
response = self.client.get(reverse('registration:update_payment', args=(payment.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse('registration:payment_hello_asso', args=(payment.pk,)),
|
||||
follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.redirect_chain[-1],
|
||||
(reverse('participation:team_detail', args=(self.team.pk,)), 302))
|
||||
self.assertIn("type=return", response.redirect_chain[1][0])
|
||||
self.assertIn("code=succeeded", response.redirect_chain[1][0])
|
||||
|
||||
payment.refresh_from_db()
|
||||
self.assertIsNotNone(payment.checkout_intent_id)
|
||||
self.assertTrue(payment.valid)
|
||||
|
||||
checkout_intent = payment.get_checkout_intent()
|
||||
self.assertIn('order', checkout_intent)
|
||||
|
||||
def test_helloasso_payment_refused(self):
|
||||
"""
|
||||
Simulates the redirection to Hello Asso and the return for a refused payment.
|
||||
"""
|
||||
with self.settings(HELLOASSO_TEST_ENDPOINT_URL=self.live_server_url):
|
||||
payment = Payment.objects.get(registrations=self.user.registration, final=False)
|
||||
checkout_intent = payment.create_checkout_intent()
|
||||
self.assertFalse(payment.valid)
|
||||
|
||||
response = self.client.get(reverse('registration:update_payment', args=(payment.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(checkout_intent['redirectUrl'] + "?refused", follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.redirect_chain[-1],
|
||||
(reverse('registration:update_payment', args=(payment.pk,)), 302))
|
||||
self.assertIn("type=return", response.redirect_chain[0][0])
|
||||
self.assertIn("code=refused", response.redirect_chain[0][0])
|
||||
|
||||
payment.refresh_from_db()
|
||||
self.assertFalse(payment.valid)
|
||||
|
||||
checkout_intent = payment.get_checkout_intent()
|
||||
self.assertNotIn('order', checkout_intent)
|
||||
|
||||
def test_helloasso_payment_error(self):
|
||||
"""
|
||||
Simulates the redirection to Hello Asso and the return for an errored payment.
|
||||
"""
|
||||
with self.settings(HELLOASSO_TEST_ENDPOINT_URL=self.live_server_url):
|
||||
payment = Payment.objects.get(registrations=self.user.registration, final=False)
|
||||
checkout_intent = payment.create_checkout_intent()
|
||||
self.assertFalse(payment.valid)
|
||||
|
||||
response = self.client.get(reverse('registration:update_payment', args=(payment.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(checkout_intent['redirectUrl'] + "?error", follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.redirect_chain[-1],
|
||||
(reverse('registration:update_payment', args=(payment.pk,)), 302))
|
||||
self.assertIn("type=error", response.redirect_chain[0][0])
|
||||
self.assertIn("error=", response.redirect_chain[0][0])
|
||||
|
||||
payment.refresh_from_db()
|
||||
self.assertFalse(payment.valid)
|
||||
|
||||
checkout_intent = payment.get_checkout_intent()
|
||||
self.assertNotIn('order', checkout_intent)
|
||||
|
||||
def test_anonymous_payment(self):
|
||||
"""
|
||||
Test to make a successful payment from an anonymous user, authenticated by token.
|
||||
"""
|
||||
self.client.logout()
|
||||
|
||||
with self.settings(HELLOASSO_TEST_ENDPOINT_URL=self.live_server_url):
|
||||
payment = Payment.objects.get(registrations=self.user.registration, final=False)
|
||||
self.assertIsNone(payment.checkout_intent_id)
|
||||
self.assertFalse(payment.valid)
|
||||
|
||||
response = self.client.get(reverse('registration:payment_hello_asso', args=(payment.pk,)),
|
||||
follow=True)
|
||||
self.assertRedirects(response,
|
||||
f"{reverse('login')}?next="
|
||||
f"{reverse('registration:payment_hello_asso', args=(payment.pk,))}")
|
||||
|
||||
response = self.client.get(
|
||||
reverse('registration:payment_hello_asso', args=(payment.pk,)) + "?token=" + payment.token,
|
||||
follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.redirect_chain[-1], (reverse('index'), 302))
|
||||
self.assertIn("type=return", response.redirect_chain[1][0])
|
||||
self.assertIn("code=succeeded", response.redirect_chain[1][0])
|
||||
|
||||
payment.refresh_from_db()
|
||||
self.assertIsNotNone(payment.checkout_intent_id)
|
||||
self.assertTrue(payment.valid)
|
||||
|
||||
checkout_intent = payment.get_checkout_intent()
|
||||
self.assertIn('order', checkout_intent)
|
||||
|
||||
def test_hello_asso_payment_verification(self):
|
||||
"""
|
||||
Check that a payment that is pending verification can be verified.
|
||||
"""
|
||||
with self.settings(HELLOASSO_TEST_ENDPOINT_URL=self.live_server_url):
|
||||
payment = Payment.objects.get(registrations=self.user.registration, final=False)
|
||||
self.assertFalse(payment.valid)
|
||||
|
||||
call_command('check_hello_asso')
|
||||
payment.refresh_from_db()
|
||||
self.assertFalse(payment.valid)
|
||||
|
||||
self.client.get(reverse('registration:payment_hello_asso', args=(payment.pk,)),
|
||||
follow=True)
|
||||
|
||||
payment.refresh_from_db()
|
||||
payment.valid = None
|
||||
payment.additional_information = ""
|
||||
payment.save()
|
||||
self.assertIsNone(payment.valid)
|
||||
|
||||
call_command('check_hello_asso')
|
||||
payment.refresh_from_db()
|
||||
self.assertTrue(payment.valid)
|
||||
self.assertTrue(payment.additional_information)
|
||||
|
||||
|
||||
class TestAdmin(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.user = User.objects.create_superuser(
|
||||
|
@ -244,25 +244,18 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView)
|
||||
if "validate" in self.request.POST:
|
||||
self.object.participation.valid = True
|
||||
self.object.participation.save()
|
||||
mail_context = dict(team=self.object, message=form.cleaned_data["message"])
|
||||
mail_plain = render_to_string("participation/mails/team_validated.txt", mail_context)
|
||||
mail_html = render_to_string("participation/mails/team_validated.html", mail_context)
|
||||
send_mail("[TFJM²] Équipe validée", mail_plain, None, [self.object.email], html_message=mail_html)
|
||||
|
||||
for student in self.object.students.all():
|
||||
payment_qs = Payment.objects.filter(registrations=student)
|
||||
if payment_qs.exists():
|
||||
payment = payment_qs.get()
|
||||
domain = Site.objects.first().domain
|
||||
for registration in self.object.participants.all():
|
||||
if registration.is_student and self.object.participation.tournament.amount:
|
||||
payment = Payment.objects.get(registrations=registration, final=False)
|
||||
else:
|
||||
payment = Payment.objects.create()
|
||||
payment.registrations.add(student)
|
||||
payment.save()
|
||||
payment.amount = self.object.participation.tournament.price
|
||||
if payment.amount == 0:
|
||||
payment.type = "free"
|
||||
payment.valid = True
|
||||
payment.save()
|
||||
|
||||
payment = None
|
||||
mail_context = dict(domain=domain, registration=registration, team=self.object, payment=payment,
|
||||
message=form.cleaned_data["message"])
|
||||
mail_plain = render_to_string("participation/mails/team_validated.txt", mail_context)
|
||||
mail_html = render_to_string("participation/mails/team_validated.html", mail_context)
|
||||
registration.user.email_user("[TFJM²] Équipe validée", mail_plain, html_message=mail_html)
|
||||
elif "invalidate" in self.request.POST:
|
||||
self.object.participation.valid = None
|
||||
self.object.participation.save()
|
||||
|
@ -12,8 +12,8 @@ class RegistrationConfig(AppConfig):
|
||||
name = 'registration'
|
||||
|
||||
def ready(self):
|
||||
from registration.signals import create_admin_registration, \
|
||||
set_username, send_email_link
|
||||
pre_save.connect(set_username, "auth.User")
|
||||
pre_save.connect(send_email_link, "auth.User")
|
||||
post_save.connect(create_admin_registration, "auth.User")
|
||||
from registration import signals
|
||||
pre_save.connect(signals.set_username, 'auth.User')
|
||||
pre_save.connect(signals.send_email_link, 'auth.User')
|
||||
pre_save.connect(signals.update_payment_amount, 'registration.Payment')
|
||||
post_save.connect(signals.create_admin_registration, 'auth.User')
|
||||
|
@ -226,6 +226,9 @@ class PaymentAdminForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["valid"].widget.choices[0] = ('unknown', _("Pending"))
|
||||
payment_type = kwargs.get('data', {}).get('type', "")
|
||||
self.fields['receipt'].required = payment_type in ["scholarship", "bank_transfer"]
|
||||
self.fields['additional_information'].required = payment_type in ["other"]
|
||||
|
||||
def clean_receipt(self):
|
||||
if "receipt" in self.files:
|
||||
|
@ -387,7 +387,7 @@ class StudentRegistration(ParticipantRegistration):
|
||||
'priority': 3,
|
||||
'content': content,
|
||||
})
|
||||
elif self.payment.valid is None:
|
||||
elif payment.valid is None:
|
||||
text = _("Your payment is under approval.")
|
||||
content = text
|
||||
informations.append({
|
||||
|
@ -41,3 +41,13 @@ def create_admin_registration(instance, **_):
|
||||
"""
|
||||
if instance.is_superuser:
|
||||
VolunteerRegistration.objects.get_or_create(user=instance, admin=True)
|
||||
|
||||
|
||||
def update_payment_amount(instance, **_):
|
||||
"""
|
||||
When a payment got created, ensure that the amount is set.
|
||||
"""
|
||||
if instance.type == 'free' or instance.type == 'scholarship':
|
||||
instance.amount = 0
|
||||
elif instance.pk:
|
||||
instance.amount = instance.registrations.count() * instance.tournament.price
|
||||
|
@ -24,25 +24,27 @@
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{% if payment.grouped %}
|
||||
{% blocktrans trimmed %}
|
||||
You want finally that each member pays its own registration? Then click on the button:
|
||||
{% endblocktrans %}
|
||||
<div class="text-center">
|
||||
<a href="{% url 'registration:update_payment_group_mode' pk=payment.pk %}" class="btn btn-warning">
|
||||
<i class="fas fa-user"></i> {% trans "Back to single payments" %}
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
{% blocktrans trimmed %}
|
||||
You want to pay for the registrations of all members of your team,
|
||||
or your school will pay for all registrations? Then click on the button:
|
||||
{% endblocktrans %}
|
||||
<div class="text-center">
|
||||
<a href="{% url 'registration:update_payment_group_mode' pk=payment.pk %}" class="btn btn-warning">
|
||||
<i class="fas fa-users"></i> {% trans "Group the payments of my team" %}
|
||||
</a>
|
||||
</div>
|
||||
{% if can_group %}
|
||||
{% if payment.grouped %}
|
||||
{% blocktrans trimmed %}
|
||||
You want finally that each member pays its own registration? Then click on the button:
|
||||
{% endblocktrans %}
|
||||
<div class="text-center">
|
||||
<a href="{% url 'registration:update_payment_group_mode' pk=payment.pk %}" class="btn btn-warning">
|
||||
<i class="fas fa-user"></i> {% trans "Back to single payments" %}
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
{% blocktrans trimmed %}
|
||||
You want to pay for the registrations of all members of your team,
|
||||
or your school will pay for all registrations? Then click on the button:
|
||||
{% endblocktrans %}
|
||||
<div class="text-center">
|
||||
<a href="{% url 'registration:update_payment_group_mode' pk=payment.pk %}" class="btn btn-warning">
|
||||
<i class="fas fa-users"></i> {% trans "Group the payments of my team" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
@ -199,13 +201,82 @@
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div id="form-content">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Update" %}</button>
|
||||
</form>
|
||||
{% if user.registration.is_volunteer %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div id="form-content">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Update" %}</button>
|
||||
</form>
|
||||
{% else %}
|
||||
{% if payment.type == 'helloasso' %}
|
||||
{% if payment.valid is True %}
|
||||
<div class="alert alert-success">
|
||||
{% with order=payment.get_checkout_intent.order %}
|
||||
{% trans "Your payment by credit card via Hello Asso is successfully validated." %}
|
||||
{% trans "The paid amount is" %} {% widthratio order.amount.total 100 1 %} €.
|
||||
{% if grouped %}
|
||||
{% trans "It includes the registrations of all members of the team." %}
|
||||
{% endif %}
|
||||
{% trans "The payer was " %} {{ order.payer.firstName }} {{ order.payer.lastName }}.
|
||||
{% trans "The payment was done on" %} {{ order.date }}.
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% elif payment.valid is None %}
|
||||
<div class="alert alert-warning">
|
||||
{% trans "The payment by credit card via Hello Asso is pending validation." %}
|
||||
{% trans "It should takes only few minutes. If it takes longer, please contact us." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if payment.valid is True %}
|
||||
<div class="alert alert-success">
|
||||
{% trans "Your payment is successfully validated by the organizers." %}
|
||||
<ul>
|
||||
<li>{% trans "Type:" %} {{ payment.get_type_display }}</li>
|
||||
<li>
|
||||
{% trans "Amount:" %} {{ payment.amount }} €
|
||||
{% if payment.grouped %}
|
||||
({% trans "It includes the registrations of all members of the team." %})
|
||||
{% endif %}
|
||||
</li>
|
||||
{% if payment.receipt %}
|
||||
<li>
|
||||
{% trans "Receipt:" %}
|
||||
<a href="{{ payment.receipt.url }}"><i class="fas fa-download"></i> {% trans "Download" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if payment.additional_information %}
|
||||
<li>{% trans "Additional information:" %} {{ payment.additional_information }}</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% elif payment.valid is None %}
|
||||
<div class="alert alert-warning">
|
||||
{% trans "Your payment is pending validation from the organizers." %}
|
||||
<ul>
|
||||
<li>{% trans "Type:" %} {{ payment.get_type_display }}</li>
|
||||
<li>
|
||||
{% trans "Amount:" %} {{ payment.amount }} €
|
||||
{% if payment.grouped %}
|
||||
({% trans "It includes the registrations of all members of the team." %})
|
||||
{% endif %}
|
||||
</li>
|
||||
{% if payment.receipt %}
|
||||
<li>
|
||||
{% trans "Receipt:" %}
|
||||
<a href="{{ payment.receipt.url }}"><i class="fas fa-download"></i> {% trans "Download" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if payment.additional_information %}
|
||||
<li>{% trans "Additional information:" %} {{ payment.additional_information }}</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock content %}
|
||||
|
||||
|
@ -451,14 +451,16 @@ class PaymentUpdateView(LoginRequiredMixin, UpdateView):
|
||||
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):
|
||||
and self.request.user.registration not in self.get_object().registrations.all():
|
||||
return self.handle_no_permission()
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data()
|
||||
context['title'] = _("Update payment")
|
||||
# Grouping is only possible if there isn't any validated payment in the team
|
||||
context['can_group'] = all(p.valid is False for reg in self.object.team.students.all()
|
||||
for p in reg.payments.filter(valid=self.object.valid).all())
|
||||
context['bank_transfer_form'] = PaymentForm(payment_type='bank_transfer',
|
||||
data=self.request.POST or None,
|
||||
instance=self.object)
|
||||
@ -474,8 +476,12 @@ class PaymentUpdateView(LoginRequiredMixin, UpdateView):
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.valid = None
|
||||
old_instance = Payment.objects.get(pk=self.object.pk)
|
||||
if self.request.user.registration.participates:
|
||||
if old_instance.valid is not False:
|
||||
raise PermissionDenied(_("This payment is already valid or pending validation."))
|
||||
else:
|
||||
form.instance.valid = None
|
||||
if old_instance.receipt:
|
||||
old_instance.receipt.delete()
|
||||
old_instance.save()
|
||||
@ -489,11 +495,18 @@ class PaymentUpdateGroupView(LoginRequiredMixin, DetailView):
|
||||
model = Payment
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
payment = self.get_object()
|
||||
|
||||
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):
|
||||
or payment.valid is not False):
|
||||
return self.handle_no_permission()
|
||||
|
||||
if any(p.valid is not False for reg in payment.team.students.all()
|
||||
for p in reg.payments.filter(valid=payment.valid).all()):
|
||||
raise PermissionDenied(_("Since one payment is already validated, or pending validation, "
|
||||
"grouping is not possible."))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
@ -6,14 +6,15 @@ from datetime import datetime, timedelta
|
||||
from django.conf import settings
|
||||
import requests
|
||||
|
||||
|
||||
_access_token = None
|
||||
_refresh_token = None
|
||||
_expires_at = None
|
||||
|
||||
|
||||
def _get_hello_asso_api_base_url():
|
||||
if not settings.DEBUG:
|
||||
if settings.HELLOASSO_TEST_ENDPOINT:
|
||||
return f"{settings.HELLOASSO_TEST_ENDPOINT_URL}/helloasso-test/api"
|
||||
elif not settings.DEBUG:
|
||||
return "https://api.helloasso.com"
|
||||
else:
|
||||
return "https://api.helloasso-sandbox.com"
|
23
tfjm/helloasso/test_urls.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Copyright (C) 2024 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.urls import path
|
||||
import tfjm.urls
|
||||
|
||||
from . import test_views
|
||||
|
||||
urlpatterns = tfjm.urls.urlpatterns
|
||||
|
||||
urlpatterns += [
|
||||
path('helloasso-test/api/oauth2/token', test_views.TestHelloAssoOAuth2View.as_view(),
|
||||
name='helloasso-test-oauth2-token'),
|
||||
path('helloasso-test/api/v5/organizations/animath/checkout-intents/',
|
||||
test_views.TestHelloAssoCheckoutIntentCreateView.as_view(),
|
||||
name='helloasso-test-checkout-intent-create'),
|
||||
path('helloasso-test/api/v5/organizations/animath/checkout-intents/<int:checkout_intent_id>/',
|
||||
test_views.TestHelloAssoCheckoutIntentDetailView.as_view(),
|
||||
name='helloasso-test-checkout-intent-detail'),
|
||||
path('helloasso-test/redirect-payment/<int:checkout_intent_id>/',
|
||||
test_views.TestHelloAssoRedirectPaymentView.as_view(),
|
||||
name='helloasso-test-redirect-payment'),
|
||||
]
|
149
tfjm/helloasso/test_views.py
Normal file
@ -0,0 +1,149 @@
|
||||
# Copyright (C) 2024 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import json
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import Http404, HttpResponse, JsonResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.generic.base import View
|
||||
|
||||
_CHECKOUT_INTENTS = {}
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class TestHelloAssoOAuth2View(View):
|
||||
def post(self, request, *args, **kwargs):
|
||||
data = {
|
||||
'access_token': 'test_access_token',
|
||||
'refresh_token': 'test_refresh_token',
|
||||
'expires_in': 3600,
|
||||
}
|
||||
return JsonResponse(data)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class TestHelloAssoCheckoutIntentCreateView(View):
|
||||
def post(self, request, *args, **kwargs):
|
||||
checkout_intent_id = len(_CHECKOUT_INTENTS) + 1
|
||||
body = json.loads(request.body.decode())
|
||||
|
||||
body['backUrl'] = body['backUrl'].replace("https", "http")
|
||||
body['returnUrl'] = body['returnUrl'].replace("https", "http")
|
||||
body['errorUrl'] = body['errorUrl'].replace("https", "http")
|
||||
|
||||
output_data = {
|
||||
'id': checkout_intent_id,
|
||||
'redirectUrl': f"{settings.HELLOASSO_TEST_ENDPOINT_URL}"
|
||||
f"{reverse('helloasso-test-redirect-payment', args=(checkout_intent_id,))}",
|
||||
'metadata': body['metadata'],
|
||||
}
|
||||
|
||||
checkout_intent = {'input': body, 'output': output_data}
|
||||
_CHECKOUT_INTENTS[checkout_intent_id] = checkout_intent
|
||||
|
||||
return JsonResponse(output_data)
|
||||
|
||||
|
||||
class TestHelloAssoCheckoutIntentDetailView(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
checkout_intent_id = kwargs['checkout_intent_id']
|
||||
if checkout_intent_id not in _CHECKOUT_INTENTS:
|
||||
raise Http404
|
||||
return JsonResponse(_CHECKOUT_INTENTS[checkout_intent_id]['output'])
|
||||
|
||||
|
||||
class TestHelloAssoRedirectPaymentView(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
checkout_intent_id = kwargs['checkout_intent_id']
|
||||
if checkout_intent_id not in _CHECKOUT_INTENTS:
|
||||
raise Http404
|
||||
|
||||
checkout_intent = _CHECKOUT_INTENTS[checkout_intent_id]
|
||||
ci_input = checkout_intent['input']
|
||||
ci_output = checkout_intent['output']
|
||||
|
||||
if 'error' in request.GET:
|
||||
return redirect(ci_input['errorUrl'] + f"&checkoutIntentId={checkout_intent_id}&error=An error occurred.")
|
||||
elif 'refused' in request.GET:
|
||||
return redirect(ci_input['returnUrl'] + f"&checkoutIntentId={checkout_intent_id}&code=refused")
|
||||
|
||||
dt = timezone.now().isoformat()
|
||||
|
||||
ci_output['order'] = {
|
||||
'payer': {
|
||||
'email': 'payer@example.com',
|
||||
'country': 'FRA',
|
||||
'dateOfBirth': '2000-01-01T00:00:00+01:00',
|
||||
'firstName': "Payer",
|
||||
'lastName': "Payer",
|
||||
},
|
||||
'items': [
|
||||
{
|
||||
'payments': [
|
||||
{
|
||||
'id': checkout_intent_id,
|
||||
'shareAmount': ci_input['totalAmount'],
|
||||
}
|
||||
],
|
||||
'name': ci_input['itemName'],
|
||||
'priceCategory': 'Fixed',
|
||||
'qrCode': '',
|
||||
'id': checkout_intent_id,
|
||||
'amount': ci_input['totalAmount'],
|
||||
'type': 'Payment',
|
||||
'state': 'Processed'
|
||||
}
|
||||
],
|
||||
'payments': [
|
||||
{
|
||||
'items': [
|
||||
{
|
||||
'id': checkout_intent_id,
|
||||
'shareAmount': ci_input['totalAmount'],
|
||||
'shareItemAmount': ci_input['totalAmount'],
|
||||
}
|
||||
],
|
||||
'cashOutState': 'MoneyIn',
|
||||
'paymentReceiptUrl': "https://example.com/",
|
||||
'id': checkout_intent_id,
|
||||
'amount': ci_input['totalAmount'],
|
||||
'date': dt,
|
||||
'paymentMeans': 'Card',
|
||||
'installmentNumber': 1,
|
||||
'state': 'Authorized',
|
||||
'meta': {
|
||||
'createdAt': dt,
|
||||
'updatedAt': dt,
|
||||
},
|
||||
'refundOperations': []
|
||||
}
|
||||
],
|
||||
'amount': {
|
||||
'total': ci_input['totalAmount'],
|
||||
'vat': 0,
|
||||
'discount': 0
|
||||
},
|
||||
'id': 13339,
|
||||
'date': dt,
|
||||
'formSlug': 'default',
|
||||
'formType': 'Checkout',
|
||||
'organizationName': 'Animath',
|
||||
'organizationSlug': 'animath',
|
||||
'checkoutIntentId': checkout_intent_id,
|
||||
'meta': {
|
||||
'createdAt': dt,
|
||||
'updatedAt': dt,
|
||||
},
|
||||
'isAnonymous': False,
|
||||
'isAmountHidden': False
|
||||
}
|
||||
|
||||
return redirect(ci_input['returnUrl'] + f"&checkoutIntentId={checkout_intent_id}&code=succeeded")
|
||||
|
||||
def head(self, request, *args, **kwargs):
|
||||
return HttpResponse()
|
@ -244,6 +244,7 @@ PHONENUMBER_DEFAULT_REGION = 'FR'
|
||||
# Hello Asso API creds
|
||||
HELLOASSO_CLIENT_ID = os.getenv('HELLOASSO_CLIENT_ID', 'CHANGE_ME_IN_ENV_SETTINGS')
|
||||
HELLOASSO_CLIENT_SECRET = os.getenv('HELLOASSO_CLIENT_SECRET', 'CHANGE_ME_IN_ENV_SETTINGS')
|
||||
HELLOASSO_TEST_ENDPOINT = False # Enable custom test endpoint, for unit tests
|
||||
|
||||
# Custom parameters
|
||||
PROBLEMS = [
|
||||
@ -284,3 +285,8 @@ if TFJM_STAGE == "prod": # pragma: no cover
|
||||
from .settings_prod import * # noqa: F401,F403
|
||||
else:
|
||||
from .settings_dev import * # noqa: F401,F403
|
||||
|
||||
try:
|
||||
from .settings_local import * # noqa: F401,F403
|
||||
except ImportError:
|
||||
pass
|
||||
|
21
tox.ini
@ -8,26 +8,11 @@ skipsdist = True
|
||||
|
||||
[testenv]
|
||||
sitepackages = False
|
||||
deps =
|
||||
coverage
|
||||
channels[daphne]~=4.0.0
|
||||
crispy-bootstrap5~=2023.10
|
||||
Django>=5.0,<6.0
|
||||
django-crispy-forms~=2.1
|
||||
django-filter~=23.5
|
||||
git+https://github.com/django-haystack/django-haystack.git#v3.3b1
|
||||
django-phonenumber-field~=7.3.0
|
||||
django-polymorphic~=3.1.0
|
||||
django-tables2~=2.7.0
|
||||
djangorestframework~=3.14.0
|
||||
django-rest-polymorphic~=0.1.10
|
||||
odfpy~=1.4.1
|
||||
phonenumbers~=8.13.27
|
||||
pypdf~=3.17.4
|
||||
python-magic~=0.4.27
|
||||
requests~=2.31.0
|
||||
deps = coverage
|
||||
-r requirements.txt
|
||||
|
||||
commands =
|
||||
python manage.py compilemessages -i .tox -i venv
|
||||
coverage run --source=api,draw,logs,participation,registration,tfjm ./manage.py test api/ draw/ logs/ participation/ registration/ tfjm/
|
||||
coverage report -m
|
||||
|
||||
|