# Viewflow - Full Documentation > Viewflow is a low-code library for Django. It adds workflows (BPMN), CRUD, forms, and reporting on top of normal Django models and views. Source: https://docs.viewflow.io - the content below is reStructuredText. ======================================================================== # Overview Source: https://docs.viewflow.io/overview/index.html ======================================================================== ======== Overview ======== .. meta:: :description: Viewflow is a Django package with ready-made components for BPMN workflows, CRUD, forms, and dashboards. Read the docs to get started. GPT assisted with Viewflow documentation: `Viewflow Pair Programming Buddy `_ Viewflow is a library for building business applications in Django. It gives you ready-made components for workflows, forms, CRUD, and dashboards. You write less code and ship faster. You already know Django and Python. Viewflow builds on that. Each component works on its own, but they also work well together. .. note:: Viewflow assumes you know Django. If you're new to Django, start with the official tutorial at https://docs.djangoproject.com/ first. What You Get ============ - **Workflow library:** Build BPMN workflows with Python code, not XML. - **CRUD views:** Create, read, update, delete - with complex forms handled for you. - **Dashboards:** Add reports and charts without writing JavaScript. - **Small API:** Learn a few classes, build a lot. - **Modern UI:** Single-page app feel, works out of the box. .. image:: /_static/img/viewflow_struct.svg :width: 450px Why Not Low-Code? ================= Low-code tools are fast until you need something custom. Then you're stuck. Viewflow gives you low-code speed with full-code control. You can: - Host it on your own servers - Connect it to your existing code and databases - Customize anything - it's just Django underneath For deeper reads, see the :doc:`Articles ` section — starting with :doc:`Python-Native BPMN: Workflows as Code `. Table of Contents ================= .. toctree:: why_viewflow quick_start whats_next ======================================================================== # Viewflow Quick Start Source: https://docs.viewflow.io/overview/quick_start.html ======================================================================== .. title:: Viewflow Quick Start =========== Quick Start =========== You know Django. You've done the official tutorial. Now you want to build something you can show to users - not just a polls app. This guide walks you through building a Viewflow app from scratch. You'll have a working site with login, navigation, and user management in about 15 minutes. Set Up the Environment ====================== Create a virtual environment: .. code-block:: shell python3 -m venv .venv source .venv/bin/activate Viewflow needs Python 3.8+ and Django 4.0+. Install the open source package: .. code-block:: shell pip install django-viewflow Or, if you have Viewflow PRO: .. code-block:: shell pip install django-viewflow-pro --extra-index-url https://pypi.viewflow.io//simple/ Create the Project ================== Create a Django project and an app: .. code-block:: shell django-admin startproject demo . ./manage.py startapp helloworld Your folder structure: .. code-block:: text demo/ ├── asgi.py ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py helloworld/ ├── admin.py ├── apps.py ├── __init__.py ├── migrations/ │ └── __init__.py ├── models.py ├── tests.py └── views.py manage.py Configure Settings ================== Open ``demo/settings.py`` and add Viewflow to your installed apps: .. code-block:: python INSTALLED_APPS = [ ... 'viewflow', 'helloworld', ] Set Up URLs =========== Viewflow uses Viewsets - classes that group related views together. Think of them as building blocks for your site. Open ``demo/urls.py`` and add: .. code-block:: python from django.urls import path from django.contrib.auth.models import User from viewflow.contrib.auth import AuthViewset from viewflow.urls import Application, Site, ModelViewset site = Site(title="ACME Corp", viewsets=[ Application( title='Sample App', icon='people', app_name='sample', viewsets=[ ModelViewset(model=User), ] ), ]) urlpatterns = [ path('accounts/', AuthViewset(with_profile_view=False).urls), path('', site.urls), ] This creates: - A site with one application - CRUD views for the Django User model - A login page Run the App =========== Apply migrations and create an admin user: .. code-block:: shell ./manage.py migrate ./manage.py createsuperuser Start the server: .. code-block:: shell ./manage.py runserver Open http://127.0.0.1:8000 in your browser. .. image:: /_static/img/QuickStart.png :width: 550px :target: http://127.0.0.1:8000 You have a working app. Now you can add your own models, workflows, and views. ======================================================================== # What's Next? Source: https://docs.viewflow.io/overview/whats_next.html ======================================================================== ============ What's Next? ============ You finished the quick start. Here's where to go from here. Tutorials ========= Pick what you need: - **BPMN Workflow:** Build a "Hello World" message approval flow using BPMN - **State Machine:** Create a sequential approval process with finite state machines - **Custom CRUD:** Build forms with complex validation and relationships - **Dashboard:** Add charts and reports to your app - **Deployment:** Get your app running on a server Learn the Components ==================== Viewflow uses these third-party libraries. Their docs will help you: - `Django `_ - the web framework underneath - `Material Components Web `_ - the UI components - `Plotly `_ - for charts and graphs Cookbook ======== The cookbook has working code for common tasks: - Custom form layouts and widgets - Authentication and permissions - Connecting to external APIs - Workflows with conditions and subprocesses - Custom dashboard charts Each recipe shows the code and explains why it works that way. https://github.com/viewflow/cookbook ======================================================================== # Why Choose Viewflow? Source: https://docs.viewflow.io/overview/why_viewflow.html ======================================================================== ==================== Why Choose Viewflow? ==================== You have two options for building web apps: 1. **Django from scratch.** Full control. But you'll spend weeks building admin panels, workflow engines, and form handling code. 2. **Low-code platforms.** Fast to start. But when the client asks for something the platform doesn't support, you're stuck. Viewflow is a third option: Django with batteries included. What This Means in Practice =========================== **Day one:** You define a model, wrap it in a Viewset, and you have a working app with login, CRUD, and a clean UI. .. image:: /_static/img/ACME-Corp.png :width: 250px :align: right .. code :: class Client(models.Model): name = models.CharField(max_length=240) phone = models.CharField(max_length=14) address = models.CharField(max_length=300) email = models.EmailField(max_length=240) site = Site(title="ACME Corp", viewsets=[ Application( title='Sample App', icon='people', app_name='emp', viewsets=[ ModelViewset(model=Client), ] ), ]) **Week two:** The client wants an approval workflow. You add a Flow class. No need to rewrite your models or switch platforms. **Month three:** They need a custom report that joins three tables and shows data in a specific way. You write Python and SQL. Viewflow doesn't stop you. Code Over Clicking ================== Most low-code tools make you drag and drop boxes to build workflows. This is slow. You can't version control it. You can't review it in a pull request. Viewflow workflows are Python code. You type them. You test them. You commit them to git. Typing is faster than clicking. You Already Know the Stack ========================== Viewflow uses: - Django models and views - Python classes and functions - HTML templates (when you need custom layouts) - JavaScript (when you need custom behavior) No new language. No proprietary format. If you've built a Django app before, you can build a Viewflow app today. ======================================================================== # BPMN Workflow Engine for Django Source: https://docs.viewflow.io/workflow/index.html ======================================================================== =============================== BPMN Workflow Engine for Django =============================== .. meta:: :description: Viewflow is an open-source BPMN workflow engine for Python and Django. Define business processes as code, render BPMN diagrams, and run them in production with parallel tasks, persistence, and Celery. BPMN (Business Process Model and Notation) is the standard for modeling business processes. Viewflow is an open-source BPMN workflow engine for Django: you write BPMN diagrams as plain Python code and run them in production. Unlike a finite state machine, BPMN supports parallel task execution. This matters when several people work on different parts of a process at the same time. .. image:: /_static/img/ShipmentProcess.png :width: 450px :alt: BPMN workflow diagram modeled in Django with Viewflow Code-First Workflows ==================== Most BPMN tools use graphical designers. You drag boxes and draw arrows. Viewflow takes a different approach: you write workflows in Python. This means you can: - Track changes in git - Review workflows in pull requests - Reuse code across different workflows - Test workflows with standard Python tools The ``viewflow.workflow`` module adds a thin layer on top of Django's Model/View/Template pattern. It extracts workflow logic from your views, so you can reuse the same view code in different workflows. FAQ === What is a BPMN workflow? ------------------------ A BPMN workflow is a business process modeled with Business Process Model and Notation — tasks, gateways, and flows that describe how work moves from start to finish. Viewflow runs these workflows inside a Django application. Is there an open-source BPMN engine for Python? ----------------------------------------------- Yes. Viewflow's core is an open-source BPMN workflow engine for Python and Django. You define the process as code and execute it; the PRO edition adds a visual frontend and extra widgets. Can I run BPMN workflows in production with Django? --------------------------------------------------- Yes. Workflow state is stored in your database, long-running and background tasks run on Celery, and processes survive restarts — so BPMN workflows run in production alongside the rest of your Django app. .. raw:: html Table of Contents ================= .. toctree:: :maxdepth: 1 quick_start writing data_flow permissions core_concepts nodes bpmn viewsets custom_views templates durability testing api ======================================================================== # Workflow API Source: https://docs.viewflow.io/workflow/api.html ======================================================================== .. title:: Workflow API === API === Activation ========== .. autoclass:: viewflow.workflow.Activation :members: Managers ======== .. autoclass:: viewflow.workflow.managers.ProcessQuerySet :members: .. autoclass:: viewflow.workflow.managers.TaskQuerySet :members: Models ====== .. autoclass:: viewflow.workflow.models.AbstractProcess :members: .. autoclass:: viewflow.workflow.models.Process :members: .. autoclass:: viewflow.workflow.models.AbstractTask :members: .. autoclass:: viewflow.workflow.models.Task :members: Timers ====== .. autofunction:: viewflow.workflow.timers.fire_due_timers .. autofunction:: viewflow.workflow.timers.fire_due_start_timers .. autofunction:: viewflow.workflow.timers.fire_due_conditions Signals ======= .. autofunction:: viewflow.workflow.nodes.broadcast_signal Shortcuts ========== .. autoclass:: viewflow.this_object.This :members: .. autoclass:: viewflow.workflow.utils.Act :members: ======================================================================== # BPMN Export Source: https://docs.viewflow.io/workflow/bpmn.html ======================================================================== =========== BPMN Export =========== Every flow is exportable as a standard BPMN 2.0 file that opens in bpmn.io or Camunda Modeler. Exported files validate against the official OMG BPMN 2.0 schema. - Append ``?format=bpmn`` to a flow's chart URL - Request the REST chart endpoint with ``?format=bpmn`` - Or use the management command: .. code-block:: shell ./manage.py flowexport helloworld/flows.HelloWorldFlow --format bpmn -o helloworld.bpmn Element mapping =============== .. list-table:: :header-rows: 1 * - Flow node - BPMN element * - ``flow.Start`` - ``startEvent`` * - ``flow.StartHandle`` - ``startEvent`` + message event definition * - ``flow.StartTimer`` - ``startEvent`` + timer event definition (``timeCycle``) * - ``flow.View`` - ``userTask`` * - ``flow.ManualTask`` - ``manualTask`` * - ``flow.Function`` - ``scriptTask`` * - ``flow.Handle`` - ``receiveTask`` * - ``flow.SendHandle`` - ``sendTask`` * - ``flow.BusinessRule`` - ``businessRuleTask`` * - ``celery.Job`` - ``serviceTask`` * - ``flow.Timer`` / ``celery.Timer`` - ``intermediateCatchEvent`` + timer event definition * - ``flow.MessageCatch`` / ``flow.MessageThrow`` - ``intermediateCatchEvent`` / ``intermediateThrowEvent`` + message event definition * - ``flow.SignalCatch`` / ``flow.SignalThrow`` - ``intermediateCatchEvent`` / ``intermediateThrowEvent`` + signal event definition * - ``flow.ConditionalCatch`` - ``intermediateCatchEvent`` + conditional event definition * - ``flow.EscalationThrow`` - ``intermediateThrowEvent`` + escalation event definition * - ``.OnEscalation(...)`` - non-interrupting ``boundaryEvent`` + escalation event definition * - ``flow.If`` / ``flow.Switch`` - ``exclusiveGateway`` * - ``flow.Split`` - ``parallelGateway``; ``inclusiveGateway`` with ``case=`` conditions * - ``flow.SplitFirst`` - ``eventBasedGateway`` * - ``flow.Join`` - ``parallelGateway``; ``complexGateway`` with ``continue_on_condition`` * - ``flow.Subprocess`` / ``flow.NSubprocess`` - ``callActivity``, multi-instance for ``NSubprocess`` (``isSequential`` with ``sequential=True``) * - ``flow.Split`` branch with ``task_data_source`` - target activity marked multi-instance * - ``.OnTimeout(...)`` / ``.OnError(...)`` - ``boundaryEvent`` + timer/error event definition * - ``.CompensateWith(...)`` handler - compensation ``boundaryEvent`` + ``association``, handler task marked ``isForCompensation`` * - ``flow.CompensateThrow`` - ``intermediateThrowEvent`` + compensate event definition * - ``flow.End`` - ``endEvent`` * - ``flow.TerminateEnd`` - ``endEvent`` + terminate event definition * - ``flow.ErrorEnd`` - ``endEvent`` + error event definition ``flow.If`` branches are labeled yes/no, ``flow.Switch`` marks its ``Default()`` branch as the gateway default flow. ``flow.Obsolete`` nodes have no BPMN counterpart and are omitted. ======================================================================== # Core Concepts Source: https://docs.viewflow.io/workflow/core_concepts.html ======================================================================== ============= Core Concepts ============= Flow and Nodes ============== Viewflow adds a Flow layer to Django's Model-View-Template pattern. This layer manages dependencies between tasks, so your views only handle CRUD operations. Each attribute of a Flow class represents a node. A node can be: - A human task (someone fills a form) - A Python function (runs synchronously or in background) - A gateway (decides which path to take next) Nodes connect with the ``this`` object, which creates references before the target is defined: .. code-block:: python from viewflow import this from viewflow.workflow import flow, lock, act from viewflow.workflow.flow import views class SampleFlow(flow.Flow): start = flow.Start(my_view).Next(this.task) task = flow.Handler(perform_task).Next(this.check_status) check_status = flow.If(this.is_completed).Then(this.end).Else(this.task) end = flow.End() def perform_task(self, activation): activation.process.completed = random.randint(0, 1) def is_completed(self, activation): return activation.process.completed Activations =========== When a node runs, Viewflow creates an activation object and injects it as ``request.activation``. The activation handles: - Checking if the task can run - Managing task and process state - Creating the next tasks All built-in activations have ``cancel()`` and ``undo()`` methods: .. code-block:: python def cancel_task_view(request, **kwargs): if not request.activation.cancel.can_proceed(): return redirect('index') if request.method == 'POST': request.activation.cancel() return redirect('index') return render(request, 'cancel_task.html') Database ======== Viewflow stores workflow state in two models: - ``Process`` - One instance per workflow execution - ``Task`` - One instance per task in the workflow .. image:: /_static/img/Models.png :width: 550px Locking ======= When multiple users work on the same process, race conditions can happen. Viewflow uses pessimistic locking to prevent this. The lock is held during view execution and released when the view returns. Locking is not enabled by default. To enable it: .. code-block:: python from viewflow.workflow import lock class SampleFlow(flow.Flow): lock_impl = lock.select_for_update_lock Views ===== Viewflow works with both class-based and function-based views. Each view expects ``process_pk`` and ``task_pk`` in the URL. Viewflow wraps the view to lock the process, activate the task, and inject ``request.activation``. .. image:: /_static/img/Views.png :width: 550px Flow Migration ============== Viewflow stores only task names in the database. You can add new tasks or change connections without migrations. To rename a task, create a data migration: .. code-block:: shell python manage.py makemigrations --empty your_app_name .. code-block:: python operations = [ migrations.RunSQL(""" UPDATE viewflow_task SET flow_task='helloworld/flows.MyFlow.new_name' WHERE flow_task='helloworld/flows.MyFlow.old_name' """) ] To remove a task but keep its history, use the ``Obsolete`` node: .. code-block:: python from viewflow.workflow import flow class SampleFlow(flow.Flow): obsolete = flow.Obsolete() This shows historical task data and lets admins cancel active obsolete tasks. Viewsets ======== A flow class is a viewset. It combines views from all its nodes into URL patterns. - ``FlowViewset`` - Exposes a single flow with Inbox, Queue, Archive, and dashboard - ``WorkflowViewset`` - Combines multiple flows into shared Inbox, Queue, and Archive views What's Next =========== - **Nodes:** Learn about gates, subflows, and other node types - **Custom views:** Write your own task views - **Templates:** Customize the UI - **Integrations:** Connect to Celery for background jobs ======================================================================== # Writing Custom Views Source: https://docs.viewflow.io/workflow/custom_views.html ======================================================================== ==================== Writing Custom Views ==================== Viewflow wraps Django views with workflow logic. It checks permissions, manages state, and injects ``request.activation``. Your view code handles only the task itself. When the task is done, call ``activation.execute()`` to mark it complete and create the next tasks. A single view can work with different workflow nodes. Keep view code focused on the task, not the workflow. Function-Based Views ==================== .. code-block:: python def task_view(request, **kwargs): form = MyForm(request.POST or None) if form.is_valid(): # Save form data object = form.save(commit=True) # Link to process request.activation.process.artifact = object # Complete the task request.activation.execute() # Redirect to next task return redirect(request.activation.get_success_url(request)) return render(request, "task.html", { "form": form, "activation": request.activation }) Class-Based Views ================= Add ``self.request.activation.execute()`` in ``form_valid``. Use ``TaskSuccessUrlMixin`` for redirect handling: .. code-block:: python from viewflow.workflow.flow.views import mixins class CustomFormView(mixins.TaskSuccessUrlMixin, generic.CreateView): model = SomeModel fields = ["acth", "estradiol", "free_t3", "free_t4"] def form_valid(self, form): object = form.save() object.save() # Link to process request.activation.process.artifact = object self.request.activation.execute() return redirect(self.get_success_url()) ======================================================================== # Data Management Source: https://docs.viewflow.io/workflow/data_flow.html ======================================================================== =============== Data Management =============== A business process starts with an event and moves toward a result. Viewflow separates workflow data from business data to keep your code organized. Business data—your users, orders, products—is stable. You design it carefully and store it in normalized tables. Workflow data—current step, decisions, intermediate values—changes as the process runs. Built-in Fields =============== The Process and Task models have two `generic foreign key `_ fields: ``seed`` and ``artifact``. Think of a workflow as a journey. ``seed`` is the starting input. ``artifact`` is the final output. .. code-block:: python class SelectSeedForm(forms.ModelForm): seed = forms.ModelChoiceField( queryset=Source.objects.filter(...) ) def save(self, commit=True): self.instance.seed = self.cleaned_data["seed"] return super().save(commit=commit) class Meta: model = Process fields = [] In your flow: .. code-block:: python class SampleFlow(flow.Flow): start = flow.Start( CreateProcessView.as_view( form_class=SelectSourceForm, ), ).Next(...) The data JSONField ================== Process and Task models have a ``data`` JSONField for workflow-related values. This data only matters during the process—intermediate calculations, user choices, temporary flags. Viewflow lets you treat JSONField contents like regular Django model fields. Define proxy models with virtual fields: .. code-block:: python class ApprovmentProcess(Process): # Stored as process.data['approved'], accessible as process.approved approved = jsonstore.BooleanField(default=False) class Meta: proxy = True class AprovementFlow(flows.Flow): ... approve = ( flow.View( views.UpdateProcessView.as_view(fields=["approved"]) ) .Next(...) ) Model Inheritance ================= For more control, inherit from the Process or Task models directly. You can also inherit from ``AbstractTask`` or ``AbstractProcess`` for full customization. Direct inheritance adds a database join when loading the model: .. code-block:: python class ShipmentProcess(Process): carrier = models.ForeignKey(Carrier, on_delete=models.CASCADE) Passing Data Between Tasks ========================== To keep tasks independent, initialize them with seed and data from previous tasks: .. code-block:: python class MyFlow(flow.Flow): # Copy start task artifact to next_task.seed start = ( flow.Start(SeedSelectionView.as_view()) .Next(this.next_task, task_seed=lambda activation: activation.task.artifact) ) # Create task with preinitialized data next_task = ( flow.If( cond=lambda activation: activation.task.seed.fresh ) .Then( this.plant, task_data=lambda activation: {'seed': activation.task.seed} ) .Else( this.eat, task_data=lambda activation: {'grain': activation.task.seed} ) ) Each task gets what it needs without depending on the full process state. Advanced Branching ================== When splitting work, initialize each branch with its own data: .. code-block:: python split = ( flow.Split() .Next( this.process_post, task_data_source=lambda activation: {'post': post} for post in activation.process.data['posts'], ) .Next(this.join) ) Each ``process_post`` task gets its own post to work on. Subprocesses can also receive seed and data: .. code-block:: python publish_post = flow.Subprocess( PublishFlow.as_subprocess, process_seed=lambda activation: activation.process.artifact, ).Next(this.end) ======================================================================== # Durable execution Source: https://docs.viewflow.io/workflow/durability.html ======================================================================== ================== Durable execution ================== Viewflow keeps all workflow state in the database and runs background steps through Celery. It targets a single Postgres + single Redis, scaled vertically — not partition tolerance. The settings below make that setup crash-safe. Database ======== Use **PostgreSQL**. The process row is the coordination point (see Locking), and ``select ... for update`` needs a real transactional database. Run the workflow tables on a durable, non-replica connection. .. code-block:: python DATABASES = {"default": env.db()} # postgres://… Locking ======= The lock serialises concurrent activations of the same process. The default is ``no_lock`` (a plain transaction) — **not safe for flows with Join / Subprocess nodes**, where two branches can finish at once. Set a real lock per flow: .. code-block:: python from viewflow.workflow import lock class MyFlow(flow.Flow): lock_impl = lock.select_for_update_lock # Postgres, recommended # lock_impl = lock.CacheLock() # only with memcached/Redis cache ... ``select_for_update_lock`` is the safe default for Postgres. ``CacheLock`` is correct only on a cache with an atomic cross-process ``add`` (memcached / Redis); ``LocMemCache`` is per-process and unsafe. Avoid ``no_lock`` for any flow that joins branches. Broker ====== Use a **durable** broker so scheduled and in-flight jobs survive a restart: .. code-block:: python CELERY_BROKER_URL = "redis://localhost:6379/0" CELERY_BROKER_TRANSPORT_OPTIONS = {"visibility_timeout": 3600} # > longest task CELERY_TASK_ACKS_LATE = True # redeliver if a worker dies CELERY_TASK_REJECT_ON_WORKER_LOST = True Enable Redis persistence (AOF), or use RabbitMQ, if you rely on ``celery.Timer`` / long ``Delay`` — an in-memory broker loses pending messages on restart. For long waits, prefer the database-backed ``flow.Timer``: its due moment lives on the task row and survives any broker failure. Fire due timers with the ``workflow_timers`` management command from cron, or schedule ``workflow_fire_timers`` with celery beat. Crash recovery ============== **Worker dies mid-run.** With ``acks_late`` (above) the broker redelivers the un-acked message and the task re-enters and finishes. This is **at least once**: the body may have partially run before the crash, so make Job side effects idempotent — guard on an external id: .. code-block:: python @shared_task def charge(activation_ref): with celery.Job.activate(activation_ref) as activation: if activation.process.charged: # already done on a prior delivery return gateway.charge(activation.process.amount) activation.process.charged = True activation.process.save() **A task that failed or is partially applied** is recovered by a human, not silently re-run: an ``ERROR`` task exposes ``undo`` → ``revive`` (both require the ``manage`` permission) so an operator can roll back and retry deliberately. Viewflow does *not* auto-reschedule running tasks — that would double-apply partial work. **Lost publish (rare).** Scheduling publishes the Celery message *after* the database commits (``connection.on_commit``), so a crash in the small window between the commit and the publish can leave a task ``SCHEDULED`` with no message. It is uncommon on a single healthy host; if it matters for your workload, front the publish with a transactional outbox. ======================================================================== # Built-in Nodes Source: https://docs.viewflow.io/workflow/nodes.html ======================================================================== ============== Built-in Nodes ============== Start a Flow ============ .. autoclass:: viewflow.workflow.flow.Start :members: Next, Available, Permission, can_execute, can_view, index_view_class, detail_view_class, undo_view_class .. autoclass:: viewflow.workflow.flow.StartHandle :members: Next, can_execute, can_view, detail_view_class, undo_view_class .. autoclass:: viewflow.workflow.flow.StartTimer :members: Next User Task ========= .. autoclass:: viewflow.workflow.flow.View :members: Assign, Next, Permission, onCreate, can_assign, can_execute, can_view, can_unassign, calc_owner, calc_owner_permission, calc_owner_permission_obj, index_view_class, assign_view_class, detail_view_class, cancel_view_class, undo_view_class, revive_view_class, unassign_view_class .. autoclass:: viewflow.workflow.flow.ManualTask :members: Next Script Tasks ============ .. autoclass:: viewflow.workflow.flow.Function :members: .. autoclass:: viewflow.workflow.flow.Handle :members: .. autoclass:: viewflow.workflow.flow.SendHandle :members: Next .. autoclass:: viewflow.workflow.flow.BusinessRule :members: Next .. autoclass:: viewflow.workflow.flow.Subprocess :members: .. autoclass:: viewflow.workflow.flow.NSubprocess :members: Job === .. autoclass:: viewflow.contrib.celery.Job :members: Gates ===== .. autoclass:: viewflow.workflow.flow.If :members: .. autoclass:: viewflow.workflow.flow.Switch :members: .. autoclass:: viewflow.workflow.flow.Split :members: .. autoclass:: viewflow.workflow.flow.SplitFirst :members: .. autoclass:: viewflow.workflow.flow.Join :members: .. autoclass:: viewflow.workflow.flow.Subprocess :members: .. autoclass:: viewflow.workflow.flow.NSubprocess :members: Timer ===== .. autoclass:: viewflow.workflow.flow.Timer :members: .. autoclass:: viewflow.contrib.celery.Timer :members: Intermediate Events =================== .. autoclass:: viewflow.workflow.flow.MessageCatch :members: Next .. autoclass:: viewflow.workflow.flow.MessageThrow :members: Next .. autoclass:: viewflow.workflow.flow.SignalCatch :members: Next .. autoclass:: viewflow.workflow.flow.SignalThrow :members: Next .. autoclass:: viewflow.workflow.flow.ConditionalCatch :members: Next .. autoclass:: viewflow.workflow.flow.EscalationThrow :members: Next Boundary Events =============== Boundary events are declared fluently on the host task with ``.OnTimeout``, ``.OnError`` and ``.OnEscalation`` (chained before ``.Next()``). See :class:`~viewflow.workflow.Node`. End Events ========== .. autoclass:: viewflow.workflow.flow.End :members: .. autoclass:: viewflow.workflow.flow.TerminateEnd :members: .. autoclass:: viewflow.workflow.flow.ErrorEnd :members: Compensation ============ .. autoclass:: viewflow.workflow.flow.CompensateThrow :members: Base class ========== .. autoclass:: viewflow.workflow.Node :members: :inherited-members: ======================================================================== # Permission Management Source: https://docs.viewflow.io/workflow/permissions.html ======================================================================== ===================== Permission Management ===================== Viewflow controls who can view, execute, and manage workflow tasks. Basic Permissions ================= View Permission --------------- Users need view permission on the process model to see process details. Manage Permission ----------------- Viewflow adds a manage permission for executing tasks and canceling processes. Without it, users can only view. Overriding Permissions ---------------------- Override permission methods in your flow class: .. code-block:: python class SampleFlow(flow.Flow): def has_view_permission(self, user: Any, obj: Optional[Any] = None) -> bool: return super().has_view_permission(user, obj) def has_manage_permission(self, user: Any, obj: Optional[Any] = None) -> bool: return super().has_manage_permission(user, obj) Task Permissions ================ Control task access with ``.Permission()`` and ``.Assign()``: .. code-block:: python class MyFlow(flow.Flow): start = ( flow.Start(views.StartView.as_view()) .Permission("app_label.can_start_request") .Next(this.task1) ) task1 = ( flow.View(...) .Assign(lambda activation: User.object.filter(...).first()) .Next(this.task2) ) task2 = ( flow.View(...) .Permission(auto_create=True) # Creates "app_label.can_next_task2" .Next(this.task3) ) task3 = ( flow.View(...) .Assign(this.task2.owner) # Assign to whoever completed task2 .Next(this.end) ) - ``.Permission()`` - Requires users to have a specific permission - ``.Permission(auto_create=True)`` - Creates a permission automatically - ``.Assign()`` - Assigns the task to a specific user ======================================================================== # Workflow Quick Start Source: https://docs.viewflow.io/workflow/quick_start.html ======================================================================== .. title:: Workflow Quick Start =========== Quick Start =========== This tutorial builds a "Hello World" approval workflow. One person submits a message, another approves it, and when approved, the message is sent. We assume you've completed the main quick start and have a ``helloworld`` Django app ready. First, add ``viewflow.workflow`` to your settings: .. code-block:: python INSTALLED_APPS = [ ... 'viewflow', 'viewflow.workflow', 'helloworld', ] Define the Model ================ The process model stores the state of each workflow instance. Open ``helloworld/models.py``: .. code-block:: python from django.db import models from viewflow import jsonstore from viewflow.workflow.models import Process class HelloWorldProcess(Process): text = jsonstore.CharField(max_length=150) approved = jsonstore.BooleanField(default=False) class Meta: proxy = True The base ``Process`` model has a ``data`` JSONField. The ``jsonstore`` package exposes parts of that JSON as regular Django fields. This lets you use them in forms and admin. Proxy models work well here because they don't require extra database joins. Define the Flow =============== The flow maps the BPMN diagram to Python code. Each node is a class attribute. .. image:: /_static/img/HelloWorld.png :width: 550px Create ``helloworld/flows.py``: .. code-block:: python from viewflow import this from viewflow.workflow import flow, lock, act from viewflow.workflow.flow import views from .models import HelloWorldProcess class HelloWorldFlow(flow.Flow): process_class = HelloWorldProcess start = ( flow.Start(views.CreateProcessView.as_view(fields=["text"])) .Annotation(title="New message") .Permission(auto_create=True) .Next(this.approve) ) approve = ( flow.View(views.UpdateProcessView.as_view(fields=["approved"])) .Permission(auto_create=True) .Next(this.check_approve) ) check_approve = ( flow.If(act.process.approved) .Then(this.send) .Else(this.end) ) send = ( flow.Function(this.send_hello_world_request) .Next(this.end) ) end = flow.End() def send_hello_world_request(self, activation): print(activation.process.text) What each node does: - ``flow.Start`` - A user fills a form to start the process - ``flow.View`` - A user task on an existing process - ``flow.If`` - A decision point that checks a condition - ``flow.Function`` - Runs Python code synchronously - ``flow.End`` - Marks the process as complete The ``this`` object creates references to other nodes before they're defined. Expose the Flow =============== Add the flow to your URL configuration. Open ``demo/urls.py``: .. code-block:: python from django.urls import path from viewflow.contrib.auth import AuthViewset from viewflow.urls import Application, Site, ModelViewset from viewflow.workflow.flow import FlowAppViewset from helloworld.flows import HelloWorldFlow site = Site(title="ACME Corp", viewsets=[ Application( title='Sample App', icon='people', app_name='sample', viewsets=[ FlowAppViewset(HelloWorldFlow, icon="assignment"), ] ), ]) urlpatterns = [ path('accounts/', AuthViewset(with_profile_view=False).urls), path('', site.urls), ] ``FlowAppViewset`` creates URLs for: - Starting new processes - Executing tasks - Viewing process details - Inbox, Queue, and Archive list views Run the App =========== Create and apply migrations: .. code-block:: shell ./manage.py makemigrations helloworld ./manage.py migrate Start the server: .. code-block:: shell ./manage.py runserver Open http://127.0.0.1:8000 and try the workflow. ======================================================================== # Workflow Templates Source: https://docs.viewflow.io/workflow/templates.html ======================================================================== .. title:: Workflow Templates ========= Templates ========= Viewflow looks for templates in a specific order, from most specific to most general. Template Lookup Order ===================== For any view, Viewflow searches: 1. ``{app_label}/{flow_label}/{task_name}_{template_filename}`` 2. ``{app_label}/{flow_label}/{template_filename}`` 3. ``viewflow/workflow/{template_filename}`` Customizing Views ================= Detail View (task_detail.html) ------------------------------ Shows task information. - **Task-specific:** ``hello_world/hello_world/approve_task_detail.html`` - **Flow-specific:** ``hello_world/hello_world/task_detail.html`` - **Global:** ``viewflow/workflow/task_detail.html`` Start View (start.html) ----------------------- Form to start a new process. - **Flow-specific:** ``{app_label}/{flow_label}/start.html`` - **Global:** ``viewflow/workflow/start.html`` Task View (task.html) --------------------- Form to complete a task. - **Task-specific:** ``{app_label}/{flow_label}/{task_name}_task.html`` - **Flow-specific:** ``{app_label}/{flow_label}/task.html`` - **Global:** ``viewflow/workflow/task.html`` Process Data (process_data.html) ================================ This template appears in multiple views. Customize it to show process information consistently. Lookup order: 1. ``{app_label}/{flow_label}/process_data.html`` 2. ``viewflow/workflow/process_data.html`` Context Variables ================= Task Detail Template -------------------- .. code-block:: python { 'activation': TaskActivation instance, 'task': Task model instance, 'flow_class': The Flow class, 'flow_task': The specific Node instance, 'form': Form instance (if applicable) } Process Detail Template ----------------------- .. code-block:: python { 'process': Process model instance, 'flow_class': The Flow class, 'tasks': QuerySet of Task instances } Task List Template ------------------ .. code-block:: python { 'flow_class': The Flow class (may be None for inbox/queue views), 'task_list': QuerySet of Task instances, 'filter_form': Task filter form (if enabled) } Process List Template --------------------- .. code-block:: python { 'flow_class': The Flow class, 'process_list': QuerySet of Process instances, 'filter_form': Process filter form (if enabled) } Example: Custom Task Detail =========================== .. code-block:: html {# myapp/myapp/approve_task_detail.html #} {% extends 'viewflow/workflow/task_detail.html' %} {% block task_details %}

