11 KiB
11 KiB
| name | description | argument-hint | allowed-tools |
|---|---|---|---|
| lp-django-model | Creates Django 5.2 models for league-planner with Fat Model pattern, custom managers, Meta indexes/constraints, and query optimization. Use when adding new models. | <model-name> [fields...] | Read, Write, Edit, Glob, Grep |
League-Planner Django Model Generator
Creates production-ready Django 5.2 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:
- Identify the app where the model belongs (scheduler, draws, qualifiers, common, api)
- Determine relationships to existing models
- List required fields with types and constraints
- Identify query patterns for index optimization
Step 2: Generate Model Structure
Follow this template based on project patterns:
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"<MyModel(id={self.pk}, name='{self.name}')>"
# 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:
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
# 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
class Meta:
indexes = [
# Composite index for filtered queries
models.Index(fields=['season', 'status', '-created_at']),
# Partial index (Django 5.2+)
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 5.2 Features
# Composite Primary Keys (new in 5.2)
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)
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)
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 specifyrelated_namefor ForeignKey and M2M fields to enable reverse lookups - No indexes on filter fields: Add indexes for fields frequently used in
filter()ororder_by() - N+1 queries: Use
select_related()for FK/O2O andprefetch_related()for M2M/reverse FK - Forgetting
db_index=True: Boolean fields used in filters need explicit indexing - Overly broad
on_delete=CASCADE: ConsiderPROTECTorSET_NULLfor important references
Verification
After creating a model:
- Run
python manage.py makemigrationsto generate migrations - Review the generated migration for correctness
- Test with
python manage.py shell:from scheduler.models import MyModel # Model should be auto-imported in Django 5.2 shell MyModel.objects.create(name='Test', season=season)