feat: Implement initial schema and add various API, service, and management command enhancements across the application.

This commit is contained in:
pacnpal
2026-01-01 15:13:01 -05:00
parent c95f99ca10
commit b243b17af7
413 changed files with 11164 additions and 17433 deletions

View File

@@ -30,10 +30,7 @@ def pending_submission(db):
User = get_user_model()
# Get or create test user
user, _ = User.objects.get_or_create(
username="testsubmitter",
defaults={"email": "testsubmitter@example.com"}
)
user, _ = User.objects.get_or_create(username="testsubmitter", defaults={"email": "testsubmitter@example.com"})
user.set_password("testpass123")
user.save()
@@ -51,7 +48,7 @@ def pending_submission(db):
submission_type="EDIT",
changes={"description": "Updated park description for testing"},
reason="E2E test submission",
status="PENDING"
status="PENDING",
)
yield submission
@@ -73,8 +70,7 @@ def pending_photo_submission(db):
# Get or create test user
user, _ = User.objects.get_or_create(
username="testphotosubmitter",
defaults={"email": "testphotosubmitter@example.com"}
username="testphotosubmitter", defaults={"email": "testphotosubmitter@example.com"}
)
user.set_password("testpass123")
user.save()
@@ -89,6 +85,7 @@ def pending_photo_submission(db):
# Check if CloudflareImage model exists and has entries
try:
from django_cloudflareimages_toolkit.models import CloudflareImage
photo = CloudflareImage.objects.first()
if not photo:
pytest.skip("No CloudflareImage available for testing")
@@ -96,12 +93,7 @@ def pending_photo_submission(db):
pytest.skip("CloudflareImage not available")
submission = PhotoSubmission.objects.create(
user=user,
content_type=content_type,
object_id=park.pk,
photo=photo,
caption="E2E test photo",
status="PENDING"
user=user, content_type=content_type, object_id=park.pk, photo=photo, caption="E2E test photo", status="PENDING"
)
yield submission
@@ -113,9 +105,7 @@ def pending_photo_submission(db):
class TestEditSubmissionTransitions:
"""Tests for EditSubmission FSM transitions via HTMX."""
def test_submission_approve_transition_as_moderator(
self, mod_page: Page, pending_submission, live_server
):
def test_submission_approve_transition_as_moderator(self, mod_page: Page, pending_submission, live_server):
"""Test approving an EditSubmission as a moderator."""
# Navigate to moderation dashboard
mod_page.goto(f"{live_server.url}/moderation/dashboard/")
@@ -127,7 +117,7 @@ class TestEditSubmissionTransitions:
submission_row = mod_page.locator(f'[data-submission-id="{pending_submission.pk}"]')
# Verify initial status is pending
status_badge = submission_row.locator('[data-status-badge]')
status_badge = submission_row.locator("[data-status-badge]")
expect(status_badge).to_contain_text("Pending")
# Click the approve button
@@ -140,7 +130,7 @@ class TestEditSubmissionTransitions:
approve_btn.click()
# Wait for toast notification
toast = mod_page.locator('[data-toast]')
toast = mod_page.locator("[data-toast]")
expect(toast).to_be_visible(timeout=5000)
expect(toast).to_contain_text("approved")
@@ -151,9 +141,7 @@ class TestEditSubmissionTransitions:
pending_submission.refresh_from_db()
assert pending_submission.status == "APPROVED"
def test_submission_reject_transition_as_moderator(
self, mod_page: Page, pending_submission, live_server
):
def test_submission_reject_transition_as_moderator(self, mod_page: Page, pending_submission, live_server):
"""Test rejecting an EditSubmission as a moderator."""
mod_page.goto(f"{live_server.url}/moderation/dashboard/")
mod_page.wait_for_load_state("networkidle")
@@ -161,7 +149,7 @@ class TestEditSubmissionTransitions:
submission_row = mod_page.locator(f'[data-submission-id="{pending_submission.pk}"]')
# Verify initial status
status_badge = submission_row.locator('[data-status-badge]')
status_badge = submission_row.locator("[data-status-badge]")
expect(status_badge).to_contain_text("Pending")
# Click reject button
@@ -173,7 +161,7 @@ class TestEditSubmissionTransitions:
reject_btn.click()
# Verify toast notification
toast = mod_page.locator('[data-toast]')
toast = mod_page.locator("[data-toast]")
expect(toast).to_be_visible(timeout=5000)
expect(toast).to_contain_text("rejected")
@@ -184,9 +172,7 @@ class TestEditSubmissionTransitions:
pending_submission.refresh_from_db()
assert pending_submission.status == "REJECTED"
def test_submission_escalate_transition_as_moderator(
self, mod_page: Page, pending_submission, live_server
):
def test_submission_escalate_transition_as_moderator(self, mod_page: Page, pending_submission, live_server):
"""Test escalating an EditSubmission as a moderator."""
mod_page.goto(f"{live_server.url}/moderation/dashboard/")
mod_page.wait_for_load_state("networkidle")
@@ -194,7 +180,7 @@ class TestEditSubmissionTransitions:
submission_row = mod_page.locator(f'[data-submission-id="{pending_submission.pk}"]')
# Verify initial status
status_badge = submission_row.locator('[data-status-badge]')
status_badge = submission_row.locator("[data-status-badge]")
expect(status_badge).to_contain_text("Pending")
# Click escalate button
@@ -206,7 +192,7 @@ class TestEditSubmissionTransitions:
escalate_btn.click()
# Verify toast notification
toast = mod_page.locator('[data-toast]')
toast = mod_page.locator("[data-toast]")
expect(toast).to_be_visible(timeout=5000)
expect(toast).to_contain_text("escalated")
@@ -221,9 +207,7 @@ class TestEditSubmissionTransitions:
class TestPhotoSubmissionTransitions:
"""Tests for PhotoSubmission FSM transitions via HTMX."""
def test_photo_submission_approve_transition(
self, mod_page: Page, pending_photo_submission, live_server
):
def test_photo_submission_approve_transition(self, mod_page: Page, pending_photo_submission, live_server):
"""Test approving a PhotoSubmission as a moderator."""
mod_page.goto(f"{live_server.url}/moderation/dashboard/")
mod_page.wait_for_load_state("networkidle")
@@ -234,9 +218,7 @@ class TestPhotoSubmissionTransitions:
photos_tab.click()
# Find the photo submission row
submission_row = mod_page.locator(
f'[data-photo-submission-id="{pending_photo_submission.pk}"]'
)
submission_row = mod_page.locator(f'[data-photo-submission-id="{pending_photo_submission.pk}"]')
if not submission_row.is_visible():
pytest.skip("Photo submission not visible in dashboard")
@@ -248,7 +230,7 @@ class TestPhotoSubmissionTransitions:
approve_btn.click()
# Verify toast notification
toast = mod_page.locator('[data-toast]')
toast = mod_page.locator("[data-toast]")
expect(toast).to_be_visible(timeout=5000)
expect(toast).to_contain_text("approved")
@@ -256,9 +238,7 @@ class TestPhotoSubmissionTransitions:
pending_photo_submission.refresh_from_db()
assert pending_photo_submission.status == "APPROVED"
def test_photo_submission_reject_transition(
self, mod_page: Page, pending_photo_submission, live_server
):
def test_photo_submission_reject_transition(self, mod_page: Page, pending_photo_submission, live_server):
"""Test rejecting a PhotoSubmission as a moderator."""
mod_page.goto(f"{live_server.url}/moderation/dashboard/")
mod_page.wait_for_load_state("networkidle")
@@ -269,9 +249,7 @@ class TestPhotoSubmissionTransitions:
photos_tab.click()
# Find the photo submission row
submission_row = mod_page.locator(
f'[data-photo-submission-id="{pending_photo_submission.pk}"]'
)
submission_row = mod_page.locator(f'[data-photo-submission-id="{pending_photo_submission.pk}"]')
if not submission_row.is_visible():
pytest.skip("Photo submission not visible in dashboard")
@@ -283,7 +261,7 @@ class TestPhotoSubmissionTransitions:
reject_btn.click()
# Verify toast notification
toast = mod_page.locator('[data-toast]')
toast = mod_page.locator("[data-toast]")
expect(toast).to_be_visible(timeout=5000)
expect(toast).to_contain_text("rejected")
@@ -304,10 +282,7 @@ class TestModerationQueueTransitions:
User = get_user_model()
user, _ = User.objects.get_or_create(
username="testflagger",
defaults={"email": "testflagger@example.com"}
)
user, _ = User.objects.get_or_create(username="testflagger", defaults={"email": "testflagger@example.com"})
queue_item = ModerationQueue.objects.create(
item_type="CONTENT_REVIEW",
@@ -315,16 +290,14 @@ class TestModerationQueueTransitions:
priority="MEDIUM",
title="E2E Test Queue Item",
description="Queue item for E2E testing",
flagged_by=user
flagged_by=user,
)
yield queue_item
queue_item.delete()
def test_moderation_queue_start_transition(
self, mod_page: Page, pending_queue_item, live_server
):
def test_moderation_queue_start_transition(self, mod_page: Page, pending_queue_item, live_server):
"""Test starting work on a ModerationQueue item."""
mod_page.goto(f"{live_server.url}/moderation/queue/")
mod_page.wait_for_load_state("networkidle")
@@ -340,16 +313,14 @@ class TestModerationQueueTransitions:
start_btn.click()
# Verify status updated to IN_PROGRESS
status_badge = queue_row.locator('[data-status-badge]')
status_badge = queue_row.locator("[data-status-badge]")
expect(status_badge).to_contain_text("In Progress", timeout=5000)
# Verify database state
pending_queue_item.refresh_from_db()
assert pending_queue_item.status == "IN_PROGRESS"
def test_moderation_queue_complete_transition(
self, mod_page: Page, pending_queue_item, live_server
):
def test_moderation_queue_complete_transition(self, mod_page: Page, pending_queue_item, live_server):
"""Test completing a ModerationQueue item."""
# First set status to IN_PROGRESS
pending_queue_item.status = "IN_PROGRESS"
@@ -370,7 +341,7 @@ class TestModerationQueueTransitions:
complete_btn.click()
# Verify toast and status
toast = mod_page.locator('[data-toast]')
toast = mod_page.locator("[data-toast]")
expect(toast).to_be_visible(timeout=5000)
pending_queue_item.refresh_from_db()
@@ -390,8 +361,7 @@ class TestBulkOperationTransitions:
User = get_user_model()
user, _ = User.objects.get_or_create(
username="testadmin",
defaults={"email": "testadmin@example.com", "is_staff": True}
username="testadmin", defaults={"email": "testadmin@example.com", "is_staff": True}
)
operation = BulkOperation.objects.create(
@@ -401,24 +371,20 @@ class TestBulkOperationTransitions:
description="E2E Test Bulk Operation",
parameters={"test": True},
created_by=user,
total_items=10
total_items=10,
)
yield operation
operation.delete()
def test_bulk_operation_cancel_transition(
self, mod_page: Page, pending_bulk_operation, live_server
):
def test_bulk_operation_cancel_transition(self, mod_page: Page, pending_bulk_operation, live_server):
"""Test canceling a BulkOperation."""
mod_page.goto(f"{live_server.url}/moderation/bulk-operations/")
mod_page.wait_for_load_state("networkidle")
# Find the operation row
operation_row = mod_page.locator(
f'[data-bulk-operation-id="{pending_bulk_operation.pk}"]'
)
operation_row = mod_page.locator(f'[data-bulk-operation-id="{pending_bulk_operation.pk}"]')
if not operation_row.is_visible():
pytest.skip("Bulk operation not visible")
@@ -430,7 +396,7 @@ class TestBulkOperationTransitions:
cancel_btn.click()
# Verify toast
toast = mod_page.locator('[data-toast]')
toast = mod_page.locator("[data-toast]")
expect(toast).to_be_visible(timeout=5000)
expect(toast).to_contain_text("cancel")
@@ -442,16 +408,12 @@ class TestBulkOperationTransitions:
class TestTransitionLoadingStates:
"""Tests for loading indicators during FSM transitions."""
def test_loading_indicator_appears_during_transition(
self, mod_page: Page, pending_submission, live_server
):
def test_loading_indicator_appears_during_transition(self, mod_page: Page, pending_submission, live_server):
"""Verify loading spinner appears during HTMX transition."""
mod_page.goto(f"{live_server.url}/moderation/dashboard/")
mod_page.wait_for_load_state("networkidle")
submission_row = mod_page.locator(
f'[data-submission-id="{pending_submission.pk}"]'
)
submission_row = mod_page.locator(f'[data-submission-id="{pending_submission.pk}"]')
# Get approve button and associated loading indicator
approve_btn = submission_row.get_by_role("button", name="Approve")
@@ -466,8 +428,8 @@ class TestTransitionLoadingStates:
# Check for htmx-indicator visibility (may be brief)
# The indicator should become visible during the request
submission_row.locator('.htmx-indicator')
submission_row.locator(".htmx-indicator")
# Wait for transition to complete
toast = mod_page.locator('[data-toast]')
toast = mod_page.locator("[data-toast]")
expect(toast).to_be_visible(timeout=5000)