bracket-feature
laurent 1 year ago
commit 03910e6d61
  1. 6
      api/serializers.py
  2. 1
      api/urls.py
  3. 15
      api/views.py
  4. 7
      tournaments/admin.py
  5. 26
      tournaments/migrations/0091_drawlog.py
  6. 18
      tournaments/migrations/0092_club_timezone.py
  7. 7
      tournaments/models/club.py
  8. 10
      tournaments/models/draw_log.py
  9. 39
      tournaments/models/match.py
  10. 83
      tournaments/models/tournament.py
  11. 2
      tournaments/static/misc/jap-test.csv
  12. 2
      tournaments/templates/tournaments/broadcast/broadcasted_match.html
  13. 4
      tournaments/templates/tournaments/match_cell.html
  14. 2
      tournaments/templates/tournaments/summon_row.html
  15. 3
      tournaments/templates/tournaments/tournament_info.html
  16. 13
      tournaments/views.py

@ -12,6 +12,7 @@ from django.contrib.sites.shortcuts import get_current_site
from api.tokens import account_activation_token
from shared.cryptography import encryption_util
from tournaments.models.draw_log import DrawLog
class EncryptedUserField(serializers.Field):
def to_representation(self, value):
@ -230,3 +231,8 @@ class DeviceTokenSerializer(serializers.ModelSerializer):
model = DeviceToken
fields = '__all__'
read_only_fields = ['user']
class DrawLogSerializer(serializers.ModelSerializer):
class Meta:
model = DrawLog
fields = '__all__'

@ -18,6 +18,7 @@ router.register(r'player-registrations', views.PlayerRegistrationViewSet)
router.register(r'purchases', views.PurchaseViewSet)
router.register(r'courts', views.CourtViewSet)
router.register(r'date-intervals', views.DateIntervalViewSet)
router.register(r'draw-logs', views.DrawLogViewSet)
router.register(r'failed-api-calls', views.FailedApiCallViewSet)
router.register(r'logs', views.LogViewSet)
router.register(r'device-token', views.DeviceTokenViewSet)

@ -1,5 +1,6 @@
from pandas.io.feather_format import pd
from .serializers import ClubSerializer, CourtSerializer, DateIntervalSerializer, TournamentSerializer, UserSerializer, ChangePasswordSerializer, EventSerializer, RoundSerializer, GroupStageSerializer, MatchSerializer, TeamScoreSerializer, TeamRegistrationSerializer, PlayerRegistrationSerializer, LiveMatchSerializer, PurchaseSerializer, UserUpdateSerializer, FailedApiCallSerializer, LogSerializer, DeviceTokenSerializer
from tournaments.models.draw_log import DrawLog
from .serializers import ClubSerializer, CourtSerializer, DateIntervalSerializer, DrawLogSerializer, TournamentSerializer, UserSerializer, ChangePasswordSerializer, EventSerializer, RoundSerializer, GroupStageSerializer, MatchSerializer, TeamScoreSerializer, TeamRegistrationSerializer, PlayerRegistrationSerializer, LiveMatchSerializer, PurchaseSerializer, UserUpdateSerializer, FailedApiCallSerializer, LogSerializer, DeviceTokenSerializer
from tournaments.models import Club, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamScore, TeamRegistration, PlayerRegistration, Court, DateInterval, Purchase, FailedApiCall, Log, DeviceToken
from rest_framework import viewsets, permissions
@ -288,3 +289,15 @@ class DeviceTokenViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class DrawLogViewSet(viewsets.ModelViewSet):
queryset = DrawLog.objects.all()
serializer_class = DrawLogSerializer
def get_queryset(self):
tournament_id = self.request.query_params.get('tournament')
if tournament_id:
return self.queryset.filter(tournament=tournament_id)
if self.request.user:
return self.queryset.filter(tournament__event__creator=self.request.user)
return []

