Changes sync

This commit is contained in:
Mikhail Podgurskiy
2024-07-12 20:28:35 +05:00
parent 3c58dfacaf
commit d00feaa7eb
22 changed files with 234 additions and 31 deletions

View File

@@ -2,6 +2,26 @@
Changelog
=========
2.2.4 2024-07-12
-----------------
- Clone data, seed, and artifacts from canceled tasks to revived tasks.
- Enhance error handling for celery.Job.
- Improve the process cancellation template.
- Redirect to the task detail page after canceling or undoing actions, instead
of redirecting to the process detail page.
- Added links to parent subprocess and parent task on the subprocess process and
task details pages.
- Updated the Process.parent_task field to use related_name='subprocess',
allowing access to subprocesses via task.subprocess
- Enhanced CreateProcessView and UpdateProcessView to set process_seed and
artifact_generic_foreign_key fields based on form.cleaned_data, as Django
model forms do not handle this automatically.
- Added tasks with an ERROR status to the process dashboard for better visibility and tracking.
- Added tooltip hover titles to nodes without text labels in the SVG workflow graph.
- Marked StartHandler nodes as BPMN Start Message events on the SVG graph.
- Fixed rendering of hidden field errors in forms.
2.2.3 2024-07-09
-----------------

View File

@@ -185,6 +185,26 @@ modifications of Viewflow. You can find the commercial license terms in
## Changelog
### 2.2.4 2024-07-12
- Clone data, seed, and artifacts from canceled tasks to revived tasks.
- Enhance error handling for celery.Job.
- Improve the process cancellation template.
- Redirect to the task detail page after canceling or undoing actions, instead
of redirecting to the process detail page.
- Added links to parent subprocess and parent task on the subprocess process and
task details pages.
- Updated the Process.parent_task field to use related_name='subprocess',
allowing access to subprocesses via task.subprocess
- Enhanced CreateProcessView and UpdateProcessView to set process_seed and
artifact_generic_foreign_key fields based on form.cleaned_data, as Django
model forms do not handle this automatically.
- Added tasks with an ERROR status to the process dashboard for better visibility and tracking.
- Added tooltip hover titles to nodes without text labels in the SVG workflow graph.
- Marked StartHandler nodes as BPMN Start Message events on the SVG graph.
- Fixed rendering of hidden field errors in forms.
### 2.2.3 2024-07-09
- Fixed issue with Split/Join operations when an immediate split to join

11
package-lock.json generated
View File

@@ -7,6 +7,7 @@
"name": "viewflow",
"dependencies": {
"@hotwired/turbo": "^7.3.0",
"@iconfu/svg-inject": "^1.2.3",
"@material/textfield": "^14.0.0",
"@material/web": "^1.2.0",
"@open-wc/lit-helpers": "^0.6.0",
@@ -1135,6 +1136,11 @@
"integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
"dev": true
},
"node_modules/@iconfu/svg-inject": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@iconfu/svg-inject/-/svg-inject-1.2.3.tgz",
"integrity": "sha512-3v1MUAJqmJS4jmhHoCkSxt+EdJrjPHlLXrWocCT25kCxnxJto8028Z6CC406EL11KA53SDZgI/QQA5GEJAoiRw=="
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
@@ -8451,6 +8457,11 @@
"integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
"dev": true
},
"@iconfu/svg-inject": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@iconfu/svg-inject/-/svg-inject-1.2.3.tgz",
"integrity": "sha512-3v1MUAJqmJS4jmhHoCkSxt+EdJrjPHlLXrWocCT25kCxnxJto8028Z6CC406EL11KA53SDZgI/QQA5GEJAoiRw=="
},
"@jridgewell/gen-mapping": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",

View File

