Skip to content

Commit 3d464a7

Browse files
committed
Add override feature to proccess admin
1 parent 7278903 commit 3d464a7

File tree

5 files changed

+151
-37
lines changed

5 files changed

+151
-37
lines changed

joeflow/admin.py

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from django.contrib import admin, messages
22
from django.contrib.auth import get_permission_codename
3+
from django.db import transaction
34
from django.utils.html import format_html
45
from django.utils.translation import ugettext_lazy as t
56

6-
from . import models
7+
from . import forms, models
78
from .contrib.reversion import VersionAdmin
89

910
__all__ = ("ProcessAdmin",)
@@ -118,13 +119,43 @@ def child_tasks(self, obj):
118119
)
119120

120121

121-
class ProcessAdmin(VersionAdmin):
122-
readonly_fields = (
123-
"modified",
122+
class TaskInlineAdmin(admin.TabularInline):
123+
model = models.Task
124+
readonly_fields = [
125+
"name",
126+
"type",
127+
"assignees",
124128
"created",
125-
)
129+
"completed",
130+
"completed_by_user",
131+
"status",
132+
]
133+
fields = readonly_fields
134+
extra = 0
135+
can_delete = False
136+
show_change_link = True
137+
classes = ["collapse"]
126138

139+
140+
class ProcessAdmin(VersionAdmin):
127141
list_filter = (
128142
"modified",
129143
"created",
130144
)
145+
form = forms.OverrideForm
146+
147+
def get_inlines(self, *args, **kwargs):
148+
return [*super().get_inlines(*args, **kwargs), TaskInlineAdmin]
149+
150+
def get_readonly_fields(self, *args, **kwargs):
151+
return [
152+
"get_instance_graph_svg",
153+
*super().get_readonly_fields(*args, **kwargs),
154+
"modified",
155+
"created",
156+
]
157+
158+
@transaction.atomic()
159+
def save_model(self, request, obj, form, change):
160+
super().save_model(request, obj, form, change)
161+
form.start_next_tasks(request.user)

joeflow/forms.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from django import forms
2+
from django.utils.translation import gettext_lazy as t
3+
4+
from . import models
5+
6+
7+
class OverrideForm(forms.ModelForm):
8+
next_tasks = forms.MultipleChoiceField(
9+
label=t("Next tasks"), choices=[], required=False,
10+
)
11+
12+
def __init__(self, *args, **kwargs):
13+
super().__init__(*args, **kwargs)
14+
self.fields["next_tasks"].choices = [
15+
(name, name) for name in dict(self._meta.model.get_nodes()).keys()
16+
]
17+
18+
def get_next_task_nodes(self):
19+
names = self.cleaned_data["next_tasks"]
20+
for name in names:
21+
yield self._meta.model.get_node(name)
22+
23+
def start_next_tasks(self, user=None):
24+
active_tasks = list(self.instance.task_set.filter(completed=None))
25+
for task in active_tasks:
26+
task.cancel(user)
27+
if active_tasks:
28+
parent_tasks = active_tasks
29+
else:
30+
try:
31+
parent_tasks = [self.instance.task_set.latest()]
32+
except models.Task.DoesNotExist:
33+
parent_tasks = []
34+
override_task = self.instance.task_set.create(
35+
name="override", type=models.Task.HUMAN,
36+
)
37+
override_task.parent_task_set.set(parent_tasks)
38+
override_task.finish(user=user)
39+
override_task.start_next_tasks(next_nodes=self.get_next_task_nodes())

joeflow/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
def get_processes() -> types.GeneratorType:
66
"""Return all registered processes."""
77
from django.apps import apps
8+
89
from .models import Process
910

1011
apps.check_models_ready()

joeflow/views.py

Lines changed: 12 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
from django import forms
21
from django.contrib.auth.mixins import PermissionRequiredMixin
32
from django.db import transaction
3+
from django.forms import modelform_factory
44
from django.shortcuts import get_object_or_404
5-
from django.utils.translation import gettext_lazy as t
65
from django.views import generic
76

8-
from . import models
7+
from . import forms, models
98
from .contrib.reversion import RevisionMixin
109

1110

@@ -38,7 +37,7 @@ def get_task(self):
3837
models.Task, pk=self.kwargs["pk"], name=self.name, completed=None,
3938
)
4039
except KeyError:
41-
return models.Task(name=self.name,)
40+
return models.Task(name=self.name, type=models.Task.HUMAN)
4241

