Laurent 2 days ago
commit 3ce30cf5f7
  1. 1
      sync/signals.py
  2. 69
      tournaments/admin.py
  3. 7
      tournaments/admin_utils.py
  4. 7
      tournaments/models/player_registration.py
  5. 68
      tournaments/models/round.py
  6. 2
      tournaments/models/team_registration.py
  7. 4
      tournaments/models/tournament.py
  8. 2
      tournaments/static/rankings/CLASSEMENT-PADEL-DAMES-10-2025.csv
  9. 16913
      tournaments/static/rankings/CLASSEMENT-PADEL-DAMES-11-2025.csv
  10. 2
      tournaments/static/rankings/CLASSEMENT-PADEL-MESSIEURS-10-2025.csv
  11. 117908
      tournaments/static/rankings/CLASSEMENT-PADEL-MESSIEURS-11-2025.csv
  12. 213
      tournaments/static/tournaments/js/tournament_bracket.js
  13. 81
      tournaments/templates/admin/tournaments/set_club_action.html
  14. 10
      tournaments/templates/tournaments/navigation_tournament.html
  15. 5
      tournaments/templatetags/tournament_tags.py
  16. 2
      tournaments/views.py

@ -126,7 +126,6 @@ def save_model_log(users, model_operation, model_name, model_id, store_id):
with transaction.atomic():
created_logs = []
for user in users:
if user.can_synchronize:
# logger.info(f'Creating ModelLog for user {user.id} - user exists: {User.objects.filter(id=user.id).exists()}')
model_log = ModelLog(
user=user,

@ -117,6 +117,7 @@ class EventAdmin(SyncedObjectAdmin):
raw_id_fields = ['related_user', 'creator', 'club']
ordering = ['-creation_date']
readonly_fields = ['display_images_preview']
actions = ['set_club_action']
fieldsets = [
(None, {'fields': ['last_update', 'related_user', 'name', 'club', 'creator', 'creation_date', 'tenup_id']}),
@ -146,6 +147,74 @@ class EventAdmin(SyncedObjectAdmin):
return mark_safe(html)
display_images_preview.short_description = 'Images Preview'
def set_club_action(self, request, queryset):
"""Action to set club for selected events"""
from django import forms
from django.contrib.admin.widgets import ForeignKeyRawIdWidget
from django.contrib.admin import helpers
from django.core.exceptions import ValidationError
class ClubSelectionForm(forms.Form):
_selected_action = forms.CharField(widget=forms.MultipleHiddenInput)
action = forms.CharField(widget=forms.HiddenInput)
club_id = forms.CharField(
label='Club',
required=True,
help_text='Enter Club ID or use the search icon to find a club',
widget=ForeignKeyRawIdWidget(
Event._meta.get_field('club').remote_field,
self.admin_site
)
)
def clean_club_id(self):
club_id = self.cleaned_data['club_id']
try:
club = Club.objects.get(pk=club_id)
return club
except Club.DoesNotExist:
raise ValidationError(f'Club with ID {club_id} does not exist.')
except (ValueError, TypeError) as e:
raise ValidationError(f'Invalid Club ID format: {club_id}')
if 'apply' in request.POST:
form = ClubSelectionForm(request.POST)
if form.is_valid():
club = form.cleaned_data['club_id'] # This is now a Club instance
updated_count = queryset.update(club=club)
self.message_user(
request,
f'Successfully updated {updated_count} event(s) with club: {club.name}',
messages.SUCCESS
)
return None
else:
# Show form errors
self.message_user(
request,
f'Form validation failed. Errors: {form.errors}',
messages.ERROR
)
# Initial form display
form = ClubSelectionForm(initial={
'_selected_action': request.POST.getlist(helpers.ACTION_CHECKBOX_NAME),
'action': 'set_club_action',
})
context = {
'form': form,
'events': queryset,
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
'action_name': 'set_club_action',
'title': 'Set Club for Events',
'media': form.media,
'has_change_permission': True,
}
return render(request, 'admin/tournaments/set_club_action.html', context)
set_club_action.short_description = "Set club for selected events"
class TournamentAdmin(SyncedObjectAdmin):
list_display = ['display_name', 'event', 'is_private', 'start_date', 'payment', 'creator', 'is_canceled']
list_filter = [StartDateRangeFilter, 'is_deleted', 'event__creator']

@ -19,9 +19,8 @@ import io
from api.utils import scrape_fft_all_tournaments, get_umpire_data
default_sexe = "H"
default_id_homologation = "82546485"
default_session_id = "JSESSIONID=E3DE6A54D5367D48B0CFA970E09EB422; AWSALB=UlkEmLYVxfS3RNwiNeNygqdqjroNzOZF3D9k6nR+NP6YPG3r6JLIzOqtw3nV1aVKsyNMldzeFOmVy/V1OPf7LNVW/sckdD1EprkGtgqjX8N8DpihxhTGtTm+0sX1; AWSALBCORS=UlkEmLYVxfS3RNwiNeNygqdqjroNzOZF3D9k6nR+NP6YPG3r6JLIzOqtw3nV1aVKsyNMldzeFOmVy/V1OPf7LNVW/sckdD1EprkGtgqjX8N8DpihxhTGtTm+0sX1; datadome=K3v~wZc~sLs5C7D4p0OoS3jOXGpeDfai9vk~TDPw2mSFbxqpfjUcR68wvPaYXHYqXgAHOrFnrBGpoyNepJ6bXfncdSmYOUfMNPbAtvBBo67zZTxxSeogLiLu1U1_5Txo; TCID=; tc_cj_v2=%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQOKSRRSNRRNQZZZ%5D777_rn_lh%5BfyfcheZZZ%2F%20%290%2BH%2C0%200%20G%24%2FH%29%20%2FZZZKQOLJNPJONLMPZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQOLJNPJSSQMNZZZ%5D777_rn_lh%5BfyfcheZZZ%2F%20%290%2BH%2C0%200%20G%24%2FH%29%20%2FZZZKQOLJNPRRONPSZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQOLJNQLKOMSOZZZ%5D777_rn_lh%5BfyfcheZZZ%2F%20%290%2BH%2C0%200%20G%24%2FH%29%20%2FZZZKQOLJPMNSNOJKZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQOLJPMRPKRKMZZZ%5D777_rn_lh%5BfyfcheZZZ%2F%20%290%2BH%2C0%200%20G%24%2FH%29%20%2FZZZKQOLJQSONNLNQZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQOLKMOPOJKSLZZZ%5D777_rn_lh%5BfyfcheZZZ%2F%20%290%2BH%2C0%200%20G%24%2FH%29%20%2FZZZKQONMQSSNRPKQZZZ%5D; tc_cj_v2_cmp=; tc_cj_v2_med=; xtan=-; xtant=1; pa_vid=%22mckhos3iasswydjm%22; _pcid=%7B%22browserId%22%3A%22mckhos3iasswydjm%22%2C%22_t%22%3A%22ms8wm9hs%7Cmckhos5s%22%7D; _pctx=%7Bu%7DN4IgrgzgpgThIC4B2YA2qA05owMoBcBDfSREQpAeyRCwgEt8oBJAE0RXSwH18yBbCAA4A7vwCcACwgAffgGMA1pMoQArPAC%2BQA; TCPID=125629554310878226394; xtvrn=$548419$"
default_id_homologation = "82553537"
default_session_id = "JSESSIONID=D4827C95015A626E3875F0B6F7595118; AWSALB=6MbqGI4p8pOK+7Z1dhU+rBcE2ahiNvCRAaHB+GPKS9y/G7LYVt/d/4ArQMqTmWSUvQNzNZNj8fu02oU2YMC5N0aag93ZlMMdMUvyiPrmrNPX8pg5jRnKrI2t5V3R; AWSALBCORS=6MbqGI4p8pOK+7Z1dhU+rBcE2ahiNvCRAaHB+GPKS9y/G7LYVt/d/4ArQMqTmWSUvQNzNZNj8fu02oU2YMC5N0aag93ZlMMdMUvyiPrmrNPX8pg5jRnKrI2t5V3R; _pcid=%7B%22browserId%22%3A%22mckhos3iasswydjm%22%2C%22_t%22%3A%22mx8qjlef%7Cmhkbm42f%22%7D; tc_cj_v2=%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQPLLNOQRKONOZZZ%5D; tc_cj_v2_cmp=; tc_cj_v2_med=; TCID=; datadome=uGOumlzX7RG4xt8Z53eYpnhe~YuSyRRgqnChCcy2Xx~fQecmm4XptKYUuJgRdpaBrGv4SsFor~jwm2rk9p37Ok6k9tGc~1~ntNWx4fI7WlbPxEfLBkEKOm4Y7WdWyuK5; xtan=-; xtant=1; SSESS7ba44afc36c80c3faa2b8fa87e7742c5=LAtWVNYPtWmJdMeJQ96O5KvPvdevS3dgvj_ipz1ykvQ; pa_vid=%22mckhos3iasswydjm%22; TCPID=125629554310878226394; xtvrn=$548419$"
def calculate_age_from_birth_date(birth_date_str):
"""
@ -1709,7 +1708,7 @@ def enrich_rankings_with_licenses(request):
try:
# Build license lookup URL with proper URL encoding
license_url = f"https://beach-padel.app.fft.fr/beachja/rechercheJoueur/licencies?idHomologation={id_homologation}&nom={urllib.parse.quote(sanitize_for_latin1(nom), encoding='latin-1')}&prenom={urllib.parse.quote(sanitize_for_latin1(prenom), encoding='latin-1')}&sexe={sexe}"
license_url = f"https://beach-padel.app.fft.fr/beachja/rechercheJoueur/licencies?idHomologation={id_homologation}&nom={urllib.parse.quote(sanitize_for_latin1(nom))}&prenom={urllib.parse.quote(sanitize_for_latin1(prenom))}&sexe={sexe}"
# Make license lookup request
license_response = requests.get(

@ -89,7 +89,7 @@ class PlayerRegistration(TournamentSubModel):
name = self.first_name
elif self.first_name and len(self.first_name) > 0:
name = f"{self.first_name[0]}. {self.last_name}"
if len(name) > 20 or forced:
if len(name) > 20:
name_parts = self.last_name.split(" ")
if len(name_parts) > 0 and self.first_name and len(self.first_name) > 0:
name = f"{self.first_name[0]}. {name_parts[0]}"
@ -235,3 +235,8 @@ class PlayerRegistration(TournamentSubModel):
if self.contact_email:
return self.contact_email
return self.email
def get_last_name(self):
if self.is_anonymous:
return "Anonyme"
return self.last_name

@ -13,6 +13,15 @@ class Round(TournamentSubModel):
group_stage_loser_bracket = models.BooleanField(default=False)
loser_bracket_mode = models.IntegerField(default=0)
# Debug flag - set to False to disable all debug prints
DEBUG_PREPARE_MATCH_GROUP = False
@staticmethod
def debug_print(*args, **kwargs):
"""Print debug messages only if DEBUG_PREPARE_MATCH_GROUP is True"""
if Round.DEBUG_PREPARE_MATCH_GROUP:
print(*args, **kwargs)
def delete_dependencies(self):
for round in self.children.all():
round.delete_dependencies()
@ -112,19 +121,35 @@ class Round(TournamentSubModel):
return True
def prepare_match_group(self, next_round, parent_round, loser_final, double_butterfly_mode, secondHalf):
Round.debug_print(f"\n[{self.name()}] === START prepare_match_group ===")
Round.debug_print(f"[{self.name()}] index={self.index}, nextRound={next_round.name() if next_round else None}, parentRound={parent_round.name() if parent_round else None}")
Round.debug_print(f"[{self.name()}] loserFinal={loser_final.name() if loser_final else None}, doubleButterfly={double_butterfly_mode}, secondHalf={secondHalf}")
short_names = double_butterfly_mode
if double_butterfly_mode and self.tournament.rounds.filter(parent=None).count() < 3:
short_names = False
Round.debug_print(f"[{self.name()}] Short names disabled (rounds < 3)")
matches = self.matches.filter(disabled=False).order_by('index')
Round.debug_print(f"[{self.name()}] Initial enabled matches: {len(matches)} - indices: {[m.index for m in matches]}")
if len(matches) == 0:
Round.debug_print(f"[{self.name()}] No matches, returning None")
return None
if next_round:
next_round_matches = next_round.matches.filter(disabled=False).order_by('index')
Round.debug_print(f"[{self.name()}] Next round matches: {len(next_round_matches)} - indices: {[m.index for m in next_round_matches]}")
else:
next_round_matches = []
Round.debug_print(f"[{self.name()}] No next round")
if len(matches) < len(next_round_matches):
Round.debug_print(f"[{self.name()}] FILTERING: matches({len(matches)}) < nextRoundMatches({len(next_round_matches)})")
all_matches = self.matches.order_by('index')
Round.debug_print(f"[{self.name()}] All matches (including disabled): {len(all_matches)} - indices: {[(m.index, m.disabled) for m in all_matches]}")
filtered_matches = []
# Process matches in pairs
@ -134,30 +159,43 @@ class Round(TournamentSubModel):
current_match = all_matches[i]
pair_match = all_matches[i+1] if i+1 < len(all_matches) else None
Round.debug_print(f"[{self.name()}] Pair {i//2}: current={current_match.index}(disabled={current_match.disabled}), pair={pair_match.index if pair_match else None}(disabled={pair_match.disabled if pair_match else None})")
# Only filter out the pair if both matches are disabled
if current_match.disabled and pair_match and pair_match.disabled:
Round.debug_print(f"[{self.name()}] Both disabled, checking next_round for index {current_match.index // 2}")
# Skip one of the matches in the pair
if next_round_matches.filter(index=current_match.index // 2).exists():
filtered_matches.append(current_match)
filtered_matches.append(pair_match)
pass
# filtered_matches.append(pair_match)
# Keeping two was bugging the bracket
Round.debug_print(f"[{self.name()}] Next round match exists, keeping one")
else:
Round.debug_print(f"[{self.name()}] No next round match, skipping both")
else:
# Keep the current match
if current_match.disabled == False:
filtered_matches.append(current_match)
Round.debug_print(f"[{self.name()}] Keeping current match {current_match.index}")
# If there's a pair match, keep it too
if pair_match and pair_match.disabled == False:
filtered_matches.append(pair_match)
Round.debug_print(f"[{self.name()}] Keeping pair match {pair_match.index}")
# Move to the next pair
i += 2
# Replace the matches list with our filtered list
matches = filtered_matches
Round.debug_print(f"[{self.name()}] After filtering: {len(matches)} matches - indices: {[m.index for m in matches]}")
if matches:
if len(matches) > 1 and double_butterfly_mode:
Round.debug_print(f"[{self.name()}] SPLITTING: doubleButterfly with {len(matches)} matches")
if len(matches) % 2 == 1:
Round.debug_print(f"[{self.name()}] ODD number of matches - using smart split logic")
# Calculate expected index range for this round
if self.index == 0:
# Final: only index 0
@ -168,47 +206,71 @@ class Round(TournamentSubModel):
start_index = (2 ** self.index) - 1
expected_indices = list(range(start_index, start_index + expected_count))
Round.debug_print(f"[{self.name()}] Expected indices: {expected_indices}")
# Get actual match indices
actual_indices = [match.index for match in matches]
missing_indices = [idx for idx in expected_indices if idx not in actual_indices]
Round.debug_print(f"[{self.name()}] Actual indices: {actual_indices}")
Round.debug_print(f"[{self.name()}] Missing indices: {missing_indices}")
if missing_indices and len(expected_indices) > 1:
# Split the expected range in half
midpoint_index = len(expected_indices) // 2
first_half_expected = expected_indices[:midpoint_index]
second_half_expected = expected_indices[midpoint_index:]
Round.debug_print(f"[{self.name()}] Expected halves: first={first_half_expected}, second={second_half_expected}")
# Count actual matches in each theoretical half
first_half_actual = sum(1 for idx in actual_indices if idx in first_half_expected)
second_half_actual = sum(1 for idx in actual_indices if idx in second_half_expected)
Round.debug_print(f"[{self.name()}] Actual counts: first={first_half_actual}, second={second_half_actual}")
# Give more display space to the half with more actual matches
if first_half_actual > second_half_actual:
midpoint = (len(matches) + 1) // 2 # More to first half
Round.debug_print(f"[{self.name()}] First half has more: midpoint={midpoint}")
else:
midpoint = len(matches) // 2 # More to second half
Round.debug_print(f"[{self.name()}] Second half has more: midpoint={midpoint}")
else:
# No missing indices or only one expected match, split normally
midpoint = len(matches) // 2
Round.debug_print(f"[{self.name()}] No missing indices: midpoint={midpoint}")
else:
# Even number of matches: split evenly
midpoint = len(matches) // 2
Round.debug_print(f"[{self.name()}] EVEN number of matches: midpoint={midpoint}")
first_half_matches = matches[:midpoint]
if secondHalf:
first_half_matches = matches[midpoint:]
Round.debug_print(f"[{self.name()}] Using SECOND half: {len(first_half_matches)} matches - indices: {[m.index for m in first_half_matches]}")
else:
Round.debug_print(f"[{self.name()}] Using FIRST half: {len(first_half_matches)} matches - indices: {[m.index for m in first_half_matches]}")
else:
Round.debug_print(f"[{self.name()}] NO SPLITTING: singleButterfly or single match")
first_half_matches = list(matches) # Convert QuerySet to a list
Round.debug_print(f"[{self.name()}] Using all {len(first_half_matches)} matches - indices: {[m.index for m in first_half_matches]}")
if self.index == 0 and loser_final:
loser_match = loser_final.matches.first()
if loser_match:
first_half_matches.append(loser_match)
Round.debug_print(f"[{self.name()}] Added loser final match: {loser_match.index}")
if first_half_matches:
name = self.plural_name()
if parent_round and first_half_matches[0].name is not None:
name = first_half_matches[0].name
Round.debug_print(f"[{self.name()}] Using custom name from first match: '{name}'")
else:
Round.debug_print(f"[{self.name()}] Using round name: '{name}'")
Round.debug_print(f"[{self.name()}] Creating match_group: name='{name}', roundId={self.id}, roundIndex={self.index}, shortNames={short_names}")
Round.debug_print(f"[{self.name()}] Final matches in group: {[m.index for m in first_half_matches]}")
match_group = self.tournament.create_match_group(
name=name,
matches=first_half_matches,
@ -216,6 +278,8 @@ class Round(TournamentSubModel):
round_index=self.index,
short_names=short_names
)
Round.debug_print(f"[{self.name()}] === END prepare_match_group - SUCCESS ===\n")
return match_group
Round.debug_print(f"[{self.name()}] === END prepare_match_group - NO MATCHES ===\n")
return None

@ -109,7 +109,7 @@ class TeamRegistration(TournamentSubModel):
def formatted_team_names(self):
if self.name:
return self.name
names = [pr.last_name for pr in self.players_sorted_by_rank][:2] # Take max first 2
names = [pr.get_last_name() for pr in self.players_sorted_by_rank][:2] # Take max first 2
joined_names = " / ".join(names)
if joined_names:
return f"Paire {joined_names}"

@ -1765,12 +1765,16 @@ class Tournament(BaseModel):
def umpire_mail(self):
if self.umpire_custom_mail is not None:
return self.umpire_custom_mail
if self.event and self.event.creator:
return self.event.creator.email
return None
def umpire_phone(self):
if self.umpire_custom_phone is not None:
return self.umpire_custom_phone
if self.event and self.event.creator:
return self.event.creator.phone
return None
def calculate_time_to_confirm(self, waiting_list_count):
"""

@ -1,6 +1,6 @@
anonymous-players:5459
max-players:16148
unrank-male-value:14060
unrank-male-value:14080
;1;COLLOMBON;Alix;FRA;1244033;0;Non;0;OCCITANIE;60 31 0803;STADE TOULOUSAIN TENNIS PADEL;0;;1993;
;2;GODALLIER;Léa;FRA;3574767;0;Non;0;PROVENCE ALPES COTE D'AZUR;60 31 8001;4PADEL TOULOUSE COLOMIERS;0;;1995;
;3;TOULY;Carla;FRA;3688236;0;Non;0;PROVENCE ALPES COTE D'AZUR;62 83 0032;TC DE BANDOL;0;;1996;

Can't render this file because it is too large.

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
anonymous-players:5459
max-players:110960
unrank-male-value:92341
unrank-male-value:92327
;1;ZAPATA PIZZARO;Teodoro Victor;ESP;2098059;4545;Oui;7;ILE DE FRANCE;57 93 0505;AS PADEL AFICIONADOS;0;;1995;
;1;HERNANDEZ QUESADA;luis;ESP;2744589;1200;Oui;1;NOUVELLE AQUITAINE;59 33 0723;BIG PADEL;0;;1999;
;1;LEYGUE;Thomas;FRA;7897265;0;Non;0;ILE DE FRANCE;57 93 0505;AS PADEL AFICIONADOS;0;;2001;

Can't render this file because it is too large.

@ -1,3 +1,12 @@
// Debug flag - set to false to disable all debug console logs
const DEBUG_BRACKET = false;
function debug_console(...args) {
if (DEBUG_BRACKET) {
console.log(...args);
}
}
function renderBracket(options) {
const bracket = document.getElementById("bracket");
const matchTemplates = document.getElementById("match-templates").children;
@ -8,6 +17,12 @@ function renderBracket(options) {
const displayLoserFinal = options.displayLoserFinal;
const tournamentId = options.tournamentId;
const isBroadcast = options.isBroadcast;
debug_console("=== RENDER BRACKET START ===");
debug_console(
`Options: doubleButterflyMode=${doubleButterflyMode}, displayLoserFinal=${displayLoserFinal}, isBroadcast=${isBroadcast}`,
);
// Group matches by round
Array.from(matchTemplates).forEach((template) => {
const roundIndex = parseInt(template.dataset.matchRound);
@ -34,6 +49,10 @@ function renderBracket(options) {
let nextMatchDistance = baseDistance;
let minimumMatchDistance = 1;
debug_console(
`Dimensions: matchHeight=${matchHeight}, baseDistance=${baseDistance}, roundCount=${roundCount}, finalRoundIndex=${finalRoundIndex}`,
);
const screenWidth = window.innerWidth;
let roundTotalCount = roundCount;
let initialPadding = 40;
@ -41,7 +60,7 @@ function renderBracket(options) {
roundTotalCount = roundCount - 1;
initialPadding = 46;
}
const padding = initialPadding * roundTotalCount; // Account for some padding/margin
const padding = initialPadding * roundTotalCount;
const availableWidth = screenWidth - padding;
let responsiveMatchWidth = Math.min(
365,
@ -82,6 +101,10 @@ function renderBracket(options) {
}
}
debug_console(
`Layout: responsiveMatchWidth=${responsiveMatchWidth}, topMargin=${topMargin}`,
);
rounds.forEach((roundMatches, roundIndex) => {
if (rounds[0].length <= 2 && doubleButterflyMode) {
minimumMatchDistance = 2;
@ -108,8 +131,13 @@ function renderBracket(options) {
const firstMatchTemplate = roundMatches[0].closest(".match-template");
const matchGroupName = firstMatchTemplate.dataset.matchGroupName;
const matchFormat = firstMatchTemplate.dataset.matchFormat;
const roundId = firstMatchTemplate.dataset.roundId; // Add this line
const realRoundIndex = firstMatchTemplate.dataset.roundIndex; // Add this line
const roundId = firstMatchTemplate.dataset.roundId;
const realRoundIndex = firstMatchTemplate.dataset.roundIndex;
debug_console(`\n=== ROUND ${roundIndex} (${matchGroupName}) ===`);
debug_console(
`realRoundIndex=${realRoundIndex}, matches=${roundMatches.length}`,
);
let nameSpan = document.createElement("div");
nameSpan.className = "round-name";
@ -145,10 +173,16 @@ function renderBracket(options) {
if (matchPositions[roundIndex] == undefined) {
matchPositions[roundIndex] = {};
}
matchDisabled[roundIndex] = []; // Initialize array for this round
matchDisabled[roundIndex] = [];
roundMatches.forEach((matchTemplate, matchIndex) => {
const matchTitle = matchTemplate.dataset.matchTitle;
const matchRealIndex = matchTemplate.dataset.matchRealIndex;
debug_console(
`\n[${matchTitle}] START - roundIndex:${roundIndex}, matchIndex:${matchIndex}, realIndex:${matchRealIndex}`,
);
const matchDiv = document.createElement("div");
matchDiv.className = "butterfly-match";
@ -159,7 +193,11 @@ function renderBracket(options) {
let isOutgoingLineIsDisabled = isDisabled;
let top;
const currentMatchesCount = roundMatches.length;
if (roundIndex > finalRoundIndex) {
debug_console(
`[${matchTitle}] CASE: Reverse bracket (roundIndex > finalRoundIndex)`,
);
matchDiv.classList.add("reverse-bracket");
if (roundIndex <= finalRoundIndex + 2) {
@ -167,18 +205,29 @@ function renderBracket(options) {
matchPositions[roundCount - roundIndex - 1],
);
top = values[matchIndex];
debug_console(
`[${matchTitle}] Reverse pos from mirror: top=${top}, mirrorRound=${roundCount - roundIndex - 1}`,
);
} else {
top = matchPositions[roundIndex][matchRealIndex];
console.log(matchTitle, top);
debug_console(`[${matchTitle}] Reverse pos direct: top=${top}`);
}
}
if (roundIndex === 0) {
debug_console(`[${matchTitle}] CASE: First round (roundIndex === 0)`);
if (doubleButterflyMode == false) {
nextMatchDistance = 0;
debug_console(
`[${matchTitle}] Single butterfly: nextMatchDistance=0`,
);
} else {
if (realRoundIndex > 1) {
nextMatchDistance = 0;
debug_console(
`[${matchTitle}] Double butterfly realRound>1: nextMatchDistance=0`,
);
}
}
if (roundCount > 1) {
@ -186,53 +235,85 @@ function renderBracket(options) {
if (currentMatchesCount == nextMatchesCount && roundCount > 2) {
nextMatchDistance = 0;
debug_console(
`[${matchTitle}] Same match count: nextMatchDistance=0`,
);
}
}
top = matchIndex * (matchHeight + matchSpacing) * minimumMatchDistance;
debug_console(
`[${matchTitle}] Calc: top=${top} (matchIdx=${matchIndex}, spacing=${matchHeight + matchSpacing}, minDist=${minimumMatchDistance})`,
);
if (roundCount == 3 && doubleButterflyMode) {
top = top + (matchHeight + matchSpacing) / 2;
debug_console(`[${matchTitle}] 3-round adjustment: top=${top}`);
}
} else if (roundIndex === roundCount - 1 && doubleButterflyMode == true) {
debug_console(`[${matchTitle}] CASE: Last round double butterfly`);
if (roundCount > 3) {
nextMatchDistance = 0;
debug_console(`[${matchTitle}] Large bracket: nextMatchDistance=0`);
} else {
nextMatchDistance = nextMatchDistance / 2;
debug_console(
`[${matchTitle}] Small bracket: nextMatchDistance=${nextMatchDistance}`,
);
}
} else if (roundIndex == finalRoundIndex && realRoundIndex == 0) {
//realRoundIndex 0 means final's round
debug_console(`[${matchTitle}] CASE: Final round (realRoundIndex=0)`);
const values = Object.values(matchPositions[roundIndex - 1]);
const parentPos1 = values[0];
const parentPos2 = values[1];
debug_console(
`[${matchTitle}] Parent positions: pos1=${parentPos1}, pos2=${parentPos2}`,
);
if (doubleButterflyMode == true) {
debug_console(`[${matchTitle}] Double butterfly final`);
let lgth = matchPositions[0].length / 2;
let index = lgth + matchIndex - 1;
// If index goes negative, use 0 instead
if (displayLoserFinal == true) {
debug_console(`[${matchTitle}] With loser final`);
if (matchIndex == 0) {
top = parentPos1 - baseDistance / 2;
debug_console(`[${matchTitle}] Winner final: top=${top}`);
} else {
top = parentPos1 + baseDistance / 2;
debug_console(`[${matchTitle}] Loser final: top=${top}`);
}
nextMatchDistance = 0;
} else {
top = parentPos1;
nextMatchDistance = 0;
debug_console(`[${matchTitle}] Single final: top=${top}`);
}
} else {
debug_console(`[${matchTitle}] Single butterfly final`);
top = (parentPos1 + parentPos2) / 2;
debug_console(`[${matchTitle}] Center between parents: top=${top}`);
if (matchIndex == 0) {
nextMatchDistance = parentPos2 - parentPos1;
debug_console(
`[${matchTitle}] First final match: nextMatchDistance=${nextMatchDistance}`,
);
} else {
nextMatchDistance = 0;
debug_console(
`[${matchTitle}] Second+ final match: nextMatchDistance=0`,
);
}
if (displayLoserFinal == true) {
if (matchIndex == 1) {
top = matchPositions[roundIndex][0] + baseDistance + 80;
isIncomingLineIsDisabled = true;
debug_console(`[${matchTitle}] Loser final offset: top=${top}`);
}
}
}
@ -240,43 +321,72 @@ function renderBracket(options) {
(roundIndex == finalRoundIndex && realRoundIndex != 0) ||
roundIndex < finalRoundIndex
) {
debug_console(`[${matchTitle}] CASE: Intermediate round`);
const parentIndex1 = matchRealIndex * 2 + 1;
const parentIndex2 = matchRealIndex * 2 + 2;
const parentPos1 = matchPositions[roundIndex - 1][parentIndex1];
const parentPos2 = matchPositions[roundIndex - 1][parentIndex2];
const parent1Disable = matchDisabled[roundIndex - 1][parentIndex1];
const parent2Disable = matchDisabled[roundIndex - 1][parentIndex2];
debug_console(
`[${matchTitle}] Parents: idx1=${parentIndex1}(pos=${parentPos1}, disabled=${parent1Disable}), idx2=${parentIndex2}(pos=${parentPos2}, disabled=${parent2Disable})`,
);
if (
(parent1Disable == undefined || parent1Disable == true) &&
(parent2Disable == undefined || parent2Disable == true)
) {
isIncomingLineIsDisabled = true;
debug_console(
`[${matchTitle}] Both parents disabled, incoming line disabled`,
);
}
if (
matchPositions[roundIndex - 1][parentIndex1] != undefined &&
matchPositions[roundIndex - 1][parentIndex2] != undefined
) {
debug_console(`[${matchTitle}] Both parents exist`);
top = (parentPos1 + parentPos2) / 2;
if (parent1Disable && parent2Disable) {
nextMatchDistance = 0;
const keys = Object.keys(matchPositions[roundIndex]).map(Number);
const lastKey = Math.max(...keys);
top =
(matchHeight + matchSpacing) * minimumMatchDistance * keys.length;
debug_console(
`[${matchTitle}] Both disabled: top=${top}, nextMatchDistance=0`,
);
} else {
nextMatchDistance = parentPos2 - parentPos1;
debug_console(
`[${matchTitle}] Center calc: top=${top}, nextMatchDistance=${nextMatchDistance}`,
);
}
} else if (matchPositions[roundIndex - 1][parentIndex1] != undefined) {
debug_console(`[${matchTitle}] Only parent1 exists`);
nextMatchDistance = 0;
top = matchPositions[roundIndex - 1][parentIndex1];
debug_console(`[${matchTitle}] Use parent1: top=${top}`);
} else if (matchPositions[roundIndex - 1][parentIndex2] != undefined) {
debug_console(`[${matchTitle}] Only parent2 exists`);
nextMatchDistance = 0;
top = matchPositions[roundIndex - 1][parentIndex2];
debug_console(`[${matchTitle}] Use parent2: top=${top}`);
} else {
debug_console(`[${matchTitle}] No parents exist`);
nextMatchDistance = 0;
top = 0;
debug_console(`[${matchTitle}] Default: top=0`);
}
} else if (roundIndex < roundCount) {
debug_console(
`[${matchTitle}] CASE: Setting future positions (roundIndex < roundCount)`,
);
const parentIndex1 = matchRealIndex * 2 + 1;
const parentIndex2 = matchRealIndex * 2 + 2;
const parentMatch1 = rounds[roundIndex + 1].find(
@ -286,57 +396,64 @@ function renderBracket(options) {
(match) => parseInt(match.dataset.matchRealIndex) === parentIndex2,
);
debug_console(
`[${matchTitle}] Looking for children: idx1=${parentIndex1}, idx2=${parentIndex2}`,
);
debug_console(
`[${matchTitle}] Found: match1=${parentMatch1?.dataset.matchTitle || "none"}, match2=${parentMatch2?.dataset.matchTitle || "none"}`,
);
if (matchPositions[roundIndex + 1] == undefined) {
matchPositions[roundIndex + 1] = {};
}
if (
parentMatch1 != undefined &&
parentMatch2 != undefined &&
parentMatch1.dataset.disabled == "false" &&
parentMatch2.dataset.disabled == "false"
) {
console.log(
roundIndex,
matchTitle,
parentMatch1.dataset.matchTitle,
parentMatch2.dataset.matchTitle,
parentMatch1.dataset.disabled,
parentMatch2.dataset.disabled,
top,
debug_console(
`[${matchTitle}] Both children active - setting their positions`,
);
nextMatchDistance = baseDistance;
matchPositions[roundIndex + 1][parentIndex1] = top - baseDistance / 2;
matchPositions[roundIndex + 1][parentIndex2] = top + baseDistance / 2;
console.log(matchPositions[roundIndex + 1]);
// } else if (parentMatch1 != undefined) {
// matchPositions[roundIndex + 1][parentIndex1] = top;
// nextMatchDistance = 0;
// } else if (parentMatch2 != undefined) {
// matchPositions[roundIndex + 1][parentIndex1] = top;
// nextMatchDistance = 0;
debug_console(
`[${matchTitle}] Set: [${parentIndex1}]=${matchPositions[roundIndex + 1][parentIndex1]}, [${parentIndex2}]=${matchPositions[roundIndex + 1][parentIndex2]}`,
);
} else if (
parentMatch2 != undefined &&
parentMatch2.dataset.disabled == "false"
) {
debug_console(`[${matchTitle}] Only child2 active`);
if (realRoundIndex == 1 && doubleButterflyMode) {
//if missing match in quarterfinals in double butterfly mode
nextMatchDistance = baseDistance;
debug_console(
`[${matchTitle}] Quarterfinal missing match: nextMatchDistance=${baseDistance}`,
);
} else {
nextMatchDistance = 0;
}
matchPositions[roundIndex + 1][parentIndex2] = top;
debug_console(`[${matchTitle}] Set child2: [${parentIndex2}]=${top}`);
} else if (
parentMatch1 != undefined &&
parentMatch1.dataset.disabled == "false"
) {
debug_console(`[${matchTitle}] Only child1 active`);
if (realRoundIndex == 1 && doubleButterflyMode) {
//if missing match in quarterfinals in double butterfly mode
nextMatchDistance = baseDistance;
debug_console(
`[${matchTitle}] Quarterfinal missing match: nextMatchDistance=${baseDistance}`,
);
} else {
nextMatchDistance = 0;
}
matchPositions[roundIndex + 1][parentIndex1] = top;
debug_console(`[${matchTitle}] Set child1: [${parentIndex1}]=${top}`);
} else {
debug_console(`[${matchTitle}] No active children`);
isOutgoingLineIsDisabled = true;
}
}
@ -344,17 +461,24 @@ function renderBracket(options) {
if (doubleButterflyMode == true) {
if (roundIndex >= finalRoundIndex - 2) {
if (roundIndex == finalRoundIndex - 1) {
debug_console(`[${matchTitle}] Semifinal adjustments`);
matchDiv.classList.add("reverse-bracket");
isIncomingLineIsDisabled = true;
nextMatchDistance = nextMatchDistance / 2;
}
if (roundIndex == finalRoundIndex + 1) {
debug_console(`[${matchTitle}] Post-final adjustments`);
matchDiv.classList.remove("reverse-bracket");
isOutgoingLineIsDisabled = true;
nextMatchDistance = nextMatchDistance;
}
}
}
debug_console(
`[${matchTitle}] FINAL: top=${top}, nextMatchDistance=${nextMatchDistance}, disabled=${isDisabled}`,
);
matchDiv.style.setProperty(
"--semi-final-distance",
`${baseDistance / 2.3}px`,
@ -379,25 +503,13 @@ function renderBracket(options) {
matchPositions[roundIndex][matchRealIndex] = top;
if (matchIndex === 0) {
// // Add logo for final round
// if (roundIndex == finalRoundIndex) {
// const logoDiv = document.createElement('div');
// logoDiv.className = 'round-logo';
// const logoImg = document.createElement('img');
// logoImg.src = '/static/tournaments/images/PadelClub_logo_512.png';
// logoImg.alt = 'PadelClub Logo';
// logoDiv.appendChild(logoImg);
// logoDiv.style.transform = `translateX(-50%)`;
// matchesContainer.appendChild(logoDiv);
// }
// Position title above the first match
titleDiv.style.top = `${topMargin - roundTopMargin}px`; // Adjust the 60px offset as needed
titleDiv.style.top = `${topMargin - roundTopMargin}px`;
if (
(roundIndex == finalRoundIndex && realRoundIndex == 0) ||
isBroadcast == true
) {
titleDiv.style.top = `${top + topMargin - roundTopMargin}px`; // Adjust the 60px offset as needed
titleDiv.style.top = `${top + topMargin - roundTopMargin}px`;
}
titleDiv.style.position = "absolute";
if (roundCount >= 5 && doubleButterflyMode == true) {
@ -436,7 +548,7 @@ function renderBracket(options) {
titleDiv.className = "round-title";
titleDiv.appendChild(nameSpan);
titleDiv.appendChild(formatSpan);
titleDiv.style.top = `${top + topMargin - 80}px`; // Adjust the 60px offset as needed
titleDiv.style.top = `${top + topMargin - 80}px`;
titleDiv.style.position = "absolute";
matchesContainer.appendChild(titleDiv);
}
@ -479,24 +591,7 @@ function renderBracket(options) {
}
}
// if (
// roundIndex == finalRoundIndex - 1 &&
// displayLoserFinal == true &&
// doubleButterflyMode == true
// ) {
// const matchDiv2 = document.createElement("div");
// matchDiv2.className = "butterfly-match";
// matchDiv2.classList.add("inward");
// matchDiv2.classList.add("semi-final");
// matchDiv2.style.setProperty(
// "--next-match-distance",
// `${baseDistance}px`,
// );
// matchDiv2.style.top = `${top}px`;
// matchDiv2.innerHTML = `<div class="match-content">${rounds[0][0].innerHTML}</div>`;
// matchesContainer.appendChild(matchDiv2); // Append to matchesContainer instead of roundDiv
// }
matchesContainer.appendChild(matchDiv); // Append to matchesContainer instead of roundDiv
matchesContainer.appendChild(matchDiv);
});
bracket.appendChild(roundDiv);
@ -549,7 +644,7 @@ function renderBracket(options) {
// Create a container that will sit at the same position for all rounds
const footerContainer = document.createElement("div");
footerContainer.style.position = "absolute";
footerContainer.style.top = `${globalMaxBottom}px`; // Same position for all footers
footerContainer.style.top = `${globalMaxBottom}px`;
footerContainer.style.width = "100%";
footerContainer.appendChild(footerDiv);
@ -562,4 +657,6 @@ function renderBracket(options) {
});
}, 100);
}
debug_console("=== RENDER BRACKET END ===\n");
}

@ -0,0 +1,81 @@
{% extends "admin/base_site.html" %}
{% load static %}
{% block extrahead %}
{{ block.super }}
{{ media }}
<script type="text/javascript" src="{% url 'admin:jsi18n' %}"></script>
{% endblock %}
{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/forms.css' %}">
{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">Home</a>
&rsaquo; <a href="{% url 'admin:tournaments_event_changelist' %}">Events</a>
&rsaquo; Set Club
</div>
{% endblock %}
{% block content %}
<h1>{{ title }}</h1>
<p>You are about to set the club for the following {{ events|length }} event(s):</p>
<div style="margin: 20px 0; padding: 15px; background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 4px;">
<ul style="margin: 0; padding-left: 20px;">
{% for event in events %}
<li>
<strong>{{ event.name }}</strong>
{% if event.club %}
(currently: {{ event.club.name }})
{% else %}
(currently: No club assigned)
{% endif %}
</li>
{% endfor %}
</ul>
</div>
<form method="post" id="club-form" action="">
{% csrf_token %}
{# Hidden fields to preserve the selection #}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
{% if form.non_field_errors %}
<div class="errors">
{{ form.non_field_errors }}
</div>
{% endif %}
<div style="margin: 20px 0;">
<fieldset class="module aligned">
<div class="form-row field-club_id">
<div>
<label for="{{ form.club_id.id_for_label }}" class="required">{{ form.club_id.label }}:</label>
<div class="related-widget-wrapper">
{{ form.club_id }}
</div>
{% if form.club_id.help_text %}
<div class="help">{{ form.club_id.help_text }}</div>
{% endif %}
{% if form.club_id.errors %}
<div class="errors">{{ form.club_id.errors }}</div>
{% endif %}
</div>
</div>
</fieldset>
</div>
<div class="submit-row" style="margin-top: 20px; padding: 10px; text-align: right;">
<input type="submit" name="apply" value="Set Club" class="default" style="margin-right: 10px;"/>
<a href="{% url 'admin:tournaments_event_changelist' %}" class="button cancel-link">Cancel</a>
</div>
</form>
{% endblock %}

@ -1,3 +1,5 @@
{% load tournament_tags %}
<nav class="margin10">
<a href="{% url 'index' %}" class="topmargin5 orange">Accueil</a>
@ -9,6 +11,14 @@
<a href="{% url 'tournament-live' tournament.id %}" class="topmargin5 orange">Live</a>
{% endif %}
{% if tournament.will_start_soon and request.user.is_authenticated %}
{% with user_team=tournament|get_user_team:request.user %}
{% if user_team %}
<a href="{% url 'team-details' tournament.id user_team.id %}" class="topmargin5 orange">Mon équipe</a>
{% endif %}
{% endwith %}
{% endif %}
{% if tournament.display_prog %}
<a href="{% url 'tournament-prog' tournament.id %}" class="topmargin5 orange">Programmation</a>
{% endif %}

@ -6,6 +6,11 @@ register = template.Library()
def get_player_status(tournament, user):
return tournament.get_player_registration_status_by_licence(user)
@register.filter
def get_user_team(tournament, user):
"""Get the team registration for a user in a tournament"""
return tournament.get_user_team_registration(user)
@register.filter
def lookup(dictionary, key):
"""Template filter to lookup dictionary values by key"""

@ -84,7 +84,7 @@ def index(request):
if club_id:
tournaments = tournaments_query(Q(end_date__isnull=True), club_id, True, 50)
else:
tournaments = tournaments_query(Q(end_date__isnull=True, start_date__gte=thirty_days_ago, start_date__lte=thirty_days_future), club_id, True, 50)
tournaments = tournaments_query(Q(end_date__isnull=True, start_date__gte=thirty_days_ago, start_date__lte=thirty_days_future), club_id, True, 100)
display_tournament = [t for t in tournaments if t.display_tournament()]
live = []

Loading…
Cancel
Save