11 KiB

name description argument-hint allowed-tools
lp-django-model Creates Django 6 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 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:

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
        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

# 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)

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 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:
    from scheduler.models import MyModel
    # Model should be auto-imported in Django 6 shell
    MyModel.objects.create(name='Test', season=season)