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,19 +1,18 @@
from typing import Any, Dict, Optional, Type, Union, cast
from typing import Any, Dict, Optional, Type, Union
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
from django.utils.text import slugify
import pghistory
from core.history import TrackedModel
UserType = Union[AbstractBaseUser, AnonymousUser]
@pghistory.track() # Track all changes by default
class EditSubmission(TrackedModel):
STATUS_CHOICES = [
@@ -51,12 +50,12 @@ class EditSubmission(TrackedModel):
changes = models.JSONField(
help_text="JSON representation of the changes or new object data"
)
# Moderator's edited version of changes before approval
moderator_changes = models.JSONField(
null=True,
blank=True,
help_text="Moderator's edited version of the changes before approval"
help_text="Moderator's edited version of the changes before approval",
)
# Metadata
@@ -104,7 +103,11 @@ class EditSubmission(TrackedModel):
for field_name, value in data.items():
try:
if (field := model_class._meta.get_field(field_name)) and isinstance(field, models.ForeignKey) and value is not None:
if (
(field := model_class._meta.get_field(field_name))
and isinstance(field, models.ForeignKey)
and value is not None
):
if related_model := field.related_model:
resolved_data[field_name] = related_model.objects.get(id=value)
except (FieldDoesNotExist, ObjectDoesNotExist):
@@ -112,28 +115,33 @@ class EditSubmission(TrackedModel):
return resolved_data
def _prepare_model_data(self, data: Dict[str, Any], model_class: Type[models.Model]) -> Dict[str, Any]:
def _prepare_model_data(
self, data: Dict[str, Any], model_class: Type[models.Model]
) -> Dict[str, Any]:
"""Prepare data for model creation/update by filtering out auto-generated fields"""
prepared_data = data.copy()
# Remove fields that are auto-generated or handled by the model's save method
auto_fields = {'created_at', 'updated_at', 'slug'}
# Remove fields that are auto-generated or handled by the model's save
# method
auto_fields = {"created_at", "updated_at", "slug"}
for field in auto_fields:
prepared_data.pop(field, None)
# Set default values for required fields if not provided
for field in model_class._meta.fields:
if not field.auto_created and not field.blank and not field.null:
if field.name not in prepared_data and field.has_default():
prepared_data[field.name] = field.get_default()
return prepared_data
def _check_duplicate_name(self, model_class: Type[models.Model], name: str) -> Optional[models.Model]:
def _check_duplicate_name(
self, model_class: Type[models.Model], name: str
) -> Optional[models.Model]:
"""Check if an object with the same name already exists"""
try:
return model_class.objects.filter(name=name).first()
except:
except BaseException:
return None
def approve(self, user: UserType) -> Optional[models.Model]:
@@ -142,19 +150,29 @@ class EditSubmission(TrackedModel):
raise ValueError("Could not resolve model class")
try:
# Use moderator_changes if available, otherwise use original changes
changes_to_apply = self.moderator_changes if self.moderator_changes is not None else self.changes
# Use moderator_changes if available, otherwise use original
# changes
changes_to_apply = (
self.moderator_changes
if self.moderator_changes is not None
else self.changes
)
resolved_data = self._resolve_foreign_keys(changes_to_apply)
prepared_data = self._prepare_model_data(resolved_data, model_class)
# For CREATE submissions, check for duplicates by name
if self.submission_type == "CREATE" and "name" in prepared_data:
if existing_obj := self._check_duplicate_name(model_class, prepared_data["name"]):
if existing_obj := self._check_duplicate_name(
model_class, prepared_data["name"]
):
self.status = "REJECTED"
self.handled_by = user # type: ignore
self.handled_at = timezone.now()
self.notes = f"A {model_class.__name__} with the name '{prepared_data['name']}' already exists (ID: {existing_obj.id})"
self.notes = f"A {
model_class.__name__} with the name '{
prepared_data['name']}' already exists (ID: {
existing_obj.id})"
self.save()
raise ValueError(self.notes)
@@ -185,7 +203,9 @@ class EditSubmission(TrackedModel):
self.save()
return obj
except Exception as e:
if self.status != "REJECTED": # Don't override if already rejected due to duplicate
if (
self.status != "REJECTED"
): # Don't override if already rejected due to duplicate
self.status = "PENDING" # Reset status if approval failed
self.save()
raise ValueError(f"Error approving submission: {str(e)}") from e
@@ -204,6 +224,7 @@ class EditSubmission(TrackedModel):
self.handled_at = timezone.now()
self.save()
@pghistory.track() # Track all changes by default
class PhotoSubmission(TrackedModel):
STATUS_CHOICES = [
@@ -244,7 +265,8 @@ class PhotoSubmission(TrackedModel):
)
handled_at = models.DateTimeField(null=True, blank=True)
notes = models.TextField(
blank=True, help_text="Notes from the moderator about this photo submission"
blank=True,
help_text="Notes from the moderator about this photo submission",
)
class Meta:
@@ -255,7 +277,9 @@ class PhotoSubmission(TrackedModel):
]
def __str__(self) -> str:
return f"Photo submission by {self.user.username} for {self.content_object}"
return f"Photo submission by {
self.user.username} for {
self.content_object}"
def approve(self, moderator: UserType, notes: str = "") -> None:
"""Approve the photo submission"""
@@ -285,12 +309,12 @@ class PhotoSubmission(TrackedModel):
self.handled_at = timezone.now()
self.notes = notes
self.save()
def auto_approve(self) -> None:
"""Auto-approve submissions from moderators"""
# Get user role safely
user_role = getattr(self.user, "role", None)
# If user is moderator or above, auto-approve
if user_role in ["MODERATOR", "ADMIN", "SUPERUSER"]:
self.approve(self.user)