Add test utilities and state machine diagrams for FSM models

- Introduced reusable test utilities in `backend/tests/utils` for FSM transitions, HTMX interactions, and common scenarios.
- Added factory functions for creating test submissions, parks, rides, and photo submissions.
- Implemented assertion helpers for verifying state changes, toast notifications, and transition logs.
- Created comprehensive state machine diagrams for all FSM-enabled models in `docs/STATE_DIAGRAMS.md`, detailing states, transitions, and guard conditions.
This commit is contained in:
pacnpal
2025-12-22 08:55:39 -05:00
parent b508434574
commit 45d97b6e68
71 changed files with 8608 additions and 633 deletions

View File

@@ -27,6 +27,13 @@
hx-swap="innerHTML">
</div>
<!-- Status Management Section (Moderators Only) -->
<div id="park-status-section"
hx-get="{% url 'parks:park_status_actions' park.slug %}"
hx-trigger="load"
hx-swap="outerHTML">
</div>
<!-- Park Header -->
<div class="p-compact mb-6 bg-white rounded-lg shadow-lg dark:bg-gray-800">
<div class="text-center">
@@ -38,13 +45,8 @@
</div>
{% endif %}
<div class="flex flex-wrap items-center justify-center gap-2 mt-3">
<span class="status-badge text-sm font-medium py-1 px-3 {% if park.status == 'OPERATING' %}status-operating
{% elif park.status == 'CLOSED_TEMP' or park.status == 'CLOSED_PERM' %}status-closed
{% elif park.status == 'UNDER_CONSTRUCTION' %}status-construction
{% elif park.status == 'DEMOLISHED' %}status-demolished
{% elif park.status == 'RELOCATED' %}status-relocated{% endif %}">
{{ park.get_status_display }}
</span>
{% include "parks/partials/park_header_badge.html" with park=park %}
{% if park.average_rating %}
<span class="flex items-center px-3 py-1 text-sm font-medium text-yellow-800 bg-yellow-100 status-badge dark:bg-yellow-600 dark:text-yellow-50">
<span class="mr-1 text-yellow-500 dark:text-yellow-200"></span>
@@ -204,8 +206,40 @@
{% endif %}
<!-- History Panel -->
<div class="p-optimized mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">History</h2>
<div class="p-optimized mb-6 bg-white rounded-lg shadow dark:bg-gray-800" x-data="{ showFsmHistory: false }">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">History</h2>
{% if perms.parks.change_park %}
<button type="button"
@click="showFsmHistory = !showFsmHistory"
class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 transition-colors duration-150">
<i class="mr-2 fas fa-history"></i>
<span x-text="showFsmHistory ? 'Hide Transitions' : 'Show Transitions'"></span>
</button>
{% endif %}
</div>
<!-- FSM Transition History (Moderators Only) -->
{% if perms.parks.change_park %}
<div x-show="showFsmHistory" x-cloak class="mb-4">
<div id="park-fsm-history-container"
x-show="showFsmHistory"
x-init="$watch('showFsmHistory', value => { if(value && !$el.dataset.loaded) { htmx.trigger($el, 'load-history'); $el.dataset.loaded = 'true'; } })"
hx-get="{% url 'moderation:moderation-reports-all-history' %}?model_type=park&object_id={{ park.id }}"
hx-trigger="load-history"
hx-target="this"
hx-swap="innerHTML"
hx-indicator="#park-fsm-loading">
<!-- Loading State -->
<div id="park-fsm-loading" class="flex items-center justify-center py-4">
<i class="mr-2 text-blue-500 fas fa-spinner fa-spin"></i>
<span class="text-sm text-gray-600 dark:text-gray-400">Loading transitions...</span>
</div>
</div>
</div>
{% endif %}
<!-- Regular History -->
<div class="space-y-4">
{% for record in history %}
<div class="p-4 rounded-lg bg-gray-50 dark:bg-gray-700/50">
@@ -221,7 +255,7 @@
<div class="text-sm">
<span class="font-medium">{{ field|title }}:</span>
<span class="text-red-600 dark:text-red-400">{{ changes.old }}</span>
<span class="mx-1"></span>
<span class="mx-1">-></span>
<span class="text-green-600 dark:text-green-400">{{ changes.new }}</span>
</div>
{% endif %}

View File

@@ -0,0 +1,30 @@
{# Park header status badge partial - refreshes via HTMX on park-status-changed #}
<span id="park-header-badge"
hx-get="{% url 'parks:park_header_badge' park.slug %}"
hx-trigger="park-status-changed from:body"
hx-swap="outerHTML">
{% if perms.parks.change_park %}
<!-- Clickable status badge for moderators -->
<button type="button"
onclick="document.getElementById('park-status-section').scrollIntoView({behavior: 'smooth'})"
class="status-badge text-sm font-medium py-1 px-3 transition-all hover:ring-2 hover:ring-blue-500 cursor-pointer
{% if park.status == 'OPERATING' %}status-operating
{% elif park.status == 'CLOSED_TEMP' or park.status == 'CLOSED_PERM' %}status-closed
{% elif park.status == 'UNDER_CONSTRUCTION' %}status-construction
{% elif park.status == 'DEMOLISHED' %}status-demolished
{% elif park.status == 'RELOCATED' %}status-relocated{% endif %}">
{{ park.get_status_display }}
<i class="fas fa-chevron-down ml-1 text-xs"></i>
</button>
{% else %}
<!-- Static status badge for non-moderators -->
<span class="status-badge text-sm font-medium py-1 px-3
{% if park.status == 'OPERATING' %}status-operating
{% elif park.status == 'CLOSED_TEMP' or park.status == 'CLOSED_PERM' %}status-closed
{% elif park.status == 'UNDER_CONSTRUCTION' %}status-construction
{% elif park.status == 'DEMOLISHED' %}status-demolished
{% elif park.status == 'RELOCATED' %}status-relocated{% endif %}">
{{ park.get_status_display }}
</span>
{% endif %}
</span>

View File

@@ -0,0 +1,23 @@
{% comment %}
Park FSM History Partial Template
Displays FSM transition history for a specific park.
Loaded via HTMX when the history section is expanded.
Required context:
- park: The Park model instance
{% endcomment %}
<div class="mt-4">
<div id="park-history-container"
hx-get="{% url 'moderation:moderation-reports-all-history' %}?model_type=park&object_id={{ park.id }}"
hx-trigger="load"
hx-swap="innerHTML"
hx-indicator="#park-history-loading">
<!-- Loading State -->
<div id="park-history-loading" class="flex items-center justify-center py-8">
<i class="mr-2 text-blue-500 fas fa-spinner fa-spin"></i>
<span class="text-gray-600 dark:text-gray-400">Loading history...</span>
</div>
</div>
</div>

View File

@@ -0,0 +1,19 @@
{% load fsm_tags %}
{# This partial is loaded via HTMX into #park-status-section. It must include the container #}
{# element with the same id and hx-* attributes to preserve targeting for subsequent transitions. #}
<div id="park-status-section"
data-park-status-actions
hx-get="{% url 'parks:park_status_actions' park.slug %}"
hx-trigger="park-status-changed from:body"
hx-swap="outerHTML">
{% if user.is_authenticated and perms.parks.change_park %}
<div class="mb-6 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<h3 class="text-sm font-semibold text-blue-900 dark:text-blue-100 mb-3">
<i class="fas fa-cog mr-2"></i>Status Management
</h3>
{% include "htmx/status_with_actions.html" with object=park user=user target_id="park-status-section" %}
</div>
{% endif %}
</div>