mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-21 20:51:13 -05:00
fixed a bunch of things, hopefully didn't break things
This commit is contained in:
@@ -1,17 +1,27 @@
|
||||
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
|
||||
from django.http import JsonResponse, HttpResponseForbidden, HttpRequest, HttpResponse
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.views.generic import DetailView
|
||||
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
|
||||
from .models import EditSubmission, PhotoSubmission, UserType
|
||||
|
||||
class EditSubmissionMixin:
|
||||
User = get_user_model()
|
||||
|
||||
class EditSubmissionMixin(DetailView):
|
||||
"""
|
||||
Mixin for handling edit submissions with proper moderation.
|
||||
"""
|
||||
def handle_edit_submission(self, request, changes, reason='', source='', submission_type='EDIT'):
|
||||
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.
|
||||
|
||||
@@ -31,6 +41,9 @@ class EditSubmissionMixin:
|
||||
'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
|
||||
@@ -46,16 +59,17 @@ class EditSubmissionMixin:
|
||||
# For edits, set the object_id
|
||||
if submission_type == 'EDIT':
|
||||
obj = self.get_object()
|
||||
submission.object_id = obj.id
|
||||
submission.object_id = getattr(obj, 'id', None)
|
||||
|
||||
# Auto-approve for moderators and above
|
||||
if request.user.role in ['MODERATOR', 'ADMIN', 'SUPERUSER']:
|
||||
obj = submission.approve(request.user)
|
||||
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': obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None
|
||||
'redirect_url': getattr(obj, 'get_absolute_url', lambda: None)()
|
||||
})
|
||||
|
||||
# Submit for approval for regular users
|
||||
@@ -66,7 +80,7 @@ class EditSubmissionMixin:
|
||||
'auto_approved': False
|
||||
})
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> JsonResponse:
|
||||
"""Handle POST requests for editing"""
|
||||
if not request.user.is_authenticated:
|
||||
return JsonResponse({
|
||||
@@ -87,7 +101,8 @@ class EditSubmissionMixin:
|
||||
'message': 'No changes provided.'
|
||||
}, status=400)
|
||||
|
||||
if not reason and request.user.role == 'USER':
|
||||
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.'
|
||||
@@ -108,11 +123,13 @@ class EditSubmissionMixin:
|
||||
'message': str(e)
|
||||
}, status=500)
|
||||
|
||||
class PhotoSubmissionMixin:
|
||||
class PhotoSubmissionMixin(DetailView):
|
||||
"""
|
||||
Mixin for handling photo submissions with proper moderation.
|
||||
"""
|
||||
def handle_photo_submission(self, request):
|
||||
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({
|
||||
@@ -120,6 +137,9 @@ class PhotoSubmissionMixin:
|
||||
'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):
|
||||
@@ -139,14 +159,15 @@ class PhotoSubmissionMixin:
|
||||
submission = PhotoSubmission(
|
||||
user=request.user,
|
||||
content_type=content_type,
|
||||
object_id=obj.id,
|
||||
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
|
||||
if request.user.role in ['MODERATOR', 'ADMIN', 'SUPERUSER']:
|
||||
user_role = getattr(request.user, 'role', None)
|
||||
if user_role in ['MODERATOR', 'ADMIN', 'SUPERUSER']:
|
||||
submission.auto_approve()
|
||||
return JsonResponse({
|
||||
'status': 'success',
|
||||
@@ -164,63 +185,81 @@ class PhotoSubmissionMixin:
|
||||
|
||||
class ModeratorRequiredMixin(UserPassesTestMixin):
|
||||
"""Require moderator or higher role for access"""
|
||||
def test_func(self):
|
||||
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
|
||||
self.request.user.role in ['MODERATOR', 'ADMIN', 'SUPERUSER']
|
||||
user_role in ['MODERATOR', 'ADMIN', 'SUPERUSER']
|
||||
)
|
||||
|
||||
def handle_no_permission(self):
|
||||
if not self.request.user.is_authenticated:
|
||||
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"""
|
||||
def test_func(self):
|
||||
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
|
||||
self.request.user.role in ['ADMIN', 'SUPERUSER']
|
||||
user_role in ['ADMIN', 'SUPERUSER']
|
||||
)
|
||||
|
||||
def handle_no_permission(self):
|
||||
if not self.request.user.is_authenticated:
|
||||
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"""
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
if hasattr(self, 'request') and self.request.user.is_authenticated:
|
||||
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
|
||||
context['can_auto_approve'] = self.request.user.role in ['MODERATOR', 'ADMIN', 'SUPERUSER']
|
||||
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()
|
||||
obj = self.get_object() # type: ignore
|
||||
context['pending_edits'] = EditSubmission.objects.filter(
|
||||
content_type=ContentType.objects.get_for_model(obj),
|
||||
object_id=obj.id,
|
||||
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):
|
||||
context = super().get_context_data(**kwargs)
|
||||
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()
|
||||
obj = self.get_object() # type: ignore
|
||||
|
||||
# Get historical records ordered by date
|
||||
context['history'] = obj.history.all().select_related('history_user').order_by('-history_date')
|
||||
# Get historical records ordered by date if available
|
||||
history = getattr(obj, 'history', None)
|
||||
if history is not None:
|
||||
context['history'] = history.all().select_related('history_user').order_by('-history_date')
|
||||
else:
|
||||
context['history'] = []
|
||||
|
||||
# Get related edit submissions
|
||||
content_type = ContentType.objects.get_for_model(obj)
|
||||
content_type = ContentType.objects.get_for_model(obj.__class__)
|
||||
context['edit_submissions'] = EditSubmission.objects.filter(
|
||||
content_type=content_type,
|
||||
object_id=obj.id
|
||||
object_id=getattr(obj, 'id', None)
|
||||
).exclude(
|
||||
status='NEW'
|
||||
).select_related('user', 'handled_by').order_by('-created_at')
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
from typing import Any, Dict, Optional, Type, Union, cast
|
||||
from django.db import models
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.apps import apps
|
||||
from django.core.exceptions import ObjectDoesNotExist, FieldDoesNotExist
|
||||
from django.contrib.auth.base_user import AbstractBaseUser
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
|
||||
UserType = Union[AbstractBaseUser, AnonymousUser]
|
||||
|
||||
class EditSubmission(models.Model):
|
||||
STATUS_CHOICES = [
|
||||
@@ -78,60 +84,76 @@ class EditSubmission(models.Model):
|
||||
models.Index(fields=['status']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
action = "creation" if self.submission_type == 'CREATE' else "edit"
|
||||
target = self.content_object or self.content_type.model_class().__name__
|
||||
model_class = self.content_type.model_class()
|
||||
target = self.content_object or (model_class.__name__ if model_class else 'Unknown')
|
||||
return f"{action} by {self.user.username} on {target}"
|
||||
|
||||
def _resolve_foreign_keys(self, data):
|
||||
def _resolve_foreign_keys(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Convert foreign key IDs to model instances"""
|
||||
model_class = self.content_type.model_class()
|
||||
if not model_class:
|
||||
raise ValueError("Could not resolve model class")
|
||||
|
||||
resolved_data = data.copy()
|
||||
|
||||
for field_name, value in data.items():
|
||||
field = model_class._meta.get_field(field_name)
|
||||
if isinstance(field, models.ForeignKey) and value is not None:
|
||||
related_model = field.related_model
|
||||
resolved_data[field_name] = related_model.objects.get(id=value)
|
||||
try:
|
||||
field = model_class._meta.get_field(field_name)
|
||||
if isinstance(field, models.ForeignKey) and value is not None:
|
||||
related_model = field.related_model
|
||||
if related_model:
|
||||
resolved_data[field_name] = related_model.objects.get(id=value)
|
||||
except (FieldDoesNotExist, ObjectDoesNotExist):
|
||||
continue
|
||||
|
||||
return resolved_data
|
||||
|
||||
def approve(self, user):
|
||||
def approve(self, user: UserType) -> Optional[models.Model]:
|
||||
"""Approve the submission and apply the changes"""
|
||||
self.status = 'APPROVED'
|
||||
self.handled_by = user
|
||||
self.handled_by = user # type: ignore
|
||||
self.handled_at = timezone.now()
|
||||
|
||||
model_class = self.content_type.model_class()
|
||||
resolved_data = self._resolve_foreign_keys(self.changes)
|
||||
if not model_class:
|
||||
raise ValueError("Could not resolve model class")
|
||||
|
||||
if self.submission_type == 'CREATE':
|
||||
# Create new object
|
||||
obj = model_class(**resolved_data)
|
||||
obj.save()
|
||||
# Update object_id after creation
|
||||
self.object_id = obj.id
|
||||
else:
|
||||
# Apply changes to existing object
|
||||
obj = self.content_object
|
||||
for field, value in resolved_data.items():
|
||||
setattr(obj, field, value)
|
||||
obj.save()
|
||||
|
||||
self.save()
|
||||
return obj
|
||||
try:
|
||||
resolved_data = self._resolve_foreign_keys(self.changes)
|
||||
|
||||
def reject(self, user):
|
||||
if self.submission_type == 'CREATE':
|
||||
# Create new object
|
||||
obj = model_class(**resolved_data)
|
||||
obj.save()
|
||||
# Update object_id after creation
|
||||
self.object_id = getattr(obj, 'id', None)
|
||||
else:
|
||||
# Apply changes to existing object
|
||||
obj = self.content_object
|
||||
if not obj:
|
||||
raise ValueError("Content object not found")
|
||||
for field, value in resolved_data.items():
|
||||
setattr(obj, field, value)
|
||||
obj.save()
|
||||
|
||||
self.save()
|
||||
return obj
|
||||
except Exception as e:
|
||||
raise ValueError(f"Error approving submission: {str(e)}") from e
|
||||
|
||||
def reject(self, user: UserType) -> None:
|
||||
"""Reject the submission"""
|
||||
self.status = 'REJECTED'
|
||||
self.handled_by = user
|
||||
self.handled_by = user # type: ignore
|
||||
self.handled_at = timezone.now()
|
||||
self.save()
|
||||
|
||||
def escalate(self, user):
|
||||
def escalate(self, user: UserType) -> None:
|
||||
"""Escalate the submission to admin"""
|
||||
self.status = 'ESCALATED'
|
||||
self.handled_by = user
|
||||
self.handled_by = user # type: ignore
|
||||
self.handled_at = timezone.now()
|
||||
self.save()
|
||||
|
||||
@@ -189,15 +211,15 @@ class PhotoSubmission(models.Model):
|
||||
models.Index(fields=['status']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return f"Photo submission by {self.user.username} for {self.content_object}"
|
||||
|
||||
def approve(self, moderator, notes=''):
|
||||
def approve(self, moderator: UserType, notes: str = '') -> None:
|
||||
"""Approve the photo submission"""
|
||||
from media.models import Photo
|
||||
|
||||
self.status = 'APPROVED'
|
||||
self.handled_by = moderator
|
||||
self.handled_by = moderator # type: ignore
|
||||
self.handled_at = timezone.now()
|
||||
self.notes = notes
|
||||
|
||||
@@ -213,15 +235,15 @@ class PhotoSubmission(models.Model):
|
||||
|
||||
self.save()
|
||||
|
||||
def reject(self, moderator, notes):
|
||||
def reject(self, moderator: UserType, notes: str) -> None:
|
||||
"""Reject the photo submission"""
|
||||
self.status = 'REJECTED'
|
||||
self.handled_by = moderator
|
||||
self.handled_by = moderator # type: ignore
|
||||
self.handled_at = timezone.now()
|
||||
self.notes = notes
|
||||
self.save()
|
||||
|
||||
def auto_approve(self):
|
||||
def auto_approve(self) -> None:
|
||||
"""Auto-approve the photo submission (for moderators/admins)"""
|
||||
from media.models import Photo
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from companies.models import Company
|
||||
from django.views.generic import DetailView
|
||||
from django.test import RequestFactory
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
@@ -28,7 +29,7 @@ class TestView(EditSubmissionMixin, PhotoSubmissionMixin, InlineEditMixin, Histo
|
||||
self.object = self.get_object()
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def setup(self, request, *args, **kwargs):
|
||||
def setup(self, request: HttpRequest, *args, **kwargs):
|
||||
super().setup(request, *args, **kwargs)
|
||||
self.request = request
|
||||
|
||||
@@ -224,8 +225,7 @@ class ModerationMixinsTests(TestCase):
|
||||
def test_moderator_required_mixin(self):
|
||||
"""Test moderator required mixin"""
|
||||
class TestModeratorView(ModeratorRequiredMixin):
|
||||
def __init__(self):
|
||||
self.request = None
|
||||
pass
|
||||
|
||||
view = TestModeratorView()
|
||||
|
||||
@@ -253,8 +253,7 @@ class ModerationMixinsTests(TestCase):
|
||||
def test_admin_required_mixin(self):
|
||||
"""Test admin required mixin"""
|
||||
class TestAdminView(AdminRequiredMixin):
|
||||
def __init__(self):
|
||||
self.request = None
|
||||
pass
|
||||
|
||||
view = TestAdminView()
|
||||
|
||||
@@ -319,7 +318,7 @@ class ModerationMixinsTests(TestCase):
|
||||
EditSubmission.objects.create(
|
||||
user=self.user,
|
||||
content_type=ContentType.objects.get_for_model(Company),
|
||||
object_id=self.company.id,
|
||||
object_id=getattr(self.company, 'id', None),
|
||||
submission_type='EDIT',
|
||||
changes={'name': 'New Name'},
|
||||
status='APPROVED'
|
||||
|
||||
Reference in New Issue
Block a user