1
0
mirror of https://gitlab.com/animath/si/plateforme.git synced 2025-02-25 05:46:30 +00:00

Compare commits

..

17 Commits

Author SHA1 Message Date
Emmy D'Anello
2155275627
Update Haystack search index in cron
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-02-23 23:08:47 +01:00
Emmy D'Anello
7b4e867e33
Linting
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-02-23 23:05:10 +01:00
Emmy D'Anello
2c54f315f6
Add payments table page
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-02-23 22:58:23 +01:00
Emmy D'Anello
5cbc72b41f
Teams tab is only accessible to admins
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-02-23 21:48:39 +01:00
Emmy D'Anello
de504398d2
Improve Django-admin interface, inlines and filters
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-02-23 21:43:44 +01:00
Emmy D'Anello
cae1c6fdb8
Send payment confirmation mail after payment, and send weekly reminders for people that have not paid
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-02-23 18:02:24 +01:00
Emmy D'Anello
6a928ee35b
Prepare mails for payment confirmations and reminders
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-02-22 18:43:18 +01:00
Emmy D'Anello
bc535f4075
Restore payment edit form for volunteers
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-02-21 23:56:29 +01:00
Emmy D'Anello
64b91cf7e0
Display payments in team detail view
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-02-21 23:41:31 +01:00
Emmy D'Anello
54dafe1cec
Improve payment messages
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-02-21 23:12:01 +01:00
Emmy D'Anello
b16b6e422f
Allow anonymous users to perform a payment using a special auth token
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-02-21 22:44:56 +01:00
Emmy D'Anello
8d08b18d08
Configure Hello Asso return endpoint
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-02-20 22:54:12 +01:00
Emmy D'Anello
8c7e9648dd
Use Hello Asso sandbox instance in dev mode
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-02-20 18:51:38 +01:00
Emmy D'Anello
b3555a7807
Create Hello Asso checkout intents
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-02-19 00:17:14 +01:00
Emmy D'Anello
98d04b9093
Make the payment group button work
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-02-18 23:02:27 +01:00
Emmy D'Anello
4d157b2bd7
Setup payment interface
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-02-18 22:36:01 +01:00
Emmy D'Anello
7c9083a6b8
Restructure payment model
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-02-12 22:58:48 +01:00
36 changed files with 2206 additions and 621 deletions

View File

@ -7,11 +7,34 @@ from django.utils.translation import gettext_lazy as _
from .models import Draw, Pool, Round, TeamDraw
class RoundInline(admin.TabularInline):
model = Round
extra = 0
autocomplete_fields = ('draw', 'current_pool',)
show_change_link = True
class PoolInline(admin.TabularInline):
model = Pool
extra = 0
autocomplete_fields = ('round', 'current_team', 'associated_pool',)
show_change_link = True
class TeamDrawInline(admin.TabularInline):
model = TeamDraw
extra = 0
autocomplete_fields = ('participation', 'round', 'pool',)
show_change_link = True
@admin.register(Draw)
class DrawAdmin(admin.ModelAdmin):
list_display = ('tournament', 'teams', 'current_round', 'get_state',)
list_filter = ('tournament', 'current_round',)
list_filter = ('tournament', 'current_round__number',)
search_fields = ('tournament__name', 'tournament__participation__team__trigram',)
autocomplete_fields = ('tournament',)
inlines = (RoundInline,)
@admin.display(description=_("teams"))
def teams(self, record: Draw):
@ -20,10 +43,16 @@ class DrawAdmin(admin.ModelAdmin):
@admin.register(Round)
class RoundAdmin(admin.ModelAdmin):
list_display = ('draw', 'number', 'teams',)
list_display = ('draw', 'tournament', 'number', 'teams',)
list_filter = ('draw__tournament', 'number',)
search_fields = ('draw__tournament__name', 'pool__teamdraw__participation__team__trigram')
ordering = ('draw__tournament__name', 'number')
autocomplete_fields = ('draw', 'current_pool',)
inlines = (PoolInline,)
@admin.display(description=_("tournament"), ordering='draw__tournament__name')
def tournament(self, record):
return record.draw.tournament
@admin.display(description=_("teams"))
def teams(self, record: Round):
@ -36,6 +65,8 @@ class PoolAdmin(admin.ModelAdmin):
list_filter = ('round__draw__tournament', 'round__number', 'letter')
ordering = ('round__draw__tournament__name', 'round', 'letter')
search_fields = ('round__draw__tournament__name', 'teamdraw__participation__team__trigram',)
autocomplete_fields = ('round', 'current_team', 'associated_pool',)
inlines = (TeamDrawInline,)
@admin.display(ordering='round__draw__tournament__name', description=_("tournament"))
def tournament(self, record):
@ -52,6 +83,7 @@ class TeamDrawAdmin(admin.ModelAdmin):
'passage_index', 'choose_index', 'passage_dice', 'choice_dice',)
list_filter = ('round__draw__tournament', 'round__number', 'pool__letter',)
search_fields = ('round__draw__tournament__name', 'participation__team__trigram',)
autocomplete_fields = ('participation', 'round', 'pool',)
@admin.display(ordering='round__draw__tournament__name', description=_("tournament"))
def tournament(self, record):

View File

@ -89,6 +89,7 @@ class Draw(models.Model):
return 'WAITING_DRAW_PROBLEM'
else:
return 'WAITING_CHOOSE_PROBLEM'
get_state.short_description = _('State')
@property
def information(self):

File diff suppressed because it is too large Load Diff

View File

@ -7,11 +7,74 @@ from django.utils.translation import gettext_lazy as _
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament, Tweak
class ParticipationInline(admin.StackedInline):
model = Participation
extra = 0
autocomplete_fields = ('team', 'tournament',)
show_change_link = True
class ParticipationTabularInline(admin.TabularInline):
model = Participation
extra = 0
fields = ('team', 'valid', 'final',)
readonly_fields = ('team',)
ordering = ('final', 'valid', 'team__trigram',)
autocomplete_fields = ('tournament',)
show_change_link = True
class SolutionInline(admin.TabularInline):
model = Solution
extra = 0
ordering = ('problem',)
autocomplete_fields = ('participation',)
show_change_link = True
class SynthesisInline(admin.TabularInline):
model = Synthesis
extra = 0
ordering = ('passage__solution_number', 'type',)
autocomplete_fields = ('passage',)
show_change_link = True
class PoolInline(admin.TabularInline):
model = Pool
extra = 0
autocomplete_fields = ('tournament', 'participations', 'juries',)
show_change_link = True
class PassageInline(admin.TabularInline):
model = Passage
extra = 0
ordering = ('position',)
autocomplete_fields = ('defender', 'opponent', 'reporter', 'observer',)
show_change_link = True
class NoteInline(admin.TabularInline):
model = Note
extra = 0
autocomplete_fields = ('jury',)
show_change_link = True
class TweakInline(admin.TabularInline):
model = Tweak
extra = 0
autocomplete_fields = ('participation', 'pool',)
show_change_link = True
@admin.register(Team)
class TeamAdmin(admin.ModelAdmin):
list_display = ('name', 'trigram', 'tournament', 'valid', 'final',)
search_fields = ('name', 'trigram',)
list_filter = ('participation__valid', 'participation__tournament', 'participation__final',)
inlines = (ParticipationInline,)
@admin.display(description=_("tournament"))
def tournament(self, record):
@ -32,6 +95,7 @@ class ParticipationAdmin(admin.ModelAdmin):
search_fields = ('team__name', 'team__trigram',)
list_filter = ('valid',)
autocomplete_fields = ('team', 'tournament',)
inlines = (SolutionInline, SynthesisInline,)
@admin.register(Pool)
@ -40,6 +104,7 @@ class PoolAdmin(admin.ModelAdmin):
list_filter = ('tournament', 'round', 'letter',)
search_fields = ('participations__team__name', 'participations__team__trigram',)
autocomplete_fields = ('tournament', 'participations', 'juries',)
inlines = (PassageInline, TweakInline,)
@admin.display(description=_("teams"))
def teams(self, record: Pool):
@ -49,28 +114,30 @@ class PoolAdmin(admin.ModelAdmin):
@admin.register(Passage)
class PassageAdmin(admin.ModelAdmin):
list_display = ('__str__', 'defender_trigram', 'solution_number', 'opponent_trigram', 'reporter_trigram',
'pool_abbr', 'tournament')
'pool_abbr', 'position', 'tournament')
list_filter = ('pool__tournament', 'pool__round', 'pool__letter', 'solution_number',)
search_fields = ('pool__participations__team__name', 'pool__participations__team__trigram',)
ordering = ('pool__tournament', 'pool__round', 'pool__letter', 'position',)
autocomplete_fields = ('pool', 'defender', 'opponent', 'reporter', 'observer',)
inlines = (NoteInline,)
@admin.display(description=_("defender"))
@admin.display(description=_("defender"), ordering='defender__team__trigram')
def defender_trigram(self, record: Passage):
return record.defender.team.trigram
@admin.display(description=_("opponent"))
@admin.display(description=_("opponent"), ordering='opponent__team__trigram')
def opponent_trigram(self, record: Passage):
return record.opponent.team.trigram
@admin.display(description=_("reporter"))
@admin.display(description=_("reporter"), ordering='reporter__team__trigram')
def reporter_trigram(self, record: Passage):
return record.reporter.team.trigram
@admin.display(description=_("pool"))
@admin.display(description=_("pool"), ordering='pool__letter')
def pool_abbr(self, record):
return f"{record.pool.get_letter_display()}{record.pool.round}"
@admin.display(description=_("tournament"))
@admin.display(description=_("tournament"), ordering='pool__tournament__name')
def tournament(self, record: Passage):
return record.pool.tournament
@ -124,9 +191,11 @@ class SynthesisAdmin(admin.ModelAdmin):
@admin.register(Tournament)
class TournamentAdmin(admin.ModelAdmin):
list_display = ('name',)
list_display = ('name', 'date_start', 'date_end',)
search_fields = ('name',)
ordering = ('date_start', 'name',)
autocomplete_fields = ('organizers',)
inlines = (ParticipationTabularInline, PoolInline,)
@admin.register(Tweak)