@@ -31,6 +31,7 @@
},
"dependencies": {
"@hotwired/turbo": "^7.3.0",
"@iconfu/svg-inject": "^1.2.3",
"@material/textfield": "^14.0.0",
"@material/web": "^1.2.0",
"@open-wc/lit-helpers": "^0.6.0",

View File

@@ -4,7 +4,7 @@ README = open("README.md", "r", encoding="utf-8").read()
setuptools.setup(
name="django-viewflow",
version="2.2.3",
version="2.2.4",
author_email="kmmbvnr@gmail.com",
author="Mikhail Podgurskiy",
description="Reusable library to build business applications fast",

View File

@@ -12,28 +12,55 @@ class Test(TestCase):
self.assertEqual(
content,
'<div class="vf-form mdc-layout-grid">'
'<div class="vf-form__hiddenfields"><input id="id_promocode" name="promocode"'
' label="Promocode" type="hidden"></div>'
'<div class="vf-form__visiblefields mdc-layout-grid__inner">'
'<div class="mdc-layout-grid__cell mdc-layout-grid__cell--span-12">'
'<vf-field-input required="required" id="id_username" name="username" label="Username"'
' type="text"></vf-field-input></div></div></div>',
)
def _test_render_form_error(self):
def test_render_form_field_error(self):
renderer = FormLayout()
content = renderer.render(TestForm(data={}))
self.assertEqual(
content,
'<div class="vf-form mdc-layout-grid"><div class="vf-form__errors">'
'<div class="vf-form__error">Form error</div></div>'
'<div class="vf-form__hiddenfields"><input id="id_promocode" name="promocode"'
' label="Promocode" type="hidden"></div>'
'<div class="vf-form__visiblefields mdc-layout-grid__inner">'
'<div class="mdc-layout-grid__cell mdc-layout-grid__cell--span-12">'
'<vf-field-input required="required" id="id_username" name="username" label="Username"'
' error="This field is required." type="text"></vf-field-input></div></div></div>',
'<vf-field-input required="required" aria-invalid="true" id="id_username" name="username"'
' label="Username" error="This field is required." type="text"></vf-field-input></div></div></div>',
)
def test_render_form_hidden_field_error(self):
renderer = FormLayout()
content = renderer.render(TestForm(data={"promocode": "invalid"}))
self.assertEqual(
content,
'<div class="vf-form mdc-layout-grid"><div class="vf-form__errors">'
'<div class="vf-form__error">Form error</div>'
'<div class="vf-form__error">Promocode must be empty</div>'
"</div>"
'<div class="vf-form__hiddenfields"><input id="id_promocode" name="promocode"'
' value="invalid" label="Promocode" error="Promocode must be empty" type="hidden"></div>'
'<div class="vf-form__visiblefields mdc-layout-grid__inner">'
'<div class="mdc-layout-grid__cell mdc-layout-grid__cell--span-12">'
'<vf-field-input required="required" aria-invalid="true" id="id_username" name="username"'
' label="Username" error="This field is required." type="text"></vf-field-input></div></div></div>',
)
class TestForm(forms.Form):
username = forms.CharField()
promocode = forms.CharField(widget=forms.HiddenInput, required=False)
def clean_promocode(self):
promo = self.cleaned_data.get("promocode", "")
if promo:
raise forms.ValidationError("Promocode must be empty")
def clean(self):
raise forms.ValidationError("Form error")

View File

@@ -510,9 +510,10 @@ class FormLayout:
def append_non_field_errors(self, form: forms.Form, root: ElementTree.Element):
errors = form.non_field_errors()
errors.extend(
form.error_class(bound_field.errors)
error
for bound_field in form.hidden_fields()
if bound_field.errors
for error in bound_field.errors
)
if errors:

View File

@@ -81,7 +81,7 @@
circle.event,
path.event {
fill: none;
fill: transparent;
stroke: #000;
}
@@ -90,12 +90,12 @@
}
path.event-marker {
fill: none;
fill: transparent;
stroke: #000;
}
path.gateway {
fill: none;
fill: transparent;
stroke: #000;
stroke-width: 1.1;
}
@@ -108,7 +108,7 @@
}
rect.task {
fill: none;
fill: transparent;
stroke: #000;
stroke-width: 1.1;
}
@@ -120,7 +120,7 @@
}
path.task-marker {
fill: none;
fill: transparent;
stroke: #000;
stroke-width: 1.1;
}
@@ -128,14 +128,14 @@
</style>
{% for cell in cells %}
<g id="{{ cell.node.name }}" transform="translate({{ cell.shape.x }}, {{ cell.shape.y }})"{% if cell.status %} class="{{ cell.status|lower }}"{% endif %}>
{% if cell.status %}<title>{{ cell.status }}</title>{% endif %}
{% if cell.title %}<title>{{ cell.title }}</title>{% endif %}
{{ cell.shape.svg|safe }}
{% for text, class, font_size, x, y in cell.shape.text %}
<text class="{{ class }}" font-size="{{ font_size }}px" x="{{ x }}" y="{{ y }}">{% if forloop.first %}{{ text|title }}{% else %}{{ text }}{% endif %}</text>
{% endfor %}
</g>
{% endfor %}{% for edge in edges %}
<path fill="none" stroke="#000000" stroke-width="2" id="{{ edge.src.name}}__{{ edge.dst.name }}" marker-end="url(#end-marker)"
<path fill="transparent" stroke="#000000" stroke-width="2" id="{{ edge.src.name}}__{{ edge.dst.name }}" marker-end="url(#end-marker)"
d="{% for x,y in edge.segments %}{% if forloop.first %}M{% else %} L{% endif %}{{ x }}, {{ y }}{% endfor %}"/>
{% endfor %}

