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',
'channels_redis',
'django_filters',
'background_task',
]
AUTH_USER_MODEL = "tournaments.CustomUser"

@ -6,3 +6,19 @@ class TournamentsConfig(AppConfig):
def ready(self):
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
class RegistrationStatus(models.TextChoices):
WAITING = 'WAITING', 'Waiting'
PENDING = 'PENDING', 'Pending'
CONFIRMED = 'CONFIRMED', 'Confirmed'
PAID = 'PAID', 'Paid'
@ -45,7 +46,7 @@ class PlayerRegistration(SideStoreModel):
coach = models.BooleanField(default=False)
registered_online = models.BooleanField(default=False)
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=[
('UNPAID', 'Unpaid'),
('PAID', 'Paid'),

@ -325,6 +325,7 @@ class Tournament(BaseModel):
# Use the teams method to get sorted list of teams
all_teams = self.teams(True)
index = -1
print(all_teams)
# Find position of team in all teams list
for i, team in enumerate(all_teams):
if team.team_registration.id == team_registration.id:
@ -397,7 +398,7 @@ class Tournament(BaseModel):
complete_teams.append(team)
else:
waiting_teams.append(team)
wildcard_bracket = []
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):
@ -1062,6 +1063,8 @@ class Tournament(BaseModel):
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(self.get_selection_status_localized)
# Cible d'équipes
if self.team_count:
options.append(f"Maximum {self.team_count} équipes")
@ -1122,11 +1125,17 @@ class Tournament(BaseModel):
return False
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):
if self.supposedly_in_progress():
return OnlineRegistrationStatus.ENDED
if self.closed_registration_date is not None:
return OnlineRegistrationStatus.ENDED
return OnlineRegistrationStatus.WAITING_LIST_POSSIBLE
if self.end_date is not None:
return OnlineRegistrationStatus.ENDED_WITH_RESULTS
@ -1140,7 +1149,10 @@ class Tournament(BaseModel):
if self.registration_date_limit is not None:
timezoned_datetime = timezone.localtime(self.registration_date_limit)
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:
# Get all team registrations excluding walk_outs
@ -1174,6 +1186,15 @@ class Tournament(BaseModel):
return True
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 self.team_count is None:
return -1
@ -1290,7 +1311,37 @@ class Tournament(BaseModel):
def min_player_rank(self):
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):
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:
return None
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):
waiting_list_team = self.waiting_list_teams(teams)
print("waiting_list_team", waiting_list_team)
if waiting_list_team is None:
return None
if len(waiting_list_team) > 0:
@ -1458,7 +1510,7 @@ class Tournament(BaseModel):
# if not tournament.has_time_to_confirm:
# return None
if waiting_list_count <= 1:
if waiting_list_count == 0:
return None
# Configuration rules
@ -1479,7 +1531,7 @@ class Tournament(BaseModel):
BUSINESS_RULES = {
"hours": {
"start": 8, # 8:00
"end": 20, # 20:00
"end": 21, # 20:00
"default_confirmation_hour": 10 # When extending to next day
},
"days": {
@ -1641,6 +1693,12 @@ class TeamSummon:
}
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):
self.names = team_registration.team_names()
self.date = team_registration.local_call_date()

@ -3,6 +3,7 @@ from django.utils import timezone
from django.urls import reverse
from enum import Enum
from ..models.player_registration import RegistrationStatus
from ..models.tournament import TeamSortingType
class TeamEmailType(Enum):
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.")
else:
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"
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 %}
{% timezone tournament.timezone %}
{% with time_now=now time_to_confirm=team.get_confirmation_deadline %}
{% if time_to_confirm > time_now %}
{% with time_to_confirm=team.get_confirmation_deadline %}
{% 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 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_diff }}</span></p>
{% endwith %}
<p>Temps restant: <span class="countdown-timer">{{ time_to_confirm|timeuntil }}</span></p>
{% else %}
<p class="alert alert-danger">
Le délai de confirmation est expiré. Votre place a été proposée à une autre équipe.
</p>
{% endif %}
{% endif %}
{% endwith %}
{% endtimezone %}
{% endif %}

@ -1,10 +1,8 @@
from django.apps import apps
from django.contrib.auth import views as auth_views
from django.urls import include, path
from django.contrib import admin
from django.conf import settings
from django.conf.urls.static import static
from .forms import EmailOrUsernameAuthenticationForm, CustomPasswordChangeForm
from .forms import CustomPasswordChangeForm
from .custom_views import CustomLoginView
from . import views
@ -79,3 +77,8 @@ urlpatterns = [
path('activation-failed/', views.activation_failed, name='activation_failed'),
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