mirror of
https://gitlab.com/animath/si/plateforme.git
synced 2025-02-25 09:06:30 +00:00
Compare commits
17 Commits
ece128836a
...
2155275627
Author | SHA1 | Date | |
---|---|---|---|
|
2155275627 | ||
|
7b4e867e33 | ||
|
2c54f315f6 | ||
|
5cbc72b41f | ||
|
de504398d2 | ||
|
cae1c6fdb8 | ||
|
6a928ee35b | ||
|
bc535f4075 | ||
|
64b91cf7e0 | ||
|
54dafe1cec | ||
|
b16b6e422f | ||
|
8d08b18d08 | ||
|
8c7e9648dd | ||
|
b3555a7807 | ||
|
98d04b9093 | ||
|
4d157b2bd7 | ||
|
7c9083a6b8 |
@ -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):
|
||||
|
@ -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
@ -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)
|
||||
|
@ -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")
|
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -74,6 +74,14 @@
|
||||
{% 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>
|
||||
|
||||
|
@ -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 %}
|
@ -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"),
|
||||
|
@ -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()
|
||||
for student in self.object.students.all():
|
||||
payment_qs = Payment.objects.filter(registrations=student)
|
||||
if payment_qs.exists():
|
||||
payment = payment_qs.get()
|
||||
else:
|
||||
for coach in self.object.coaches.all():
|
||||
coach.payment.type = "free"
|
||||
coach.payment.valid = True
|
||||
coach.payment.save()
|
||||
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.
|
||||
|
@ -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]
|
||||
|
@ -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")
|
||||
|
@ -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',)
|
||||
|
25
registration/management/commands/check_hello_asso.py
Normal file
25
registration/management/commands/check_hello_asso.py
Normal 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()
|
17
registration/management/commands/remind_payments.py
Normal file
17
registration/management/commands/remind_payments.py
Normal 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()
|
@ -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')),
|
||||
|
@ -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",
|
||||
),
|
||||
),
|
||||
]
|
@ -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",
|
||||
),
|
||||
),
|
||||
]
|
@ -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,12 +374,13 @@ class StudentRegistration(ParticipantRegistration):
|
||||
})
|
||||
|
||||
if self.team and self.team.participation.valid:
|
||||
if self.payment.valid is False:
|
||||
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=(self.payment.id,))
|
||||
content = format_lazy(text, amount=self.team.participation.tournament.price, url=url)
|
||||
url = reverse_lazy("registration:update_payment", args=(payment.id,))
|
||||
content = format_lazy(text, amount=payment.amount, url=url)
|
||||
informations.append({
|
||||
'title': _("Payment"),
|
||||
'type': "danger",
|
||||
@ -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")
|
||||
|
@ -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()
|
||||
|
@ -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.")
|
||||
|
@ -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>
|
@ -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" %}
|
||||
|
@ -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>
|
@ -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
|
@ -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.
|
||||
{% 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 %}
|
||||
|
@ -143,35 +143,42 @@
|
||||
</dl>
|
||||
|
||||
{% if user_object.registration.participates and user_object.registration.team.participation.valid %}
|
||||
{% 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=user_object.registration.payment.additional_information %}
|
||||
{% with info=payment.additional_information %}
|
||||
{% if info %}
|
||||
<abbr title="{{ info }}">
|
||||
{{ user_object.registration.payment.get_type_display }}, {% trans "valid:" %} {{ user_object.registration.payment.valid|yesno:yesnodefault }}
|
||||
{{ payment.get_type_display }}, {% trans "valid:" %} {{ payment.valid|yesno:yesnodefault }}
|
||||
</abbr>
|
||||
{% else %}
|
||||
{{ user_object.registration.payment.get_type_display }}, {% trans "valid:" %} {{ user_object.registration.payment.valid|yesno:yesnodefault }}
|
||||
{{ payment.get_type_display }}, {% trans "valid:" %} {{ 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">
|
||||
{% 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" %}
|
||||
</button>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if user_object.registration.payment.type == "scholarship" %}
|
||||
{% if payment.type == "scholarship" or payment.type == "bank_transfer" %}
|
||||
{% 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" %}
|
||||
<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 %}
|
||||
|
@ -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"),
|
||||
|
@ -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
|
||||
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}"
|
||||
|
@ -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
103
tfjm/helloasso.py
Normal 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()
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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 %}
|
||||
|
@ -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'),
|
||||
|
Loading…
x
Reference in New Issue
Block a user