View File

@@ -3,8 +3,8 @@
{% block content %}
<div class="mdc-layout-grid vf-page__grid">
<div class="mdc-layout-grid__inner">
<div class="mdc-layout-grid__cell mdc-layout-grid__cell--span-9-desktop mdc-layout-grid__cell--span-8-tablet mdc-layout-grid__cell--span-4-phone">
<div class="mdc-layout-grid__inner vf-page__grid-inner">
<div class="mdc-layout-grid__cell mdc-layout-grid__cell--span-8-desktop mdc-layout-grid__cell--span-8-tablet mdc-layout-grid__cell--span-4-phone">
<div class="mdc-card vf-card">
<section class="vf-card__header">
<h1 class="vf-card__title">
@@ -90,7 +90,7 @@
</div>
</div>
{% block panel-cell %}
<div class="mdc-layout-grid__cell {% block panel-cell-span %}mdc-layout-grid__cell--span-3-desktop mdc-layout-grid__cell--span-8-tablet mdc-layout-grid__cell--span-4-phone{% endblock %}">
<div class="mdc-layout-grid__cell {% block panel-cell-span %}mdc-layout-grid__cell--span-4-desktop mdc-layout-grid__cell--span-8-tablet mdc-layout-grid__cell--span-4-phone{% endblock %}">
{% include_process_data process %}
</div>
{% endblock panel-cell %}

View File

@@ -1,4 +1,4 @@
{% load viewflow %}
{% load viewflow i18n %}
<div class="mdc-card vf-card">
<section class="vf-card__header">
<h1 class="vf-card__title">{{ process }}</h1>
@@ -20,13 +20,33 @@
</td>
</tr>
{% for field, field_name, value in process_data %}
{% if field.name != 'data' and field.name != 'flow_class' and field.name != 'artifact_object_id' and field.name != 'artifact_content_type'%}
{% if field.name != 'parent_task' and field.name != 'data' and field.name != 'flow_class' and field.name != 'artifact_object_id' and field.name != 'artifact_content_type' and field.name != 'seed_object_id' and field.name != 'seed_content_type'%}
<tr>
<th class="vf-list__table-header vf-list__table-header-text">{{ field_name }}</th>
<td>{{ value }}</td>
</tr>
{% endif %}
{% endfor %}
{% if process.parent_task %}
<tr>
<th class="vf-list__table-header vf-list__table-header-text">{% translate 'Parent Task' %}</th>
<td>
<a href="{% reverse process.parent_task.flow_task 'index' process.parent_task.process_id process.parent_task.pk %}">{{ process.parent_task.flow_task }}</a>
</td>
</tr>
{% endif %}
{% if process.seed %}
<tr>
<th class="vf-list__table-header vf-list__table-header-text">{% translate 'Seed' %}</th>
<td>{{ process.seed }}</td>
</tr>
{% endif %}
{% if process.artifact %}
<tr>
<th class="vf-list__table-header vf-list__table-header-text">{% translate 'Artifact' %}</th>
<td>{{ process.artifact }}</td>
</tr>
{% endif %}
</tbody>
</table>
</section>

