first commit

This commit is contained in:
pacnpal
2024-10-28 17:09:57 -04:00
commit 2e1b4d7af7
9993 changed files with 1182741 additions and 0 deletions

0
core/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

34
core/admin.py Normal file
View File

@@ -0,0 +1,34 @@
from django.contrib import admin
from django.contrib.contenttypes.models import ContentType
from django.utils.html import format_html
from .models import SlugHistory
@admin.register(SlugHistory)
class SlugHistoryAdmin(admin.ModelAdmin):
list_display = ['content_object_link', 'old_slug', 'created_at']
list_filter = ['content_type', 'created_at']
search_fields = ['old_slug', 'object_id']
readonly_fields = ['content_type', 'object_id', 'old_slug', 'created_at']
date_hierarchy = 'created_at'
ordering = ['-created_at']
def content_object_link(self, obj):
"""Create a link to the related object's admin page"""
try:
url = obj.content_object.get_absolute_url()
return format_html(
'<a href="{}">{}</a>',
url,
str(obj.content_object)
)
except (AttributeError, ValueError):
return str(obj.content_object)
content_object_link.short_description = 'Object'
def has_add_permission(self, request):
"""Disable manual creation of slug history records"""
return False
def has_change_permission(self, request, obj=None):
"""Disable editing of slug history records"""
return False

5
core/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class CoreConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'core'

View File

@@ -0,0 +1,31 @@
# Generated by Django 5.1.2 on 2024-10-28 20:17
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.CreateModel(
name='SlugHistory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.CharField(max_length=50)),
('old_slug', models.SlugField(max_length=200)),
('created_at', models.DateTimeField(auto_now_add=True)),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
],
options={
'verbose_name_plural': 'Slug histories',
'ordering': ['-created_at'],
'indexes': [models.Index(fields=['content_type', 'object_id'], name='core_slughi_content_8bbf56_idx'), models.Index(fields=['old_slug'], name='core_slughi_old_slu_aaef7f_idx')],
},
),
]

View File

Binary file not shown.

92
core/models.py Normal file
View File

@@ -0,0 +1,92 @@
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.utils.text import slugify
class SlugHistory(models.Model):
"""
Model for tracking slug changes across all models that use slugs.
Uses generic relations to work with any model.
"""
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.CharField(max_length=50) # Using CharField to work with our custom IDs
content_object = GenericForeignKey('content_type', 'object_id')
old_slug = models.SlugField(max_length=200)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
models.Index(fields=['content_type', 'object_id']),
models.Index(fields=['old_slug']),
]
verbose_name_plural = 'Slug histories'
ordering = ['-created_at']
def __str__(self):
return f"Old slug '{self.old_slug}' for {self.content_object}"
class SluggedModel(models.Model):
"""
Abstract base model that provides slug functionality with history tracking.
"""
name = models.CharField(max_length=200)
slug = models.SlugField(max_length=200, unique=True)
class Meta:
abstract = True
def save(self, *args, **kwargs):
# Get the current instance from DB if it exists
if self.pk:
try:
old_instance = self.__class__.objects.get(pk=self.pk)
# If slug has changed, save the old one to history
if old_instance.slug != self.slug:
SlugHistory.objects.create(
content_type=ContentType.objects.get_for_model(self),
object_id=getattr(self, self.get_id_field_name()),
old_slug=old_instance.slug
)
except self.__class__.DoesNotExist:
pass
# Generate slug if not set
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def get_id_field_name(self):
"""
Returns the name of the read-only ID field for this model.
Should be overridden by subclasses.
"""
raise NotImplementedError(
"Subclasses of SluggedModel must implement get_id_field_name()"
)
@classmethod
def get_by_slug(cls, slug):
"""
Get an object by its current or historical slug.
Returns (object, is_old_slug) tuple.
"""
try:
# Try to get by current slug first
return cls.objects.get(slug=slug), False
except cls.DoesNotExist:
# Try to find in slug history
history = SlugHistory.objects.filter(
content_type=ContentType.objects.get_for_model(cls),
old_slug=slug
).order_by('-created_at').first()
if history:
return cls.objects.get(
**{cls.get_id_field_name(): history.object_id}
), True
raise cls.DoesNotExist(
f"{cls.__name__} with slug '{slug}' does not exist"
)

3
core/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

43
core/views.py Normal file
View File

@@ -0,0 +1,43 @@
from django.shortcuts import redirect
from django.urls import reverse
class SlugRedirectMixin:
"""
Mixin that handles redirects for old slugs.
Requires the model to inherit from SluggedModel.
"""
def get(self, request, *args, **kwargs):
# Get the object using current or historical slug
try:
self.object = self.get_object()
# Check if we used an old slug
current_slug = kwargs.get(self.slug_url_kwarg)
if current_slug != self.object.slug:
# Get the URL pattern name from the view
url_pattern = self.get_redirect_url_pattern()
# Build kwargs for reverse()
reverse_kwargs = self.get_redirect_url_kwargs()
# Redirect to the current slug URL
return redirect(
reverse(url_pattern, kwargs=reverse_kwargs),
permanent=True
)
return super().get(request, *args, **kwargs)
except self.model.DoesNotExist:
return super().get(request, *args, **kwargs)
def get_redirect_url_pattern(self):
"""
Get the URL pattern name for redirects.
Should be overridden by subclasses.
"""
raise NotImplementedError(
"Subclasses must implement get_redirect_url_pattern()"
)
def get_redirect_url_kwargs(self):
"""
Get the kwargs for reverse() when redirecting.
Should be overridden by subclasses if they need custom kwargs.
"""
return {self.slug_url_kwarg: self.object.slug}