Status management module for Bazis framework.
Project description
Bazis Statusy
An extension for the Bazis framework that implements a status system and transitions between them for Django models.
Description
Bazis Statusy is an extension package for the Bazis framework that adds a powerful status management and transition system. The package enables you to:
- Define statuses for models
- Configure transitions between statuses with validation
- Execute actions before and after transitions
- Manage related (child) objects during transitions
- Control access rights for executing transitions
- Track the history of status changes
Table of Contents
- Features
- Requirements
- Installation
- Quick Start
- Core Concepts
- Usage
- API
- Examples
- Architecture
- Development
- Contributing
- License
Features
- Status System: Define and manage statuses for any models
- Transits: Customizable transitions between statuses with validation
- Validators: Check conditions before executing a transition
- Actions: Execute code before and after transitions
- Child Objects: Automatic handling of related objects during transitions
- Transition History: Complete tracking of all status changes
- Access Control Integration: Control permissions for executing transitions
- Validation Schemas: Validate object data before transitions
- Payload: Pass additional data during transitions
- Multi-level Relations: Support for cascading transitions for related objects
Requirements
- bazis: Bazis framework core package
- bazis-permit: Access control system (for transition control)
- translated-fields: Translated field support used by status/transit names
- Python: 3.12+
- PostgreSQL: 12+
- Django: 4.2+
- FastAPI: 0.100+
Installation
Using uv (recommended)
uv add bazis-statusy
Using pip
pip install bazis-statusy
For Development
git clone <repository-url>
cd bazis-statusy
uv sync --dev
Quick Start
1. Add the applications to settings
INSTALLED_APPS = [
# ...
'translated_fields',
'bazis.contrib.permit',
'bazis.contrib.statusy',
# ...
]
2. Configure parameters in settings.py
# Default initial status
BAZIS_STATUS_INITIAL = ('draft', 'Draft')
# Status system models
BAZIS_STATUSY_STATUS_MODEL = 'statusy.Status'
BAZIS_STATUSY_TRANSIT_MODEL = 'statusy.Transit'
BAZIS_STATUSY_TRANSIT_RELATION_MODEL = 'statusy.TransitRelation'
BAZIS_STATUSY_TRANSIT_MIDDLE_ABSTRACT_MODEL = 'statusy.StatusyTransit'
3. Create a model with statuses
from django.db import models
from bazis.contrib.statusy.models_abstract import StatusyMixin
from bazis.core.models_abstract import DtMixin, UuidMixin, JsonApiMixin
class Order(StatusyMixin, DtMixin, UuidMixin, JsonApiMixin):
"""Order"""
number = models.CharField('Number', max_length=50)
description = models.TextField('Description', blank=True)
class Meta:
verbose_name = 'Order'
verbose_name_plural = 'Orders'
4. Create a route
from django.apps import apps
from bazis.contrib.statusy.routes_abstract import StatusyRouteSetBase
class OrderRouteSet(StatusyRouteSetBase):
model = apps.get_model('myapp.Order')
5. Configure transitions via admin panel
- Create statuses: draft, processing, completed
- Create transitions between them
- Configure validators and actions if necessary
Core Concepts
Statuses (Status)
A status is a state in which an object can exist. Each status has:
id- unique identifier (string)name- human-readable name (supports translations)
Transits (Transit)
A transit defines the possibility of changing an object's status from one to another. Each transit includes:
status_src- source statusstatus_dst- destination statusvalidators- list of validators to check conditionsactions_before- actions executed before status changeactions_after- actions executed after status changeis_schema_validate- flag for object schema validation before transition
StatusyMixin
Base mixin for models requiring a status system. Adds:
statusfield - current object statusstatus_dtfield - date and time of last status changestatus_authorfield - user who changed the status- Methods for working with transitions
StatusyChildMixin
Mixin for child objects whose status depends on the parent object. Enables:
- Defining path to parent status
- Automatic participation in validation during parent transitions
Decorators for Configuring Transitions
The package provides special decorators for defining business logic of transitions:
@transit_validator
Creates a transition validator that is called before the transaction:
@transit_validator('Validator description')
def validator_name(self, transit: TransitBase, user, payload):
# Check conditions
if not self.is_valid:
raise TransitError('Condition not met')
Parameters:
transit- transition objectuser- current user (or None)payload- additional data (orpayload_validate_noneduring pre-validation)
Important: Always check payload is not payload_validate_none before using payload.
@transit_before
Creates an action executed before status change:
@transit_before('Action description')
def before_action(self, statusy_transit: StatusyTransit, payload):
# Logic before status change
self.prepared_at = now()
self.save()
@transit_after
Creates an action executed after status change:
@transit_after('Action description')
def after_action(self, statusy_transit: StatusyTransit, payload):
# Logic after status change
# Can now work with new status
self.send_notification()
Action parameters:
statusy_transit- transition fact object (contains transit, status, dt, author)payload- additional transition data
@transit_link
Creates a link to a transition for programmatic invocation:
@transit_link('Auto-transition on payment')
def get_transit_payment(self):
# Additional checks (optional)
return self.is_paid
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# Programmatic transition call
if transit := self.get_transit_payment():
transit(user=some_user, payload=some_data)
Features:
- Returns
transit_applymethod with preset transition - Can return
Noneif transition is not assigned or conditions not met - Useful for automatic transitions in code
Usage
Creating Models
Model with Own Statuses
from django.db import models
from bazis.contrib.statusy.models_abstract import StatusyMixin, TransitBase, StatusyTransit
from bazis.contrib.statusy import transit_validator, transit_before, transit_after, TransitError
from bazis.core.models_abstract import DtMixin, UuidMixin, JsonApiMixin
class Order(StatusyMixin, DtMixin, UuidMixin, JsonApiMixin):
number = models.CharField('Number', max_length=50)
total_amount = models.DecimalField('Total Amount', max_digits=10, decimal_places=2)
is_paid = models.BooleanField('Paid', default=False)
completed_at = models.DateTimeField('Completion Date', null=True, blank=True)
class Meta:
verbose_name = 'Order'
verbose_name_plural = 'Orders'
@transit_validator('Payment Check')
def validator_check_payment(self, transit: TransitBase, user, payload):
"""Validator to check payment before order completion"""
if not self.is_paid:
raise TransitError('Order must be paid before completion')
@transit_validator('Order Amount Check')
def validator_check_amount(self, transit: TransitBase, user, payload):
"""Example validator using payload"""
from bazis.contrib.statusy.schemas import payload_validate_none
# Important: always check payload before use
if payload is not payload_validate_none:
if hasattr(payload, 'min_amount'):
if self.total_amount < payload.min_amount:
raise TransitError('Order amount is less than minimum')
@transit_before('Set Completion Date')
def before_set_completed(self, statusy_transit: StatusyTransit, payload):
"""Action before transition - set completion date"""
from django.utils.timezone import now
self.completed_at = now()
self.save()
@transit_after('Send Notification')
def after_send_notification(self, statusy_transit: StatusyTransit, payload):
"""Action after transition - send notification"""
# Notification sending logic
pass
Child Model
from bazis.contrib.statusy.models_abstract import StatusyChildMixin
class OrderItem(StatusyChildMixin, DtMixin, UuidMixin, JsonApiMixin):
order = models.ForeignKey(
Order,
on_delete=models.CASCADE,
related_name='items'
)
product_name = models.CharField('Product Name', max_length=255)
quantity = models.IntegerField('Quantity')
price = models.DecimalField('Price', max_digits=10, decimal_places=2)
@classmethod
def get_status_field(cls):
"""Path to parent object status"""
return 'order__status_id'
class Meta:
verbose_name = 'Order Item'
verbose_name_plural = 'Order Items'
Important: The get_status_field() method must return a Django string path to the parent's status field. This allows the child model to automatically participate in validation during parent object transitions.
Model with Payload for Transition
from pydantic import BaseModel, Field
from datetime import datetime
class CompleteOrderPayload(BaseModel):
"""Schema for order completion data"""
completion_note: str = Field(..., description='Completion note')
completed_by: str = Field(..., description='Completed by')
class Order(StatusyMixin, DtMixin, UuidMixin, JsonApiMixin):
# ... model fields ...
@transit_before('Save Completion Data')
def before_save_completion(
self,
statusy_transit: StatusyTransit,
payload: CompleteOrderPayload
):
"""Save additional data from payload"""
statusy_transit.extra = {
'completion_note': payload.completion_note,
'completed_by': payload.completed_by,
}
statusy_transit.save()
Configuring Transitions
Transitions are configured through Django admin panel:
- Go to "Statusy" → "Transits" section
- Create a new transit
- Specify:
- Model
- Source status
- Destination status
- Validators (optional)
- Actions before transition (optional)
- Actions after transition (optional)
Example of Configuring "To Processing" Transition
- Model: Order
- Source Status: draft
- Destination Status: processing
- Validators: validator_check_items (check for items presence)
- Actions Before: before_calculate_total (calculate total)
- Validate Schema: Yes
Creating Routes
Basic Route with Statuses
from django.apps import apps
from bazis.contrib.statusy.routes_abstract import StatusyRouteSetBase
from bazis.core.schemas import SchemaFields
class OrderRouteSet(StatusyRouteSetBase):
model = apps.get_model('myapp.Order')
fields = {
None: SchemaFields(
include={
'items': None, # Include related items
},
),
}
Route for Child Object
from bazis.contrib.statusy.routes_abstract import StatusySimpleRouteSetBase
class OrderItemRouteSet(StatusySimpleRouteSetBase):
model = apps.get_model('myapp.OrderItem')
fields = {
None: SchemaFields(
include={
'order': None, # Include parent order
},
),
}
Route with Child Routes Specification
class OrderRouteSet(StatusyRouteSetBase):
model = apps.get_model('myapp.Order')
# Specify child object routes
routes_child = [
'myapp.routes.OrderItemRouteSet',
]
fields = {
None: SchemaFields(
include={
'items': None,
},
),
}
Why routes_child is needed:
The routes_child attribute defines a list of child routes for correct validation during transitions. When a parent object transition is executed, the system automatically:
- Validates all child objects through their routes
- Checks child objects' compliance with their schemas
- Considers user's access rights to child objects
Example with multiple child routes:
class FacilityRouteSet(StatusyRouteSetBase):
model = apps.get_model('facility.Facility')
routes_child = [
'facility.routes.FacilityOperationRouteSet',
'facility.routes.FacilityEquipmentRouteSet',
'facility.routes.FacilityPersonnelRouteSet',
]
Important: Each child route must inherit from StatusySimpleRouteSetBase, and its model must contain StatusyChildMixin with correctly defined get_status_field().
Working with Admin Panel
Admin Setup for Model with Statuses
from django.contrib import admin
from bazis.contrib.statusy.admin_abstract import StatusyAdminMixin
from bazis.core.admin_abstract import DtAdminMixin
@admin.register(Order)
class OrderAdmin(StatusyAdminMixin, DtAdminMixin, admin.ModelAdmin):
list_display = ('id', 'number', 'status_id', 'total_amount', 'is_paid')
list_filter = ('status', 'is_paid')
search_fields = ('number',)
readonly_fields = ('status', 'status_dt', 'status_author')
StatusyAdminMixin adds:
- Current status display in list
- Status search
- Inline with transition history (read-only)
- Readonly fields for status fields
Status System Models Setup
from django.contrib import admin
from bazis.contrib.statusy.admin_abstract import (
StatusAdminBase,
TransitAdminBase,
StatusyContentTypeAdminBase,
TransitInlineBase,
TransitRelationInlineBase,
)
from .models import Status, Transit, StatusyContentType, TransitRelation
@admin.register(Status)
class StatusAdmin(StatusAdminBase):
pass
@admin.register(Transit)
class TransitAdmin(TransitAdminBase):
pass
@admin.register(StatusyContentType)
class StatusyContentTypeAdmin(StatusyContentTypeAdminBase):
inlines = [TransitInlineBase]
API
Endpoints
Get Transition Schema
GET /api/v1/{model}/{item_id}/schema_transit/
Returns validation schema for transition, including available transitions and their parameters.
Execute Transition
POST /api/v1/{model}/{item_id}/transit/
Request Body:
{
"transit": "to_processing",
"payload": {
"note": "Starting order execution"
}
}
Success Response (200):
{
"data": {
"id": "uuid",
"type": "myapp.order",
"attributes": {
"number": "ORD-001",
"total_amount": "1500.00"
},
"relationships": {
"status": {
"data": {
"id": "processing",
"type": "statusy.status"
}
}
}
}
}
Validation Error (422):
{
"errors": [
{
"status": 422,
"title": "Transit Error",
"code": "ERR_TRANSIT",
"detail": "Order must be paid before completion"
}
]
}
Meta Fields
When retrieving an object with statuses, the API returns additional meta fields:
state_actions
List of available actions (transitions) for the current object. Actions can be represented as individual objects or as packages (arrays) for execution through bulk API:
{
"data": {
"id": "uuid",
"type": "myapp.order",
"meta": {
"state_actions": [
{
"hint": "Move order to processing",
"hint_title": "To Processing",
"hint_action": "Start order execution",
"endpoint": {
"url": "/api/v1/myapp/order/uuid/transit/",
"method": "POST",
"body": {
"type": "object",
"properties": {
"transit": {
"type": "string",
"const": "to_processing"
},
"payload": {
"type": "object",
"properties": {
"note": {"type": "string"}
}
}
}
}
},
"resource": {
"id": "uuid",
"type": "myapp.order"
}
}
]
}
}
}
Batch Transitions:
If a state_actions element is represented as an array (not an object), this means a set of related actions that should be executed as a single package via /api/web/v1/bulk/:
{
"meta": {
"state_actions": [
[
{
"hint": "Sign document",
"endpoint": {
"url": "/api/v1/document/uuid1/transit/",
"method": "POST"
}
},
{
"hint": "Approve application",
"endpoint": {
"url": "/api/v1/application/uuid2/transit/",
"method": "POST"
}
}
]
]
}
}
Important:
- Order of actions in package matters - send requests in the same order
- When requesting an entity, only actions directly related to it are returned
- To get information about related objects, request their endpoints separately
status_aggs (list only)
Aggregation of object count by statuses:
{
"meta": {
"status_aggs": {
"draft": 15,
"processing": 8,
"completed": 42
}
}
}
status_allowed (list only)
List of all allowed statuses for the model:
{
"meta": {
"status_allowed": ["draft", "processing", "completed", "cancelled"]
}
}
Examples
Simple Transition
# Model
class Article(StatusyMixin, DtMixin, UuidMixin, JsonApiMixin):
title = models.CharField('Title', max_length=255)
content = models.TextField('Content')
class Meta:
verbose_name = 'Article'
verbose_name_plural = 'Articles'
# Route
class ArticleRouteSet(StatusyRouteSetBase):
model = apps.get_model('blog.Article')
API Request:
curl -X POST http://api.example.com/api/v1/blog/article/{id}/transit/ \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {token}" \
-d '{"transit": "publish"}'
Transition with Validation
class Article(StatusyMixin, DtMixin, UuidMixin, JsonApiMixin):
title = models.CharField('Title', max_length=255)
content = models.TextField('Content')
reviewed_by = models.ForeignKey(
User,
null=True,
blank=True,
on_delete=models.SET_NULL
)
@transit_validator('Review Check')
def validator_check_review(self, transit, user, payload):
if not self.reviewed_by:
raise TransitError('Article must be reviewed')
Transition with Payload
from pydantic import BaseModel
from datetime import datetime
class PublishPayload(BaseModel):
publish_date: datetime
featured: bool = False
class Article(StatusyMixin, DtMixin, UuidMixin, JsonApiMixin):
# ... fields ...
published_at = models.DateTimeField(null=True, blank=True)
is_featured = models.BooleanField(default=False)
@transit_before('Set Publishing Parameters')
def before_publish(
self,
statusy_transit: StatusyTransit,
payload: PublishPayload
):
self.published_at = payload.publish_date
self.is_featured = payload.featured
self.save()
API Request:
curl -X POST http://api.example.com/api/v1/blog/article/{id}/transit/ \
-H "Content-Type: application/json" \
-d '{
"transit": "publish",
"payload": {
"publish_date": "2024-03-15T10:00:00Z",
"featured": true
}
}'
Programmatic Transition Call via @transit_link
from django.utils.timezone import now
class Order(StatusyMixin, DtMixin, UuidMixin, JsonApiMixin):
number = models.CharField('Number', max_length=50)
is_paid = models.BooleanField('Paid', default=False)
payment_received_at = models.DateTimeField(null=True, blank=True)
@transit_link('Auto-transition on payment receipt')
def get_transit_payment(self):
"""Link to transition executed on payment"""
# Additional condition check (optional)
return self.is_paid and not self.payment_received_at
def process_payment(self, payment_data):
"""Payment processing"""
self.is_paid = True
self.payment_received_at = now()
self.save()
# Automatic transition after successful payment
if transit := self.get_transit_payment():
# transit() returns transit_apply method with preset transition
transit(user=payment_data.get('user'))
Usage in Code:
order = Order.objects.get(id=order_id)
order.process_payment({'user': request.user})
# Status will automatically change after payment
Cascading Transitions (parent-child)
# Parent model
class ParentEntity(StatusyMixin, DtMixin, UuidMixin, JsonApiMixin):
name = models.CharField(max_length=255)
is_active = models.BooleanField(default=False)
dt_approved = models.DateTimeField(null=True, blank=True)
@transit_after('Update Child Objects')
def after_update_children(self, statusy_transit: StatusyTransit, payload):
"""Update all child objects on transition"""
self.child_entities.all().update(
child_dt_approved=self.dt_approved,
child_is_active=self.is_active
)
# Child model
class ChildEntity(StatusyChildMixin, DtMixin, UuidMixin, JsonApiMixin):
parent_entities = models.ManyToManyField(
ParentEntity,
related_name='child_entities'
)
child_name = models.CharField(max_length=255)
child_is_active = models.BooleanField(default=False)
child_dt_approved = models.DateTimeField(null=True, blank=True)
@classmethod
def get_status_field(cls):
return 'parent_entities__status_id'
# Routes
class ParentEntityRouteSet(StatusyRouteSetBase):
model = apps.get_model('myapp.ParentEntity')
routes_child = ['myapp.routes.ChildEntityRouteSet']
fields = {
None: SchemaFields(
include={'child_entities': None},
),
}
class ChildEntityRouteSet(StatusySimpleRouteSetBase):
model = apps.get_model('myapp.ChildEntity')
fields = {
None: SchemaFields(
include={'parent_entities': None},
),
}
Architecture
Package Structure
bazis.contrib.statusy/
├── __init__.py # Export of main classes
├── models_abstract.py # Abstract models
├── models.py # Concrete models
├── admin_abstract.py # Abstract admin classes
├── admin.py # Admin registration
├── routes_abstract.py # Abstract routes
├── routes.py # Concrete routes
├── schemas.py # Pydantic schemas
├── services.py # Services
└── apps.py # Application configuration
Main Components
StatusyMixin
Main mixin adding status functionality to a model. Provides:
- Status fields
- Methods for working with transitions
- Schema validation
- Child object management
StatusyChildMixin
Mixin for child objects. Features:
- Automatic registration in parent model
- Participation in validation during parent transitions
- Definition of path to parent status
Transit (Transition)
Model describing a transition between statuses:
- Model connection via ContentType
- Source and destination statuses
- Lists of validators and actions
- Relations with other transitions
StatusyTransit (Transition Fact)
Model for storing transition history:
- Link to specific transition
- Set status
- Author and transition time
- Additional data (extra)
Transition Lifecycle
- Initiation: Request to execute transition via API
- Access Check: Check user's permissions for transition
- Transit Search: Determine specific transition by label
- Payload Validation: Validate data passed with transition
- Validator Execution: Run all configured validators
- Schema Validation: Check object's compliance with schema (if enabled)
- Transition Fact Creation: Create StatusyTransit record
- Actions Before Transition: Execute actions_before
- Status Setting: Change object status
- Actions After Transition: Execute actions_after
- Save: Save all changes
Integration with bazis-permit
The package is tightly integrated with the access control system:
StatusyAccessAction.TRANSIT- special action for transitions- Permissions checked at specific transition level
- Permission format:
{app}.{model}.item.transit.{scope}.{status_from}.{transit_label}
Example permission:
entity.order.item.transit.author.draft.to_processing
Means: author can move their order from "draft" status via "to_processing" transition
Development
Setting Up Development Environment
# Clone repository
git clone <repository-url>
cd bazis-statusy
# Install dependencies
uv sync --dev
# Run tests (requires local PostgreSQL)
cd sample
uv run pytest ../tests
# Code check
ruff check .
# Code formatting
ruff format .
Running Tests
# All tests
uv run pytest ../tests
# Specific test
uv run pytest ../tests/test_transit.py::test_transit
# With coverage
uv run pytest ../tests --cov=bazis.contrib.statusy
Contributing
We welcome contributions to the project! Here's how you can help:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make changes
- Run tests (
cd sample && pytest ../tests) - Commit changes (
git commit -m 'Add amazing feature') - Push to branch (
git push origin feature/amazing-feature) - Open a Pull Request
Please ensure that:
- Tests are written for new functionality
- Documentation is updated when necessary
- Code follows existing style
- Changes are added to changelog
License
Apache License 2.0
See LICENSE file for details.
Links
- Bazis Core — framework core
- Issue Tracker — report a bug or suggest improvement
- Bazis Documentation — complete framework documentation
Support
If you have questions or problems:
- Check the documentation
- Search existing issues
- Create a new issue with detailed description
Made with ❤️ by the Bazis team
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file bazis_statusy-2.2.2.tar.gz.
File metadata
- Download URL: bazis_statusy-2.2.2.tar.gz
- Upload date:
- Size: 126.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.7 {"installer":{"name":"uv","version":"0.10.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2e04958e4b355b2804b84d5023c5045d58e18c1de170d959fc9a433b9fe50330
|
|
| MD5 |
dc566f9201d6f09786f8eb0e018bd2c3
|
|
| BLAKE2b-256 |
4a509e4d02c832f1a98cb29708f19ef5b3f45ac62cd51b27beaa3441980ce18e
|
File details
Details for the file bazis_statusy-2.2.2-py3-none-any.whl.
File metadata
- Download URL: bazis_statusy-2.2.2-py3-none-any.whl
- Upload date:
- Size: 42.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.7 {"installer":{"name":"uv","version":"0.10.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
97d8a1466db748c49b658aeeccd89475114e1d6b0344de6afb583c02dedf9e98
|
|
| MD5 |
2255252520f0996431c969d4863117af
|
|
| BLAKE2b-256 |
0229821528610832d24c2aa1034be5733d8884ea21b50a71c69c0dc25923a8a0
|