@ -2,6 +2,7 @@ from django.contrib import admin
from tournaments.models import team_registration
from tournaments.models.device_token import DeviceToken
from tournaments.models.draw_log import DrawLog
from .models import Club, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, Court, DateInterval, FailedApiCall, Log
from django.contrib.auth.admin import UserAdmin
@ -105,6 +106,11 @@ class LogAdmin(admin.ModelAdmin):
class DeviceTokenAdmin(admin.ModelAdmin):
list_display = ['user', 'value']
class DrawLogAdmin(admin.ModelAdmin):
list_display = ['tournament', 'draw_date', 'draw_seed', 'draw_match_index', 'draw_team_position']
list_filter = [SimpleTournamentListFilter]
ordering = ['draw_date']
admin.site.register(CustomUser, CustomUserAdmin)
admin.site.register(Club, ClubAdmin)
admin.site.register(Event, EventAdmin)
@ -121,3 +127,4 @@ admin.site.register(DateInterval, DateIntervalAdmin)
admin.site.register(FailedApiCall, FailedApiCallAdmin)
admin.site.register(Log, LogAdmin)
admin.site.register(DeviceToken, DeviceTokenAdmin)
admin.site.register(DrawLog, DrawLogAdmin)

@ -0,0 +1,26 @@
# Generated by Django 4.2.11 on 2024-10-24 06:55
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0090_tournament_initial_seed_count_and_more'),
]
operations = [
migrations.CreateModel(
name='DrawLog',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('draw_date', models.DateTimeField()),
('draw_seed', models.IntegerField()),
('draw_match_index', models.IntegerField()),
('draw_team_position', models.IntegerField()),
('tournament', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tournaments.tournament')),
],
),
]

File diff suppressed because one or more lines are too long

@ -1,4 +1,5 @@
from django.db import models
from zoneinfo import available_timezones
import uuid
class Club(models.Model):
@ -15,7 +16,11 @@ class Club(models.Model):
zip_code = models.CharField(max_length=10, null=True, blank=True)
latitude = models.FloatField(null=True, blank=True)
longitude = models.FloatField(null=True, blank=True)
timezone = models.CharField(
max_length=50, null=True, blank=True,
choices=[(tz, tz) for tz in sorted(available_timezones())],
default='CET'
)
court_count = models.IntegerField(default=2)
broadcast_code = models.CharField(max_length=10, null=True, blank=True, unique=True)

@ -0,0 +1,10 @@
from django.db import models
import uuid
class DrawLog(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True)
tournament = models.ForeignKey('Tournament', on_delete=models.CASCADE)
draw_date = models.DateTimeField()
draw_seed = models.IntegerField()
draw_match_index = models.IntegerField()
draw_team_position = models.IntegerField()

