|
from django.db import models |
|
|
|
|
|
class TimestampStateField(models.Field): |
|
""" |
|
An state timestamp field that automatically creates a generated boolean field. |
|
|
|
The boolean field name is automatically derived from the timestamp field name. |
|
|
|
Example usage: |
|
|
|
class User(models.Model): |
|
active_at = TimestampStateField() # Creates 'is_active' bool field |
|
verified_at = TimestampStateField() # Creates 'is_verified' bool field |
|
|
|
User.objects.filter(is_active=True) # Filter active users |
|
User.objects.filter(active_at__year=2021) # Filter users activated in 2021 |
|
""" |
|
|
|
boolean_field_prefix: str |
|
boolean_field_name: str | None |
|
datetime_kwargs: dict |
|
|
|
def __init__( |
|
self, |
|
*args, |
|
boolean_field_prefix: str = "is", |
|
boolean_field_name: str | None = None, |
|
**kwargs, |
|
): |
|
self.boolean_field_prefix = boolean_field_prefix |
|
self.boolean_field_name = boolean_field_name |
|
|
|
kwargs.setdefault("null", True) |
|
kwargs.setdefault("blank", True) |
|
|
|
if "help_text" not in kwargs: |
|
kwargs["help_text"] = ( |
|
"Timestamp when this state was set to true. A null value indicates the state is false." |
|
) |
|
|
|
self.datetime_kwargs = kwargs |
|
|
|
def contribute_to_class(self, cls: type[models.Model], name): |
|
self._add_datetime_field(cls, name) |
|
self._add_generated_field(cls, name) |
|
|
|
def _add_datetime_field(self, cls: type[models.Model], name: str) -> None: |
|
field = models.DateTimeField(**self.datetime_kwargs) |
|
field.contribute_to_class(cls, name) |
|
|
|
def _add_generated_field(self, cls: type[models.Model], name: str) -> None: |
|
field_name = ( |
|
self.boolean_field_name |
|
if self.boolean_field_name |
|
else f"{self.boolean_field_prefix}_{name.removesuffix("_at")}" |
|
) |
|
|
|
field = models.GeneratedField( |
|
expression=models.Q((f"{name}__isnull", True)), |
|
output_field=models.BooleanField(), |
|
db_persist=True, |
|
help_text=f"Generated from {name}", |
|
) |
|
field.contribute_to_class(cls, field_name) |
|
|
|
def deconstruct(self): |
|
name, path, args, kwargs = super().deconstruct() |
|
if "null" in kwargs: |
|
del kwargs["null"] |
|
if "blank" in kwargs: |
|
del kwargs["blank"] |
|
if "help_text" in kwargs: |
|
del kwargs["help_text"] |
|
return name, path, args, kwargs |
|
|