Source code for kata.models

from django.db import models
from django.core import validators
from django.core.exceptions import ValidationError
from django.urls import reverse

from registration.models import EventLink

from more_itertools import peekable
# Create your models here.


[docs]def validate_score(score): if score < 0 or score > 10: raise ValidationError('{} is not a number between 0 and 10.'.format(score))
[docs]class KataMatch(models.Model): eventlink = models.ForeignKey(EventLink, on_delete=models.PROTECT) round = models.ForeignKey('KataRound', on_delete=models.CASCADE) done = models.BooleanField(default=False) score1 = models.DecimalField(max_digits=3, decimal_places=1, validators=(validate_score,), blank=True, null=True) score2 = models.DecimalField(max_digits=3, decimal_places=1, validators=(validate_score,), blank=True, null=True) score3 = models.DecimalField(max_digits=3, decimal_places=1, validators=(validate_score,), blank=True, null=True) score4 = models.DecimalField(max_digits=3, decimal_places=1, validators=(validate_score,), blank=True, null=True) score5 = models.DecimalField(max_digits=3, decimal_places=1, validators=(validate_score,), blank=True, null=True) combined_score = models.DecimalField(max_digits=3, decimal_places=1, editable=False, blank=True, null=True) # Will be populated by save() tie_score = models.DecimalField(max_digits=3, decimal_places=1, editable=False, blank=True, null=True) # Will be populated by save() class Meta: ordering = ['-combined_score', '-tie_score'] def __str__(self): s = str(self.round) if self.round else "Orphan" s = s + " - " + str(self.eventlink) return s @property def scores(self): return (self.score1, self.score2, self.score3, self.score4, self.score5) @scores.setter def scores(self, value): (self.score1, self.score2, self.score3, self.score4, self.score5) = value
[docs] def save(self, *args, **kwargs): if self.round.locked: raise ValidationError("Can't modify match if round is locked.") scores = self.scores self.done = all((x is not None for x in scores)) if self.done: self.combined_score = sum(scores) - max(scores) - min(scores) self.tie_score = sum(scores) else: self.combined_score = None self.tie_score = None super().save(*args, **kwargs) self.round.match_callback(self)
[docs] def diff(self, other): try: if self.combined_score is not None and other.combined_score is not None: d = self.combined_score - other.combined_score if d == 0: d = self.tie_score - other.tie_score return d elif self.combined_score is None and other.combined_score is not None: d = -1 elif self.combined_score is not None and other.combined_score is None: d = 1 else: # both none d = 0 return d except AttributeError: return NotImplemented
def __eq__(self, other): return self.diff(other) == 0 # works with NotImplemented def __lt__(self, other): d = self.diff(other) if d == NotImplemented: return NotImplemented else: return d < 0 def __le__(self, other): d = self.diff(other) if d == NotImplemented: return NotImplemented else: return d <= 0 def __gt__(self, other): d = self.diff(other) if d == NotImplemented: return NotImplemented else: return d > 0 def __ge__(self, other): d = self.diff(other) if d == NotImplemented: return NotImplemented else: return d >= 0
[docs]class KataRound(models.Model): class Meta: ordering = ['round', 'order'] bracket = models.ForeignKey('KataBracket', on_delete=models.CASCADE) round = models.SmallIntegerField() order = models.SmallIntegerField() prev_round = models.ForeignKey('KataRound', blank=True, null=True, on_delete=models.CASCADE) locked = models.BooleanField(default=False) n_winner_needed = models.PositiveSmallIntegerField() def __str__(self): s = str(self.bracket) if self.bracket else "Orphan" s = s + " - round {}, {}".format(self.round, self.order) return s @property def done(self): return len(self.katamatch_set.filter(done=False)) == 0 @property def started(self): return len(self.katamatch_set.filter(done=True)) > 0 @property def matches(self): return self.katamatch_set.all()
[docs] def get_next_match(self): matches = self.katamatch_set.filter(done=False) if len(matches) >= 1: return matches[0] else: return None
[docs] def match_callback(self, match=None): # Match is none when adding or removing a person if self.locked: raise ValidationError("Can't modify round if locked.") n_done = len(self.katamatch_set.filter(done=True)) n_not_started = len(self.katamatch_set.filter(done=False)) # Lock predecessor rounds if self.prev_round is not None: old_locked = self.prev_round.locked self.prev_round.locked = n_done > 0 if self.prev_round.locked != old_locked: self.prev_round.save() # Clear child rounds. If any of them had started, we would be locked. children = self.kataround_set.all() if len(children) > 0: children.delete() # Spawn child rounds if self.done: batch = [] n_winner = 0 order = 0 itr = peekable(self.katamatch_set.all()) for m in itr: # Sorted already batch.append(m) next_m = itr.peek(None) if next_m is None or next_m < m: if len(batch) == 1: pass # Have winner. Don't actually care who they are. else: # Have tie round = KataRound(bracket=self.bracket, prev_round=self, round=self.round+1, order=order, n_winner_needed=min(len(batch), self.n_winner_needed - n_winner)) round.save() order -= 1 for p in batch: m = KataMatch(eventlink=p.eventlink, round=round) m.save() n_winner += len(batch) del batch[:] if n_winner >= self.n_winner_needed: break
[docs]class KataBracket(models.Model): division = models.ForeignKey('registration.Division', null=True, on_delete=models.CASCADE) def __str__(self): return str(self.division) + " - Kata"
[docs] def get_absolute_url(self): return reverse('kata:bracket', args=[self.id])
@property def n_round(self): return self.kataround_set.all().aggregate(models.Max('round'))['round__max'] + 1 @property def rounds(self): return self.kataround_set.all()
[docs] def get_next_match(self): for r in self.rounds: match = r.get_next_match() if match is not None: return match return None
[docs] def build(self, people): round = KataRound(bracket=self, round=0, order=0, n_winner_needed=min(3, len(people))) round.save() for p in people: m = KataMatch(eventlink=p, round=round) m.save()
[docs] def add_person(self, p): round = self.kataround_set.get(round=0) if round.locked: raise ValueError("Round is locked.") m = KataMatch(eventlink=p, round=round) m.save() round.match_callback(m)
[docs] def get_people(self): # All participants are in the first round round = self.kataround_set.get(round=0) return EventLink.objects.filter(katamatch__round=round)
[docs] def get_winners(self): n_round = self.n_round points = {p: [0] * (2*n_round + 1) for p in self.get_people()} n_winner = min(len(points), 3) matches = KataMatch.objects.filter(round__bracket=self, done=True) for m in matches: r = m.round.round points[m.eventlink][2*r] += m.combined_score points[m.eventlink][2*r+1] += m.tie_score points[m.eventlink][2*n_round] += 1 points = [(p, score) for (p, score) in points.items() if score[-1] > 0] for (p, score) in points: del score[-1] points = sorted(points, key=lambda x: x[1], reverse=True) prev_score = None rank = 0 n = 0 n_tie = 0 winners = [] for (p, score) in points: n_tie += 1 if score != prev_score: rank += n_tie n_tie = 0 if rank > n_winner: break winners.append((rank, p)) n += 1 prev_score = score for rank in range(n+1, n_winner+1): winners.append((rank, None)) return winners