View File

@@ -20,6 +20,28 @@
</div>
{% endif %}
{% if seed_data %}
<div class="mdc-card vf-card">
<section class="vf-card__header">
<h1 class="vf-card__title">{{ task.seed }}</h1>
</section>
<section class="vf-card__body">
<table class="vf-list__table">
<tbody>
{% for field, field_name, value in seed_data %}
{% if field.name != 'process' and field.name != 'task' %}
<tr>
<th class="vf-list__table-header vf-list__table-header-text">{{ field_name }}</th>
<td>{{ value }}</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</section>
</div>
{% endif %}
{% if data %}
{{ data }}

View File

@@ -37,6 +37,16 @@
{% endfor %}
</div>
</div>
{% with subprocesses=task.subprocesses.all %}{% if subprocesses %}
<div class="mdc-layout-grid__cell mdc-layout-grid__cell--span-12">
<h4 class="mdc-typography mdc-typography--headline5" style="margin-bottom:10px">{% trans "Subprocess" %}</h4>
<div>
{% for subprocess in subprocesses %}
<a class="mdc-typography mdc-typography--subtitle1" href="{% reverse subprocess.flow_class 'process_detail' subprocess.pk %}">{{ subprocess }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}
</div>
</div>
{% endif %}{% endwith %}
{% endblock %}
{% block task_actions %}

View File

@@ -51,6 +51,8 @@ def include_task_data(context, task):
try:
if task.artifact_object_id:
context["artifact_data"] = get_object_data(task.artifact)
if task.seed_object_id:
context["seed_data"] = get_object_data(task.seed)
context["data"] = task.data
context["task"] = task
return template.render(

View File

@@ -47,7 +47,18 @@ class Shape(object):
class Cell(object):
__slots__ = ["col", "row", "x", "y", "width", "height", "node", "shape", "status"]
__slots__ = [
"col",
"row",
"x",
"y",
"width",
"height",
"node",
"shape",
"title",
"status",
]
def __init__(
self,
@@ -59,6 +70,7 @@ class Cell(object):
width=-1,
height=-1,
shape=None,
title=None,
status=None,
):
self.node = node
@@ -69,6 +81,7 @@ class Cell(object):
self.width = width
self.height = height
self.shape = shape if shape is not None else Shape()
self.title = title
self.status = status
def incoming(self):
@@ -536,11 +549,13 @@ def calc_text(grid):
shape = getattr(cell.node, "shape", DEFAULT_SHAPE)
text_align = shape.get("text-align")
font_size = shape.get("font-size", 12)
if cell.node.task_title:
title = force_str(cell.node.task_title)
else:
title = " ".join(cell.node.name.capitalize().split("_"))
if text_align == "middle":
if cell.node.task_title:
title = force_str(cell.node.task_title)
else:
title = " ".join(cell.node.name.capitalize().split("_"))
segments = wrap(title, 20)
block_height = max(len(segments) - 1, 0) * font_size * 1.2
@@ -557,6 +572,8 @@ def calc_text(grid):
y_start + (n * font_size * 1.2),
)
)
else:
cell.title = title
def calc_cell_status(flow_class, grid, process_pk):

View File