4342
def get_object(self, queryset=None):
4443
task = self.get_task()
@@ -47,11 +46,15 @@ def get_object(self, queryset=None):
4746
@transaction.atomic
4847
def post(self, request, *args, **kwargs):
4948
response = super().post(request, *args, **kwargs)
49+
self.create_task(request)
50+
return response
51+
52+
def create_task(self, request):
5053
task = self.get_task()
51-
task.process = self.object
54+
task.process = self.model._base_manager.get(pk=self.object.pk)
5255
task.finish(request.user)
5356
task.start_next_tasks()
54-
return response
57+
return task
5558

5659

5760
class ProcessDetailView(ProcessTemplateNameViewMixin, generic.DetailView):
@@ -66,37 +69,14 @@ class OverrideView(
6669
):
6770
permission_required = "override"
6871
name = "override"
72+
form_class = forms.OverrideForm
6973
fields = "__all__"
7074

71-
@staticmethod
72-
def get_task_choices(process):
73-
for name in dict(process.get_nodes()).keys():
74-
yield name, name
75-
7675
def get_form_class(self):
77-
form_class = super().get_form_class()
78-
79-
class OverrideForm(form_class):
80-
next_tasks = forms.MultipleChoiceField(
81-
label=t("Next tasks"), choices=self.get_task_choices(self.object),
82-
)
83-
84-
return OverrideForm
85-
86-
def get_next_task_nodes(self, form):
87-
names = form.cleaned_data["next_tasks"]
88-
for name in names:
89-
yield self.object.get_node(name)
76+
return modelform_factory(self.model, form=self.form_class, fields=self.fields)
9077

9178
@transaction.atomic()
9279
def form_valid(self, form):
93-
next_nodes = self.get_next_task_nodes(form)
9480
response = super().form_valid(form)
95-
active_tasks = list(self.object.task_set.filter(completed=None))
96-
for task in active_tasks:
97-
task.finish()
98-
override_task = self.object.task_set.create(name="override",)
99-
override_task.parent_task_set.set(active_tasks)
100-
override_task.finish()
101-
override_task.start_next_tasks(next_nodes=next_nodes)
81+
form.start_next_tasks(self.request.user)
10282
return response

tests/test_forms.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from joeflow import forms
2+
from tests.testapp.models import SimpleProcess
3+
4+
5+
class TestOverrideForm:
6+
def test_get_next_task_nodes(self):
7+
class SimpleProcessForm(forms.OverrideForm):
8+
class Meta:
9+
model = SimpleProcess
10+
fields = "__all__"
11+
12+
form = SimpleProcessForm({"next_tasks": ["end"]})
13+
assert form.is_valid()
14+
assert list(form.get_next_task_nodes()) == [SimpleProcess.end]
15+
16+
def test_start_next_tasks(self, db, admin_user):
17+
process = SimpleProcess.start_method()
18+
assert process.task_set.scheduled().exists()
19+
20+
class SimpleProcessForm(forms.OverrideForm):
21+
class Meta:
22+
model = SimpleProcess
23+
fields = "__all__"
24+
25+
form = SimpleProcessForm({"next_tasks": ["end"]}, instance=process)
26+
assert form.is_valid()
27+
form.start_next_tasks()
28+
29+
assert process.task_set.scheduled()[0].name == "end"
30+
31+
def test_start_next_tasks__user(self, db, admin_user):
32+
process = SimpleProcess.start_method()
33+
assert process.task_set.scheduled().exists()
34+
35+
class SimpleProcessForm(forms.OverrideForm):
36+
class Meta:
37+
model = SimpleProcess
38+
fields = "__all__"
39+
40+
form = SimpleProcessForm({"next_tasks": ["end"]}, instance=process)
41+
assert form.is_valid()
42+
form.start_next_tasks(admin_user)
43+
44+
assert process.task_set.canceled()[0].completed_by_user == admin_user
45+
assert (
46+
process.task_set.filter(name="override")[0].completed_by_user == admin_user
47+
)
48+
assert process.task_set.filter(name="override")[0].status == "succeeded"
49+
50+
def test_start_next_tasks__no_next_task(self, db, admin_user):
51+
process = SimpleProcess.start_method()
52+
assert process.task_set.scheduled().exists()
53+
54+
class SimpleProcessForm(forms.OverrideForm):
55+
class Meta:
56+
model = SimpleProcess
57+
fields = "__all__"
58+
59+
form = SimpleProcessForm({"next_tasks": []}, instance=process)
60+
assert form.is_valid()
61+
form.start_next_tasks()
62+
63+
assert not process.task_set.scheduled().exists()

0 commit comments

Comments
 (0)