@ -2,7 +2,8 @@ from django.db import models
from tournaments.models import group_stage
from . import Round, GroupStage, FederalMatchCategory
from django.utils import timezone, formats
from datetime import timedelta
from datetime import datetime, timedelta
import uuid
from ..utils.extensions import format_seconds
@ -41,6 +42,8 @@ class Match(models.Model):
if self.tournament().event:
club = self.tournament().event.club
if self.confirmed is False:
return ""
if club:
return club.court_name(index)
elif index is not None:
@ -78,14 +81,16 @@ class Match(models.Model):
def player_names(self):
return map(lambda ts: ts.player_names(), self.team_scores.all())
def local_start_date(self):
timezone = self.tournament().timezone()
return self.start_date.astimezone(timezone)
def formatted_start_date(self):
if self.start_date:
timezoned_datetime = timezone.localtime(self.start_date)
return formats.date_format(timezoned_datetime, format='H:i')
# return formats.date_format(self.start_date, format='H:i')
local_start = self.local_start_date()
return formats.date_format(local_start, format='H:i')
else:
return ''
# return str(self.start_date) #.strftime("%H:%M")
def time_indication(self):
if self.end_date:
@ -95,10 +100,18 @@ class Match(models.Model):
return ''
elif self.start_date:
if self.started():
if self.confirmed:
return self.formatted_duration()
else:
timezoned_datetime = timezone.localtime(self.start_date)
return formats.date_format(timezoned_datetime, format='l H:i')
return 'À suivre'
else:
# timezoned_datetime = timezone.localtime(self.start_date)
timezone = self.tournament().timezone()
local_start = self.start_date.astimezone(timezone)
if self.confirmed:
return formats.date_format(local_start, format='l H:i')
else:
return f"Estimée : {formats.date_format(local_start, format='l H:i')}"
else:
return 'À venir...'
@ -185,14 +198,14 @@ class Match(models.Model):
def live_match(self):
title = self.name if self.name else self.backup_name()
date = self.formatted_start_date()
duration = self.time_indication()
time_indication = self.time_indication()
court = self.court_name(self.court_index)
group_stage_name = None
if self.group_stage:
group_stage_name = self.group_stage.display_name()
ended = self.end_date is not None
livematch = LiveMatch(title, date, duration, court, self.started(), ended, group_stage_name)
livematch = LiveMatch(title, date, time_indication, court, self.started(), ended, group_stage_name)
for team_score in self.sorted_team_scores():
if team_score.team_registration:
@ -254,11 +267,11 @@ class Team:
}
class LiveMatch:
def __init__(self, title, date, duration, court, started, ended, group_stage_name):
def __init__(self, title, date, time_indication, court, started, ended, group_stage_name):
self.title = title
self.date = date
self.teams = []
self.duration = duration
self.time_indication = time_indication
self.court = court
self.started = started
self.ended = ended
@ -275,7 +288,7 @@ class LiveMatch:
"title": self.title,
"date": self.date,
"teams": [team.to_dict() for team in self.teams],
"duration": self.duration,
"time_indication": self.time_indication,
"court": self.court,
"started": self.started,
"ended": self.ended,
@ -283,7 +296,7 @@ class LiveMatch:
"group_stage_name": self.group_stage_name,
}
def show_duration(self):
def show_time_indication(self):
for team in self.teams:
if team.walk_out and len(team.scores) == 0:
return False

