handle tasks

timetoconfirm
Raz 8 months ago
parent 48638e76f8
commit dc5a033e63
  1. 2
      padelclub_backend/settings.py
  2. 16
      tournaments/apps.py
  3. 10
      tournaments/management/commands/check_deadlines.py
  4. 63
      tournaments/management/commands/schedule_tasks.py
  5. 3
      tournaments/models/player_registration.py
  6. 68
      tournaments/models/tournament.py
  7. 8
      tournaments/services/email_service.py
  8. 89
      tournaments/tasks.py
  9. 11
      tournaments/templates/tournaments/tournament_info.html
  10. 11
      tournaments/urls.py

@ -49,6 +49,8 @@ INSTALLED_APPS = [
'qr_code', 'qr_code',
'channels_redis', 'channels_redis',
'django_filters', 'django_filters',
'background_task',
] ]
AUTH_USER_MODEL = "tournaments.CustomUser" AUTH_USER_MODEL = "tournaments.CustomUser"

@ -6,3 +6,19 @@ class TournamentsConfig(AppConfig):
def ready(self): def ready(self):
import tournaments.signals # This will ensure the signals are registered import tournaments.signals # This will ensure the signals are registered
# Start background tasks in production
# Schedule background tasks on startup
from django.conf import settings
if not settings.DEBUG: # Only in production
try:
from background_task.models import Task
from tournaments.tasks import check_confirmation_deadlines
# Clear existing tasks first to avoid duplicates
Task.objects.filter(task_name='tournaments.tasks.check_confirmation_deadlines').delete()
# Schedule the task to run every 30 minutes (1800 seconds)
check_confirmation_deadlines(repeat=1800)
except:
# Handle exceptions during startup
pass

@ -0,0 +1,10 @@
from django.core.management.base import BaseCommand
from tournaments.tasks import check_confirmation_deadlines
class Command(BaseCommand):
help = 'Run confirmation deadline check immediately'
def handle(self, *args, **options):
# Run the function directly (not through the task queue)
check_confirmation_deadlines(schedule=0)
self.stdout.write(self.style.SUCCESS('Successfully checked confirmation deadlines'))

@ -0,0 +1,63 @@
from django.core.management.base import BaseCommand
from tournaments.tasks import check_confirmation_deadlines
from django.utils import timezone
import datetime
import pytz
class Command(BaseCommand):
help = 'Schedule background tasks to run at :00 and :30 of every hour'
def handle(self, *args, **options):
# Clear existing tasks first to avoid duplicates
from background_task.models import Task
Task.objects.filter(task_name='tournaments.tasks.check_confirmation_deadlines').delete()
# Get the current timezone-aware time
now = timezone.now()
# Get local timezone for display purposes
local_timezone = timezone.get_current_timezone()
local_now = now.astimezone(local_timezone)
# Calculate time until next half-hour mark (either :00 or :30)
current_minute = local_now.minute
if current_minute < 30:
# Next run at XX:30:00
next_minute = 30
next_hour = local_now.hour
else:
# Next run at (XX+1):00:00
next_minute = 0
next_hour = local_now.hour + 1
# Create a datetime with exactly XX:30:00 or XX:00:00 in local time
first_run_local = local_now.replace(
hour=next_hour,
minute=next_minute + 1, #let the expiration time be off first
second=0,
microsecond=0
)
# Handle day rollover if needed
if first_run_local < local_now: # This would happen if we crossed midnight
first_run_local += datetime.timedelta(days=1)
# Calculate seconds from now until the first run
seconds_until_first_run = (first_run_local - local_now).total_seconds()
if seconds_until_first_run < 0:
seconds_until_first_run = 0 # If somehow negative, run immediately
# Schedule with seconds delay instead of a specific datetime
check_confirmation_deadlines(
schedule=int(seconds_until_first_run), # Delay in seconds before first run
repeat=1800 # 30 minutes in seconds
)
# Show the message with proper timezone info
local_timezone_name = local_timezone.tzname(local_now)
self.stdout.write(self.style.SUCCESS(
f'Task scheduled to first run at {first_run_local.strftime("%H:%M:%S")} {local_timezone_name} '
f'(in {int(seconds_until_first_run)} seconds) '
f'and then every 30 minutes'
))

