Changes sync

This commit is contained in:
Mikhail Podgurskiy
2024-08-23 14:40:29 +05:00
parent adda28bfe1
commit 66df5bed9e
13 changed files with 76 additions and 19 deletions

1
.gitignore vendored
View File

@@ -20,6 +20,7 @@
/MANIFEST
/crm.sqlite3
db*.sqlite3
dj*.sqlite3
db*.sqlite3-journal
node_modules/
/docs/_build/**

View File

@@ -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

View File

@@ -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

View File

@@ -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]

View File

@@ -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):

View File

@@ -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)

View File

@@ -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

View File

@@ -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
):

View File

@@ -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)

View File

@@ -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):

View File

@@ -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):

View File

@@ -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

View File

@@ -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))