View File

@ -1,87 +0,0 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
import os
from django.contrib.auth.models import User
from django.core.management import BaseCommand
from django.db.models import Q
import requests
class Command(BaseCommand):
def handle(self, *args, **options): # noqa: C901
# Get access token
response = requests.post('https://api.helloasso.com/oauth2/token', headers={
'Content-Type': 'application/x-www-form-urlencoded',
}, data={
'client_id': os.getenv('HELLOASSO_CLIENT_ID', ''),
'client_secret': os.getenv('HELLOASSO_CLIENT_SECRET', ''),
'grant_type': 'client_credentials',
}).json()
token = response['access_token']
organization = "animath"
form_slug = "tfjm-2023-tournois-regionaux"
from_date = "2000-01-01"
url = f"https://api.helloasso.com/v5/organizations/{organization}/forms/Event/{form_slug}/payments" \
f"?from={from_date}&pageIndex=1&pageSize=100&retrieveOfflineDonations=false"
headers = {
"Accept": "application/json",
"Authorization": f"Bearer {token}",
}
http_response = requests.get(url, headers=headers)
response = http_response.json()
if http_response.status_code != 200:
message = response["message"]
self.stderr.write(f"Error while querying Hello Asso: {message}")
return
for payment in response["data"]:
if payment["state"] != "Authorized":
continue
payer = payment["payer"]
email = payer["email"]
last_name = payer["lastName"]
first_name = payer["firstName"]
base_filter = Q(
registration__participantregistration__isnull=False,
registration__participantregistration__team__isnull=False,
registration__participantregistration__team__participation__valid=True,
)
qs = User.objects.filter(
base_filter,
email=email,
)
if not qs.exists():
qs = User.objects.filter(
base_filter,
last_name__icontains=last_name,
)
if qs.count() >= 2:
qs = qs.filter(first_name__icontains=first_name)
if not qs.exists():
self.stderr.write(f"Warning: a payment was found by {first_name} {last_name} ({email}), "
"but this user is unknown.")
continue
if qs.count() > 1:
self.stderr.write(f"Warning: a payment was found by {first_name} {last_name} ({email}), "
f"but there are {qs.count()} matching users.")
continue
user = qs.get()
if not user.registration.participates:
self.stderr.write(f"Warning: a payment was found by the email address {email}, "
"but this user is not a participant.")
continue
payment_obj = user.registration.payment
payment_obj.valid = True
payment_obj.type = "helloasso"
payment_obj.additional_information = f"Identifiant de transation : {payment['id']}\n" \
f"Date : {payment['date']}\n" \
f"Reçu : {payment['paymentReceiptUrl']}\n" \
f"Montant : {payment['amount'] / 100:.2f}"
payment_obj.save()
self.stdout.write(f"{payment_obj} is validated")

View File

