From 82305a09a9c869104d1e27f2b0f4f27dd0912ede Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Mon, 21 Feb 2022 11:03:26 +0100
Subject: [PATCH] backoffice: organize workflows by categories in select widget
 (#56002)

---
 tests/admin_pages/test_form.py | 63 +++++++++++++++++++++++++++++++++-
 wcs/admin/forms.py             | 41 +++++++++++++++++++---
 2 files changed, 99 insertions(+), 5 deletions(-)

diff --git a/tests/admin_pages/test_form.py b/tests/admin_pages/test_form.py
index 9f5e2d58f..fb9a383d9 100644
--- a/tests/admin_pages/test_form.py
+++ b/tests/admin_pages/test_form.py
@@ -11,7 +11,7 @@ from webtest import Upload
 
 from wcs import fields
 from wcs.carddef import CardDef
-from wcs.categories import Category
+from wcs.categories import Category, WorkflowCategory
 from wcs.data_sources import NamedDataSource
 from wcs.formdef import FormDef
 from wcs.qommon.errors import ConnectionError
@@ -553,6 +553,8 @@ def test_form_workflow_change(pub):
 
     resp = app.get('/backoffice/forms/1/')
     resp = resp.click(href='workflow', index=1)
+    # no categories, no optgroup
+    assert [x[2] for x in resp.form['workflow_id'].options] == ['Default', 'Workflow Two']
     assert 'Workflow One' not in resp.text  # this workflow doesn't have any status
     resp.forms[0]['workflow_id'] = workflow.id
     resp = resp.forms[0].submit('submit')
@@ -561,6 +563,65 @@ def test_form_workflow_change(pub):
     # run a SQL SELECT and we known all columns are defined.
     FormDef.get(formdef.id).data_class().select()
 
+    Category.wipe()
+    WorkflowCategory.wipe()
+    cat1 = WorkflowCategory(name='Foo')
+    cat1.store()
+    cat2 = WorkflowCategory(name='Bar')
+    cat2.store()
+
+    wf = Workflow(name='Workflow Foo zz')
+    wf.possible_status = Workflow.get_default_workflow().possible_status[:]
+    wf.category = cat1
+    wf.store()
+    wf = Workflow(name='Workflow Foo aa')
+    wf.possible_status = Workflow.get_default_workflow().possible_status[:]
+    wf.category = cat1
+    wf.store()
+    wf = Workflow(name='Workflow Bar bb')
+    wf.possible_status = Workflow.get_default_workflow().possible_status[:]
+    wf.category = cat2
+    wf.store()
+    wf = Workflow(name='Workflow Bar yy')
+    wf.possible_status = Workflow.get_default_workflow().possible_status[:]
+    wf.category = cat2
+    wf.store()
+    resp = app.get('/backoffice/forms/1/workflow')
+    assert [x[2] for x in resp.form['workflow_id'].options] == [
+        'Default',
+        'Workflow Bar bb',
+        'Workflow Bar yy',
+        'Workflow Foo aa',
+        'Workflow Foo zz',
+        'Workflow Two',
+    ]
+
+    cat = Category(name='Foo')
+    cat.store()
+    formdef.category = cat
+    formdef.store()
+    resp = app.get('/backoffice/forms/1/workflow')
+    assert [x[2] for x in resp.form['workflow_id'].options] == [
+        'Default',
+        'Workflow Foo aa',
+        'Workflow Foo zz',
+        'Workflow Bar bb',
+        'Workflow Bar yy',
+        'Workflow Two',
+    ]
+
+    workflow.category = cat1
+    workflow.store()
+    resp = app.get('/backoffice/forms/1/workflow')
+    assert [x[2] for x in resp.form['workflow_id'].options] == [
+        'Default',
+        'Workflow Foo aa',
+        'Workflow Foo zz',
+        'Workflow Two',
+        'Workflow Bar bb',
+        'Workflow Bar yy',
+    ]
+
 
 def test_form_workflow_link(pub):
     create_superuser(pub)
diff --git a/wcs/admin/forms.py b/wcs/admin/forms.py
index 58e422952..14c55e909 100644
--- a/wcs/admin/forms.py
+++ b/wcs/admin/forms.py
@@ -19,6 +19,7 @@ import io
 import tarfile
 import time
 import xml.etree.ElementTree as ET
+from collections import defaultdict
 
 from quixote import get_publisher, get_request, get_response, get_session, redirect
 from quixote.directory import AccessControlled, Directory
@@ -41,6 +42,7 @@ from wcs.qommon.form import (
     FileWidget,
     Form,
     HtmlWidget,
+    OptGroup,
     SingleSelectWidget,
     SlugWidget,
     StringWidget,
@@ -87,10 +89,39 @@ class FormDefUI:
         return get_categories(self.category_class, filter_function=filter_function)
 
     @classmethod
-    def get_workflows(cls, condition=lambda x: True):
+    def get_workflows(cls, condition=lambda x: True, formdef_category=None):
         default_workflow = cls.formdef_class.get_default_workflow()
-        t = sorted((misc.simplify(x.name), x.id, x.name, x.id) for x in Workflow.select() if condition(x))
-        return [(None, default_workflow.name, '')] + [x[1:] for x in t]
+        t = sorted(
+            (misc.simplify(x.name), x.category, x.id, x.name, x.id) for x in Workflow.select() if condition(x)
+        )
+        workflows_by_category_names = defaultdict(list)
+        for x in t:
+            name = ''
+            if x[1]:
+                name = x[1].name
+            workflows_by_category_names[name].append(x[2:])
+        category_names = list(workflows_by_category_names.keys())
+        if len(category_names) == 1 and category_names[0] == '':
+            # no category found
+            return [(None, default_workflow.name, '')] + [x[2:] for x in t]
+        options = []
+        options.append((None, default_workflow.name, ''))
+        # first options, workflows with the same category name as updated formdef
+        if formdef_category and formdef_category.name in workflows_by_category_names:
+            options.append(OptGroup(formdef_category.name))
+            options.extend(workflows_by_category_names[formdef_category.name])
+        # then other categories
+        for name in sorted(category_names):
+            if not name:
+                continue
+            if formdef_category and formdef_category.name == name:
+                continue
+            options.append(OptGroup(name))
+            options.extend(workflows_by_category_names[name])
+        # and workflows without category
+        options.append(OptGroup(_('Without category')))
+        options.extend(workflows_by_category_names[''])
+        return options
 
     def new_form_ui(self):
         form = Form(enctype='multipart/form-data')
@@ -1058,7 +1089,9 @@ class FormDefPage(Directory):
 
     def workflow(self):
         form = Form(enctype='multipart/form-data')
-        workflows = self.formdef_ui_class.get_workflows(condition=lambda x: x.possible_status)
+        workflows = self.formdef_ui_class.get_workflows(
+            condition=lambda x: x.possible_status, formdef_category=self.formdef.category
+        )
         form.add(SingleSelectWidget, 'workflow_id', value=self.formdef.workflow_id, options=workflows)
         if not self.formdef.is_readonly():
             form.add_submit('submit', _('Submit'))
-- 
2.34.1

