django-model-utils/model_utils/models.py
Maarten ter Huurne 0043fedf46 Enable postponed evaluation of annotations for all source modules
This allows using the latest annotation syntax supported by the type
checker regardless of the runtime Python version.
2024-06-13 12:02:05 +02:00

175 lines
5.1 KiB
Python

from __future__ import annotations
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.db.models.functions import Now
from django.utils.translation import gettext_lazy as _
from model_utils.fields import (
AutoCreatedField,
AutoLastModifiedField,
MonitorField,
StatusField,
UUIDField,
)
from model_utils.managers import QueryManager, SoftDeletableManager
now = Now()
class TimeStampedModel(models.Model):
"""
An abstract base class model that provides self-updating
``created`` and ``modified`` fields.
"""
created = AutoCreatedField(_('created'))
modified = AutoLastModifiedField(_('modified'))
def save(self, *args, **kwargs):
"""
Overriding the save method in order to make sure that
modified field is updated even if it is not given as
a parameter to the update field argument.
"""
update_fields = kwargs.get('update_fields', None)
if update_fields:
kwargs['update_fields'] = set(update_fields).union({'modified'})
super().save(*args, **kwargs)
class Meta:
abstract = True
class TimeFramedModel(models.Model):
"""
An abstract base class model that provides ``start``
and ``end`` fields to record a timeframe.
"""
start = models.DateTimeField(_('start'), null=True, blank=True)
end = models.DateTimeField(_('end'), null=True, blank=True)
class Meta:
abstract = True
class StatusModel(models.Model):
"""
An abstract base class model with a ``status`` field that
automatically uses a ``STATUS`` class attribute of choices, a
``status_changed`` date-time field that records when ``status``
was last modified, and an automatically-added manager for each
status that returns objects with that status only.
"""
status = StatusField(_('status'))
status_changed = MonitorField(_('status changed'), monitor='status')
def save(self, *args, **kwargs):
"""
Overriding the save method in order to make sure that
status_changed field is updated even if it is not given as
a parameter to the update field argument.
"""
update_fields = kwargs.get('update_fields', None)
if update_fields and 'status' in update_fields:
kwargs['update_fields'] = set(update_fields).union({'status_changed'})
super().save(*args, **kwargs)
class Meta:
abstract = True
def add_status_query_managers(sender, **kwargs):
"""
Add a Querymanager for each status item dynamically.
"""
if not issubclass(sender, StatusModel):
return
default_manager = sender._meta.default_manager
for value, display in getattr(sender, 'STATUS', ()):
if _field_exists(sender, value):
raise ImproperlyConfigured(
"StatusModel: Model '%s' has a field named '%s' which "
"conflicts with a status of the same name."
% (sender.__name__, value)
)
sender.add_to_class(value, QueryManager(status=value))
sender._meta.default_manager_name = default_manager.name
def add_timeframed_query_manager(sender, **kwargs):
"""
Add a QueryManager for a specific timeframe.
"""
if not issubclass(sender, TimeFramedModel):
return
if _field_exists(sender, 'timeframed'):
raise ImproperlyConfigured(
"Model '%s' has a field named 'timeframed' "
"which conflicts with the TimeFramedModel manager."
% sender.__name__
)
sender.add_to_class('timeframed', QueryManager(
(models.Q(start__lte=now) | models.Q(start__isnull=True))
& (models.Q(end__gte=now) | models.Q(end__isnull=True))
))
models.signals.class_prepared.connect(add_status_query_managers)
models.signals.class_prepared.connect(add_timeframed_query_manager)
def _field_exists(model_class, field_name):
return field_name in [f.attname for f in model_class._meta.local_fields]
class SoftDeletableModel(models.Model):
"""
An abstract base class model with a ``is_removed`` field that
marks entries that are not going to be used anymore, but are
kept in db for any reason.
Default manager returns only not-removed entries.
"""
is_removed = models.BooleanField(default=False)
class Meta:
abstract = True
objects = SoftDeletableManager(_emit_deprecation_warnings=True)
available_objects = SoftDeletableManager()
all_objects = models.Manager()
def delete(self, using=None, *args, soft=True, **kwargs):
"""
Soft delete object (set its ``is_removed`` field to True).
Actually delete object if setting ``soft`` to False.
"""
if soft:
self.is_removed = True
self.save(using=using)
else:
return super().delete(using, *args, **kwargs)
class UUIDModel(models.Model):
"""
This abstract base class provides id field on any model that inherits from it
which will be the primary key.
"""
id = UUIDField(
primary_key=True,
version=4,
editable=False,
)
class Meta:
abstract = True