@ -14,7 +14,7 @@ from django.utils import timezone
from django.utils.crypto import get_random_string
from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy as _
from registration.models import VolunteerRegistration, Payment
from registration.models import Payment, VolunteerRegistration
from tfjm.lists import get_sympa_client
@ -465,14 +465,15 @@ class Participation(models.Model):
def important_informations(self):
informations = []
missing_payments = Payment.objects.filter(registration__in=self.team.participants.all(), valid=False)
missing_payments = Payment.objects.filter(registrations__in=self.team.participants.all(), valid=False)
if missing_payments.exists():
text = _("<p>The team {trigram} has {nb_missing_payments} missing payments. Each member of the team "
"must have a valid payment (or send a scholarship notification) "
"to participate to the tournament.</p>"
"<p>Participants that have not paid yet are: {participants}.</p>")
content = format_lazy(text, trigram=self.team.trigram, nb_missing_payments=missing_payments.count(),
participants=", ".join(str(p.registration) for p in missing_payments.all()))
participants=", ".join(", ".join(str(r) for r in p.registrations.all())
for p in missing_payments.all()))
informations.append({
'title': _("Missing payments"),
'type': "danger",

View File

@ -112,7 +112,6 @@
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadMotivationLetterModal">{% trans "Replace" %}</button>
{% endif %}
</dd>
</dl>
{% if user.registration.is_volunteer %}
{% if user.registration in self.team.participation.tournament.organizers or user.registration.is_admin %}
@ -123,6 +122,30 @@
</div>
{% endif %}
{% endif %}
{% if team.participation.valid %}
<hr class="my-3">
{% for student in team.students.all %}
{% for payment in student.payments.all %}
<dt class="col-sm-6 text-end">
{% trans "Payment of" %} {{ student }}
{% if payment.grouped %}({% trans "grouped" %}){% endif %}
{% if payment.final %} ({% trans "final" %}){% endif %} :
</dt>
<dd class="col-sm-6">
Valide : {{ payment.valid|yesno }}
{% if payment.valid is False or user.registration.is_volunteer %}
{% if user.registration in payment.registrations.all or user.registration.is_coach or user.registration.is_volunteer %}
<a href="{% url "registration:update_payment" pk=payment.pk %}" class="btn btn-secondary">
<i class="fas fa-money-bill-wave"></i> {% trans "Update payment" %}
</a>
{% endif %}
{% endif %}
</dd>
{% endfor %}
{% endfor %}
{% endif %}
</dl>
</div>
<div class="card-footer text-center">
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#updateTeamModal">{% trans "Update" %}</button>

View File

@ -73,6 +73,14 @@
<div id="teams_table">
{% render_table teams %}
</div>
{% if user.registration.is_admin or user.registration in tournament.organizers.all %}
<div class="text-center">
<a href="{% url "participation:tournament_payments" pk=tournament.pk %}" class="btn btn-secondary">
<i class="fas fa-money-bill-wave"></i> {% trans "Access to payments list" %}
</a>
</div>
{% endif %}
{% if pools.data %}
<hr>

View File

@ -0,0 +1,11 @@
{% extends "base.html" %}
{% load django_tables2 i18n %}
{% block content %}
{% render_table table %}
<a href="{% url "participation:tournament_detail" pk=tournament.pk %}" class="btn btn-primary">
<i class="fas fa-long-arrow-alt-left"></i> {% trans "Back to tournament page" %}
</a>
{% endblock %}

View File

@ -10,7 +10,7 @@ from .views import CreateTeamView, FinalNotationSheetTemplateView, JoinTeamView,
PoolUpdateTeamsView, PoolUpdateView, PoolUploadNotesView, ScaleNotationSheetTemplateView, SolutionUploadView, \
SynthesisUploadView, TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \
TeamUploadMotivationLetterView, TournamentCreateView, TournamentDetailView, TournamentExportCSVView, \
TournamentListView, TournamentUpdateView
TournamentListView, TournamentPaymentsView, TournamentUpdateView
app_name = "participation"
@ -33,6 +33,7 @@ urlpatterns = [
path("tournament/create/", TournamentCreateView.as_view(), name="tournament_create"),
path("tournament/<int:pk>/", TournamentDetailView.as_view(), name="tournament_detail"),
path("tournament/<int:pk>/update/", TournamentUpdateView.as_view(), name="tournament_update"),
path("tournament/<int:pk>/payments/", TournamentPaymentsView.as_view(), name="tournament_payments"),
path("tournament/<int:pk>/csv/", TournamentExportCSVView.as_view(), name="tournament_csv"),
path("pools/create/", PoolCreateView.as_view(), name="pool_create"),
path("pools/<int:pk>/", PoolDetailView.as_view(), name="pool_detail"),

View File

@ -6,6 +6,7 @@ from io import BytesIO
import os
import subprocess
from tempfile import mkdtemp
from typing import Any, Dict
from zipfile import ZipFile
from django.conf import settings
@ -15,6 +16,7 @@ from django.contrib.sites.models import Site
from django.core.exceptions import PermissionDenied
from django.core.mail import send_mail
from django.db import transaction
from django.db.models import F
from django.http import FileResponse, Http404, HttpResponse
from django.shortcuts import redirect
from django.template.loader import render_to_string
@ -24,13 +26,14 @@ from django.utils.crypto import get_random_string
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, DetailView, FormView, RedirectView, TemplateView, UpdateView, View
from django.views.generic.edit import FormMixin, ProcessFormView
from django_tables2 import MultiTableMixin, SingleTableView
from django_tables2 import MultiTableMixin, SingleTableMixin, SingleTableView
from magic import Magic
from odf.opendocument import OpenDocumentSpreadsheet
from odf.style import Style, TableCellProperties, TableColumnProperties, TextProperties
from odf.table import CoveredTableCell, Table, TableCell, TableColumn, TableRow
from odf.text import P
from registration.models import StudentRegistration, VolunteerRegistration
from registration.models import Payment, StudentRegistration, VolunteerRegistration
from registration.tables import PaymentTable
from tfjm.lists import get_sympa_client
from tfjm.views import AdminMixin, VolunteerMixin
@ -246,16 +249,20 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView)
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)
if self.object.participation.tournament.price == 0:
for registration in self.object.participants.all():
registration.payment.type = "free"
registration.payment.valid = True
registration.payment.save()
else:
for coach in self.object.coaches.all():
coach.payment.type = "free"
coach.payment.valid = True
coach.payment.save()
for student in self.object.students.all():
payment_qs = Payment.objects.filter(registrations=student)
if payment_qs.exists():
payment = payment_qs.get()
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()
elif "invalidate" in self.request.POST:
self.object.participation.valid = None
self.object.participation.save()
@ -568,6 +575,31 @@ class TournamentDetailView(MultiTableMixin, DetailView):
return context
class TournamentPaymentsView(VolunteerMixin, SingleTableMixin, DetailView):
"""
Display the list of payments of a tournament.
"""
model = Tournament
table_class = PaymentTable
template_name = "participation/tournament_payments.html"
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
context["title"] = _("Payments of {tournament}").format(tournament=self.object)
return context
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated or not self.request.user.registration.is_admin \
and not (self.request.user.registration.is_volunteer
and self.get_object() in self.request.user.registration.organized_tournaments.all()):
return self.handle_no_permission()
return super().dispatch(request, *args, **kwargs)
def get_table_data(self):
return Payment.objects.filter(registrations__team__participation__tournament=self.get_object()) \
.annotate(team_id=F('registrations__team')).order_by('-valid', 'registrations__team__trigram').all()
class TournamentExportCSVView(VolunteerMixin, DetailView):
"""
Export team information in a CSV file.

View File

@ -3,13 +3,45 @@
from django.contrib import admin
from django.contrib.admin import ModelAdmin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _
from polymorphic.admin import PolymorphicChildModelAdmin, PolymorphicChildModelFilter, PolymorphicParentModelAdmin
from polymorphic.admin import PolymorphicChildModelAdmin, PolymorphicChildModelFilter, PolymorphicInlineSupportMixin, \
PolymorphicParentModelAdmin, StackedPolymorphicInline
from .models import CoachRegistration, ParticipantRegistration, Payment, Registration, \
StudentRegistration, VolunteerRegistration
class RegistrationInline(StackedPolymorphicInline):
class StudentRegistrationInline(StackedPolymorphicInline.Child):
model = StudentRegistration
autocomplete_fields = ('team',)
show_change_link = True
class CoachRegistrationInline(StackedPolymorphicInline.Child):
model = CoachRegistration
autocomplete_fields = ('team',)
show_change_link = True
class VolunteerRegistrationInline(StackedPolymorphicInline.Child):
model = VolunteerRegistration
show_change_link = True
model = Registration
child_inlines = (
StudentRegistrationInline,
CoachRegistrationInline,
VolunteerRegistrationInline,
)
class PaymentInline(admin.TabularInline):
model = Payment
extra = 0
autocomplete_fields = ('registrations',)
@admin.register(Registration)
class RegistrationAdmin(PolymorphicParentModelAdmin):
child_models = (StudentRegistration, CoachRegistration, VolunteerRegistration,)
@ -97,11 +129,34 @@ class VolunteerRegistrationAdmin(PolymorphicChildModelAdmin):
@admin.register(Payment)
class PaymentAdmin(ModelAdmin):
list_display = ('registration', 'registration_type', 'type', 'valid', )
search_fields = ('registration__user__last_name', 'registration__user__first_name', 'registration__user__email',)
list_filter = ('registration__team__participation__valid', 'type', 'type', 'valid',)
autocomplete_fields = ('registration',)
list_display = ('concerned_people', 'tournament', 'team', 'grouped', 'type', 'amount', 'valid', )
search_fields = ('registrations__user__last_name', 'registrations__user__first_name', 'registrations__user__email',
'registrations__team__name', 'registrations__team__participation__team__trigram',)
list_filter = ('registrations__team__participation__valid', 'type',
'grouped', 'valid', 'registrations__team__participation__tournament', 'final',)
autocomplete_fields = ('registrations',)
actions = ('mark_as_valid', 'mark_as_pending', 'mark_as_invalid',)
@admin.display(description=_('registration type'), ordering='registration__polymorphic_ctype')
def registration_type(self, record: Payment):
return record.registration.get_real_instance().type
@admin.display(description=_('concerned people'))
def concerned_people(self, record: Payment):
return ", ".join(f"{reg.user.first_name} {reg.user.last_name}" for reg in record.registrations.all())
@admin.action(description=_('Mark as valid'))
def mark_as_valid(self, request, queryset):
queryset.update(valid=True)
@admin.action(description=_('Mark as pending'))
def mark_as_pending(self, request, queryset):
queryset.update(valid=None)
@admin.action(description=_('Mark as invalid'))
def mark_as_invalid(self, request, queryset):
queryset.update(valid=False)
admin.site.unregister(User)
@admin.register(User)
class UserCustomAdmin(PolymorphicInlineSupportMixin, UserAdmin):
inlines = [RegistrationInline]

View File

@ -12,11 +12,8 @@ class RegistrationConfig(AppConfig):
name = 'registration'
def ready(self):
from registration.signals import create_admin_registration, create_payment, \
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")
post_save.connect(create_payment, "registration.Registration")
post_save.connect(create_payment, "registration.StudentRegistration")
post_save.connect(create_payment, "registration.CoachRegistration")

View File

@ -219,7 +219,7 @@ class VolunteerRegistrationForm(forms.ModelForm):
fields = ('professional_activity', 'admin', 'give_contact_to_animath', 'email_confirmed',)
class PaymentForm(forms.ModelForm):
class PaymentAdminForm(forms.ModelForm):
"""
Indicate payment information
"""
@ -227,25 +227,57 @@ class PaymentForm(forms.ModelForm):
super().__init__(*args, **kwargs)
self.fields["valid"].widget.choices[0] = ('unknown', _("Pending"))
def clean_scholarship_file(self):
print(self.files)
if "scholarship_file" in self.files:
file = self.files["scholarship_file"]
def clean_receipt(self):
if "receipt" in self.files:
file = self.files["receipt"]
if file.size > 2e6:
raise ValidationError(_("The uploaded file size must be under 2 Mo."))
if file.content_type not in ["application/pdf", "image/png", "image/jpeg"]:
raise ValidationError(_("The uploaded file must be a PDF, PNG of JPEG file."))
return self.cleaned_data["scholarship_file"]
return self.cleaned_data["receipt"]
def clean(self):
cleaned_data = super().clean()
if "type" in cleaned_data and cleaned_data["type"] == "scholarship" \
and "scholarship_file" not in self.files and not self.instance.scholarship_file:
self.add_error("scholarship_file", _("You must upload your scholarship attestation."))
if "type" in cleaned_data and cleaned_data['type'] in ["scholarship", "bank_transfer"] \
and "receipt" not in self.files and not self.instance.receipt:
self.add_error("receipt", _("You must upload your receipt."))
return cleaned_data
class Meta:
model = Payment
fields = ('type', 'scholarship_file', 'additional_information', 'valid',)
fields = ('type', 'receipt', 'additional_information', 'valid',)
class PaymentForm(forms.ModelForm):
"""
Indicate payment information
"""
def __init__(self, payment_type, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['type'].widget = forms.HiddenInput(attrs={'value': payment_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:
file = self.files["receipt"]
if file.size > 2e6:
raise ValidationError(_("The uploaded file size must be under 2 Mo."))
if file.content_type not in ["application/pdf", "image/png", "image/jpeg"]:
raise ValidationError(_("The uploaded file must be a PDF, PNG of JPEG file."))
return self.cleaned_data["receipt"]
def clean(self):
cleaned_data = super().clean()
if "type" in cleaned_data and cleaned_data['type'] in ["scholarship", "bank_transfer"] \
and "receipt" not in self.files and not self.instance.receipt:
self.add_error("receipt", _("You must upload your receipt."))
return cleaned_data
class Meta:
model = Payment
fields = ('type', 'receipt', 'additional_information',)

View File

@ -0,0 +1,25 @@
# Copyright (C) 2024 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
import json
from django.core.management import BaseCommand
from ...models import Payment
class Command(BaseCommand):
"""
This command checks if the initiated Hello Asso payments are validated or not.
"""
help = "Vérifie si les paiements Hello Asso initiés sont validés ou non. Si oui, valide les inscriptions."
def handle(self, *args, **options):
for payment in Payment.objects.exclude(valid=True).filter(checkout_intent_id__isnull=False).all():
checkout_intent = payment.get_checkout_intent()
if checkout_intent is not None and 'order' in checkout_intent:
payment.type = 'helloasso'
payment.valid = True
payment.additional_information = json.dumps(checkout_intent['order'])
payment.save()
payment.send_helloasso_payment_confirmation_mail()

View File

@ -0,0 +1,17 @@
# Copyright (C) 2024 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from django.core.management import BaseCommand
from ...models import Payment
class Command(BaseCommand):
"""
This command sends a mail to each participant who has not yet paid.
"""
help = "Envoie un mail de rappel à toustes les participant⋅es qui n'ont pas encore payé ou déclaré de paiement."
def handle(self, *args, **options):
for payment in Payment.objects.filter(valid=False).filter(registrations__team__participation__valid=True).all():
payment.send_remind_mail()

View File

@ -113,7 +113,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', models.CharField(blank=True, choices=[('', 'No payment'), ('helloasso', 'Hello Asso'), ('scholarship', 'Scholarship'), ('bank_transfer', 'Bank transfer'), ('other', 'Other (please indicate)'), ('free', 'The tournament is free')], default='', max_length=16, verbose_name='type')),
('scholarship_file', models.FileField(blank=True, default='', help_text='only if you have a scholarship.', upload_to=registration.models.get_scholarship_filename, verbose_name='scholarship file')),
('scholarship_file', models.FileField(blank=True, default='', help_text='only if you have a scholarship.', upload_to=registration.models.get_receipt_filename, verbose_name='scholarship file')),
('additional_information', models.TextField(blank=True, default='', help_text='To help us to find your payment.', verbose_name='additional information')),
('valid', models.BooleanField(default=False, null=True, verbose_name='valid')),
('registration', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='payment', to='registration.participantregistration', verbose_name='registration')),

View File

@ -0,0 +1,76 @@
# Generated by Django 5.0.1 on 2024-02-12 20:40
import registration.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registration", "0010_coachregistration_last_degree"),
]
operations = [
migrations.RemoveField(
model_name="payment",
name="registration",
),
migrations.RemoveField(
model_name="payment",
name="scholarship_file",
),
migrations.AddField(
model_name="payment",
name="amount",
field=models.PositiveSmallIntegerField(
default=0,
help_text="Corresponds to the total required amount to pay, in euros.",
verbose_name="total amount",
),
),
migrations.AddField(
model_name="payment",
name="checkout_intent_id",
field=models.IntegerField(
blank=True,
default=None,
null=True,
verbose_name="Hello Asso checkout intent ID",
),
),
migrations.AddField(
model_name="payment",
name="final",
field=models.BooleanField(
default=False, verbose_name="for final tournament"
),
),
migrations.AddField(
model_name="payment",
name="grouped",
field=models.BooleanField(
default=False,
help_text="If set to true, then one payment is made for the full team, for example if the school pays for all.",
verbose_name="grouped",
),
),
migrations.AddField(
model_name="payment",
name="receipt",
field=models.FileField(
blank=True,
default="",
help_text="only if you have a scholarship or if you chose a bank transfer.",
upload_to=registration.models.get_receipt_filename,
verbose_name="receipt",
),
),
migrations.AddField(
model_name="payment",
name="registrations",
field=models.ManyToManyField(
related_name="payments",
to="registration.participantregistration",
verbose_name="registrations",
),
),
]

View File

@ -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",
),
),
]

View File

@ -1,14 +1,15 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import date
from datetime import date, datetime
from django.contrib.sites.models import Site
from django.core.mail import send_mail
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.template import loader
from django.urls import reverse_lazy
from django.utils import timezone
from django.urls import reverse, reverse_lazy
from django.utils import timezone, translation
from django.utils.crypto import get_random_string
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
@ -16,6 +17,7 @@ from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy as _
from phonenumber_field.modelfields import PhoneNumberField
from polymorphic.models import PolymorphicModel
from tfjm import helloasso
from tfjm.tokens import email_validation_token
@ -78,6 +80,14 @@ class Registration(PolymorphicModel):
def participates(self):
return isinstance(self, ParticipantRegistration)
@property
def is_student(self):
return isinstance(self, StudentRegistration)
@property
def is_coach(self):
return isinstance(self, CoachRegistration)
@property
def is_admin(self):
return isinstance(self, VolunteerRegistration) and self.admin or self.user.is_superuser
@ -364,27 +374,28 @@ class StudentRegistration(ParticipantRegistration):
})
if self.team and self.team.participation.valid:
if self.payment.valid is False:
text = _("You have to pay {amount} € for your registration, or send a scholarship "
"notification or a payment proof. "
"You can do it on <a href=\"{url}\">the payment page</a>.")
url = reverse_lazy("registration:update_payment", args=(self.payment.id,))
content = format_lazy(text, amount=self.team.participation.tournament.price, url=url)
informations.append({
'title': _("Payment"),
'type': "danger",
'priority': 3,
'content': content,
})
elif self.payment.valid is None:
text = _("Your payment is under approval.")
content = text
informations.append({
'title': _("Payment"),
'type': "warning",
'priority': 3,
'content': content,
})
for payment in self.payments.all():
if payment.valid is False:
text = _("You have to pay {amount} € for your registration, or send a scholarship "
"notification or a payment proof. "
"You can do it on <a href=\"{url}\">the payment page</a>.")
url = reverse_lazy("registration:update_payment", args=(payment.id,))
content = format_lazy(text, amount=payment.amount, url=url)
informations.append({
'title': _("Payment"),
'type': "danger",
'priority': 3,
'content': content,
})
elif self.payment.valid is None:
text = _("Your payment is under approval.")
content = text
informations.append({
'title': _("Payment"),
'type': "warning",
'priority': 3,
'content': content,
})
return informations
@ -484,6 +495,24 @@ class VolunteerRegistration(Registration):
'content': content,
})
payments = Payment.objects.filter(registrations__team__participation__tournament=tournament).all()
valid = payments.filter(valid=True).count()
pending = payments.filter(valid=None).count()
invalid = payments.filter(valid=False).count()
if pending + invalid > 0:
text = _("There are {valid} validated payments, {pending} pending and {invalid} invalid for the "
"tournament of {tournament}. You can check the status of the payments on the "
"<a href=\"{url}\">payments list</a>.")
url = reverse_lazy("participation:tournament_payments", args=(tournament.id,))
content = format_lazy(text, valid=valid, pending=pending, invalid=invalid, tournament=tournament.name,
url=url)
informations.append({
'title': _("Payments"),
'type': "info",
'priority': 5,
'content': content,
})
return informations
class Meta:
@ -491,16 +520,44 @@ class VolunteerRegistration(Registration):
verbose_name_plural = _("volunteer registrations")
def get_scholarship_filename(instance, filename):
return f"authorization/scholarship/scholarship_{instance.registration.pk}"
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):
registration = models.OneToOneField(
registrations = models.ManyToManyField(
ParticipantRegistration,
on_delete=models.CASCADE,
related_name="payment",
verbose_name=_("registration"),
related_name="payments",
verbose_name=_("registrations"),
)
grouped = models.BooleanField(
verbose_name=_("grouped"),
default=False,
help_text=_("If set to true, then one payment is made for the full team, "
"for example if the school pays for all."),
)
amount = models.PositiveSmallIntegerField(
verbose_name=_("total amount"),
help_text=_("Corresponds to the total required amount to pay, in euros."),
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,
)
type = models.CharField(
@ -508,7 +565,7 @@ class Payment(models.Model):
max_length=16,
choices=[
('', _("No payment")),
('helloasso', "Hello Asso"),
('helloasso', _("Credit card")),
('scholarship', _("Scholarship")),
('bank_transfer', _("Bank transfer")),
('other', _("Other (please indicate)")),
@ -518,10 +575,17 @@ class Payment(models.Model):
default="",
)
scholarship_file = models.FileField(
verbose_name=_("scholarship file"),
help_text=_("only if you have a scholarship."),
upload_to=get_scholarship_filename,
checkout_intent_id = models.IntegerField(
verbose_name=_("Hello Asso checkout intent ID"),
blank=True,
null=True,
default=None,
)
receipt = models.FileField(
verbose_name=_("receipt"),
help_text=_("only if you have a scholarship or if you chose a bank transfer."),
upload_to=get_receipt_filename,
blank=True,
default="",
)
@ -539,11 +603,95 @@ class Payment(models.Model):
default=False,
)
@property
def team(self):
return self.registrations.first().team
team.fget.short_description = _("team")
team.fget.admin_order_field = 'registrations__team__trigram'
@property
def tournament(self):
if self.final:
from participation.models import Tournament
return Tournament.final_tournament()
return self.registrations.first().team.participation.tournament
tournament.fget.short_description = _("tournament")
tournament.fget.admin_order_field = 'registrations__team__participation__tournament'
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, none_if_link_disabled=none_if_link_disabled)
def create_checkout_intent(self):
checkout_intent = self.get_checkout_intent(none_if_link_disabled=True)
if checkout_intent is not None:
return checkout_intent
tournament = self.tournament
year = datetime.now().year
base_site = "https://" + Site.objects.first().domain
checkout_intent = helloasso.create_checkout_intent(
amount=100 * self.amount,
name=f"Participation au TFJM² {year} - {tournament.name} - {self.team.trigram}",
back_url=base_site + reverse('registration:update_payment', args=(self.id,)),
error_url=f"{base_site}{reverse('registration:payment_hello_asso_return', args=(self.id,))}?type=error",
return_url=f"{base_site}{reverse('registration:payment_hello_asso_return', args=(self.id,))}?type=return",
contains_donation=False,
metadata=dict(
users=[
dict(user_id=registration.user.id,
first_name=registration.user.first_name,
last_name=registration.user.last_name,
email=registration.user.email,)
for registration in self.registrations.all()
],
payment_id=self.id,
final=self.final,
tournament_id=tournament.id,
)
)
self.checkout_intent_id = checkout_intent["id"]
self.save()
return checkout_intent
def send_remind_mail(self):
translation.activate('fr')
subject = "[TFJM²] " + str(_("Reminder for your payment"))
site = Site.objects.first()
for registration in self.registrations.all():
message = loader.render_to_string('registration/mails/payment_reminder.txt',
dict(registration=registration, payment=self, domain=site.domain))
html = loader.render_to_string('registration/mails/payment_reminder.html',
dict(registration=registration, payment=self, domain=site.domain))
registration.user.email_user(subject, message, html_message=html)
def send_helloasso_payment_confirmation_mail(self):
translation.activate('fr')
subject = "[TFJM²] " + str(_("Payment confirmation"))
site = Site.objects.first()
for registration in self.registrations.all():
message = loader.render_to_string('registration/mails/payment_confirmation.txt',
dict(registration=registration, payment=self, domain=site.domain))
html = loader.render_to_string('registration/mails/payment_confirmation.html',
dict(registration=registration, payment=self, domain=site.domain))
registration.user.email_user(subject, message, html_message=html)
payer = self.get_checkout_intent()['order']['payer']
payer_name = f"{payer['firstName']} {payer['lastName']}"
if not self.registrations.filter(user__email=payer['email']).exists():
message = loader.render_to_string('registration/mails/payment_confirmation.txt',
dict(registration=payer_name, payment=self, domain=site.domain))
html = loader.render_to_string('registration/mails/payment_confirmation.html',
dict(registration=payer_name, payment=self, domain=site.domain))
send_mail(subject, message, None, [payer['email']], html_message=html)
def get_absolute_url(self):
return reverse_lazy("registration:user_detail", args=(self.registration.user.id,))
return reverse_lazy("registration:update_payment", args=(self.pk,))
def __str__(self):
return _("Payment of {registration}").format(registration=self.registration)
return _("Payment of {registrations}").format(registrations=", ".join(map(str, self.registrations.all())))
class Meta:
verbose_name = _("payment")

View File

@ -4,7 +4,7 @@
from django.contrib.auth.models import User
from tfjm.lists import get_sympa_client
from .models import Payment, Registration, VolunteerRegistration
from .models import Registration, VolunteerRegistration
def set_username(instance, **_):
@ -41,16 +41,3 @@ def create_admin_registration(instance, **_):
"""
if instance.is_superuser:
VolunteerRegistration.objects.get_or_create(user=instance, admin=True)
def create_payment(instance: Registration, raw, **_):
"""
When a user is saved, create the associated payment.
For a free tournament, the payment is valid.
"""
if instance.participates and not raw:
payment = Payment.objects.get_or_create(registration=instance)[0]
if instance.team and instance.team.participation.valid and instance.team.participation.tournament.price == 0:
payment.valid = True
payment.type = "free"
payment.save()

View File

@ -1,10 +1,11 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
import django_tables2 as tables
from participation.models import Team
from .models import Registration
from .models import Payment, Registration
class RegistrationTable(tables.Table):
@ -28,3 +29,41 @@ class RegistrationTable(tables.Table):
model = Registration
fields = ('last_name', 'user__first_name', 'user__email', 'type',)
order_by = ('type', 'last_name', 'first_name',)
class PaymentTable(tables.Table):
"""
Table of all payments.
"""
team_id = tables.Column(
verbose_name=_("team").capitalize,
)
update_payment = tables.LinkColumn(
'registration:update_payment',
accessor='id',
args=[tables.A("id")],
verbose_name=_("Update"),
orderable=False,
)
def render_team_id(self, value):
return Team.objects.get(id=value).trigram
def render_amount(self, value):
return f"{value}"
def render_update_payment(self, record):
return mark_safe(f"<button class='btn btn-secondary'><i class='fas fa-money-bill-wave'></i> {_('Update')}</button>")
class Meta:
attrs = {
'class': 'table table-condensed table-striped',
}
row_attrs = {
'class': lambda record: ('table-success' if record.valid else
'table-danger' if record.valid is False else 'table-warning'),
}
model = Payment
fields = ('registrations', 'team_id', 'type', 'amount', 'valid', 'update_payment',)
empty_text = _("No payment yet.")

View File

@ -0,0 +1,48 @@
{% load i18n %}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<p>
{% trans "Hi" %} {{ registration }},
</p>
<p>
{% blocktrans trimmed with amount=payment.amount team=payment.team.trigram %}
We successfully received the payment of {{ amount }} € for the TFJM² registration in the team {{ team }}!
{% endblocktrans %}
</p>
<p>
{% trans "Your registration is now fully completed, and you can work on your solutions." %}
{% trans "Be sure first that other members of your team also pay their registration." %}
</p>
<p>
{% trans "As a reminder, here are the following important dates:" %}
<ul>
<li>{% trans "Deadline to send the solutions:" %} {{ payment.tournament.solution_limit|date }}</li>
<li>{% trans "Problems draw:" %} {{ payment.tournament.solutions_draw|date }}</li>
<li>{% trans "Tournament dates:" %} {% trans "From" %} {{ payment.tournament.date_start|date }} {% trans "to" %} {{ payment.tournament.date_end|date }}</li>
</ul>
</p>
<p>
{% trans "Please note that these dates may be subject to change. If your local organizers gave you different dates, trust them." %}
</p>
<p>
{% trans "NB: This mail don't represent a payment receipt. The payer should receive a mail from Hello Asso. If it is not the case, please contact us if necessary" %}
</p>
--
<p>
{% trans "The TFJM² team." %}<br>
</p>
</body>
</html>

View File

@ -0,0 +1,22 @@
{% load i18n %}
{% trans "Hi" %} {{ registration|safe }},
{% blocktrans trimmed with amount=payment.amount team=payment.team.trigram tournament=payment.tournament.name %}
We successfully received the payment of {{ amount }} € for the TFJM² registration in the team {{ team }} for the tournament {{ tournament }}!
{% endblocktrans %}
{% trans "Your registration is now fully completed, and you can work on your solutions." %}
{% trans "Be sure first that other members of your team also pay their registration." %}
{% trans "As a reminder, here are the following important dates:" %}
* {% trans "Deadline to send the solutions:" %} {{ payment.tournament.solution_limit|date }}
* {% trans "Problems draw:" %} {{ payment.tournament.solutions_draw|date }}
* {% trans "Tournament dates:" %} {% trans "From" %} {{ payment.tournament.date_start|date }} {% trans "to" %} {{ payment.tournament.date_end|date }}
{% trans "Please note that these dates may be subject to change. If your local organizers gave you different dates, trust them." %}
{% trans "NB: This mail don't represent a payment receipt. The payer should receive a mail from Hello Asso. If it is not the case, please contact us if necessary" %}
--
{% trans "The TFJM² team" %}

View File

@ -0,0 +1,55 @@
{% load i18n %}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<p>
{% trans "Hi" %} {{ registration }},
</p>
<p>
{% blocktrans trimmed with amount=payment.amount team=payment.team.trigram tournament=payment.tournament %}
You are registered for the TFJM² of {{ tournament }}. Your team {{ team }} has been successfully validated.
To end your inscription, you must pay the amount of {{ amount }} €.
{% endblocktrans %}
</p>
{% if payment.grouped %}
<p>
{% trans "This price includes the registrations of all members of your team." %}
</p>
{% endif %}
<p>
{% trans "You can pay by credit card or by bank transfer. You can read full instructions on the payment page:" %}
</p>
<p>
<a href="https://{{ domain }}{% url "registration:update_payment" pk=payment.pk %}">
https://{{ domain }}{% url "registration:update_payment" pk=payment.pk %}
</a>
</p>
<p>
{% trans "If you have a scholarship, then the registration is free for you. You must then upload it on the payment page using the above link." %}
</p>
<p>
{% trans "It is also possible to allow an external person (your parents, your school, etc.) to pay for you with credit card. Instructions are also available on the payment page." %}
</p>
<p>
{% trans "If you have any problem, feel free to contact us." %}
</p>
--
<p>
{% trans "The TFJM² team." %}<br>
</p>
</body>
</html>

View File

@ -0,0 +1,22 @@
{% load i18n %}
{% trans "Hi" %} {{ registration|safe }},
{% blocktrans trimmed with amount=payment.amount team=payment.team.trigram tournament=payment.tournament %}
You are registered for the TFJM² of {{ tournament }}. Your team {{ team }} has been successfully validated.
To end your inscription, you must pay the amount of {{ amount }} €.
{% endblocktrans %}
{% if payment.grouped %}
{% trans "This price includes the registrations of all members of your team." %}
{% endif %}
{% trans "You can pay by credit card or by bank transfer. You can read full instructions on the payment page:" %}
https://{{ domain }}{% url "registration:update_payment" pk=payment.pk %}
{% trans "If you have a scholarship, then the registration is free for you. You must then upload it on the payment page using the above link." %}
{% trans "It is also possible to allow an external person (your parents, your school, etc.) to pay for you with credit card. Instructions are also available on the payment page." %}
{% trans "If you have any problem, feel free to contact us." %}
--
The TFJM² team

View File

@ -3,8 +3,251 @@
{% load crispy_forms_filters i18n %}
{% block content %}
<div class="alert alert-warning">
Le formulaire de paiement est temporairement désactivé. Il sera accessible d'ici quelques jours.
</div>
{% if payment.valid is False %}
<div class="alert alert-info">
<p>
{% blocktrans trimmed with amount=payment.amount team=payment.team.trigram tournament=payment.tournament %}
You must pay {{ amount }} € for your registration in the team {{ team }}
for the tournament {{ tournament }}.
{% endblocktrans %}
{% if payment.grouped %}
{% blocktrans trimmed %}
This price includes the registrations of all members of your team.
{% endblocktrans %}
{% else %}
{% blocktrans trimmed %}
This price includes only your own registration.
You are exempt from payment if you have a scholarship,
but you must then send us a proof of your scholarship.
{% endblocktrans %}
{% endif %}
</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>
{% endif %}
</p>
<ul>
<li>{% trans "team"|capfirst %} : <a href="{% url "participation:team_detail" pk=payment.team.pk %}">{{ payment.team }}</a></li>
<li>{% trans "tournament"|capfirst %} : <a href="{% url "participation:tournament_detail" pk=payment.tournament.pk %}">{{ payment.tournament }}</a></li>
<li>
{% trans "Concerned students" %} :
<ul>
{% for reg in payment.registrations.all %}
<li><a href="{% url "registration:user_detail" pk=reg.user_id %}">{{ reg }}</a></li>
{% endfor %}
</ul>
</li>
</ul>
</div>
<div class="card">
<div class="card-header">
<nav>
<div class="nav nav-tabs card-header-tabs" id="payment-method-tab" role="tablist">
<button class="nav-link active" id="credit-card-tab" data-bs-toggle="tab"
data-bs-target="#credit-card" type="button" role="tab"
aria-controls="credit-card" aria-selected="true">
<i class="fas fa-credit-card"></i> {% trans "Credit card" %}
</button>
<button class="nav-link" id="bank-transfer-tab" data-bs-toggle="tab"
data-bs-target="#bank-transfer" type="button" role="tab"
aria-controls="bank-transfer" aria-selected="true">
<i class="fas fa-money-check"></i> {% trans "Bank transfer" %}
</button>
{% if not payment.grouped %}
<button class="nav-link" id="scholarship-tab" data-bs-toggle="tab"
data-bs-target="#scholarship" type="button" role="tab"
aria-controls="scholarship" aria-selected="true">
<i class="fas fa-file-invoice"></i> {% trans "I have a scholarship" %}
</button>
{% endif %}
<button class="nav-link" id="other-tab" data-bs-toggle="tab"
data-bs-target="#other" type="button" role="tab"
aria-controls="other" aria-selected="true">
<i class="fas fa-question"></i> {% trans "Other" %}
</button>
</div>
</nav>
</div>
<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>
{% blocktrans trimmed %}
The payment by credit card is made via Hello Asso. To do this, you can click on the
button below, which will redirect you to the secure payment page of Hello Asso. The payment
validation will then be done automatically, within a few minutes.
{% endblocktrans %}
</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> {% trans "Go to the Hello Asso page" %}
</a>
</div>
<p>
{% blocktrans trimmed %}
If a third party must pay for you (parents, school,…), you can send them the link to
pay for you:
{% endblocktrans %}
</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="{% trans "Copied!" %}"
onclick="event.preventDefault();copyToClipboard('{{ request.scheme }}://{{ request.site.domain }}{{ payment_url }}?token={{ payment.token }}')">
<i class="fas fa-copy"></i> Copier
</a>
</div>
<p>
{% blocktrans trimmed %}
If this is the case and if an invoice is necessary, please contact the tournament
organizers by providing the name of the team, the number of participants, the name of the
paying establishment, the email address of the establishment and/or the email address of the
establishment manager.
{% endblocktrans %}
</p>
</div>
<div class="tab-pane fade" id="bank-transfer" role="tabpanel" aria-labelledby="bank-transfer-tab">
<p>
{% blocktrans trimmed %}
You can also pay by bank transfer. To do this, you must put in the reference of
the transfer "TFJMpu" followed by the last name and the first name of the student.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
The bank details are as follows:
{% endblocktrans %}
</p>
<p>
IBAN : FR76 1027 8065 0000 0206 4290 127<br>
BIC : CMCIFR2A
</p>
<p>
{% blocktrans trimmed %}
Once your payment done, please send us a proof of your transfer using the below form.
The validation of your payment will then be done manually, within a few days.
{% endblocktrans %}
</p>
<form id="bank-transfer-form" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ bank_transfer_form|crispy }}
<input type="submit" class="btn btn-primary" value="{% trans "Submit" %}" />
</form>
</div>
<div class="tab-pane fade" id="scholarship" role="tabpanel" aria-labelledby="scholarship-tab">
<p>
{% blocktrans trimmed %}
The tournament is free for you if you have a scholarship. However, you must send us a
proof of your scholarship. You can do this using the below form.
{% endblocktrans %}
</p>
<form id="scholarship-form" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ scholarship_form|crispy }}
<input type="submit" class="btn btn-primary" value="{% trans "Submit" %}" />
</form>
</div>
<div class="tab-pane fade" id="other" role="tabpanel" aria-labelledby="other-tab">
<p>
{% blocktrans trimmed %}
If you want to use another payment method, please contact the tournament organizers
first. Then, if you need to send a proof or your payment, you can use the below form.
{% endblocktrans %}
</p>
<form id="other-form" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ other_form|crispy }}
<input type="submit" class="btn btn-primary" value="{% trans "Submit" %}" />
</form>
</div>
</div>
</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>
{% endif %}
{% endblock content %}
{% block extrajavascript %}
<script>
document.addEventListener('DOMContentLoaded', () => {
if (document.location.hash) {
// Open the tab of the tournament that is present in the hash
document.querySelectorAll('button[data-bs-toggle="tab"]').forEach(elem => {
if ('#' + elem.getAttribute('aria-controls') === document.location.hash.toLowerCase()) {
elem.click()
}
})
}
// When a tab is opened, add the tournament name in the hash
document.querySelectorAll('button[data-bs-toggle="tab"]').forEach(
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 %}

View File

@ -143,35 +143,42 @@
</dl>
{% if user_object.registration.participates and user_object.registration.team.participation.valid %}
<hr>
<dl class="row">
<dt class="col-sm-6 text-end">{% trans "Payment information:" %}</dt>
<dd class="col-sm-6">
{% trans "yes,no,pending" as yesnodefault %}
{% with info=user_object.registration.payment.additional_information %}
{% if info %}
<abbr title="{{ info }}">
{{ user_object.registration.payment.get_type_display }}, {% trans "valid:" %} {{ user_object.registration.payment.valid|yesno:yesnodefault }}
</abbr>
{% else %}
{{ user_object.registration.payment.get_type_display }}, {% trans "valid:" %} {{ user_object.registration.payment.valid|yesno:yesnodefault }}
{% endif %}
{% if user.registration.is_admin or user_object.registration.payment.valid is False %}
<button class="btn-sm btn-secondary" data-bs-toggle="modal" data-bs-target="#updatePaymentModal">
<i class="fas fa-money-bill-wave"></i> {% trans "Update payment" %}
</button>
{% endif %}
{% if user_object.registration.payment.type == "scholarship" %}
{% if user.registration.is_admin or user == user_object %}
<a href="{{ user_object.registration.payment.scholarship_file.url }}" class="btn btn-info">
<i class="fas fa-file-pdf"></i> {% trans "Download scholarship attestation" %}
{% for payment in user_object.registration.payments.all %}
<hr>
<dl class="row">
<dt class="col-sm-6 text-end">{% trans "Payment information:" %}</dt>
<dd class="col-sm-6">
{% trans "yes,no,pending" as yesnodefault %}
{% with info=payment.additional_information %}
{% if info %}
<abbr title="{{ info }}">
{{ payment.get_type_display }}, {% trans "valid:" %} {{ payment.valid|yesno:yesnodefault }}
</abbr>
{% else %}
{{ payment.get_type_display }}, {% trans "valid:" %} {{ payment.valid|yesno:yesnodefault }}
{% endif %}
{% if user.registration.is_volunteer or payment.valid is False %}
<a href="{% url "registration:update_payment" pk=payment.pk %}" class="btn btn-secondary">
<i class="fas fa-money-bill-wave"></i> {% trans "Update payment" %}
</a>
{% endif %}
{% endif %}
{% endwith %}
</dd>
</dl>
{% if payment.type == "scholarship" or payment.type == "bank_transfer" %}
{% if user.registration.is_admin or user == user_object %}
<a href="{{ payment.receipt.url }}" class="btn btn-info">
<i class="fas fa-file-pdf"></i>
{% if payment.type == "scholarship" %}
{% trans "Download scholarship attestation" %}
{% elif payment.type == "bank_transfer" %}
{% trans "Download bank transfer receipt" %}
{% endif %}
</a>
{% endif %}
{% endif %}
{% endwith %}
</dd>
</dl>
{% endfor %}
{% endif %}
</div>
{% if user.pk == user_object.pk or user.registration.is_admin %}
@ -210,13 +217,6 @@
{% url "registration:upload_user_parental_authorization" pk=user_object.registration.pk as modal_action %}
{% include "base_modal.html" with modal_id="uploadParentalAuthorization" modal_enctype="multipart/form-data" %}
{% endif %}
{% if user_object.registration.team.participation.valid %}
{% trans "Update payment" as modal_title %}
{% trans "Update" as modal_button %}
{% url "registration:update_payment" pk=user_object.registration.payment.pk as modal_action %}
{% include "base_modal.html" with modal_id="updatePayment" modal_additional_class="modal-xl" modal_enctype="multipart/form-data" %}
{% endif %}
{% endblock %}
{% block extrajavascript %}
@ -228,10 +228,6 @@
initModal("uploadVaccineSheet", "{% url "registration:upload_user_vaccine_sheet" pk=user_object.registration.pk %}")
initModal("uploadParentalAuthorization", "{% url "registration:upload_user_parental_authorization" pk=user_object.registration.pk %}")
{% endif %}
{% if user_object.registration.team.participation.valid %}
initModal("updatePayment", "{% url "registration:update_payment" pk=user_object.registration.payment.pk %}")
{% endif %}
});
</script>
{% endblock %}

