From 03c6086e4b72026a23cf536806a7b5d3dba5ceb2 Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 12 Sep 2024 14:48:11 +0200 Subject: [PATCH 01/10] broadcast codes only generate lowercases --- tournaments/signals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tournaments/signals.py b/tournaments/signals.py index ded9d54..a5ae266 100644 --- a/tournaments/signals.py +++ b/tournaments/signals.py @@ -8,7 +8,7 @@ from .models import Club, FailedApiCall, CustomUser, Log import requests def generate_unique_code(): - characters = string.ascii_letters + string.digits + characters = string.ascii_lowercase + string.digits while True: code = ''.join(random.sample(characters, 3)) if not Club.objects.filter(broadcast_code=code).exists(): From 6617a2cc9b259e6c8ea09ed94dc9dbf8c5936e44 Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 16 Sep 2024 15:00:52 +0200 Subject: [PATCH 02/10] when posting an existing Purchase, returns a 208 instead of a 400 for failing uniqueness --- api/serializers.py | 1 - api/views.py | 32 +++++++++++++++++++ ...0082_alter_purchase_identifier_and_more.py | 23 +++++++++++++ tournaments/models/purchase.py | 2 +- 4 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 tournaments/migrations/0082_alter_purchase_identifier_and_more.py diff --git a/api/serializers.py b/api/serializers.py index ec03749..61ac1a0 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -9,7 +9,6 @@ from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode from django.utils.encoding import force_bytes from django.core.mail import EmailMessage from django.contrib.sites.shortcuts import get_current_site - from api.tokens import account_activation_token class UserSerializer(serializers.ModelSerializer): diff --git a/api/views.py b/api/views.py index b8d93d2..18c21ce 100644 --- a/api/views.py +++ b/api/views.py @@ -107,6 +107,38 @@ class PurchaseViewSet(viewsets.ModelViewSet): return self.queryset.filter(user=self.request.user) return [] + def create(self, request, *args, **kwargs): + identifier = request.data.get('identifier') + if Purchase.objects.filter(identifier=identifier).exists(): + return Response({"detail": "This transaction identifier is already registered."}, status=status.HTTP_208_ALREADY_REPORTED) + + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + + # def create(self, request, *args, **kwargs): + # serializer = PurchaseSerializer(data=request.data) + # # Check if the serializer is valid + # # + # print('create') + # if serializer.is_valid(): + # print('isvalid') + + # identifier = serializer.validated_data['identifier'] + # # If the unique field already exists, return 208 + # if Purchase.objects.filter(identifier=identifier).exists(): + # print('return 208') + + # return Response({'detail': 'Already exists.'}, status=status.HTTP_208_ALREADY_REPORTED) + # # Otherwise, perform the creation + # self.perform_create(serializer) + # return Response(serializer.data, status=status.HTTP_201_CREATED) + # print('return 400') + + # return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + def put(self, request, pk): raise MethodNotAllowed('PUT') diff --git a/tournaments/migrations/0082_alter_purchase_identifier_and_more.py b/tournaments/migrations/0082_alter_purchase_identifier_and_more.py new file mode 100644 index 0000000..0310ea3 --- /dev/null +++ b/tournaments/migrations/0082_alter_purchase_identifier_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1 on 2024-09-16 08:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tournaments', '0081_round_group_stage_loser_bracket'), + ] + + operations = [ + migrations.AlterField( + model_name='purchase', + name='identifier', + field=models.BigIntegerField(unique=True), + ), + migrations.AlterField( + model_name='tournament', + name='federal_age_category', + field=models.IntegerField(choices=[(0, ''), (120, 'U12'), (140, 'U14'), (160, 'U16'), (180, 'U18'), (200, 'Senior'), (450, '+45 ans'), (550, '+55 ans')], default=200), + ), + ] diff --git a/tournaments/models/purchase.py b/tournaments/models/purchase.py index 6dc72e4..da2b335 100644 --- a/tournaments/models/purchase.py +++ b/tournaments/models/purchase.py @@ -5,7 +5,7 @@ from . import CustomUser class Purchase(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) user = models.ForeignKey(CustomUser, on_delete=models.CASCADE) - identifier = models.BigIntegerField() + identifier = models.BigIntegerField(unique=True) purchase_date = models.DateTimeField() product_id = models.CharField(max_length=100) quantity = models.IntegerField(null=True, blank=True) From bc983e9561e80c95d86dac2ffd0dcecb9e76d245 Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 16 Sep 2024 16:11:55 +0200 Subject: [PATCH 03/10] cleanup + add expiration date to purchases --- api/views.py | 24 ------------------- tournaments/admin.py | 3 ++- .../0083_purchase_expiration_date.py | 18 ++++++++++++++ tournaments/models/purchase.py | 1 + 4 files changed, 21 insertions(+), 25 deletions(-) create mode 100644 tournaments/migrations/0083_purchase_expiration_date.py diff --git a/api/views.py b/api/views.py index 18c21ce..6e19f66 100644 --- a/api/views.py +++ b/api/views.py @@ -118,30 +118,6 @@ class PurchaseViewSet(viewsets.ModelViewSet): headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) - # def create(self, request, *args, **kwargs): - # serializer = PurchaseSerializer(data=request.data) - # # Check if the serializer is valid - # # - # print('create') - # if serializer.is_valid(): - # print('isvalid') - - # identifier = serializer.validated_data['identifier'] - # # If the unique field already exists, return 208 - # if Purchase.objects.filter(identifier=identifier).exists(): - # print('return 208') - - # return Response({'detail': 'Already exists.'}, status=status.HTTP_208_ALREADY_REPORTED) - # # Otherwise, perform the creation - # self.perform_create(serializer) - # return Response(serializer.data, status=status.HTTP_201_CREATED) - # print('return 400') - - # return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def put(self, request, pk): - raise MethodNotAllowed('PUT') - def patch(self, request, pk): raise MethodNotAllowed('PATCH') diff --git a/tournaments/admin.py b/tournaments/admin.py index 3d14e54..bc7ee41 100644 --- a/tournaments/admin.py +++ b/tournaments/admin.py @@ -74,7 +74,8 @@ class ClubAdmin(admin.ModelAdmin): search_fields = ('name', 'acronym') class PurchaseAdmin(admin.ModelAdmin): - list_display = ['user', 'identifier', 'product_id', 'quantity', 'purchase_date', 'revocation_date'] + list_display = ['user', 'identifier', 'product_id', 'quantity', 'purchase_date', 'revocation_date', 'expiration_date'] + list_filter = ['user'] class CourtAdmin(admin.ModelAdmin): list_display = ['index', 'name', 'club'] diff --git a/tournaments/migrations/0083_purchase_expiration_date.py b/tournaments/migrations/0083_purchase_expiration_date.py new file mode 100644 index 0000000..d97750f --- /dev/null +++ b/tournaments/migrations/0083_purchase_expiration_date.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1 on 2024-09-16 13:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tournaments', '0082_alter_purchase_identifier_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='purchase', + name='expiration_date', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/tournaments/models/purchase.py b/tournaments/models/purchase.py index da2b335..8acca70 100644 --- a/tournaments/models/purchase.py +++ b/tournaments/models/purchase.py @@ -10,6 +10,7 @@ class Purchase(models.Model): product_id = models.CharField(max_length=100) quantity = models.IntegerField(null=True, blank=True) revocation_date = models.DateTimeField(null=True, blank=True) + expiration_date = models.DateTimeField(null=True, blank=True) def __str__(self): return f"{self.identifier} > {self.product_id} - {self.purchase_date} - {self.user.username}" From bd9111908621507c66db1d88ed32f1dff3ea4e22 Mon Sep 17 00:00:00 2001 From: Raz Date: Wed, 18 Sep 2024 11:55:00 +0200 Subject: [PATCH 04/10] add loser bracket mode management --- ..._customuser_loser_bracket_mode_and_more.py | 28 +++++++++++++++++++ tournaments/models/custom_user.py | 3 +- tournaments/models/round.py | 1 + tournaments/models/tournament.py | 1 + 4 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 tournaments/migrations/0084_customuser_loser_bracket_mode_and_more.py diff --git a/tournaments/migrations/0084_customuser_loser_bracket_mode_and_more.py b/tournaments/migrations/0084_customuser_loser_bracket_mode_and_more.py new file mode 100644 index 0000000..8c6f69b --- /dev/null +++ b/tournaments/migrations/0084_customuser_loser_bracket_mode_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.11 on 2024-09-18 08:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tournaments', '0083_purchase_expiration_date'), + ] + + operations = [ + migrations.AddField( + model_name='customuser', + name='loser_bracket_mode', + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name='round', + name='loser_bracket_mode', + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name='tournament', + name='loser_bracket_mode', + field=models.IntegerField(default=0), + ), + ] diff --git a/tournaments/models/custom_user.py b/tournaments/models/custom_user.py index a05a246..5f6ecdf 100644 --- a/tournaments/models/custom_user.py +++ b/tournaments/models/custom_user.py @@ -30,6 +30,7 @@ class CustomUser(AbstractUser): loser_bracket_match_format_preference = models.IntegerField(default=enums.FederalMatchCategory.NINE_GAMES, choices=enums.FederalMatchCategory.choices, null=True, blank=True) device_id = models.CharField(max_length=50, null=True, blank=True) + loser_bracket_mode = models.IntegerField(default=0) ### ### ### ### ### ### ### ### ### ### ### WARNING ### ### ### ### ### ### ### ### ### ### ### WARNING : Any added field MUST be inserted in the method below: fields_for_update() ### @@ -42,7 +43,7 @@ class CustomUser(AbstractUser): 'summons_message_body', 'summons_message_signature', 'summons_available_payment_methods', 'summons_display_format', 'summons_display_entry_fee', 'summons_use_full_custom_message', 'match_formats_default_duration', 'bracket_match_format_preference', - 'group_stage_match_format_preference', 'loser_bracket_match_format_preference', 'device_id'] + 'group_stage_match_format_preference', 'loser_bracket_match_format_preference', 'device_id', 'loser_bracket_mode'] def __str__(self): return self.username diff --git a/tournaments/models/round.py b/tournaments/models/round.py index 0ad10e3..08729a6 100644 --- a/tournaments/models/round.py +++ b/tournaments/models/round.py @@ -10,6 +10,7 @@ class Round(models.Model): format = models.IntegerField(default=FederalMatchCategory.NINE_GAMES, choices=FederalMatchCategory.choices, null=True, blank=True) start_date = models.DateTimeField(null=True, blank=True) group_stage_loser_bracket = models.BooleanField(default=False) + loser_bracket_mode = models.IntegerField(default=0) def __str__(self): if self.parent: diff --git a/tournaments/models/tournament.py b/tournaments/models/tournament.py index 0b43c87..c951d41 100644 --- a/tournaments/models/tournament.py +++ b/tournaments/models/tournament.py @@ -59,6 +59,7 @@ class Tournament(models.Model): publish_tournament = models.BooleanField(default=False) hide_points_earned = models.BooleanField(default=False) publish_rankings = models.BooleanField(default=False) + loser_bracket_mode = models.IntegerField(default=0) def __str__(self): if self.name: From 43c06c13dadcd084e1e019c6e6f76f3864615890 Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 19 Sep 2024 11:40:53 +0200 Subject: [PATCH 05/10] Adds fields for Purchase --- .gitignore | 2 +- api/serializers.py | 17 +++++++ api/views.py | 6 +-- shared/cryptography.py | 51 +++++++++++++++++++ tournaments/admin.py | 2 +- ...e_purchase_id_alter_purchase_identifier.py | 22 ++++++++ .../0085_rename_identifier_purchase_id.py | 18 +++++++ tournaments/models/purchase.py | 5 +- tournaments/models/tournament.py | 11 ++-- tournaments/utils/cryptography.py | 20 -------- 10 files changed, 120 insertions(+), 34 deletions(-) create mode 100644 shared/cryptography.py create mode 100644 tournaments/migrations/0084_remove_purchase_id_alter_purchase_identifier.py create mode 100644 tournaments/migrations/0085_rename_identifier_purchase_id.py delete mode 100644 tournaments/utils/cryptography.py diff --git a/.gitignore b/.gitignore index 4e82b22..2f99ee3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ padelclub_backend/settings_local.py myenv/ -tournaments/config_local.py +shared/config_local.py # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/api/serializers.py b/api/serializers.py index 61ac1a0..1e2d6de 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -11,6 +11,21 @@ from django.core.mail import EmailMessage from django.contrib.sites.shortcuts import get_current_site from api.tokens import account_activation_token +from shared.cryptography import encryption_util + +class EncryptedUserField(serializers.Field): + def to_representation(self, value): + # Encrypt data when sending it out + return encryption_util.encrypt_aes_gcm(str(value.id)) + + def to_internal_value(self, data): + # Decrypt data when receiving it + decrypted_user_id = encryption_util.decrypt_aes_gcm(data) + user = CustomUser.objects.get(id=decrypted_user_id) + if decrypted_user_id is None: + raise serializers.ValidationError("Invalid encrypted data") + return user + class UserSerializer(serializers.ModelSerializer): password = serializers.CharField(write_only=True) @@ -152,6 +167,8 @@ class PlayerRegistrationSerializer(serializers.ModelSerializer): # ['id', 'team_registration_id', 'first_name', 'last_name', 'licence_id', 'rank', 'has_paid'] class PurchaseSerializer(serializers.ModelSerializer): + user = EncryptedUserField() + class Meta: model = Purchase fields = '__all__' diff --git a/api/views.py b/api/views.py index 6e19f66..acda60c 100644 --- a/api/views.py +++ b/api/views.py @@ -108,9 +108,9 @@ class PurchaseViewSet(viewsets.ModelViewSet): return [] def create(self, request, *args, **kwargs): - identifier = request.data.get('identifier') - if Purchase.objects.filter(identifier=identifier).exists(): - return Response({"detail": "This transaction identifier is already registered."}, status=status.HTTP_208_ALREADY_REPORTED) + id = request.data.get('id') + if Purchase.objects.filter(id=id).exists(): + return Response({"detail": "This transaction id is already registered."}, status=status.HTTP_208_ALREADY_REPORTED) serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) diff --git a/shared/cryptography.py b/shared/cryptography.py new file mode 100644 index 0000000..1ca41d4 --- /dev/null +++ b/shared/cryptography.py @@ -0,0 +1,51 @@ +from Crypto.Cipher import AES +import base64 +import os +from .config_local import CRYPTO_KEY + +class EncryptionUtil: + + def __init__(self, key): + # In a real application, store this key securely (e.g., environment variables) + self.crypto_key = key + + def encrypt_aes_gcm(self, plaintext): + # Decode the base64 encoded key + key = base64.b64decode(self.crypto_key) + + # Generate a random 12-byte nonce + nonce = os.urandom(12) + + # Create the cipher object + cipher = AES.new(key, AES.MODE_GCM, nonce=nonce) + + # Encrypt the plaintext + ciphertext, tag = cipher.encrypt_and_digest(plaintext.encode('utf-8')) + + # Combine nonce, ciphertext, and tag + encrypted_data = nonce + ciphertext + tag + + # Encode the result in base64 + encrypted_base64 = base64.b64encode(encrypted_data).decode('utf-8') + + return encrypted_base64 + + def decrypt_aes_gcm(self, encrypted_base64): + # Decode the base64 encoded data and key + encrypted_data = base64.b64decode(encrypted_base64) + key = base64.b64decode(self.crypto_key) + + # Extract the nonce, tag, and ciphertext from the combined encrypted data + nonce = encrypted_data[:12] # AES GCM nonce is 12 bytes + tag = encrypted_data[-16:] # AES GCM tag is 16 bytes + ciphertext = encrypted_data[12:-16] # Ciphertext is everything in between + + # Create the cipher object and decrypt the data + cipher = AES.new(key, AES.MODE_GCM, nonce=nonce) + decrypted_data = cipher.decrypt_and_verify(ciphertext, tag) + + # Convert decrypted bytes to string (assuming UTF-8 encoding) + decrypted_text = decrypted_data.decode('utf-8') + return decrypted_text + +encryption_util = EncryptionUtil(CRYPTO_KEY) diff --git a/tournaments/admin.py b/tournaments/admin.py index bc7ee41..e0696cc 100644 --- a/tournaments/admin.py +++ b/tournaments/admin.py @@ -74,7 +74,7 @@ class ClubAdmin(admin.ModelAdmin): search_fields = ('name', 'acronym') class PurchaseAdmin(admin.ModelAdmin): - list_display = ['user', 'identifier', 'product_id', 'quantity', 'purchase_date', 'revocation_date', 'expiration_date'] + list_display = ['id', 'user', 'product_id', 'quantity', 'purchase_date', 'revocation_date', 'expiration_date'] list_filter = ['user'] class CourtAdmin(admin.ModelAdmin): diff --git a/tournaments/migrations/0084_remove_purchase_id_alter_purchase_identifier.py b/tournaments/migrations/0084_remove_purchase_id_alter_purchase_identifier.py new file mode 100644 index 0000000..36fcb03 --- /dev/null +++ b/tournaments/migrations/0084_remove_purchase_id_alter_purchase_identifier.py @@ -0,0 +1,22 @@ +# Generated by Django 5.1 on 2024-09-18 08:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tournaments', '0083_purchase_expiration_date'), + ] + + operations = [ + migrations.RemoveField( + model_name='purchase', + name='id', + ), + migrations.AlterField( + model_name='purchase', + name='identifier', + field=models.BigIntegerField(primary_key=True, serialize=False, unique=True), + ), + ] diff --git a/tournaments/migrations/0085_rename_identifier_purchase_id.py b/tournaments/migrations/0085_rename_identifier_purchase_id.py new file mode 100644 index 0000000..2de4b5b --- /dev/null +++ b/tournaments/migrations/0085_rename_identifier_purchase_id.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1 on 2024-09-18 08:34 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tournaments', '0084_remove_purchase_id_alter_purchase_identifier'), + ] + + operations = [ + migrations.RenameField( + model_name='purchase', + old_name='identifier', + new_name='id', + ), + ] diff --git a/tournaments/models/purchase.py b/tournaments/models/purchase.py index 8acca70..014a15e 100644 --- a/tournaments/models/purchase.py +++ b/tournaments/models/purchase.py @@ -3,9 +3,8 @@ import uuid from . import CustomUser class Purchase(models.Model): - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) + id = models.BigIntegerField(primary_key=True, unique=True, editable=True) user = models.ForeignKey(CustomUser, on_delete=models.CASCADE) - identifier = models.BigIntegerField(unique=True) purchase_date = models.DateTimeField() product_id = models.CharField(max_length=100) quantity = models.IntegerField(null=True, blank=True) @@ -13,4 +12,4 @@ class Purchase(models.Model): expiration_date = models.DateTimeField(null=True, blank=True) def __str__(self): - return f"{self.identifier} > {self.product_id} - {self.purchase_date} - {self.user.username}" + return f"{self.id} > {self.product_id} - {self.purchase_date} - {self.user.username}" diff --git a/tournaments/models/tournament.py b/tournaments/models/tournament.py index 0b43c87..0ff7b6f 100644 --- a/tournaments/models/tournament.py +++ b/tournaments/models/tournament.py @@ -7,8 +7,7 @@ from . import Event, TournamentPayment, FederalMatchCategory, FederalCategory, F import uuid from django.utils import timezone, formats from datetime import datetime, timedelta -from .. import config_local -from ..utils.cryptography import decrypt_aes_gcm +from shared.cryptography import encryption_util from ..utils.extensions import plural_format class TeamSortingType(models.IntegerChoices): @@ -67,8 +66,8 @@ class Tournament(models.Model): return self.display_name() def is_canceled(self): - if self.local_id and config_local.CRYPTO_KEY: - decrypted = decrypt_aes_gcm(self.local_id, config_local.CRYPTO_KEY) + if self.local_id: + decrypted = encryption_util.decrypt_aes_gcm(self.local_id) value = int(decrypted[18]) if 0 <= value <= 4: return True @@ -78,8 +77,8 @@ class Tournament(models.Model): return False def payment(self): - if self.global_id and config_local.CRYPTO_KEY: - decrypted = decrypt_aes_gcm(self.global_id, config_local.CRYPTO_KEY) + if self.global_id: + decrypted = encryption_util.decrypt_aes_gcm(self.global_id) value = int(decrypted[18]) return TournamentPayment(value) else: diff --git a/tournaments/utils/cryptography.py b/tournaments/utils/cryptography.py deleted file mode 100644 index 172c481..0000000 --- a/tournaments/utils/cryptography.py +++ /dev/null @@ -1,20 +0,0 @@ -from Crypto.Cipher import AES -import base64 - -def decrypt_aes_gcm(encrypted_base64, key_base64): - # Decode the base64 encoded data and key - encrypted_data = base64.b64decode(encrypted_base64) - key = base64.b64decode(key_base64) - - # Extract the nonce, tag, and ciphertext from the combined encrypted data - nonce = encrypted_data[:12] # AES GCM nonce is 12 bytes - tag = encrypted_data[-16:] # AES GCM tag is 16 bytes - ciphertext = encrypted_data[12:-16] # Ciphertext is everything in between - - # Create the cipher object and decrypt the data - cipher = AES.new(key, AES.MODE_GCM, nonce=nonce) - decrypted_data = cipher.decrypt_and_verify(ciphertext, tag) - - # Convert decrypted bytes to string (assuming UTF-8 encoding) - decrypted_text = decrypted_data.decode('utf-8') - return decrypted_text From 478e4706bf2fda8de466cd323b266062cf2debcf Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 19 Sep 2024 11:42:50 +0200 Subject: [PATCH 06/10] merge migrations --- tournaments/migrations/0086_merge_20240919_1142.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tournaments/migrations/0086_merge_20240919_1142.py diff --git a/tournaments/migrations/0086_merge_20240919_1142.py b/tournaments/migrations/0086_merge_20240919_1142.py new file mode 100644 index 0000000..07e4b71 --- /dev/null +++ b/tournaments/migrations/0086_merge_20240919_1142.py @@ -0,0 +1,14 @@ +# Generated by Django 5.1 on 2024-09-19 09:42 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tournaments', '0084_customuser_loser_bracket_mode_and_more'), + ('tournaments', '0085_rename_identifier_purchase_id'), + ] + + operations = [ + ] From 6b584d940a57e60a0ef3790ae23c7d16c0f509ac Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 19 Sep 2024 17:25:35 +0200 Subject: [PATCH 07/10] fix broadcast display name --- tournaments/models/tournament.py | 12 ++++++++++++ .../broadcast/broadcasted_auto.html | 18 +++++++++--------- .../broadcast/broadcasted_group_stages.html | 4 ++-- .../broadcast/broadcasted_matches.html | 4 ++-- .../broadcast/broadcasted_rankings.html | 4 ++-- .../broadcast/broadcasted_summons.html | 4 ++-- 6 files changed, 29 insertions(+), 17 deletions(-) diff --git a/tournaments/models/tournament.py b/tournaments/models/tournament.py index c951d41..41e2ff9 100644 --- a/tournaments/models/tournament.py +++ b/tournaments/models/tournament.py @@ -92,6 +92,18 @@ class Tournament(models.Model): else: return self.base_name() + def broadcast_display_name(self): + if self.name: + return self.short_base_name() + " " + self.name + else: + return self.base_name() + + def broadcast_event_display_name(self): + if self.event is not None: + return self.event.display_name() + else: + return " " + def base_name(self): return f"{self.level()} {self.category()}" diff --git a/tournaments/templates/tournaments/broadcast/broadcasted_auto.html b/tournaments/templates/tournaments/broadcast/broadcasted_auto.html index 74cdf64..6888cc7 100644 --- a/tournaments/templates/tournaments/broadcast/broadcasted_auto.html +++ b/tournaments/templates/tournaments/broadcast/broadcasted_auto.html @@ -37,7 +37,7 @@ paginatedGroupStages: null, paginatedSummons: null, active: 1, - title: '', + prefixTitle: '', retrieveData() { fetch('/tournament/{{ tournament.id }}/broadcast/json/') .then(res => res.json()) @@ -45,7 +45,7 @@ this.paginatedMatches = this.paginate(data.matches, 8) this.paginatedGroupStages = this.paginate(data.group_stages, 4) this.paginatedSummons = this.paginateSummons(data.summons) - this.setTitle() + this.setPrefixTitle() }) }, paginateSummons(array) { @@ -76,19 +76,19 @@ setInterval(() => { this.retrieveData() this.active = this.active === this.pageCount() ? 1 : this.active+1 - this.setTitle() + this.setPrefixTitle() }, 15000) }, pageCount() { return this.paginatedMatches.length + this.paginatedGroupStages.length + this.paginatedSummons.length }, - setTitle() { + setPrefixTitle() { if (this.active < 1 + this.paginatedSummons.length) { - this.title = 'Convocations' + this.prefixTitle = 'Convocations' } else if (this.active < 1 + this.paginatedSummons.length + this.paginatedMatches.length) { - this.title = 'Matchs' + this.prefixTitle = 'Matchs' } else { - this.title = 'Poules' + this.prefixTitle = 'Poules' } } @@ -104,8 +104,8 @@ class="logo inline" />
-

