From 154cf82123c6b7cd6d058db51e73fe6e9339aeaa Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 20 Feb 2025 11:12:28 +0100 Subject: [PATCH 01/10] fix walkout check when registering online --- tournaments/services/tournament_registration.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tournaments/services/tournament_registration.py b/tournaments/services/tournament_registration.py index f7fbb3a..90492ba 100644 --- a/tournaments/services/tournament_registration.py +++ b/tournaments/services/tournament_registration.py @@ -307,5 +307,6 @@ class TournamentRegistrationService: def _license_already_registered(self, stripped_license): return PlayerRegistration.objects.filter( team_registration__tournament=self.tournament, - licence_id__startswith=stripped_license + licence_id__startswith=stripped_license, + team_registration__walk_out=False ).exists() From f8e25ff169a4b3730c8e1aa7814a20af300fe0b8 Mon Sep 17 00:00:00 2001 From: Laurent Date: Fri, 21 Feb 2025 16:11:24 +0100 Subject: [PATCH 02/10] adds version check when creating a tournament --- api/utils.py | 11 +++++++++++ api/views.py | 15 ++++++++++++++- shared/discord.py | 16 ++++++++++++++++ tournaments/signals.py | 40 ++++++++++++++++++++-------------------- 4 files changed, 61 insertions(+), 21 deletions(-) create mode 100644 shared/discord.py diff --git a/api/utils.py b/api/utils.py index bbe5dc2..5511be0 100644 --- a/api/utils.py +++ b/api/utils.py @@ -3,3 +3,14 @@ import re def is_valid_email(email): email_regex = r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$' return re.match(email_regex, email) is not None + +def check_version_smaller_than_1_1_12(version_str): + # Remove the parentheses part if it exists, example of version: 1.1.12 (2) + version_str = version_str.split()[0] + + # Split version into components + version_parts = [int(x) for x in version_str.split('.')] + target_parts = [1, 1, 12] + + # Compare version components + return version_parts < target_parts diff --git a/api/views.py b/api/views.py index 310c867..0f12567 100644 --- a/api/views.py +++ b/api/views.py @@ -19,11 +19,13 @@ from django.db.models import Q from django.core.exceptions import ObjectDoesNotExist from .permissions import IsClubOwner -from .utils import is_valid_email +from .utils import is_valid_email, check_version_smaller_than_1_1_12 from django.views.decorators.csrf import csrf_exempt from django.utils.decorators import method_decorator +from shared.discord import send_discord_log_message + @method_decorator(csrf_exempt, name='dispatch') class CustomAuthToken(APIView): permission_classes = [] @@ -112,6 +114,17 @@ class TournamentViewSet(SoftDeleteViewSet): return [] return self.queryset.filter(event__creator=self.request.user) + def perform_create(self, serializer): + serializer.save() + # version check + app_version = self.request.headers.get('App-Version') + self.warn_if_version_is_too_small(app_version) + + def warn_if_version_is_too_small(self, version): + if check_version_smaller_than_1_1_12(version): + message = f'{self.request.user.username} app version is {version}' + send_discord_log_message(message) + class PurchaseViewSet(SoftDeleteViewSet): queryset = Purchase.objects.all() serializer_class = PurchaseSerializer diff --git a/shared/discord.py b/shared/discord.py new file mode 100644 index 0000000..8ecbcc6 --- /dev/null +++ b/shared/discord.py @@ -0,0 +1,16 @@ +import requests + +DISCORD_FAILED_CALLS_WEBHOOK_URL = 'https://discord.com/api/webhooks/1248191778134163486/sSoTL6cULCElWr2YFwyllsg7IXxHcCx_YMDJA_cUHtVUU4WOfN-5M7drCJuwNBBfAk9a' +DISCORD_LOGS_WEBHOOK_URL = 'https://discord.com/api/webhooks/1257987637449588736/TtOUwzYgSlQH2d3Ps7SfIKRcFALQVa3hfkC-j9K4_UAcWtsfiw4v8NUPbnX2_ZPOYzuv' + +def send_discord_failed_calls_message(message): + send_discord_message(DISCORD_FAILED_CALLS_WEBHOOK_URL, message) + +def send_discord_log_message(message): + send_discord_message(DISCORD_LOGS_WEBHOOK_URL, message) + +def send_discord_message(webhook_url, content): + data = { + "content": content + } + requests.post(webhook_url, json=data) diff --git a/tournaments/signals.py b/tournaments/signals.py index 848dbca..494a623 100644 --- a/tournaments/signals.py +++ b/tournaments/signals.py @@ -12,6 +12,8 @@ import requests from tournaments.services.email_service import TournamentEmailService from tournaments.models import PlayerDataSource +from shared.discord import send_discord_log_message, send_discord_failed_calls_message + def generate_unique_code(): characters = string.ascii_lowercase + string.digits while True: @@ -25,39 +27,37 @@ def assign_unique_code(sender, instance, created, **kwargs): instance.broadcast_code = generate_unique_code() instance.save() -DISCORD_FAILED_CALLS_WEBHOOK_URL = 'https://discord.com/api/webhooks/1248191778134163486/sSoTL6cULCElWr2YFwyllsg7IXxHcCx_YMDJA_cUHtVUU4WOfN-5M7drCJuwNBBfAk9a' -DISCORD_LOGS_WEBHOOK_URL = 'https://discord.com/api/webhooks/1257987637449588736/TtOUwzYgSlQH2d3Ps7SfIKRcFALQVa3hfkC-j9K4_UAcWtsfiw4v8NUPbnX2_ZPOYzuv' @receiver(post_save, sender=FailedApiCall) def notify_discord_on_create(sender, instance, created, **kwargs): - notify_object_creation_on_discord(created, instance, DISCORD_FAILED_CALLS_WEBHOOK_URL) - -# @receiver(post_save, sender=CustomUser) -# def notify_user_creation_on_discord(sender, instance, created, **kwargs): -# notify_object_creation_on_discord(created, instance, DISCORD_LOGS_WEBHOOK_URL) + notify_object_creation_on_discord(created, instance) @receiver(post_save, sender=Log) def notify_log_creation_on_discord(sender, instance, created, **kwargs): - notify_object_creation_on_discord(created, instance, DISCORD_LOGS_WEBHOOK_URL) + notify_object_creation_on_discord(created, instance) # WARNING: using this method requires the instance to have a discord_string method -def notify_object_creation_on_discord(created, instance, webhook_url): +def notify_object_creation_on_discord(created, instance): if created: default_db_engine = settings.DATABASES['default']['ENGINE'] if default_db_engine != 'django.db.backends.sqlite3': site_name = settings.SITE_NAME message = f'{site_name} > New {instance.__class__.__name__} created: {instance.discord_string()}' - send_discord_message(webhook_url, message) - -def send_discord_message(webhook_url, content): - data = { - "content": content - } - requests.post(webhook_url, json=data) - # if response.status_code != 204: - # raise ValueError( - # f'Error sending message to Discord webhook: {response.status_code}, {response.text}' - # ) + if isinstance(instance, FailedApiCall): + send_discord_failed_calls_message(message) + else: + send_discord_log_message(message) + + +# def send_discord_message(webhook_url, content): +# data = { +# "content": content +# } +# requests.post(webhook_url, json=data) +# # if response.status_code != 204: +# # raise ValueError( +# # f'Error sending message to Discord webhook: {response.status_code}, {response.text}' +# # ) @receiver(pre_delete, sender=TeamRegistration) def unregister_team(sender, instance, **kwargs): From 2d877563ff2ba8219d5a005af5a9fb82909b68e5 Mon Sep 17 00:00:00 2001 From: Laurent Date: Fri, 21 Feb 2025 16:18:04 +0100 Subject: [PATCH 03/10] adds try catch to avoid sending exceptions --- shared/discord.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/shared/discord.py b/shared/discord.py index 8ecbcc6..9b7e262 100644 --- a/shared/discord.py +++ b/shared/discord.py @@ -10,7 +10,10 @@ def send_discord_log_message(message): send_discord_message(DISCORD_LOGS_WEBHOOK_URL, message) def send_discord_message(webhook_url, content): - data = { - "content": content - } - requests.post(webhook_url, json=data) + try: + data = { + "content": content + } + requests.post(webhook_url, json=data) + except Exception as e: + print(f"Failed to send Discord message: {str(e)}") From 39c946929b0573a2ca2d307498bdaf6aaccdecf1 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 21 Feb 2025 16:54:07 +0100 Subject: [PATCH 04/10] fix team list sorting --- tournaments/models/tournament.py | 70 ++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/tournaments/models/tournament.py b/tournaments/models/tournament.py index 10f59f3..37ed021 100644 --- a/tournaments/models/tournament.py +++ b/tournaments/models/tournament.py @@ -324,6 +324,10 @@ class Tournament(models.Model): print("else", index, self.team_count) return -1 + def group_stage_spots(self): + """Returns the total number of spots in all group stages.""" + return sum(gs.size for gs in self.groupstage_set.all()) + def teams(self, include_waiting_list): """ Get sorted list of teams for the tournament. @@ -358,6 +362,14 @@ class Tournament(models.Model): (team_reg.registration_date and team_reg.registration_date <= closed_date) ) + # Set initial stage + if team_reg.group_stage_position is not None: + team.set_stage("Poule") + elif team_reg.bracket_position is not None: + team.set_stage("Tableau") + else: + team.set_stage("Attente") + # Categorize team if team_reg.wild_card_bracket: wildcard_bracket.append(team) @@ -368,13 +380,15 @@ class Tournament(models.Model): else: waiting_teams.append(team) - # Set initial stage - if team_reg.group_stage_position is not None: - team.set_stage("Poule") - elif team_reg.bracket_position is not None: - team.set_stage("Tableau") - else: - team.set_stage("Attente") + + # Initialize group stage spots + group_stage_spots = self.group_stage_spots() + bracket_seeds = self.team_count - group_stage_spots - len(wildcard_bracket) + group_stage_team_count = group_stage_spots - len(wildcard_group_stage) + if group_stage_team_count < 0: + group_stage_team_count = 0 + if bracket_seeds < 0: + bracket_seeds = 0 # Sort teams based on tournament rules if self.team_sorting == TeamSortingType.INSCRIPTION_DATE: @@ -394,25 +408,45 @@ class Tournament(models.Model): complete_teams.sort(key=lambda t: (t.initial_weight, t.team_registration.id)) waiting_teams.sort(key=lambda t: (t.initial_weight, t.team_registration.id)) - # Determine final team list based on tournament settings - if len(complete_teams) <= self.team_count: - all_teams = wildcard_bracket + wildcard_group_stage + complete_teams - if include_waiting_list: - all_teams.extend(waiting_teams) - return all_teams + wildcard_group_stage.sort(key=lambda t: (t.initial_weight, t.team_registration.id)) + wildcard_bracket.sort(key=lambda t: (t.initial_weight, t.team_registration.id)) # Split teams into main bracket and waiting list - qualified_teams = complete_teams[:self.team_count] - excess_teams = complete_teams[self.team_count:] + computed_team_count = self.team_count - len(wildcard_bracket) - len(wildcard_group_stage) + if computed_team_count < 0: + computed_team_count = 0 + qualified_teams = complete_teams[:computed_team_count] + excess_teams = complete_teams[computed_team_count:] + + qualified_teams.sort(key=lambda t: (t.initial_weight, t.team_registration.id)) + excess_teams.sort(key=lambda t: (t.initial_weight, t.team_registration.id)) # Combine all waiting list teams waiting_list = excess_teams + waiting_teams + if self.team_sorting == TeamSortingType.INSCRIPTION_DATE: + waiting_list.sort(key=lambda t: ( + t.registration_date is None, + t.registration_date or datetime.min, + t.initial_weight, + t.team_registration.id + )) + else: + waiting_list.sort(key=lambda t: (t.initial_weight, t.team_registration.id)) # Return final sorted list - if include_waiting_list: - return wildcard_bracket + wildcard_group_stage + qualified_teams + waiting_list - return wildcard_bracket + wildcard_group_stage + qualified_teams + bracket_teams = qualified_teams[:bracket_seeds] + wildcard_bracket + gs_teams = qualified_teams[bracket_seeds:(bracket_seeds+group_stage_team_count)] + wildcard_group_stage + bracket_teams.sort(key=lambda t: (t.initial_weight, t.team_registration.id)) + gs_teams.sort(key=lambda t: (t.initial_weight, t.team_registration.id)) + all_teams = bracket_teams + gs_teams + for team in bracket_teams: + team.set_stage("Tableau") + for team in gs_teams: + team.set_stage("Poule") + if include_waiting_list: + all_teams.extend(waiting_list) + return all_teams def match_groups(self, broadcasted, group_stage_id, round_id): From 5990583af369aad271ba3253832b0eee621b83fb Mon Sep 17 00:00:00 2001 From: Laurent Date: Sat, 22 Feb 2025 11:38:00 +0100 Subject: [PATCH 05/10] adds new import view --- tournaments/urls.py | 1 + tournaments/views.py | 60 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/tournaments/urls.py b/tournaments/urls.py index 3b8e8e3..1be64d4 100644 --- a/tournaments/urls.py +++ b/tournaments/urls.py @@ -66,6 +66,7 @@ urlpatterns = [ path('reset/done/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'), path('profile/', views.ProfileUpdateView.as_view(), name='profile'), path('admin/tournament-import/', views.tournament_import_view, name='tournament_import'), + path('admin/tournament-import-tr/', views.tournament_import_team_reg, name='tournament_import'), path('admin/users-export/', views.UserListExportView.as_view(), name='users_export'), ] diff --git a/tournaments/views.py b/tournaments/views.py index ae0cbb0..45a2842 100644 --- a/tournaments/views.py +++ b/tournaments/views.py @@ -838,6 +838,66 @@ def tournament_import_view(request): else: return render(request, 'tournaments/admin/tournament_cleaner.html') +@staff_member_required +def tournament_import_team_reg(request): + if request.method == 'POST': + zip_file = request.FILES.get('tournament_zip') + if zip_file: + try: + tournament_id = os.path.splitext(zip_file.name)[0] + tournament = Tournament.objects.get(id=tournament_id) + + # Delete existing relationships + # tournament.round_set.all().delete() + # tournament.groupstage_set.all().delete() + # tournament.teamregistration_set.all().delete() + + with zipfile.ZipFile(zip_file) as z: + # First, process rounds + # rounds_data = get_file_data(z, f"{tournament_id}/rounds.json") + # rounds_data = get_file_data(z, f"{tournament_id}/rounds.json") + # if rounds_data: + # # First pass: Create rounds with preserved UUIDs + # for item in rounds_data: + # item['tournament'] = tournament.id + # round_id = item['id'] # Preserve the original UUID + # Round.objects.create( + # id=round_id, + # tournament_id=tournament.id, + # index=item['index'], + # format=item.get('format'), + # start_date=item.get('start_date'), + # group_stage_loser_bracket=item.get('group_stage_loser_bracket', False), + # loser_bracket_mode=item.get('loser_bracket_mode', 0) + # ) + + # Second pass: Set parent relationships + # for item in rounds_data: + # if item.get('parent'): + # round_obj = Round.objects.get(id=item['id']) + # round_obj.parent_id = item['parent'] + # round_obj.save() + + # Then process all other files + serializer_mapping = { + 'group-stages.json': GroupStageSerializer, + # 'team-registrations.json': TeamRegistrationSerializer, + # 'matches.json': MatchSerializer, + # 'player-registrations.json': PlayerRegistrationSerializer, + # 'team-scores.json': TeamScoreSerializer + } + + # Process each remaining file + for filename, serializer_class in serializer_mapping.items(): + process_file(z, filename, tournament_id, tournament, serializer_class) + + return JsonResponse({'status': 'success'}) + + except Exception as e: + return JsonResponse({'status': 'error', 'message': str(e)}) + else: + return render(request, 'tournaments/admin/tournament_cleaner.html') + def process_file(zip_file, filename, tournament_id, tournament, serializer_class): """Helper function to process individual files""" try: From ee2028d8043b8bcba3d7ee0b46d6458de1dd9c0e Mon Sep 17 00:00:00 2001 From: Laurent Date: Sat, 22 Feb 2025 11:40:52 +0100 Subject: [PATCH 06/10] fix view --- tournaments/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tournaments/views.py b/tournaments/views.py index 45a2842..0499ea6 100644 --- a/tournaments/views.py +++ b/tournaments/views.py @@ -880,8 +880,8 @@ def tournament_import_team_reg(request): # Then process all other files serializer_mapping = { - 'group-stages.json': GroupStageSerializer, - # 'team-registrations.json': TeamRegistrationSerializer, + # 'group-stages.json': GroupStageSerializer, + 'team-registrations.json': TeamRegistrationSerializer, # 'matches.json': MatchSerializer, # 'player-registrations.json': PlayerRegistrationSerializer, # 'team-scores.json': TeamScoreSerializer From 7d5ed059b5f763017c26e09397479728f4d7ad72 Mon Sep 17 00:00:00 2001 From: Laurent Date: Sat, 22 Feb 2025 11:41:59 +0100 Subject: [PATCH 07/10] fix again --- tournaments/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tournaments/views.py b/tournaments/views.py index 0499ea6..79765aa 100644 --- a/tournaments/views.py +++ b/tournaments/views.py @@ -849,8 +849,8 @@ def tournament_import_team_reg(request): # Delete existing relationships # tournament.round_set.all().delete() - # tournament.groupstage_set.all().delete() - # tournament.teamregistration_set.all().delete() + tournament.groupstage_set.all().delete() + tournament.teamregistration_set.all().delete() with zipfile.ZipFile(zip_file) as z: # First, process rounds @@ -880,7 +880,7 @@ def tournament_import_team_reg(request): # Then process all other files serializer_mapping = { - # 'group-stages.json': GroupStageSerializer, + 'group-stages.json': GroupStageSerializer, 'team-registrations.json': TeamRegistrationSerializer, # 'matches.json': MatchSerializer, # 'player-registrations.json': PlayerRegistrationSerializer, From 75315c7e3a618278d8c5c299c652aaf62041a47e Mon Sep 17 00:00:00 2001 From: Laurent Date: Sat, 22 Feb 2025 11:43:20 +0100 Subject: [PATCH 08/10] fix #3 --- tournaments/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tournaments/views.py b/tournaments/views.py index 79765aa..56976f7 100644 --- a/tournaments/views.py +++ b/tournaments/views.py @@ -883,7 +883,7 @@ def tournament_import_team_reg(request): 'group-stages.json': GroupStageSerializer, 'team-registrations.json': TeamRegistrationSerializer, # 'matches.json': MatchSerializer, - # 'player-registrations.json': PlayerRegistrationSerializer, + 'player-registrations.json': PlayerRegistrationSerializer, # 'team-scores.json': TeamScoreSerializer } From e6caf1bb568f9fba2de3e01f05ada64732e9e16a Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 24 Feb 2025 08:38:09 +0100 Subject: [PATCH 09/10] separate teams list in two block in teams.html --- tournaments/templates/tournaments/teams.html | 34 ++++++++++---------- tournaments/views.py | 5 ++- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/tournaments/templates/tournaments/teams.html b/tournaments/templates/tournaments/teams.html index 0466c98..f978b62 100644 --- a/tournaments/templates/tournaments/teams.html +++ b/tournaments/templates/tournaments/teams.html @@ -10,31 +10,31 @@ {% load static %} - {% include 'tournaments/navigation_tournament.html' %} +{% include 'tournaments/navigation_tournament.html' %} +
+
+ {% if selected_teams|length > 0 %} + - {% if teams %} - -
- -
- {% if tournament.registration_count_display %} - - {% endif %} + {% for team in selected_teams %} + {% include 'tournaments/team_row.html' %} + {% endfor %} - {% for team in teams %} +
+ {% endif %} - {% include 'tournaments/team_row.html' %} + {% if waiting_teams|length > 0 %} + +
+ {% for team in waiting_teams %} + {% include 'tournaments/team_row.html' %} {% endfor %} -
-
- + {% endif %}
- - {% endif %} - +
{% endblock %} {% endif %} diff --git a/tournaments/views.py b/tournaments/views.py index ae0cbb0..c041b47 100644 --- a/tournaments/views.py +++ b/tournaments/views.py @@ -271,10 +271,13 @@ def tournament_teams(request, tournament_id): tournament = get_object_or_404(Tournament, pk=tournament_id) teams = tournament.teams(True) + selected_teams = [team for team in teams if team.stage != 'Attente'] + waiting_teams = [team for team in teams if team.stage == 'Attente'] return render(request, 'tournaments/teams.html', { 'tournament': tournament, - 'teams': teams, + 'selected_teams': selected_teams, + 'waiting_teams': waiting_teams, }) def tournament_summons(request, tournament_id): From 4ddeadcd38047761bc9f9e10e2487af5c2171e24 Mon Sep 17 00:00:00 2001 From: Laurent Date: Tue, 25 Feb 2025 19:57:07 +0100 Subject: [PATCH 10/10] Fix crash --- api/utils.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/api/utils.py b/api/utils.py index 5511be0..71f145f 100644 --- a/api/utils.py +++ b/api/utils.py @@ -7,10 +7,13 @@ def is_valid_email(email): def check_version_smaller_than_1_1_12(version_str): # Remove the parentheses part if it exists, example of version: 1.1.12 (2) version_str = version_str.split()[0] + if version_str: - # Split version into components - version_parts = [int(x) for x in version_str.split('.')] - target_parts = [1, 1, 12] + # Split version into components + version_parts = [int(x) for x in version_str.split('.')] + target_parts = [1, 1, 12] - # Compare version components - return version_parts < target_parts + # Compare version components + return version_parts < target_parts + else: + return False