Files
thrillwiki_django_no_react/memory-bank/projects/django-to-symfony-conversion/02-model-analysis-detailed.md

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:

  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

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

  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