{{ task.flow_task.task_title }}

{{ task.get_status_display }}

Created: {{ task.created|date:"F j, Y, H:i" }}

{% if task.owner %}

Assigned to: {{ task.owner.get_full_name|default:task.owner.username }}

{% endif %}
{% if task|can_execute:request.user %} Process Task {% endif %}
{% endblock %} Example: Custom Process Data ============================ .. code-block:: html {# myapp/myapp/process_data.html #}

Process Information

{{ process.pk }}
{{ process.created|date:"F j, Y, H:i" }}
{{ process.get_status_display }}
{% if process.artifact %}
View Details
{% endif %}
Template Tags and Filters ========================= .. code-block:: html {% load viewflow workflow %} {# Task action URLs #} Assign Execute Unassign {# Process action URLs #} Cancel Process View Details {# Flow diagram #} {% flow_diagram flow_class task=task %} {# Permission checks #} {% if task|can_execute:request.user %} {% endif %} {% if task|can_assign:request.user %} {% endif %} ======================================================================== # Testing Workflows Source: https://docs.viewflow.io/workflow/testing.html ======================================================================== .. title:: Testing Workflows ======= Testing ======= Viewflow workflows integrate with Django's test framework. Basic Flow Testing ================== Test the full flow to verify node connections work: .. code-block:: python from django.test import TestCase from django.contrib.auth.models import User, Permission from myapp.flows import MyFlow from viewflow.workflow import PROCESS class MyFlowTests(TestCase): def setUp(self): self.user = User.objects.create_user( username='testuser', password='password' ) # Grant permissions for permission in MyFlow.get_required_permissions(): self.user.user_permissions.add( Permission.objects.get(codename=permission.codename) ) def test_flow_execution(self): self.client.login(username='testuser', password='password') # Start the flow response = self.client.post( MyFlow.start.reverse('execute'), {'text': 'Test message'} ) self.assertEqual(response.status_code, 302) # Get the process process = MyFlow.process_class.objects.get() self.assertEqual(process.text, 'Test message') # Find the approval task approval_task = process.task_set.get(flow_task=MyFlow.approve) # Assign and complete it self.client.post(approval_task.reverse('assign'), {}) self.client.post( approval_task.reverse('execute'), {'approved': True} ) # Verify completion process.refresh_from_db() self.assertEqual(process.status, PROCESS.DONE) Testing Individual Nodes ======================== Test complex nodes in isolation: .. code-block:: python from viewflow.workflow import activation class NodeTests(TestCase): def test_approve_node(self): process = MyFlow.process_class.objects.create(text='Test') task = process.task_set.create(flow_task=MyFlow.approve) act = activation.Context(task) act.prepare() act.process.approved = True act.done() self.assertTrue(process.approved) self.assertTrue( process.task_set.filter(flow_task=MyFlow.check_approve).exists() ) Testing Branches ================ Verify each branch path: .. code-block:: python def test_approval_branching(self): # Test approved branch process = MyFlow.process_class.objects.create(text='Test', approved=True) task = process.task_set.create(flow_task=MyFlow.check_approve) act = activation.Context(task) act.prepare() act.done() self.assertTrue( process.task_set.filter(flow_task=MyFlow.send).exists() ) # Test rejected branch process = MyFlow.process_class.objects.create(text='Test', approved=False) task = process.task_set.create(flow_task=MyFlow.check_approve) act = activation.Context(task) act.prepare() act.done() self.assertTrue( process.task_set.filter(flow_task=MyFlow.end).exists() ) Mocking External Services ========================= .. code-block:: python from unittest.mock import patch class ExternalServiceTests(TestCase): @patch('myapp.services.external_api.send_notification') def test_notification_sending(self, mock_send): mock_send.return_value = {'status': 'sent', 'id': '123'} process = MyFlow.process_class.objects.create(text='Test') task = process.task_set.create(flow_task=MyFlow.send_notification) act = activation.Context(task) act.prepare() act.done() mock_send.assert_called_once_with(text='Test') Testing Parallel Execution ========================== .. code-block:: python def test_parallel_execution(self): process = ParallelFlow.process_class.objects.create() task = process.task_set.create(flow_task=ParallelFlow.split) act = activation.Context(task) act.prepare() act.done() # Both branches created self.assertTrue( process.task_set.filter(flow_task=ParallelFlow.task1).exists() ) self.assertTrue( process.task_set.filter(flow_task=ParallelFlow.task2).exists() ) # Complete first branch task1 = process.task_set.get(flow_task=ParallelFlow.task1) act = activation.Context(task1) act.prepare() act.done() # Join not ready yet join_task = process.task_set.get(flow_task=ParallelFlow.join) self.assertFalse(join_task.ready) # Complete second branch task2 = process.task_set.get(flow_task=ParallelFlow.task2) act = activation.Context(task2) act.prepare() act.done() # Join now ready join_task.refresh_from_db() self.assertTrue(join_task.ready) Testing Permissions =================== .. code-block:: python def test_permission_enforcement(self): regular_user = User.objects.create_user( username='regular', password='password' ) self.client.login(username='regular', password='password') # Without permission - denied response = self.client.post( MyFlow.start.reverse('execute'), {'text': 'Test message'} ) self.assertEqual(response.status_code, 403) # Add permission permission = Permission.objects.get(codename='can_start_myflow') regular_user.user_permissions.add(permission) # Now allowed response = self.client.post( MyFlow.start.reverse('execute'), {'text': 'Test message'} ) self.assertEqual(response.status_code, 302) Test Fixtures ============= Create reusable fixtures: .. code-block:: python class WorkflowFixturesMixin: def create_process_at_approval(self, text='Default text', **kwargs): """Create a process at the approval stage.""" process = MyFlow.process_class.objects.create(text=text, **kwargs) process.task_set.create( flow_task=MyFlow.start, status=TASK.DONE ) approval_task = process.task_set.create( flow_task=MyFlow.approve, status=TASK.NEW ) return process, approval_task class ApprovalTests(TestCase, WorkflowFixturesMixin): def test_approval_form_validation(self): process, task = self.create_process_at_approval() # Test the approval form... ======================================================================== # Built-in Viewsets Source: https://docs.viewflow.io/workflow/viewsets.html ======================================================================== ================= Built-in Viewsets ================= .. autoclass:: viewflow.workflow.flow.FlowViewset :members: :inherited-members: .. autoclass:: viewflow.workflow.flow.FlowAppViewset :members: :inherited-members: .. autoclass:: viewflow.workflow.flow.WorkflowAppViewset :members: :inherited-members: ======================================================================== # Writing Your Flow Source: https://docs.viewflow.io/workflow/writing.html ======================================================================== ================== Writing Your Flow ================== This guide covers the main concepts for building workflows. If you've finished the quick start, this page explains how to design your own flows. Data Management =============== Workflows have two kinds of data: - **Flow data** - State of the workflow: current step, decisions made, temporary values - **Business data** - Your application's core data: users, orders, products Keep them separate. Business data lives in your Django models. Flow data lives in the Process model. Flow Data --------- The Process model has a ``data`` JSONField. Use ``viewflow.jsonstore`` to expose fields from that JSON: .. code-block:: python from viewflow import jsonstore from viewflow.workflow.models import Process class HelloWorldProcess(Process): # Stored as process.data['approved'], accessible as process.approved approved = jsonstore.BooleanField(default=False) class Meta: proxy = True Business Data ------------- Store business data in separate models. The Process model has an ``artifact`` generic foreign key for linking to your business objects: .. code-block:: python class MyModel(models.Model): message = models.CharField(max_length=150) def task_view(request, **kwargs): form = MyModelForm(request.POST or None) if form.is_valid(): object = form.save(commit=True) request.activation.process.artifact = object # additional code here.. Task Data --------- Each task also has a ``data`` JSONField for task-specific information. This keeps data scoped to individual tasks rather than the whole process. Starting a Flow =============== Interactive Start ----------------- Use ``flow.Start`` with a view. Viewflow provides ``CreateProcessView`` and ``CreateArtifactView``: .. code-block:: python from viewflow import this from viewflow.workflow import flow from viewflow.workflow.flow import views class MyFlow(flow.Flow): start_with_artifact = ( flow.Start(views.CreateArtifactView.as_view(model=MyModel, fields=['message'])) .Annotation(title=_("Fill a form to start flow")) .Permission("myapp.can_start_request") .Next(this.next_task) ) Programmatic Start ------------------ Use ``flow.StartHandle`` to start flows from code: .. code-block:: python from viewflow import this from viewflow.workflow import flow class MyFlow(flow.Flow): start_noninteractive = flow.StartHandle(this.start_process).Next(this.approve) def start_process(self, activation, message=''): object = MyModel.objects.create(message=message) activation.process.artifact = object return activation.process # Start from anywhere in your code process = MyFlow.start_noninteractive.run(message="Hello World") User Tasks ========== User tasks need human input. Use ``flow.View`` with built-in views like ``UpdateProcessView`` or ``UpdateArtifactView``: .. code-block:: python class MyFlow(flow.Flow): ... approve = ( flow.View(views.UpdateProcessView.as_view(fields=["approved"])) .Permission(auto_create=True) .Next(this.check_approve) ) Use ``flow.ManualTask()`` for work performed outside any system -- the task appears in the task list and the user marks it done with a plain confirmation form: .. code-block:: python greet_client = flow.ManualTask().Next(this.end) Non-Interactive Tasks ===================== flow.Function ------------- Runs a function immediately when the previous task finishes. Runs in the same database transaction: .. code-block:: python from viewflow import this from viewflow.workflow import flow class MyFlow(flow.Flow): process_class = MyProcess start = ( flow.Start(...) .Next(this.process_data) ) process_data = ( flow.Function(this.process_data_function) .Next(this.end) ) def process_data_function(self, activation): activation.process.sample_text = activation.process.sample_text.upper() activation.done() end = flow.End() ``flow.SendHandle`` and ``flow.BusinessRule`` work the same way but export as dedicated BPMN task types -- an outbound message hook (send task) and a rule evaluation (business rule task): .. code-block:: python send = flow.SendHandle(this.notify_customer).Next(this.discount) discount = flow.BusinessRule(this.calc_discount).Next(this.end) flow.Handle ----------- Waits for external code to call it. Use this for webhooks or external events: .. code-block:: python from viewflow import this from viewflow.workflow import flow class MyFlow(flow.Flow): process_class = MyProcess start = ( flow.Start(...) .Next(this.wait_for_external_event) ) wait_for_external_event = ( flow.Handle(this.handle_external_event) .Next(this.end) ) def handle_external_event(self, activation, data): activation.process.sample_text = data['new_text'] activation.done() end = flow.End() # Call from external code process = MyFlow.wait_for_external_event.run( process=my_process_instance, data={'new_text': 'Updated Text'} ) celery.Job ---------- Runs a task in Celery for long-running work: .. code-block:: python # tasks.py from celery import shared_task from viewflow.flow import flow_job @shared_task def send_hello_world_request(activation_ref): with Job.activate(activation_ref) as activation: result = 'Background Processing Done' # No locks during long jobs - save carefully activation.process.sample_text = result activation.process.save(updated_fields=['sample_text']) .. seealso:: :class:`~viewflow.contrib.celery.Timer` flow.Timer ---------- Waits until a moment stored in the database, so pending timers survive a message broker restart or flush: .. code-block:: python class MyFlow(flow.Flow): ... wait = flow.Timer(timedelta(days=1)).Next(this.escalate) ... The delay accepts a ``timedelta``, an absolute ``datetime``, or a callable ``activation -> timedelta | datetime``. Due timers are fired by a periodic dispatcher -- run the ``workflow_timers`` management command from cron, or schedule the ``viewflow.workflow.tasks.workflow_fire_timers`` task with celery beat: .. code-block:: python app.conf.beat_schedule = { "viewflow-timers": { "task": "viewflow.workflow.tasks.workflow_fire_timers", "schedule": 60.0, }, } flow.StartTimer --------------- Starts a new process on a schedule, fired by the same dispatcher: .. code-block:: python class ReportFlow(flow.Flow): start = flow.StartTimer(interval=timedelta(days=1)).Next(this.report) ... For cron-style schedules, point celery beat or OS cron directly at ``MyFlow.start.run()``. Intermediate Events =================== Intermediate events sit between tasks. Catch events wait; throw events fire and continue. ``flow.MessageCatch`` waits for external code, like ``flow.Handle`` but exported as a message event; ``flow.MessageThrow`` runs an outbound hook: .. code-block:: python wait_payment = flow.MessageCatch(this.on_payment).Next(this.notify) notify = flow.MessageThrow(this.send_receipt).Next(this.end) ``flow.SignalThrow`` broadcasts a named signal to every armed ``flow.SignalCatch``, across all processes and flows -- one throw releases them all: .. code-block:: python # in one flow shipped = flow.SignalThrow("order-shipped").Next(this.end) # in another wait = flow.SignalCatch("order-shipped").Next(this.invoice) ``flow.ConditionalCatch`` waits until a condition over the process data becomes true. It is evaluated by the ``workflow_timers`` dispatcher, the same sweep that fires ``flow.Timer``: .. code-block:: python wait = flow.ConditionalCatch( lambda activation: activation.process.approved ).Next(this.proceed) ``flow.EscalationThrow`` raised inside a subprocess notifies the parent's non-interrupting ``.OnEscalation`` boundary without stopping either process -- see Boundary Events below. Boundary Events =============== A boundary event attaches to a task and fires while it is still active. Chain ``.OnTimeout`` / ``.OnError`` / ``.OnEscalation`` onto the host task, before ``.Next()``. Each creates an auto-named boundary node (``__timeout``, ``__error``, ``__escalation``): .. code-block:: python class MyFlow(flow.Flow): approve = ( flow.View(...) .OnTimeout(timedelta(days=3), this.escalate) .Next(this.end) ) ... ``.OnTimeout(delay, then)`` fires when the delay elapses first -- deadlines and escalations. ``.OnError(then, code=...)`` fires when the host task fails in a background context (job, function, timer) and routes to a recovery path: .. code-block:: python deploy = flow.Function(this.run_deploy).OnError(this.rollback).Next(this.end) By default a boundary event interrupts (cancels) its host task; pass ``interrupting=False`` to start a parallel path and leave the host running. Pass ``title=`` to label the boundary in the chart. Boundary events are canceled automatically when the host task completes. Timer boundaries are fired by the same ``workflow_timers`` dispatcher as ``flow.Timer``. ``.OnEscalation(then, code=...)`` catches a ``flow.EscalationThrow`` raised inside a subprocess. It never interrupts -- the subprocess and the child both keep running: .. code-block:: python sub = ( flow.Subprocess(OrderFlow.start) .OnEscalation(this.notify_manager, code="over-budget") .Next(this.end) ) Gates ===== BPMN separates tasks (do something) from gates (decide what's next). This makes workflows easier to understand and modify. flow.If ------- Takes one of two paths based on a condition: .. code-block:: python from viewflow import this from viewflow.workflow import flow class MyFlow(flow.Flow): ... check_condition = ( flow.If(lambda activation: activation.process.approved) .Then(this.approved_task) .Else(this.rejected_task) ) ... .. seealso:: :class:`~viewflow.workflow.flow.Switch` for more than two branches flow.Split and flow.Join ------------------------ ``Split`` creates parallel branches. ``Join`` waits for all branches to finish: .. code-block:: python from viewflow import this from viewflow.workflow import flow class MyFlow(flow.Flow): process_class = MyProcess start = ( flow.Start(...) .Next(this.parallel_tasks) ) parallel_tasks = ( flow.Split() .Next(this.task1).Next(this.task2) ) task1 = flow.View(...) .Next(this.join) task2 = flow.View(...) .Next(this.join) join = flow.Join() .Next(this.next_step) next_step = flow.End() .. seealso:: :class:`~viewflow.workflow.flow.SplitFirst` Subprocess ========== **PRO-only** Use ``flow.Subprocess`` and ``flow.NSubprocess`` to break large flows into smaller, reusable pieces. ``flow.NSubprocess(..., sequential=True)`` runs one child process at a time instead of all at once. .. seealso:: :class:`~viewflow.workflow.flow.Subprocess` .. seealso:: :class:`~viewflow.workflow.flow.NSubprocess` End === Use ``flow.End`` to finish a process. You can have multiple end nodes for different outcomes: .. code-block:: python class MyFlow(flow.Flow): # previous flow steps... check_approve = ( flow.If(act.process.approved) .Then(this.approved) .Else(this.rejected) ) approved = flow.End() rejected = flow.End() ``flow.TerminateEnd()`` cancels all other active tasks and finishes the process immediately, without waiting for parallel branches. ``flow.ErrorEnd(code)`` does the same and records the process as failed. Inside a subprocess, the parent task is marked ``ERROR`` so the parent can catch it with ``.OnError(this.recover, code=...)`` on the subprocess. Compensation ============ Register an undo handler on a task and run all handlers in reverse completion order with ``flow.CompensateThrow``: .. code-block:: python book_hotel = ( flow.Function(this.do_book) .CompensateWith(this.cancel_hotel) .Next(this.pay) ) cancel_hotel = flow.Function(this.do_cancel) ... compensate = flow.CompensateThrow().Next(this.fail_end) fail_end = flow.ErrorEnd("payment-failed") Each completed task is compensated at most once. A handler is a ``flow.Function`` with no incoming connections; it receives the compensated task as ``activation.task.previous``. Diagram export ============== Every flow is exportable as a standard BPMN 2.0 file that opens in bpmn.io or Camunda Modeler. See :doc:`bpmn`. ======================================================================== # django-fsm successor: Finite State Machine for Django Source: https://docs.viewflow.io/fsm/index.html ======================================================================== .. title:: django-fsm successor: Finite State Machine for Django ================================================== Django Finite State Machine (django-fsm successor) ================================================== .. meta:: :description: django-fsm 3.0 was renamed viewflow.fsm — the same finite state machine for Django models, under a new name. Define states and transitions; rules are enforced at runtime so a method only runs when the state allows. A finite state machine defines a set of states and the transitions between them. ``viewflow.fsm`` enforces these rules at runtime — a method only runs when the current state allows it. It is the maintained successor to the original ``django-fsm`` library. FSM fits simple, sequential workflows. For parallel execution or complex branching, use the :doc:`BPMN workflow engine ` instead. Quick Start =========== The ``State`` class holds a value from a Python enum or Django choices class. You can't change it with direct assignment—only through transitions. .. code:: from enum import Enum from viewflow.fsm import State class States(Enum): NEW = 1 DONE = 2 HIDDEN = 3 class MyFlow(object): state_field = State(States, default=States.NEW) @state_field.transition(source=States.NEW, target=States.DONE) def complete(): pass @state_field.transition(source=State.ANY, target=States.HIDDEN) def hide(): pass flow = MyFlow() flow.state_field == States.NEW # True flow.state_field = States.DONE # Raises AttributeError flow.complete() flow.state_field == States.DONE # True flow.complete() # Raises TransitionNotAllowed The ``@transition`` decorator adds runtime checks. Methods can only run when the object is in the right state. ``TransitionNotAllowed`` has two subclasses so callers can tell why a transition was refused: ``NoTransition`` when the current state has no matching transition, and ``TransitionConditionsUnmet`` when a ``conditions=`` callback returned false. The latter carries ``failed_condition`` (the callable that failed) and ``unmet_message`` (set from a ``State.CONDITION(False, "...")`` result), so you can report the specific reason without re-checking the conditions yourself:: from viewflow.fsm import NoTransition, TransitionConditionsUnmet try: flow.complete() except TransitionConditionsUnmet as exc: print(exc.failed_condition.__name__, exc.unmet_message) except NoTransition: print("no transition from the current state") Coming from django-fsm? ======================= ``viewflow.fsm`` is the maintained successor to the original ``django-fsm`` library. The idea is the same — a state value plus ``@transition``-guarded methods — with one structural change: the state stays a plain model field, and the transitions live in a separate flow class instead of on the model. .. code:: python # Before — django-fsm: state and transitions live on the model from django_fsm import FSMField, transition class Report(models.Model): state = FSMField(default="NEW") @transition(field=state, source="NEW", target="APPROVED") def approve(self): ... # After — viewflow.fsm: plain field on the model, transitions in a flow class class Report(models.Model): state_field = models.CharField( max_length=150, choices=ReportState.choices, default=ReportState.NEW) class ReportFlow(object): state_field = fsm.State(ReportState, default=ReportState.NEW) def __init__(self, report): self.report = report @state_field.getter() def _get(self): return self.report.state_field @state_field.setter() def _set(self, value): self.report.state_field = value @state_field.transition(source=ReportState.NEW, target=ReportState.APPROVED) def approve(self): ... Keeping the machine in its own class separates transition rules from the model definition and lets you reuse plain ``TextChoices``. See :doc:`models` for the full pattern. FAQ === Is django-fsm still maintained? ------------------------------- The original ``django-fsm`` is in maintenance mode. ``viewflow.fsm`` is its actively developed successor and the recommended choice for new Django projects. How do I add a state machine to a Django model? ----------------------------------------------- Store the state in a normal ``CharField`` with ``TextChoices``, then wrap the model in a flow class whose ``fsm.State`` descriptor declares the allowed transitions. See :doc:`models`. What is the difference between FSM and BPMN in Viewflow? -------------------------------------------------------- FSM models one object moving through sequential states. BPMN models a whole process with parallel branches and multiple participants. Use FSM for simple status fields and the :doc:`BPMN workflow engine ` for multi-step processes. .. raw:: html Table of Contents ================= .. toctree:: :maxdepth: 2 :titlesonly: options models fields inheritance viewset admin rest visualization api Cookbook ======== `FSM 101 sample`_ .. _`python enum`: https://docs.python.org/3/library/enum.html .. _`django enumeration type`: https://docs.djangoproject.com/en/3.0/ref/models/fields/#field-choices-enum-types ======================================================================== # Django Admin Support Source: https://docs.viewflow.io/fsm/admin.html ======================================================================== ==================== Django Admin Support ==================== .. container:: pro-only PRO ONLY ``FlowAdminMixin`` adds transition actions to Django admin. .. code-block:: python from django.contrib import admin from viewflow import fsm from .flows import ReviewFlow from .models import Review @admin.register(Review) class ReviewFlowAdmin(fsm.FlowAdminMixin, admin.ModelAdmin): readonly_fields = ('state', ) flow_state = ReviewFlow.state def get_object_flow(self, request, obj): return ReviewFlow( obj, user=request.user, ip_address=request.META.get('REMOTE_ADDR') ) If your flow class takes only the model instance in its constructor, the admin discovers it automatically. Override ``get_object_flow`` for custom initialization. Change Object Data ================== Let admins edit fields alongside transitions: .. code-block:: python def get_transition_fields(self, request, obj, slug): if slug == 'approve': return ['text', 'comment'] Custom Transition View ====================== Override a transition view completely: .. code-block:: python def get_urls(self): return [path( '/transition/reject/', self.admin_site.admin_view(self.reject_view), )] + super().get_urls() def reject_view(self, request, object_id): obj = get_object_or_404(self.model, pk=object_id) flow = self.get_object_flow(request, obj) if not flow.reject.has_perm(request.user) or not flow.reject.can_proceed(): raise PermissionDenied if request.method == "POST": flow.reject() obj.save() return redirect('../../') return render(request, ...) django-guardian =============== ``FlowAdminMixin`` works with ``reversion.admin.VersionAdmin``. ======================================================================== # FSM API Source: https://docs.viewflow.io/fsm/api.html ======================================================================== .. title:: FSM API === API === .. module: viewflow.fsm .. autoclass:: viewflow.fsm.TransitionNotAllowed .. autoclass:: viewflow.fsm.NoTransition .. autoclass:: viewflow.fsm.TransitionConditionsUnmet .. autoclass:: viewflow.fsm.InvalidTargetState .. autoclass:: viewflow.fsm.State .. autodecorator:: viewflow.fsm.State.transition .. autoclass:: viewflow.fsm.State.ANY .. autoclass:: viewflow.fsm.State.CONDITION .. autoclass:: viewflow.fsm.State.RETURN_VALUE .. autoclass:: viewflow.fsm.State.GET_STATE .. autoclass:: viewflow.fsm.FlowAdminMixin .. autoclass:: viewflow.fsm.rest.FlowRESTMixin .. autofunction:: viewflow.fsm.chart .. autoclass:: viewflow.fsm.FSMField .. autofunction:: viewflow.fsm.transition .. autoclass:: viewflow.fsm.NonInitialStateOnCreate ======================================================================== # Coming from django-fsm: ``FSMField`` Source: https://docs.viewflow.io/fsm/fields.html ======================================================================== ======================================== Coming from django-fsm: ``FSMField`` ======================================== The pattern on the previous page -- a plain Django field plus ``State.setter``/``State.getter`` on a separate flow class -- is how ``viewflow.fsm`` is meant to be used, and it's the one this documentation builds on elsewhere. If you're porting a model straight out of django-fsm, ``viewflow.fsm.FSMField`` is a same-column drop-in for django-fsm's own ``FSMField``: swap the import, and the ``@transition(field=..., source=..., target=...)`` decorator style keeps working unchanged. .. code-block:: python # Before: django-fsm from django_fsm import FSMField, transition # After: viewflow.fsm from viewflow.fsm import FSMField, transition class Job(models.Model): status = FSMField(default='new') @transition(field=status, source='new', target='running') def start(self): pass ``status.transition(...)`` (calling the method directly on the field) works the same way, since ``transition(field=status, ...)`` is a thin wrapper over it: .. code-block:: python class Job(models.Model): status = FSMField(default='new') @status.transition(source='new', target='running') def start(self): pass The value lives in the model's own column -- no ``setter``/``getter`` pair to write, ``Job.objects.filter(status='running')`` and serialization work as they would for any ``CharField``, and the column type is unchanged from django-fsm's, so a migration generated after the swap is a same-column ``AlterField`` at most, never a schema change. Everything past this point -- ``conditions=``, ``permission=``, ``State.ANY``, ``State.RETURN_VALUE``/``State.GET_STATE``, ``on_success``, ``NoTransition``/``TransitionConditionsUnmet`` -- is the same machinery described throughout these docs; ``FSMField`` only changes where the value is stored. Two Opt-in Guarantees ====================== ``FSMField`` defaults to django-fsm's own behavior on two points where django-fsm was permissive. Both default to ``False`` so a ported model behaves identically to begin with; turn either on once you're ready for viewflow's stricter guarantee. ``protected=True`` ------------------- Blocks direct assignment (``job.status = 'running'``) after the instance's first value is set, the same way a plain ``State`` field already blocks ``flow.state_field = x`` unconditionally. Transitions are unaffected -- they write through ``State.set()``, not attribute assignment. .. code-block:: python class Job(models.Model): status = FSMField(default='new', protected=True) @status.transition(source='new', target='running') def start(self): pass job = Job.objects.create() job.status = 'running' # AttributeError job.start() # fine Known limitation: ``protected=True`` also blocks a plain ``instance.refresh_from_db()``, since that reassigns the field through the same guarded path and the slot is already populated by then. There's no workaround yet -- leave ``protected`` off for a model that relies on ``refresh_from_db()``. ``enforce_initial=True`` ------------------------- Closes the gap tracked as `django-fsm#218 `_: without it, nothing stops ``Job.objects.create(status='running')`` from inserting a row that never went through ``start()``. With it, creating a row (an ``INSERT``, not an ``UPDATE``) at any value other than the field's ``default`` raises ``NonInitialStateOnCreate``, a ``TransitionNotAllowed`` subclass -- before the row is written. .. code-block:: python class Job(models.Model): status = FSMField(default='new', enforce_initial=True) @status.transition(source='new', target='running') def start(self): pass Job.objects.create() # fine -- status='new' Job.objects.create(status='running') # NonInitialStateOnCreate The check runs from the field's ``pre_save``, which Django calls for ``.save()``, ``.create()``, and ``get_or_create()``/``update_or_create()``. It does **not** run for ``bulk_create()`` or a raw SQL ``INSERT`` -- both skip ``pre_save`` entirely, so neither is guarded by this. ======================================================================== # Flow Inheritance Source: https://docs.viewflow.io/fsm/inheritance.html ======================================================================== ================ Flow Inheritance ================ Inherit from a flow class to add or override transitions. Use ``State.super()`` to wrap the parent method while keeping its state changes. Access the unwrapped parent method with ``.original``: .. code-block:: python class MyFlow(object): state_field = fsm.State(States, default=States.NEW) @state_field.transition(source=States.DONE, target=States.HIDDEN) def hide(): print('base hide') class GuestFlow(MyFlow): @MyFlow.state_field.super() def hide(self): # Additional code before calling parent super().hide.original() ======================================================================== # Wrapping Django Models Source: https://docs.viewflow.io/fsm/models.html ======================================================================== ====================== Wrapping Django Models ====================== To persist state in a database, create a flow class that wraps a Django model. First, define your model with a state field: .. code-block:: python # models.py from django.db import models from django.db.models import TextChoices from django.utils.translation import gettext_lazy as _ class ReportState(TextChoices): NEW = 'NEW', _('New') APPROVED = 'APPROVED', _('Approved') REJECTED = 'REJECTED', _('Rejected') PUBLISHED = 'PUBLISHED', _('Published') class Report(models.Model): text = models.TextField() state_field = models.CharField( max_length=150, choices=ReportState.choices, default=ReportState.NEW ) Then create a flow class. Use ``State.setter`` and ``State.getter`` to connect the FSM to your model field: .. code-block:: python # flow.py from viewflow import fsm from .models import Report, ReportState class ReportFlow(object): state_field = fsm.State(ReportState, default=ReportState.NEW) def __init__(self, report): self.report = report @state_field.setter() def _set_report_state(self, value): self.report.state_field = value @state_field.getter() def _get_report_state(self): return self.report.state_field @state_field.transition(source=ReportState.NEW, target=ReportState.APPROVED) def approve(self): pass @state_field.transition(source=ReportState.NEW, target=ReportState.REJECTED) def reject(self): pass @state_field.transition(source=ReportState.APPROVED, target=ReportState.PUBLISHED) def publish(self): pass @state_field.on_success() def _on_transition_success(self, descriptor, source, target): self.report.save() The ``on_success`` decorator runs after a transition completes. Use it to save the model or trigger other actions. State Change Views ================== Handle transitions in Django views with permission checks: .. code-block:: python # views.py from django.shortcuts import get_object_or_404, redirect, render from django.core.exceptions import PermissionDenied from .models import Report from .forms import ApproveForm from .flow import ReportFlow def approve(request, report_pk): report = get_object_or_404(Report, pk=report_pk) flow = ReportFlow(report) if not flow.approve.has_perm(request.user): raise PermissionDenied form = ApproveForm(request.POST or None, instance=report) if form.is_valid(): form.save(commit=False) flow.approve() return redirect('../') return render(request, 'approve.html', { 'report': report, 'flow': flow, 'form': form }) Logging State Changes ==================== Track state changes with a log model: .. code-block:: python # models.py from django.db import models from django.utils import timezone class ReportChangeLog(models.Model): report = models.ForeignKey(Report, on_delete=models.CASCADE) changed = models.DateTimeField(default=timezone.now) source = models.CharField(max_length=150) target = models.CharField(max_length=150) class Meta: ordering = ['-changed'] Add logging to your flow class: .. code-block:: python # flow.py from django.db import transaction from .models import Report, ReportState, ReportChangeLog class ReportFlow(object): state_field = fsm.State(ReportState, default=ReportState.NEW) # ... other methods ... @state_field.on_success() def _log_state_change(self, descriptor, source, target, **kwargs): with transaction.atomic(): self.report.save() ReportChangeLog.objects.create( report=self.report, source=source.value, target=target.value ) ======================================================================== # Transition Options Source: https://docs.viewflow.io/fsm/options.html ======================================================================== ================== Transition Options ================== Source ====== Specify one or multiple source states: .. code-block:: python @state_field.transition(source={States.NEW, States.DONE}, target=States.CANCELED) def cancel(self): pass Use ``State.ANY`` to allow transition from any state except the target: .. code-block:: python @state_field.transition(source=State.ANY, target=States.CANCELED) def cancel(self): pass Target ====== The state changes before the method runs. This lets you chain transitions: .. code-block:: python @state_field.transition(source=States.NEW, target=States.IN_PROCESS) def pay(self): try: self.perform_payment() except: self.error() @state_field.transition(source=States.IN_PROCESS, target=States.DONE) def perform_payment(self): pass @state_field.transition(source=States.IN_PROCESS, target=States.ERROR) def payment_error(self): pass If you omit the target, no state change happens: .. code-block:: python @state_field.transition(source=States.NEW) def notify(self): mail_admins('New flow is waiting', self.text) Stack multiple decorators for different source/target combinations: .. code-block:: python @state_field.transition(source=States.NEW, target=States.DONE) @state_field.transition(source=States.DONE, target=States.NEW) def toggle(self): pass Dynamic Target ============== For a target only known at call time, use ``State.RETURN_VALUE`` or ``State.GET_STATE`` instead of a fixed state. ``State.RETURN_VALUE(*allowed_states)`` takes the target from the method's own return value -- resolved only once the method returns, so it can't change the state before its own side effects run: .. code-block:: python @state_field.transition( source=States.NEW, target=State.RETURN_VALUE(States.PUBLISHED, States.FOR_MODERATION), ) def publish(self, is_public): return States.PUBLISHED if is_public else States.FOR_MODERATION ``State.GET_STATE(func, states=[...])`` computes the target from the call's own arguments via ``func(instance, *args, **kwargs)``, resolved *before* the method runs -- so, unlike ``RETURN_VALUE``, the method body already observes the new state: .. code-block:: python def get_review_target(instance, approved): return States.APPROVED if approved else States.REJECTED @state_field.transition( source=States.NEW, target=State.GET_STATE(get_review_target, states=[States.APPROVED, States.REJECTED]), ) def review(self, approved): pass ``allowed_states``/``states`` are optional but recommended: when given, an out-of-set resolved target raises ``InvalidTargetState`` (a ``TransitionNotAllowed`` subclass) instead of silently landing on an undeclared state, and :func:`~viewflow.fsm.chart` draws an edge for each. Omit them only when the destinations genuinely can't be enumerated ahead of time -- the chart then shows no outgoing edge for that transition. Coming from django-fsm: ``GET_STATE`` there resolves *after* the method body runs, same as ``RETURN_VALUE``. Here it resolves *before*, so the method body already observes the new state -- matching how a plain, non-dynamic ``target=`` already behaves in ``viewflow.fsm``. If your ``GET_STATE`` function reads state the method body itself mutates, port it to ``RETURN_VALUE`` instead, since only that one sees the method's own effects. ``source=State.ANY`` combined with a dynamic target interacts with ``get_outgoing_transitions()``/``get_available_transitions()`` (used to build transition UIs in the admin, REST API, and custom views): a transition is left out of a state's outgoing list only when that state is one of its *declared* ``allowed_states``/``states``. An undeclared dynamic target can't be checked this way, so it is listed as outgoing from every state -- including one it might, at runtime, just return you to: .. code-block:: python @state_field.transition(source=State.ANY, target=State.RETURN_VALUE()) def force(self, next_state): return next_state # force() is always in get_outgoing_transitions(), for every state -- # declare allowed_states/states if you need it excluded from its own # possible targets. Label ===== Add a human-readable label: .. code-block:: python @state_field.transition(source=States.NEW, target=States.DONE) @state_field.transition(source=States.DONE, target=States.NEW, label=_("Toggle back to New")) def toggle(self): pass toggle.label = _("Toggle report state") Conditions ========== Require conditions to be met before a transition can happen. Conditions are functions that return True or False. They should not have side effects. .. code-block:: python def can_publish(instance): # No publishing after 17 hours if datetime.datetime.now().hour > 17: return False return True Or use a method on the flow class: .. code-block:: python def can_destroy(self): return self.is_under_investigation() Apply conditions: .. code-block:: python @state_field.transition(States.NEW, target=States.DONE, conditions=[this.can_publish]) def publish(self): pass Permissions =========== Attach permission checks to transitions. Use a callable that takes the flow instance and user: .. code-block:: python @state_field.transition( source=States.NEW, target=States.DONE, permission=lambda flow, user: user.has_perm('myapp.delete_review', obj=flow.report) ) def remove(self): pass @state_field.transition(source=States.NEW, target=States.DONE, permission=this.is_owner) def hide(self): pass def is_owner(self, user): return self.author == user Check permissions in your code: .. code-block:: python if not flow.remove.has_perm(request.user): raise PermissionDenied Why permissions are not enforced inside transitions ---------------------------------------------------- A transition permission is a query, not a gate. ``flow.remove()`` runs whether or not the user has permission. Check ``has_perm()`` yourself in the view, as shown above. This is on purpose: - Authorization belongs in the view. Management commands, Celery tasks, and system transitions have no user but still change state. - A failed check is ``PermissionDenied`` (403), not ``TransitionNotAllowed``, which means the transition is impossible from the current state. - ``get_available_transitions()`` lets a view list the allowed transitions and build the UI before any call. A call-time check would duplicate it. The :doc:`workflow ` layer wires this up for you. Custom Properties ================= Add custom metadata to transitions: .. code-block:: python @state.transition( field=state, source=STATE.ANY, target=States.ON_HOLD, custom=dict(verbose='Hold for legal reasons')) def legal_hold(self): """ Side effects galore """ ======================================================================== # Expose REST Interface Source: https://docs.viewflow.io/fsm/rest.html ======================================================================== ===================== Expose REST Interface ===================== **PRO Only** ``FlowRESTMixin`` adds REST endpoints for state transitions. .. code-block:: python from viewflow.fsm.rest import FlowRESTMixin class ReviewViewSet(FlowRESTMixin, viewsets.ModelViewSet): flow_state = ReviewFlow.state_field queryset = Review.objects.all() serializer_class = ReviewSerializer def get_object_flow(self, request, obj): """Custom flow initialization""" return ReviewFlow( obj, user=request.user, ip_address=request.META.get('REMOTE_ADDR') ) If your flow class takes only the model instance in its constructor, the REST interface discovers it automatically. Custom Serializer Per Transition ================================ .. code-block:: python def get_serializer_class(self): if self.action in ('approve', 'reject'): return ReviewAuditSerializer return super().get_serializer_class() Custom Transition Action ======================== .. code-block:: python @action(methods=['POST'], detail=True, url_path='transition/approve') def approve(self, request, *args, **kwargs): instance = self.get_object() flow = self.get_object_flow(request, instance) if not flow.approve.has_perm(request.user): raise PermissionDenied if not flow.approve.can_proceed(): raise ValidationError(_('Transition is not allowed')) serializer = self.get_serializer(instance, data=request.data, partial=True) serializer.is_valid(raise_exception=True) self.perform_update(serializer) flow.approve() return Response(serializer.data) ======================================================================== # Frontend Viewset Source: https://docs.viewflow.io/fsm/viewset.html ======================================================================== ================ Frontend Viewset ================ Integrate FSM with Viewflow's frontend viewsets to expose transitions as user actions. Basic Integration ================= .. code-block:: python # viewset.py from viewflow.fsm import FlowViewsMixin from viewflow.views import ModelViewSet from .flow import ReportFlow from .models import Report class ReportViewSet(FlowViewsMixin, ModelViewSet): model = Report flow_class = ReportFlow queryset = Report.objects.all() list_display = ['id', 'text', 'state_field'] detail_actions = ['approve', 'reject', 'publish'] This adds transition buttons to detail views and handles permission checks automatically. Custom Transition Forms ======================= For transitions that need user input: .. code-block:: python # forms.py from django import forms from .models import Report class ApproveForm(forms.ModelForm): comment = forms.CharField(widget=forms.Textarea) class Meta: model = Report fields = [] # viewset.py class ReportViewSet(FlowViewsMixin, ModelViewSet): # ...previous configuration... def get_transition_form_class(self, transition_name): if transition_name == 'approve': return ApproveForm return super().get_transition_form_class(transition_name) def perform_transition(self, instance, transition_name, form=None): flow = self.get_flow_instance(instance) if transition_name == 'approve' and form is not None: instance.approval_comment = form.cleaned_data['comment'] transition = getattr(flow, transition_name) transition() Template Customization ===================== Override templates at these paths: - ``viewflow/fsm/{model_name}/{transition_name}.html`` - Specific transition - ``viewflow/fsm/{model_name}/transition_form.html`` - Model-specific form - ``viewflow/fsm/transition_form.html`` - Generic form Example: .. code-block:: html {% extends "viewflow/fsm/transition_form.html" %} {% block transition_title %} Approve Report #{{ object.pk }} {% endblock %} {% block transition_subtitle %} Please review the report carefully before approval. {% endblock %} State Visualization ================== ``FlowViewsMixin`` adds a ``chart/`` URL that renders the state machine as a diagram, and a "State chart" action on the list page linking to it. No setup needed beyond mixing in ``FlowViewsMixin``. ======================================================================== # State Visualization Source: https://docs.viewflow.io/fsm/visualization.html ======================================================================== =================== State Visualization =================== Generate visual diagrams of your state machines. Basic Chart Generation ===================== The ``chart()`` function returns a DOT language representation: .. code-block:: python from viewflow.fsm import chart from myapp.flow import ReportFlow dot_graph = chart(ReportFlow.state_field) print(dot_graph) Render the DOT output with Graphviz or similar tools. Rendering in Django Views ======================== Return the chart as plain text: .. code-block:: python from django.http import HttpResponse from viewflow.fsm import chart from .flow import ReportFlow def flow_chart_view(request): dot_graph = chart(ReportFlow.state_field) return HttpResponse(dot_graph, content_type='text/plain') Or convert to PNG using pydot: .. code-block:: python import pydot from django.http import HttpResponse from viewflow.fsm import chart from .flow import ReportFlow def flow_chart_image(request): dot_graph = chart(ReportFlow.state_field) graphs = pydot.graph_from_dot_data(dot_graph) graph = graphs[0] png_data = graph.create_png() return HttpResponse(png_data, content_type='image/png') Chart Customization ================= .. code-block:: python from viewflow.fsm import chart from .flow import ReportFlow dot_graph = chart( ReportFlow, title="Report Approval Process", node_options={'shape': 'box', 'style': 'filled', 'fillcolor': '#f5f5f5'}, edge_options={'fontsize': '11'}, graph_options={'rankdir': 'LR'} # Left to right layout ) Admin and Viewset Integration ============================= Charts are built into ``FlowAdminMixin`` and ``FlowViewsMixin``. With these mixins, the chart appears on the change form or at the ``chart/`` URL. Example Output ============== A document review FSM in DOT format: .. code-block:: text digraph { graph [rankdir=TB, size="8,8"]; node [shape=box, style="filled", fillcolor="#f5f5f5", fontsize=10]; edge [fontsize=9]; NEW [label="NEW", fillcolor="#e1f5fe"]; UNDER_REVIEW [label="UNDER REVIEW"]; APPROVED [label="APPROVED"]; REJECTED [label="REJECTED"]; PUBLISHED [label="PUBLISHED", fillcolor="#e8f5e9"]; NEW -> UNDER_REVIEW [label="submit"]; UNDER_REVIEW -> APPROVED [label="approve"]; UNDER_REVIEW -> REJECTED [label="reject"]; APPROVED -> PUBLISHED [label="publish"]; REJECTED -> NEW [label="revise"]; } ======================================================================== # CRUD Frontend Source: https://docs.viewflow.io/crud/index.html ======================================================================== ============= CRUD Frontend ============= Viewflow provides quick ready-to-use templates and views to build a user interface Class-based URL configuration ============================= To make the interface reusable and easy customizable whole frontend built around one simple concept - Class based URL Config or **Viewset** Basically, **Viewset** is a class with `.urls` property suitable to include into the Django url configuration. .. code-block:: python viewset = MyViewset() urlpatterns = [ path('', viewset.urls) ] **Viewset** is not a new notion for the Django world. `django.contrib.admin.ite` and `restframework` Router classes provides similar functionality. Viewflow distils the idea, withing generic **Viewset** interface and provide pre-built viewsets to create CRUD interfaces and people workflows. To customize existing viewsets, they could be subclassed and their properties and methods been overridden. Several viewsets for 3d-party packages available in `viewflow.contrib`. Look-and-feel ============= `viewflow.frontend` templates built with `Google Material Web Components library `_ and uses `BEM `_ for CSS class names, to avoids CSS names collisions. `Turbolinks `_ library used to speedup page loading and give a user near the same experience as SPA application. All javascript code wrapped in the standard `Web Components `_ and `Svelte `_ compiled. Table of Contents ================= .. toctree:: :maxdepth: 2 :titlesonly: quickstart viewset site crud templates css_and_js api ======================================================================== # CRUD API Source: https://docs.viewflow.io/crud/api.html ======================================================================== .. title:: CRUD API === API === Class-based URL Config ====================== .. autoclass:: viewflow.urls.route .. autoclass:: viewflow.urls.BaseViewset .. autoclass:: viewflow.urls.ViewsetMeta .. autoclass:: viewflow.urls.Viewset .. autoclass:: viewflow.urls.IndexViewMixin Frontend Viewset ================ .. autoclass:: viewflow.urls.Site .. autoclass:: viewflow.urls.Application .. autoclass:: viewflow.urls.AppMenuMixin Frontend CRUD ============= .. autoclass:: viewflow.urls.BaseModelViewset .. autoclass:: viewflow.urls.ModelViewset .. autoclass:: viewflow.urls.ReadonlyModelViewset .. autoclass:: viewflow.urls.DeleteViewMixin .. autoclass:: viewflow.urls.DetailViewMixin .. autoclass:: viewflow.views.CreateModelView .. autoclass:: viewflow.views.DeleteModelView .. autoclass:: viewflow.views.DetailModelView .. autoclass:: viewflow.views.ListModelView .. autoclass:: viewflow.views.UpdateModelView .. autoclass:: viewflow.views.DeleteBulkActionView .. autoclass:: viewflow.views.Action ) Middleware ========== .. autoclass:: viewflow.middleware.SiteMiddleware ======================================================================== # CRUD Viewset Source: https://docs.viewflow.io/crud/crud.html ======================================================================== ============ CRUD Viewset ============ CRUD or create, read, update, and delete are the four basic functions on Django Model. Viewflow provides ready-to-use CRUD viewsets, views, and templates based on google material design. Combined all together they allow to implement Django admin like functionality. You can quickly instantiate a Viewset by passing required parameters to a class constructor .. code-block:: python from viewflow import Icon from viewflow.urls import ReadonlyModelViewset, ModelViewset categories_viewset = ReadonlyModelViewset( app_name='category', icon=Icon('category'), model=models.Category, list_view=views.custom_list_view, ) Or inherit from a viewset class and override methods and attributes .. code-block:: python class DepartmentViewset(ModelViewset): icon = Icon('people') model = models.Department list_columns = ('name', 'manager', 'parent') list_filter_fields = ('parent', ) After inclusion into an *Application* viewset, you will get a model list page with links points to model details or edit pages. :class:`~viewflow.urls.ModelViewset` is the viewset that mixes list model view with create/update views. Use :class:`~viewflow.url.DetailViewMixin` to point links from list view to model detail page, before change form. :class:`~viewflow.url.DetailViewMixin` adds ability to delete a model instance. :class:`~viewflow.urls.ReadonlyModelViewset` only list a model and provide model details page. Basic options ============= The only mandatory option for CRUD viewsets is the *model* class. You would also like to customize *icon* and *title* appearance in the site menu. To optimize querying or restrict models listed, specify *queryset* attribute or override *get_queryset* method. .. code-block:: python class EmployeeViewset(DetailViewMixin, ModelViewset): model = models.Employee queryset = model._default_manager.select_related('department') def get_queryset(self, request); if not request.user.is_staff: return self.queryset.exclude(department__parent_isnull=True) return self.queryset You can replace a build-in view with our own functional or class-based view. Or pass additional keyword parameters to `.as_view` call .. code-block:: python list_view_class = views.EmployeeListView create_view = views.create_employee_view def get_update_view_kwargs(self): return { 'success_url': reverse('emp:employee:index') } As for any viewset, you can add additional views, just by adding an attribute named with *_url* suffix .. code-block:: python manager_change_url = path( '/manages/', views.change_manager, name='change_manager' ) Form options ============ In Viewflow, you have the ability to customize form layouts and other aspects of your CRUD viewsets through various options. Available Options: ------------------ - ``create_form_layout``: Sets the layout for the Create form. - ``create_form_class``: Specifies the class to use for the Create form. - ``create_form_widgets``: Defines the widgets to use in the Create form. - ``update_form_layout``: Sets the layout for the Update form. - ``update_form_class``: Specifies the class to use for the Update form. - ``update_form_widgets``: Defines the widgets to use in the Update form. - ``form_layout``: Sets the layout for both Create and Update forms if not individually specified. - ``form_class``: Specifies the class for both Create and Update forms if not individually specified. - ``form_widgets``: Defines the widgets for both Create and Update forms if not individually specified. .. code-block:: python class ContinentViewset(ModelViewset): # ... other options ... create_form_layout = Layout( # Layout configuration for create form ) form_layout = Layout( # Layout configuration ) update_form_class = forms.ContinentForm form_widgets = { 'planet': DependentModelSelect( depends_on='galaxy', queryset=lambda galaxy: Planet.objects.filter(galaxy=galaxy) ) } Dynamic Form Customization -------------------------- You can dynamically generate form classes based on the request object by implementing these methods in your viewset: - ``get_create_form_class(request)``: Dynamically create a form class for the create view - ``get_update_form_class(request)``: Dynamically create a form class for the update view - ``get_form_class(request)``: Dynamically create a form class for both views if the specific methods aren't defined These methods allow you to modify form fields based on the current user or other request context. .. code-block:: python class CityViewset(ModelViewset): # ... other options ... def get_create_form_class(self, request): """ Custom form class that restricts capital city creation to admin users """ from django import forms from viewflow.forms import ModelForm class CityCreateForm(ModelForm): class Meta: model = self.model fields = ['name', 'country', 'population', 'is_capital'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Only admin users can create capital cities if not request.user.is_superuser: self.fields['is_capital'].disabled = True self.fields['is_capital'].help_text = _("Only administrators can create capital cities") return CityCreateForm List options ============ list_columns ------------ Set *list_columns* to control which fields are displayed on the change list view. If you don’t set *list_columns*, the list view will display a single column that with *__str__()* representation of each object. There are three types of values that can be used in list_display: The name of a model field. For example: .. code-block:: python class EmployeeViewset(ModelViewset): list_columns = ('first_name', 'last_name') A string representing a Viewset method that accepts one argument, the model instance. For example: .. code-block:: python class EmployeeViewset(ModelViewset): list_columns = ('upper_case_name',) def upper_case_name(self, obj): return ("%s %s" % (obj.first_name, obj.last_name)).upper() upper_case_name.short_description = 'Name' Or a string representing a model attribute or method (without any required arguments) Sortable columns ~~~~~~~~~~~~~~~~~ Columns backed by a model field are sortable out of the box. A virtual column (a viewset/model method or property) is not tied to a database field, so the list view can't guess how to order by it. Declare an ``orderby_column`` on the method to make its header clickable. The value can be a field lookup string, which is handy for a ``JSONField`` key: .. code-block:: python class OrderViewset(ModelViewset): list_columns = ('number', 'total') def total(self, obj): return obj.data.get('total') total.short_description = 'Total' total.orderby_column = 'data__total' It can also be a query expression, e.g. to force a numeric sort of a JSON value (so ``2`` sorts before ``10`` instead of the lexical ``"10" < "2"``): .. code-block:: python from django.db.models import IntegerField from django.db.models.functions import Cast class OrderViewset(ModelViewset): list_columns = ('number', 'total') def total(self, obj): return obj.data.get('total') total.short_description = 'Total' total.orderby_column = Cast('data__total', IntegerField()) list_object_link_columns ------------------------- Use *list_object_link_columns* to control if and which fields in list_display should be linked to the “change” or "detail" page for an object. list_page_actions ----------------- TODO List filterset -------------- list_filter_fields ~~~~~~~~~~~~~~~~~~ To allow users to filter the list of displayed items Viewflow offers two key options for setting filters: ``list_filter_fields`` and ``list_filterset_class``. The ``list_filter_fields`` option allows you to specify a tuple of fields based on which the user can filter the list view. This is a simple but effective way to add basic filtering capabilities to your viewset. .. code-block:: python class CityViewset(ExportViewsetMixin, DetailViewMixin, DeleteViewMixin, ModelViewset): ... list_filter_fields = ('is_capital', 'country', ) In the above example, the list view for cities can be filtered by whether the city is a capital and by its country. list_filterset_class ~~~~~~~~~~~~~~~~~~~~ For more advanced filtering needs, you can use the ``list_filterset_class`` option. This allows you to specify a custom ``django_filters.FilterSet`` class that defines the available filters and their behavior. .. code-block:: python from django_filters import FilterSet, ModelChoiceFilter from .models import Ocean, Sea class SeaFilterSet(FilterSet): parent = ModelChoiceFilter( queryset=Sea.objects.filter( pk__in=Sea.objects.filter(parent__isnull=False).values('parent') ) ) ocean = ModelChoiceFilter(queryset=Ocean.objects.all(), help_text='') class SeaViewset(DeleteViewMixin, ModelViewset): ... list_filterset_class = filters.SeaFilterSet In this example, the `SeaFilterSet` class defines two filters: one for the parent sea and one for the ocean. These filters are then applied to the `SeaViewset` through the ``list_filterset_class`` option. Permissions =========== Viewflow CRUd viewsets check standard Django add/change/delete/view user per-object permissions. Unlike default django behavior, if user have no-object specific permission, for example if *user.has_perm('myapp.change_employee', obj=None)* equals *True*, default viewflow behavior is to assume that user have the permission for all objects. on if *has_perm* with *obj=None* return *False* object specific permission is checked. You can override in corresponding *has_view_permission*, *has_add_permission*, *has_change_permission*, *has_delete_permission* methods. .. code-block:: python def has_delete_permission(self, request, obj=None): return request.user.is_staff Views ===== Pre-built views for admin like interfaces. All *viewflow.views* are inherited from core Django generic views with very few additions and method redefinitions. All of them are accepts `viewset` as keyword parameter for the `.as_view()` method. If viewset present, the viewset methods and options would be used, permission checking methods like *has_add_permission* need to be overridden only in a viewset, and they would be used by a view. Same for *get_queryset* method. .. code-block:: python from viewflow.views import CreateView class EmployeeViewset(ModelViewset): create_view_class = views.EmployeeCreateView def get_success_url(self): return '../' class EmployeeCreateView(CreateView): pass Cookbook ======== `CRUD 101 sample`_ ======================================================================== # Writing JS and CSS Source: https://docs.viewflow.io/crud/css_and_js.html ======================================================================== ================== Writing JS and CSS ================== The default Viewflow interface is equipped with Hotwire/Turbo. This means that you should be cautious with JavaScript code initialization, as entire pages are updated with AJAX. Viewflow suggests wrapping all code as standard browser WebElements, which automates the initialization as soon as a component is added to a page. Setup building pipeline ======================= With numerous options available for setting up a JavaScript build pipeline, this guide focuses on using Vite as the build tool. Setup Vite ========== 1. Create a `package.json` install Vite as a development dependency: .. code-block:: shell npm init -y npm install vite --save-dev 2. Update your package.json to include a build script for Vite: .. code-block:: json "scripts": { "vite": "vite build", }, Configuring Vite ================ 1. Create a Vite configuration file, `vite.config.js`, with the following content: .. code-block:: javascript import { defineConfig } from 'vite' export default defineConfig({ build: { sourcemap: true, emptyOutDir: false, outDir: 'static/js/', lib: { entry: 'components/index.js', formats: ['iife'], name: 'my_components', fileName: () => "my_components.min.js", }, } }) This configuration enables source maps, specifies the output directory, and defines the library settings for your components. 2. Prepare your project structure for Vite by creating a components directory with an empty index.js file: .. code-block:: shell mkdir components/ touch components/index.js 3. Verify the setup by running Vite: .. code-block:: shell npm run vite This step confirms that your Vite configuration is correctly set up and ready for work. Setting Up Static Files in Django ================================= Configuring the Static Directory -------------------------------- Before diving into coding, it's essential to configure the directories where Django will look for JavaScript and CSS files. 1. To include a static/ folder in the root directory of your project, update your settings.py file with the following line: .. code-block:: python STATICFILES_DIRS = [BASE_DIR / "static"] This tells Django to include the specified static/ directory when searching for static files. Integrating Static Files into Templates --------------------------------------- After setting up your static directory, the next step is to incorporate your static files into Django templates. To include the generated JavaScript file in your base_page.html template, modify the template as follows: .. code-block:: html {% extends 'viewflow/base_page.html' %}{% load static %} {% block extrahead %} {% endblock %} This snippet extends the base page template from Viewflow, loads Django's static file handling mechanism, and includes your JavaScript file in the extrahead block, ensuring it's loaded with the page. Implementing a JavaScript Component =================================== Creating a JavaScript Component ------------------------------- To create a custom JavaScript component, you will start by defining the component in a new file within the components directory. Create the file components/my_component.js and define your component as follows: .. code-block:: javascript export class MyComponent extends HTMLElement { connectedCallback() { // ... } disconnectedCallback() { // ... } } The connectedCallback method is used for initializing the component or setting up event listeners when the component is added to the document. The disconnectedCallback method serves to clean up anything necessary when the component is removed, such as removing event listeners. Including and Registering the Component --------------------------------------- After creating your component, the next step is to include and register it so it can be used within your HTML files. In your components/index.js file, import the MyComponent class and register it as a custom element: .. code-block:: javascript import {MyComponent} from './my_component.js'; window.customElements.define('my-component', MyComponent); By calling window.customElements.define, you register the new element 'my-component' with the browser's Custom Elements registry, allowing you to use it as in your HTML. This step is crucial for integrating custom JavaScript functionality with your web components. ======================================================================== # CRUD Quick Start Source: https://docs.viewflow.io/crud/quickstart.html ======================================================================== .. title:: CRUD Quick Start =========== Quick start =========== Here's a quick start guide for using the Viewflow library with Django, assuming that the Viewflow library is already configured in your Django settings: Start a New App =============== Begin by creating a new Django app if you haven't already. Use the Django `startapp` command to do this. For example, if you want to create an app named 'atlas', run: .. code-block:: bash python manage.py startapp atlas Create a Model ============== Define a new model in your app. In this example, we'll create a `City` model. Add the following code to the `models.py` file in your app: .. code-block:: python from django.db import models class City(models.Model): name = models.CharField(max_length=250) population = models.PositiveIntegerField() ``` This model includes two fields: `name` for the city name and `population` for its population. Create a Viewset ================ You will need to create a viewset for your model. This can be done by using `ModelViewset` from Viewflow. Add the following code to a new file (or an existing one if you prefer) in your app: .. code-block:: python from viewflow.urls import ModelViewset from . import models class CityViewset(ModelViewset): model = models.City ``` Here, `CityViewset` is a class based on `ModelViewset` that is linked to the `City` model. Viewset to urls.py ================== Finally, link your viewset to a URL by editing the `urls.py` file in your Django project. Add the following code to the `urls.py` file: .. code-block:: python from django.contrib import admin from django.urls import path from atlas.viewsets import CityViewset urlpatterns = [ path("", CityViewset().urls), path("admin/", admin.site.urls), ] ``` This code sets up a URL pattern for your viewset and the Django admin site. By following these steps, you've set up a basic structure using the Viewflow library in your Django project. Remember to run migrations for your new model using `python manage.py makemigrations` and `python manage.py migrate`. Additionally, you might want to customize your viewset further based on the requirements of your project. ======================================================================== # Site Viewsets Source: https://docs.viewflow.io/crud/site.html ======================================================================== ============= Site Viewsets ============= :class:`~viewflow.urls.Site` and :class:`~viewflow.urls.Application` two viewsets helps to create website user interface and navigation. A Site could contains several applications, and each application could contains several viewsets. .. code-block:: python from viewflow.contrib.auth import AuthViewset from viewflow.urls import Site, Application site = Site(title="Workforce management", viewsets=[ Application( title='Employees', icon=Icon('people'), app_name='emp', viewsets=[ EmployeeViewset(), DepartmentViewset(), ), ]) urlpatterns = [ path('accounts/', AuthViewset().urls), path('', site.urls), ] .. seealso:: see the :class:`~viewflow.contrib.auth.AuthViewset` Current site and app variables are available in the template context as *{{ request.resolver_match.site }}* and *{{ request.resolver_match.app }}* Root `viewflow/base.html` template aliases it as *{{ site }}* and *{{ app }}* for later usage. It's common to have all application views templates extended from shared base template. Viewflow build-in templates extends *`{[ app.base_template_name }}`* that by default points to *`viewflow/base_page.html`* .. seealso:: Built-in :doc:`templates` override instructions *Site* and *Application* automatically builds user navigation menu from included viewsets. A viewset need to be included into application menu, should be mixed with :class:`viewflow.urls.AppMenuMixin` .. code-block:: python from viewflow import Icon from viewflow.urls import AppMenuMixin, Viewset class EmployeeViewset(AppMenuMixin, Viewset): title = _('Employee') icon = Icon('people') If you need to create a custom menu, override *.menu_template_name* attribute. .. code-block:: python class EmpApplication(Application): base_template_name = 'employees/base_page.html' menu_template_name = 'employees/menu.html' app_name = 'emp' viewsets = [ EmployeeViewset(), DepartmentViewset(), ] And in *'employees/menu.html'*: .. code-block:: django {% load viewflow_site %}
To restrict access to site or application override *.has_perm* method .. code-block:: python def has_perm(request, user): return user.is_staff Branding ======== To quickly change site colors, you can set *primary_color* and *secondary_color* attributes .. code-block:: python site = Site( title="Workforce management", primary_color='#3949ab', secondary_color='#5c6bc0', viewsets=[ EmployeeViewset(), DepartmentViewset(), ] ) ======================================================================== # CRUD Templates Source: https://docs.viewflow.io/crud/templates.html ======================================================================== .. title:: CRUD Templates ========= Templates ========= .. raw:: html To override any application template in django, you can set DIRS settings for the django template loader: .. code-block:: python TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [ # ./templates/ subdirectory is located at the same folder as ./manage.py os.path.join(BASE_DIR, 'templates') ], ... } To override a part of template, you can create a template with the same name in your `templates/` folder, extend it from the corresponding Viewflow template and specify block content to be redefined. For example, your file `$BASE_DIR/templates/viewflow/base.html` could looks like .. code-block:: django {% extends 'viewflow/base.html' %} {% load i18n static %} {% block title %}{{ site.title|default:"Corp. Ink."{% endblock %} base.html ========= Base site template with all css/js in header included and empty body. Provides current {{ site }} and {{ app }} viewset variables for body of inherited templates. {% block favicon %} ------------------- Redefine to get custom site favicon on the users browser .. code-block:: django {% block favicon %} {% endblock %} {% block title %} ----------------- Website header title .. code-block:: django {% block title %}{{ site.title|default:"The Corp. Inc." }}{% endblock %} {% block theme %} ----------------- Block to add additional css theme stylesheet. .. code-block:: django {% block theme %} {% endblock %} By default this block contains only *--mdc-theme-primary* and *--mdc-theme-secondary* filled with {{ site.primary_color }}, {{ site.secondary_color }} values. You can find more about theme options at https://material.io/develop/web/components/theme/ {% block css %} --------------------- Block to include additional site-global styles .. code-block:: django {% block css %} {{ block.super }} {% endblock %} {% block extrahead %} --------------------- Block to include additional css/js on the specific page. The good practice is to override the block on on a specific page template, and leave it empty in the base.html .. code-block:: django {% block extrahead %} {% endblock %} {% block body %} --------------- Page body content, use this block if you directly extends 'base.html' .. code-block:: django {% block body %}
Lorem ipsum...
{% endblock %} base_page.html ============== Base template for application specific pages. override it to change global drawer appearance: things like user name and avatar. Use as base template for application-wide page template. .. code-block:: django {% extends 'viewflow/base_page.html' %} TODO: Sample... {% block page-menu %} --------------------- Whole left column with drawer of a typical frontend page {% block page-menu-avatar %} ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: django {% block page-menu-avatar %} {% endblock %} {% block page-menu-user-info %} ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ {% block page-menu-app %} ^^^^^^^^^^^^^^^^^^^^^^^^^ Includes application {{ app.menu_template_name }} and site {[ site.menu_template_name }} menu templates. {% block page-menu-user-actions %} ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ User actions dropdown menu content .. code-block:: django {% block page-menu-user-actions %} account_box {% trans 'Preferences' %} {[ block.super }} {% endblock %} {% block page-toolbar-extra %} ------------------------------ Place for additional content on the top bar .. code-block:: django {% block page-toolbar-extra %}
settings
{% endblock %} {% block content %} ------------------- Main page content. To mach visual style of existing pages, whap it "mdc-layout-grid vf-page__grid" classes .. code-block:: django {% block content %}

Sample

Lorem ipsum...
{% endblock %} base_lockscreen.html ==================== Base template for authentication and error reporting pages {% block lockscreen-sidebar-header-title %} ------------------------------------------- {% block lockscreen-sidebar-content %} -------------------------------------- {% block lockscreen-sidebar-footer %} ------------------------------------- Bottom part of a lockscreen sidebar .. code-block:: django {% block lockscreen-sidebar-footer %} {% trans 'Log in' %} {% endblock %} {% block lockscreen-content-icon %} ----------------------------------- Big icon on the right side of the page. .. code-block:: django {% block lockscreen-content-icon %} lock {% endblock %} includes/ ========= To use viewflow components and styles outside of `viewflow/base.html` template, just include corresponding template inside your template `` .. code-block:: django {% include 'viewflow/includes/viewflow_css.html' %} {% include 'viewflow/includes/viewflow_js.html' %} ======================================================================== # Class-based URLs Source: https://docs.viewflow.io/crud/viewset.html ======================================================================== ================ Class-based URLs ================ Django url routing configuration composed from lists of mapping between url path expressions and views functions. URL configuration could be more then just a list. :class:`viewflow.fsm.Viewset` is the class that can be used as part of url configuration. These allow you to structure your code and quickly connect and customize predefined sets of URLs. Viewflow provides viewset for organizing CRUD, Workflow logic, perform authentication and connect other 3d part packages. The default behavior could be customized by inheriting and overriding base classes attributes and methods. .. code-block:: python from django.urls import path from viewflow.urls import Viewset class WebsiteViewset(Viewset): index_path = path('', index_view, name='index') Basically, a Viewset is the class with *.urls* property, suitable to include into Django root URL config in your main urls.py .. code-block:: python viewset = WebsiteViewset() urlpatterns = [ path('', viewset.urls) ] *viewset.urls* list contains all paths from the class attributes with *_path* suffix To mix url definition with other class method, an url attribute could be a property .. code-block:: python @property def page_path(self): return path('page/', self.page_view, name='page')) def page_view(self, request); return render(request, 'page.html') A Viewset could contain other viewset .. code-block:: python from viewflow.urls import route, Viewset class UserViewset(Viewset): page_path = path('account/', self.account_view name="account") class WebsiteViewset(Viewset): users_path = route('auth/', UserViewset()) All Viewset URLs are prefixed with namespace. Each part of Django URL configuration could have `application and instance namespace `_ By default, Viewset application namespace are determined from viewset class name, by lowering in and stripping common suffixes like *Viewset*. In the sample above, *WebsiteViewset* has *'website:'* namespace, and nested *UserViewset* has *'website:user:'* .. code-block:: python from django.urls import reverse >>> reverse('website:index') '/' >>> reverse('website:page') '/page/' >>> reverse('website:user:account') '/auth/account/' To manually specify a viewset namespace, use *app_name* class attribute or pass the *app_name* keyword parameter to the viewset constructor. .. code-block:: python class UserViewset(Viewset): page_path = path('account/', self.account_view name="account") class WebsiteViewset(Viewset): app_name = 'main' users_path = route('auth/', UserViewset(app_name='profile')) >>> reverse('main:profile:account') '/auth/account/' ======================================================================== # Forms and Widgets Source: https://docs.viewflow.io/forms/index.html ======================================================================== ================= Forms and Widgets ================= Viewflow separates form logic from HTML rendering. You define field layout in Python. The template handles the HTML. .. image:: /_static/img/Form.png :width: 450px Standard Django forms mix Python and HTML. You end up with widget attributes scattered across your code, or templates full of conditionals. Viewflow keeps them separate: Python controls which fields exist and their order, templates control how they look. Viewflow renders forms using Google Material Web Components. The ```` web component handles form submission with Hotwire/Turbo, so pages don't reload after submit. You get single-page app behavior without writing JavaScript. Table of Contents ================= .. toctree:: :maxdepth: 1 quick_start layout formsets widgets ======================================================================== # Formsets and Inlines Source: https://docs.viewflow.io/forms/formsets.html ======================================================================== =================== Formsets and Inlines =================== **PRO-only** Django formsets let you edit multiple related objects at once. Viewflow adds two field types that embed formsets inside a parent form, so you don't need separate views for related data. ModelFormField ============== Use ``ModelFormField`` for one-to-one relationships. It nests one form inside another. .. code-block:: python from viewflow.forms import ModelFormField class UserProfileForm(forms.ModelForm): class Meta: model = UserProfile fields = ['bio', 'website'] class UserForm(forms.ModelForm): profile = ModelFormField(form_class=UserProfileForm) class Meta: model = User fields = ['username', 'email', 'profile'] This creates a single form where you edit the User and their UserProfile together. InlineFormSetField ================== Use ``InlineFormSetField`` for one-to-many relationships. It embeds a formset that lets users add, edit, and delete multiple related objects. .. code-block:: python from viewflow.forms import InlineFormSetField OrderItemFormSet = inlineformset_factory( Order, OrderItem, fields=('product', 'quantity'), extra=1 ) class OrderForm(forms.ModelForm): items = InlineFormSetField(formset_class=OrderItemFormSet) class Meta: model = Order fields = ['customer', 'date'] This creates an Order form with a table of OrderItems. Users can add rows, edit existing ones, and delete items. Many-to-Many Relationships ========================== ``InlineFormSetField`` also works for many-to-many relationships. Use the through model. .. code-block:: python from django.contrib.auth.models import User, Group from django.forms.models import inlineformset_factory from viewflow.forms import ModelForm, InlineFormSetField, AjaxModelSelect GroupFormSet = inlineformset_factory( User, Group.user_set.through, fields=("group",), can_delete=False, extra=1, widgets={"group": AjaxModelSelect(lookups=["name__istartswith"])}, ) class UserForm(ModelForm): groups = InlineFormSetField(formset_class=GroupFormSet) class Meta: model = User fields = ["username", "first_name", "last_name"] This adds a group selector to the User form. The ``AjaxModelSelect`` widget loads groups on demand instead of fetching all at once. ======================================================================== # Form Layout Source: https://docs.viewflow.io/forms/layout.html ======================================================================== .. title:: Form Layout ====== Layout ====== .. autoclass:: viewflow.forms.Layout .. autoclass:: viewflow.forms.Row .. autoclass:: viewflow.forms.Column .. autoclass:: viewflow.forms.Span .. autoclass:: viewflow.forms.FieldSet .. autoclass:: viewflow.forms.FormSet .. autoclass:: viewflow.forms.FormLayout .. autoclass:: viewflow.forms.Caption .. autoclass:: viewflow.forms.Tag ======================================================================== # Forms Quick Start Source: https://docs.viewflow.io/forms/quick_start.html ======================================================================== .. title:: Forms Quick Start =========== Quick Start =========== Form ==== Create a form class that inherits from ``viewflow.forms.Form`` or ``viewflow.forms.ModelForm``. Add a ``layout`` attribute to control how fields appear. .. code-block:: python from django import forms from viewflow.forms import Layout, Row, FieldSet class RegistrationForm(forms.Form): username = forms.CharField( widget=forms.TextInput(attrs={'leading-icon': 'account_box'}) ) password = forms.CharField( widget=forms.PasswordInput(attrs={'leading-icon': 'lock_open'}) ) ... layout = Layout( 'username', 'email', Row('password', 'password_confirm'), FieldSet( 'Personal details', Row('first_name', 'last_name'), 'gender', 'receive_news', 'agree_toc' ) ) The layout defines field order, which fields share a row, and how fields are grouped. ``Row('password', 'password_confirm')`` puts two fields side by side. ``FieldSet`` groups fields under a heading. Template ======== Use the ``{% render %}`` tag to output the form. Wrap it in ```` for AJAX submission. .. code-block:: html {% load viewflow %}
{% csrf_token %}
{% render form form.layout %}
Disabling Turbo =============== Some buttons produce non-HTML output (like PDF downloads). Add ``data-turbo="false"`` to skip AJAX submission for that button. .. code-block:: html ======================================================================== # Form Widgets Source: https://docs.viewflow.io/forms/widgets.html ======================================================================== .. title:: Form Widgets ======= Widgets ======= .. autoclass:: viewflow.forms.InlineCalendar .. autoclass:: viewflow.forms.AjaxModelSelect .. autoclass:: viewflow.forms.AjaxMultipleModelSelect .. autoclass:: viewflow.forms.TrixEditorWidget .. autoclass:: viewflow.forms.DependentModelSelect .. autoclass:: viewflow.forms.TotalCounterWidget .. autoclass:: viewflow.forms.JSONEditorWiget ======================================================================== # Reporting Dashboards Source: https://docs.viewflow.io/dashboard/index.html ======================================================================== ==================== Reporting Dashboards ==================== Reporting Dashboards with Viewflow allow you to quickly implement interactive reports based on your Django models. This integration with Plotly makes it easy to create custom dashboards and visualizations for your application data. Prerequisites ============= Before you start, ensure you have Dash installed as it is a required dependency for creating dashboards. If you haven't installed Dash yet, you can do so by running: .. code-block:: sh pip install dash Getting Started =============== To get started, you can create a new Dashboard Viewset object. As any viewset the Dashboard object takes a number of parameters to customize its appearance and behavior, including a title, app_name, and dashboard_template_name. The layout parameter allows you to define the layout of your dashboard using a material.PageGrid object, which consists of one or more material.InnerRow objects. Within each InnerRow, you can place one or more dashboard elements such as material.Span or material.Card objects. Example: Recent Active Users Dashboard -------------------------------------- Here's an example of a Dashboard with a single Card element that displays the number of new flows started today: .. code-block:: python from dash import dcc from dash.dependencies import Input, Output from django.contrib.auth.models import User from django.utils import timezone from viewflow.contrib.plotly import Dashboard, material viewflowDashboard = Dashboard( title='Viewflow Demo Stats', app_name='vf_stats', dashboard_template_name='viewflow/contrib/plotly.html', layout=material.PageGrid([ dcc.Interval( id='interval-component', interval=2500, # in milliseconds n_intervals=0 ), material.InnerRow([ material.Span4([ material.Card( value_id='id_active_users', title='Active Users', icon='person' ) ]), ]) ]) ) @viewflowDashboard.callback( Output('id_active_users', 'children'), [Input('interval-component', 'n_intervals')], ) def update_active_users(n): today = timezone.now().date() active_users = User.objects.filter(last_login__date=today).count() return active_users This example defines a single Card element with the value_id of id_active_users, which is used in the update_active_users function to update the value displayed in the Card. The update_active_users function is called automatically when the dashboard is loaded or refreshed. URL Configuration ----------------- To use your Dashboard, you need to include it in your URL configuration. You can do this by adding the Dashboard object to your Viewset and including it in your urlpatterns: .. code-block:: python site = Site( title="Workflow 101 Demo", primary_color="#01579b", secondary_color="#0097a7", viewsets=[ Application( app_name="Reports", icon="account_balance", menu_template_name=None, viewsets=[viewflowDashboard], ) ] ) urlpatterns = [ path("", site.urls), ] Once your Dashboard is defined and included in your URL configuration, you can navigate to it in your application and see your data visualized in real-time. API === .. autoclass:: viewflow.contrib.plotly.material.PageGrid .. autoclass:: viewflow.contrib.plotly.material.InnerGrid .. autoclass:: viewflow.contrib.plotly.material.Span .. autoclass:: viewflow.contrib.plotly.material.Card ======================================================================== # ORM Extensions Source: https://docs.viewflow.io/orm/index.html ======================================================================== ============== ORM Extensions ============== Viewflow provides several extensions for Django ORM that can handle important real-life enterprise scenarios: Viewflow compositeFK fields: This allows you to connect to legacy databases and tables without a single column primary key. Jsonstore: This allows you to expose the internals of a JSON field as a usual Django model virtual field, enabling the use of Model forms, CRUD, and admin interfaces as usual. Table of Contents ================= .. toctree:: composite_fk json_storage ======================================================================== # Composite Foreign Key Field Source: https://docs.viewflow.io/orm/composite_fk.html ======================================================================== =========================== Composite Foreign Key Field =========================== *viewflow.fields.CompositeKey* - Virtual field allows Django to get access to database tables with ForeignKey. The field does not provide support for migrations, and suitable only for `Meta.managed = False` models only Usage ===== To use CompositeKey, import it from viewflow.fields and define it in your model. The following example demonstrates how to use CompositeKey with a Seat model, which references an Aircraft model via a composite key consisting of aircraft_code and seat_no. .. code-block:: python from viewflow.fields import CompositeKey class Seat(models.Model): id = CompositeKey(columns=['aircraft_code', 'seat_no']) aircraft_code = models.ForeignKey(Aircraft, models.DO_NOTHING) seat_no = models.CharField(max_length=4) class Meta: managed = False db_table = 'aircrafts_data' Important Notes =============== - The CompositeKey field is virtual and does not provide support for database migrations. It should only be used in models where Meta.managed = False. - Ensure that the db_table attribute in the Meta class matches the actual table name in the database. .. seealso:: - `Legacy DB `_ cookbook sample - `Timescale DB `_ cookbook sample ======================================================================== # JSON Storage Source: https://docs.viewflow.io/orm/json_storage.html ======================================================================== ============ JSON Storage ============ The viewflow.jsonstore module provides a set of virtual Django Model fields that store their data inside a single JSON database column. This is suitable for storing simple business data, quick prototypes without requiring DB migrations, and replacing multi-table inheritance joins. .. note:: These fields are maintained as a standalone package, `django-jsonstore `_, which Viewflow depends on and installs for you. ``viewflow.jsonstore`` re-exports it, so ``from viewflow import jsonstore`` and a plain ``import jsonstore`` give you the very same classes -- use whichever reads better. This page documents both. Quick start =========== .. code-block:: python from viewflow import jsonstore from django import forms from django.contrib import admin from django.db import models class Employee(models.Model): data = JSONField(default=dict) full_name = jsonstore.CharField(max_length=250) hire_date = jsonstore.DateField() salary = jsonstore.DecimalField(max_digits=10, decimal_places=2) The resulting model functions like a typical Django model. All virtual fields are available for constructing ModelForms, Viewsets, and Admin interfaces. .. code-block:: python class EmployeeForm(forms.ModelForm): class Meta: model = Employee fields = ['full_name', 'hire_date', 'salary'] @admin.register(Employee) class EmployeeAdmin(admin.ModelAdmin): list_display = ['full_name', 'hire_date'] fields = ['full_name', ('hire_date', 'salary')] Custom keys and nesting ======================= By default a field is stored under its own name inside a JSON column named ``data``. Both are configurable. ``json_field_name`` selects which JSON column holds the value, so one model can spread its virtual fields across several columns: .. code-block:: python class Employee(models.Model): public = models.JSONField(default=dict) private = models.JSONField(default=dict) full_name = jsonstore.CharField(max_length=250) # -> public ssn = jsonstore.CharField(max_length=20, json_field_name="private") # -> private ``json_key`` overrides the key used inside that column. Pass a string for a custom key, or a list/tuple for a nested path -- intermediate dicts are created automatically, and several fields can share a parent: .. code-block:: python class Person(models.Model): data = models.JSONField(default=dict) full_name = jsonstore.CharField(max_length=250, json_key="name") city = jsonstore.CharField(max_length=100, json_key=("address", "city")) zip_code = jsonstore.CharField(max_length=10, json_key=("address", "zip")) Person(full_name="Ann", city="Paris", zip_code="75001").data # {"name": "Ann", "address": {"city": "Paris", "zip": "75001"}} The custom key / nested path is used everywhere: reading and writing, filtering (``Person.objects.filter(city="Paris")``, ``filter(city__isnull=True)``) and ordering (``order_by("city")``). ``json_key`` works with every field type, including ``ForeignKey`` and ``ManyToManyField``. Polymorphic models ================== JSON Store plays well with `proxy models `_. Additional virtual fields allows to model different real-life objects, without involving multi-table inheritance. .. code-block:: python from django.contrib.auth.models import AbstractUser from viewflow import jsonstore class User(PolymorphicModel, AbstractUser): data = jsonstore.JSONField(null=True, default=dict) class Client(User): address = jsonstore.CharField(max_length=250) zip_code = jsonstore.CharField(max_length=250) city = jsonstore.CharField(max_length=250) vip = jsonstore.BooleanField() class Meta: proxy = True Foreign keys ============ Besides primitive fields, JSON Store can hold a reference to another model with ``jsonstore.ForeignKey``. The related object's primary key is stored in the JSON column under ``_id``; ``instance.`` returns the related object (loaded lazily and cached) and ``instance._id`` the raw pk. .. code-block:: python from viewflow import jsonstore from django.db import models class Book(models.Model): data = models.JSONField(default=dict) title = jsonstore.CharField(max_length=250) author = jsonstore.ForeignKey(Author, null=True, blank=True) book = Book(title="Viewflow", author=some_author) book.data # {"title": "Viewflow", "author_id": some_author.pk} book.author # book.author_id # some_author.pk Like the other JSON Store fields it is virtual (no database column, no migration) and its ``formfield()`` is a ``ModelChoiceField``, so it works in ModelForms, Viewsets and the Admin. The target model's primary key type does not matter -- an integer or a ``UUIDField`` pk is stored in its JSON-native form and coerced back on read. Because the value lives in a JSON document there is no database-level foreign key constraint: ``on_delete`` has no effect, no reverse accessor is added to the target model, and there is no join support. Query it through the JSON key instead:: Book.objects.filter(data__author_id=some_author.pk) ORM traversals such as ``filter(author__name=...)`` and ``select_related`` are not available. ``jsonstore.OneToOneField`` is available too and behaves the same on the forward side; being document-based it has no ``UNIQUE`` constraint, so the one-to-one nature is a modelling convention rather than an enforced invariant. Many-to-many ============ ``jsonstore.ManyToManyField`` keeps a many-to-many relation as a list of primary keys inside the JSONField -- no join table. ``instance.`` returns a manager with the familiar related-manager API. .. code-block:: python from viewflow import jsonstore from django.db import models class Post(models.Model): data = models.JSONField(default=dict) tags = jsonstore.ManyToManyField(Tag) post.tags.set([tag1, tag2]) # data == {"tags": [tag1.pk, tag2.pk]} post.tags.add(tag3) post.tags.remove(tag1) list(post.tags.all()) # [, ...] post.tags.count() ``formfield()`` yields a ``ModelMultipleChoiceField``, so it works in ModelForms, Viewsets and the Admin; a selection made through a form is persisted for you. The manager mutates the in-memory document only, so after ``add`` / ``remove`` / ``set`` you must ``instance.save()`` to persist -- exactly like the other JSON Store fields. There is no join table, ``through`` model or reverse accessor; ``all()`` is a ``filter(pk__in=...)`` queryset (database order). Target models with a ``UUIDField`` primary key are supported. Membership queries go through the JSON key and depend on the database's JSON support (e.g. on PostgreSQL ``Post.objects.filter(data__tags__contains=[tag.pk])``). Embedded models =============== For structured sub-data, declare a schema-only ``jsonstore.EmbeddedModel`` -- a "virtual model" with typed fields but no database table of its own -- and embed an instance in a host model with ``jsonstore.EmbeddedField``. It is stored as a nested JSON document. .. code-block:: python from viewflow import jsonstore from django.db import models class Money(jsonstore.EmbeddedModel): amount = jsonstore.IntegerField() currency = jsonstore.CharField(max_length=3, default="USD") class Product(models.Model): data = models.JSONField(default=dict) price = jsonstore.EmbeddedField(Money, null=True, blank=True) product.price = Money(amount=100, currency="USD") product.data # {"price": {"amount": 100, "currency": "USD"}} product.price.amount # 100 ``EmbeddedModel`` accepts every scalar/typed JSON Store field (``UUIDField``, ``DateField``, ... serialize at depth), honors ``json_key`` inside the document, and can itself contain an ``EmbeddedField`` for nested documents. Reading ``product.price`` returns an instance bound to the stored sub-document, so ``product.price.amount = 120`` followed by ``product.save()`` persists -- the same in-memory-then-save contract as the other JSON Store fields. ``EmbeddedField`` also accepts ``json_key`` and ``json_field_name`` to place the document anywhere in the parent. Query the nested values through the raw JSON key, e.g. ``Product.objects.filter(data__price__amount=100)``. For a *list* of embedded documents use ``jsonstore.EmbeddedListField`` -- the embedded-document analogue of ``ManyToManyField``: .. code-block:: python class Order(models.Model): data = models.JSONField(default=dict) lines = jsonstore.EmbeddedListField(LineItem) order.lines = [LineItem(sku="a", qty=1), LineItem(sku="b", qty=2)] order.lines.append(LineItem(sku="c", qty=3)) order.lines[0].qty = 5 # in-place edit, persisted on save() order.data # {"lines": [{...}, {...}, {...}]} ``instance.`` is a mutable, list-like accessor (indexing, iteration, ``len``, ``append``, ``insert``, ``del``) of embedded instances; each element is bound to its stored document so in-place edits are saved. It accepts ``json_key`` / ``json_field_name`` too. Editing embedded documents as forms ==================================== Embedded documents plug into ``viewflow.forms`` nested forms. Build a form from an ``EmbeddedModel`` with ``EmbeddedModelForm``, then edit a single document as a nested form (``EmbeddedFormField``) and a list of them as inlines (``EmbeddedFormSetField``) inside a ``viewflow.forms.ModelForm`` -- all persisted to the one JSON column. .. code-block:: python from viewflow import forms as vforms class AddressForm(vforms.EmbeddedModelForm): class Meta: model = Address class LineItemForm(vforms.EmbeddedModelForm): class Meta: model = LineItem class InvoiceForm(vforms.ModelForm): billing = vforms.EmbeddedFormField(AddressForm) # nested form lines = vforms.EmbeddedFormSetField(LineItemForm, extra=2, can_delete=True) class Meta: model = Invoice fields = ["number", "customer"] Saving the ``InvoiceForm`` writes the nested address and the line items into the model's JSON column. A runnable demo lives in ``cookbook/embed101`` (mounted at ``/embedded/`` in the demo site, with a Django admin showing the raw JSON). Supported databases =================== PostgreSQL 12+, MySQL 8+, MariaDB 10.5+, Oracle 21c+ and SQLite 3.38+. .. note:: On MariaDB and Oracle, ``order_by()`` on a numeric virtual field sorts the extracted value as text -- 10 comes before 2. PostgreSQL, MySQL and SQLite sort numerically. Django's own ``JSONField`` does the storage, so the requirements match `Django's `_. Querying a virtual field compiles to a key transform on the JSON column, so the database has to be able to index into the document. Cookbook ======== * ``cookbook/json101`` -- virtual fields on a plain model * ``cookbook/embed101`` -- embedded documents edited as nested forms API === Every class below is importable from ``viewflow.jsonstore`` and from the standalone ``jsonstore`` package under the same name. :class: viewflow.jsonstore.BigIntegerField :class: viewflow.jsonstore.BinaryField :class: viewflow.jsonstore.BooleanField :class: viewflow.jsonstore.CharField :class: viewflow.jsonstore.DateField :class: viewflow.jsonstore.DateTimeField :class: viewflow.jsonstore.DecimalField :class: viewflow.jsonstore.DurationField :class: viewflow.jsonstore.EmailField :class: viewflow.jsonstore.EmbeddedField :class: viewflow.jsonstore.EmbeddedListField :class: viewflow.jsonstore.EmbeddedModel :class: viewflow.jsonstore.FilePathField :class: viewflow.jsonstore.FloatField :class: viewflow.jsonstore.ForeignKey :class: viewflow.jsonstore.IntegerField :class: viewflow.jsonstore.IPAddressField :class: viewflow.jsonstore.GenericIPAddressField :class: viewflow.jsonstore.ManyToManyField :class: viewflow.jsonstore.NullBooleanField :class: viewflow.jsonstore.OneToOneField :class: viewflow.jsonstore.PositiveBigIntegerField :class: viewflow.jsonstore.PositiveIntegerField :class: viewflow.jsonstore.PositiveSmallIntegerField :class: viewflow.jsonstore.SlugField :class: viewflow.jsonstore.SmallIntegerField :class: viewflow.jsonstore.TextField :class: viewflow.jsonstore.TimeField :class: viewflow.jsonstore.URLField :class: viewflow.jsonstore.UUIDField ======================================================================== # 3d party integration Source: https://docs.viewflow.io/contrib/index.html ======================================================================== ==================== 3d party integration ==================== Table of Contents ================= .. toctree:: :maxdepth: 1 admin auth celery ======================================================================== # Administration Source: https://docs.viewflow.io/contrib/admin.html ======================================================================== ============== Administration ============== Shortcut for quicks access to django admininsration from main website .. autoclass:: viewflow.contrib.admin.Admin ======================================================================== # Authentication Source: https://docs.viewflow.io/contrib/auth.html ======================================================================== ============== Authentication ============== This viewset allows for quick integration of all standard Django authentication views into the URL configuration. It provides customization options for password change, profile view, and integration with Django-allauth if required. By using this viewset, developers can easily set up authentication-related routes, including login, logout, password change, password reset, and profile management, with minimal configuration effort. .. autoclass:: viewflow.contrib.auth.AuthViewset :members: __init__, login_view_class, get_login_view_kwargs, login_view, login_path, logout_view_class, get_logout_view_kwargs, logout_view, logout_path, pass_change_view_class, get_pass_change_view_kwargs, pass_change_view, pass_change_path, pass_change_done_view_class, get_pass_change_done_view_kwargs, pass_change_done_view, pass_change_done_path, pass_reset_view_class, get_pass_reset_view_kwargs, pass_reset_view, pass_reset_path, pass_reset_done_view_class, get_pass_reset_done_view_kwargs, pass_reset_done_view, pass_reset_done_path, pass_reset_confirm_view_class, get_pass_reset_confirm_view_kwargs, pass_reset_confirm_view, pass_reset_confirm_path, pass_reset_complete_view_class, get_pass_reset_complete_view_kwargs, pass_reset_complete_view, pass_reset_complete_path, profile_view_class, get_profile_view_kwargs, profile_view, profile_path, get_allauth_providers ======================================================================== # Celery Integration Source: https://docs.viewflow.io/contrib/celery.html ======================================================================== .. title:: Celery Integration ====== Celery ====== Celery is an asynchronous task queue/job queue based on distributed message passing. It is focused on real-time operation but supports scheduling as well. You can use Celery to run background tasks, making your applications more efficient and responsive. Usage ====== .. code-block:: python from celery import shared_task from viewflow.contrib import celery class MyFlow(Flow): ... perform = celery.Job(this.perform_task) @staticmethod @shared_task def perform_task(cls, activation_ref:str) -> None: with celery.Job.activate(activation_ref) as activation: print(activation.process.message) # Beware of race conditions. Unlike user tasks, for # long-running Celery jobs, Viewflow does not set up a lock. activation.process.sent_at = datetime.now() activation.process.save(update_fields="sent_at") # You can also set the lock manually for a short time with activation.flow_class.lock(activation.process.pk): activation.process.refresh_from_db() activation.process.sent_at = datetime.now() activation.process.save() .. seealso:: see the :class:`~viewflow.contrib.celery.Job` .. seealso:: see the :class:`~viewflow.contrib.celery.Timer` ======================================================================== # Articles Source: https://docs.viewflow.io/articles/index.html ======================================================================== :orphan: .. title:: Articles ======== Articles ======== .. meta:: :description: In-depth articles on building workflow-driven Django applications with Viewflow — code-first BPMN, testing, and design patterns for Python business processes. Guides and deep dives on building business applications with Django and Viewflow. Each article stands on its own and links back to the reference documentation. .. toctree:: :maxdepth: 1 python_native_bpmn django_workflow_engine python_bpmn low_code_django django_workflow_library_comparison ======================================================================== # Django Workflow Engine Source: https://docs.viewflow.io/articles/django_workflow_engine.html ======================================================================== .. title:: Django Workflow Engine ========================================== Choosing a Django Workflow Engine ========================================== .. meta:: :description: A Django workflow engine runs multi-step business processes inside your Django app. Learn when you need one, how it differs from a state machine or a task queue, and how to add it with Viewflow. A Django workflow engine runs a multi-step business process inside your Django application. It tracks where each process is, decides what happens next, and holds the state between steps — approvals, hand-offs, parallel tasks, and long waits for a human or an external event. Plain Django models and views handle one request at a time. A workflow engine handles the flow *across* many requests, days, and people. This guide covers when you need one, how it compares to the alternatives, and how to add it. Do You Need a Workflow Engine? ============================== Not every app does. Reach for one when the process, not the form, is the hard part. Three questions decide it: - **Does work pass between people or systems?** An approval that goes from a clerk to a manager to finance is a flow, not a single view. - **Does a process wait?** If a step pauses for a signature, a payment, or a timer, something has to remember where it stopped. - **Do steps run in parallel?** When two people work on different parts of the same case at once, you need to track and later join those branches. If you answered yes to any of these, hand-rolled status fields and ``if`` branches will sprawl fast. That is the job a workflow engine removes. Three Tools, Three Jobs ======================= "Workflow" covers three different Django tools. Picking the wrong one is the common mistake. .. list-table:: :header-rows: 1 :widths: 22 39 39 * - Tool - Use it for - Example * - State machine (FSM) - One object moving through states, guarded transitions - An invoice: draft → sent → paid * - Workflow engine (BPMN) - Multi-step processes with people, branches, and waits - A loan application across three departments * - Task queue (Celery) - Running background jobs off the request cycle - Sending email, resizing an image They compose. Viewflow ships the first two — :doc:`viewflow.fsm ` for state machines and :doc:`viewflow.workflow ` for BPMN — and calls Celery for the background jobs a workflow step needs. State Machine or Workflow Engine? ================================= Use a **state machine** when a single object walks through a fixed set of states, and each transition needs only a rule. It is a light layer on one model. See :doc:`viewflow.fsm `. Use a **workflow engine** when the process spans several tasks, branches on data, runs steps in parallel, or waits for people and events. This is where BPMN earns its keep: it models gateways, parallel splits, and joins that a state machine cannot express. The line is parallelism and hand-off. One object changing state is a machine. Work moving between actors is a workflow. Adding a Workflow Engine with Viewflow ====================================== Viewflow is an open-source workflow engine for Django. You define the process as a ``Flow`` class in Python, and Viewflow runs it — persisting state, assigning tasks, and driving the branches. .. code-block:: python from viewflow import this from viewflow.workflow import flow from . import models, views class LeaveRequestFlow(flow.Flow): process_class = models.LeaveRequestProcess start = flow.Start(views.RequestView.as_view()).Next(this.approve) approve = flow.View(views.ApproveView.as_view()).Next(this.check) check = ( flow.If(cond=lambda activation: activation.process.approved) .Then(this.notify) .Else(this.start) ) notify = flow.Function(this.send_email).Next(this.end) end = flow.End() def send_email(self, activation): ... Each node is a step. Gateways route the process; the engine keeps the state in the database, so a request can sit for a week and pick up exactly where it stopped. Common Questions ================ What is a Django workflow engine? --------------------------------- A Django workflow engine is a library that runs multi-step business processes inside a Django project. It records the state of each running process, decides the next step, assigns tasks to users, and survives restarts because the state lives in the database. When should I use a workflow engine instead of model status fields? ------------------------------------------------------------------- Use one once a process has more than a couple of steps, branches on data, waits for people or events, or runs steps in parallel. Status fields and ``if`` branches work for simple cases but sprawl into unmaintainable code as the process grows. What is the difference between a state machine and a workflow engine in Django? ------------------------------------------------------------------------------- A state machine guards the transitions of one object through a set of states. A workflow engine coordinates a whole process — several tasks, branches, parallel work, and waits — across many objects and people. Viewflow provides both. Is there an open-source workflow engine for Django? --------------------------------------------------- Yes. Viewflow is an open-source BPMN workflow engine for Django. You define the process as Python code and run it in production with parallel tasks, persistence, and Celery. The PRO edition adds a visual frontend. Next Steps ========== - Read the :doc:`BPMN workflow engine ` reference. - Compare it with the :doc:`finite state machine ` for simpler cases. - See :doc:`Python-Native BPMN ` for the code-first case. ======================================================================== # Viewflow vs SpiffWorkflow vs django-river Source: https://docs.viewflow.io/articles/django_workflow_library_comparison.html ======================================================================== .. title:: Viewflow vs SpiffWorkflow vs django-river ================================================= Viewflow vs SpiffWorkflow vs django-river ================================================= .. meta:: :description: An honest comparison of three Python workflow libraries — Viewflow, SpiffWorkflow, and django-river. How each defines processes, where it fits, and which to choose for a Django project. Three libraries come up when you need workflows in Python: **Viewflow**, **SpiffWorkflow**, and **django-river**. They solve overlapping problems in different ways, and the right choice depends on how much of the stack you want the library to own. Short version: Viewflow is a Django-native BPMN engine you write as code; SpiffWorkflow is a framework-agnostic BPMN engine you drive from diagrams; django-river is a Django state machine you configure at runtime. At a Glance =========== .. list-table:: :header-rows: 1 :widths: 26 25 25 24 * - - Viewflow - SpiffWorkflow - django-river * - Framework - Django-native - Framework-agnostic - Django-native * - Model - BPMN, code-first - BPMN, diagram-first - State machine * - Process defined in - Python ``Flow`` class - BPMN XML (or code) - Database / admin, at runtime * - Parallel gateways - Yes - Yes - No (state transitions) * - Persistence & task UI - Built in (Django ORM/views) - You build it - Django ORM + admin * - Diagram - Rendered from code - The source of truth - None * - DMN decision tables - No - Yes - No Each fits a different job. SpiffWorkflow: Diagram-First and Framework-Agnostic =================================================== `SpiffWorkflow `_ is a pure-Python BPMN engine. It parses full BPMN — pools and lanes, multi-instance tasks, sub-workflows, timers, signals, messages, and boundary events — and it adds DMN decision tables. The BPMN diagram is the source of truth; you author it in a designer and SpiffWorkflow executes it. It is not tied to any web framework, which is the trade-off: it runs the process, but persistence, the user interface, and task assignment are yours to build. In a Django project you wire SpiffWorkflow in yourself. The deeper difference is philosophy. In SpiffWorkflow the BPMN diagram is the source of truth: you draw the process in a designer, and the code executes the drawing. For a team that lives in code and version control, that extra artifact is a step away from native — the opposite of defining the flow as a Python class. **Choose SpiffWorkflow when** analysts own the BPMN diagrams, you need DMN, or you are not on Django and want a standalone, framework-agnostic engine. django-river: Runtime-Configurable State Transitions ==================================================== `django-river `_ is a Django workflow library built around a state machine you configure at runtime. States, transitions, and authorization rules live in the database, and you edit them through the admin — change the flow without a redeploy. On paper that is appealing: business users adjust the process, no engineering release needed. In practice, that same design tends to create more problems than it removes. Because the flow lives in the database instead of in code, it sits outside version control — so you cannot test it in CI, review a change in a pull request, or promote a known-good configuration from dev to production. An edit in the admin changes production behavior on the spot, with no diff and no safety net. You pay for that convenience later, in fragility. It also models only state transitions of a single object — no BPMN parallel branches, splits, or joins — and it has shipped no release since January 2021. **For a new project, django-river is hard to recommend.** The runtime-config approach looks flexible but works against testing, review, and safe releases, and the library is dormant. A maintained, code-first engine is the safer foundation. Viewflow: Django-Native BPMN as Code ==================================== Viewflow is an :doc:`open-source BPMN workflow engine ` for Django. You define the process as a Python ``Flow`` class — with gateways, parallel splits, and joins — and Viewflow runs it, persisting state in the Django ORM, assigning tasks to users, and rendering the BPMN diagram from the same code. See :doc:`Python-Native BPMN ` for why code-first matters. Because it is Django-native, Viewflow already includes the parts SpiffWorkflow leaves to you: persistence, views, and task lists. For simpler needs, it also ships :doc:`viewflow.fsm `, a state machine without the workflow engine. You can see it running before you install anything. `demo.viewflow.io `_ is a live Viewflow application — real workflows, task lists, and forms in a working Django app, not a slide deck or a diagram sandbox. Click through it, then read the code that produces it. .. code-block:: python from viewflow import this from viewflow.workflow import flow from . import models, views class ApprovalFlow(flow.Flow): process_class = models.ApprovalProcess start = flow.Start(views.RequestView.as_view()).Next(this.approve) approve = flow.View(views.ApproveView.as_view()).Next(this.end) end = flow.End() **Choose Viewflow when** you are on Django and want code-first BPMN with persistence and a task UI included, versioned and tested like the rest of your app. Which Should You Pick? ====================== - **On Django, want BPMN as code with batteries included** → Viewflow. - **Need a standalone, framework-agnostic BPMN engine, or DMN** → SpiffWorkflow. - **Runtime-editable state transitions** was django-river's niche, but its dormancy and the fragility of database-defined flows make it a weak choice for anything new. For a Django project it comes down to framework and philosophy: Viewflow's Django-native, code-first BPMN, or a standalone engine you drive from diagrams and integrate yourself. And with Viewflow you can judge it first-hand — the `live demo `_ is one click away. Common Questions ================ What is the difference between Viewflow and SpiffWorkflow? ---------------------------------------------------------- Viewflow is a Django-native BPMN engine where the process is Python code and persistence, views, and task assignment are built in. SpiffWorkflow is a framework-agnostic BPMN engine driven by BPMN diagrams, where you supply the persistence and user interface yourself. Viewflow fits Django apps; SpiffWorkflow fits standalone or non-Django use. Is django-river a BPMN engine? ------------------------------ No. django-river is a Django state machine with runtime-configurable states, transitions, and authorization rules. It handles state transitions of an object, not BPMN processes with parallel gateways, splits, and joins. Which Python workflow library is best for Django? ------------------------------------------------- For a Django project that needs full BPMN as code — with parallel tasks, persistence, and a task UI included — Viewflow is the most direct fit. django-river suits runtime-editable state transitions; SpiffWorkflow suits standalone BPMN execution you integrate yourself. Can I define workflows in code instead of a BPMN diagram? --------------------------------------------------------- Yes. Viewflow defines the process as a Python class and renders the diagram from it. SpiffWorkflow also supports building workflows in code, though its primary model is a BPMN diagram. django-river uses runtime configuration rather than a BPMN diagram. Next Steps ========== - Try the `live demo `_ — a running Viewflow app. - Read the :doc:`BPMN workflow engine ` reference. - See :doc:`Python-Native BPMN ` for the code-first case. - Compare BPMN with a :doc:`finite state machine ` for simpler flows. ======================================================================== # Low-Code Django Source: https://docs.viewflow.io/articles/low_code_django.html ======================================================================== .. title:: Low-Code Django ========================================== Low-Code Django Without the Lock-In ========================================== .. meta:: :description: Low-code Django means building apps from ready-made components (CRUD, forms, dashboards, workflows) while keeping full Python control. Viewflow gives low-code speed on plain Django, with no platform lock-in. Low-code Django means building an application from ready-made components instead of wiring every screen by hand. The app stays plain Django underneath. You get CRUD, forms, dashboards, and workflows out of the box, and you drop into Python the moment you need something the components don't cover. That last part is the difference from a low-code *platform*. Viewflow is a library, not a walled garden: it runs on your servers, against your models, and you own every line. The Low-Code Trap ================= Low-code platforms start fast. You click together a few screens and demo it the same afternoon. Then the client asks for something the platform can't express: a custom report, an odd integration, a rule the UI won't model. Now you hit a wall you cannot code your way through. The cost shows up late, once the project is already underway. You don't control the runtime, you can't leave the vendor, and the export button gives you data but not the app. Low-code Django avoids the trap by keeping the escape hatch open at all times. The components handle the common 80%. Plain Django handles the rest. What You Get Out of the Box =========================== Viewflow bundles the parts every business app repeats, so you write the parts that are yours. - **CRUD:** model viewsets with list, detail, create, update, and delete, wired to a clean UI. See :doc:`the CRUD docs `. - **Forms:** Material-styled rendering, layouts, and widgets. See :doc:`forms `. - **Dashboards:** reports and charts without hand-written JavaScript. See :doc:`dashboard `. - **Workflows:** BPMN business processes as Python code. See :doc:`the workflow engine `. Each part works on its own. Together they cover a full application. From Model to App ================= You define a Django model and wrap it in a viewset. That is the whole low-code step, and it is ordinary Python, so nothing stops you from extending it. .. code-block:: python from viewflow.urls import Application, Site, ModelViewset class Client(models.Model): name = models.CharField(max_length=240) email = models.EmailField(max_length=240) site = Site(title="ACME Corp", viewsets=[ Application(title="Sample App", app_name="crm", viewsets=[ ModelViewset(model=Client, list_display=["name", "email"]), ]), ]) You get a working app — login, list, detail, forms, a modern UI — from a model and a viewset. Need a custom view, an extra query, a different template? Write it. It is Django all the way down. Low-Code Platform vs. Low-Code Django ===================================== .. list-table:: :header-rows: 1 :widths: 30 35 35 * - Concern - Proprietary low-code - Low-code Django (Viewflow) * - Speed to first app - Fast - Fast * - Custom logic - Limited to the platform - Any Python you want * - Hosting - Vendor cloud - Your own servers * - Data and code ownership - Export, not portable - Yours, it is a Django project * - Escape hatch - None - Drop into Django anytime * - Lock-in - High - None Common Questions ================ What is low-code Django? ------------------------ Low-code Django is building a Django application from ready-made components (CRUD viewsets, forms, dashboards, and workflows) instead of coding every screen by hand, while the project stays a normal Django codebase you control. Is Viewflow a low-code platform? -------------------------------- No. Viewflow is an open-source Django library, not a hosted platform. It gives you low-code speed through reusable components, but the app runs on your servers, uses your models, and stays plain Django, with no vendor lock-in. Can I customize a low-code Django app? -------------------------------------- Yes, without limits. Because the app is an ordinary Django project, you can add custom views, queries, templates, and integrations wherever the components stop. The low-code parts and your own Python code live side by side. When should I use low-code Django instead of a no-code platform? ---------------------------------------------------------------- Use low-code Django when you need to ship fast *and* keep control: custom logic, your own hosting, and no lock-in. No-code platforms fit throwaway or simple internal tools; a Django-based app fits software you intend to own and grow. Next Steps ========== - Start with :doc:`the CRUD viewsets `. - Add :doc:`forms ` and a :doc:`dashboard `. - Model a process with the :doc:`workflow engine `. ======================================================================== # Python BPMN Source: https://docs.viewflow.io/articles/python_bpmn.html ======================================================================== .. title:: Python BPMN ========================================== Python BPMN: Running Workflows in Python ========================================== .. meta:: :description: Python BPMN means modeling and running BPMN business processes in Python. Learn how BPMN concepts map to Python code and how to execute workflows in a Django app with Viewflow. Python BPMN means modeling and running BPMN business processes in Python. You express the process — its tasks, gateways, and events — and a Python engine executes it: tracking state, routing work, and waiting for people or timers. BPMN (Business Process Model and Notation) is the standard notation for business processes. Designers usually draw it and store it as XML. In Python you choose: import that XML, or write the process as code. This article covers both, and how the concepts line up. How BPMN Concepts Map to Python =============================== BPMN has a small vocabulary. Each element maps one-to-one to code, so a diagram and a Python class describe the same thing. .. list-table:: :header-rows: 1 :widths: 34 66 * - BPMN element - In Python (Viewflow) * - Start / End event - ``flow.Start`` / ``flow.End`` * - User task - ``flow.View`` — a Django view a person completes * - Service task - ``flow.Function`` — inline Python code, or a Celery job * - Exclusive gateway - ``flow.If`` / ``flow.Switch`` * - Parallel gateway - ``flow.Split`` / ``flow.Join`` Because the mapping is one-to-one, you lose nothing by writing the process as code — and you gain everything Python gives you around it. Two Ways to Run BPMN in Python ============================== **Import the XML.** Some engines load a ``.bpmn`` file a designer produced and interpret it at runtime. The diagram stays the source of truth. This suits teams where analysts own the process and developers wire in the tasks. **Write it as code.** Others define the process as a Python class. The code is the source of truth, and the engine renders the diagram from it. This suits engineering teams who want the process in version control, under test, and open to their tools. See :doc:`Python-Native BPMN ` for why this matters. Both are "Python BPMN". The difference is which artifact leads — the drawing or the code. Running a BPMN Workflow with Viewflow ===================================== Viewflow is an open-source BPMN engine for Django that takes the code-first route. You write the ``Flow`` class; Viewflow executes it and renders the diagram from the same source. .. code-block:: python from viewflow import this from viewflow.workflow import flow from . import models, views class ShipmentFlow(flow.Flow): process_class = models.ShipmentProcess start = flow.Start(views.OrderView.as_view()).Next(this.split) split = flow.Split().Next(this.pack).Next(this.invoice) pack = flow.View(views.PackView.as_view()).Next(this.join) invoice = flow.View(views.InvoiceView.as_view()).Next(this.join) join = flow.Join().Next(this.ship) ship = flow.View(views.ShipView.as_view()).Next(this.end) end = flow.End() Packing and invoicing run in parallel after the split, then the join waits for both before shipping. The engine persists every step, so the process resumes after a restart or a long wait. Common Questions ================ What is Python BPMN? -------------------- Python BPMN is the modeling and execution of BPMN business processes in Python. You describe the process — tasks, gateways, and events — and a Python workflow engine runs it, keeping the state and routing work between people and systems. Can I run BPMN workflows in Python? ----------------------------------- Yes. Python workflow engines run BPMN processes either by importing BPMN XML or by defining the process as Python code. Viewflow takes the code-first route: you write a ``Flow`` class and run it in production with parallel tasks and persistence. How do BPMN elements map to Python code? ---------------------------------------- Each BPMN element becomes a node in a Python ``Flow`` class. Start and end events, user and service tasks, and exclusive or parallel gateways all have a direct code node. The diagram and the class describe the same process. Do I need to draw a BPMN diagram to run a workflow in Python? ------------------------------------------------------------- No. With a code-first engine you write the process as a Python class, and the engine generates the diagram from it. You get the picture for free without drawing it, and it never drifts out of sync with the code that runs. Next Steps ========== - Read the :doc:`BPMN workflow engine ` reference. - See :doc:`Python-Native BPMN ` for the code-first case. - Learn how to :doc:`write a flow `. ======================================================================== # Python-Native BPMN Source: https://docs.viewflow.io/articles/python_native_bpmn.html ======================================================================== .. title:: Python-Native BPMN ======================================= Python-Native BPMN: Workflows as Code ======================================= .. meta:: :description: Python-native BPMN means defining business processes as plain Python code instead of XML or a graphical designer. The payoff: git history, real tests, atomic changes with your models, and AI-friendly workflows. Python-native BPMN means you write a business process as plain Python code. No graphical editor, no hand-edited XML. The workflow lives in your codebase, next to the models it drives — versioned, reviewed, and tested like any other code. Viewflow is an :doc:`open-source BPMN workflow engine ` built on this idea. You write a ``Flow`` class in Python; Viewflow runs it in production and renders the BPMN diagram from the same source. .. contents:: On this page :local: :depth: 1 Why Define BPMN as Code? ======================== Traditional BPMN tools store the process as an XML file that a graphical designer generates. That XML is the source of truth, and everything else — your code, your tests, your review process — works around it. Python-native BPMN inverts that: the code *is* the process. Four things follow from that choice. Git History and Atomic Changes ------------------------------ Because the workflow is code, every change to it is a normal commit. You see who changed a gateway, when, and why — the same ``git blame`` and ``git log`` you already use. The bigger win is atomicity. A real change usually touches the model *and* the process together: you add a field, add the task that fills it, and add the migration in **one commit**. With XML-based tools, the model lives in code but the process lives in a separate designer. The two drift apart, and you re-sync them by hand. Code-first keeps them in lockstep. .. code-block:: python from viewflow import this from viewflow.workflow import flow from . import models, views class ApprovalFlow(flow.Flow): process_class = models.ApprovalProcess start = ( flow.Start(views.StartView.as_view()) .Next(this.approve) ) approve = ( flow.View(views.ApproveView.as_view()) .Next(this.check) ) check = ( flow.If(cond=lambda activation: activation.process.approved) .Then(this.end) .Else(this.start) ) end = flow.End() The diagram and the executable process come from this one class — no second file to keep in sync. Real, Fast Tests ---------------- You test a Python workflow with the tools you already run in CI. Run a node in isolation, set the process state, and assert on which task comes next. No BPMN simulator, no clicking through a designer. .. code-block:: python from django.test import TestCase from viewflow.workflow import activation class ApprovalFlowTests(TestCase): def test_approved_moves_to_check(self): process = ApprovalFlow.process_class.objects.create() task = process.task_set.create(flow_task=ApprovalFlow.approve) act = activation.Context(task) act.prepare() act.process.approved = True act.done() self.assertTrue( process.task_set.filter(flow_task=ApprovalFlow.check).exists() ) Because these are plain ``unittest`` (or ``pytest``) tests, they run in milliseconds and gate every pull request. Process logic stops being the untested part of the system. AI-Friendly by Construction --------------------------- Large language models read and write Python well. They are far weaker at hand-writing valid BPMN 2.0 XML. When your process is a Python class, an AI assistant can propose a new gateway, refactor a branch, or explain what a flow does — working in the same language as the rest of your project. This compounds with the points above. The model can read the git history to see how a process evolved, run the tests to check its own change, and submit the result as a reviewable diff. A workflow locked inside a proprietary designer offers the AI none of that context. Refactor and Reuse ------------------ Code-first workflows are Python, so ordinary refactoring applies. Shared logic goes in a function or a mixin. Common patterns become base ``Flow`` classes. You reuse a view from one process in another. Copy-paste between XML diagrams gives you none of this. Code-First vs. Graphical BPMN ============================= .. list-table:: :header-rows: 1 :widths: 34 33 33 * - Capability - Python-native BPMN (Viewflow) - XML / graphical BPMN * - Source of truth - Python code - Generated XML file * - Version control - Native ``git`` diffs and blame - Opaque XML diffs * - Code review - Standard pull requests - Separate tooling, if any * - Testing - ``pytest`` / ``unittest`` - Dedicated simulator * - Model + process changes - One atomic commit - Two sources, synced by hand * - AI support - First-class (plain Python) - Limited (BPMN XML) * - Diagram - Rendered from the code - Drawn by hand When a Graphical Designer Still Helps ===================================== Code-first is not a rejection of diagrams. Non-developers often need to *read* a process, and a rendered picture communicates better than a class. Viewflow keeps both: the code stays authoritative, and Viewflow renders the BPMN diagram from it. The picture never falls out of date with what runs. The PRO edition adds a visual frontend on top of the same code-defined flows. Common Questions ================ What is Python-native BPMN? --------------------------- Python-native BPMN is the practice of defining a business process in Python code — a class with tasks and gateways — instead of XML or a drag-and-drop designer. That code runs as the workflow and renders the diagram. Is code-first BPMN better than a graphical designer? ---------------------------------------------------- For engineering teams, yes on the axes that matter to them: version control, code review, automated testing, and atomic changes alongside the data model. Graphical designers remain useful for letting non-developers read a process, which is why Viewflow renders the diagram from the code rather than replacing it. Can I run BPMN workflows as Python code in production with Django? ------------------------------------------------------------------ Yes. Viewflow is an open-source BPMN engine for Django. You define the process as a ``Flow`` class and run it in production with support for parallel tasks, persistence, and Celery. See the :doc:`workflow documentation `. Why is code-first BPMN a good fit for AI coding assistants? ----------------------------------------------------------- AI assistants generate and refactor Python well, but struggle with valid BPMN 2.0 XML. A process written as a Python class lets the assistant use the project's real context — its git history, its tests, and its diffs — to make and verify changes. Next Steps ========== - Read the :doc:`BPMN workflow engine ` reference. - See :doc:`Writing Your Flow ` for the full node API. - Learn how to :doc:`test workflows `.