Changeset View
Changeset View
Standalone View
Standalone View
looper/model_mixins.py
| import copy | import copy | ||||
| import typing | from typing import Any, Mapping, Set, Tuple, Union | ||||
| from django.db import models | from django.db import models | ||||
| from django.db.models.expressions import Combinable | |||||
| OldStateType = typing.Mapping[str, typing.Any] | OldStateType = Mapping[str, Any] | ||||
| """Type declaration for the old state of a model instance. | """Type declaration for the old state of a model instance. | ||||
| See RecordModificationMixin.pre_save_record(). | See RecordModificationMixin.pre_save_record(). | ||||
| """ | """ | ||||
| class CreatedUpdatedMixin(models.Model): | class CreatedUpdatedMixin(models.Model): | ||||
| """Store creation and update timestamps.""" | """Store creation and update timestamps.""" | ||||
| Show All 12 Lines | class RecordModifcationMixin(models.Model): | ||||
| the model can send signals upon changes in fields. | the model can send signals upon changes in fields. | ||||
| Only acts on fields listed in self.record_modification_fields. | Only acts on fields listed in self.record_modification_fields. | ||||
| """ | """ | ||||
| class Meta: | class Meta: | ||||
| abstract = True | abstract = True | ||||
| record_modification_fields: typing.Set[str] | record_modification_fields: Set[str] | ||||
| def _check_modification(self, old_instance) -> bool: | def _check_modification(self, old_instance: object) -> bool: | ||||
| """Returns True iff the membership was modified. | """Returns True iff the membership was modified. | ||||
| Only checks fields listed in self.record_modification_fields. | Only checks fields listed in self.record_modification_fields. | ||||
| """ | """ | ||||
| for field in self.record_modification_fields: | for field in self.record_modification_fields: | ||||
| old_val = getattr(old_instance, field, ...) | old_val = getattr(old_instance, field, ...) | ||||
| new_val = getattr(self, field, ...) | new_val = getattr(self, field, ...) | ||||
| if old_val != new_val: | if old_val != new_val: | ||||
| return True | return True | ||||
| return False | return False | ||||
| def pre_save_record(self) -> typing.Tuple[bool, OldStateType]: | def pre_save_record(self) -> Tuple[bool, OldStateType]: | ||||
| """Records the previous state of this object. | """Records the previous state of this object. | ||||
| Only records fields listed in self.record_modification_fields. | Only records fields listed in self.record_modification_fields. | ||||
| :returns: (was changed, old state) tuple. | :returns: (was changed, old state) tuple. | ||||
| """ | """ | ||||
| if not self.pk: | if not self.pk: | ||||
| return True, {} | return True, {} | ||||
| try: | try: | ||||
| db_instance = type(self).objects.get(id=self.pk) | db_instance = type(self).objects.get(id=self.pk) | ||||
| except type(self).DoesNotExist: | except type(self).DoesNotExist: | ||||
| return True, {} | return True, {} | ||||
| was_modified = self._check_modification(db_instance) | was_modified = self._check_modification(db_instance) | ||||
| old_instance_data = {attr: copy.deepcopy(getattr(db_instance, attr)) | old_instance_data = { | ||||
| for attr in self.record_modification_fields} | attr: copy.deepcopy(getattr(db_instance, attr)) | ||||
| for attr in self.record_modification_fields | |||||
| } | |||||
| return was_modified, old_instance_data | return was_modified, old_instance_data | ||||
| class StateMachineMixin(models.Model): | class StateMachineMixin(models.Model): | ||||
| """Model with a 'status' field and well-defined status transitions. | """Model with a 'status' field and well-defined status transitions. | ||||
| Python code can perform any state transition. Manual changes via the | Python code can perform any state transition. Manual changes via the | ||||
| admin can be handled by using a custom Admin form that subclasses | admin can be handled by using a custom Admin form that subclasses | ||||
| `looper.forms.StateMachineMixin`. | `looper.forms.StateMachineMixin`. | ||||
| """ | """ | ||||
| class Meta: | class Meta: | ||||
| abstract = True | abstract = True | ||||
| # Map 'current status': {set of allowed new statuses}; define in subclass: | # Map 'current status': {set of allowed new statuses}; define in subclass: | ||||
| VALID_STATUS_TRANSITIONS: typing.Mapping[str, typing.Set[str]] | VALID_STATUS_TRANSITIONS: Mapping[str, Set[str]] | ||||
| # Add this field in a subclass, so that it uses the subclass' STATUSES | # Add this field in a subclass, so that it uses the subclass' STATUSES | ||||
| # and DEFAULT_STATUS values: | # and DEFAULT_STATUS values: | ||||
| # status = models.CharField(choices=STATUSES, default=DEFAULT_STATUS, max_length=20) | # status = models.CharField(choices=STATUSES, default=DEFAULT_STATUS, max_length=20) | ||||
| status: str | status: 'models.CharField[Union[str, int, Combinable], str]' | ||||
| def may_transition_to(self, new_status: str) -> bool: | def may_transition_to(self, new_status: str) -> bool: | ||||
| """Validate the potential transition from the current status to 'new_status'.""" | """Validate the potential transition from the current status to 'new_status'.""" | ||||
| try: | try: | ||||
| valid_transitions = self.VALID_STATUS_TRANSITIONS[self.status] | valid_transitions = self.VALID_STATUS_TRANSITIONS[self.status] | ||||
| except KeyError: | except KeyError: | ||||
| return False | return False | ||||
| return new_status in valid_transitions | return new_status in valid_transitions | ||||