@ -4,6 +4,7 @@ import uuid
from django.utils import timezone from django.utils import timezone
class RegistrationStatus(models.TextChoices): class RegistrationStatus(models.TextChoices):
WAITING = 'WAITING', 'Waiting'
PENDING = 'PENDING', 'Pending' PENDING = 'PENDING', 'Pending'
CONFIRMED = 'CONFIRMED', 'Confirmed' CONFIRMED = 'CONFIRMED', 'Confirmed'
PAID = 'PAID', 'Paid' PAID = 'PAID', 'Paid'
@ -45,7 +46,7 @@ class PlayerRegistration(SideStoreModel):
coach = models.BooleanField(default=False) coach = models.BooleanField(default=False)
registered_online = models.BooleanField(default=False) registered_online = models.BooleanField(default=False)
time_to_confirm = models.DateTimeField(null=True, blank=True) time_to_confirm = models.DateTimeField(null=True, blank=True)
registration_status = models.CharField(max_length=20, choices=RegistrationStatus.choices, default=RegistrationStatus.PENDING) registration_status = models.CharField(max_length=20, choices=RegistrationStatus.choices, default=RegistrationStatus.WAITING)
payment_status = models.CharField(max_length=20, default='UNPAID', choices=[ payment_status = models.CharField(max_length=20, default='UNPAID', choices=[
('UNPAID', 'Unpaid'), ('UNPAID', 'Unpaid'),
('PAID', 'Paid'), ('PAID', 'Paid'),

@ -325,6 +325,7 @@ class Tournament(BaseModel):
# Use the teams method to get sorted list of teams # Use the teams method to get sorted list of teams
all_teams = self.teams(True) all_teams = self.teams(True)
index = -1 index = -1
print(all_teams)
# Find position of team in all teams list # Find position of team in all teams list
for i, team in enumerate(all_teams): for i, team in enumerate(all_teams):
if team.team_registration.id == team_registration.id: if team.team_registration.id == team_registration.id:
@ -397,7 +398,7 @@ class Tournament(BaseModel):
complete_teams.append(team) complete_teams.append(team)
else: else:
waiting_teams.append(team) waiting_teams.append(team)
wildcard_bracket = []
return complete_teams, wildcard_bracket, wildcard_group_stage, waiting_teams return complete_teams, wildcard_bracket, wildcard_group_stage, waiting_teams
def sort_teams(self, include_waiting_list, complete_teams, wildcard_bracket, wildcard_group_stage, waiting_teams): def sort_teams(self, include_waiting_list, complete_teams, wildcard_bracket, wildcard_group_stage, waiting_teams):
@ -1062,6 +1063,8 @@ class Tournament(BaseModel):
date = formats.date_format(timezone.localtime(self.registration_date_limit), format='j F Y H:i') date = formats.date_format(timezone.localtime(self.registration_date_limit), format='j F Y H:i')
options.append(f"Clôture des inscriptions le {date}") options.append(f"Clôture des inscriptions le {date}")
options.append(self.get_selection_status_localized)
# Cible d'équipes # Cible d'équipes
if self.team_count: if self.team_count:
options.append(f"Maximum {self.team_count} équipes") options.append(f"Maximum {self.team_count} équipes")
@ -1122,11 +1125,17 @@ class Tournament(BaseModel):
return False return False
return True return True
def get_selection_status_localized(self):
if self.team_sorting == TeamSortingType.RANK:
return "La sélection se fait par le poids de l'équipe"
else:
return "La sélection se fait par date d'inscription"
def get_online_registration_status(self): def get_online_registration_status(self):
if self.supposedly_in_progress(): if self.supposedly_in_progress():
return OnlineRegistrationStatus.ENDED return OnlineRegistrationStatus.ENDED
if self.closed_registration_date is not None: if self.closed_registration_date is not None:
return OnlineRegistrationStatus.ENDED return OnlineRegistrationStatus.WAITING_LIST_POSSIBLE
if self.end_date is not None: if self.end_date is not None:
return OnlineRegistrationStatus.ENDED_WITH_RESULTS return OnlineRegistrationStatus.ENDED_WITH_RESULTS
@ -1140,7 +1149,10 @@ class Tournament(BaseModel):
if self.registration_date_limit is not None: if self.registration_date_limit is not None:
timezoned_datetime = timezone.localtime(self.registration_date_limit) timezoned_datetime = timezone.localtime(self.registration_date_limit)
if now > timezoned_datetime: if now > timezoned_datetime:
return OnlineRegistrationStatus.ENDED return OnlineRegistrationStatus.WAITING_LIST_POSSIBLE
if self.team_sorting == TeamSortingType.RANK:
return OnlineRegistrationStatus.OPEN
if self.team_count is not None: if self.team_count is not None:
# Get all team registrations excluding walk_outs # Get all team registrations excluding walk_outs
@ -1174,6 +1186,15 @@ class Tournament(BaseModel):
return True return True
def get_waiting_list_position(self): def get_waiting_list_position(self):
current_time = timezone.now()
current_time = current_time.astimezone(self.timezone())
local_registration_federal_limit = self.local_registration_federal_limit()
if self.team_sorting == TeamSortingType.RANK and local_registration_federal_limit is not None:
if current_time < local_registration_federal_limit:
return -1
else:
return 0
# If no target team count exists, no one goes to waiting list # If no target team count exists, no one goes to waiting list
if self.team_count is None: if self.team_count is None:
return -1 return -1
@ -1290,7 +1311,37 @@ class Tournament(BaseModel):
def min_player_rank(self): def min_player_rank(self):
return FederalLevelCategory.min_player_rank(self.federal_level_category, self.federal_category, self.federal_age_category) return FederalLevelCategory.min_player_rank(self.federal_level_category, self.federal_category, self.federal_age_category)
def local_registration_federal_limit(self):
if self.registration_date_limit is not None:
return self.registration_date_limit
if self.closed_registration_date is not None:
return self.closed_registration_date
local_start_date = self.local_start_date()
if local_start_date is None:
return None
if self.federal_level_category == FederalLevelCategory.P500:
# 7 days before at 23:59
return (local_start_date - timedelta(days=7)).replace(hour=23, minute=59, second=59)
elif self.federal_level_category in [FederalLevelCategory.P1000,
FederalLevelCategory.P1500,
FederalLevelCategory.P2000]:
# 14 days before at 23:59
return (local_start_date - timedelta(days=14)).replace(hour=23, minute=59, second=59)
return None
def waiting_list_teams(self, teams): def waiting_list_teams(self, teams):
current_time = timezone.now()
current_time = current_time.astimezone(self.timezone())
local_registration_federal_limit = self.local_registration_federal_limit()
if self.team_sorting == TeamSortingType.RANK and local_registration_federal_limit is not None:
if current_time < local_registration_federal_limit:
print("current_time < local_registration_federal_limit")
return None
if len(teams)<=self.team_count: if len(teams)<=self.team_count:
return None return None
waiting_teams = [team for team in teams if team.stage == "Attente"] waiting_teams = [team for team in teams if team.stage == "Attente"]
@ -1298,6 +1349,7 @@ class Tournament(BaseModel):
def first_waiting_list_team(self, teams): def first_waiting_list_team(self, teams):
waiting_list_team = self.waiting_list_teams(teams) waiting_list_team = self.waiting_list_teams(teams)
print("waiting_list_team", waiting_list_team)
if waiting_list_team is None: if waiting_list_team is None:
return None return None
if len(waiting_list_team) > 0: if len(waiting_list_team) > 0:
@ -1458,7 +1510,7 @@ class Tournament(BaseModel):
# if not tournament.has_time_to_confirm: # if not tournament.has_time_to_confirm:
# return None # return None
if waiting_list_count <= 1: if waiting_list_count == 0:
return None return None
# Configuration rules # Configuration rules
@ -1479,7 +1531,7 @@ class Tournament(BaseModel):
BUSINESS_RULES = { BUSINESS_RULES = {
"hours": { "hours": {
"start": 8, # 8:00 "start": 8, # 8:00
"end": 20, # 20:00 "end": 21, # 20:00
"default_confirmation_hour": 10 # When extending to next day "default_confirmation_hour": 10 # When extending to next day
}, },
"days": { "days": {
@ -1641,6 +1693,12 @@ class TeamSummon:
} }
class TeamItem: class TeamItem:
def __str__(self):
return f"TeamItem({self.team_registration.id}, names={self.names}, stage={self.stage})"
def __repr__(self):
return self.__str__()
def __init__(self, team_registration): def __init__(self, team_registration):
self.names = team_registration.team_names() self.names = team_registration.team_names()
self.date = team_registration.local_call_date() self.date = team_registration.local_call_date()

@ -3,6 +3,7 @@ from django.utils import timezone
from django.urls import reverse from django.urls import reverse
from enum import Enum from enum import Enum
from ..models.player_registration import RegistrationStatus from ..models.player_registration import RegistrationStatus
from ..models.tournament import TeamSortingType
class TeamEmailType(Enum): class TeamEmailType(Enum):
REGISTERED = "registered" REGISTERED = "registered"
@ -75,6 +76,13 @@ class TournamentEmailService:
body_parts.append(f"Votre inscription en liste d'attente du tournoi {tournament_details_str} est confirmée.") body_parts.append(f"Votre inscription en liste d'attente du tournoi {tournament_details_str} est confirmée.")
else: else:
body_parts.append(f"Votre inscription au tournoi {tournament_details_str} est confirmée.") body_parts.append(f"Votre inscription au tournoi {tournament_details_str} est confirmée.")
if tournament.team_sort == TeamSortingType.RANK:
cloture_date = tournament.local_registration_federal_limit().strftime("%d/%m/%Y à %H:%M")
loc = ""
if cloture_date is not None:
loc = f", prévu le {cloture_date}"
body_parts.append(f"Attention, la sélection définitive se fera par poids d'équipe à la clôture des inscriptions{loc}.")
absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info" absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info"
link_text = "informations sur le tournoi" link_text = "informations sur le tournoi"

@ -0,0 +1,89 @@
from background_task import background
from django.utils import timezone
from django.db import transaction
from .models import PlayerRegistration
from .models.player_registration import RegistrationStatus
from .services.email_service import TournamentEmailService, TeamEmailType
@background(schedule=1) # Run every 30 minutes (30*60 seconds)
def check_confirmation_deadlines():
"""
Periodic task to check for expired confirmation deadlines
and notify the next team in the waiting list.
"""
now = timezone.now()
print(f"[{now}] Running confirmation deadline check...")
# Find players with expired confirmation deadlines
expired_confirmations = PlayerRegistration.objects.filter(
registration_status=RegistrationStatus.PENDING,
registered_online=True,
team_registration__isnull=False
).select_related('team_registration', 'team_registration__tournament')
print(f"Found {expired_confirmations.count()} expired confirmations")
# Process each expired confirmation
processed_teams = set() # To avoid processing the same team multiple times
for player in expired_confirmations:
team_registration = player.team_registration
# Skip if we've already processed this team
if team_registration.id in processed_teams:
continue
processed_teams.add(team_registration.id)
tournament = team_registration.tournament
if not tournament or not tournament.enable_online_registration:
continue
teams = tournament.teams(True)
waiting_list_teams = tournament.waiting_list_teams(teams)
if waiting_list_teams is not None:
ttc = tournament.calculate_time_to_confirm(len(waiting_list_teams))
else:
ttc = None
first_waiting_list_team = tournament.first_waiting_list_team(teams)
# Process in a transaction to ensure atomic operations
with transaction.atomic():
# Get all players in this team and mark them as expired
team_players = PlayerRegistration.objects.filter(
team_registration=team_registration,
registered_online=True
)
should_update_team = False
should_send_mail = False
for team_player in team_players:
if team_player.time_to_confirm is None and first_waiting_list_team is not None:
team_registration.set_time_to_confirm(ttc)
team_player.save()
should_send_mail = True
print(team_player, "team_player.time_to_confirm is None and", ttc)
elif team_player.time_to_confirm is not None and now > team_player.time_to_confirm:
team_player.registration_status = RegistrationStatus.CANCELED
team_player.time_to_confirm = None
team_player.save()
team_registration.registration_date = now
print(team_player, "time_to_confirm = ", team_player.time_to_confirm)
should_update_team = True
# elif team_player.time_to_confirm is not None and team_player.time_to_confirm > ttc:
# team_player.registration_status = RegistrationStatus.PENDING
# team_player.time_to_confirm = ttc
# team_player.save()
# should_update_team = True
if should_send_mail:
TournamentEmailService.notify_team(
team_registration,
tournament,
TeamEmailType.OUT_OF_WAITING_LIST
)
if should_update_team:
team_registration.save()
print(f"Team {team_registration} confirmation expired in tournament {tournament.id}")

@ -51,19 +51,20 @@
{% if team.get_confirmation_deadline %} {% if team.get_confirmation_deadline %}
{% timezone tournament.timezone %} {% timezone tournament.timezone %}
{% with time_now=now time_to_confirm=team.get_confirmation_deadline %} {% with time_to_confirm=team.get_confirmation_deadline %}
{% if time_to_confirm > time_now %} {% now "Y-m-d H:i:s" as current_time_str %}
{% if time_to_confirm %}
{% if time_to_confirm|date:"Y-m-d H:i:s" > current_time_str %}
<p>Vous devez confirmer votre participation avant le:</p> <p>Vous devez confirmer votre participation avant le:</p>
<p class="semibold highlight">{{ time_to_confirm|date:"d/m/Y à H:i" }}</p> <p class="semibold highlight">{{ time_to_confirm|date:"d/m/Y à H:i" }}</p>
{% with time_diff=time_to_confirm|timeuntil:time_now %} <p>Temps restant: <span class="countdown-timer">{{ time_to_confirm|timeuntil }}</span></p>
<p>Temps restant: <span class="countdown-timer">{{ time_diff }}</span></p>
{% endwith %}
{% else %} {% else %}
<p class="alert alert-danger"> <p class="alert alert-danger">
Le délai de confirmation est expiré. Votre place a été proposée à une autre équipe. Le délai de confirmation est expiré. Votre place a été proposée à une autre équipe.
</p> </p>
{% endif %} {% endif %}
{% endif %}
{% endwith %} {% endwith %}
{% endtimezone %} {% endtimezone %}
{% endif %} {% endif %}

@ -1,10 +1,8 @@
from django.apps import apps
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from django.urls import include, path from django.urls import include, path
from django.contrib import admin
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from .forms import CustomPasswordChangeForm
from .forms import EmailOrUsernameAuthenticationForm, CustomPasswordChangeForm
from .custom_views import CustomLoginView from .custom_views import CustomLoginView
from . import views from . import views
@ -79,3 +77,8 @@ urlpatterns = [
path('activation-failed/', views.activation_failed, name='activation_failed'), path('activation-failed/', views.activation_failed, name='activation_failed'),
path('tournaments/<str:tournament_id>/confirm/', views.confirm_tournament_registration, name='confirm_tournament_registration'), path('tournaments/<str:tournament_id>/confirm/', views.confirm_tournament_registration, name='confirm_tournament_registration'),
] ]
# Start background tasks when Django starts
if apps.is_installed('django_background_tasks') and not settings.DEBUG:
from django_background_tasks import background_task_scheduler
background_task_scheduler.start()

Loading…
Cancel
Save