Refactor test utilities and enhance ASGI settings

- Cleaned up and standardized assertions in ApiTestMixin for API response validation.
- Updated ASGI settings to use os.environ for setting the DJANGO_SETTINGS_MODULE.
- Removed unused imports and improved formatting in settings.py.
- Refactored URL patterns in urls.py for better readability and organization.
- Enhanced view functions in views.py for consistency and clarity.
- Added .flake8 configuration for linting and style enforcement.
- Introduced type stubs for django-environ to improve type checking with Pylance.
This commit is contained in:
pacnpal
2025-08-20 19:51:59 -04:00
parent 69c07d1381
commit 66ed4347a9
230 changed files with 15094 additions and 11578 deletions

View File

@@ -1,45 +1,57 @@
from typing import Any, Dict, Optional, Type, Union, cast
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from typing import Any, Dict, Optional, Type, cast
from django.contrib.auth.mixins import 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.http import (
JsonResponse,
HttpResponseForbidden,
HttpRequest,
HttpResponse,
)
from django.views.generic import DetailView
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:
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)
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")
@@ -53,89 +65,101 @@ class EditSubmissionMixin(DetailView):
submission_type=submission_type,
changes=changes,
reason=reason,
source=source
source=source,
)
# For edits, set the object_id
if submission_type == 'EDIT':
if submission_type == "EDIT":
obj = self.get_object()
submission.object_id = getattr(obj, 'id', None)
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']:
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)()
})
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
})
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)
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')
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)
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)
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)
return JsonResponse(
{"status": "error", "message": "Invalid JSON data."},
status=400,
)
except Exception as e:
return JsonResponse({
'status': 'error',
'message': str(e)
}, status=500)
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)
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")
@@ -143,125 +167,148 @@ class PhotoSubmissionMixin(DetailView):
try:
obj = self.get_object()
except (AttributeError, self.model.DoesNotExist):
return JsonResponse({
'status': 'error',
'message': 'Invalid object.'
}, status=400)
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)
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')
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']:
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
})
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
})
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']
)
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']
)
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']
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')
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()
context["history"] = obj.get_history()
except (AttributeError, TypeError):
context['history'] = []
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')
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