# 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