"""
Custom object and query managers.
"""
from signoffs import registry
[docs]class QuerySetApiMixin:
"""
Delegates common methods to a query manager or queryset to emulate a queryset-like API,
without making extra queries where possible. Assumes a small number of instances on the queryset
"""
qs = None # queryset or query manager, to be defined on mixed-in instance
[docs] def all(self):
"""Return list of objects in this set, ordered chronologically"""
return self.qs.all()
[docs] def count(self):
"""Return the number of objects in the qs"""
return len(self.all())
[docs] def exists(self):
"""Rerturn True iff at least one Object exists in this set"""
return bool(self.all())
[docs] def earliest(self):
"""Return the first object from this set, or None if not self.exists()"""
all = self.all()
return all[0] if all else None
[docs] def latest(self):
"""Return most recent object from this set, or None if not self.exists()"""
all = self.all()
return all[-1] if all else None
# Signoff / Signet Sets
[docs]class SignetSetApiMixin(QuerySetApiMixin):
"""Delegates common methods to a signet_set manager or queryset"""
signet_set = None # Signet qs or manager, to be defined on mixed-in instance
@property
def qs(self):
return self.signet_set
[docs]class SignoffSetManager(SignetSetApiMixin):
"""
Manage a set of Signoff objects backed by a signet manager or queryset.
May be used, for example, on the reverse side of a many-to-one relation formed by a Signet with a FK.
For example::
signets = Signets.objects.filter(user=bob)
bobs_report_signoffs = SignoffSetManager('myapp.signoffs.report', signets)
For "reverse" access from a signet-related object, use a SignoffSet field to define the reverse manager.
The API is modelled on django's related object managers, with familiar operations that work roughly equivalently.
"""
def __init__(self, signoff_type, signet_set, signet_set_owner=None):
"""
Manage the signet_set (query manager or queryset), filtered for the given Signoff Type
If an approval instance is supplied, signets in this set are created with a reference to the approval's stamp.
"""
self.signoff_type = registry.get_signoff_type(signoff_type)
self.signet_set = signet_set
self.signet_set_owner = signet_set_owner
# Customize queryset emulation provided by Signets Set API Mixin
[docs] def all(self):
"""Return list of signoffs in this set, ordered chronologically"""
return super().all().signoffs(signoff_id=self.signoff_type.id)
[docs] def _pre_save_owner(self):
"""Bit of a hack - if signet_set_owner is an unsaved model, save it before saving related signets"""
try:
if self.signet_set_owner._meta.model and not self.signet_set_owner.pk:
self.signet_set_owner.save()
except AttributeError:
pass
[docs] def create(self, user, **kwargs):
"""Create and return a new Signoff in this set"""
self._pre_save_owner()
signet = self.signet_set.create(
signoff_id=self.signoff_type.id, user=user, **kwargs
)
return self.signoff_type(signet)
# signoff delegate / aggregate methods
[docs] def can_sign(self, user):
"""Return True iff given user would be allowed to sign (create) a new signoff in this set"""
return self.signoff_type().can_sign(user)
[docs] def has_signed(self, user):
"""Return True iff given user is a signatory in this set of signoffs"""
return any(s.signatory == user for s in self.all())
@property
def forms(self):
"""
Return the forms manager for adding a new signoff to this set or revoking a signoff in this set.
"""
return self.signoff_type.forms
# related querysets
[docs] def revoked(self):
"""
Returns a queryset of revoked signets related to the manager's instance.
related 'revoked' and 'revoked.user' objects are selected, assuming why else get the revoked signoffs
"""
return (
self.signoff_type.get_revoked_signets_queryset()
.filter(**self.signet_set.core_filters)
.select_related("revoked__user")
)
[docs]class SignoffSingleManager(SignoffSetManager):
"""
A SignoffSetManager that attempts to limit the number of related signets to one
and provide simplified API for dealing with the single signoff.
Why?
b/c it is convenient to manage all signoffs, e.g., on an Approval, backed by one Signet model.
If every Signoff is a single, then a set of SignoffFields is well-suited to the task.
But if any Signoff may be a series, then it is convenient to model all as SignoffSets.
This class helps clarify that a specific reverse Many2One relation is being modeled as a OneToOne.
Caveats
Can't really prevent multiple signoffs being added via non-API access. That's up to the application programmer.
"""
[docs] def get(self):
"""Get this signoff"""
return self.all()[0] if self.exists() else self.signoff_type()
[docs] def create(self, user, **kwargs):
"""Create and return the new Signoff. Raise ??? if self.exists()"""
if self.exists():
raise Exception # TODO: what exception here???
return super().create(user, **kwargs)
[docs] def can_sign(self, user):
"""Return True iff given user would be allowed to sign (create) a new signoff in this set"""
return not self.exists() and super().can_sign(user)
[docs]class StampSignoffsManager(SignetSetApiMixin):
"""
Manage the entire set of signoffs related to a Stamp of Approval via reverse signet_set manager.
Note that a single Stamp manages one set of Signets that may be of various Signoff Types
- recommend using a SignoffSetManager instead, unless access to entire set of signoffs is needed.
Provides a unified API for accessing a single Stamp instance's related signoffs and signets.
For example::
stamp = ProjectStamp.objects.filter(project_id='bob').first()
bobs_project_signoffs = StampSignoffsManager(stamp)
The API is modelled on django's related object managers, with familiar operations that work roughly equivalently.
"""
def __init__(self, stamp, subject=None):
"""Manage the given Approval instance and all of its related data"""
self.stamp = stamp
self.subject = subject
@property
def signet_set(self):
"""required for SignetSetApiMixin"""
return self.stamp.signatories
# Customize queryset emulation provided by Signets Set API Mixin
[docs] def all(self):
"""Return list of signoffs in this approval, ordered chronologically"""
return super().all().signoffs(subject=self.subject)
# Approval / Stamp Sets
[docs]class ApprovalStampSetApiMixin(QuerySetApiMixin):
"""Delegates common methods to a stamp_set manager or queryset"""
stamp_set = (
None # Stamp queryset or query manager, to be defined on mixed-in instance
)
@property
def qs(self):
return self.stamp_set
[docs]class ApprovalSetManager(ApprovalStampSetApiMixin):
"""
Manage a set of Approval objects backed by a Stamp manager or queryset.
May be used, for example, on the reverse side of a many-to-one relation formed by a Stamp with a FK.
Provides a unified API for accessing Approval instances, and related SigningOrder, Stamp, signoffs, and signets.
For example::
stamps = Stamp.objects.filter(user=bob)
bobs_report_approvals = ApprovalSetManager('myapp.approvals.report', stamps)
For "reverse" access from a stamp-related object, use an ApprovalSet field to define the reverse manager.
The API is modelled on django's related object managers, with familiar operations that work roughly equivalently.
"""
def __init__(self, approval_type, stamp_set, subject=None):
"""Manage the stamp_set (query manager or queryset), filtered for the given Signoff Type"""
self.approval_type = registry.get_approval_type(approval_type)
self.stamp_set = stamp_set
self.subject = subject
# Customize queryset emulation provided by Stamp Set API Mixin
[docs] def all(self):
"""Return list of approvals in this set, ordered chronologically"""
return (
super()
.all()
.approvals(approval_id=self.approval_type.id, subject=self.subject)
)
[docs] def create(self, **kwargs):
"""Create and return a new Approval in this set"""
stamp = self.stamp_set.create(approval_id=self.approval_type.id, **kwargs)
return self.approval_type(stamp, subject=self.subject)