Changes sync

This commit is contained in:
Mikhail Podgurskiy
2023-05-09 13:21:07 +06:00
parent 6a0dad6aaa
commit ee598bc45e
20 changed files with 336 additions and 166 deletions

View File

@@ -5,8 +5,8 @@ Changelog
GIT VERSION
-----------
Introduce SplitFirst Node
- Introduce SplitFirst Node
- celery.Timer Node
2.0.0.b3 2023-04-25
-------------------

View File

@@ -1,3 +1,20 @@
from .celery import app as celery_app
__all__ = ('celery_app',)
__all__ = ("celery_app",)
def print_urls():
"""For debug purpose"""
def list_urls(urls, parent_pattern=""):
for entry in urls:
full_pattern = parent_pattern + entry.pattern.regex.pattern
if hasattr(entry, "url_patterns"):
list_urls(entry.url_patterns, full_pattern)
else:
print(f"{entry.name or '<unnamed>'}: {full_pattern}")
from django.conf import settings
urlconf = __import__(settings.ROOT_URLCONF, {}, {}, [""])
list_urls(urlconf.urlpatterns)

View File

@@ -0,0 +1,59 @@
import os
import signal
import subprocess
import sys
import time
import unittest
from django.db import connection
from django.test import TransactionTestCase
from viewflow.workflow.models import Task
@unittest.skipUnless(
"DATABASE_URL" in os.environ, "Celery test requires specific database config"
)
class CeleryTestCase(TransactionTestCase):
def setUp(self):
"""Start celery worker connection the the test database"""
env = os.environ.copy()
database_url = env["DATABASE_URL"]
env["DATABASE_URL"] = "{}/{}".format(
database_url[: database_url.rfind("/")], connection.settings_dict["NAME"]
)
cmd = [
"celery",
"--app",
"cookbook.workflow101.config",
"worker",
"--loglevel",
"DEBUG",
"-E",
"-c",
"1",
]
self.process = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env
)
self.assertIsNone(self.process.poll())
def tearDown(self):
self.process.send_signal(signal.SIGTERM)
if "-v3" in sys.argv:
stdout, stderr = self.process.communicate()
print(" CELERY STDOUT ".center(80, "="))
print(stdout.decode())
print(" CELERY STDERR ".center(80, "="), file=sys.stderr)
print(stderr.decode(), file=sys.stderr)
def wait_for_task(self, process, flow_task, status=None):
for _ in range(100):
try:
return process.task_set.filter(flow_task=flow_task, status=status).get()
except Task.DoesNotExist:
time.sleep(0.05)
assert False, "Task {} not found".format(flow_task)

View File

@@ -5,46 +5,56 @@ from viewflow.urls import AppMenuMixin, Application, Site, Viewset, route
class NestedViewset(Viewset):
app_name = 'nested'
page_path = path('page/', TemplateView.as_view(template_name='viewflow/base.html'), name="page")
app_name = "nested"
page_path = path(
"page/", TemplateView.as_view(template_name="viewflow/base.html"), name="page"
)
class CityViewset(AppMenuMixin, Viewset):
index_path = path('', TemplateView.as_view(template_name='viewflow/base.html'), name="index")
nested_path = route('nested', NestedViewset())
index_path = path(
"", TemplateView.as_view(template_name="viewflow/base.html"), name="index"
)
nested_path = route("nested/", NestedViewset())
site = Site(title="Test site", viewsets=[
Application(app_name="test", viewsets=[
CityViewset(),
])
])
site = Site(
title="Test site",
viewsets=[
Application(
app_name="test",
viewsets=[
CityViewset(),
],
)
],
)
urlpatterns = [
path('', site.urls)
]
urlpatterns = [path("", site.urls)]
@override_settings(ROOT_URLCONF=__name__)
class Test(TestCase):
def test_context_injected(self):
response = self.client.get('/test/city/')
response = self.client.get("/test/city/")
match = response.wsgi_request.resolver_match
self.assertTrue(hasattr(match, 'site'))
self.assertTrue(hasattr(match, "site"))
self.assertEqual(match.site, site)
self.assertTrue(hasattr(match, 'app'))
self.assertTrue(hasattr(match, "app"))
self.assertEqual(match.app, site._children[0])
# App -> Site -> CityViewset
self.assertTrue(hasattr(match, 'viewset'))
self.assertTrue(hasattr(match, "viewset"))
self.assertEqual(match.viewset, site._children[0]._children[0])
def test_nested_viewset_injected(self):
response = self.client.get('/test/city/nested/page/')
response = self.client.get("/test/city/nested/page/")
match = response.wsgi_request.resolver_match
# App -> Site -> CityViewset -> Nested
self.assertTrue(hasattr(match, 'viewset'))
self.assertEqual(match.viewset, site._children[0]._children[0].nested_path.viewset)
self.assertTrue(hasattr(match, "viewset"))
self.assertEqual(
match.viewset, site._children[0]._children[0].nested_path.viewset
)

