fixed a bunch of things, hopefully didn't break things

This commit is contained in:
pacnpal
2024-11-05 21:51:02 +00:00
parent 2e8a725933
commit eb5d2acab5
30 changed files with 944 additions and 569 deletions

View File

@@ -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')

View File

@@ -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

View File

@@ -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'