""" Core base models and utilities for ThrillWiki. These abstract models provide common functionality for all entities. """ import uuid from django.db import models from model_utils.models import TimeStampedModel from django_lifecycle import LifecycleModel, hook, AFTER_CREATE, AFTER_UPDATE from dirtyfields import DirtyFieldsMixin class BaseModel(LifecycleModel, TimeStampedModel): """ Abstract base model for all entities. Provides: - UUID primary key - created_at and updated_at timestamps (from TimeStampedModel) - Lifecycle hooks for versioning """ id = models.UUIDField( primary_key=True, default=uuid.uuid4, editable=False ) class Meta: abstract = True def __str__(self): return f"{self.__class__.__name__}({self.id})" class VersionedModel(DirtyFieldsMixin, BaseModel): """ Abstract base model for entities that need version tracking. Automatically creates a version record whenever the model is created or updated. Uses DirtyFieldsMixin to track which fields changed. """ @hook(AFTER_CREATE) def create_version_on_create(self): """Create initial version when entity is created""" self._create_version('created') @hook(AFTER_UPDATE) def create_version_on_update(self): """Create version when entity is updated""" if self.get_dirty_fields(): self._create_version('updated') def _create_version(self, change_type): """ Create a version record for this entity. Deferred import to avoid circular dependencies. """ try: from apps.versioning.services import VersionService VersionService.create_version( entity=self, change_type=change_type, changed_fields=self.get_dirty_fields() if change_type == 'updated' else {} ) except ImportError: # Versioning app not yet available (e.g., during initial migrations) pass class Meta: abstract = True # Location Models class Country(BaseModel): """ Country reference data (ISO 3166-1). Examples: United States, Canada, United Kingdom, etc. """ name = models.CharField(max_length=255, unique=True) code = models.CharField( max_length=2, unique=True, help_text="ISO 3166-1 alpha-2 country code" ) code3 = models.CharField( max_length=3, blank=True, help_text="ISO 3166-1 alpha-3 country code" ) class Meta: db_table = 'countries' ordering = ['name'] verbose_name_plural = 'countries' def __str__(self): return self.name class Subdivision(BaseModel): """ State/Province/Region reference data (ISO 3166-2). Examples: California, Ontario, England, etc. """ country = models.ForeignKey( Country, on_delete=models.CASCADE, related_name='subdivisions' ) name = models.CharField(max_length=255) code = models.CharField( max_length=10, help_text="ISO 3166-2 subdivision code (without country prefix)" ) subdivision_type = models.CharField( max_length=50, blank=True, help_text="Type of subdivision (state, province, region, etc.)" ) class Meta: db_table = 'subdivisions' ordering = ['country', 'name'] unique_together = [['country', 'code']] def __str__(self): return f"{self.name}, {self.country.code}" class Locality(BaseModel): """ City/Town reference data. Examples: Los Angeles, Toronto, London, etc. """ subdivision = models.ForeignKey( Subdivision, on_delete=models.CASCADE, related_name='localities' ) name = models.CharField(max_length=255) latitude = models.DecimalField( max_digits=9, decimal_places=6, null=True, blank=True ) longitude = models.DecimalField( max_digits=9, decimal_places=6, null=True, blank=True ) class Meta: db_table = 'localities' ordering = ['subdivision', 'name'] verbose_name_plural = 'localities' indexes = [ models.Index(fields=['subdivision', 'name']), ] def __str__(self): return f"{self.name}, {self.subdivision.code}" @property def full_location(self): """Return full location string: City, State, Country""" return f"{self.name}, {self.subdivision.name}, {self.subdivision.country.name}" # Date Precision Tracking class DatePrecisionMixin(models.Model): """ Mixin for models that need to track date precision. Allows tracking whether a date is known to year, month, or day precision. This is important for historical records where exact dates may not be known. """ DATE_PRECISION_CHOICES = [ ('year', 'Year'), ('month', 'Month'), ('day', 'Day'), ] class Meta: abstract = True @classmethod def add_date_precision_field(cls, field_name): """ Helper to add a precision field for a date field. Usage in subclass: opening_date = models.DateField(null=True, blank=True) opening_date_precision = models.CharField(...) """ return models.CharField( max_length=20, choices=cls.DATE_PRECISION_CHOICES, default='day', help_text=f"Precision level for {field_name}" ) # Soft Delete Mixin class SoftDeleteMixin(models.Model): """ Mixin for soft-deletable models. Instead of actually deleting records, mark them as deleted. This preserves data integrity and allows for undelete functionality. """ is_deleted = models.BooleanField(default=False, db_index=True) deleted_at = models.DateTimeField(null=True, blank=True) deleted_by = models.ForeignKey( 'users.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='%(class)s_deletions' ) class Meta: abstract = True def soft_delete(self, user=None): """Mark this record as deleted""" from django.utils import timezone self.is_deleted = True self.deleted_at = timezone.now() if user: self.deleted_by = user self.save(update_fields=['is_deleted', 'deleted_at', 'deleted_by']) def undelete(self): """Restore a soft-deleted record""" self.is_deleted = False self.deleted_at = None self.deleted_by = None self.save(update_fields=['is_deleted', 'deleted_at', 'deleted_by']) # Model Managers class ActiveManager(models.Manager): """Manager that filters out soft-deleted records by default""" def get_queryset(self): return super().get_queryset().filter(is_deleted=False) class AllObjectsManager(models.Manager): """Manager that includes all records, even soft-deleted ones""" def get_queryset(self): return super().get_queryset()