View File

@@ -5,52 +5,56 @@ from viewflow.urls import route, IndexViewMixin, Viewset
class NestedViewset(IndexViewMixin, Viewset):
app_name = 'nested'
app_name = "nested"
page_path = path('page/', TemplateView.as_view(template_name='viewflow/base.html'), name="page")
route_path = route('test', Viewset())
page_path = path(
"page/", TemplateView.as_view(template_name="viewflow/base.html"), name="page"
)
route_path = route("test/", Viewset())
class InheritedViewset(NestedViewset):
app_name = 'nested'
app_name = "nested"
page_path = path('page2/', TemplateView.as_view(template_name='viewflow/base.html'), name="page")
page_path = path(
"page2/", TemplateView.as_view(template_name="viewflow/base.html"), name="page"
)
class RootViewset(Viewset):
app_name = 'root'
app_name = "root"
index_path = path('', TemplateView.as_view(template_name='viewflow/base.html'), name="index")
nested_path = route('test', NestedViewset())
nested2_path = route('nested2', NestedViewset(app_name='nested2'))
index_path = path(
"", TemplateView.as_view(template_name="viewflow/base.html"), name="index"
)
nested_path = route("test/", NestedViewset())
nested2_path = route("nested2/", NestedViewset(app_name="nested2"))
# check here that route_url mounted second time successfully
nested3_path = route('inherited', InheritedViewset(app_name='nested2'))
nested3_path = route("inherited/", InheritedViewset(app_name="nested2"))
urlconfig = RootViewset()
urlpatterns = [
path('', urlconfig.urls)
]
urlpatterns = [path("", urlconfig.urls)]
@override_settings(ROOT_URLCONF=__name__)
class Test(TestCase): # noqa: D101
def test_created_urls(self):
self.assertEqual('/', reverse('root:index'))
self.assertEqual("/", reverse("root:index"))
self.assertEqual('/test/', reverse('root:nested:index'))
self.assertEqual('/test/page/', reverse('root:nested:page'))
self.assertEqual("/test/", reverse("root:nested:index"))
self.assertEqual("/test/page/", reverse("root:nested:page"))
self.assertEqual('/nested2/', reverse('root:nested2:index'))
self.assertEqual('/nested2/page/', reverse('root:nested2:page'))
self.assertEqual("/nested2/", reverse("root:nested2:index"))
self.assertEqual("/nested2/page/", reverse("root:nested2:page"))
def test_urlconf_resolve(self):
self.assertEqual('/', urlconfig.reverse('index'))
self.assertEqual('/test/', urlconfig.nested_path.viewset.reverse('index'))
self.assertEqual('/test/page/', urlconfig.nested_path.viewset.reverse('page'))
self.assertEqual("/", urlconfig.reverse("index"))
self.assertEqual("/test/", urlconfig.nested_path.viewset.reverse("index"))
self.assertEqual("/test/page/", urlconfig.nested_path.viewset.reverse("page"))
def test_auto_redirect(self):
response = self.client.get(reverse('root:nested:index'))
self.assertRedirects(response, '/test/page/')
response = self.client.get(reverse("root:nested:index"))
self.assertRedirects(response, "/test/page/")

View File

@@ -10,7 +10,7 @@
<section class="vf-card__header">
<h1 class="vf-card__title">{{ view.model|verbose_name_plural|title }}</h1>
<h2 class="vf-card__breadcrumbs">
<a href="{% reverse viewset 'list' %}">{{ view.model|verbose_name_plural|title }}</a>
<a href="{% current_viewset_reverse viewset 'list' %}">{{ view.model|verbose_name_plural|title }}</a>
<a>{% trans 'Export' %}</a>
</h2>
</section>

View File

