18 KiB
Django Model Analysis - Detailed Implementation Patterns
Date: January 7, 2025
Analyst: Roo (Architect Mode)
Purpose: Detailed Django model analysis for Symfony Doctrine mapping
Status: Complete model pattern documentation
Overview
This document provides detailed analysis of Django model implementations, focusing on patterns, relationships, and features that must be mapped to Symfony Doctrine entities during conversion.
Core Entity Models Analysis
1. Park Model - Main Entity
@pghistory.track()
class Park(TrackedModel):
# Primary Fields
id: int # Auto-generated primary key
name = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True)
description = models.TextField(blank=True)
# Status Enumeration
STATUS_CHOICES = [
("OPERATING", "Operating"),
("CLOSED_TEMP", "Temporarily Closed"),
("CLOSED_PERM", "Permanently Closed"),
("UNDER_CONSTRUCTION", "Under Construction"),
("DEMOLISHED", "Demolished"),
("RELOCATED", "Relocated"),
]
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="OPERATING")
# Temporal Fields
opening_date = models.DateField(null=True, blank=True)
closing_date = models.DateField(null=True, blank=True)
operating_season = models.CharField(max_length=255, blank=True)
# Numeric Fields
size_acres = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
# URL Field
website = models.URLField(blank=True)
# Statistics (Computed/Cached)
ride_count = models.PositiveIntegerField(default=0)
roller_coaster_count = models.PositiveIntegerField(default=0)
# Foreign Key Relationships
operator = models.ForeignKey(
Operator,
on_delete=models.CASCADE,
related_name='parks'
)
property_owner = models.ForeignKey(
PropertyOwner,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='owned_parks'
)
# Generic Relationships
location = GenericRelation(Location, related_query_name='park')
photos = GenericRelation(Photo, related_query_name='park')
reviews = GenericRelation(Review, related_query_name='park')
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
Symfony Conversion Notes:
- Enum status field → DoctrineEnum or string with validation
- Generic relations → Polymorphic associations or separate entity relations
- History tracking → Event sourcing or audit bundle
- Computed fields → Doctrine lifecycle callbacks or cached properties
2. Ride Model - Complex Entity with Specifications
@pghistory.track()
class Ride(TrackedModel):
# Core Identity
name = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True)
description = models.TextField(blank=True)
# Ride Type Enumeration
TYPE_CHOICES = [
('RC', 'Roller Coaster'),
('DR', 'Dark Ride'),
('FR', 'Flat Ride'),
('WR', 'Water Ride'),
('TR', 'Transport Ride'),
('OT', 'Other'),
]
ride_type = models.CharField(max_length=2, choices=TYPE_CHOICES)
# Status with Complex Workflow
STATUS_CHOICES = [
('OPERATING', 'Operating'),
('CLOSED_TEMP', 'Temporarily Closed'),
('CLOSED_PERM', 'Permanently Closed'),
('UNDER_CONSTRUCTION', 'Under Construction'),
('RELOCATED', 'Relocated'),
('DEMOLISHED', 'Demolished'),
]
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='OPERATING')
# Required Relationship
park = models.ForeignKey(Park, on_delete=models.CASCADE, related_name='rides')
# Optional Relationships
park_area = models.ForeignKey(
'ParkArea',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='rides'
)
manufacturer = models.ForeignKey(
Manufacturer,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='manufactured_rides'
)
designer = models.ForeignKey(
Designer,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='designed_rides'
)
ride_model = models.ForeignKey(
'RideModel',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='installations'
)
# Temporal Data
opening_date = models.DateField(null=True, blank=True)
closing_date = models.DateField(null=True, blank=True)
# Generic Relationships
photos = GenericRelation(Photo, related_query_name='ride')
reviews = GenericRelation(Review, related_query_name='ride')
# One-to-One Extensions
# Note: RollerCoasterStats as separate model with OneToOne relationship
Symfony Conversion Notes:
- Multiple optional foreign keys → Nullable Doctrine associations
- Generic relations → Polymorphic or separate photo/review entities
- Complex status workflow → State pattern or enum with validation
- One-to-one extensions → Doctrine inheritance or separate entities
3. User Model - Extended Authentication
class User(AbstractUser):
# Role-Based Access Control
ROLE_CHOICES = [
('USER', 'User'),
('MODERATOR', 'Moderator'),
('ADMIN', 'Admin'),
('SUPERUSER', 'Superuser'),
]
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='USER')
# Public Identifier (Non-PK)
user_id = models.CharField(max_length=20, unique=True)
# Profile Extensions
theme_preference = models.CharField(
max_length=10,
choices=[('LIGHT', 'Light'), ('DARK', 'Dark'), ('AUTO', 'Auto')],
default='AUTO'
)
# Social Fields
google_id = models.CharField(max_length=255, blank=True)
discord_id = models.CharField(max_length=255, blank=True)
# Statistics (Cached)
review_count = models.PositiveIntegerField(default=0)
photo_count = models.PositiveIntegerField(default=0)
# Relationships
# Note: UserProfile as separate model with OneToOne relationship
Symfony Conversion Notes:
- AbstractUser → Symfony UserInterface implementation
- Role choices → Symfony Role hierarchy
- Social authentication → OAuth2 bundle integration
- Cached statistics → Event listeners or message bus updates
4. RollerCoasterStats - Detailed Specifications
class RollerCoasterStats(models.Model):
# One-to-One with Ride
ride = models.OneToOneField(
Ride,
on_delete=models.CASCADE,
related_name='coaster_stats'
)
# Physical Specifications (Metric)
height_ft = models.DecimalField(max_digits=6, decimal_places=2, null=True, blank=True)
height_m = models.DecimalField(max_digits=6, decimal_places=2, null=True, blank=True)
length_ft = models.DecimalField(max_digits=8, decimal_places=2, null=True, blank=True)
length_m = models.DecimalField(max_digits=8, decimal_places=2, null=True, blank=True)
speed_mph = models.DecimalField(max_digits=5, decimal_places=1, null=True, blank=True)
speed_kmh = models.DecimalField(max_digits=5, decimal_places=1, null=True, blank=True)
# Technical Specifications
inversions = models.PositiveSmallIntegerField(null=True, blank=True)
duration_seconds = models.PositiveIntegerField(null=True, blank=True)
capacity_per_hour = models.PositiveIntegerField(null=True, blank=True)
# Design Elements
launch_system = models.CharField(max_length=50, blank=True)
track_material = models.CharField(max_length=30, blank=True)
# Restrictions
height_requirement_in = models.PositiveSmallIntegerField(null=True, blank=True)
height_requirement_cm = models.PositiveSmallIntegerField(null=True, blank=True)
Symfony Conversion Notes:
- OneToOne relationship → Doctrine OneToOne or embedded value objects
- Dual unit measurements → Value objects with conversion methods
- Optional numeric fields → Nullable types with validation
- Technical specifications → Embedded value objects or separate specification entity
Generic Relationship Patterns
Generic Foreign Key Implementation
class Photo(models.Model):
# Generic relationship to any model
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
# Photo-specific fields
image = models.ImageField(upload_to='photos/%Y/%m/%d/')
caption = models.CharField(max_length=255, blank=True)
credit = models.CharField(max_length=100, blank=True)
# Approval workflow
APPROVAL_CHOICES = [
('PENDING', 'Pending Review'),
('APPROVED', 'Approved'),
('REJECTED', 'Rejected'),
]
approval_status = models.CharField(
max_length=10,
choices=APPROVAL_CHOICES,
default='PENDING'
)
# Metadata
exif_data = models.JSONField(default=dict, blank=True)
file_size = models.PositiveIntegerField(null=True, blank=True)
uploaded_by = models.ForeignKey(User, on_delete=models.CASCADE)
uploaded_at = models.DateTimeField(auto_now_add=True)
Symfony Conversion Options:
- Polymorphic Associations - Use Doctrine inheritance mapping
- Interface-based - Create PhotoableInterface and separate photo entities
- Union Types - Use discriminator mapping with specific photo types
Review System with Generic Relations
class Review(models.Model):
# Generic relationship
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
# Review content
title = models.CharField(max_length=255)
content = models.TextField()
rating = models.PositiveSmallIntegerField(
validators=[MinValueValidator(1), MaxValueValidator(10)]
)
# Metadata
author = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# Engagement
likes = models.ManyToManyField(User, through='ReviewLike', related_name='liked_reviews')
# Moderation
is_approved = models.BooleanField(default=False)
moderated_by = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='moderated_reviews'
)
Symfony Conversion Notes:
- Generic reviews → Separate ParkReview, RideReview entities or polymorphic mapping
- Many-to-many through model → Doctrine association entities
- Rating validation → Symfony validation constraints
- Moderation fields → Workflow component or state machine
Location and Geographic Data
PostGIS Integration
class Location(models.Model):
# Generic relationship to any model
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
# Geographic data (PostGIS)
location = models.PointField(geography=True, null=True, blank=True)
# Legacy coordinate support
coordinates = models.JSONField(default=dict, blank=True)
latitude = models.DecimalField(max_digits=10, decimal_places=8, null=True, blank=True)
longitude = models.DecimalField(max_digits=11, decimal_places=8, null=True, blank=True)
# Address components
address_line_1 = models.CharField(max_length=255, blank=True)
address_line_2 = models.CharField(max_length=255, blank=True)
city = models.CharField(max_length=100, blank=True)
state_province = models.CharField(max_length=100, blank=True)
postal_code = models.CharField(max_length=20, blank=True)
country = models.CharField(max_length=2, blank=True) # ISO country code
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
Symfony Conversion Notes:
- PostGIS Point field → Doctrine DBAL geographic types or custom mapping
- Generic location → Polymorphic or interface-based approach
- Address components → Value objects or embedded entities
- Coordinate legacy support → Migration strategy during conversion
History Tracking Implementation
TrackedModel Base Class
@pghistory.track()
class TrackedModel(models.Model):
"""Base model with automatic history tracking"""
class Meta:
abstract = True
# Automatic fields
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# Slug management
slug = models.SlugField(max_length=255, unique=True)
def save(self, *args, **kwargs):
# Auto-generate slug if not provided
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
PgHistory Event Tracking
# Automatic event models created by pghistory
# Example for Park model:
class ParkEvent(models.Model):
"""Auto-generated history table"""
# All fields from original Park model
# Plus:
pgh_created_at = models.DateTimeField()
pgh_label = models.CharField(max_length=100) # Event type
pgh_id = models.AutoField(primary_key=True)
pgh_obj = models.ForeignKey(Park, on_delete=models.CASCADE)
# Context fields (from middleware)
pgh_context = models.JSONField(default=dict)
Symfony Conversion Notes:
- History tracking → Doctrine Extensions Loggable or custom event sourcing
- Auto-timestamps → Doctrine lifecycle callbacks
- Slug generation → Symfony String component with event listeners
- Context tracking → Event dispatcher with context gathering
Moderation System Models
Content Submission Workflow
class EditSubmission(models.Model):
"""User-submitted edits for approval"""
STATUS_CHOICES = [
('PENDING', 'Pending Review'),
('APPROVED', 'Approved'),
('REJECTED', 'Rejected'),
('ESCALATED', 'Escalated'),
]
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='PENDING')
# Submission content
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField(null=True, blank=True) # Null for new objects
# Change data (JSON)
submitted_data = models.JSONField()
current_data = models.JSONField(default=dict, blank=True)
# Workflow fields
submitted_by = models.ForeignKey(User, on_delete=models.CASCADE)
submitted_at = models.DateTimeField(auto_now_add=True)
reviewed_by = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='reviewed_submissions'
)
reviewed_at = models.DateTimeField(null=True, blank=True)
# Review notes
review_notes = models.TextField(blank=True)
# Auto-approval logic
auto_approved = models.BooleanField(default=False)
Symfony Conversion Notes:
- Status workflow → Symfony Workflow component
- JSON change data → Doctrine JSON type with validation
- Generic content reference → Polymorphic approach or interface
- Auto-approval → Event system with rule engine
Conversion Mapping Summary
Model → Entity Mapping Strategy
| Django Pattern | Symfony Approach |
|---|---|
models.Model |
Doctrine Entity |
AbstractUser |
User implementing UserInterface |
GenericForeignKey |
Polymorphic associations or interfaces |
@pghistory.track() |
Event sourcing or audit bundle |
choices=CHOICES |
Enums with validation |
JSONField |
Doctrine JSON type |
models.PointField |
Custom geographic type |
auto_now_add=True |
Doctrine lifecycle callbacks |
GenericRelation |
Separate entity relationships |
Through models |
Association entities |
Key Conversion Considerations
-
Generic Relations - Most complex conversion aspect
- Option A: Polymorphic inheritance mapping
- Option B: Interface-based approach with separate entities
- Option C: Discriminator mapping with union types
-
History Tracking - Choose appropriate strategy
- Event sourcing for full audit trails
- Doctrine Extensions for simple logging
- Custom audit bundle for workflow tracking
-
Geographic Data - PostGIS equivalent
- Doctrine DBAL geographic extensions
- Custom types for Point/Polygon fields
- Migration strategy for existing coordinates
-
Validation - Move from Django to Symfony
- Model choices → Symfony validation constraints
- Custom validators → Constraint classes
- Form validation → Symfony Form component
-
Relationships - Preserve data integrity
- Maintain all foreign key constraints
- Convert cascade behaviors appropriately
- Handle nullable relationships correctly
Next Steps
- Entity Design - Create Doctrine entity classes for each Django model
- Association Mapping - Design polymorphic strategies for generic relations
- Value Objects - Extract embedded data into value objects
- Migration Scripts - Plan database schema migration from Django to Symfony
- Repository Patterns - Convert Django QuerySets to Doctrine repositories
Status: ✅ COMPLETED - Detailed model analysis for Symfony conversion
Next: Symfony entity design and mapping strategy