bracket-feature
laurent 1 year ago
commit 35c150de5e
  1. 2
      .gitignore
  2. 19
      api/serializers.py
  3. 12
      api/views.py
  4. 51
      shared/cryptography.py
  5. 6
      tournaments/admin.py
  6. 23
      tournaments/migrations/0082_alter_purchase_identifier_and_more.py
  7. 18
      tournaments/migrations/0083_purchase_expiration_date.py
  8. 28
      tournaments/migrations/0084_customuser_loser_bracket_mode_and_more.py
  9. 22
      tournaments/migrations/0084_remove_purchase_id_alter_purchase_identifier.py
  10. 18
      tournaments/migrations/0085_rename_identifier_purchase_id.py
  11. 14
      tournaments/migrations/0086_merge_20240919_1142.py
  12. 3
      tournaments/models/custom_user.py
  13. 6
      tournaments/models/purchase.py
  14. 1
      tournaments/models/round.py
  15. 24
      tournaments/models/tournament.py
  16. 2
      tournaments/signals.py
  17. 18
      tournaments/templates/tournaments/broadcast/broadcasted_auto.html
  18. 4
      tournaments/templates/tournaments/broadcast/broadcasted_group_stages.html
  19. 4
      tournaments/templates/tournaments/broadcast/broadcasted_matches.html
  20. 4
      tournaments/templates/tournaments/broadcast/broadcasted_rankings.html
  21. 4
      tournaments/templates/tournaments/broadcast/broadcasted_summons.html
  22. 20
      tournaments/utils/cryptography.py

2
.gitignore vendored

@ -6,7 +6,7 @@ padelclub_backend/settings_local.py
myenv/
tournaments/config_local.py
shared/config_local.py
# Byte-compiled / optimized / DLL files
__pycache__/

@ -9,9 +9,23 @@ 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
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)
@ -53,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)
@ -153,6 +168,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__'

@ -107,8 +107,16 @@ class PurchaseViewSet(viewsets.ModelViewSet):
return self.queryset.filter(user=self.request.user)
return []
def put(self, request, pk):
raise MethodNotAllowed('PUT')
def create(self, request, *args, **kwargs):
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)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def patch(self, request, pk):
raise MethodNotAllowed('PATCH')

@ -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)

@ -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 = [
@ -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']
@ -74,7 +75,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 = ['id', 'user', 'product_id', 'quantity', 'purchase_date', 'revocation_date', 'expiration_date']
list_filter = ['user']
class CourtAdmin(admin.ModelAdmin):
list_display = ['index', 'name', 'club']

@ -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),
),
]

@ -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),
),
]

@ -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),
),
]

@ -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),
),
]

@ -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',
),
]

@ -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 = [
]

@ -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

@ -3,13 +3,13 @@ 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()
purchase_date = models.DateTimeField()
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}"
return f"{self.id} > {self.product_id} - {self.purchase_date} - {self.user.username}"

@ -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:

@ -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):
@ -59,6 +58,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:
@ -67,8 +67,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 +78,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:
@ -91,6 +91,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()}"

@ -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():

@ -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"
/>
<div class="inline">
<h1 class="club">{{ tournament.display_name }}</h1>
<h1 class="event"><span x-text="title"></span></h1>
<h1 class="club">{{ tournament.broadcast_event_display_name }}</h1>
<h1 class="event"><span x-text="prefixTitle"></span> {{ tournament.broadcast_display_name }}</h1>
<!-- <span>Propulsé par Padel Club</span> -->
</div>
</div>

@ -67,8 +67,8 @@
<div class="left-content bubble">
<img src="{% static 'tournaments/images/PadelClub_logo_512.png' %}" alt="logo" class="logo">
<div class="left-margin">
<h1 class="club">{{ tournament.display_name }}</h1>
<h1 class="event">Poules</h1>
<h1 class="club">{{ tournament.broadcast_event_display_name }}</h1>
<h1 class="event">Poules {{ tournament.broadcast_display_name }}</h1>
</div>
</div>
<div class="right-content">{% qr_from_text qr_code_url options=qr_code_options %}</div>

@ -67,8 +67,8 @@
<div class="left-content bubble">
<img src="{% static 'tournaments/images/PadelClub_logo_512.png' %}" alt="logo" class="logo">
<div class="left-margin">
<h1 class="club">{{ tournament.display_name }}</h1>
<h1 class="event">Matchs</h1>
<h1 class="club">{{ tournament.broadcast_event_display_name }}</h1>
<h1 class="event">Matchs {{ tournament.broadcast_display_name }}</h1>
</div>
</div>
<div class="right-content">{% qr_from_text qr_code_url options=qr_code_options %}</div>

@ -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 %}

@ -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 %}

@ -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
Loading…
Cancel
Save