@@ -4,7 +4,7 @@
{% if app.title %}<h3 class="vf-page__menu-subheader mdc-list-group__subheader">{{ app.title }}</h3>{% endif %}
{% block viewset_links %}
{% if app.parent and app.parent != site %}
{% reverse app.parent 'index' as parent_index %}{% if parent_index %}
{% current_viewset_reverse app.parent 'index' as parent_index %}{% if parent_index %}
<a class="mdc-list-item mdc-list-item--with-one-line mdc-list-item--with-leading-icon vf-page__menu-list-item" href="{{ parent_index }}">
<span class="mdc-list-item__start"><i class="material-icons">arrow_back</i></span>
<span class="mdc-list-item__content">{% trans 'Back' %}</span>
@@ -12,7 +12,7 @@
{% endif %}
{% endif %}
{% for viewset in app.menu_items %}{% if viewset|has_perm:request.user %}
{% reverse viewset 'index' as index_url %}{% if index_url %}
{% current_viewset_reverse viewset 'index' as index_url %}{% if index_url %}
<a class="mdc-list-item mdc-list-item--with-one-line mdc-list-item--with-leading-icon vf-page__menu-list-item" href="{{ index_url }}" {% if app.turbo_disabled or viewset.turbo_disabled %} data-turbo="false"{% endif %}>
<span class="mdc-list-item__start">{% if viewset.icon %}{{ viewset.icon }}{% else %}<i class="material-icons">view_carousel</i>{% endif %}</span>
<span class="mdc-list-item__content">{% trans viewset.title %}</span>

View File

@@ -40,7 +40,7 @@
<ul class="mdc-list" role="listbox">
{% for action in bulk_actions %}
{% if action.viewname %}
<li class="mdc-list-item mdc-list-item--with-one-line" data-value="{% reverse viewset action.viewname %}?{{ view.filterset.data.urlencode }}" role="option">
<li class="mdc-list-item mdc-list-item--with-one-line" data-value="{% current_viewset_reverse viewset action.viewname %}?{{ view.filterset.data.urlencode }}" role="option">
{% else %}
<li class="mdc-list-item mdc-list-item--with-one-line" data-value="{{ action.url }}?{{ view.filterset.data.urlencode }}" role="option">
{% endif %}

View File

@@ -4,7 +4,7 @@
<section class="vf-card__header">
<h1 class="vf-card__title">{{ view.object }}</h1>
<h2 class="vf-card__breadcrumbs">
<a href="{% reverse viewset 'list' %}">{{ view.model|verbose_name_plural|title }}</a>
<a href="{% current_viewset_reverse viewset 'list' %}">{{ view.model|verbose_name_plural|title }}</a>
<a>{{ view.object }}</a>
</h2>
<div class="vf-card__menu">

View File

@@ -20,6 +20,7 @@
<span class="mdc-list-item__start">{{ app.icon }}</span>
<span class="mdc-list-item__content">{% trans app.title %}</span>
</a>
{% block app_links_extra %}{% endblock %}
{% endif %}{% empty %}
<div class="mdc-list-item" style="height:auto; color: red">
<small>No applications found.</small>

View File

@@ -11,13 +11,13 @@
<h1 class="vf-card__title">{{ view.object }}</h1>
{% block confirm-header-subtitle %}
<h2 class="vf-card__breadcrumbs">
<a href="{% reverse viewset 'list' %}">{{ view.model|verbose_name_plural|title }}</a>
<a href="{% current_viewset_reverse viewset 'list' %}">{{ view.model|verbose_name_plural|title }}</a>
{% if view.object.pk %}
{% reverse viewset 'detail' view.object.pk as detail_url %}
{% current_viewset_reverse viewset 'detail' view.object.pk as detail_url %}
{% if detail_url %}
<a href="{{ detail_url }}">{{ view.object }}</a>
{% else %}
<a href="{% reverse viewset 'change' view.object.pk %}">{{ view.object }}</a>
<a href="{% current_viewset_reverse viewset 'change' view.object.pk %}">{{ view.object }}</a>
{% endif %}
{% endif %}
<a>{% trans 'Delete' %}</a>

View File

@@ -11,7 +11,7 @@
<section class="vf-card__header">
<h1 class="vf-card__title">{{ view.model|verbose_name_plural|title }}</h1>
<h2 class="vf-card__breadcrumbs">
<a href="{% reverse viewset 'list' %}">{{ view.model|verbose_name_plural|title }}</a>
<a href="{% current_viewset_reverse viewset 'list' %}">{{ view.model|verbose_name_plural|title }}</a>
<a>{% trans 'Delete' %}</a>
</h2>
</section>

