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