|
4ce269c…
|
ragelink
|
1 |
import uuid |
|
4ce269c…
|
ragelink
|
2 |
|
|
4ce269c…
|
ragelink
|
3 |
from django.conf import settings |
|
4ce269c…
|
ragelink
|
4 |
from django.db import models |
|
4ce269c…
|
ragelink
|
5 |
from django.utils import timezone |
|
4ce269c…
|
ragelink
|
6 |
from django.utils.text import slugify |
|
4ce269c…
|
ragelink
|
7 |
from simple_history.models import HistoricalRecords |
|
4ce269c…
|
ragelink
|
8 |
|
|
4ce269c…
|
ragelink
|
9 |
|
|
4ce269c…
|
ragelink
|
10 |
class Tracking(models.Model): |
|
4ce269c…
|
ragelink
|
11 |
"""Abstract base providing audit trails and soft deletes for all business models.""" |
|
4ce269c…
|
ragelink
|
12 |
|
|
4ce269c…
|
ragelink
|
13 |
version = models.PositiveIntegerField(default=1, editable=False) |
|
4ce269c…
|
ragelink
|
14 |
created_at = models.DateTimeField(auto_now_add=True) |
|
4ce269c…
|
ragelink
|
15 |
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name="+") |
|
4ce269c…
|
ragelink
|
16 |
updated_at = models.DateTimeField(auto_now=True) |
|
4ce269c…
|
ragelink
|
17 |
updated_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name="+") |
|
4ce269c…
|
ragelink
|
18 |
deleted_at = models.DateTimeField(null=True, blank=True) |
|
4ce269c…
|
ragelink
|
19 |
deleted_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name="+") |
|
4ce269c…
|
ragelink
|
20 |
history = HistoricalRecords(inherit=True) |
|
4ce269c…
|
ragelink
|
21 |
|
|
4ce269c…
|
ragelink
|
22 |
class Meta: |
|
4ce269c…
|
ragelink
|
23 |
abstract = True |
|
4ce269c…
|
ragelink
|
24 |
|
|
4ce269c…
|
ragelink
|
25 |
def save(self, *args, **kwargs): |
|
4ce269c…
|
ragelink
|
26 |
if self.pk: |
|
4ce269c…
|
ragelink
|
27 |
self.version += 1 |
|
4ce269c…
|
ragelink
|
28 |
super().save(*args, **kwargs) |
|
4ce269c…
|
ragelink
|
29 |
|
|
4ce269c…
|
ragelink
|
30 |
def soft_delete(self, user=None): |
|
4ce269c…
|
ragelink
|
31 |
self.deleted_at = timezone.now() |
|
4ce269c…
|
ragelink
|
32 |
self.deleted_by = user |
|
4ce269c…
|
ragelink
|
33 |
self.save(update_fields=["deleted_at", "deleted_by", "updated_at", "version"]) |
|
4ce269c…
|
ragelink
|
34 |
|
|
4ce269c…
|
ragelink
|
35 |
@property |
|
4ce269c…
|
ragelink
|
36 |
def is_deleted(self): |
|
4ce269c…
|
ragelink
|
37 |
return self.deleted_at is not None |
|
4ce269c…
|
ragelink
|
38 |
|
|
4ce269c…
|
ragelink
|
39 |
|
|
4ce269c…
|
ragelink
|
40 |
class BaseCoreModel(Tracking): |
|
4ce269c…
|
ragelink
|
41 |
"""Abstract base for named, addressable entities with UUID external identifiers.""" |
|
4ce269c…
|
ragelink
|
42 |
|
|
4ce269c…
|
ragelink
|
43 |
guid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False, db_index=True) |
|
4ce269c…
|
ragelink
|
44 |
name = models.CharField(max_length=200) |
|
4ce269c…
|
ragelink
|
45 |
slug = models.SlugField(max_length=200, unique=True, db_index=True) |
|
4ce269c…
|
ragelink
|
46 |
description = models.TextField(blank=True, default="") |
|
4ce269c…
|
ragelink
|
47 |
|
|
4ce269c…
|
ragelink
|
48 |
class Meta: |
|
4ce269c…
|
ragelink
|
49 |
abstract = True |
|
4ce269c…
|
ragelink
|
50 |
|
|
4ce269c…
|
ragelink
|
51 |
def __str__(self): |
|
4ce269c…
|
ragelink
|
52 |
return self.name |
|
4ce269c…
|
ragelink
|
53 |
|
|
4ce269c…
|
ragelink
|
54 |
def save(self, *args, **kwargs): |
|
4ce269c…
|
ragelink
|
55 |
if not self.slug: |
|
4ce269c…
|
ragelink
|
56 |
base_slug = slugify(self.name) |
|
4ce269c…
|
ragelink
|
57 |
slug = base_slug |
|
4ce269c…
|
ragelink
|
58 |
counter = 1 |
|
4ce269c…
|
ragelink
|
59 |
model_class = type(self) |
|
4ce269c…
|
ragelink
|
60 |
while model_class.objects.filter(slug=slug).exclude(pk=self.pk).exists(): |
|
4ce269c…
|
ragelink
|
61 |
slug = f"{base_slug}-{counter}" |
|
4ce269c…
|
ragelink
|
62 |
counter += 1 |
|
4ce269c…
|
ragelink
|
63 |
self.slug = slug |
|
4ce269c…
|
ragelink
|
64 |
super().save(*args, **kwargs) |
|
4ce269c…
|
ragelink
|
65 |
|
|
4ce269c…
|
ragelink
|
66 |
def get_absolute_url(self): |
|
4ce269c…
|
ragelink
|
67 |
return f"/{self._meta.app_label}/{self.slug}/" |
|
4ce269c…
|
ragelink
|
68 |
|
|
4ce269c…
|
ragelink
|
69 |
|
|
4ce269c…
|
ragelink
|
70 |
class ActiveManager(models.Manager): |
|
4ce269c…
|
ragelink
|
71 |
"""Manager that excludes soft-deleted records by default.""" |
|
4ce269c…
|
ragelink
|
72 |
|
|
4ce269c…
|
ragelink
|
73 |
def get_queryset(self): |
|
4ce269c…
|
ragelink
|
74 |
return super().get_queryset().filter(deleted_at__isnull=True) |