{{ tournament.display_name }}

-

+

{{ tournament.broadcast_event_display_name }}

+

{{ tournament.broadcast_display_name }}

diff --git a/tournaments/templates/tournaments/broadcast/broadcasted_group_stages.html b/tournaments/templates/tournaments/broadcast/broadcasted_group_stages.html index 7af7d40..69b7028 100644 --- a/tournaments/templates/tournaments/broadcast/broadcasted_group_stages.html +++ b/tournaments/templates/tournaments/broadcast/broadcasted_group_stages.html @@ -67,8 +67,8 @@
-

{{ tournament.display_name }}

-

Poules

+

{{ tournament.broadcast_event_display_name }}

+

Poules {{ tournament.broadcast_display_name }}

{% qr_from_text qr_code_url options=qr_code_options %}
diff --git a/tournaments/templates/tournaments/broadcast/broadcasted_matches.html b/tournaments/templates/tournaments/broadcast/broadcasted_matches.html index 9718077..57db0a0 100644 --- a/tournaments/templates/tournaments/broadcast/broadcasted_matches.html +++ b/tournaments/templates/tournaments/broadcast/broadcasted_matches.html @@ -67,8 +67,8 @@
-

{{ tournament.display_name }}

-

Matchs

+

{{ tournament.broadcast_event_display_name }}

