mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-23 00:11:09 -05:00
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:
@@ -1,5 +1,37 @@
|
||||
<div class="p-6 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-6 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.rides.change_ride %}
|
||||
<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.rides.change_ride %}
|
||||
<div x-show="showFsmHistory" x-cloak class="mb-4">
|
||||
<div id="ride-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=ride&object_id={{ ride.id }}"
|
||||
hx-trigger="load-history"
|
||||
hx-target="this"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#ride-fsm-loading">
|
||||
<!-- Loading State -->
|
||||
<div id="ride-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">
|
||||
@@ -8,7 +40,7 @@
|
||||
{% if record.pgh_context.user %}
|
||||
by {{ record.pgh_context.user }}
|
||||
{% endif %}
|
||||
• {{ record.pgh_label }}
|
||||
- {{ record.pgh_label }}
|
||||
</div>
|
||||
{% if record.diff_against_previous %}
|
||||
<div class="mt-2 space-y-2">
|
||||
@@ -16,7 +48,7 @@
|
||||
<div class="text-sm">
|
||||
<span class="font-medium">{{ field }}:</span>
|
||||
<span class="text-red-600 dark:text-red-400">{{ change.old }}</span>
|
||||
<span class="mx-1">→</span>
|
||||
<span class="mx-1">-></span>
|
||||
<span class="text-green-600 dark:text-green-400">{{ change.new }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
30
backend/templates/rides/partials/ride_header_badge.html
Normal file
30
backend/templates/rides/partials/ride_header_badge.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{# Ride header status badge partial - refreshes via HTMX on ride-status-changed #}
|
||||
<span id="ride-header-badge"
|
||||
hx-get="{% url 'parks:rides:ride_header_badge' park_slug=ride.park.slug ride_slug=ride.slug %}"
|
||||
hx-trigger="ride-status-changed from:body"
|
||||
hx-swap="outerHTML">
|
||||
{% if perms.rides.change_ride %}
|
||||
<!-- Clickable status badge for moderators -->
|
||||
<button type="button"
|
||||
onclick="document.getElementById('ride-status-section').scrollIntoView({behavior: 'smooth'})"
|
||||
class="px-3 py-1 text-sm font-medium status-badge transition-all hover:ring-2 hover:ring-blue-500 cursor-pointer
|
||||
{% if ride.status == 'OPERATING' %}status-operating
|
||||
{% elif ride.status == 'CLOSED_TEMP' or ride.status == 'CLOSED_PERM' %}status-closed
|
||||
{% elif ride.status == 'UNDER_CONSTRUCTION' %}status-construction
|
||||
{% elif ride.status == 'DEMOLISHED' %}status-demolished
|
||||
{% elif ride.status == 'RELOCATED' %}status-relocated{% endif %}">
|
||||
{{ ride.get_status_display }}
|
||||
<i class="fas fa-chevron-down ml-1 text-xs"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
<!-- Static status badge for non-moderators -->
|
||||
<span class="px-3 py-1 text-sm font-medium status-badge
|
||||
{% if ride.status == 'OPERATING' %}status-operating
|
||||
{% elif ride.status == 'CLOSED_TEMP' or ride.status == 'CLOSED_PERM' %}status-closed
|
||||
{% elif ride.status == 'UNDER_CONSTRUCTION' %}status-construction
|
||||
{% elif ride.status == 'DEMOLISHED' %}status-demolished
|
||||
{% elif ride.status == 'RELOCATED' %}status-relocated{% endif %}">
|
||||
{{ ride.get_status_display }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
23
backend/templates/rides/partials/ride_history.html
Normal file
23
backend/templates/rides/partials/ride_history.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{% comment %}
|
||||
Ride FSM History Partial Template
|
||||
|
||||
Displays FSM transition history for a specific ride.
|
||||
Loaded via HTMX when the history section is expanded.
|
||||
|
||||
Required context:
|
||||
- ride: The Ride model instance
|
||||
{% endcomment %}
|
||||
|
||||
<div class="mt-4">
|
||||
<div id="ride-history-container"
|
||||
hx-get="{% url 'moderation:moderation-reports-all-history' %}?model_type=ride&object_id={{ ride.id }}"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#ride-history-loading">
|
||||
<!-- Loading State -->
|
||||
<div id="ride-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>
|
||||
26
backend/templates/rides/partials/ride_status_actions.html
Normal file
26
backend/templates/rides/partials/ride_status_actions.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% load fsm_tags %}
|
||||
|
||||
{# This partial is loaded via HTMX into #ride-status-section. It must include the container #}
|
||||
{# element with the same id and hx-* attributes to preserve targeting for subsequent transitions. #}
|
||||
<div id="ride-status-section"
|
||||
data-ride-status-actions
|
||||
hx-get="{% url 'parks:rides:ride_status_actions' park_slug=ride.park.slug ride_slug=ride.slug %}"
|
||||
hx-trigger="ride-status-changed from:body"
|
||||
hx-swap="outerHTML">
|
||||
{% if user.is_authenticated and perms.rides.change_ride %}
|
||||
<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=ride user=user target_id="ride-status-section" %}
|
||||
|
||||
{% if ride.status == 'CLOSING' and ride.post_closing_status %}
|
||||
<div class="mt-3 p-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded text-sm">
|
||||
<i class="fas fa-info-circle mr-2 text-yellow-600"></i>
|
||||
<strong>Scheduled:</strong> Will transition to {{ ride.get_post_closing_status_display }} on {{ ride.closing_date }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -8,12 +8,12 @@
|
||||
<!-- Action Buttons - Above header -->
|
||||
{% if user.is_authenticated %}
|
||||
<div class="flex justify-end gap-2 mb-2">
|
||||
<a href="{% url 'parks:rides:ride_update' park_slug=ride.park.slug ride_slug=ride.slug %}"
|
||||
<a href="{% url 'parks:rides:ride_update' park_slug=ride.park.slug ride_slug=ride.slug %}"
|
||||
class="transition-transform btn-secondary hover:scale-105">
|
||||
<i class="mr-1 fas fa-pencil-alt"></i>Edit
|
||||
</a>
|
||||
{% if perms.media.add_photo %}
|
||||
<button class="transition-transform btn-secondary hover:scale-105"
|
||||
<button class="transition-transform btn-secondary hover:scale-105"
|
||||
@click="$dispatch('show-photo-upload')">
|
||||
<i class="mr-1 fas fa-camera"></i>Upload Photo
|
||||
</button>
|
||||
@@ -21,6 +21,13 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Status Management Section (Moderators Only) -->
|
||||
<div id="ride-status-section"
|
||||
hx-get="{% url 'parks:rides:ride_status_actions' park_slug=ride.park.slug ride_slug=ride.slug %}"
|
||||
hx-trigger="load"
|
||||
hx-swap="outerHTML">
|
||||
</div>
|
||||
|
||||
<!-- Ride Header -->
|
||||
<div class="p-compact mb-6 bg-white rounded-lg shadow-lg dark:bg-gray-800">
|
||||
<div class="text-center">
|
||||
@@ -34,13 +41,8 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center justify-center gap-2 mt-3">
|
||||
<span class="px-3 py-1 text-sm font-medium status-badge {% if ride.status == 'OPERATING' %}status-operating
|
||||
{% elif ride.status == 'CLOSED_TEMP' or ride.status == 'CLOSED_PERM' %}status-closed
|
||||
{% elif ride.status == 'UNDER_CONSTRUCTION' %}status-construction
|
||||
{% elif ride.status == 'DEMOLISHED' %}status-demolished
|
||||
{% elif ride.status == 'RELOCATED' %}status-relocated{% endif %}">
|
||||
{{ ride.get_status_display }}
|
||||
</span>
|
||||
{% include "rides/partials/ride_header_badge.html" with ride=ride %}
|
||||
|
||||
<span class="px-3 py-1 text-sm font-medium text-blue-800 bg-blue-100 status-badge dark:bg-blue-700 dark:text-blue-50">
|
||||
{{ ride.get_category_display }}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user