Docs

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 movesConfirmable document
StagesAny states you declareDraft, confirmed, cancelled
Best forWorkflows of your own: deals, jobs, invoices with a paid stepDocuments that need a full revision history
SeeActions and movesThis 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:

Python
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.confirm runs a check only when the document is confirmed. Saving a draft with a zero amount is fine. Confirming it fails with amount: Enter an amount above zero.
  • @validate.cancel works the same way for cancelling.
  • track_changes = True keeps a history of changes.

#The lifecycle

A document keeps one id and one number through its whole life:

Output
draft -> confirmed -> cancelled -> draft (reopened) -> confirmed again

Work with it through a document handle:

Python
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:

Output
{'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=True may 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 number field 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.