View File

@@ -25,9 +25,9 @@
{% block form-header-subtitle %}
<h2 class="vf-card__breadcrumbs">
{% block breadcrumbs_items %}
<a href="{% reverse viewset 'list' %}">{{ view.model|verbose_name_plural|title }}</a>
<a href="{% current_viewset_reverse viewset 'list' %}">{{ view.model|verbose_name_plural|title }}</a>
{% if view.object.pk %}
{% reverse viewset 'detail' view.object.pk as detail_url %}
{% current_viewset_reverse viewset 'detail' view.object.pk as detail_url %}
{% if detail_url %}
<a href="{{ detail_url }}">{{ view.object }}</a>
{% endif %}

View File

@@ -2,9 +2,9 @@
{% load i18n viewflow %}
{% block breadcrumbs_items %}
<a href="{% reverse viewset 'list' %}">{{ view.model|verbose_name_plural|title }}</a>
<a href="{% current_viewset_reverse viewset 'list' %}">{{ view.model|verbose_name_plural|title }}</a>
{% if view.object.pk %}
{% reverse viewset 'detail' view.object.pk as detail_url %}
{% current_viewset_reverse viewset 'detail' view.object.pk as detail_url %}
{% if detail_url %}
<a href="{{ detail_url }}">{{ view.object }}</a>
{% endif %}

View File

