Files
kasu/src/mahjong_ranking/models.py

684 lines
28 KiB
Python

# -*- encoding: utf-8 -*-
# TODO: Rankings archiv Flag erstellen, womit sie nicht mehr neuberechnet
# werden dürfen.
from __future__ import division
from datetime import datetime, time
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext as _
from events.models import Event
from . import DAN_RANKS_DICT, LOGGER, set_dirty
from . import managers
kyu_dan_rankings = set()
ladder_rankings = set()
ladder_seasons = set()
class EventRanking(models.Model):
"""
Event Rankings funktionieren genauso wie Season Rankings.
Sie beschränken sich aber auf einen Event und werden nur dann angestossen,
wenn der Event als Turnier markiert wurde.
"""
user = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.PROTECT)
event = models.ForeignKey(Event, on_delete=models.CASCADE)
placement = models.PositiveIntegerField(blank=True, null=True)
avg_placement = models.FloatField(default=4)
avg_score = models.FloatField(default=0)
hanchan_count = models.PositiveIntegerField(default=0)
good_hanchans = models.PositiveIntegerField(default=0)
won_hanchans = models.PositiveIntegerField(default=0)
class Meta(object):
ordering = ('placement', 'avg_placement', '-avg_score',)
def get_absolute_url(self):
return reverse('event-ranking', args=[self.event_id])
def recalculate(self):
"""
Berechnet die durchschnittliche Platzierung und Punkte, u.v.m. neu.
Diese Daten werden benötigt, um die Platzierung zu erstellen. Sie
können zwar sehr leicht errechnet werden, es macht trotzdem Sinn
sie zwischenzuspeichern.
"""
LOGGER.info(
u'Recalculate EventRanking for Player %s in %s',
self.user, self.event.name
)
sum_placement = 0.0
sum_score = 0.0
event_hanchans = Hanchan.objects.confirmed(
user=self.user_id,
event=self.event_id
)
self.hanchan_count = event_hanchans.count()
self.won_hanchans = 0
self.good_hanchans = 0
if self.hanchan_count <= 0:
self.delete()
else:
for hanchan in event_hanchans:
hanchan.get_playerdata(user=self.user)
sum_placement += hanchan.placement
sum_score += hanchan.game_score
self.won_hanchans += 1 if hanchan.placement == 1 else 0
self.good_hanchans += 1 if hanchan.placement == 2 else 0
self.avg_placement = sum_placement / self.hanchan_count
self.avg_score = sum_score / self.hanchan_count
self.save(force_update=True)
class Hanchan(models.Model):
"""
Ein komplette Runde Mahjong, die aus genau 4 Spielern bestehen muss.
Es werden aber noch andere Tests durchgeführt, ob sie gültig ist.
Außerdem gehört jede Hanchan zu einer Veranstaltung.
"""
event = models.ForeignKey(Event, on_delete=models.CASCADE)
start = models.DateTimeField(
_('Start'),
help_text=_('This is crucial to get the right Hanchans that scores')
)
player1 = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
related_name='user_hanchan+',
verbose_name=_('Player 1'))
player1_input_score = models.IntegerField(_('Score'))
player1_game_score = models.PositiveIntegerField(
_('Score'), default=0, editable=False)
player1_placement = models.PositiveSmallIntegerField(
default=0, editable=False)
player1_kyu_points = models.SmallIntegerField(
blank=True, null=True, editable=False)
player1_dan_points = models.SmallIntegerField(
blank=True, null=True, editable=False)
player1_bonus_points = models.SmallIntegerField(
blank=True, null=True, editable=False)
player1_comment = models.CharField(
_('Comment'), blank=True, max_length=255, editable=False)
player2 = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
related_name='user_hanchan+',
verbose_name=_('Player 2'))
player2_input_score = models.IntegerField(_('Score'))
player2_game_score = models.PositiveIntegerField(
_('Score'), default=0, editable=False)
player2_placement = models.PositiveSmallIntegerField(
default=0, editable=False)
player2_kyu_points = models.SmallIntegerField(
blank=True, null=True, editable=False)
player2_dan_points = models.SmallIntegerField(
blank=True, null=True, editable=False)
player2_bonus_points = models.SmallIntegerField(
blank=True, null=True, editable=False)
player2_comment = models.CharField(
_('Comment'), blank=True, max_length=255, editable=False)
player3 = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
related_name='user_hanchan+',
verbose_name=_('Player 3'))
player3_input_score = models.IntegerField(_('Score'))
player3_game_score = models.PositiveIntegerField(
_('Score'), default=0, editable=False)
player3_placement = models.PositiveSmallIntegerField(
default=0, editable=False)
player3_kyu_points = models.SmallIntegerField(
blank=True, null=True, editable=False)
player3_dan_points = models.SmallIntegerField(
blank=True, null=True, editable=False)
player3_bonus_points = models.SmallIntegerField(
blank=True, null=True, editable=False)
player3_comment = models.CharField(
_('Comment'), blank=True, max_length=255, editable=False)
player4 = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
related_name='user_hanchan+',
verbose_name=_('Player 4'))
player4_input_score = models.IntegerField(_('Score'))
player4_game_score = models.PositiveIntegerField(
_('Score'), default=0, editable=False)
player4_placement = models.PositiveSmallIntegerField(
default=0, editable=False)
player4_kyu_points = models.SmallIntegerField(
blank=True, null=True, editable=False)
player4_dan_points = models.SmallIntegerField(
blank=True, null=True, editable=False)
player4_bonus_points = models.SmallIntegerField(
blank=True, null=True, editable=False)
player4_comment = models.CharField(
_('Comment'), blank=True, max_length=255, editable=False)
comment = models.TextField(_('Comment'), blank=True)
confirmed = models.BooleanField(
_('Has been Confirmed'), default=True,
help_text=_('Only valid and confirmed Hanchans will be counted in the '
'rating.')
)
player_names = models.CharField(max_length=255, editable=False)
season = models.PositiveSmallIntegerField(
_('Season'), editable=False, db_index=True)
objects = managers.HanchanManager()
class Meta(object):
ordering = ('-start',)
verbose_name = _(u'Hanchan')
verbose_name_plural = _(u'Hanchans')
def __str__(self):
return _("Hanchan from {0:%Y-%m-%d} at {0:%H:%M} with {1}").format(
self.start, self.player_names
)
def clean(self):
"""
Check if 4 different Players are attending the Game,
if the Game between the opening hours and the Hanchan has a total of
100.000 Points.
"""
errors = {}
score_sum = 0
player_set = set()
try:
self.clean_fields()
except ValidationError as exception:
exception.update_error_dict(errors)
return
for i in range(1, 5):
player = getattr(self, 'player%d' % i)
input_score = getattr(self, 'player%d_input_score' % i)
score_sum += input_score
game_score = input_score if input_score > 0 else 0
if player in player_set:
errors['player%d' % i] = _(
"%s can't attend the same game multiple times") % player
else:
player_set.add(player)
setattr(self, 'player%d_game_score' % i, game_score)
# Check if the game was played during the event
if self.start > timezone.now():
errors['start'] = _(
"Games in the future may not be added, Dr. Brown")
elif not (self.event.start <= self.start <= self.event.end):
errors['start'] = _("Only games during the event are allowed")
elif score_sum < 100000:
errors['player4_input_score'] = _(
'Gamescore is lower then 100.000 Pt.')
elif score_sum > 100000:
errors['player4_input_score'] = _('Gamescore is over 100.000 Pt.')
if errors:
raise ValidationError(errors)
def compute_player_placements(self):
u"""
Bestimmt die Platzierung eines der Spieler einer Hanchan und speichert
diese beim jeweiligen Spieler ab.
"""
LOGGER.debug("Berechne die Platzierungen neu...")
player_names = []
other_player_placement = 0
other_player_game_score = 0
placement = 1
player_nr = 1
for player in self.player_list:
if player['game_score'] == other_player_game_score:
player['placement'] = other_player_placement
else:
player['placement'] = placement
setattr(self, "player%d" % player_nr, player['user'])
setattr(self, "player%d_input_score" %
player_nr, player['input_score'])
setattr(self, "player%d_game_score" %
player_nr, player['game_score'])
setattr(self, "player%d_placement" %
player_nr, player['placement'])
setattr(self, "player%d_kyu_points" %
player_nr, player['kyu_points'])
setattr(self, "player%d_dan_points" %
player_nr, player['dan_points'])
setattr(self, "player%d_bonus_points" %
player_nr, player['bonus_points'])
setattr(self, "player%d_comment" % player_nr, player['comment'])
other_player_placement = player['placement']
other_player_game_score = player['game_score']
player_names.append(player['user'].username)
player_nr += 1
placement += 1
self.player_names = ', '.join(player_names)
def get_absolute_url(self):
return "{url:s}#{id:d}".format(
url=reverse('event-hanchan-list', kwargs={'event': self.event_id}),
id=self.pk
)
def get_playerdata(self, user):
"""small workaround to access score, placement and points of a
specific user prominent in the user templates"""
for player in ('player1', 'player2', 'player3', 'player4'):
if getattr(self, player) == user:
self.user = user
self.input_score = getattr(self, '%s_input_score' % player)
self.game_score = getattr(self, '%s_game_score' % player)
self.placement = getattr(self, '%s_placement' % player)
self.kyu_points = getattr(self, '%s_kyu_points' % player)
self.dan_points = getattr(self, '%s_dan_points' % player)
self.bonus_points = getattr(self, '%s_bonus_points' % player)
self.player_comment = getattr(self, '%s_comment' % player)
def update_player_data(self, user, **kwargs):
"""to access scores and placement of a specific user from templates"""
for player in ('player1', 'player2', 'player3', 'player4'):
if getattr(self, player) == user:
setattr(self, '%s_input_score' % player, self.input_score)
setattr(self, '%s_game_score' % player, self.game_score)
setattr(self, '%s_placement' % player, self.placement)
setattr(self, '%s_kyu_points' % player, self.kyu_points)
setattr(self, '%s_dan_points' % player, self.dan_points)
setattr(self, '%s_bonus_points' % player, self.bonus_points)
setattr(self, '%s_comment' % player, self.player_comment)
@property
def player_list(self):
player_list = []
for i in range(1, 5):
player_list.append({
'user': getattr(self, 'player%d' % i),
'input_score': getattr(self, 'player%d_input_score' % i),
'game_score': getattr(self, 'player%d_game_score' % i),
'placement': getattr(self, 'player%d_placement' % i),
'kyu_points': getattr(self, 'player%d_kyu_points' % i),
'dan_points': getattr(self, 'player%d_dan_points' % i),
'bonus_points': getattr(self, 'player%d_bonus_points' % i),
'comment': getattr(self, 'player%d_comment' % i),
})
# sort player by Score:
return sorted(player_list, key=lambda player: player['input_score'],
reverse=True)
def save(self, recalculate=True, **kwargs):
self.season = self.event.mahjong_season or self.start.year
self.full_clean()
if recalculate:
self.compute_player_placements()
update_ranking(sender=Hanchan, instance=self)
return models.Model.save(self, **kwargs)
class KyuDanRanking(models.Model):
u"""
Die Einstufung des Spielers im Kyu bzw. Dan System.
Im Gegensatz zum Ladder Ranking ist das nicht Saison gebunden.
Deswegen läuft es getrennt.
"""
user = models.OneToOneField(settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)
dan = models.PositiveSmallIntegerField(blank=True, null=True)
dan_points = models.PositiveIntegerField(default=0)
max_dan_points = models.PositiveIntegerField(default=0)
kyu = models.PositiveSmallIntegerField(default=10, blank=True, null=True)
kyu_points = models.PositiveIntegerField(default=0)
won_hanchans = models.PositiveIntegerField(default=0)
good_hanchans = models.PositiveIntegerField(default=0)
hanchan_count = models.PositiveIntegerField(default=0)
legacy_date = models.DateField(blank=True, null=True)
legacy_dan = models.PositiveSmallIntegerField(blank=True, null=True)
legacy_dan_points = models.PositiveIntegerField(blank=True, null=True)
legacy_max_dan_points = models.PositiveIntegerField(default=0)
legacy_kyu = models.PositiveSmallIntegerField(blank=True, null=True)
legacy_kyu_points = models.PositiveIntegerField(blank=True, null=True)
legacy_hanchan_count = models.PositiveIntegerField(blank=True, null=True)
legacy_good_hanchans = models.PositiveIntegerField(blank=True, null=True)
legacy_won_hanchans = models.PositiveIntegerField(blank=True, null=True)
wins_in_a_row = models.PositiveIntegerField(default=0)
last_hanchan_date = models.DateTimeField(blank=True, null=True)
objects = managers.KyuDanRankingManager()
class Meta(object):
ordering = (models.F("dan").desc(nulls_last=True),
'-dan_points', '-kyu_points',
'-won_hanchans', '-good_hanchans',
'-last_hanchan_date')
verbose_name = _(u'Kyū/Dan Ranking')
verbose_name_plural = _(u'Kyū/Dan Rankings')
@property
def rank(self):
if self.dan is not None:
return "{0:d}. Dan".format(self.dan)
else:
return "{0:d}. Kyū".format(self.kyu or 10)
@property
def points(self):
return self.dan_points if self.dan is not None else self.kyu_points
def __str__(self):
if self.dan is not None:
return u"%s - %d. Dan" % (self.user.username, self.dan or 1)
else:
return u"%s - %d. Kyū" % (self.user.username, self.kyu or 10)
def append_3_in_a_row_bonuspoints(self, hanchan):
u"""
Wenn der Spieler 3 Siege in Folge hatte, bekommt er so viele Punkte
das er einen Dan Rang aufsteigt. Dies wird als Kommentar abgespeichert,
um es besser nachvollziehen zu können.
"""
if not self.dan or not settings.DAN_3_WINS_IN_A_ROW:
return
if hanchan.placement == 1:
self.wins_in_a_row += 1
else:
self.wins_in_a_row = 0
return
if self.wins_in_a_row >= 3 and self.dan < 9:
LOGGER.info(
'adding bonuspoints for 3 wins in a row for %s', self.user)
new_dan_rank = self.dan + 1
new_dan_points = DAN_RANKS_DICT[new_dan_rank] + 1
bonus_points = new_dan_points - self.dan_points
LOGGER.debug("Stats for %s:", self.user)
LOGGER.debug("current dan_points: %d", self.dan_points)
LOGGER.debug("current dan: %d", self.dan)
LOGGER.debug(
"min required points for the next dan: %d", new_dan_points)
LOGGER.debug("bonus points to add: %d", bonus_points)
hanchan.dan_points += bonus_points
hanchan.bonus_points += bonus_points
hanchan.player_comment += '3 Siege in Folge: +%d Punkte auf den ' \
'%d. Dan. ' % (
bonus_points, new_dan_rank)
self.dan_points += bonus_points
self.wins_in_a_row = 0
self.update_rank()
def append_tournament_bonuspoints(self, hanchan):
"""
Prüft, ob es die letzte Hanchan in einem Turnier war. Wenn ja, werden
bei Bedarf Bonuspunkte vergeben, falls der Spieler das Turnier
gewonnen hat.
:param hanchan: Ein Player Objekt
"""
bonus_points = 0
hanchans_this_event = Hanchan.objects.user_hanchans(
user=self.user, event=hanchan.event
).order_by('-start')
last_hanchan_this_event = hanchans_this_event[0]
# Das braucht nur am Ende eines Turnieres gemacht werden.
if hanchan != last_hanchan_this_event:
return False
event_ranking = EventRanking.objects.get(
user=self.user,
event=hanchan.event
)
if event_ranking.placement == 1:
bonus_points += settings.TOURNAMENT_WIN_BONUSPOINTS
hanchan.player_comment += u'+{0:d} Punkte Turnier gewonnen. '.format(
settings.TOURNAMENT_WIN_BONUSPOINTS)
if event_ranking.avg_placement == 1:
bonus_points += settings.TOURNAMENT_FLAWLESS_VICTORY_BONUSPOINTS
hanchan.player_comment += u'+{0:d} Pkt: alle Spiele des Turnieres gewonnen. '.format(
settings.TOURNAMENT_FLAWLESS_VICTORY_BONUSPOINTS)
if bonus_points and self.dan:
hanchan.dan_points += bonus_points
self.dan_points += bonus_points
elif bonus_points:
hanchan.kyu_points += bonus_points
self.kyu_points += bonus_points
hanchan.bonus_points += bonus_points
return True
def get_absolute_url(self):
if self.dan or self.dan_points > 0:
return reverse('player-dan-score', args=[self.user.username])
else:
return reverse('player-kyu-score', args=[self.user.username])
def calculate(self, since=None, until=None, force_recalc=False):
"""
Fetches all valid Hanchans from this Player and recalculates his
Kyu/Dan Ranking.
"""
valid_hanchans = Hanchan.objects.confirmed(user=self.user)
valid_hanchans = valid_hanchans.order_by('start')
if since and self.last_hanchan_date and since < self.last_hanchan_date:
force_recalc = True
if until and self.last_hanchan_date and until < self.last_hanchan_date:
force_recalc = True
if force_recalc:
# Setze alles auf die legacy Werte und berechne alles von neuem.
self.dan = self.legacy_dan
self.dan_points = self.legacy_dan_points or 0
self.max_dan_points = self.legacy_max_dan_points or 0
self.kyu = self.legacy_kyu
self.kyu_points = self.legacy_kyu_points or 0
self.hanchan_count = self.legacy_hanchan_count or 0
self.good_hanchans = self.legacy_good_hanchans or 0
self.won_hanchans = self.legacy_won_hanchans or 0
self.last_hanchan_date = None
self.update_rank()
since = timezone.make_aware(datetime.combine(
self.legacy_date,
time(23, 59, 59))) if self.legacy_date else None
elif self.last_hanchan_date:
since = self.last_hanchan_date
elif self.legacy_date:
since = timezone.make_aware(
datetime.combine(self.legacy_date, time(0, 0, 0))
)
if since:
valid_hanchans = valid_hanchans.filter(start__gt=since)
else:
since = valid_hanchans.aggregate(since=models.Min("start"))["since"]
if until:
valid_hanchans = valid_hanchans.filter(start__lte=until)
else:
until = valid_hanchans.aggregate(until=models.Max("start"))["until"]
if valid_hanchans.count() > 0:
LOGGER.info(f"recalculating Kyu/Dan points for {self.user} ({since:%Y-%m-%d} - {until:%Y-%m-%d})...")
else:
LOGGER.info(f"No new valid Hanchans for {self.user}...")
for hanchan in valid_hanchans:
self.hanchan_count += 1
LOGGER.info(f"{self.user} Hanchan no. {self.hanchan_count} from {hanchan.start}")
hanchan.get_playerdata(self.user)
if since and hanchan.start < since:
LOGGER.info(hanchan, "<", since, "no recalc")
self.dan_points += hanchan.dan_points or 0
self.kyu_points += hanchan.kyu_points or 0
self.update_rank()
else:
hanchan.bonus_points = 0
hanchan.player_comment = ""
self.update_hanchan_points(hanchan)
if hanchan.event.mahjong_tournament:
self.append_tournament_bonuspoints(hanchan)
self.update_rank()
self.append_3_in_a_row_bonuspoints(hanchan)
hanchan.update_player_data(self.user)
hanchan.save(recalculate=False)
self.won_hanchans += 1 if hanchan.placement == 1 else 0
self.good_hanchans += 1 if hanchan.placement == 2 else 0
self.last_hanchan_date = hanchan.start
self.save(force_update=True)
def update_hanchan_points(self, hanchan):
"""
Berechne die Kyu bzw. Dan Punkte für eine Hanchan neu.
:type hanchan: Hanchan
:param hanchan: das Player-Objekt, welches neu berechnet werden soll.
"""
hanchan.kyu_points = None
hanchan.dan_points = None
if hanchan.event.mahjong_tournament and settings.TOURNAMENT_POINT_SYSTEM:
"""Für Turniere gelten andere Regeln zur Punktevergabe:
1. Platz 4 Punkte
2. Platz 3 Punkte
3. Platz 2 Punkte
4. Platz 1 Punkt"""
tourney_points = 4 - hanchan.placement
if self.dan:
hanchan.dan_points = tourney_points
else:
hanchan.kyu_points = tourney_points
elif self.dan:
if hanchan.game_score >= 60000:
hanchan.dan_points = 3
elif hanchan.game_score == 0:
hanchan.dan_points = -3
elif hanchan.placement == 1:
hanchan.dan_points = 2
elif hanchan.placement == 2:
hanchan.dan_points = 1
elif hanchan.placement == 3:
hanchan.dan_points = -1
elif hanchan.placement == 4:
hanchan.dan_points = -2
# otherwise player must be in the kyu ranking
elif hanchan.game_score >= 60000:
hanchan.kyu_points = 3
elif hanchan.game_score >= 30000:
hanchan.kyu_points = 1
elif hanchan.input_score < 10000:
hanchan.kyu_points = -1
else:
hanchan.kyu_points = 0
# Add the hanchans points to the players points
if self.dan:
# Only substract so much points that player has 0 Points:
if self.dan_points + hanchan.dan_points < 0:
hanchan.player_comment = 'Spieler unterschreitet 0 Punkte.' \
'(Original {} Punkte)'.format(hanchan.dan_points)
hanchan.dan_points -= (self.dan_points + hanchan.dan_points)
self.dan_points += hanchan.dan_points
else:
# Only substract so much points that player has 0 Points:
if self.kyu_points + hanchan.kyu_points < 0:
hanchan.player_comment = 'Spieler unterschreitet 0 Punkte.' \
'(Original {} Punkte)'.format(hanchan.kyu_points)
hanchan.kyu_points -= (self.kyu_points + hanchan.kyu_points)
self.kyu_points += hanchan.kyu_points
def update_rank(self):
# Update Dan ranking:
if self.dan or self.dan_points > 0:
if settings.DAN_ALLOW_DROP_DOWN:
self.dan = max((dan for min_points, dan in settings.DAN_RANKS
if self.dan_points > min_points))
else:
self.max_dan_points = max(self.max_dan_points, self.dan_points)
self.dan = max((dan for min_points, dan in settings.DAN_RANKS
if self.max_dan_points > min_points))
# jump from Kyu to Dan
elif self.kyu_points > 50:
self.dan = 1
self.dan_points = 0
self.kyu = None
self.kyu_points = 0
self.wins_in_a_row = 0
# update Kyu ranking_
else:
self.kyu = min((kyu for min_points, kyu in settings.KYU_RANKS
if self.kyu_points > min_points))
class SeasonRanking(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.PROTECT)
season = models.PositiveSmallIntegerField(_('Season'))
placement = models.PositiveIntegerField(blank=True, null=True)
avg_placement = models.FloatField(blank=True, null=True)
avg_score = models.FloatField(blank=True, null=True)
hanchan_count = models.PositiveIntegerField(default=0)
good_hanchans = models.PositiveIntegerField(default=0)
won_hanchans = models.PositiveIntegerField(default=0)
objects = managers.SeasonRankingManager()
class Meta(object):
ordering = ('placement', 'avg_placement', '-avg_score',)
def get_absolute_url(self):
return reverse('player-ladder-score', args=[self.user.username])
def recalculate(self, until=None):
season_hanchans = Hanchan.objects.season_hanchans(
user=self.user, season=self.season, until=until)
sum_placement = 0
sum_score = 0
self.placement = None
self.hanchan_count = season_hanchans.count()
self.good_hanchans = 0
self.won_hanchans = 0
LOGGER.info(
u'Recalculate LadderRanking for Player %s in Season %s',
self.user, self.season)
for hanchan in season_hanchans:
sum_placement += hanchan.placement
sum_score += hanchan.game_score
self.won_hanchans += 1 if hanchan.placement == 1 else 0
self.good_hanchans += 1 if hanchan.placement == 2 else 0
try:
self.avg_placement = sum_placement / self.hanchan_count
self.avg_score = sum_score / self.hanchan_count
except ZeroDivisionError:
self.avg_placement = None
self.avg_score = None
self.save(force_update=True)
def update_ranking(sender, instance, **kwargs):
for user in (
instance.player1, instance.player2, instance.player3,
instance.player4):
LOGGER.debug(
"marking %(user)s's kyu/dan for recalculation since %(start)s",
{'user': user, 'start': str(instance.start)}
)
set_dirty(user=user.id, hanchan_date=instance.start)
LOGGER.debug("marking event %s for recalculation.", instance.event)
set_dirty(event=instance.event_id, user=user.id)
if instance.season:
LOGGER.debug(
"marking %s's ladder %i season for recalculation.",
user, instance.season
)
set_dirty(user=user.id, season=instance.season)
LOGGER.debug("marking season %d for recalculation.", instance.season)
set_dirty(season=instance.season)
models.signals.pre_delete.connect(update_ranking, sender=Hanchan)