# 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 ```python @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 ```python @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 ```python 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 ```python 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 ```python 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:** 1. **Polymorphic Associations** - Use Doctrine inheritance mapping 2. **Interface-based** - Create PhotoableInterface and separate photo entities 3. **Union Types** - Use discriminator mapping with specific photo types ### Review System with Generic Relations ```python 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 ```python 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 ```python @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 ```python # 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 ```python 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 1. **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 2. **History Tracking** - Choose appropriate strategy - Event sourcing for full audit trails - Doctrine Extensions for simple logging - Custom audit bundle for workflow tracking 3. **Geographic Data** - PostGIS equivalent - Doctrine DBAL geographic extensions - Custom types for Point/Polygon fields - Migration strategy for existing coordinates 4. **Validation** - Move from Django to Symfony - Model choices → Symfony validation constraints - Custom validators → Constraint classes - Form validation → Symfony Form component 5. **Relationships** - Preserve data integrity - Maintain all foreign key constraints - Convert cascade behaviors appropriately - Handle nullable relationships correctly ## Next Steps 1. **Entity Design** - Create Doctrine entity classes for each Django model 2. **Association Mapping** - Design polymorphic strategies for generic relations 3. **Value Objects** - Extract embedded data into value objects 4. **Migration Scripts** - Plan database schema migration from Django to Symfony 5. **Repository Patterns** - Convert Django QuerySets to Doctrine repositories --- **Status:** ✅ **COMPLETED** - Detailed model analysis for Symfony conversion **Next:** Symfony entity design and mapping strategy