@ -1,3 +1,4 @@
from zoneinfo import ZoneInfo
from django.db import models
from typing import TYPE_CHECKING
if TYPE_CHECKING:
@ -7,6 +8,8 @@ from . import Event, TournamentPayment, FederalMatchCategory, FederalCategory, F
import uuid
from django.utils import timezone, formats
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from shared.cryptography import encryption_util
from ..utils.extensions import plural_format
@ -129,6 +132,19 @@ class Tournament(models.Model):
components.append(self.name)
return (' ').join(components)
def timezone(self):
tz = 'CET'
if self.event and self.event.club:
tz = self.event.club.timezone
return ZoneInfo(tz)
def local_start_date(self):
timezone = self.timezone()
return self.start_date.astimezone(timezone)
def local_start_date_formatted(self):
return formats.date_format(self.local_start_date(), format='j F Y H:i')
def level(self):
if self.federal_level_category == 0:
return "Anim."
@ -216,6 +232,7 @@ class Tournament(models.Model):
def team_summons(self):
summons = []
print('>>> team_summons')
if self.supposedly_in_progress() and self.end_date is None:
for team in self.teams(False):
names = team.names
@ -231,7 +248,7 @@ class Tournament(models.Model):
names = team_registration.team_names()
stage = next_match.summon_stage_name()
weight = team_registration.weight
summon = TeamSummon(names, next_match.start_date, weight, stage, next_match.court_name(next_match.court_index), team_registration.logo)
summon = TeamSummon(names, next_match.local_start_date(), weight, stage, next_match.court_name(next_match.court_index), team_registration.logo)
summons.append(summon)
summons.sort(key=lambda s: (s.date is None, s.date or datetime.min))
@ -259,7 +276,7 @@ class Tournament(models.Model):
return rankings
def teams(self, includeWaitingList):
print("Starting teams method")
# print("Starting teams method")
bracket_teams = []
group_stage_teams = []
waiting_teams = []
@ -268,10 +285,10 @@ class Tournament(models.Model):
wildcard_group_stage = []
complete_teams = []
closed_registration_date = self.closed_registration_date
print(f"Closed registration date: {closed_registration_date}")
# print(f"Closed registration date: {closed_registration_date}")
for team_registration in self.teamregistration_set.all():
print(f"Processing team registration: {team_registration}")
# print(f"Processing team registration: {team_registration}")
is_valid = False
if closed_registration_date is not None and team_registration.registration_date is not None and team_registration.registration_date <= closed_registration_date:
is_valid = True
@ -279,7 +296,7 @@ class Tournament(models.Model):
is_valid = True
if team_registration.registration_date is None:
is_valid = True
print(f"Is valid: {is_valid}")
# print(f"Is valid: {is_valid}")
if team_registration.walk_out is False:
names = team_registration.team_names()
@ -287,14 +304,14 @@ class Tournament(models.Model):
initial_weight = team_registration.initial_weight()
date = team_registration.call_date
team = TeamList(names, weight, date, initial_weight, team_registration.wild_card_bracket, team_registration.wild_card_group_stage, team_registration.logo)
print(f"Created team: {team}")
# print(f"Created team: {team}")
if team_registration.group_stage_position is not None:
team.set_stage("Poule")
elif team_registration.bracket_position is not None:
team.set_stage("Tableau")
else:
team.set_stage("Attente")
print(f"Team stage: {team.stage}")
# print(f"Team stage: {team.stage}")
teams.append(team)
if team_registration.wild_card_bracket:
@ -306,11 +323,11 @@ class Tournament(models.Model):
else:
waiting_teams.append(team)
print(f"Total teams: {len(teams)}")
print(f"Wildcard bracket: {len(wildcard_bracket)}")
print(f"Wildcard group stage: {len(wildcard_group_stage)}")
print(f"Complete teams: {len(complete_teams)}")
print(f"Waiting teams: {len(waiting_teams)}")
# print(f"Total teams: {len(teams)}")
# print(f"Wildcard bracket: {len(wildcard_bracket)}")
# print(f"Wildcard group stage: {len(wildcard_group_stage)}")
# print(f"Complete teams: {len(complete_teams)}")
# print(f"Waiting teams: {len(waiting_teams)}")
if len(teams) < self.team_count:
teams.sort(key=lambda s: (s.initial_weight, s.date))
@ -323,8 +340,8 @@ class Tournament(models.Model):
group_stage_members_count = 0
if seeds_count < 0:
seeds_count = 0
print(f"Seeds count: {seeds_count}")
print(f"Group stage members count: {group_stage_members_count}")
# print(f"Seeds count: {seeds_count}")
# print(f"Group stage members count: {group_stage_members_count}")
if self.team_sorting == TeamSortingType.INSCRIPTION_DATE:
complete_teams.sort(key=lambda s: (s.date is None, s.date or datetime.min, s.initial_weight))
@ -333,25 +350,25 @@ class Tournament(models.Model):
selected_teams = complete_teams[:self.team_count]
selected_teams.sort(key=lambda s: s.initial_weight)
print(f"Selected teams: {len(selected_teams)}")
# print(f"Selected teams: {len(selected_teams)}")
if seeds_count > 0:
bracket_teams = selected_teams[:seeds_count] + wildcard_bracket
else:
bracket_teams = []
print(f"Bracket teams: {len(bracket_teams)}")
# print(f"Bracket teams: {len(bracket_teams)}")
if group_stage_members_count:
group_stage_end = seeds_count + group_stage_members_count
group_stage_teams = selected_teams[seeds_count:group_stage_end] + wildcard_group_stage
else:
group_stage_teams = []
print(f"Group stage teams: {len(group_stage_teams)}")
# print(f"Group stage teams: {len(group_stage_teams)}")
waiting_list_count = len(teams) - self.team_count
if waiting_list_count < 0:
waiting_list_count = 0
print(f"Waiting list count: {waiting_list_count}")
# print(f"Waiting list count: {waiting_list_count}")
if waiting_list_count > 0 or len(waiting_teams) > 0:
if waiting_list_count > 0:
@ -362,7 +379,7 @@ class Tournament(models.Model):
waiting_teams.sort(key=lambda s: (s.initial_weight, s.date))
else:
waiting_teams = []
print(f"Final waiting teams: {len(waiting_teams)}")
# print(f"Final waiting teams: {len(waiting_teams)}")
bracket_teams.sort(key=lambda s: s.weight)
group_stage_teams.sort(key=lambda s: s.weight)
@ -380,10 +397,10 @@ class Tournament(models.Model):
if includeWaitingList is True:
final_teams = bracket_teams + group_stage_teams + waiting_teams
print(f"Final teams with waiting list: {len(final_teams)}")
# print(f"Final teams with waiting list: {len(final_teams)}")
else:
final_teams = bracket_teams + group_stage_teams
print(f"Final teams without waiting list: {len(final_teams)}")
# print(f"Final teams without waiting list: {len(final_teams)}")
return final_teams
@ -791,8 +808,23 @@ class Tournament(models.Model):
return date.replace(hour=8, minute=0, second=0, microsecond=0, tzinfo=date.tzinfo)
def supposedly_in_progress(self):
end = self.start_date + timedelta(days=self.day_duration + 1)
return self.start_date.replace(hour=0, minute=0) <= timezone.now() <= end
# end = self.start_date + timedelta(days=self.day_duration + 1)
# return self.start_date.replace(hour=0, minute=0) <= timezone.now() <= end
timezoned_datetime = timezone.localtime(self.start_date)
end = timezoned_datetime + timedelta(days=self.day_duration + 1)
now = timezone.now()
start = timezoned_datetime.replace(hour=0, minute=0)
print(f"timezoned_datetime: {timezoned_datetime}")
print(f"tournament end date: {end}")
print(f"current time: {now}")
print(f"tournament start: {start}")
print(f"start <= now <= end: {start <= now <= end}")
return start <= now <= end
def display_points_earned(self):
return self.federal_level_category != FederalLevelCategory.UNLISTED and self.hide_points_earned is False
@ -840,10 +872,9 @@ class TeamSummon:
def formatted_date(self):
if self.date:
timezoned_datetime = timezone.localtime(self.date)
return formats.date_format(timezoned_datetime, format='l H:i')
return formats.date_format(self.date, format='l H:i')
else:
return None
return ''
def to_dict(self):
return {

@ -1,3 +1 @@
CAN PADEL,POPOVITCH,Laurent,laurent@padelclub.app,0629445485
CAN PADEL,POPOVITCH,Razmig,razmig@padelclub.app,0629445485
CAN PADEL,POPOVITCH,Xavier,xavier@padelclub.app,0629445485

1 CAN PADEL POPOVITCH Laurent laurent@padelclub.app 0629445485
CAN PADEL POPOVITCH Razmig razmig@padelclub.app 0629445485
CAN PADEL POPOVITCH Xavier xavier@padelclub.app 0629445485

@ -51,7 +51,7 @@
</template>
<div class="top-margin flex-row">
<label class="left-label minor-info bold"><span x-text="match.duration"></span></label>
<label class="left-label minor-info bold"><span x-text="match.time_indication"></span></label>
<label class="right-label minor-info semibold"><span x-text="match.court"></span></label>
</div>

@ -42,8 +42,8 @@
<div class="flex-row top-margin">
<label class="left-label minor-info bold">
{% if match.show_duration %}
{{ match.duration }}
{% if match.show_time_indication %}
{{ match.time_indication }}
{% endif %}
</label>
<label class="right-label minor-info">

@ -9,7 +9,7 @@
</div>
<div class="table-cell left">
<div class="table-cell large">{{ summon.date|date:'l H:i' }}</div>
<div class="table-cell large">{{ summon.formatted_date }}</div>
<div class="table-cell">{{ summon.court }}</div>
</div>
<div class="table-cell right"><div class="mybox center">{{ summon.stage }}</div></div>

@ -7,6 +7,7 @@
{% block content %}
{% load static %}
{% load tz %}
{% include 'tournaments/navigation_tournament.html' %}
@ -17,7 +18,7 @@
<div class="bubble">
<p>
<div class="semibold">{{ tournament.start_date }}</div>
<div class="semibold">{{ tournament.local_start_date_formatted }}</div>
<div>{{ tournament.day_duration_formatted }}</div>
<div>{{ tournament.court_count }} terrains</div>
</p>

@ -79,8 +79,8 @@ def live_tournaments(club_id):
return [t for t in tournaments if t.display_tournament() and t.supposedly_in_progress()]
def future_tournaments(club_id):
tomorrow = date.today() + timedelta(days=1)
tournaments = tournaments_query(Q(end_date__isnull=True, start_date__gt=tomorrow), club_id, True)
tomorrow = datetime.now().date() + timedelta(days=1)
tournaments = tournaments_query(Q(end_date__isnull=True, start_date__gte=tomorrow), club_id, True)
return [t for t in tournaments if t.display_tournament()]
def tournament_info(request, tournament_id):
@ -147,13 +147,8 @@ def tournament(request, tournament_id):
rounds = list(tournament.round_set.filter(group_stage_loser_bracket=True))
rounds.extend(bracket_rounds)
#group_stages = tournament.groupstage_set.order_by('index')
group_stages = sorted(tournament.get_computed_group_stage(), key=lambda s: (s.step, s.index))
#print(len(match_groups))
#print(len(rounds))
#print(len(group_stages))
if tournament.display_matches() or tournament.display_group_stages():
return render(request, 'tournaments/matches.html', {
'tournament': tournament,
@ -430,7 +425,7 @@ def simple_form_view(request):
return HttpResponse(f"Hello, {name}!") # Simple response with name
else:
form = SimpleForm()
print(request.method)
# print(request.method)
# If this is a GET request, we display an empty form
return render(request, 'tournaments/admin/mail_test.html', {'form': form})
@ -484,7 +479,7 @@ def send_email(mail, name):
name = ""
subject = "Tes tournois en toute simplicité avec Padel Club"
body = f"Salut {name} !\n\nJe me permets de t'écrire car je suis JAP2 en région PACA et développeur, et je viens de lancer Padel Club, une app iOS qui facilite enfin l'organisation des tournois. Avec elle, tu peux convoquer rapidement, simuler et programmer tes structures, diffuser tous les résultats à tous les joueurs, et ce depuis un iPhone.\n\nTu peux l'essayer gratuitement pour découvrir tout son potentiel ! Télécharge l'app ici et teste la dès ton prochain tournoi: https://padelclub.app/download/\n\nJe suis disponible pour échanger avec toi par mail ou téléphone au 06 81 59 81 93 et voir ce que tu en penses.\nÀ bientôt j'espère !\n\nRazmig"
body = f"Salut {name} !\n\nJe me permets de t'écrire car je suis JAP2 en région PACA et développeur, et je viens de lancer Padel Club, une app iOS qui facilite enfin l'organisation des tournois.\n\nAvec elle, tu peux convoquer rapidement, simuler et programmer tes structures, diffuser tous les résultats à tous les joueurs, et ce depuis un iPhone.\n\nTu peux l'essayer gratuitement pour découvrir tout son potentiel ! Télécharge l'app ici et teste la dès ton prochain tournoi: https://padelclub.app/download/\n\nJe suis disponible pour échanger avec toi par mail ou téléphone au 06 81 59 81 93 et voir ce que tu en penses.\nÀ bientôt j'espère !\n\nRazmig"
email = EmailMessage(subject, body, to=[mail])
email.send()

Loading…
Cancel
Save