You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

110 lines
3.3 KiB

from django.db import models
from django.utils.translation import gettext_lazy as _
# 1. Define choices globally so they can be reused
class StatusColorChoices(models.TextChoices):
RED = 'red', _('Red')
GREEN = 'green', _('Green')
BLUE = 'blue', _('Blue')
YELLOW = 'yellow', _('Yellow')
ORANGE = 'orange', _('Orange')
PURPLE = 'purple', _('Purple')
GRAY = 'gray', _('Gray')
# 2. Create the Abstract Mixin
class ColorPaletteMixin(models.Model):
"""
An abstract base class that provides color choice fields
and hex code properties to any model that inherits it.
"""
COLOR_PALETTE = {
'red': '#D33A3A',
'green': '#1DAC43',
'blue': '#5172E1',
'yellow': '#EDC130',
'orange': '#E67E22',
'purple': '#7C5CC4',
'gray': '#374151',
}
color = models.CharField(
max_length=20,
choices=StatusColorChoices.choices,
verbose_name=_('color'),
default=StatusColorChoices.GRAY
)
class Meta:
abstract = True
@property
def main_color_code(self):
return self.COLOR_PALETTE.get(self.color, '#000000')
from django.utils.text import slugify
from django.http import HttpResponsePermanentRedirect
from django.urls import reverse
class LowercaseSlugMixin(models.Model):
"""
Mixin to automatically generate a lowercase, unicode-supporting unique slug
from a source field (default: 'title') on save.
"""
slug_source_field = 'title'
class Meta:
abstract = True
def save(self, *args, **kwargs):
from utils.slug import build_slug_for_instance
build_slug_for_instance(self, source_field=self.slug_source_field)
super().save(*args, **kwargs)
class CanonicalSlugViewSetMixin:
"""
Mixin for DRF Views/ViewSets to enforce lowercase slugs and redirect with 301.
"""
def dispatch(self, request, *args, **kwargs):
if request.method == 'GET':
slug = kwargs.get('slug')
if slug and any(char.isupper() for char in slug):
resolved = request.resolver_match
new_kwargs = {**resolved.kwargs, 'slug': slug.lower()}
# Reconstruct path using reverse
new_path = reverse(resolved.view_name, args=resolved.args, kwargs=new_kwargs)
# Ensure trailing slash
if not new_path.endswith('/'):
new_path += '/'
# Append query parameters if any
query_string = request.META.get('QUERY_STRING')
if query_string:
new_path = f"{new_path}?{query_string}"
return HttpResponsePermanentRedirect(new_path)
return super().dispatch(request, *args, **kwargs)
from utils.exceptions import ResourceGone
class Gone410ViewMixin:
"""
Mixin for DRF detail Views/ViewSets to return 410 Gone instead of 404
for inactive/soft-deleted records.
"""
active_field_name = 'status'
active_expected_value = True
def get_object(self):
obj = super().get_object()
active_val = getattr(obj, self.active_field_name, None)
if active_val != self.active_expected_value:
raise ResourceGone()
return obj