Documents and lifecycle
Some records are formal documents: once confirmed, they must not change silently. Evlet gives these a fixed lifecycle. Each confirmation keeps a revision you can read back and compare.
#Two ways to model stages
Evlet has two ways to give a record stages. Pick one per model.
| State and moves | Confirmable document | |
|---|---|---|
| Stages | Any states you declare | Draft, confirmed, cancelled |
| Best for | Workflows of your own: deals, jobs, invoices with a paid step | Documents that need a full revision history |
| See | Actions and moves | This page |
A confirmable model cannot also declare a State.
#A confirmable model
Set confirmable = True. The lifecycle adds three operations, confirm, cancel and reopen, which you grant in the rules like any other. Create apps/shop/models/receipt.py:
from evlet import Currency, Model, Rule, Text, ValidationError, validate
class Receipt(Model):
confirmable = True
track_changes = True
number = Text(required=True, unique=True, max_length=40)
amount = Currency(currency='AED', required=True)
remarks = Text(editable_when_locked=True)
permissions = {
'admin': Rule(('read', 'create', 'update', 'delete', 'confirm', 'cancel', 'reopen')),
'editor': Rule(('read', 'create', 'update', 'confirm')),
'viewer': Rule(('read',)),
}
@validate.confirm
def positive_amount(self):
if self.amount <= 0:
raise ValidationError({'amount': 'Enter an amount above zero'})@validate.confirmruns a check only when the document is confirmed. Saving a draft with a zero amount is fine. Confirming it fails withamount: Enter an amount above zero.@validate.cancelworks the same way for cancelling.track_changes = Truekeeps a history of changes.
#The lifecycle
A document keeps one id and one number through its whole life:
draft -> confirmed -> cancelled -> draft (reopened) -> confirmed againWork with it through a document handle:
with app.business.context(user) as context:
document = context.document(Receipt, number='RC-001', amount='150.00')
document.save()
document.confirm() # revision 1
document.cancel(reason='Wrong customer')
document.reopen() # the same id, still revision 1
document.amount = '175.00'
document.save()
document.confirm() # revision 2
revisions = document.revisions(limit=10, offset=0)
changes = document.diff(1, 2)diff(1, 2) returns what changed between the two confirmations:
{'from': 1, 'to': 2, 'changes': {'fields': {'amount': {'old': '150.00', 'new': '175.00'}}}}What the lifecycle protects:
- Confirmed means fixed. Once confirmed, only fields with
editable_when_locked=Truemay change. Child tables stay locked. - Revisions are kept. Every confirmation stores a snapshot. Read it with
document.snapshot(revision=1). - Drafts can go. A document that was never confirmed can be deleted. Once confirmed, it is kept, even after it is reopened, so its history stays readable.
- The number stays. The
numberfield is fixed after the first confirmation.
The context has the same operations as methods: context.confirm, context.cancel, context.reopen, context.history, context.revisions, context.snapshot and context.diff.
#Saving a document over HTTP
A model with a State or an @action is a document in the API. Every update, move, action and delete of a document needs the record's current ETag in an If-Match header:
- no
If-Match: the server answers 428; - an old ETag, because someone else saved first: the server answers 409.
This keeps two people from overwriting each other's work. The browser handles it for you.