from typing import Any, Dict, Optional, Type, Union, cast from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.contrib.contenttypes.models import ContentType from django.http import JsonResponse, HttpResponseForbidden, HttpRequest, HttpResponse from django.core.exceptions import PermissionDenied from django.views.generic import DetailView, View from django.utils import timezone from django.db import models from django.contrib.auth import get_user_model from django.contrib.auth.base_user import AbstractBaseUser from django.contrib.auth.models import AnonymousUser import json from .models import EditSubmission, PhotoSubmission, UserType User = get_user_model() class EditSubmissionMixin(DetailView): """ Mixin for handling edit submissions with proper moderation. """ model: Optional[Type[models.Model]] = None def handle_edit_submission(self, request: HttpRequest, changes: Dict[str, Any], reason: str = '', source: str = '', submission_type: str = 'EDIT') -> JsonResponse: """ Handle an edit submission based on user's role. Args: request: The HTTP request changes: Dict of field changes {field_name: new_value} reason: Why this edit is needed source: Source of information (optional) submission_type: 'EDIT' or 'CREATE' Returns: JsonResponse with status and message """ if not request.user.is_authenticated: return JsonResponse({ 'status': 'error', 'message': 'You must be logged in to make edits.' }, status=403) if not self.model: raise ValueError("model attribute must be set") content_type = ContentType.objects.get_for_model(self.model) # Create the submission submission = EditSubmission( user=request.user, content_type=content_type, submission_type=submission_type, changes=changes, reason=reason, source=source ) # For edits, set the object_id if submission_type == 'EDIT': obj = self.get_object() submission.object_id = getattr(obj, 'id', None) # Auto-approve for moderators and above user_role = getattr(request.user, 'role', None) if user_role in ['MODERATOR', 'ADMIN', 'SUPERUSER']: obj = submission.approve(cast(UserType, request.user)) return JsonResponse({ 'status': 'success', 'message': 'Changes saved successfully.', 'auto_approved': True, 'redirect_url': getattr(obj, 'get_absolute_url', lambda: None)() }) # Submit for approval for regular users submission.save() return JsonResponse({ 'status': 'success', 'message': 'Your changes have been submitted for approval.', 'auto_approved': False }) def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> JsonResponse: """Handle POST requests for editing""" if not request.user.is_authenticated: return JsonResponse({ 'status': 'error', 'message': 'You must be logged in to make edits.' }, status=403) try: data = json.loads(request.body) changes = data.get('changes', {}) reason = data.get('reason', '') source = data.get('source', '') submission_type = data.get('submission_type', 'EDIT') if not changes: return JsonResponse({ 'status': 'error', 'message': 'No changes provided.' }, status=400) user_role = getattr(request.user, 'role', None) if not reason and user_role == 'USER': return JsonResponse({ 'status': 'error', 'message': 'Please provide a reason for your changes.' }, status=400) return self.handle_edit_submission( request, changes, reason, source, submission_type ) except json.JSONDecodeError: return JsonResponse({ 'status': 'error', 'message': 'Invalid JSON data.' }, status=400) except Exception as e: return JsonResponse({ 'status': 'error', 'message': str(e) }, status=500) class PhotoSubmissionMixin(DetailView): """ Mixin for handling photo submissions with proper moderation. """ model: Optional[Type[models.Model]] = None def handle_photo_submission(self, request: HttpRequest) -> JsonResponse: """Handle a photo submission based on user's role""" if not request.user.is_authenticated: return JsonResponse({ 'status': 'error', 'message': 'You must be logged in to upload photos.' }, status=403) if not self.model: raise ValueError("model attribute must be set") try: obj = self.get_object() except (AttributeError, self.model.DoesNotExist): return JsonResponse({ 'status': 'error', 'message': 'Invalid object.' }, status=400) if not request.FILES.get('photo'): return JsonResponse({ 'status': 'error', 'message': 'No photo provided.' }, status=400) content_type = ContentType.objects.get_for_model(obj) submission = PhotoSubmission( user=request.user, content_type=content_type, object_id=getattr(obj, 'id', None), photo=request.FILES['photo'], caption=request.POST.get('caption', ''), date_taken=request.POST.get('date_taken') ) # Auto-approve for moderators and above user_role = getattr(request.user, 'role', None) if user_role in ['MODERATOR', 'ADMIN', 'SUPERUSER']: submission.auto_approve() return JsonResponse({ 'status': 'success', 'message': 'Photo uploaded successfully.', 'auto_approved': True }) # Submit for approval for regular users submission.save() return JsonResponse({ 'status': 'success', 'message': 'Your photo has been submitted for approval.', 'auto_approved': False }) class ModeratorRequiredMixin(UserPassesTestMixin): """Require moderator or higher role for access""" request: Optional[HttpRequest] = None def test_func(self) -> bool: if not self.request: return False user_role = getattr(self.request.user, 'role', None) return ( self.request.user.is_authenticated and user_role in ['MODERATOR', 'ADMIN', 'SUPERUSER'] ) def handle_no_permission(self) -> HttpResponse: if not self.request or not self.request.user.is_authenticated: return super().handle_no_permission() return HttpResponseForbidden("You must be a moderator to access this page.") class AdminRequiredMixin(UserPassesTestMixin): """Require admin or superuser role for access""" request: Optional[HttpRequest] = None def test_func(self) -> bool: if not self.request: return False user_role = getattr(self.request.user, 'role', None) return ( self.request.user.is_authenticated and user_role in ['ADMIN', 'SUPERUSER'] ) def handle_no_permission(self) -> HttpResponse: if not self.request or not self.request.user.is_authenticated: return super().handle_no_permission() return HttpResponseForbidden("You must be an admin to access this page.") class InlineEditMixin: """Add inline editing context to views""" request: Optional[HttpRequest] = None def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: context = super().get_context_data(**kwargs) # type: ignore if self.request and self.request.user.is_authenticated: context['can_edit'] = True user_role = getattr(self.request.user, 'role', None) context['can_auto_approve'] = user_role in ['MODERATOR', 'ADMIN', 'SUPERUSER'] if isinstance(self, DetailView): obj = self.get_object() # type: ignore context['pending_edits'] = EditSubmission.objects.filter( content_type=ContentType.objects.get_for_model(obj.__class__), object_id=getattr(obj, 'id', None), status='NEW' ).select_related('user').order_by('-created_at') return context class HistoryMixin: """Add edit history context to views""" def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: context = super().get_context_data(**kwargs) # type: ignore # Only add history context for DetailViews if isinstance(self, DetailView): obj = self.get_object() # type: ignore # Get historical records ordered by date if available try: # Use pghistory's get_history method context['history'] = obj.get_history() except (AttributeError, TypeError): context['history'] = [] # Get related edit submissions content_type = ContentType.objects.get_for_model(obj.__class__) context['edit_submissions'] = EditSubmission.objects.filter( content_type=content_type, object_id=getattr(obj, 'id', None) ).exclude( status='NEW' ).select_related('user', 'handled_by').order_by('-created_at') return context