@@ -15,8 +15,7 @@ from django.utils.html import conditional_escape
from viewflow.contrib import auth
from viewflow.forms import FormLayout
from viewflow.urls import Site, Viewset
from viewflow.urls import Site, Viewset, current_viewset_reverse
register = template.Library()
@@ -53,13 +52,9 @@ def _resolve_args(context, args, kwargs):
return args, kwargs
@register.tag("reverse")
class ViewsetURLNode(template.Node):
class BaseViewsetURLNode(template.Node):
"""
Reverse a url to a view within viewset
Example::
{% reverse viewset viewname args kwargs %}
Base class for reversing a url to a view within a viewset
"""
def __init__(self, parser, token):
@@ -97,8 +92,8 @@ class ViewsetURLNode(template.Node):
url = ""
try:
url = viewset.reverse(
view_name, args=args, kwargs=kwargs, current_app=current_app
url = self._reverse_url(
viewset, view_name, args, kwargs, current_app, context
)
except NoReverseMatch:
if self.variable_name is None:
@@ -113,6 +108,36 @@ class ViewsetURLNode(template.Node):
return url
@register.tag("reverse")
class ViewsetURLNode(BaseViewsetURLNode):
"""
Reverse a url to a view within viewset
Example::
{% reverse viewset viewname args kwargs %}
"""
def _reverse_url(self, viewset, view_name, args, kwargs, current_app, context):
return viewset.reverse(
view_name, args=args, kwargs=kwargs, current_app=current_app
)
@register.tag("current_viewset_reverse")
class CurrentViewsetURLNode(BaseViewsetURLNode):
"""
Reverse a url to a view within viewset
Example::
{% current_viewset_reverse viewset viewname args kwargs %}
"""
def _reverse_url(self, viewset, view_name, args, kwargs, current_app, context):
return current_viewset_reverse(
context.request, viewset, view_name, args=args, kwargs=kwargs
)
@register.tag("render")
class FormNode(template.Node):
"""

View File

@@ -1,18 +1,62 @@
"""Class based CRUD."""
from .base import (
BaseViewset, IndexViewMixin, Viewset, ViewsetMeta, route
)
from .base import BaseViewset, IndexViewMixin, Viewset, ViewsetMeta, route
from .sites import AppMenuMixin, Application, Site
from .model import (
BaseModelViewset, DeleteViewMixin, DetailViewMixin, CreateViewMixin,
UpdateViewMixin, ListBulkActionsMixin, ModelViewset, ReadonlyModelViewset,
BaseModelViewset,
DeleteViewMixin,
DetailViewMixin,
CreateViewMixin,
UpdateViewMixin,
ListBulkActionsMixin,
ModelViewset,
ReadonlyModelViewset,
)
__all__ = (
'BaseViewset', 'IndexViewMixin', 'Viewset', 'ViewsetMeta', 'route',
'AppMenuMixin', 'Application', 'Site',
'BaseModelViewset', 'DeleteViewMixin', 'DetailViewMixin', 'CreateViewMixin',
'UpdateViewMixin', 'ListBulkActionsMixin', 'ModelViewset', 'ReadonlyModelViewset',
"BaseViewset",
"IndexViewMixin",
"Viewset",
"ViewsetMeta",
"route",
"AppMenuMixin",
"Application",
"Site",
"BaseModelViewset",
"DeleteViewMixin",
"DetailViewMixin",
"CreateViewMixin",
"UpdateViewMixin",
"ListBulkActionsMixin",
"ModelViewset",
"ReadonlyModelViewset",
)
def current_viewset_reverse(request, viewset, view_name, args=None, kwargs=None):
"""
Reverse a URL within the current viewset. This function is specifically
designed for use in templates that belong to a view within a viewset.
"""
current_app = getattr(request, "current_app", None)
if current_app is None:
current_app = getattr(request.resolver_match, "namespace", None)
if args:
current_viewset = viewset
while current_viewset is not None:
for kwarg in current_viewset.extra_kwargs or []:
args = [request.resolver_match.kwargs[kwarg]] + args
current_viewset = current_viewset.parent
else:
if kwargs is None:
kwargs = {}
current_viewset = viewset
while current_viewset is not None:
for kwarg in current_viewset.extra_kwargs or []:
kwargs.setdefault(kwarg, request.resolver_match.kwargs[kwarg])
current_viewset = current_viewset.parent
return viewset.reverse(view_name, args=args, kwargs=kwargs, current_app=current_app)

View File

@@ -4,7 +4,6 @@
# This work is dual-licensed under AGPL defined in file 'LICENSE' with
# LICENSE_EXCEPTION and the Commercial license defined in file 'COMM_LICENSE',
# which is part of this source code package.
import copy
import types
import warnings
@@ -14,7 +13,12 @@ from django.views.generic import RedirectView
from django.urls import URLPattern, URLResolver, include, path, reverse
from django.urls.resolvers import RoutePattern
from viewflow.utils import camel_case_to_underscore, strip_suffixes, DEFAULT
from viewflow.utils import (
camel_case_to_underscore,
strip_suffixes,
list_path_components,
DEFAULT,
)
class _UrlName(str):
@@ -69,6 +73,7 @@ class BaseViewset(object):
app_name = None
namespace = None
parent_namespace = None
extra_kwargs = None
def __init__(self):
super().__init__()
@@ -207,6 +212,7 @@ class Viewset(BaseViewset, metaclass=ViewsetMeta):
attr_value = getattr(self.__class__, attr_name)
if isinstance(attr_value, Route):
viewset = copy.copy(attr_value.viewset)
viewset.extra_kwargs = list_path_components(attr_value.prefix)
setattr(self, attr_name, Route(attr_value.prefix, viewset))
self._children.append(viewset)
@@ -217,7 +223,7 @@ class Viewset(BaseViewset, metaclass=ViewsetMeta):
value.viewset.parent = self
patterns, app_name, namespace = value.viewset.urls
pattern = path(
"{}/".format(value.prefix) if value.prefix else "",
value.prefix if value.prefix else "",
include((patterns, app_name), namespace=namespace),
)
return pattern
@@ -256,7 +262,7 @@ class Viewset(BaseViewset, metaclass=ViewsetMeta):
viewset
)
urlpatterns.append(
self._create_url_pattern(route(viewset.app_name, viewset))
self._create_url_pattern(route(f"{viewset.app_name}/", viewset))
)
return urlpatterns

View File

@@ -6,12 +6,12 @@
# which is part of this source code package.
import re
from functools import update_wrapper
from typing import Any, Callable, Iterator, List, Optional, Tuple, Type, TypeVar
from django.apps import apps
from django.core import mail
from django.conf import settings
from django.contrib import auth
from django.core import mail
from django.db import models
from django.utils.html import conditional_escape
from django.utils.safestring import mark_safe
@@ -19,6 +19,10 @@ from django.utils.safestring import mark_safe
__all__ = ("has_object_perm", "viewprop", "DEFAULT", "first_not_default")
T = TypeVar("T")
TCallable = TypeVar("TCallable", bound=Callable[..., Any])
class MARKER(object):
def __init__(self, marker: str):
self.marker = marker
@@ -117,52 +121,66 @@ def get_containing_app_data(module):
return app_config.label, app_config.module.__name__
def is_owner(owner, user):
def is_owner(owner: models.Model, user: models.Model) -> bool:
"""
Checks whether the specified user instance or subclass is equal to the
specified owner instance or subclass.
"""
return isinstance(user, owner.__class__) and owner.pk == user.pk
return isinstance(user, type(owner)) and owner.pk == user.pk
class viewprop(object):
class viewprop:
"""
A property that can be overridden.
The viewprop class is a descriptor that works similarly to the built-in
`property` decorator but allows its value to be overridden on instances
of the class it is used in.
"""
def __init__(self, func):
def __init__(self, func: Any):
self.__doc__ = getattr(func, "__doc__")
self.fget = func
def __get__(self, obj, objtype=None):
def __get__(self, obj: Optional[Any], objtype: Optional[Type[Any]] = None) -> Any:
if obj is None:
return self
if self.fget.__name__ not in obj.__dict__:
obj.__dict__[self.fget.__name__] = self.fget(obj)
return obj.__dict__[self.fget.__name__]
def __set__(self, obj, value):
def __set__(self, obj: Any, value: Any) -> None:
obj.__dict__[self.fget.__name__] = value
def __repr__(self):
def __repr__(self) -> str:
return "<view_property func={}>".format(self.fget)
class LazySingletonDescriptor(object):
class LazySingletonDescriptor:
"""
Descriptor class that creates a lazy singleton instance.
This descriptor can be used as a class attribute, and the first time the
attribute is accessed, it creates an instance of the class. Subsequent
accesses return the same instance, effectively making the class a singleton.
"""
def __init__(self): # noqa D102
self.instance = None
def __init__(self) -> None: # noqa D102
self.instance: Optional[T] = None
def __get__(self, instance=None, owner=None):
def __get__(
self,
instance: Optional[T] = None,
owner: Optional[Type[T]] = None,
) -> T:
if self.instance is None:
if owner is None:
raise ValueError("Owner class not provided")
self.instance = owner()
return self.instance
class Icon(object):
class Icon:
"""
Class representing an HTML icon element.
@@ -174,11 +192,11 @@ class Icon(object):
The CSS class to apply to the icon element.
"""
def __init__(self, icon_name, class_=None):
def __init__(self, icon_name: str, class_: Optional[str] = None):
self.icon_name = icon_name
self.class_ = class_ or ""
def __str__(self):
def __str__(self) -> str:
icon_name = conditional_escape(self.icon_name)
class_name = conditional_escape(self.class_)
return mark_safe(
@@ -186,7 +204,7 @@ class Icon(object):
)
def get_object_data(obj):
def get_object_data(obj: models.Model) -> Iterator[Tuple[models.Field, str, Any]]:
"""
List of object fields to display. Choice fields values are expanded to
readable choice label.
@@ -212,3 +230,23 @@ def get_object_data(obj):
if hasattr(obj, "artifact_object_id") and obj.artifact_object_id:
yield from get_object_data(obj.artifact)
PATH_PARAMETER_COMPONENT_RE = re.compile(
r"<(?:(?P<converter>[^>:]+):)?(?P<parameter>[^>]+)>"
)
def list_path_components(route: str) -> List[str]:
"""
Extract keyword arguments from a Django path expression, which are used as
input parameters for a view function.
Example Usage:
>>> list_path_components('/prefix/<str:pk>')
['pk']
>>> list_path_components('<str:pk>/<int:id>')
['pk', 'id']
"""
return [match["parameter"] for match in PATH_PARAMETER_COMPONENT_RE.finditer(route)]

View File

@@ -15,20 +15,19 @@ from django.views import generic
from viewflow.utils import has_object_perm, get_object_data
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
class DetailModelView(generic.DetailView):
viewset = None
page_actions = None
object_actions = None
def has_view_permission(self, user, obj=None):
if self.viewset is not None and hasattr(self.viewset, 'has_view_permission'):
if self.viewset is not None and hasattr(self.viewset, "has_view_permission"):
return self.viewset.has_view_permission(user, obj=obj)
else:
return (
has_object_perm(user, 'view', self.model, obj=obj)
or has_object_perm(user, 'change', self.model, obj=obj)
)
return has_object_perm(
user, "view", self.model, obj=obj
) or has_object_perm(user, "change", self.model, obj=obj)
def get_object_data(self):
"""List of object fields to display.
@@ -38,22 +37,32 @@ class DetailModelView(generic.DetailView):
def get_page_actions(self, *actions):
if self.viewset:
actions = self.viewset.get_detail_page_actions(self.request, self.object) + actions
actions = (
self.viewset.get_detail_page_actions(self.request, self.object)
+ actions
)
if self.page_actions:
actions = self.page_actions + actions
return actions
def get_object_actions(self, *actions):
if self.viewset:
actions = self.viewset.get_detail_page_object_actions(self.request, self.object) + actions
actions = (
self.viewset.get_detail_page_object_actions(self.request, self.object)
+ actions
)
if self.object_actions:
actions = self.object_actions + actions
return actions
def get_object_change_link(self):
if self.viewset and hasattr(self.viewset, 'has_change_permission'):
from viewflow.urls import current_viewset_reverse
if self.viewset and hasattr(self.viewset, "has_change_permission"):
if self.viewset.has_change_permission(self.request.user, self.object):
return self.viewset.reverse('change', args=[self.object.pk])
return current_viewset_reverse(
self.request, self.viewset, "change", kwargs={"pk": self.object.pk}
)
def get_object(self):
pk = self.kwargs.get(self.pk_url_kwarg)
@@ -80,7 +89,9 @@ class DetailModelView(generic.DetailView):
if self.template_name is None:
opts = self.model._meta
return [
'{}/{}{}.html'.format(opts.app_label, opts.model_name, self.template_name_suffix),
'viewflow/views/detail.html',
"{}/{}{}.html".format(
opts.app_label, opts.model_name, self.template_name_suffix
),
"viewflow/views/detail.html",
]
return [self.template_name]

View File

@@ -1,12 +1,9 @@
import uuid
from contextlib import contextmanager
from django.db import transaction
from django.utils import timezone
from ..activation import Activation
from ..base import Node
from ..exceptions import FlowRuntimeError
from ..fields import get_flow_ref, import_flow_by_ref
from ..fields import get_flow_ref
from ..status import STATUS
from . import mixins
@@ -20,7 +17,7 @@ class AbstractJobActivation(mixins.NextNodeActivationMixin, Activation):
process=prev_activation.process,
flow_task=flow_task,
token=token,
external_task_id=str(uuid.uuid4())
external_task_id=str(uuid.uuid4()),
)
task.save()
task.previous.add(prev_activation.task)
@@ -41,20 +38,17 @@ class AbstractJobActivation(mixins.NextNodeActivationMixin, Activation):
self.activate_next()
def ref(self):
return f'{get_flow_ref(self.flow_class)}/{self.process.pk}/{self.task.pk}'
return f"{get_flow_ref(self.flow_class)}/{self.process.pk}/{self.task.pk}"
class AbstractJob(
mixins.NextNodeMixin,
Node
):
task_type = 'JOB'
class AbstractJob(mixins.NextNodeMixin, Node):
task_type = "JOB"
shape = {
'width': 150,
'height': 100,
'text-align': 'middle',
'svg': """
"width": 150,
"height": 100,
"text-align": "middle",
"svg": """
<rect class="task" width="150" height="100" rx="5" ry="5"/>
<path class="task-label"
d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65
@@ -68,46 +62,7 @@ class AbstractJob(
c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46
c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5
s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"/>
"""
""",
}
bpmn_element = 'scriptTask'
@staticmethod
@contextmanager
def activate(activation_ref):
try:
flow_ref, process_pk_ref, task_pk_ref = activation_ref.rsplit('/', 2)
process_pk, task_pk = int(process_pk_ref), int(task_pk_ref)
except ValueError as exc:
raise FlowRuntimeError(f'Invalid activation reference - "{activation_ref}"') from exc
flow_class = import_flow_by_ref(flow_ref)
# start
with transaction.atomic(), flow_class.lock(process_pk):
try:
task = flow_class.task_class.objects.get(pk=task_pk, process_id=process_pk)
if task.status == STATUS.CANCELED:
return
except flow_class.task_class.DoesNotExist:
# There was rollback on job task created transaction,
# we don't need to do the job
return
else:
activation = task.flow_task.activation_class(task)
activation.start()
try:
yield activation # long-running job without lock
except Exception as exc:
# error
with transaction.atomic(), flow_class.lock(process_pk):
activation = task.flow_task.activation_class(task)
activation.error(exc)
raise exc
else:
# success
with transaction.atomic(), flow_class.lock(process_pk):
activation = task.flow_task.activation_class(task)
activation.execute()
bpmn_element = "scriptTask"