"""Models for the mainapp.""" from django.db import models from django.utils import timezone # region Anglers & Groups class Angler(models.Model): """A participant of one or more events. Attributes: id (int): Automatically incrementing identifier number. name (str): Full name or label describing the angler. redact (bool): Determines if the `name` attr should be redacted. """ id = models.AutoField(primary_key=True) name = models.CharField(max_length=128) redact = models.BooleanField() class Meta: verbose_name = "angler" verbose_name_plural = "anglers" def __str__(self): return self.name class AnglerGroup(models.Model): """A group of one or more anglers. Attributes: id (int): Automatically incrementing identifier number. name (str): A human-readable label to identify this group. anglers (list of `Angler`): The members of this group. type (int): An enum-like value representing the type of group. """ id = models.AutoField(primary_key=True) name = models.CharField(max_length=128) anglers = models.ManyToManyField(to=Angler) TYPES = ( (0, "Set"), # Collection of commonly used anglers to aide in their selection for a match or league. (1, "Team"), # Collection of anglers that acts as a single competing unit in a match or league. (2, "Pair") # Two anglers that acts as a single competing unit in a match or league. ) type = models.PositiveSmallIntegerField(choices=TYPES) class Meta: verbose_name = "angler group" verbose_name_plural = "angler groups" def __str__(self): return f"{self.name} ({self.anglers.count} anglers)" # region Venues & Waters class VenueAddress(models.Model): id = models.AutoField(primary_key=True) street_number = models.IntegerField(null=True, blank=True) street_address = models.CharField(max_length=256) town = models.CharField(max_length=256) provence = models.CharField(max_length=256) post_code = models.CharField(max_length=32) satnav_post_code = models.CharField(max_length=32, null=True, blank=True) country = models.CharField(max_length=128) latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) class Meta: verbose_name = "venue address" verbose_name_plural = "venue addresses" def __str__(self): return f"{self.street_address}, {self.town} ({self.country})" class VenueContacts(models.Model): id = models.AutoField(primary_key=True) phone_number = models.CharField(max_length=64, null=True, blank=True) email_address = models.EmailField(null=True, blank=True) website_url = models.URLField(null=True, blank=True) facebook_url = models.URLField(null=True, blank=True) twitter_url = models.URLField(null=True, blank=True) instagram_url = models.URLField(null=True, blank=True) class Meta: verbose_name = "venue contacts" verbose_name_plural = "venue contacts" def __str__(self): return self.email_address or str(self.id) class Venue(models.Model): id = models.AutoField(primary_key=True) name = models.CharField(max_length=128) description = models.CharField(max_length=384, null=True, blank=True) extra_notes = models.CharField(max_length=1028, null=True, blank=True) created_at = models.DateTimeField(default=timezone.now, editable=False) updated_at = models.DateTimeField(default=timezone.now, editable=False) profile_picture = models.ImageField(null=True, blank=True) banner_picture = models.ImageField(null=True, blank=True) class Types: FISHERY = 0 CLUB = 1 PRIVATE = 2 type = models.PositiveSmallIntegerField( choices=( (Types.FISHERY, "Fishery"), (Types.CLUB, "Club"), (Types.PRIVATE, "Private") ) ) address = models.ForeignKey(to=VenueAddress, on_delete=models.CASCADE) contacts = models.ForeignKey(to=VenueContacts, on_delete=models.CASCADE) class Meta: verbose_name = "venue" verbose_name_plural = "venues" def __str__(self): return self.name def save(self, *args, **kwargs): self.updated_at = timezone.now() return super().save(*args, **kwargs) @property def waters(self): """Returns all waters belonging to this venue.""" return Waters.objects.filter(venue=self) class Waters(models.Model): """A body of water belonging to a particular venue. Attributes: id (int): Automatically incrementing identifier number. name (str): A human-readable label to identify this item. pegs_from (int): The lowest peg number available. pegs_to (int): The highest peg number available. map (file): An image map showing the body of water. venue (`Venue`): The real world venue where this waters can be found. type (int): The type of waters represented (canal, river, etc...) fish_type (int): The fish categories that can be found in this water. """ id = models.AutoField(primary_key=True) name = models.CharField(max_length=128) pegs_from = models.IntegerField() pegs_to = models.IntegerField() map = models.ImageField(null=True, blank=True) venue = models.ForeignKey(to=Venue, on_delete=models.CASCADE) class Types: COMMERCIAL = 0 NATURAL_STILL = 1 CANAL = 2 RIVER = 3 LOCH = 4 type = models.PositiveSmallIntegerField( choices=( (Types.COMMERCIAL, "Commercial Water"), (Types.NATURAL_STILL, "Natural Still Water"), (Types.CANAL, "Canal"), (Types.RIVER, "River"), (Types.LOCH, "Loch") ) ) class FishTypes: COARSE = 0 SPECIMEN_CARP = 1 GAME = 2 PREDATOR = 3 fish_types = models.PositiveSmallIntegerField( choices=( (FishTypes.COARSE, "Coarse"), (FishTypes.SPECIMEN_CARP, "Specimen Carp"), (FishTypes.GAME, "Game"), (FishTypes.PREDATOR, "Predator") ) ) class Meta: verbose_name = "waters" verbose_name_plural = "waters" def __str__(self): return self.name # region Leagues & Matches class Match(models.Model): """Represents an a fishing event/competition. Attributes: id (int): Automatically incrementing identifier number. name (str): A human-readable label to identify this item. description (str): A detailed description of the event. venue (`Venue`): Where this match is hosted. waters (`Waters`): Which waters will be used in this match, can only use waters from the assigned venue. meeting_point (str): TODO: what is this for? use_metric (bool): Should the metric system be used in measurements, imperial is used if `False`. allow_in_tournaments (bool): Should this match be allowed in tournaments? (TODO: is this correct?) start_datetime (datetime): When the match will begin. end_datetime (datetime): When the match will finish. draw_datetime (datetime): When the draw is made (during the match). type (int): TODO: match types? competitor_type (int): TODO what are these? """ id = models.AutoField(primary_key=True) name = models.CharField(max_length=128) description = models.CharField(max_length=1024) venue = models.ForeignKey(to=Venue, on_delete=models.CASCADE) waters = models.ForeignKey(to=Waters, on_delete=models.CASCADE) # can only select waters from the matching venue meeting_point = models.CharField(max_length=1024) use_metric = models.BooleanField() allow_in_tournaments = models.BooleanField() start_datetime = models.DateTimeField() end_datetime = models.DateTimeField() draw_datetime = models.DateTimeField() TYPES = ( (0, "Club Match"), (1, "Open Match"), (2, "Majors") ) type = models.PositiveSmallIntegerField(choices=TYPES) # TODO: this might be wrong, maybe should inherit value from related league or league rules COMPETITOR_TYPES = ( (0, "Individuals"), (1, "Pairs"), (2, "Teams") ) competitor_type = models.PositiveSmallIntegerField(choices=COMPETITOR_TYPES) class Meta: verbose_name = "match" verbose_name_plural = "matches" def __str__(self): return self.name class LeagueRule(models.Model): """Rules for leagues. Rules can be reused against multiple leagues, and aren't strictly owned by any particular `League` instance. Attributes: id (int): Automatically incrementing identifier number. name (str): A human-readable label to identify this item. ranking_system (int): TODO points_allocation (int): Whether people win by having the lowest or heighest points. points_awarded (int): TODO place_secondly_by_weight (bool): TODO team_places_secondly_by_section (bool): TODO section_placed_positions = (int): TODO worst_place_limits (bool): TODO attendance_points (int): TODO did_not_weigh (int): TODO did_not_weigh_value (int): TODO left_early (int): TODO left_early_value (int): TODO did_not_book (int): TODO did_not_book_value (int): TODO disqualification (int): TODO disqualification_value (int): TODO best_league_sessions (int): TODO worst_league_sessions (int): TODO match_placed_positions (int): TODO """ id = models.AutoField(primary_key=True) name = models.CharField(max_length=128) RANKING_SYSTEMS = ( (0, "By Points"), (1, "By Weight") ) ranking_system = models.PositiveSmallIntegerField(choices=RANKING_SYSTEMS) POINTS_ALLOCATIONS = ( (0, "First Place Low"), (1, "First Place High") ) points_allocation = models.PositiveSmallIntegerField(choices=POINTS_ALLOCATIONS) POINTS_AWARDED = ( (0, "Per Section"), (1, "Per Zone"), # TODO: what is a zone? (2, "Per Match") ) points_awarded = models.PositiveSmallIntegerField(choices=POINTS_AWARDED) # Use fish weight as a fallback for anglers with matching points? Otherwise they get the same placement. place_secondly_by_weight = models.BooleanField() # False: # people at the end have the same amount of points, they both come first # True: # two people have same number of points, winner is the section with the highest section placement team_places_secondly_by_section = models.BooleanField() # The number of awarded placements per section section_placed_positions = models.IntegerField() # max: 200, min: 0 # TODO: waiting on james to ask kelly for clarification on this. worst_place_limits = models.BooleanField() # Number of points adjusted for attending a match: # if points allocation == 0: adjusted = deducted # if points allocation == 1: adjusted = added attendance_points = models.IntegerField() # max: 20, min: 0 # Impact on points if an angler hasn't weighed his fish DID_NOT_WEIGHS = ( (0, "Threshold Points"), # TODO: waiting on description (1, "Worst Place Points"), # The points given for a DNW are equal to the points of last place (2, "Incremental Points") # A fixed number of points added for a DWN (2, "Absolute Points") # A fixed number of points given for a DNW (2, "Not Scored") # Don't count to overall points ) did_not_weigh = models.PositiveSmallIntegerField(choices=DID_NOT_WEIGHS) did_not_weigh_value = models.IntegerField() # Same as DNW, except is invoked because the angler left before the match end. LEFT_EARLYS = DID_NOT_WEIGHS left_early = models.PositiveSmallIntegerField(choices=LEFT_EARLYS) left_early_value = models.IntegerField() # Same as DNW, except invoked for not showing up. # This is used to balance teams who are missing players. NO_SHOWS = ( (0, "Threshold Points"), (1, "Worst Place Points"), (2, "Absolute Points"), (3, "Not Scored") ) no_show = models.PositiveSmallIntegerField(choices=NO_SHOWS) no_show_value = models.IntegerField() # Same as Now Show, except for angler's who didn't book their place DID_NOT_BOOKS = NO_SHOWS did_not_book = models.PositiveSmallIntegerField(choices=DID_NOT_BOOKS) did_not_book = models.IntegerField() # Same as DNW, except invoked when an angler is disqualified. DISQUALIFICATIONS = DID_NOT_WEIGHS disqualification = models.PositiveSmallIntegerField(choices=DISQUALIFICATIONS) disqualification_value = models.IntegerField() # TODO best_league_sessions = models.IntegerField() # max: 20, min: 0 worst_league_sessions = models.IntegerField() # max: 20, min: 0 match_placed_positions = models.IntegerField() # max: 200, min: 0 class Meta: verbose_name = "league rule" verbose_name_plural = "league rules" def __str__(self): return self.name class League(models.Model): """A League of matches (TODO: should match not be foreignkey linked to league?) Attributes: id (int): Automatically incrementing identifier number. name (str): A human-readable label to identify this item. description (str): extra_notes (str): profile_picture (file): banner_picture (file): matches (list of `Match`): anglers (list of `Angler`): rules (list of `LeagueRule`): """ id = models.AutoField(primary_key=True) name = models.CharField(max_length=128) description = models.CharField(max_length=384) extra_notes = models.CharField(max_length=1028) profile_picture = models.ImageField() banner_picture = models.ImageField() matches = models.ManyToManyField(to=Match) anglers = models.ManyToManyField(to=Angler) rules = models.ManyToManyField(to=LeagueRule) class Meta: verbose_name = "league" verbose_name_plural = "leagues" def __str__(self): return self.name @property def results(self): return LeagueResult.objects.filter(league=self) class Sponsor(models.Model): """Represent those sponsoring. Attribute: id (int): Automatically incrementing identifier number. name (str): The sponsor's name. url (str): Link to the sponsor's external resource. image (file): An image representing the sponsor. """ id = models.AutoField(primary_key=True) name = models.CharField(max_length=128) url = models.URLField() image = models.ImageField() class Meta: verbose_name = "sponsor" verbose_name_plural = "sponsors" def __str__(self): return self.name class LeagueResult(models.Model): """An entry representing an angler's result in a league. Attributes: TODO """ id = models.AutoField(primary_key=True) league = models.ForeignKey(to=League, on_delete=models.CASCADE) angler = models.ForeignKey(to=Angler, on_delete=models.CASCADE) sponsor = models.ForeignKey(to=Sponsor, on_delete=models.CASCADE, null=True, blank=True) total_weight = models.CharField(max_length=64) matches = models.IntegerField() date = models.DateField(default=timezone.now) class Meta: verbose_name = "league result" verbose_name_plural = "league results" def __str__(self): return f"{self.league.name} - {self.angler.name}"