+

Matchs {{ tournament.broadcast_display_name }}

{% qr_from_text qr_code_url options=qr_code_options %}
diff --git a/tournaments/templates/tournaments/broadcast/broadcasted_rankings.html b/tournaments/templates/tournaments/broadcast/broadcasted_rankings.html index 237d821..ed4ef95 100644 --- a/tournaments/templates/tournaments/broadcast/broadcasted_rankings.html +++ b/tournaments/templates/tournaments/broadcast/broadcasted_rankings.html @@ -1,8 +1,8 @@ {% extends 'tournaments/broadcast/broadcast_base.html' %} {% block head_title %}Classement{% endblock %} -{% block first_title %}{{ tournament.display_name }}{% endblock %} -{% block second_title %}Classement{% endblock %} +{% block first_title %}{{ tournament.broadcast_event_display_name }}{% endblock %} +{% block second_title %}Classement {{ tournament.broadcast_display_name }}{% endblock %} {% block content %} diff --git a/tournaments/templates/tournaments/broadcast/broadcasted_summons.html b/tournaments/templates/tournaments/broadcast/broadcasted_summons.html index a2a4007..08157a9 100644 --- a/tournaments/templates/tournaments/broadcast/broadcasted_summons.html +++ b/tournaments/templates/tournaments/broadcast/broadcasted_summons.html @@ -1,8 +1,8 @@ {% extends 'tournaments/broadcast/broadcast_base.html' %} {% block head_title %}Convocations{% endblock %} -{% block first_title %}{{ tournament.display_name }}{% endblock %} -{% block second_title %}Convocations{% endblock %} +{% block first_title %}{{ tournament.broadcast_event_display_name }}{% endblock %} +{% block second_title %}Convocations {{ tournament.broadcast_display_name }}{% endblock %} {% block content %} From 522f514345d0503971d2a2b3a328479275609566 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 20 Sep 2024 10:16:16 +0200 Subject: [PATCH 08/10] fix user update new field --- api/serializers.py | 1 + tournaments/admin.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/api/serializers.py b/api/serializers.py index 1e2d6de..d5b44f3 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -67,6 +67,7 @@ class UserSerializer(serializers.ModelSerializer): bracket_match_format_preference=validated_data.get('bracket_match_format_preference'), group_stage_match_format_preference=validated_data.get('group_stage_match_format_preference'), loser_bracket_match_format_preference=validated_data.get('loser_bracket_match_format_preference'), + loser_bracket_mode=validated_data.get('loser_bracket_mode'), ) self.send_email(self.context['request'], user) diff --git a/tournaments/admin.py b/tournaments/admin.py index e0696cc..68784a7 100644 --- a/tournaments/admin.py +++ b/tournaments/admin.py @@ -23,7 +23,7 @@ class CustomUserAdmin(UserAdmin): 'summons_message_body', 'summons_message_signature', 'summons_available_payment_methods', 'summons_display_format', 'summons_display_entry_fee', 'summons_use_full_custom_message', 'match_formats_default_duration', 'bracket_match_format_preference', 'group_stage_match_format_preference', - 'loser_bracket_match_format_preference', 'device_id' + 'loser_bracket_match_format_preference', 'device_id', 'loser_bracket_mode' ]}), ] add_fieldsets = [ From 6327d7fde6c9a4cc46aa848f2f9f84e3de02e9d8 Mon Sep 17 00:00:00 2001 From: Raz Date: Sat, 21 Sep 2024 11:37:46 +0200 Subject: [PATCH 09/10] fix stuff website match and broadcast --- tournaments/models/group_stage.py | 2 +- tournaments/models/round.py | 1 - tournaments/models/tournament.py | 12 ++++++++---- tournaments/templates/tournaments/matches.html | 4 +++- .../templates/tournaments/navigation_tournament.html | 2 +- tournaments/views.py | 2 +- 6 files changed, 14 insertions(+), 9 deletions(-) diff --git a/tournaments/models/group_stage.py b/tournaments/models/group_stage.py index 85415a9..a902a93 100644 --- a/tournaments/models/group_stage.py +++ b/tournaments/models/group_stage.py @@ -104,7 +104,7 @@ class GroupStage(models.Model): def has_at_least_one_started_match(self): for match in self.match_set.all(): - if match.start_date: + if match.start_date is not None and match.start_date <= timezone.now(): return True return False diff --git a/tournaments/models/round.py b/tournaments/models/round.py index 08729a6..4f5a348 100644 --- a/tournaments/models/round.py +++ b/tournaments/models/round.py @@ -59,7 +59,6 @@ class Round(models.Model): def get_matches_recursive(self, hide_empty_matches): matches = list(self.match_set.all()) # Retrieve matches associated with the current round - matches = [m for m in matches if m.should_appear()] if hide_empty_matches: matches = [m for m in matches if m.should_appear()] else: diff --git a/tournaments/models/tournament.py b/tournaments/models/tournament.py index 03de6aa..063a7a0 100644 --- a/tournaments/models/tournament.py +++ b/tournaments/models/tournament.py @@ -349,13 +349,15 @@ class Tournament(models.Model): def match_groups(self, broadcasted, group_stage_id, round_id): + display_brackets = self.display_matches() + match_groups = [] if group_stage_id: group_stage = self.groupstage_set.filter(id=group_stage_id).first() match_groups.append(self.group_stage_match_group(group_stage, broadcasted, hide_empty_matches=False)) elif round_id: round = self.round_set.filter(id=round_id).first() - if round: + if round and display_brackets is True: match_groups = self.round_match_groups(round, broadcasted, hide_empty_matches=False) else: match_groups = self.all_groups(broadcasted) @@ -364,8 +366,10 @@ class Tournament(models.Model): def all_groups(self, broadcasted): groups = [] - for round in self.round_set.filter(parent=None).all().order_by('index'): - groups.extend(self.round_match_groups(round, broadcasted, hide_empty_matches=True)) + + if self.display_matches(): + for round in self.round_set.filter(parent=None).all().order_by('index'): + groups.extend(self.round_match_groups(round, broadcasted, hide_empty_matches=True)) if self.display_group_stages(): for group_stage in self.groupstage_set.all().order_by('index'): @@ -697,7 +701,7 @@ class Tournament(models.Model): def has_all_group_stages_started(self): for group_stage in self.groupstage_set.all(): - if group_stage.has_at_least_one_started_match() == False: + if group_stage.has_at_least_one_started_match() is False: return False return True diff --git a/tournaments/templates/tournaments/matches.html b/tournaments/templates/tournaments/matches.html index 9080ded..0f5090c 100644 --- a/tournaments/templates/tournaments/matches.html +++ b/tournaments/templates/tournaments/matches.html @@ -9,7 +9,7 @@ {% include 'tournaments/navigation_tournament.html' %} -{% if tournament.display_matches %} +{% if tournament.display_matches or tournament.display_group_stages %} {% if rounds or group_stages %} {% endif %} diff --git a/tournaments/templates/tournaments/navigation_tournament.html b/tournaments/templates/tournaments/navigation_tournament.html index cb26aad..b219178 100644 --- a/tournaments/templates/tournaments/navigation_tournament.html +++ b/tournaments/templates/tournaments/navigation_tournament.html @@ -2,7 +2,7 @@ Informations - {% if tournament.display_matches %} + {% if tournament.display_matches or tournament.display_group_stages %} Matches {% endif %} diff --git a/tournaments/views.py b/tournaments/views.py index 73a2781..cd5ab8e 100644 --- a/tournaments/views.py +++ b/tournaments/views.py @@ -151,7 +151,7 @@ def tournament(request, tournament_id): print(len(rounds)) print(len(group_stages)) - if tournament.display_matches(): + if tournament.display_matches() or tournament.display_group_stages(): return render(request, 'tournaments/matches.html', { 'tournament': tournament, 'rounds': rounds, From f9b7dc56a9add5f01255e7eb32669b8875eb7918 Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 23 Sep 2024 11:39:41 +0200 Subject: [PATCH 10/10] Adds a tournament filter for PlayerRegistration --- tournaments/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tournaments/admin.py b/tournaments/admin.py index e0696cc..ec5f279 100644 --- a/tournaments/admin.py +++ b/tournaments/admin.py @@ -60,6 +60,7 @@ class RoundAdmin(admin.ModelAdmin): class PlayerRegistrationAdmin(admin.ModelAdmin): list_display = ['first_name', 'last_name', 'licence_id', 'rank'] search_fields = ('first_name', 'last_name') + list_filter = [TeamScoreTournamentListFilter] class MatchAdmin(admin.ModelAdmin): list_display = ['__str__', 'round', 'group_stage', 'start_date', 'index']