@@ -58,7 +58,6 @@ class UnassignTaskView(
class CancelTaskView(
mixins.SuccessMessageMixin,
mixins.TaskSuccessUrlMixin,
mixins.TaskViewTemplateNames,
generic.FormView,
):
@@ -77,10 +76,16 @@ class CancelTaskView(
self.request.activation.cancel()
return super().form_valid(*args, **kwargs)
def get_success_url(self):
"""Continue on task or redirect back to task list."""
activation = self.request.activation
return activation.flow_task.reverse(
"detail", args=[activation.process.pk, activation.task.pk]
)
class UndoTaskView(
mixins.SuccessMessageMixin,
mixins.TaskSuccessUrlMixin,
mixins.TaskViewTemplateNames,
generic.FormView,
):
@@ -99,10 +104,16 @@ class UndoTaskView(
self.request.activation.undo()
return super().form_valid(*args, **kwargs)
def get_success_url(self):
"""Continue on task or redirect back to task list."""
activation = self.request.activation
return activation.flow_task.reverse(
"detail", args=[activation.process.pk, activation.task.pk]
)
class ReviveTaskView(
mixins.SuccessMessageMixin,
mixins.TaskSuccessUrlMixin,
mixins.TaskViewTemplateNames,
generic.FormView,
):

View File

@@ -25,6 +25,10 @@ class CreateProcessView(
def form_valid(self, form):
"""If the form is valid, save the associated model and finish the task."""
self.object = form.save()
if "seed" in form.cleaned_data:
self.object.seed = form.cleaned_data["seed"]
if "artifact" in form.cleaned_data:
self.object.artifact = form.cleaned_data["artifact"]
self.request.activation.execute()
return HttpResponseRedirect(self.get_success_url())

View File

@@ -1,5 +1,6 @@
from django.core.exceptions import PermissionDenied
from django.contrib.auth.decorators import login_required
from django.db.models import Q
from django.utils.decorators import method_decorator
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
@@ -7,7 +8,7 @@ from django.views import generic
from viewflow.views import ListModelView
from viewflow.utils import viewprop, has_object_perm
from viewflow.workflow import chart
from viewflow.workflow import chart, STATUS
from viewflow.workflow.fields import get_task_ref
from . import mixins, filters
@@ -56,7 +57,8 @@ class DashboardView(
"tasks": self.flow_class.task_class._default_manager.filter_available(
[self.flow_class], self.request.user
).filter(
finished__isnull=True, flow_task=node
Q(finished__isnull=True) | Q(status=STATUS.ERROR),
flow_task=node,
)[
: self.MAX_ROWS
],

View File

@@ -24,6 +24,10 @@ class UpdateProcessView(
def form_valid(self, form):
"""If the form is valid, save the associated model and finish the task."""
self.object = form.save()
if "seed" in form.cleaned_data:
self.object.seed = form.cleaned_data["seed"]
if "artifact" in form.cleaned_data:
self.object.artifact = form.cleaned_data["artifact"]
self.request.activation.execute()
return HttpResponseRedirect(self.get_success_url())

View File

@@ -0,0 +1,25 @@
# Generated by Django 5.0.1 on 2024-07-11 05:14
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("viewflow", "0013_process_seed_content_type_process_seed_object_id_and_more"),
]
operations = [
migrations.AlterField(
model_name="process",
name="parent_task",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="subprocesses",
to="viewflow.task",
),
),
]

View File

@@ -236,7 +236,7 @@ class Process(AbstractProcess):
blank=True,
null=True,
on_delete=models.CASCADE,
related_name="+",
related_name="subprocesses",
to="Task",
)

View File

@@ -55,9 +55,13 @@ class AbstractJobActivation(mixins.NextNodeActivationMixin, Activation):
tb = exception.__traceback__
while tb.tb_next:
tb = tb.tb_next
serialized_locals = json.dumps(
tb.tb_frame.f_locals, default=lambda obj: str(obj)
)
try:
serialized_locals = json.dumps(
tb.tb_frame.f_locals, default=lambda obj: str(obj)
)
except Exception as ex:
serialized_locals = json.dumps({"_serialization_exception": str(ex)})
self.task.data["_exception"] = {
"title": str(exception),

View File

@@ -170,6 +170,8 @@ class StartHandle(mixins.NextNodeMixin, Node):
"height": 50,
"svg": """
<circle class="event" cx="25" cy="25" r="25"/>
<rect xmlns="http://www.w3.org/2000/svg" x="7.5" y="15" width="35" height="20" fill="transparent" stroke="rgb(0, 0, 0)"/>
<path xmlns="http://www.w3.org/2000/svg" d="M 7.5 15 L 25 25 L 42.5 15" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10"/>
""",
}