fix(fsm): Fix StateLog.by capture and cycle validation; add photographer field to photos

## FSM State Machine Fixes

### StateLog.by Field Capture
- Modified TransitionMethodFactory to pass 'user' as 'by' kwarg to enable
  django-fsm-log's @fsm_log_by decorator to correctly capture the user who
  performed the transition
- Applied fix to both escalate_transition and create_transition_method
- Uses exec() to dynamically create transition functions with correct __name__
  before decorators are applied, ensuring django-fsm's method registration works

### Cycle Validation Behavior
- Changed validate_no_cycles() to return ValidationWarning instead of ValidationError
- Cycles are now treated as warnings, not blocking errors, since cycles are often
  intentional in operational status FSMs (e.g., reopening after temporary closure)

### Ride Status Transitions
- Added TEMPORARY_CLOSURE -> OPERATING transition (reopen after temporary closure)
- Added SBNO -> OPERATING transition (revival - ride returns to operation)

## Field Parity

### Photo Models
- Added 'photographer' field to RidePhoto and ParkPhoto models
- Maps to frontend 'photographer_credit' field for full schema parity
- Includes corresponding migrations for both apps

### Serializers
- Added 'photographer' to RidePhotoSerializer and ParkPhotoSerializer read_only_fields
This commit is contained in:
pacnpal
2026-01-09 08:04:44 -05:00
parent fe960e8b62
commit cf54df0416
9 changed files with 157 additions and 32 deletions

View File

@@ -0,0 +1,41 @@
# Generated by Django 5.2.9 on 2026-01-08 18:48
import pgtrigger.compiler
import pgtrigger.migrations
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('parks', '0030_company_schema_parity'),
]
operations = [
pgtrigger.migrations.RemoveTrigger(
model_name='parkphoto',
name='insert_insert',
),
pgtrigger.migrations.RemoveTrigger(
model_name='parkphoto',
name='update_update',
),
migrations.AddField(
model_name='parkphoto',
name='photographer',
field=models.CharField(blank=True, help_text='Photographer credit (maps to frontend photographer_credit)', max_length=200),
),
migrations.AddField(
model_name='parkphotoevent',
name='photographer',
field=models.CharField(blank=True, help_text='Photographer credit (maps to frontend photographer_credit)', max_length=200),
),
pgtrigger.migrations.AddTrigger(
model_name='parkphoto',
trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "parks_parkphotoevent" ("alt_text", "caption", "created_at", "date_taken", "id", "image_id", "is_approved", "is_primary", "park_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "photographer", "updated_at", "uploaded_by_id") VALUES (NEW."alt_text", NEW."caption", NEW."created_at", NEW."date_taken", NEW."id", NEW."image_id", NEW."is_approved", NEW."is_primary", NEW."park_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."photographer", NEW."updated_at", NEW."uploaded_by_id"); RETURN NULL;', hash='151f82660bda74a8d10ddf581e509c63e4e7e6e0', operation='INSERT', pgid='pgtrigger_insert_insert_e2033', table='parks_parkphoto', when='AFTER')),
),
pgtrigger.migrations.AddTrigger(
model_name='parkphoto',
trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD.* IS DISTINCT FROM NEW.*)', func='INSERT INTO "parks_parkphotoevent" ("alt_text", "caption", "created_at", "date_taken", "id", "image_id", "is_approved", "is_primary", "park_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "photographer", "updated_at", "uploaded_by_id") VALUES (NEW."alt_text", NEW."caption", NEW."created_at", NEW."date_taken", NEW."id", NEW."image_id", NEW."is_approved", NEW."is_primary", NEW."park_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."photographer", NEW."updated_at", NEW."uploaded_by_id"); RETURN NULL;', hash='9a33e713d26165877f27ae3f993c9c0675f61620', operation='UPDATE', pgid='pgtrigger_update_update_42711', table='parks_parkphoto', when='AFTER')),
),
]

View File

@@ -43,6 +43,11 @@ class ParkPhoto(TrackedModel):
)
caption = models.CharField(max_length=255, blank=True, help_text="Photo caption or description")
photographer = models.CharField(
max_length=200,
blank=True,
help_text="Photographer credit (maps to frontend photographer_credit)"
)
alt_text = models.CharField(max_length=255, blank=True, help_text="Alternative text for accessibility")
is_primary = models.BooleanField(default=False, help_text="Whether this is the primary photo for the park")
is_approved = models.BooleanField(default=False, help_text="Whether this photo has been approved by moderators")