View File

@ -4,7 +4,8 @@
from django.urls import path
from .views import AddOrganizerView, AdultPhotoAuthorizationTemplateView, ChildPhotoAuthorizationTemplateView, \
InstructionsTemplateView, MyAccountDetailView, ParentalAuthorizationTemplateView, PaymentUpdateView, \
InstructionsTemplateView, MyAccountDetailView, ParentalAuthorizationTemplateView, \
PaymentHelloAssoReturnView, PaymentRedirectHelloAssoView, PaymentUpdateGroupView, PaymentUpdateView, \
ResetAdminView, SignupView, UserDetailView, UserImpersonateView, UserListView, UserResendValidationEmailView, \
UserUpdateView, UserUploadHealthSheetView, UserUploadParentalAuthorizationView, UserUploadPhotoAuthorizationView, \
UserUploadVaccineSheetView, UserValidateView, UserValidationEmailSentView
@ -37,6 +38,11 @@ urlpatterns = [
path("user/<int:pk>/upload-parental-authorization/", UserUploadParentalAuthorizationView.as_view(),
name="upload_user_parental_authorization"),
path("update-payment/<int:pk>/", PaymentUpdateView.as_view(), name="update_payment"),
path("update-payment/<int:pk>/toggle-group-mode/", PaymentUpdateGroupView.as_view(),
name="update_payment_group_mode"),
path("update-payment/<int:pk>/hello-asso/", PaymentRedirectHelloAssoView.as_view(), name="payment_hello_asso"),
path("update-payment/<int:pk>/hello-asso/return/", PaymentHelloAssoReturnView.as_view(),
name="payment_hello_asso_return"),
path("user/<int:pk>/impersonate/", UserImpersonateView.as_view(), name="user_impersonate"),
path("user/list/", UserListView.as_view(), name="user_list"),
path("reset-admin/", ResetAdminView.as_view(), name="reset_admin"),

View File

@ -1,12 +1,13 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
import json
import os
import subprocess
from tempfile import mkdtemp
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages
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
@ -19,6 +20,7 @@ from django.urls import reverse_lazy
from django.utils import timezone, translation
from django.utils.crypto import get_random_string
from django.utils.http import urlsafe_base64_decode
from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, DetailView, RedirectView, TemplateView, UpdateView, View
from django_tables2 import SingleTableView
@ -27,8 +29,8 @@ from participation.models import Passage, Solution, Synthesis, Tournament
from tfjm.tokens import email_validation_token
from tfjm.views import UserMixin, UserRegistrationMixin, VolunteerMixin
from .forms import AddOrganizerForm, CoachRegistrationForm, HealthSheetForm, \
ParentalAuthorizationForm, PaymentForm, PhotoAuthorizationForm, SignupForm, StudentRegistrationForm, UserForm, \
from .forms import AddOrganizerForm, CoachRegistrationForm, HealthSheetForm, ParentalAuthorizationForm, \
PaymentAdminForm, PaymentForm, PhotoAuthorizationForm, SignupForm, StudentRegistrationForm, UserForm, \
VaccineSheetForm, VolunteerRegistrationForm
from .models import ParticipantRegistration, Payment, Registration, StudentRegistration
from .tables import RegistrationTable
@ -215,6 +217,7 @@ class MyAccountDetailView(LoginRequiredMixin, RedirectView):
"""
Redirect to our own profile detail page.
"""
def get_redirect_url(self, *args, **kwargs):
return reverse_lazy("registration:user_detail", args=(self.request.user.pk,))
@ -443,37 +446,204 @@ class InstructionsTemplateView(AuthorizationTemplateView):
class PaymentUpdateView(LoginRequiredMixin, UpdateView):
model = Payment
form_class = PaymentForm
form_class = PaymentAdminForm
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 != self.get_object().registration.user
and (self.request.user.registration not in self.get_object().registrations.all()
or self.get_object().valid is not False):
return self.handle_no_permission()
return super().dispatch(request, *args, **kwargs)
def get_form(self, form_class=None):
form = super().get_form(form_class)
if not self.request.user.registration.is_admin:
form.fields["type"].widget.choices = list(form.fields["type"].widget.choices)[:-1]
del form.fields["valid"]
return form
def get_context_data(self, **kwargs):
context = super().get_context_data()
context['title'] = _("Update payment")
context['bank_transfer_form'] = PaymentForm(payment_type='bank_transfer',
data=self.request.POST or None,
instance=self.object)
if not self.object.grouped:
context['scholarship_form'] = PaymentForm(payment_type='scholarship',
data=self.request.POST or None,
instance=self.object)
context['other_form'] = PaymentForm(payment_type='other',
data=self.request.POST or None,
instance=self.object)
return context
def form_valid(self, form):
if not self.request.user.registration.is_admin:
form.instance.valid = None
form.instance.valid = None
old_instance = Payment.objects.get(pk=self.object.pk)
if old_instance.scholarship_file:
old_instance.scholarship_file.delete()
if old_instance.receipt:
old_instance.receipt.delete()
old_instance.save()
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy("participation:team_detail", args=(self.object.registrations.first().team.pk,))
class PaymentUpdateGroupView(LoginRequiredMixin, 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):
return self.handle_no_permission()
return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
payment = self.get_object()
if payment.valid is not False:
raise PermissionDenied(_("This payment is already valid or pending validation."))
if payment.grouped:
registrations = list(payment.registrations.all())
first_reg = registrations[0]
payment.registrations.set([first_reg])
payment.grouped = False
tournament = first_reg.team.participation.tournament if not payment.final else Tournament.final_tournament()
payment.amount = tournament.price
payment.checkout_intent_id = None
payment.save()
for registration in registrations[1:]:
p = Payment.objects.create(type=payment.type,
grouped=False,
final=payment.final,
amount=tournament.price,
receipt=payment.receipt,
additional_information=payment.additional_information)
p.registrations.set([registration])
p.save()
else:
reg = payment.registrations.get()
tournament = reg.team.participation.tournament if not payment.final else Tournament.final_tournament()
for student in reg.team.students.all():
if student != reg:
Payment.objects.filter(registrations=student, final=payment.final).delete()
payment.registrations.add(student)
payment.amount = tournament.price * reg.team.students.count()
payment.grouped = True
payment.checkout_intent_id = None
payment.save()
return redirect(reverse_lazy("registration:update_payment", args=(payment.pk,)))
class PaymentRedirectHelloAssoView(AccessMixin, DetailView):
model = Payment
def dispatch(self, request, *args, **kwargs):
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()
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"])
class PaymentHelloAssoReturnView(DetailView):
model = Payment
def get(self, request, *args, **kwargs):
checkout_id = request.GET.get("checkoutIntentId")
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
tournament = payment.tournament
right_to_see = not request.user.is_anonymous \
and (request.user.registration.is_admin
or request.user.registration in payment.registrations.all()
or (request.user.registration.is_volunteer
and tournament in request.user.registration.organized_tournaments.all())
or (request.user.registration.is_coach and request.user.registration.team == team))
if right_to_see:
error_response = redirect("registration:update_payment", pk=payment.pk)
else:
error_response = redirect("index")
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")), "danger")
return error_response
elif return_type == "return":
code = request.GET.get("code")
if code == "refused":
messages.error(request, _("The payment has been refused."), "danger")
return error_response
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), "danger")
return error_response
checkout_intent = payment.get_checkout_intent()
if 'order' in checkout_intent:
payment.type = "helloasso"
payment.valid = True
payment.additional_information = json.dumps(checkout_intent['order'])
payment.save()
messages.success(request, _("The payment has been successfully validated! "
"Your registration is now complete."))
payment.send_helloasso_payment_confirmation_mail()
else:
payment.type = "helloasso"
payment.valid = None
payment.save()
messages.success(request, _("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 contact us."))
if right_to_see:
return redirect("participation:team_detail", pk=team.pk)
else:
return redirect("index")
class PhotoAuthorizationView(LoginRequiredMixin, View):
"""
Display the sent photo authorization.
"""
def get(self, request, *args, **kwargs):
filename = kwargs["filename"]
path = f"media/authorization/photo/{filename}"
@ -497,6 +667,7 @@ class HealthSheetView(LoginRequiredMixin, View):
"""
Display the sent health sheet.
"""
def get(self, request, *args, **kwargs):
filename = kwargs["filename"]
path = f"media/authorization/health/{filename}"
@ -520,6 +691,7 @@ class VaccineSheetView(LoginRequiredMixin, View):
"""
Display the sent health sheet.
"""
def get(self, request, *args, **kwargs):
filename = kwargs["filename"]
path = f"media/authorization/vaccine/{filename}"
@ -543,6 +715,7 @@ class ParentalAuthorizationView(LoginRequiredMixin, View):
"""
Display the sent parental authorization.
"""
def get(self, request, *args, **kwargs):
filename = kwargs["filename"]
path = f"media/authorization/parental/{filename}"
@ -562,25 +735,26 @@ class ParentalAuthorizationView(LoginRequiredMixin, View):
return FileResponse(open(path, "rb"), content_type=mime_type, filename=true_file_name)
class ScholarshipView(LoginRequiredMixin, View):
class ReceiptView(LoginRequiredMixin, View):
"""
Display the sent scholarship paper.
Display the sent payment receipt or scholarship notification.
"""
def get(self, request, *args, **kwargs):
filename = kwargs["filename"]
path = f"media/authorization/scholarship/{filename}"
path = f"media/authorization/receipt/{filename}"
if not os.path.exists(path):
raise Http404
payment = Payment.objects.get(scholarship_file__endswith=filename)
payment = Payment.objects.get(receipt__endswith=filename)
user = request.user
if not (payment.registration.user == user or user.registration.is_admin):
if not (user.registration in payment.registrations.all() or user.registration.is_admin):
raise PermissionDenied
# Guess mime type of the file
mime = Magic(mime=True)
mime_type = mime.from_file(path)
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
# Replace file name
true_file_name = _("Scholarship attestation of {user}.{ext}").format(user=str(user.registration), ext=ext)
true_file_name = _("Payment receipt of {user}.{ext}").format(user=str(user.registration), ext=ext)
return FileResponse(open(path, "rb"), content_type=mime_type, filename=true_file_name)
@ -588,6 +762,7 @@ class SolutionView(LoginRequiredMixin, View):
"""
Display the sent solution.
"""
def get(self, request, *args, **kwargs):
filename = kwargs["filename"]
path = f"media/solutions/{filename}"
@ -631,6 +806,7 @@ class SynthesisView(LoginRequiredMixin, View):
"""
Display the sent synthesis.
"""
def get(self, request, *args, **kwargs):
filename = kwargs["filename"]
path = f"media/syntheses/{filename}"

