State transitions¶
TransitionsMixin turns a set of named state moves into API operations, with the same
permissions, object checks and row locking as the rest of a model controller.
from ninja_devx import IsStaff
from ninja_devx.crud import CRUDController
from ninja_devx.crud.transitions import Transition, TransitionsMixin
class ArticleController(
TransitionsMixin[Article, ArticleOut],
CRUDController[Article, ArticleOut, ArticleIn],
):
state_field = "status"
transitions = {
"publish": Transition(source=("draft",), target="published", permissions=[IsStaff()]),
"archive": Transition(source=("published",), target="archived"),
}
- Each entry becomes
POST /{pk}/<name>, returning the output schema. Permissions are the controller's plus the transition's own (Alsois applied for you); object-level checks and row locking (get_object(..., lock=True)) run the same way a write does. - The current state must be one of
source; otherwise the response is 409invalid_transition. An optionalTransition.guard(request, instance) -> booladds a business precondition beyond the state itself; a failing guard also answers 409. Transition.on_transition(request, instance)runs after the new state is saved, inside the write transaction.on_transition(request, instance, name)on the controller runs after that, for every transition.GET /{pk}/transitionslists the names allowed from the object's current state for the caller: source state, guard and permissions are all evaluated, nothing is performed.- Rename or disable routes with
routes, keyed bytransition_<name>andtransitions_list(GET /{pk}/transitions's operation name). - Sync and async controllers both work;
TransitionsMixinfollows the controller'smodelike every other CRUD mixin.
Validation¶
At startup (when as_router() builds the routes):
state_fieldmust name an existingCharField. When it declareschoices, everysourceandtargetvalue must be one of them.- A transition's name must not collide with an existing route (
/{pk}/<name>already used by another operation, including another transition or the mixin's own/{pk}/transitions).
What this is not¶
This is an API-level state machine: it only knows the states you declare and does not
inspect the model. If a project already uses django-fsm-2 or a similar model-level FSM,
call its @transition-decorated method from Transition.on_transition (or from
on_transition()) instead of duplicating the rules here.