mirror of
https://gitlab.com/animath/si/plateforme.git
synced 2025-02-25 15:06:31 +00:00
Compare commits
No commits in common. "21552756270d1ff4ad82af0b7f1dcad5c2fb739f" and "ece128836a910532d3e52c5ddc4a35e988b7e36f" have entirely different histories.
2155275627
...
ece128836a
@ -7,34 +7,11 @@ 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__number',)
|
list_filter = ('tournament', 'current_round',)
|
||||||
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):
|
||||||
@ -43,16 +20,10 @@ class DrawAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(Round)
|
@admin.register(Round)
|
||||||
class RoundAdmin(admin.ModelAdmin):
|
class RoundAdmin(admin.ModelAdmin):
|
||||||
list_display = ('draw', 'tournament', 'number', 'teams',)
|
list_display = ('draw', '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):
|
||||||
@ -65,8 +36,6 @@ 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):
|
||||||
@ -83,7 +52,6 @@ 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,7 +89,6 @@ 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,74 +7,11 @@ 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):
|
||||||
@ -95,7 +32,6 @@ 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)
|
||||||
@ -104,7 +40,6 @@ 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):
|
||||||
@ -114,30 +49,28 @@ 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', 'position', 'tournament')
|
'pool_abbr', '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"), ordering='defender__team__trigram')
|
@admin.display(description=_("defender"))
|
||||||
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"), ordering='opponent__team__trigram')
|
@admin.display(description=_("opponent"))
|
||||||
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"), ordering='reporter__team__trigram')
|
@admin.display(description=_("reporter"))
|
||||||
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"), ordering='pool__letter')
|
@admin.display(description=_("pool"))
|
||||||
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"), ordering='pool__tournament__name')
|
@admin.display(description=_("tournament"))
|
||||||
def tournament(self, record: Passage):
|
def tournament(self, record: Passage):
|
||||||
return record.pool.tournament
|
return record.pool.tournament
|
||||||
|
|
||||||
@ -191,11 +124,9 @@ class SynthesisAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(Tournament)
|
@admin.register(Tournament)
|
||||||
class TournamentAdmin(admin.ModelAdmin):
|
class TournamentAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'date_start', 'date_end',)
|
list_display = ('name',)
|
||||||
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)
|
||||||
|
87
participation/management/commands/check_hello_asso.py
Normal file
87
participation/management/commands/check_hello_asso.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
# 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 Payment, VolunteerRegistration
|
from registration.models import VolunteerRegistration, Payment
|
||||||
from tfjm.lists import get_sympa_client
|
from tfjm.lists import get_sympa_client
|
||||||
|
|
||||||
|
|
||||||
@ -465,15 +465,14 @@ class Participation(models.Model):
|
|||||||
def important_informations(self):
|
def important_informations(self):
|
||||||
informations = []
|
informations = []
|
||||||
|
|
||||||
missing_payments = Payment.objects.filter(registrations__in=self.team.participants.all(), valid=False)
|
missing_payments = Payment.objects.filter(registration__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(", ".join(str(r) for r in p.registrations.all())
|
participants=", ".join(str(p.registration) for p in missing_payments.all()))
|
||||||
for p in missing_payments.all()))
|
|
||||||
informations.append({
|
informations.append({
|
||||||
'title': _("Missing payments"),
|
'title': _("Missing payments"),
|
||||||
'type': "danger",
|
'type': "danger",
|
||||||
|
@ -112,6 +112,7 @@
|
|||||||
<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 %}
|
||||||
@ -122,30 +123,6 @@
|
|||||||
</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>
|
||||||
|
@ -74,14 +74,6 @@
|
|||||||
{% 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>
|
||||||
|
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
{% 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, TournamentPaymentsView, TournamentUpdateView
|
TournamentListView, TournamentUpdateView
|
||||||
|
|
||||||
|
|
||||||
app_name = "participation"
|
app_name = "participation"
|
||||||
@ -33,7 +33,6 @@ 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,7 +6,6 @@ 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
|
||||||
@ -16,7 +15,6 @@ 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
|
||||||
@ -26,14 +24,13 @@ 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, SingleTableMixin, SingleTableView
|
from django_tables2 import MultiTableMixin, 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 Payment, StudentRegistration, VolunteerRegistration
|
from registration.models import 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
|
||||||
|
|
||||||
@ -249,20 +246,16 @@ 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)
|
||||||
|
|
||||||
for student in self.object.students.all():
|
if self.object.participation.tournament.price == 0:
|
||||||
payment_qs = Payment.objects.filter(registrations=student)
|
for registration in self.object.participants.all():
|
||||||
if payment_qs.exists():
|
registration.payment.type = "free"
|
||||||
payment = payment_qs.get()
|
registration.payment.valid = True
|
||||||
else:
|
registration.payment.save()
|
||||||
payment = Payment.objects.create()
|
else:
|
||||||
payment.registrations.add(student)
|
for coach in self.object.coaches.all():
|
||||||
payment.save()
|
coach.payment.type = "free"
|
||||||
payment.amount = self.object.participation.tournament.price
|
coach.payment.valid = True
|
||||||
if payment.amount == 0:
|
coach.payment.save()
|
||||||
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()
|
||||||
@ -575,31 +568,6 @@ 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,45 +3,13 @@
|
|||||||
|
|
||||||
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, PolymorphicInlineSupportMixin, \
|
from polymorphic.admin import PolymorphicChildModelAdmin, PolymorphicChildModelFilter, PolymorphicParentModelAdmin
|
||||||
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,)
|
||||||
@ -129,34 +97,11 @@ class VolunteerRegistrationAdmin(PolymorphicChildModelAdmin):
|
|||||||
|
|
||||||
@admin.register(Payment)
|
@admin.register(Payment)
|
||||||
class PaymentAdmin(ModelAdmin):
|
class PaymentAdmin(ModelAdmin):
|
||||||
list_display = ('concerned_people', 'tournament', 'team', 'grouped', 'type', 'amount', 'valid', )
|
list_display = ('registration', 'registration_type', 'type', 'valid', )
|
||||||
search_fields = ('registrations__user__last_name', 'registrations__user__first_name', 'registrations__user__email',
|
search_fields = ('registration__user__last_name', 'registration__user__first_name', 'registration__user__email',)
|
||||||
'registrations__team__name', 'registrations__team__participation__team__trigram',)
|
list_filter = ('registration__team__participation__valid', 'type', 'type', 'valid',)
|
||||||
list_filter = ('registrations__team__participation__valid', 'type',
|
autocomplete_fields = ('registration',)
|
||||||
'grouped', 'valid', 'registrations__team__participation__tournament', 'final',)
|
|
||||||
autocomplete_fields = ('registrations',)
|
|
||||||
actions = ('mark_as_valid', 'mark_as_pending', 'mark_as_invalid',)
|
|
||||||
|
|
||||||
@admin.display(description=_('concerned people'))
|
@admin.display(description=_('registration type'), ordering='registration__polymorphic_ctype')
|
||||||
def concerned_people(self, record: Payment):
|
def registration_type(self, record: Payment):
|
||||||
return ", ".join(f"{reg.user.first_name} {reg.user.last_name}" for reg in record.registrations.all())
|
return record.registration.get_real_instance().type
|
||||||
|
|
||||||
@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,8 +12,11 @@ class RegistrationConfig(AppConfig):
|
|||||||
name = 'registration'
|
name = 'registration'
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from registration.signals import create_admin_registration, \
|
from registration.signals import create_admin_registration, create_payment, \
|
||||||
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 PaymentAdminForm(forms.ModelForm):
|
class PaymentForm(forms.ModelForm):
|
||||||
"""
|
"""
|
||||||
Indicate payment information
|
Indicate payment information
|
||||||
"""
|
"""
|
||||||
@ -227,57 +227,25 @@ class PaymentAdminForm(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_receipt(self):
|
def clean_scholarship_file(self):
|
||||||
if "receipt" in self.files:
|
print(self.files)
|
||||||
file = self.files["receipt"]
|
if "scholarship_file" in self.files:
|
||||||
|
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["receipt"]
|
return self.cleaned_data["scholarship_file"]
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
|
|
||||||
if "type" in cleaned_data and cleaned_data['type'] in ["scholarship", "bank_transfer"] \
|
if "type" in cleaned_data and cleaned_data["type"] == "scholarship" \
|
||||||
and "receipt" not in self.files and not self.instance.receipt:
|
and "scholarship_file" not in self.files and not self.instance.scholarship_file:
|
||||||
self.add_error("receipt", _("You must upload your receipt."))
|
self.add_error("scholarship_file", _("You must upload your scholarship attestation."))
|
||||||
|
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Payment
|
model = Payment
|
||||||
fields = ('type', 'receipt', 'additional_information', 'valid',)
|
fields = ('type', 'scholarship_file', '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',)
|
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
# 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()
|
|
@ -1,17 +0,0 @@
|
|||||||
# 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_receipt_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_scholarship_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')),
|
||||||
|
@ -1,76 +0,0 @@
|
|||||||
# 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",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,41 +0,0 @@
|
|||||||
# 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,15 +1,14 @@
|
|||||||
# 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, datetime
|
from datetime import date
|
||||||
|
|
||||||
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, reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils import timezone, translation
|
from django.utils import timezone
|
||||||
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
|
||||||
@ -17,7 +16,6 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@ -80,14 +78,6 @@ 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
|
||||||
@ -374,28 +364,27 @@ class StudentRegistration(ParticipantRegistration):
|
|||||||
})
|
})
|
||||||
|
|
||||||
if self.team and self.team.participation.valid:
|
if self.team and self.team.participation.valid:
|
||||||
for payment in self.payments.all():
|
if self.payment.valid is False:
|
||||||
if payment.valid is False:
|
text = _("You have to pay {amount} € for your registration, or send a scholarship "
|
||||||
text = _("You have to pay {amount} € for your registration, or send a scholarship "
|
"notification or a payment proof. "
|
||||||
"notification or a payment proof. "
|
"You can do it on <a href=\"{url}\">the payment page</a>.")
|
||||||
"You can do it on <a href=\"{url}\">the payment page</a>.")
|
url = reverse_lazy("registration:update_payment", args=(self.payment.id,))
|
||||||
url = reverse_lazy("registration:update_payment", args=(payment.id,))
|
content = format_lazy(text, amount=self.team.participation.tournament.price, url=url)
|
||||||
content = format_lazy(text, amount=payment.amount, url=url)
|
informations.append({
|
||||||
informations.append({
|
'title': _("Payment"),
|
||||||
'title': _("Payment"),
|
'type': "danger",
|
||||||
'type': "danger",
|
'priority': 3,
|
||||||
'priority': 3,
|
'content': content,
|
||||||
'content': content,
|
})
|
||||||
})
|
elif self.payment.valid is None:
|
||||||
elif self.payment.valid is None:
|
text = _("Your payment is under approval.")
|
||||||
text = _("Your payment is under approval.")
|
content = text
|
||||||
content = text
|
informations.append({
|
||||||
informations.append({
|
'title': _("Payment"),
|
||||||
'title': _("Payment"),
|
'type': "warning",
|
||||||
'type': "warning",
|
'priority': 3,
|
||||||
'priority': 3,
|
'content': content,
|
||||||
'content': content,
|
})
|
||||||
})
|
|
||||||
|
|
||||||
return informations
|
return informations
|
||||||
|
|
||||||
@ -495,24 +484,6 @@ 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:
|
||||||
@ -520,44 +491,16 @@ class VolunteerRegistration(Registration):
|
|||||||
verbose_name_plural = _("volunteer registrations")
|
verbose_name_plural = _("volunteer registrations")
|
||||||
|
|
||||||
|
|
||||||
def get_receipt_filename(instance, filename):
|
def get_scholarship_filename(instance, filename):
|
||||||
return f"authorization/receipt/receipt_{instance.id}"
|
return f"authorization/scholarship/scholarship_{instance.registration.pk}"
|
||||||
|
|
||||||
|
|
||||||
def get_random_token():
|
|
||||||
return get_random_string(32)
|
|
||||||
|
|
||||||
|
|
||||||
class Payment(models.Model):
|
class Payment(models.Model):
|
||||||
registrations = models.ManyToManyField(
|
registration = models.OneToOneField(
|
||||||
ParticipantRegistration,
|
ParticipantRegistration,
|
||||||
related_name="payments",
|
on_delete=models.CASCADE,
|
||||||
verbose_name=_("registrations"),
|
related_name="payment",
|
||||||
)
|
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(
|
||||||
@ -565,7 +508,7 @@ class Payment(models.Model):
|
|||||||
max_length=16,
|
max_length=16,
|
||||||
choices=[
|
choices=[
|
||||||
('', _("No payment")),
|
('', _("No payment")),
|
||||||
('helloasso', _("Credit card")),
|
('helloasso', "Hello Asso"),
|
||||||
('scholarship', _("Scholarship")),
|
('scholarship', _("Scholarship")),
|
||||||
('bank_transfer', _("Bank transfer")),
|
('bank_transfer', _("Bank transfer")),
|
||||||
('other', _("Other (please indicate)")),
|
('other', _("Other (please indicate)")),
|
||||||
@ -575,17 +518,10 @@ class Payment(models.Model):
|
|||||||
default="",
|
default="",
|
||||||
)
|
)
|
||||||
|
|
||||||
checkout_intent_id = models.IntegerField(
|
scholarship_file = models.FileField(
|
||||||
verbose_name=_("Hello Asso checkout intent ID"),
|
verbose_name=_("scholarship file"),
|
||||||
blank=True,
|
help_text=_("only if you have a scholarship."),
|
||||||
null=True,
|
upload_to=get_scholarship_filename,
|
||||||
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="",
|
||||||
)
|
)
|
||||||
@ -603,95 +539,11 @@ 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:update_payment", args=(self.pk,))
|
return reverse_lazy("registration:user_detail", args=(self.registration.user.id,))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return _("Payment of {registrations}").format(registrations=", ".join(map(str, self.registrations.all())))
|
return _("Payment of {registration}").format(registration=self.registration)
|
||||||
|
|
||||||
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 Registration, VolunteerRegistration
|
from .models import Payment, Registration, VolunteerRegistration
|
||||||
|
|
||||||
|
|
||||||
def set_username(instance, **_):
|
def set_username(instance, **_):
|
||||||
@ -41,3 +41,16 @@ 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,11 +1,10 @@
|
|||||||
# 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 Payment, Registration
|
from .models import Registration
|
||||||
|
|
||||||
|
|
||||||
class RegistrationTable(tables.Table):
|
class RegistrationTable(tables.Table):
|
||||||
@ -29,41 +28,3 @@ 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.")
|
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
{% 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>
|
|
@ -1,22 +0,0 @@
|
|||||||
{% 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" %}
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
|||||||
{% 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>
|
|
@ -1,22 +0,0 @@
|
|||||||
{% 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,251 +3,8 @@
|
|||||||
{% load crispy_forms_filters i18n %}
|
{% load crispy_forms_filters i18n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if payment.valid is False %}
|
<div class="alert alert-warning">
|
||||||
<div class="alert alert-info">
|
Le formulaire de paiement est temporairement désactivé. Il sera accessible d'ici quelques jours.
|
||||||
<p>
|
</div>
|
||||||
{% 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,42 +143,35 @@
|
|||||||
</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 %}
|
||||||
{% for payment in user_object.registration.payments.all %}
|
<hr>
|
||||||
<hr>
|
|
||||||
|
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
<dt class="col-sm-6 text-end">{% trans "Payment information:" %}</dt>
|
<dt class="col-sm-6 text-end">{% trans "Payment information:" %}</dt>
|
||||||
<dd class="col-sm-6">
|
<dd class="col-sm-6">
|
||||||
{% trans "yes,no,pending" as yesnodefault %}
|
{% trans "yes,no,pending" as yesnodefault %}
|
||||||
{% with info=payment.additional_information %}
|
{% with info=user_object.registration.payment.additional_information %}
|
||||||
{% if info %}
|
{% if info %}
|
||||||
<abbr title="{{ info }}">
|
<abbr title="{{ info }}">
|
||||||
{{ payment.get_type_display }}, {% trans "valid:" %} {{ payment.valid|yesno:yesnodefault }}
|
{{ user_object.registration.payment.get_type_display }}, {% trans "valid:" %} {{ user_object.registration.payment.valid|yesno:yesnodefault }}
|
||||||
</abbr>
|
</abbr>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ payment.get_type_display }}, {% trans "valid:" %} {{ payment.valid|yesno:yesnodefault }}
|
{{ user_object.registration.payment.get_type_display }}, {% trans "valid:" %} {{ user_object.registration.payment.valid|yesno:yesnodefault }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if user.registration.is_volunteer or payment.valid is False %}
|
{% if user.registration.is_admin or user_object.registration.payment.valid is False %}
|
||||||
<a href="{% url "registration:update_payment" pk=payment.pk %}" class="btn btn-secondary">
|
<button class="btn-sm btn-secondary" data-bs-toggle="modal" data-bs-target="#updatePaymentModal">
|
||||||
<i class="fas fa-money-bill-wave"></i> {% trans "Update payment" %}
|
<i class="fas fa-money-bill-wave"></i> {% trans "Update payment" %}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% if user_object.registration.payment.type == "scholarship" %}
|
||||||
|
{% if user.registration.is_admin or user == user_object %}
|
||||||
|
<a href="{{ user_object.registration.payment.scholarship_file.url }}" class="btn btn-info">
|
||||||
|
<i class="fas fa-file-pdf"></i> {% trans "Download scholarship attestation" %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if payment.type == "scholarship" or payment.type == "bank_transfer" %}
|
{% endif %}
|
||||||
{% if user.registration.is_admin or user == user_object %}
|
{% endwith %}
|
||||||
<a href="{{ payment.receipt.url }}" class="btn btn-info">
|
</dd>
|
||||||
<i class="fas fa-file-pdf"></i>
|
</dl>
|
||||||
{% 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 %}
|
||||||
@ -217,6 +210,13 @@
|
|||||||
{% 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,6 +228,10 @@
|
|||||||
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,8 +4,7 @@
|
|||||||
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, \
|
InstructionsTemplateView, MyAccountDetailView, ParentalAuthorizationTemplateView, PaymentUpdateView, \
|
||||||
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
|
||||||
@ -38,11 +37,6 @@ 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,13 +1,12 @@
|
|||||||
# 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 import messages
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.contrib.auth.mixins import AccessMixin, LoginRequiredMixin
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.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
|
||||||
@ -20,7 +19,6 @@ 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
|
||||||
@ -29,8 +27,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, ParentalAuthorizationForm, \
|
from .forms import AddOrganizerForm, CoachRegistrationForm, HealthSheetForm, \
|
||||||
PaymentAdminForm, PaymentForm, PhotoAuthorizationForm, SignupForm, StudentRegistrationForm, UserForm, \
|
ParentalAuthorizationForm, 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
|
||||||
@ -217,7 +215,6 @@ 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,))
|
||||||
|
|
||||||
@ -446,204 +443,37 @@ class InstructionsTemplateView(AuthorizationTemplateView):
|
|||||||
|
|
||||||
class PaymentUpdateView(LoginRequiredMixin, UpdateView):
|
class PaymentUpdateView(LoginRequiredMixin, UpdateView):
|
||||||
model = Payment
|
model = Payment
|
||||||
form_class = PaymentAdminForm
|
form_class = PaymentForm
|
||||||
|
|
||||||
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.registration not in self.get_object().registrations.all()
|
and (self.request.user != self.get_object().registration.user
|
||||||
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_context_data(self, **kwargs):
|
def get_form(self, form_class=None):
|
||||||
context = super().get_context_data()
|
form = super().get_form(form_class)
|
||||||
context['title'] = _("Update payment")
|
if not self.request.user.registration.is_admin:
|
||||||
context['bank_transfer_form'] = PaymentForm(payment_type='bank_transfer',
|
form.fields["type"].widget.choices = list(form.fields["type"].widget.choices)[:-1]
|
||||||
data=self.request.POST or None,
|
del form.fields["valid"]
|
||||||
instance=self.object)
|
return form
|
||||||
|
|
||||||
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):
|
||||||
form.instance.valid = None
|
if not self.request.user.registration.is_admin:
|
||||||
|
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.receipt:
|
if old_instance.scholarship_file:
|
||||||
old_instance.receipt.delete()
|
old_instance.scholarship_file.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}"
|
||||||
@ -667,7 +497,6 @@ 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}"
|
||||||
@ -691,7 +520,6 @@ 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}"
|
||||||
@ -715,7 +543,6 @@ 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}"
|
||||||
@ -735,26 +562,25 @@ 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 ReceiptView(LoginRequiredMixin, View):
|
class ScholarshipView(LoginRequiredMixin, View):
|
||||||
"""
|
"""
|
||||||
Display the sent payment receipt or scholarship notification.
|
Display the sent scholarship paper.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
filename = kwargs["filename"]
|
filename = kwargs["filename"]
|
||||||
path = f"media/authorization/receipt/{filename}"
|
path = f"media/authorization/scholarship/{filename}"
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
raise Http404
|
raise Http404
|
||||||
payment = Payment.objects.get(receipt__endswith=filename)
|
payment = Payment.objects.get(scholarship_file__endswith=filename)
|
||||||
user = request.user
|
user = request.user
|
||||||
if not (user.registration in payment.registrations.all() or user.registration.is_admin):
|
if not (payment.registration.user == user 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 = _("Payment receipt of {user}.{ext}").format(user=str(user.registration), ext=ext)
|
true_file_name = _("Scholarship attestation 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)
|
||||||
|
|
||||||
|
|
||||||
@ -762,7 +588,6 @@ 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}"
|
||||||
@ -806,7 +631,6 @@ 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,16 +4,11 @@
|
|||||||
* * * * * 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/*
|
||||||
|
@ -1,103 +0,0 @@
|
|||||||
# 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,11 +30,8 @@ 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 = TFJM_STAGE != "prod"
|
DEBUG = True
|
||||||
|
|
||||||
SITE_ID = 1
|
SITE_ID = 1
|
||||||
|
|
||||||
@ -79,7 +76,7 @@ if "test" not in sys.argv: # pragma: no cover
|
|||||||
'mailer',
|
'mailer',
|
||||||
]
|
]
|
||||||
|
|
||||||
if TFJM_STAGE == "prod": # pragma: no cover
|
if os.getenv("TFJM_STAGE", "dev") == "prod": # pragma: no cover
|
||||||
INSTALLED_APPS += [
|
INSTALLED_APPS += [
|
||||||
'channels_redis',
|
'channels_redis',
|
||||||
]
|
]
|
||||||
@ -241,9 +238,7 @@ else:
|
|||||||
PHONENUMBER_DB_FORMAT = 'NATIONAL'
|
PHONENUMBER_DB_FORMAT = 'NATIONAL'
|
||||||
PHONENUMBER_DEFAULT_REGION = 'FR'
|
PHONENUMBER_DEFAULT_REGION = 'FR'
|
||||||
|
|
||||||
# Hello Asso API creds
|
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
|
||||||
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 = [
|
||||||
@ -280,7 +275,7 @@ CHANNEL_LAYERS = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if TFJM_STAGE == "prod": # pragma: no cover
|
if os.getenv("TFJM_STAGE", "dev") == "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,6 +68,9 @@
|
|||||||
{% 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" %}
|
||||||
|
|
||||||
@ -98,6 +101,7 @@
|
|||||||
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_admin %}
|
{% if user.is_authenticated and user.registration.is_volunteer %}
|
||||||
<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="{% url "participation:team_list" %}" class="nav-link"><i class="fas fa-users"></i> {% trans "Teams" %}</a>
|
<a href="#" class="nav-link" data-bs-toggle="modal" data-bs-target="#teamsModal"><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, \
|
||||||
ReceiptView, SolutionView, SynthesisView, VaccineSheetView
|
ScholarshipView, 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/receipt/<str:filename>/', ReceiptView.as_view(),
|
path('media/authorization/scholarship/<str:filename>/', ScholarshipView.as_view(),
|
||||||
name='receipt'),
|
name='scholarship'),
|
||||||
path('media/authorization/motivation_letters/<str:filename>/', MotivationLetterView.as_view(),
|
path('media/authorization/motivation_letters/<str:filename>/', MotivationLetterView.as_view(),
|
||||||
name='motivation_letter'),
|
name='scholarship'),
|
||||||
|
|
||||||
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