mirror of
https://gitlab.com/animath/si/plateforme.git
synced 2025-02-25 05:46: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
|
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)
|
@admin.register(Draw)
|
||||||
class DrawAdmin(admin.ModelAdmin):
|
class DrawAdmin(admin.ModelAdmin):
|
||||||
list_display = ('tournament', 'teams', 'current_round', 'get_state',)
|
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',)
|
search_fields = ('tournament__name', 'tournament__participation__team__trigram',)
|
||||||
|
autocomplete_fields = ('tournament',)
|
||||||
|
inlines = (RoundInline,)
|
||||||
|
|
||||||
@admin.display(description=_("teams"))
|
@admin.display(description=_("teams"))
|
||||||
def teams(self, record: Draw):
|
def teams(self, record: Draw):
|
||||||
@ -20,10 +43,16 @@ class DrawAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(Round)
|
@admin.register(Round)
|
||||||
class RoundAdmin(admin.ModelAdmin):
|
class RoundAdmin(admin.ModelAdmin):
|
||||||
list_display = ('draw', 'number', 'teams',)
|
list_display = ('draw', 'tournament', 'number', 'teams',)
|
||||||
list_filter = ('draw__tournament', 'number',)
|
list_filter = ('draw__tournament', 'number',)
|
||||||
search_fields = ('draw__tournament__name', 'pool__teamdraw__participation__team__trigram')
|
search_fields = ('draw__tournament__name', 'pool__teamdraw__participation__team__trigram')
|
||||||
ordering = ('draw__tournament__name', 'number')
|
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"))
|
@admin.display(description=_("teams"))
|
||||||
def teams(self, record: Round):
|
def teams(self, record: Round):
|
||||||
@ -36,6 +65,8 @@ class PoolAdmin(admin.ModelAdmin):
|
|||||||
list_filter = ('round__draw__tournament', 'round__number', 'letter')
|
list_filter = ('round__draw__tournament', 'round__number', 'letter')
|
||||||
ordering = ('round__draw__tournament__name', 'round', 'letter')
|
ordering = ('round__draw__tournament__name', 'round', 'letter')
|
||||||
search_fields = ('round__draw__tournament__name', 'teamdraw__participation__team__trigram',)
|
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"))
|
@admin.display(ordering='round__draw__tournament__name', description=_("tournament"))
|
||||||
def tournament(self, record):
|
def tournament(self, record):
|
||||||
@ -52,6 +83,7 @@ class TeamDrawAdmin(admin.ModelAdmin):
|
|||||||
'passage_index', 'choose_index', 'passage_dice', 'choice_dice',)
|
'passage_index', 'choose_index', 'passage_dice', 'choice_dice',)
|
||||||
list_filter = ('round__draw__tournament', 'round__number', 'pool__letter',)
|
list_filter = ('round__draw__tournament', 'round__number', 'pool__letter',)
|
||||||
search_fields = ('round__draw__tournament__name', 'participation__team__trigram',)
|
search_fields = ('round__draw__tournament__name', 'participation__team__trigram',)
|
||||||
|
autocomplete_fields = ('participation', 'round', 'pool',)
|
||||||
|
|
||||||
@admin.display(ordering='round__draw__tournament__name', description=_("tournament"))
|
@admin.display(ordering='round__draw__tournament__name', description=_("tournament"))
|
||||||
def tournament(self, record):
|
def tournament(self, record):
|
||||||
|
@ -89,6 +89,7 @@ class Draw(models.Model):
|
|||||||
return 'WAITING_DRAW_PROBLEM'
|
return 'WAITING_DRAW_PROBLEM'
|
||||||
else:
|
else:
|
||||||
return 'WAITING_CHOOSE_PROBLEM'
|
return 'WAITING_CHOOSE_PROBLEM'
|
||||||
|
get_state.short_description = _('State')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def information(self):
|
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
|
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)
|
@admin.register(Team)
|
||||||
class TeamAdmin(admin.ModelAdmin):
|
class TeamAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'trigram', 'tournament', 'valid', 'final',)
|
list_display = ('name', 'trigram', 'tournament', 'valid', 'final',)
|
||||||
search_fields = ('name', 'trigram',)
|
search_fields = ('name', 'trigram',)
|
||||||
list_filter = ('participation__valid', 'participation__tournament', 'participation__final',)
|
list_filter = ('participation__valid', 'participation__tournament', 'participation__final',)
|
||||||
|
inlines = (ParticipationInline,)
|
||||||
|
|
||||||
@admin.display(description=_("tournament"))
|
@admin.display(description=_("tournament"))
|
||||||
def tournament(self, record):
|
def tournament(self, record):
|
||||||
@ -32,6 +95,7 @@ class ParticipationAdmin(admin.ModelAdmin):
|
|||||||
search_fields = ('team__name', 'team__trigram',)
|
search_fields = ('team__name', 'team__trigram',)
|
||||||
list_filter = ('valid',)
|
list_filter = ('valid',)
|
||||||
autocomplete_fields = ('team', 'tournament',)
|
autocomplete_fields = ('team', 'tournament',)
|
||||||
|
inlines = (SolutionInline, SynthesisInline,)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Pool)
|
@admin.register(Pool)
|
||||||
@ -40,6 +104,7 @@ class PoolAdmin(admin.ModelAdmin):
|
|||||||
list_filter = ('tournament', 'round', 'letter',)
|
list_filter = ('tournament', 'round', 'letter',)
|
||||||
search_fields = ('participations__team__name', 'participations__team__trigram',)
|
search_fields = ('participations__team__name', 'participations__team__trigram',)
|
||||||
autocomplete_fields = ('tournament', 'participations', 'juries',)
|
autocomplete_fields = ('tournament', 'participations', 'juries',)
|
||||||
|
inlines = (PassageInline, TweakInline,)
|
||||||
|
|
||||||
@admin.display(description=_("teams"))
|
@admin.display(description=_("teams"))
|
||||||
def teams(self, record: Pool):
|
def teams(self, record: Pool):
|
||||||
@ -49,28 +114,30 @@ class PoolAdmin(admin.ModelAdmin):
|
|||||||
@admin.register(Passage)
|
@admin.register(Passage)
|
||||||
class PassageAdmin(admin.ModelAdmin):
|
class PassageAdmin(admin.ModelAdmin):
|
||||||
list_display = ('__str__', 'defender_trigram', 'solution_number', 'opponent_trigram', 'reporter_trigram',
|
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',)
|
list_filter = ('pool__tournament', 'pool__round', 'pool__letter', 'solution_number',)
|
||||||
search_fields = ('pool__participations__team__name', 'pool__participations__team__trigram',)
|
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',)
|
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):
|
def defender_trigram(self, record: Passage):
|
||||||
return record.defender.team.trigram
|
return record.defender.team.trigram
|
||||||
|
|
||||||
@admin.display(description=_("opponent"))
|
@admin.display(description=_("opponent"), ordering='opponent__team__trigram')
|
||||||
def opponent_trigram(self, record: Passage):
|
def opponent_trigram(self, record: Passage):
|
||||||
return record.opponent.team.trigram
|
return record.opponent.team.trigram
|
||||||
|
|
||||||
@admin.display(description=_("reporter"))
|
@admin.display(description=_("reporter"), ordering='reporter__team__trigram')
|
||||||
def reporter_trigram(self, record: Passage):
|
def reporter_trigram(self, record: Passage):
|
||||||
return record.reporter.team.trigram
|
return record.reporter.team.trigram
|
||||||
|
|
||||||
@admin.display(description=_("pool"))
|
@admin.display(description=_("pool"), ordering='pool__letter')
|
||||||
def pool_abbr(self, record):
|
def pool_abbr(self, record):
|
||||||
return f"{record.pool.get_letter_display()}{record.pool.round}"
|
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):
|
def tournament(self, record: Passage):
|
||||||
return record.pool.tournament
|
return record.pool.tournament
|
||||||
|
|
||||||
@ -124,9 +191,11 @@ class SynthesisAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(Tournament)
|
@admin.register(Tournament)
|
||||||
class TournamentAdmin(admin.ModelAdmin):
|
class TournamentAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name',)
|
list_display = ('name', 'date_start', 'date_end',)
|
||||||
search_fields = ('name',)
|
search_fields = ('name',)
|
||||||
|
ordering = ('date_start', 'name',)
|
||||||
autocomplete_fields = ('organizers',)
|
autocomplete_fields = ('organizers',)
|
||||||
|
inlines = (ParticipationTabularInline, PoolInline,)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Tweak)
|
@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.crypto import get_random_string
|
||||||
from django.utils.text import format_lazy
|
from django.utils.text import format_lazy
|
||||||
from django.utils.translation import gettext_lazy as _
|
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
|
from tfjm.lists import get_sympa_client
|
||||||
|
|
||||||
|
|
||||||
@ -465,14 +465,15 @@ class Participation(models.Model):
|
|||||||
def important_informations(self):
|
def important_informations(self):
|
||||||
informations = []
|
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():
|
if missing_payments.exists():
|
||||||
text = _("<p>The team {trigram} has {nb_missing_payments} missing payments. Each member of the team "
|
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) "
|
"must have a valid payment (or send a scholarship notification) "
|
||||||
"to participate to the tournament.</p>"
|
"to participate to the tournament.</p>"
|
||||||
"<p>Participants that have not paid yet are: {participants}.</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(),
|
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({
|
informations.append({
|
||||||
'title': _("Missing payments"),
|
'title': _("Missing payments"),
|
||||||
'type': "danger",
|
'type': "danger",
|
||||||
|
@ -112,7 +112,6 @@
|
|||||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadMotivationLetterModal">{% trans "Replace" %}</button>
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadMotivationLetterModal">{% trans "Replace" %}</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
|
||||||
|
|
||||||
{% if user.registration.is_volunteer %}
|
{% if user.registration.is_volunteer %}
|
||||||
{% if user.registration in self.team.participation.tournament.organizers or user.registration.is_admin %}
|
{% if user.registration in self.team.participation.tournament.organizers or user.registration.is_admin %}
|
||||||
@ -123,6 +122,30 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% 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>
|
||||||
<div class="card-footer text-center">
|
<div class="card-footer text-center">
|
||||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#updateTeamModal">{% trans "Update" %}</button>
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#updateTeamModal">{% trans "Update" %}</button>
|
||||||
|
@ -73,6 +73,14 @@
|
|||||||
<div id="teams_table">
|
<div id="teams_table">
|
||||||
{% render_table teams %}
|
{% render_table teams %}
|
||||||
</div>
|
</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 %}
|
{% if pools.data %}
|
||||||
<hr>
|
<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, \
|
PoolUpdateTeamsView, PoolUpdateView, PoolUploadNotesView, ScaleNotationSheetTemplateView, SolutionUploadView, \
|
||||||
SynthesisUploadView, TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \
|
SynthesisUploadView, TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \
|
||||||
TeamUploadMotivationLetterView, TournamentCreateView, TournamentDetailView, TournamentExportCSVView, \
|
TeamUploadMotivationLetterView, TournamentCreateView, TournamentDetailView, TournamentExportCSVView, \
|
||||||
TournamentListView, TournamentUpdateView
|
TournamentListView, TournamentPaymentsView, TournamentUpdateView
|
||||||
|
|
||||||
|
|
||||||
app_name = "participation"
|
app_name = "participation"
|
||||||
@ -33,6 +33,7 @@ urlpatterns = [
|
|||||||
path("tournament/create/", TournamentCreateView.as_view(), name="tournament_create"),
|
path("tournament/create/", TournamentCreateView.as_view(), name="tournament_create"),
|
||||||
path("tournament/<int:pk>/", TournamentDetailView.as_view(), name="tournament_detail"),
|
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>/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("tournament/<int:pk>/csv/", TournamentExportCSVView.as_view(), name="tournament_csv"),
|
||||||
path("pools/create/", PoolCreateView.as_view(), name="pool_create"),
|
path("pools/create/", PoolCreateView.as_view(), name="pool_create"),
|
||||||
path("pools/<int:pk>/", PoolDetailView.as_view(), name="pool_detail"),
|
path("pools/<int:pk>/", PoolDetailView.as_view(), name="pool_detail"),
|
||||||
|
@ -6,6 +6,7 @@ from io import BytesIO
|
|||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from tempfile import mkdtemp
|
from tempfile import mkdtemp
|
||||||
|
from typing import Any, Dict
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
|
||||||
from django.conf import settings
|
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.exceptions import PermissionDenied
|
||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from django.db.models import F
|
||||||
from django.http import FileResponse, Http404, HttpResponse
|
from django.http import FileResponse, Http404, HttpResponse
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.template.loader import render_to_string
|
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.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import CreateView, DetailView, FormView, RedirectView, TemplateView, UpdateView, View
|
from django.views.generic import CreateView, DetailView, FormView, RedirectView, TemplateView, UpdateView, View
|
||||||
from django.views.generic.edit import FormMixin, ProcessFormView
|
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 magic import Magic
|
||||||
from odf.opendocument import OpenDocumentSpreadsheet
|
from odf.opendocument import OpenDocumentSpreadsheet
|
||||||
from odf.style import Style, TableCellProperties, TableColumnProperties, TextProperties
|
from odf.style import Style, TableCellProperties, TableColumnProperties, TextProperties
|
||||||
from odf.table import CoveredTableCell, Table, TableCell, TableColumn, TableRow
|
from odf.table import CoveredTableCell, Table, TableCell, TableColumn, TableRow
|
||||||
from odf.text import P
|
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.lists import get_sympa_client
|
||||||
from tfjm.views import AdminMixin, VolunteerMixin
|
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)
|
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)
|
send_mail("[TFJM²] Équipe validée", mail_plain, None, [self.object.email], html_message=mail_html)
|
||||||
|
|
||||||
if self.object.participation.tournament.price == 0:
|
for student in self.object.students.all():
|
||||||
for registration in self.object.participants.all():
|
payment_qs = Payment.objects.filter(registrations=student)
|
||||||
registration.payment.type = "free"
|
if payment_qs.exists():
|
||||||
registration.payment.valid = True
|
payment = payment_qs.get()
|
||||||
registration.payment.save()
|
else:
|
||||||
else:
|
payment = Payment.objects.create()
|
||||||
for coach in self.object.coaches.all():
|
payment.registrations.add(student)
|
||||||
coach.payment.type = "free"
|
payment.save()
|
||||||
coach.payment.valid = True
|
payment.amount = self.object.participation.tournament.price
|
||||||
coach.payment.save()
|
if payment.amount == 0:
|
||||||
|
payment.type = "free"
|
||||||
|
payment.valid = True
|
||||||
|
payment.save()
|
||||||
|
|
||||||
elif "invalidate" in self.request.POST:
|
elif "invalidate" in self.request.POST:
|
||||||
self.object.participation.valid = None
|
self.object.participation.valid = None
|
||||||
self.object.participation.save()
|
self.object.participation.save()
|
||||||
@ -568,6 +575,31 @@ class TournamentDetailView(MultiTableMixin, DetailView):
|
|||||||
return context
|
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):
|
class TournamentExportCSVView(VolunteerMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
Export team information in a CSV file.
|
Export team information in a CSV file.
|
||||||
|
@ -3,13 +3,45 @@
|
|||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.admin import ModelAdmin
|
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 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, \
|
from .models import CoachRegistration, ParticipantRegistration, Payment, Registration, \
|
||||||
StudentRegistration, VolunteerRegistration
|
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)
|
@admin.register(Registration)
|
||||||
class RegistrationAdmin(PolymorphicParentModelAdmin):
|
class RegistrationAdmin(PolymorphicParentModelAdmin):
|
||||||
child_models = (StudentRegistration, CoachRegistration, VolunteerRegistration,)
|
child_models = (StudentRegistration, CoachRegistration, VolunteerRegistration,)
|
||||||
@ -97,11 +129,34 @@ class VolunteerRegistrationAdmin(PolymorphicChildModelAdmin):
|
|||||||
|
|
||||||
@admin.register(Payment)
|
@admin.register(Payment)
|
||||||
class PaymentAdmin(ModelAdmin):
|
class PaymentAdmin(ModelAdmin):
|
||||||
list_display = ('registration', 'registration_type', 'type', 'valid', )
|
list_display = ('concerned_people', 'tournament', 'team', 'grouped', 'type', 'amount', 'valid', )
|
||||||
search_fields = ('registration__user__last_name', 'registration__user__first_name', 'registration__user__email',)
|
search_fields = ('registrations__user__last_name', 'registrations__user__first_name', 'registrations__user__email',
|
||||||
list_filter = ('registration__team__participation__valid', 'type', 'type', 'valid',)
|
'registrations__team__name', 'registrations__team__participation__team__trigram',)
|
||||||
autocomplete_fields = ('registration',)
|
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')
|
@admin.display(description=_('concerned people'))
|
||||||
def registration_type(self, record: Payment):
|
def concerned_people(self, record: Payment):
|
||||||
return record.registration.get_real_instance().type
|
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'
|
name = 'registration'
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from registration.signals import create_admin_registration, create_payment, \
|
from registration.signals import create_admin_registration, \
|
||||||
set_username, send_email_link
|
set_username, send_email_link
|
||||||
pre_save.connect(set_username, "auth.User")
|
pre_save.connect(set_username, "auth.User")
|
||||||
pre_save.connect(send_email_link, "auth.User")
|
pre_save.connect(send_email_link, "auth.User")
|
||||||
post_save.connect(create_admin_registration, "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',)
|
fields = ('professional_activity', 'admin', 'give_contact_to_animath', 'email_confirmed',)
|
||||||
|
|
||||||
|
|
||||||
class PaymentForm(forms.ModelForm):
|
class PaymentAdminForm(forms.ModelForm):
|
||||||
"""
|
"""
|
||||||
Indicate payment information
|
Indicate payment information
|
||||||
"""
|
"""
|
||||||
@ -227,25 +227,57 @@ class PaymentForm(forms.ModelForm):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields["valid"].widget.choices[0] = ('unknown', _("Pending"))
|
self.fields["valid"].widget.choices[0] = ('unknown', _("Pending"))
|
||||||
|
|
||||||
def clean_scholarship_file(self):
|
def clean_receipt(self):
|
||||||
print(self.files)
|
if "receipt" in self.files:
|
||||||
if "scholarship_file" in self.files:
|
file = self.files["receipt"]
|
||||||
file = self.files["scholarship_file"]
|
|
||||||
if file.size > 2e6:
|
if file.size > 2e6:
|
||||||
raise ValidationError(_("The uploaded file size must be under 2 Mo."))
|
raise ValidationError(_("The uploaded file size must be under 2 Mo."))
|
||||||
if file.content_type not in ["application/pdf", "image/png", "image/jpeg"]:
|
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."))
|
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):
|
def clean(self):
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
|
|
||||||
if "type" in cleaned_data and cleaned_data["type"] == "scholarship" \
|
if "type" in cleaned_data and cleaned_data['type'] in ["scholarship", "bank_transfer"] \
|
||||||
and "scholarship_file" not in self.files and not self.instance.scholarship_file:
|
and "receipt" not in self.files and not self.instance.receipt:
|
||||||
self.add_error("scholarship_file", _("You must upload your scholarship attestation."))
|
self.add_error("receipt", _("You must upload your receipt."))
|
||||||
|
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Payment
|
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=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('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')),
|
('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')),
|
('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')),
|
('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')),
|
('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
|
# Copyright (C) 2020 by Animath
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# 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.contrib.sites.models import Site
|
||||||
|
from django.core.mail import send_mail
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.template import loader
|
from django.template import loader
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils import timezone
|
from django.utils import timezone, translation
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.encoding import force_bytes
|
from django.utils.encoding import force_bytes
|
||||||
from django.utils.http import urlsafe_base64_encode
|
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 django.utils.translation import gettext_lazy as _
|
||||||
from phonenumber_field.modelfields import PhoneNumberField
|
from phonenumber_field.modelfields import PhoneNumberField
|
||||||
from polymorphic.models import PolymorphicModel
|
from polymorphic.models import PolymorphicModel
|
||||||
|
from tfjm import helloasso
|
||||||
from tfjm.tokens import email_validation_token
|
from tfjm.tokens import email_validation_token
|
||||||
|
|
||||||
|
|
||||||
@ -78,6 +80,14 @@ class Registration(PolymorphicModel):
|
|||||||
def participates(self):
|
def participates(self):
|
||||||
return isinstance(self, ParticipantRegistration)
|
return isinstance(self, ParticipantRegistration)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_student(self):
|
||||||
|
return isinstance(self, StudentRegistration)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_coach(self):
|
||||||
|
return isinstance(self, CoachRegistration)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_admin(self):
|
def is_admin(self):
|
||||||
return isinstance(self, VolunteerRegistration) and self.admin or self.user.is_superuser
|
return isinstance(self, VolunteerRegistration) and self.admin or self.user.is_superuser
|
||||||
@ -364,27 +374,28 @@ class StudentRegistration(ParticipantRegistration):
|
|||||||
})
|
})
|
||||||
|
|
||||||
if self.team and self.team.participation.valid:
|
if self.team and self.team.participation.valid:
|
||||||
if self.payment.valid is False:
|
for payment in self.payments.all():
|
||||||
text = _("You have to pay {amount} € for your registration, or send a scholarship "
|
if payment.valid is False:
|
||||||
"notification or a payment proof. "
|
text = _("You have to pay {amount} € for your registration, or send a scholarship "
|
||||||
"You can do it on <a href=\"{url}\">the payment page</a>.")
|
"notification or a payment proof. "
|
||||||
url = reverse_lazy("registration:update_payment", args=(self.payment.id,))
|
"You can do it on <a href=\"{url}\">the payment page</a>.")
|
||||||
content = format_lazy(text, amount=self.team.participation.tournament.price, url=url)
|
url = reverse_lazy("registration:update_payment", args=(payment.id,))
|
||||||
informations.append({
|
content = format_lazy(text, amount=payment.amount, url=url)
|
||||||
'title': _("Payment"),
|
informations.append({
|
||||||
'type': "danger",
|
'title': _("Payment"),
|
||||||
'priority': 3,
|
'type': "danger",
|
||||||
'content': content,
|
'priority': 3,
|
||||||
})
|
'content': content,
|
||||||
elif self.payment.valid is None:
|
})
|
||||||
text = _("Your payment is under approval.")
|
elif self.payment.valid is None:
|
||||||
content = text
|
text = _("Your payment is under approval.")
|
||||||
informations.append({
|
content = text
|
||||||
'title': _("Payment"),
|
informations.append({
|
||||||
'type': "warning",
|
'title': _("Payment"),
|
||||||
'priority': 3,
|
'type': "warning",
|
||||||
'content': content,
|
'priority': 3,
|
||||||
})
|
'content': content,
|
||||||
|
})
|
||||||
|
|
||||||
return informations
|
return informations
|
||||||
|
|
||||||
@ -484,6 +495,24 @@ class VolunteerRegistration(Registration):
|
|||||||
'content': content,
|
'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
|
return informations
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -491,16 +520,44 @@ class VolunteerRegistration(Registration):
|
|||||||
verbose_name_plural = _("volunteer registrations")
|
verbose_name_plural = _("volunteer registrations")
|
||||||
|
|
||||||
|
|
||||||
def get_scholarship_filename(instance, filename):
|
def get_receipt_filename(instance, filename):
|
||||||
return f"authorization/scholarship/scholarship_{instance.registration.pk}"
|
return f"authorization/receipt/receipt_{instance.id}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_random_token():
|
||||||
|
return get_random_string(32)
|
||||||
|
|
||||||
|
|
||||||
class Payment(models.Model):
|
class Payment(models.Model):
|
||||||
registration = models.OneToOneField(
|
registrations = models.ManyToManyField(
|
||||||
ParticipantRegistration,
|
ParticipantRegistration,
|
||||||
on_delete=models.CASCADE,
|
related_name="payments",
|
||||||
related_name="payment",
|
verbose_name=_("registrations"),
|
||||||
verbose_name=_("registration"),
|
)
|
||||||
|
|
||||||
|
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(
|
type = models.CharField(
|
||||||
@ -508,7 +565,7 @@ class Payment(models.Model):
|
|||||||
max_length=16,
|
max_length=16,
|
||||||
choices=[
|
choices=[
|
||||||
('', _("No payment")),
|
('', _("No payment")),
|
||||||
('helloasso', "Hello Asso"),
|
('helloasso', _("Credit card")),
|
||||||
('scholarship', _("Scholarship")),
|
('scholarship', _("Scholarship")),
|
||||||
('bank_transfer', _("Bank transfer")),
|
('bank_transfer', _("Bank transfer")),
|
||||||
('other', _("Other (please indicate)")),
|
('other', _("Other (please indicate)")),
|
||||||
@ -518,10 +575,17 @@ class Payment(models.Model):
|
|||||||
default="",
|
default="",
|
||||||
)
|
)
|
||||||
|
|
||||||
scholarship_file = models.FileField(
|
checkout_intent_id = models.IntegerField(
|
||||||
verbose_name=_("scholarship file"),
|
verbose_name=_("Hello Asso checkout intent ID"),
|
||||||
help_text=_("only if you have a scholarship."),
|
blank=True,
|
||||||
upload_to=get_scholarship_filename,
|
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,
|
blank=True,
|
||||||
default="",
|
default="",
|
||||||
)
|
)
|
||||||
@ -539,11 +603,95 @@ class Payment(models.Model):
|
|||||||
default=False,
|
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):
|
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):
|
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:
|
class Meta:
|
||||||
verbose_name = _("payment")
|
verbose_name = _("payment")
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from tfjm.lists import get_sympa_client
|
from tfjm.lists import get_sympa_client
|
||||||
|
|
||||||
from .models import Payment, Registration, VolunteerRegistration
|
from .models import Registration, VolunteerRegistration
|
||||||
|
|
||||||
|
|
||||||
def set_username(instance, **_):
|
def set_username(instance, **_):
|
||||||
@ -41,16 +41,3 @@ def create_admin_registration(instance, **_):
|
|||||||
"""
|
"""
|
||||||
if instance.is_superuser:
|
if instance.is_superuser:
|
||||||
VolunteerRegistration.objects.get_or_create(user=instance, admin=True)
|
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
|
# Copyright (C) 2020 by Animath
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
|
from participation.models import Team
|
||||||
|
|
||||||
from .models import Registration
|
from .models import Payment, Registration
|
||||||
|
|
||||||
|
|
||||||
class RegistrationTable(tables.Table):
|
class RegistrationTable(tables.Table):
|
||||||
@ -28,3 +29,41 @@ class RegistrationTable(tables.Table):
|
|||||||
model = Registration
|
model = Registration
|
||||||
fields = ('last_name', 'user__first_name', 'user__email', 'type',)
|
fields = ('last_name', 'user__first_name', 'user__email', 'type',)
|
||||||
order_by = ('type', 'last_name', 'first_name',)
|
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 %}
|
{% load crispy_forms_filters i18n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="alert alert-warning">
|
{% if payment.valid is False %}
|
||||||
Le formulaire de paiement est temporairement désactivé. Il sera accessible d'ici quelques jours.
|
<div class="alert alert-info">
|
||||||
</div>
|
<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 %}
|
{% 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>
|
</dl>
|
||||||
|
|
||||||
{% if user_object.registration.participates and user_object.registration.team.participation.valid %}
|
{% if user_object.registration.participates and user_object.registration.team.participation.valid %}
|
||||||
<hr>
|
{% for payment in user_object.registration.payments.all %}
|
||||||
|
<hr>
|
||||||
<dl class="row">
|
|
||||||
<dt class="col-sm-6 text-end">{% trans "Payment information:" %}</dt>
|
<dl class="row">
|
||||||
<dd class="col-sm-6">
|
<dt class="col-sm-6 text-end">{% trans "Payment information:" %}</dt>
|
||||||
{% trans "yes,no,pending" as yesnodefault %}
|
<dd class="col-sm-6">
|
||||||
{% with info=user_object.registration.payment.additional_information %}
|
{% trans "yes,no,pending" as yesnodefault %}
|
||||||
{% if info %}
|
{% with info=payment.additional_information %}
|
||||||
<abbr title="{{ info }}">
|
{% if info %}
|
||||||
{{ user_object.registration.payment.get_type_display }}, {% trans "valid:" %} {{ user_object.registration.payment.valid|yesno:yesnodefault }}
|
<abbr title="{{ info }}">
|
||||||
</abbr>
|
{{ payment.get_type_display }}, {% trans "valid:" %} {{ payment.valid|yesno:yesnodefault }}
|
||||||
{% else %}
|
</abbr>
|
||||||
{{ user_object.registration.payment.get_type_display }}, {% trans "valid:" %} {{ user_object.registration.payment.valid|yesno:yesnodefault }}
|
{% else %}
|
||||||
{% endif %}
|
{{ payment.get_type_display }}, {% trans "valid:" %} {{ payment.valid|yesno:yesnodefault }}
|
||||||
{% if user.registration.is_admin or user_object.registration.payment.valid is False %}
|
{% endif %}
|
||||||
<button class="btn-sm btn-secondary" data-bs-toggle="modal" data-bs-target="#updatePaymentModal">
|
{% if user.registration.is_volunteer or payment.valid is False %}
|
||||||
<i class="fas fa-money-bill-wave"></i> {% trans "Update payment" %}
|
<a href="{% url "registration:update_payment" pk=payment.pk %}" class="btn btn-secondary">
|
||||||
</button>
|
<i class="fas fa-money-bill-wave"></i> {% trans "Update payment" %}
|
||||||
{% endif %}
|
|
||||||
{% if user_object.registration.payment.type == "scholarship" %}
|
|
||||||
{% if user.registration.is_admin or user == user_object %}
|
|
||||||
<a href="{{ user_object.registration.payment.scholarship_file.url }}" class="btn btn-info">
|
|
||||||
<i class="fas fa-file-pdf"></i> {% trans "Download scholarship attestation" %}
|
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% if payment.type == "scholarship" or payment.type == "bank_transfer" %}
|
||||||
{% endwith %}
|
{% if user.registration.is_admin or user == user_object %}
|
||||||
</dd>
|
<a href="{{ payment.receipt.url }}" class="btn btn-info">
|
||||||
</dl>
|
<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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if user.pk == user_object.pk or user.registration.is_admin %}
|
{% 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 %}
|
{% 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" %}
|
{% include "base_modal.html" with modal_id="uploadParentalAuthorization" modal_enctype="multipart/form-data" %}
|
||||||
{% endif %}
|
{% 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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extrajavascript %}
|
{% block extrajavascript %}
|
||||||
@ -228,10 +228,6 @@
|
|||||||
initModal("uploadVaccineSheet", "{% url "registration:upload_user_vaccine_sheet" pk=user_object.registration.pk %}")
|
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 %}")
|
initModal("uploadParentalAuthorization", "{% url "registration:upload_user_parental_authorization" pk=user_object.registration.pk %}")
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if user_object.registration.team.participation.valid %}
|
|
||||||
initModal("updatePayment", "{% url "registration:update_payment" pk=user_object.registration.payment.pk %}")
|
|
||||||
{% endif %}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -4,7 +4,8 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from .views import AddOrganizerView, AdultPhotoAuthorizationTemplateView, ChildPhotoAuthorizationTemplateView, \
|
from .views import AddOrganizerView, AdultPhotoAuthorizationTemplateView, ChildPhotoAuthorizationTemplateView, \
|
||||||
InstructionsTemplateView, MyAccountDetailView, ParentalAuthorizationTemplateView, PaymentUpdateView, \
|
InstructionsTemplateView, MyAccountDetailView, ParentalAuthorizationTemplateView, \
|
||||||
|
PaymentHelloAssoReturnView, PaymentRedirectHelloAssoView, PaymentUpdateGroupView, PaymentUpdateView, \
|
||||||
ResetAdminView, SignupView, UserDetailView, UserImpersonateView, UserListView, UserResendValidationEmailView, \
|
ResetAdminView, SignupView, UserDetailView, UserImpersonateView, UserListView, UserResendValidationEmailView, \
|
||||||
UserUpdateView, UserUploadHealthSheetView, UserUploadParentalAuthorizationView, UserUploadPhotoAuthorizationView, \
|
UserUpdateView, UserUploadHealthSheetView, UserUploadParentalAuthorizationView, UserUploadPhotoAuthorizationView, \
|
||||||
UserUploadVaccineSheetView, UserValidateView, UserValidationEmailSentView
|
UserUploadVaccineSheetView, UserValidateView, UserValidationEmailSentView
|
||||||
@ -37,6 +38,11 @@ urlpatterns = [
|
|||||||
path("user/<int:pk>/upload-parental-authorization/", UserUploadParentalAuthorizationView.as_view(),
|
path("user/<int:pk>/upload-parental-authorization/", UserUploadParentalAuthorizationView.as_view(),
|
||||||
name="upload_user_parental_authorization"),
|
name="upload_user_parental_authorization"),
|
||||||
path("update-payment/<int:pk>/", PaymentUpdateView.as_view(), name="update_payment"),
|
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/<int:pk>/impersonate/", UserImpersonateView.as_view(), name="user_impersonate"),
|
||||||
path("user/list/", UserListView.as_view(), name="user_list"),
|
path("user/list/", UserListView.as_view(), name="user_list"),
|
||||||
path("reset-admin/", ResetAdminView.as_view(), name="reset_admin"),
|
path("reset-admin/", ResetAdminView.as_view(), name="reset_admin"),
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
# Copyright (C) 2020 by Animath
|
# Copyright (C) 2020 by Animath
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from tempfile import mkdtemp
|
from tempfile import mkdtemp
|
||||||
|
|
||||||
from django.conf import settings
|
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.auth.models import User
|
||||||
from django.contrib.sites.models import Site
|
from django.contrib.sites.models import Site
|
||||||
from django.core.exceptions import PermissionDenied, ValidationError
|
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 import timezone, translation
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.http import urlsafe_base64_decode
|
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.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import CreateView, DetailView, RedirectView, TemplateView, UpdateView, View
|
from django.views.generic import CreateView, DetailView, RedirectView, TemplateView, UpdateView, View
|
||||||
from django_tables2 import SingleTableView
|
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.tokens import email_validation_token
|
||||||
from tfjm.views import UserMixin, UserRegistrationMixin, VolunteerMixin
|
from tfjm.views import UserMixin, UserRegistrationMixin, VolunteerMixin
|
||||||
|
|
||||||
from .forms import AddOrganizerForm, CoachRegistrationForm, HealthSheetForm, \
|
from .forms import AddOrganizerForm, CoachRegistrationForm, HealthSheetForm, ParentalAuthorizationForm, \
|
||||||
ParentalAuthorizationForm, PaymentForm, PhotoAuthorizationForm, SignupForm, StudentRegistrationForm, UserForm, \
|
PaymentAdminForm, PaymentForm, PhotoAuthorizationForm, SignupForm, StudentRegistrationForm, UserForm, \
|
||||||
VaccineSheetForm, VolunteerRegistrationForm
|
VaccineSheetForm, VolunteerRegistrationForm
|
||||||
from .models import ParticipantRegistration, Payment, Registration, StudentRegistration
|
from .models import ParticipantRegistration, Payment, Registration, StudentRegistration
|
||||||
from .tables import RegistrationTable
|
from .tables import RegistrationTable
|
||||||
@ -215,6 +217,7 @@ class MyAccountDetailView(LoginRequiredMixin, RedirectView):
|
|||||||
"""
|
"""
|
||||||
Redirect to our own profile detail page.
|
Redirect to our own profile detail page.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_redirect_url(self, *args, **kwargs):
|
def get_redirect_url(self, *args, **kwargs):
|
||||||
return reverse_lazy("registration:user_detail", args=(self.request.user.pk,))
|
return reverse_lazy("registration:user_detail", args=(self.request.user.pk,))
|
||||||
|
|
||||||
@ -443,37 +446,204 @@ class InstructionsTemplateView(AuthorizationTemplateView):
|
|||||||
|
|
||||||
class PaymentUpdateView(LoginRequiredMixin, UpdateView):
|
class PaymentUpdateView(LoginRequiredMixin, UpdateView):
|
||||||
model = Payment
|
model = Payment
|
||||||
form_class = PaymentForm
|
form_class = PaymentAdminForm
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
if not self.request.user.is_authenticated or \
|
if not self.request.user.is_authenticated or \
|
||||||
not self.request.user.registration.is_admin \
|
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):
|
or self.get_object().valid is not False):
|
||||||
return self.handle_no_permission()
|
return self.handle_no_permission()
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_form(self, form_class=None):
|
def get_context_data(self, **kwargs):
|
||||||
form = super().get_form(form_class)
|
context = super().get_context_data()
|
||||||
if not self.request.user.registration.is_admin:
|
context['title'] = _("Update payment")
|
||||||
form.fields["type"].widget.choices = list(form.fields["type"].widget.choices)[:-1]
|
context['bank_transfer_form'] = PaymentForm(payment_type='bank_transfer',
|
||||||
del form.fields["valid"]
|
data=self.request.POST or None,
|
||||||
return form
|
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):
|
def form_valid(self, form):
|
||||||
if not self.request.user.registration.is_admin:
|
form.instance.valid = None
|
||||||
form.instance.valid = None
|
|
||||||
old_instance = Payment.objects.get(pk=self.object.pk)
|
old_instance = Payment.objects.get(pk=self.object.pk)
|
||||||
if old_instance.scholarship_file:
|
if old_instance.receipt:
|
||||||
old_instance.scholarship_file.delete()
|
old_instance.receipt.delete()
|
||||||
old_instance.save()
|
old_instance.save()
|
||||||
return super().form_valid(form)
|
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):
|
class PhotoAuthorizationView(LoginRequiredMixin, View):
|
||||||
"""
|
"""
|
||||||
Display the sent photo authorization.
|
Display the sent photo authorization.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
filename = kwargs["filename"]
|
filename = kwargs["filename"]
|
||||||
path = f"media/authorization/photo/{filename}"
|
path = f"media/authorization/photo/{filename}"
|
||||||
@ -497,6 +667,7 @@ class HealthSheetView(LoginRequiredMixin, View):
|
|||||||
"""
|
"""
|
||||||
Display the sent health sheet.
|
Display the sent health sheet.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
filename = kwargs["filename"]
|
filename = kwargs["filename"]
|
||||||
path = f"media/authorization/health/{filename}"
|
path = f"media/authorization/health/{filename}"
|
||||||
@ -520,6 +691,7 @@ class VaccineSheetView(LoginRequiredMixin, View):
|
|||||||
"""
|
"""
|
||||||
Display the sent health sheet.
|
Display the sent health sheet.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
filename = kwargs["filename"]
|
filename = kwargs["filename"]
|
||||||
path = f"media/authorization/vaccine/{filename}"
|
path = f"media/authorization/vaccine/{filename}"
|
||||||
@ -543,6 +715,7 @@ class ParentalAuthorizationView(LoginRequiredMixin, View):
|
|||||||
"""
|
"""
|
||||||
Display the sent parental authorization.
|
Display the sent parental authorization.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
filename = kwargs["filename"]
|
filename = kwargs["filename"]
|
||||||
path = f"media/authorization/parental/{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)
|
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):
|
def get(self, request, *args, **kwargs):
|
||||||
filename = kwargs["filename"]
|
filename = kwargs["filename"]
|
||||||
path = f"media/authorization/scholarship/{filename}"
|
path = f"media/authorization/receipt/{filename}"
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
raise Http404
|
raise Http404
|
||||||
payment = Payment.objects.get(scholarship_file__endswith=filename)
|
payment = Payment.objects.get(receipt__endswith=filename)
|
||||||
user = request.user
|
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
|
raise PermissionDenied
|
||||||
# Guess mime type of the file
|
# Guess mime type of the file
|
||||||
mime = Magic(mime=True)
|
mime = Magic(mime=True)
|
||||||
mime_type = mime.from_file(path)
|
mime_type = mime.from_file(path)
|
||||||
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
|
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
|
||||||
# Replace file name
|
# 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)
|
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.
|
Display the sent solution.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
filename = kwargs["filename"]
|
filename = kwargs["filename"]
|
||||||
path = f"media/solutions/{filename}"
|
path = f"media/solutions/{filename}"
|
||||||
@ -631,6 +806,7 @@ class SynthesisView(LoginRequiredMixin, View):
|
|||||||
"""
|
"""
|
||||||
Display the sent synthesis.
|
Display the sent synthesis.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
filename = kwargs["filename"]
|
filename = kwargs["filename"]
|
||||||
path = f"media/syntheses/{filename}"
|
path = f"media/syntheses/{filename}"
|
||||||
|
@ -4,11 +4,16 @@
|
|||||||
* * * * * cd /code && python manage.py retry_deferred -c 1
|
* * * * * cd /code && python manage.py retry_deferred -c 1
|
||||||
0 0 * * * cd /code && python manage.py purge_mail_log 7 -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
|
# Recreate sympa lists
|
||||||
*/2 * * * * cd /code && python manage.py fix_sympa_lists &> /dev/null
|
*/2 * * * * cd /code && python manage.py fix_sympa_lists &> /dev/null
|
||||||
|
|
||||||
# Check payments from Hello Asso
|
# Check payments from Hello Asso
|
||||||
*/6 * * * * cd /code && python manage.py check_hello_asso &> /dev/null
|
*/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
|
# Clean temporary files
|
||||||
30 * * * * rm -rf /tmp/*
|
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!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'CHANGE_ME_IN_ENV_SETTINGS')
|
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!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = TFJM_STAGE != "prod"
|
||||||
|
|
||||||
SITE_ID = 1
|
SITE_ID = 1
|
||||||
|
|
||||||
@ -76,7 +79,7 @@ if "test" not in sys.argv: # pragma: no cover
|
|||||||
'mailer',
|
'mailer',
|
||||||
]
|
]
|
||||||
|
|
||||||
if os.getenv("TFJM_STAGE", "dev") == "prod": # pragma: no cover
|
if TFJM_STAGE == "prod": # pragma: no cover
|
||||||
INSTALLED_APPS += [
|
INSTALLED_APPS += [
|
||||||
'channels_redis',
|
'channels_redis',
|
||||||
]
|
]
|
||||||
@ -238,7 +241,9 @@ else:
|
|||||||
PHONENUMBER_DB_FORMAT = 'NATIONAL'
|
PHONENUMBER_DB_FORMAT = 'NATIONAL'
|
||||||
PHONENUMBER_DEFAULT_REGION = 'FR'
|
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
|
# Custom parameters
|
||||||
PROBLEMS = [
|
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
|
from .settings_prod import * # noqa: F401,F403
|
||||||
else:
|
else:
|
||||||
from .settings_dev import * # noqa: F401,F403
|
from .settings_dev import * # noqa: F401,F403
|
||||||
|
@ -68,9 +68,6 @@
|
|||||||
{% include "base_modal.html" with modal_id="tournamentList" modal_additional_class="modal-lg" %}
|
{% include "base_modal.html" with modal_id="tournamentList" modal_additional_class="modal-lg" %}
|
||||||
|
|
||||||
{% if user.is_authenticated %}
|
{% 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 %}
|
{% trans "Search results" as modal_title %}
|
||||||
{% include "base_modal.html" with modal_id="search" modal_form_method="get" modal_additional_class="modal-lg" %}
|
{% 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" %}")
|
initModal("tournamentList", "{% url "participation:tournament_list" %}")
|
||||||
|
|
||||||
{% if user.is_authenticated and user.registration.is_admin %}
|
{% if user.is_authenticated and user.registration.is_admin %}
|
||||||
initModal("teams", "{% url "participation:team_list" %}")
|
|
||||||
initModal("search",
|
initModal("search",
|
||||||
() => "{% url "haystack_search" %}?q=" + encodeURI(document.getElementById("search-term").value),
|
() => "{% url "haystack_search" %}?q=" + encodeURI(document.getElementById("search-term").value),
|
||||||
"search-results")
|
"search-results")
|
||||||
|
@ -21,12 +21,12 @@
|
|||||||
<i class="fas fa-calendar-day"></i> {% trans "Tournaments" %}
|
<i class="fas fa-calendar-day"></i> {% trans "Tournaments" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% if user.is_authenticated and user.registration.is_volunteer %}
|
{% if user.is_authenticated and user.registration.is_admin %}
|
||||||
<li class="nav-item active">
|
<li class="nav-item active">
|
||||||
<a href="{% url "registration:user_list" %}" class="nav-link"><i class="fas fa-user"></i> {% trans "Users" %}</a>
|
<a href="{% url "registration:user_list" %}" class="nav-link"><i class="fas fa-user"></i> {% trans "Users" %}</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item active">
|
<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>
|
</li>
|
||||||
{% elif user.is_authenticated and user.registration.participates %}
|
{% elif user.is_authenticated and user.registration.participates %}
|
||||||
{% if not user.registration.team %}
|
{% 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 django.views.generic import TemplateView
|
||||||
from participation.views import MotivationLetterView
|
from participation.views import MotivationLetterView
|
||||||
from registration.views import HealthSheetView, ParentalAuthorizationView, PhotoAuthorizationView, \
|
from registration.views import HealthSheetView, ParentalAuthorizationView, PhotoAuthorizationView, \
|
||||||
ScholarshipView, SolutionView, SynthesisView, VaccineSheetView
|
ReceiptView, SolutionView, SynthesisView, VaccineSheetView
|
||||||
|
|
||||||
from .views import AdminSearchView
|
from .views import AdminSearchView
|
||||||
|
|
||||||
@ -49,10 +49,10 @@ urlpatterns = [
|
|||||||
name='vaccine_sheet'),
|
name='vaccine_sheet'),
|
||||||
path('media/authorization/parental/<str:filename>/', ParentalAuthorizationView.as_view(),
|
path('media/authorization/parental/<str:filename>/', ParentalAuthorizationView.as_view(),
|
||||||
name='parental_authorization'),
|
name='parental_authorization'),
|
||||||
path('media/authorization/scholarship/<str:filename>/', ScholarshipView.as_view(),
|
path('media/authorization/receipt/<str:filename>/', ReceiptView.as_view(),
|
||||||
name='scholarship'),
|
name='receipt'),
|
||||||
path('media/authorization/motivation_letters/<str:filename>/', MotivationLetterView.as_view(),
|
path('media/authorization/motivation_letters/<str:filename>/', MotivationLetterView.as_view(),
|
||||||
name='scholarship'),
|
name='motivation_letter'),
|
||||||
|
|
||||||
path('media/solutions/<str:filename>/', SolutionView.as_view(),
|
path('media/solutions/<str:filename>/', SolutionView.as_view(),
|
||||||
name='solution'),
|
name='solution'),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user