--- name: lp-django-model description: Creates Django 6 models for league-planner with Fat Model pattern, custom managers, Meta indexes/constraints, and query optimization. Use when adding new models. argument-hint: [fields...] allowed-tools: Read, Write, Edit, Glob, Grep --- # League-Planner Django Model Generator Creates production-ready Django 6 models following the league-planner project patterns with Fat Models, custom managers, proper Meta configuration, and query optimization hints. ## When to Use - Creating new models for scheduler, draws, qualifiers, or other apps - Adding fields or relationships to existing models - Implementing custom managers or querysets - Setting up model indexes and constraints ## Prerequisites - Model should be placed in the appropriate app's `models.py` - Related models should already exist or be created together - Consider the model hierarchy: League → Season → Scenario → Match ## Instructions ### Step 1: Analyze Requirements Before generating the model: 1. Identify the app where the model belongs (scheduler, draws, qualifiers, common, api) 2. Determine relationships to existing models 3. List required fields with types and constraints 4. Identify query patterns for index optimization ### Step 2: Generate Model Structure Follow this template based on project patterns: ```python from django.db import models from django.db.models import Manager, QuerySet from django.utils.translation import gettext_lazy as _ class MyModelQuerySet(QuerySet): """Custom QuerySet with chainable methods.""" def active(self): return self.filter(is_active=True) def for_season(self, season): return self.filter(season=season) class MyModelManager(Manager): """Custom manager using the QuerySet.""" def get_queryset(self) -> QuerySet: return MyModelQuerySet(self.model, using=self._db) def active(self): return self.get_queryset().active() class MyModel(models.Model): """ Brief description of the model's purpose. Part of the hierarchy: Parent → ThisModel → Child """ # Foreign Keys (always first) season = models.ForeignKey( 'scheduler.Season', on_delete=models.CASCADE, related_name='mymodels', verbose_name=_('Season'), ) # Required fields name = models.CharField( max_length=255, verbose_name=_('Name'), help_text=_('Unique name within the season'), ) # Optional fields with defaults is_active = models.BooleanField( default=True, db_index=True, verbose_name=_('Active'), ) # Timestamps (project convention) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) # Managers objects = MyModelManager() class Meta: verbose_name = _('My Model') verbose_name_plural = _('My Models') ordering = ['-created_at'] # Indexes for common query patterns indexes = [ models.Index(fields=['season', 'is_active']), models.Index(fields=['name']), ] # Constraints for data integrity constraints = [ models.UniqueConstraint( fields=['season', 'name'], name='unique_mymodel_name_per_season' ), models.CheckConstraint( check=models.Q(name__isnull=False), name='mymodel_name_not_null' ), ] def __str__(self) -> str: return f"{self.name} ({self.season})" def __repr__(self) -> str: return f"" # Business logic methods (Fat Model pattern) def activate(self) -> None: """Activate this model instance.""" self.is_active = True self.save(update_fields=['is_active', 'updated_at']) def deactivate(self) -> None: """Deactivate this model instance.""" self.is_active = False self.save(update_fields=['is_active', 'updated_at']) @property def display_name(self) -> str: """Computed property for display purposes.""" return f"{self.name} - {self.season.name}" # Query optimization hints @classmethod def get_with_related(cls, pk: int): """Fetch with all related objects pre-loaded.""" return cls.objects.select_related( 'season', 'season__league', ).prefetch_related( 'children', ).get(pk=pk) ``` ### Step 3: Add Admin Registration Create or update admin configuration in `admin.py`: ```python from django.contrib import admin from .models import MyModel @admin.register(MyModel) class MyModelAdmin(admin.ModelAdmin): list_display = ['name', 'season', 'is_active', 'created_at'] list_filter = ['is_active', 'season__league'] search_fields = ['name', 'season__name'] raw_id_fields = ['season'] readonly_fields = ['created_at', 'updated_at'] def get_queryset(self, request): return super().get_queryset(request).select_related('season', 'season__league') ``` ## Patterns & Best Practices ### Field Naming Conventions | Type | Convention | Example | |------|------------|---------| | Boolean | `is_*` or `has_*` | `is_active`, `has_permission` | | Foreign Key | Singular noun | `season`, `team`, `user` | | M2M | Plural noun | `teams`, `users`, `matches` | | DateTime | `*_at` | `created_at`, `published_at` | | Date | `*_date` | `start_date`, `end_date` | ### Relationship Patterns ```python # One-to-Many (ForeignKey) season = models.ForeignKey( 'scheduler.Season', on_delete=models.CASCADE, # Delete children with parent related_name='scenarios', # season.scenarios.all() ) # Many-to-Many with through model teams = models.ManyToManyField( 'scheduler.Team', through='TeamInGroup', related_name='groups', ) # Self-referential parent = models.ForeignKey( 'self', on_delete=models.CASCADE, null=True, blank=True, related_name='children', ) ``` ### Index Strategies ```python class Meta: indexes = [ # Composite index for filtered queries models.Index(fields=['season', 'status', '-created_at']), # Partial index models.Index( fields=['name'], condition=models.Q(is_active=True), name='idx_active_names' ), # Covering index for read-heavy queries models.Index( fields=['season', 'team'], include=['score', 'status'], name='idx_match_lookup' ), ] ``` ### Django 6 Features ```python # Composite Primary Keys from django.db.models import CompositePrimaryKey class TeamMatch(models.Model): team = models.ForeignKey(Team, on_delete=models.CASCADE) match = models.ForeignKey(Match, on_delete=models.CASCADE) class Meta: pk = CompositePrimaryKey('team', 'match') # GeneratedField for computed columns from django.db.models import GeneratedField, F, Value from django.db.models.functions import Concat class Player(models.Model): first_name = models.CharField(max_length=100) last_name = models.CharField(max_length=100) full_name = GeneratedField( expression=Concat(F('first_name'), Value(' '), F('last_name')), output_field=models.CharField(max_length=201), db_persist=True, ) ``` ## Examples ### Example 1: Match Model (Scheduler App) ```python class Match(models.Model): """A single game between two teams in a scenario.""" scenario = models.ForeignKey( 'Scenario', on_delete=models.CASCADE, related_name='matches', ) home_team = models.ForeignKey( 'Team', on_delete=models.CASCADE, related_name='home_matches', ) away_team = models.ForeignKey( 'Team', on_delete=models.CASCADE, related_name='away_matches', ) day = models.ForeignKey( 'Day', on_delete=models.SET_NULL, null=True, blank=True, related_name='matches', ) kick_off_time = models.ForeignKey( 'KickOffTime', on_delete=models.SET_NULL, null=True, blank=True, ) is_final = models.BooleanField(default=False, db_index=True) is_confirmed = models.BooleanField(default=False) class Meta: verbose_name = _('Match') verbose_name_plural = _('Matches') ordering = ['day__number', 'kick_off_time__time'] indexes = [ models.Index(fields=['scenario', 'day']), models.Index(fields=['home_team', 'away_team']), ] constraints = [ models.CheckConstraint( check=~models.Q(home_team=models.F('away_team')), name='match_different_teams' ), ] @classmethod def get_for_scenario(cls, scenario_id: int): """Optimized query for listing matches.""" return cls.objects.filter( scenario_id=scenario_id ).select_related( 'home_team', 'away_team', 'day', 'kick_off_time', ).order_by('day__number', 'kick_off_time__time') ``` ### Example 2: Group Model (Draws App) ```python class Group(models.Model): """A group within a tournament draw.""" super_group = models.ForeignKey( 'SuperGroup', on_delete=models.CASCADE, related_name='groups', ) name = models.CharField(max_length=50) position = models.PositiveSmallIntegerField(default=0) teams = models.ManyToManyField( 'scheduler.Team', through='TeamInGroup', related_name='draw_groups', ) class Meta: ordering = ['super_group', 'position'] constraints = [ models.UniqueConstraint( fields=['super_group', 'name'], name='unique_group_name_in_supergroup' ), ] @property def team_count(self) -> int: return self.teams.count() def get_teams_with_country(self): """Optimized team fetch with country data.""" return self.teams.select_related('country').order_by('name') ``` ## Common Pitfalls - **Missing `related_name`**: Always specify `related_name` for ForeignKey and M2M fields to enable reverse lookups - **No indexes on filter fields**: Add indexes for fields frequently used in `filter()` or `order_by()` - **N+1 queries**: Use `select_related()` for FK/O2O and `prefetch_related()` for M2M/reverse FK - **Forgetting `db_index=True`**: Boolean fields used in filters need explicit indexing - **Overly broad `on_delete=CASCADE`**: Consider `PROTECT` or `SET_NULL` for important references ## Verification After creating a model: 1. Run `python manage.py makemigrations` to generate migrations 2. Review the generated migration for correctness 3. Test with `python manage.py shell`: ```python from scheduler.models import MyModel # Model should be auto-imported in Django 6 shell MyModel.objects.create(name='Test', season=season) ```