mirror of
https://github.com/viewflow/viewflow.git
synced 2026-03-13 10:32:34 +08:00
Changes sync
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -20,6 +20,7 @@
|
||||
/MANIFEST
|
||||
/crm.sqlite3
|
||||
db*.sqlite3
|
||||
dj*.sqlite3
|
||||
db*.sqlite3-journal
|
||||
node_modules/
|
||||
/docs/_build/**
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
2.2.8 GIT VERSION
|
||||
-----------------
|
||||
|
||||
- No exception raiased by Process/Task models in case if flow class deleted, but
|
||||
still referenced in DB
|
||||
- Fix jsonstore.DecimalField serialization
|
||||
- Add missing 'index' view for celery.Task node
|
||||
- Allow to revive flow.Subprocess and flow.NSubprocess nodes from error state
|
||||
|
||||
2.2.7 2024-08-16
|
||||
----------------
|
||||
|
||||
@@ -412,7 +421,7 @@ This is the cumulative release with many backward incompatibility changes.
|
||||
|
||||
* Activation now passed as a request attribute. You need to remove
|
||||
explicit activation parameter from view function signature, and use
|
||||
request.activation instead.
|
||||
request.activation instead.
|
||||
|
||||
* Built-in class based views are renamed, to be more consistent. Check
|
||||
the documentation to find a new view name.
|
||||
@@ -429,7 +438,7 @@ This is the cumulative release with many backward incompatibility changes.
|
||||
and signal handlers reusable over different flows.
|
||||
|
||||
* Flow namespace are no longer hard-coded. Flow views now can be
|
||||
attached to any namespace in a URL config.
|
||||
attached to any namespace in a URL config.
|
||||
|
||||
* flow_start_func, flow_start_signal decorators need to be used for
|
||||
the start nodes handlers. Decorators would establish a proper
|
||||
|
||||
5
tox.ini
5
tox.ini
@@ -68,6 +68,11 @@ deps =
|
||||
tblib==3.0.0
|
||||
twine==4.0.2
|
||||
|
||||
# typing
|
||||
django-stubs==5.0.4
|
||||
django-filter-stubs==0.1.3
|
||||
mypy==1.11.1
|
||||
|
||||
# packaging
|
||||
pyc-wheel==1.2.7
|
||||
setuptools
|
||||
|
||||
@@ -13,7 +13,7 @@ if TYPE_CHECKING:
|
||||
|
||||
UserModel = Any
|
||||
StateValue = Any
|
||||
Condition = Union[ThisObject, Callable[[object, object], bool]]
|
||||
Condition = Union[ThisObject, Callable[[object], bool]]
|
||||
Permission = Union[ThisObject, Callable[[object, Any], bool]]
|
||||
StateTransitions = Mapping["TransitionMethod", List["Transition"]]
|
||||
TransitionFunction = Callable[..., Any]
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import copy
|
||||
import json
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
from functools import partialmethod
|
||||
|
||||
from django.core.exceptions import FieldError
|
||||
@@ -190,7 +191,13 @@ class DateTimeField(JSONFieldMixin, fields.DateTimeField):
|
||||
|
||||
|
||||
class DecimalField(JSONFieldMixin, fields.DecimalField):
|
||||
pass
|
||||
def to_json(self, value):
|
||||
if value is not None:
|
||||
return str(value)
|
||||
|
||||
def from_json(self, value):
|
||||
if value is not None:
|
||||
return Decimal(value)
|
||||
|
||||
|
||||
class EmailField(JSONFieldMixin, fields.EmailField):
|
||||
|
||||
@@ -8,10 +8,9 @@ import copy
|
||||
import types
|
||||
import warnings
|
||||
from collections import namedtuple, OrderedDict
|
||||
from typing import Optional, Dict, Any, List, Union
|
||||
|
||||
from typing import Optional, Dict, Any, List
|
||||
from django.views.generic import RedirectView
|
||||
from django.urls import URLPattern, URLResolver, include, path, reverse
|
||||
from django.urls import ResolverMatch, URLPattern, URLResolver, include, path, reverse
|
||||
from django.urls.resolvers import RoutePattern
|
||||
|
||||
from viewflow.utils import (
|
||||
@@ -40,7 +39,7 @@ class _URLResolver(URLResolver):
|
||||
self.extra = kwargs.pop("extra", {})
|
||||
super(_URLResolver, self).__init__(*args, **kwargs)
|
||||
|
||||
def resolve(self, *args, **kwargs) -> str:
|
||||
def resolve(self, *args, **kwargs) -> ResolverMatch:
|
||||
result = super(_URLResolver, self).resolve(*args, **kwargs)
|
||||
if not isinstance(result.url_name, _UrlName):
|
||||
result.url_name = _UrlName(result.url_name)
|
||||
|
||||
@@ -17,7 +17,7 @@ class AppMenuMixin:
|
||||
"""A route that can be listed in an Application menu."""
|
||||
|
||||
title = None
|
||||
icon = Icon("view_carousel")
|
||||
icon: Icon | str = Icon("view_carousel")
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
@@ -47,7 +47,7 @@ class AppMenuMixin:
|
||||
|
||||
class Application(IndexViewMixin, Viewset):
|
||||
title = ""
|
||||
icon = Icon("view_module")
|
||||
icon: Icon | str = Icon("view_module")
|
||||
menu_template_name = "viewflow/includes/app_menu.html"
|
||||
base_template_name = "viewflow/base_page.html"
|
||||
permission = None
|
||||
|
||||
@@ -230,6 +230,7 @@ def get_object_data(obj: models.Model) -> Iterator[Tuple[models.Field, str, Any]
|
||||
|
||||
if (
|
||||
hasattr(obj, "artifact_object_id")
|
||||
and hasattr(obj, "artifact")
|
||||
and obj.artifact_object_id
|
||||
and obj.artifact is not None
|
||||
):
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
# LICENSE_EXCEPTION and the Commercial licence defined in file 'COMM_LICENSE',
|
||||
# which is part of this source code package.
|
||||
|
||||
from typing import Any, List, Union
|
||||
from django import forms
|
||||
from viewflow.utils import viewprop
|
||||
from viewflow.forms import Span
|
||||
@@ -29,7 +30,7 @@ class FormLayoutMixin(object):
|
||||
Mixin for FormView to infer View.fields definition from form Layout.
|
||||
"""
|
||||
|
||||
form_class = None
|
||||
form_class: Any = None
|
||||
|
||||
@viewprop
|
||||
def layout(self):
|
||||
@@ -37,7 +38,7 @@ class FormLayoutMixin(object):
|
||||
return self.form_class.layout
|
||||
|
||||
@viewprop
|
||||
def fields(self):
|
||||
def fields(self) -> Any:
|
||||
if self.form_class is None:
|
||||
if self.layout is not None:
|
||||
return _collect_elements(self.layout)
|
||||
|
||||
@@ -94,6 +94,8 @@ class FlowReferenceField(models.CharField):
|
||||
return import_flow_by_ref(value)
|
||||
except LookupError:
|
||||
return None
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
def get_prep_value(self, value): # noqa D1o2
|
||||
if value and not isinstance(value, str):
|
||||
@@ -118,7 +120,11 @@ class TaskReferenceField(models.CharField):
|
||||
def from_db_value(self, value, expression, connection):
|
||||
if value is None:
|
||||
return value
|
||||
return import_task_by_ref(value)
|
||||
|
||||
try:
|
||||
return import_task_by_ref(value)
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
def get_prep_value(self, value): # noqa D102
|
||||
if value and not isinstance(value, str):
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Type, Optional
|
||||
from django.views import View
|
||||
from django.urls import path
|
||||
from viewflow import viewprop
|
||||
from viewflow.urls import ViewsetMeta
|
||||
@@ -7,7 +9,7 @@ from . import utils
|
||||
class NodeDetailMixin(metaclass=ViewsetMeta):
|
||||
"""Task detail view."""
|
||||
|
||||
index_view_class = None
|
||||
index_view_class: Optional[Type[View]] = None
|
||||
|
||||
@viewprop
|
||||
def index_view(self):
|
||||
@@ -24,7 +26,7 @@ class NodeDetailMixin(metaclass=ViewsetMeta):
|
||||
name="index",
|
||||
)
|
||||
|
||||
detail_view_class = None
|
||||
detail_view_class: Optional[Type[View]] = None
|
||||
|
||||
@viewprop
|
||||
def detail_view(self):
|
||||
@@ -49,7 +51,7 @@ class NodeDetailMixin(metaclass=ViewsetMeta):
|
||||
class NodeExecuteMixin(metaclass=ViewsetMeta):
|
||||
"""Re-execute a gate manually."""
|
||||
|
||||
execute_view_class = None
|
||||
execute_view_class: Optional[Type[View]] = None
|
||||
|
||||
@viewprop
|
||||
def execute_view(self):
|
||||
@@ -70,7 +72,7 @@ class NodeExecuteMixin(metaclass=ViewsetMeta):
|
||||
class NodeUndoMixin(metaclass=ViewsetMeta):
|
||||
"""Allow to undo a completed task."""
|
||||
|
||||
undo_view_class = None
|
||||
undo_view_class: Optional[Type[View]] = None
|
||||
|
||||
@viewprop
|
||||
def undo_view(self):
|
||||
@@ -94,7 +96,7 @@ class NodeUndoMixin(metaclass=ViewsetMeta):
|
||||
class NodeCancelMixin(metaclass=ViewsetMeta):
|
||||
"""Cancel a task action."""
|
||||
|
||||
cancel_view_class = None
|
||||
cancel_view_class: Optional[Type[View]] = None
|
||||
|
||||
@viewprop
|
||||
def cancel_view(self):
|
||||
@@ -120,7 +122,7 @@ class NodeCancelMixin(metaclass=ViewsetMeta):
|
||||
class NodeReviveMixin(metaclass=ViewsetMeta):
|
||||
"""Review a canceled task"""
|
||||
|
||||
revive_view_class = None
|
||||
revive_view_class: Optional[Type[View]] = None
|
||||
|
||||
@viewprop
|
||||
def revive_view(self):
|
||||
|
||||
@@ -420,6 +420,7 @@ try:
|
||||
mixins.NodeDetailMixin,
|
||||
mixins.NodeCancelMixin,
|
||||
mixins.NodeUndoMixin,
|
||||
mixins.NodeReviveMixin,
|
||||
nodes.Subprocess,
|
||||
):
|
||||
"""
|
||||
@@ -452,11 +453,13 @@ try:
|
||||
detail_view_class = views.DetailTaskView
|
||||
cancel_view_class = views.CancelTaskView
|
||||
undo_view_class = views.UndoTaskView
|
||||
revive_view_class = views.ReviveTaskView
|
||||
|
||||
class NSubprocess(
|
||||
mixins.NodeDetailMixin,
|
||||
mixins.NodeCancelMixin,
|
||||
mixins.NodeUndoMixin,
|
||||
mixins.NodeReviveMixin,
|
||||
nodes.NSubprocess,
|
||||
):
|
||||
"""
|
||||
@@ -491,6 +494,7 @@ try:
|
||||
detail_view_class = views.DetailTaskView
|
||||
cancel_view_class = views.CancelTaskView
|
||||
undo_view_class = views.UndoTaskView
|
||||
revive_view_class = views.ReviveTaskView
|
||||
|
||||
except AttributeError:
|
||||
"""Pro-only functionality"""
|
||||
@@ -504,6 +508,22 @@ class Switch(
|
||||
mixins.NodeReviveMixin,
|
||||
nodes.Switch,
|
||||
):
|
||||
"""
|
||||
Gateway that selects one of the outgoing node.
|
||||
|
||||
Activates first node with matched condition.
|
||||
|
||||
Example::
|
||||
|
||||
select_responsible_person = (
|
||||
flow.Switch()
|
||||
.Case(this.dean_approval, lambda act: a.process.need_dean)
|
||||
.Case(this.head_approval, lambda act: a.process.need_head)
|
||||
.Default(this.supervisor_approval)
|
||||
)
|
||||
|
||||
"""
|
||||
|
||||
index_view_class = views.IndexTaskView
|
||||
detail_view_class = views.DetailTaskView
|
||||
cancel_view_class = views.CancelTaskView
|
||||
|
||||
@@ -43,6 +43,9 @@ class AbstractProcess(models.Model):
|
||||
@property
|
||||
def brief(self):
|
||||
"""Quick textual process state representation for end user."""
|
||||
if self.flow_class is None:
|
||||
return None
|
||||
|
||||
template_content = ""
|
||||
|
||||
if self.finished:
|
||||
@@ -175,6 +178,9 @@ class AbstractTask(models.Model):
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
if self.flow_task is None:
|
||||
return None
|
||||
|
||||
if self.flow_task.task_title:
|
||||
return self.flow_task.task_title
|
||||
return _(str(self.flow_task))
|
||||
|
||||
Reference in New Issue
Block a user