Implement entity submission services for ThrillWiki

- Added BaseEntitySubmissionService as an abstract base for entity submissions.
- Created specific submission services for entities: Park, Ride, Company, RideModel.
- Implemented create, update, and delete functionalities with moderation workflow.
- Enhanced logging and validation for required fields.
- Addressed foreign key handling and special field processing for each entity type.
- Noted existing issues with JSONField usage in Company submissions.
This commit is contained in:
pacnpal
2025-11-08 22:23:41 -05:00
parent 9122320e7e
commit 2884bc23ce
29 changed files with 8699 additions and 330 deletions

View File

@@ -28,7 +28,17 @@ from ..schemas import (
VoteResponse,
ErrorResponse,
UserSchema,
HistoryListResponse,
HistoryEventDetailSchema,
HistoryComparisonSchema,
HistoryDiffCurrentSchema,
FieldHistorySchema,
HistoryActivitySummarySchema,
RollbackRequestSchema,
RollbackResponseSchema,
ErrorSchema,
)
from ..services.history_service import HistoryService
router = Router(tags=["Reviews"])
logger = logging.getLogger(__name__)
@@ -583,3 +593,252 @@ def get_review_stats(request, entity_type: str, entity_id: UUID):
'total_reviews': stats['total_reviews'] or 0,
'rating_distribution': distribution,
}
# ============================================================================
# History Endpoints
# ============================================================================
@router.get(
'/{review_id}/history/',
response={200: HistoryListResponse, 404: ErrorSchema},
summary="Get review history",
description="Get historical changes for a review"
)
def get_review_history(
request,
review_id: int,
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=100),
date_from: Optional[str] = Query(None, description="Filter from date (YYYY-MM-DD)"),
date_to: Optional[str] = Query(None, description="Filter to date (YYYY-MM-DD)")
):
"""Get history for a review."""
from datetime import datetime
# Verify review exists
review = get_object_or_404(Review, id=review_id)
# Parse dates if provided
date_from_obj = datetime.fromisoformat(date_from).date() if date_from else None
date_to_obj = datetime.fromisoformat(date_to).date() if date_to else None
# Get history
offset = (page - 1) * page_size
events, accessible_count = HistoryService.get_history(
'review', str(review_id), request.user,
date_from=date_from_obj, date_to=date_to_obj,
limit=page_size, offset=offset
)
# Format events
formatted_events = []
for event in events:
formatted_events.append({
'id': event['id'],
'timestamp': event['timestamp'],
'operation': event['operation'],
'snapshot': event['snapshot'],
'changed_fields': event.get('changed_fields'),
'change_summary': event.get('change_summary', ''),
'can_rollback': HistoryService.can_rollback(request.user)
})
# Calculate pagination
total_pages = (accessible_count + page_size - 1) // page_size
return {
'entity_id': str(review_id),
'entity_type': 'review',
'entity_name': f"Review by {review.user.username}",
'total_events': accessible_count,
'accessible_events': accessible_count,
'access_limited': HistoryService.is_access_limited(request.user),
'access_reason': HistoryService.get_access_reason(request.user),
'events': formatted_events,
'pagination': {
'page': page,
'page_size': page_size,
'total_pages': total_pages,
'total_items': accessible_count
}
}
@router.get(
'/{review_id}/history/{event_id}/',
response={200: HistoryEventDetailSchema, 404: ErrorSchema},
summary="Get specific review history event",
description="Get detailed information about a specific historical event"
)
def get_review_history_event(request, review_id: int, event_id: int):
"""Get a specific history event for a review."""
review = get_object_or_404(Review, id=review_id)
event = HistoryService.get_event('review', event_id, request.user)
if not event:
return 404, {"error": "Event not found or not accessible"}
return {
'id': event['id'],
'timestamp': event['timestamp'],
'operation': event['operation'],
'entity_id': str(review_id),
'entity_type': 'review',
'entity_name': f"Review by {review.user.username}",
'snapshot': event['snapshot'],
'changed_fields': event.get('changed_fields'),
'metadata': event.get('metadata', {}),
'can_rollback': HistoryService.can_rollback(request.user),
'rollback_preview': None
}
@router.get(
'/{review_id}/history/compare/',
response={200: HistoryComparisonSchema, 400: ErrorSchema, 404: ErrorSchema},
summary="Compare two review history events",
description="Compare two historical events for a review"
)
def compare_review_history(
request,
review_id: int,
event1: int = Query(..., description="First event ID"),
event2: int = Query(..., description="Second event ID")
):
"""Compare two historical events for a review."""
review = get_object_or_404(Review, id=review_id)
try:
comparison = HistoryService.compare_events(
'review', event1, event2, request.user
)
if not comparison:
return 404, {"error": "One or both events not found"}
return {
'entity_id': str(review_id),
'entity_type': 'review',
'entity_name': f"Review by {review.user.username}",
'event1': comparison['event1'],
'event2': comparison['event2'],
'differences': comparison['differences'],
'changed_field_count': comparison['changed_field_count'],
'unchanged_field_count': comparison['unchanged_field_count'],
'time_between': comparison['time_between']
}
except ValueError as e:
return 400, {"error": str(e)}
@router.get(
'/{review_id}/history/{event_id}/diff-current/',
response={200: HistoryDiffCurrentSchema, 404: ErrorSchema},
summary="Compare historical event with current state",
description="Compare a historical event with the current review state"
)
def diff_review_history_with_current(request, review_id: int, event_id: int):
"""Compare historical event with current review state."""
review = get_object_or_404(Review, id=review_id)
try:
diff = HistoryService.compare_with_current(
'review', event_id, review, request.user
)
if not diff:
return 404, {"error": "Event not found"}
return {
'entity_id': str(review_id),
'entity_type': 'review',
'entity_name': f"Review by {review.user.username}",
'event': diff['event'],
'current_state': diff['current_state'],
'differences': diff['differences'],
'changed_field_count': diff['changed_field_count'],
'time_since': diff['time_since']
}
except ValueError as e:
return 404, {"error": str(e)}
@router.post(
'/{review_id}/history/{event_id}/rollback/',
response={200: RollbackResponseSchema, 400: ErrorSchema, 403: ErrorSchema},
summary="Rollback review to historical state",
description="Rollback review to a historical state (Moderators/Admins only)"
)
def rollback_review(request, review_id: int, event_id: int, payload: RollbackRequestSchema):
"""
Rollback review to a historical state.
**Permission:** Moderators, Admins, Superusers only
"""
# Check authentication
if not request.user or not request.user.is_authenticated:
return 401, {"error": "Authentication required"}
# Check rollback permission
if not HistoryService.can_rollback(request.user):
return 403, {"error": "Only moderators and administrators can perform rollbacks"}
review = get_object_or_404(Review, id=review_id)
try:
result = HistoryService.rollback_to_event(
review, 'review', event_id, request.user,
fields=payload.fields,
comment=payload.comment,
create_backup=payload.create_backup
)
return result
except (ValueError, PermissionError) as e:
return 400, {"error": str(e)}
@router.get(
'/{review_id}/history/field/{field_name}/',
response={200: FieldHistorySchema, 404: ErrorSchema},
summary="Get field-specific history",
description="Get history of changes to a specific review field"
)
def get_review_field_history(request, review_id: int, field_name: str):
"""Get history of changes to a specific review field."""
review = get_object_or_404(Review, id=review_id)
history = HistoryService.get_field_history(
'review', str(review_id), field_name, request.user
)
return {
'entity_id': str(review_id),
'entity_type': 'review',
'entity_name': f"Review by {review.user.username}",
'field': field_name,
'field_type': 'CharField', # Could introspect this
**history
}
@router.get(
'/{review_id}/history/summary/',
response={200: HistoryActivitySummarySchema, 404: ErrorSchema},
summary="Get review activity summary",
description="Get activity summary for a review"
)
def get_review_activity_summary(request, review_id: int):
"""Get activity summary for a review."""
review = get_object_or_404(Review, id=review_id)
summary = HistoryService.get_activity_summary(
'review', str(review_id), request.user
)
return {
'entity_id': str(review_id),
'entity_type': 'review',
'entity_name': f"Review by {review.user.username}",
**summary
}