View File

@ -4,11 +4,16 @@
* * * * * cd /code && python manage.py retry_deferred -c 1
0 0 * * * cd /code && python manage.py purge_mail_log 7 -c 1
# Update search index
*/2 * * * * cd /code && python manage.py update_index &> /dev/null
# Recreate sympa lists
*/2 * * * * cd /code && python manage.py fix_sympa_lists &> /dev/null
# Check payments from Hello Asso
*/6 * * * * cd /code && python manage.py check_hello_asso &> /dev/null
# Send reminders for payments
30 6 * * 1 cd /code && python manage.py remind_payments &> /dev/null
# Clean temporary files
30 * * * * rm -rf /tmp/*

103
tfjm/helloasso.py Normal file
View File

@ -0,0 +1,103 @@
# Copyright (C) 2024 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
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:
return "https://api.helloasso.com"
else:
return "https://api.helloasso-sandbox.com"
def get_hello_asso_access_token():
global _access_token, _refresh_token, _expires_at
base_url = _get_hello_asso_api_base_url()
now = datetime.now()
if _access_token is None:
response = requests.post(
f"{base_url}/oauth2/token",
data={
"grant_type": "client_credentials",
"client_id": settings.HELLOASSO_CLIENT_ID,
"client_secret": settings.HELLOASSO_CLIENT_SECRET,
},
)
elif now >= _expires_at:
response = requests.post(
f"{base_url}/oauth2/token",
data={
"grant_type": "refresh_token",
"refresh_token": _refresh_token,
},
)
else:
return _access_token
if response.status_code == 400:
raise ValueError(str(response.json()))
response.raise_for_status()
data = response.json()
_access_token = data["access_token"]
_refresh_token = data["refresh_token"]
_expires_at = now + timedelta(seconds=data["expires_in"])
return _access_token
def get_checkout_intent(checkout_id, none_if_link_disabled=False):
base_url = _get_hello_asso_api_base_url()
token = get_hello_asso_access_token()
response = requests.get(
f"{base_url}/v5/organizations/animath/checkout-intents/{checkout_id}",
headers={"Authorization": f"Bearer {token}"},
)
if response.status_code == 404:
return None
elif response.status_code == 400:
raise ValueError(str(response.json()['errors']))
response.raise_for_status()
checkout_intent = response.json()
if none_if_link_disabled and requests.head(checkout_intent["redirectUrl"]).status_code == 404:
return None
return checkout_intent
def create_checkout_intent(amount, name, back_url, error_url, return_url, contains_donation=False, metadata=None):
base_url = _get_hello_asso_api_base_url()
token = get_hello_asso_access_token()
metadata = metadata or {}
response = requests.post(
f"{base_url}/v5/organizations/animath/checkout-intents/",
headers={"Authorization": f"Bearer {token}"},
json={
"totalAmount": amount,
"initialAmount": amount,
"itemName": name,
"backUrl": back_url,
"errorUrl": error_url,
"returnUrl": return_url,
"containsDonation": contains_donation,
"metadata": metadata,
},
)
if response.status_code == 400:
raise ValueError(str(response.json()['errors']))
response.raise_for_status()
return response.json()

View File

@ -30,8 +30,11 @@ ADMINS = [("Emmy D'Anello", "emmy.danello@animath.fr")]
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'CHANGE_ME_IN_ENV_SETTINGS')
# dev in development mode, prod in production mode
TFJM_STAGE = os.getenv('TFJM_STAGE', 'dev')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
DEBUG = TFJM_STAGE != "prod"
SITE_ID = 1
@ -76,7 +79,7 @@ if "test" not in sys.argv: # pragma: no cover
'mailer',
]
if os.getenv("TFJM_STAGE", "dev") == "prod": # pragma: no cover
if TFJM_STAGE == "prod": # pragma: no cover
INSTALLED_APPS += [
'channels_redis',
]
@ -238,7 +241,9 @@ else:
PHONENUMBER_DB_FORMAT = 'NATIONAL'
PHONENUMBER_DEFAULT_REGION = 'FR'
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
# 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')
# Custom parameters
PROBLEMS = [
@ -275,7 +280,7 @@ CHANNEL_LAYERS = {
}
}
if os.getenv("TFJM_STAGE", "dev") == "prod": # pragma: no cover
if TFJM_STAGE == "prod": # pragma: no cover
from .settings_prod import * # noqa: F401,F403
else:
from .settings_dev import * # noqa: F401,F403

View File

@ -68,9 +68,6 @@
{% include "base_modal.html" with modal_id="tournamentList" modal_additional_class="modal-lg" %}
{% if user.is_authenticated %}
{% trans "All teams" as modal_title %}
{% include "base_modal.html" with modal_id="teams" modal_additional_class="modal-lg" %}
{% trans "Search results" as modal_title %}
{% include "base_modal.html" with modal_id="search" modal_form_method="get" modal_additional_class="modal-lg" %}
@ -101,7 +98,6 @@
initModal("tournamentList", "{% url "participation:tournament_list" %}")
{% if user.is_authenticated and user.registration.is_admin %}
initModal("teams", "{% url "participation:team_list" %}")
initModal("search",
() => "{% url "haystack_search" %}?q=" + encodeURI(document.getElementById("search-term").value),
"search-results")

View File

@ -21,12 +21,12 @@
<i class="fas fa-calendar-day"></i> {% trans "Tournaments" %}
</a>
</li>
{% if user.is_authenticated and user.registration.is_volunteer %}
{% if user.is_authenticated and user.registration.is_admin %}
<li class="nav-item active">
<a href="{% url "registration:user_list" %}" class="nav-link"><i class="fas fa-user"></i> {% trans "Users" %}</a>
</li>
<li class="nav-item active">
<a href="#" class="nav-link" data-bs-toggle="modal" data-bs-target="#teamsModal"><i class="fas fa-users"></i> {% trans "Teams" %}</a>
<a href="{% url "participation:team_list" %}" class="nav-link"><i class="fas fa-users"></i> {% trans "Teams" %}</a>
</li>
{% elif user.is_authenticated and user.registration.participates %}
{% if not user.registration.team %}

View File

@ -23,7 +23,7 @@ from django.views.defaults import bad_request, page_not_found, permission_denied
from django.views.generic import TemplateView
from participation.views import MotivationLetterView
from registration.views import HealthSheetView, ParentalAuthorizationView, PhotoAuthorizationView, \
ScholarshipView, SolutionView, SynthesisView, VaccineSheetView
ReceiptView, SolutionView, SynthesisView, VaccineSheetView
from .views import AdminSearchView
@ -49,10 +49,10 @@ urlpatterns = [
name='vaccine_sheet'),
path('media/authorization/parental/<str:filename>/', ParentalAuthorizationView.as_view(),
name='parental_authorization'),
path('media/authorization/scholarship/<str:filename>/', ScholarshipView.as_view(),
name='scholarship'),
path('media/authorization/receipt/<str:filename>/', ReceiptView.as_view(),
name='receipt'),
path('media/authorization/motivation_letters/<str:filename>/', MotivationLetterView.as_view(),
name='scholarship'),
name='motivation_letter'),
path('media/solutions/<str:filename>/', SolutionView.as_view(),
name='solution'),