general: add categories for workflows (#55945)
This commit is contained in:
parent
bc90b08f5c
commit
16604d04a9
|
@ -9,6 +9,7 @@ from webtest import Upload
|
|||
|
||||
from wcs import fields
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.categories import WorkflowCategory
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.mail_templates import MailTemplate
|
||||
from wcs.qommon.errors import ConnectionError
|
||||
|
@ -176,6 +177,45 @@ def test_workflows_edit(pub):
|
|||
assert 'baz' in resp.text
|
||||
|
||||
|
||||
def test_workflows_category(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
WorkflowCategory.wipe()
|
||||
Workflow.wipe()
|
||||
|
||||
workflow = Workflow(name='foo')
|
||||
workflow.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/workflows/categories/')
|
||||
resp = resp.click('New Category')
|
||||
resp.forms[0]['name'] = 'a new category'
|
||||
resp.forms[0]['description'] = 'description of the category'
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert WorkflowCategory.get(1).name == 'a new category'
|
||||
|
||||
resp = app.get('/backoffice/workflows/1/')
|
||||
resp = resp.click(href='category')
|
||||
resp.forms[0]['category_id'] = '1'
|
||||
resp = resp.forms[0].submit('cancel').follow()
|
||||
workflow.refresh_from_storage()
|
||||
assert workflow.category_id is None
|
||||
|
||||
resp = app.get('/backoffice/workflows/1/')
|
||||
resp = resp.click(href='category')
|
||||
resp.forms[0]['category_id'] = '1'
|
||||
resp = resp.forms[0].submit('submit').follow()
|
||||
workflow.refresh_from_storage()
|
||||
assert str(workflow.category_id) == '1'
|
||||
|
||||
resp = app.get('/backoffice/workflows/categories/')
|
||||
resp = resp.click('a new category')
|
||||
resp = resp.click('Delete')
|
||||
resp = resp.forms[0].submit()
|
||||
workflow.refresh_from_storage()
|
||||
assert not workflow.category_id
|
||||
|
||||
|
||||
def test_workflows_edit_status(pub):
|
||||
create_superuser(pub)
|
||||
Workflow.wipe()
|
||||
|
@ -2592,26 +2632,54 @@ def test_workflows_unused(pub):
|
|||
Workflow.wipe()
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/workflows/')
|
||||
assert 'Unused workflows' not in resp.text
|
||||
assert 'Unused' not in resp.text
|
||||
|
||||
workflow = Workflow(name='Workflow One')
|
||||
workflow.store()
|
||||
resp = app.get('/backoffice/workflows/')
|
||||
assert 'Unused workflows' in resp.text
|
||||
assert 'Unused' in resp.text
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = []
|
||||
formdef.store()
|
||||
resp = app.get('/backoffice/workflows/')
|
||||
assert 'Unused workflows' in resp.text
|
||||
assert 'Unused' in resp.text
|
||||
|
||||
formdef.workflow = workflow
|
||||
formdef.store()
|
||||
resp = app.get('/backoffice/workflows/')
|
||||
assert 'Unused workflows' not in resp.text
|
||||
assert 'Unused' not in resp.text
|
||||
|
||||
workflow = Workflow(name='Workflow Two')
|
||||
workflow.store()
|
||||
resp = app.get('/backoffice/workflows/')
|
||||
assert 'Unused workflows' in resp.text
|
||||
assert 'Unused' in resp.text
|
||||
|
||||
|
||||
def test_workflows_categories_in_index(pub):
|
||||
create_superuser(pub)
|
||||
FormDef.wipe()
|
||||
Workflow.wipe()
|
||||
WorkflowCategory.wipe()
|
||||
|
||||
wf1 = Workflow(name='wf1')
|
||||
wf1.store()
|
||||
wf2 = Workflow(name='wf2')
|
||||
wf2.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/workflows/')
|
||||
assert 'Uncategorised' not in resp.text
|
||||
|
||||
cat = WorkflowCategory(name='XcategoryY')
|
||||
cat.store()
|
||||
resp = app.get('/backoffice/workflows/')
|
||||
assert 'Uncategorised' in resp.text
|
||||
assert 'XcategoryY' not in resp.text
|
||||
|
||||
wf2.category_id = cat.id
|
||||
wf2.store()
|
||||
resp = app.get('/backoffice/workflows/')
|
||||
assert 'Uncategorised' in resp.text
|
||||
assert 'XcategoryY' in resp.text
|
||||
|
|
|
@ -5,6 +5,7 @@ import pytest
|
|||
from quixote.http_request import Upload
|
||||
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.categories import WorkflowCategory
|
||||
from wcs.fields import FileField, StringField
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.mail_templates import MailTemplate
|
||||
|
@ -978,3 +979,14 @@ def test_unknown_data_source(pub):
|
|||
export = ET.tostring(export_to_indented_xml(wf))
|
||||
with pytest.raises(WorkflowImportError, match='Unknown datasources'):
|
||||
Workflow.import_from_xml(io.BytesIO(export))
|
||||
|
||||
|
||||
def test_worklow_with_category(pub):
|
||||
category = WorkflowCategory(name='test category')
|
||||
category.store()
|
||||
|
||||
wf = Workflow(name='test category')
|
||||
wf.category_id = category.id
|
||||
wf.store()
|
||||
wf2 = assert_import_export_works(wf, include_id=True)
|
||||
assert wf2.category_id == wf.category_id
|
||||
|
|
|
@ -19,11 +19,17 @@ from quixote.directory import Directory
|
|||
from quixote.html import TemplateIO, htmltext
|
||||
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.categories import CardDefCategory, Category
|
||||
from wcs.categories import CardDefCategory, Category, WorkflowCategory
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.qommon import _, template
|
||||
from wcs.qommon import _, misc, template
|
||||
from wcs.qommon.backoffice.menu import html_top
|
||||
from wcs.qommon.form import Form, HtmlWidget, SingleSelectWidget, StringWidget, WidgetList, WysiwygTextWidget
|
||||
from wcs.workflows import Workflow
|
||||
|
||||
|
||||
def get_categories(category_class):
|
||||
t = sorted((misc.simplify(x.name), x.id, x.name, x.id) for x in category_class.select())
|
||||
return [x[1:] for x in t]
|
||||
|
||||
|
||||
class CategoryUI:
|
||||
|
@ -60,21 +66,22 @@ class CategoryUI:
|
|||
if not new:
|
||||
# include permission fields
|
||||
roles = list(get_publisher().role_class.select(order_by='name'))
|
||||
form.add(
|
||||
WidgetList,
|
||||
'export_roles',
|
||||
title=_('Export Roles'),
|
||||
element_type=SingleSelectWidget,
|
||||
value=self.category.export_roles,
|
||||
add_element_label=_('Add Role'),
|
||||
element_kwargs={
|
||||
'render_br': False,
|
||||
'options': [(None, '---', None)]
|
||||
+ [(x, x.name, x.id) for x in roles if not x.is_internal()],
|
||||
},
|
||||
hint=_('Roles allowed to export data'),
|
||||
)
|
||||
if self.category_class is Category:
|
||||
if 'export_roles' in [x[0] for x in self.category_class.XML_NODES]:
|
||||
form.add(
|
||||
WidgetList,
|
||||
'export_roles',
|
||||
title=_('Export Roles'),
|
||||
element_type=SingleSelectWidget,
|
||||
value=self.category.export_roles,
|
||||
add_element_label=_('Add Role'),
|
||||
element_kwargs={
|
||||
'render_br': False,
|
||||
'options': [(None, '---', None)]
|
||||
+ [(x, x.name, x.id) for x in roles if not x.is_internal()],
|
||||
},
|
||||
hint=_('Roles allowed to export data'),
|
||||
)
|
||||
if 'statistics_roles' in [x[0] for x in self.category_class.XML_NODES]:
|
||||
form.add(
|
||||
WidgetList,
|
||||
'statistics_roles',
|
||||
|
@ -115,10 +122,14 @@ class CardDefCategoryUI(CategoryUI):
|
|||
category_class = CardDefCategory
|
||||
|
||||
|
||||
class WorkflowCategoryUI(CategoryUI):
|
||||
category_class = WorkflowCategory
|
||||
|
||||
|
||||
class CategoryPage(Directory):
|
||||
category_class = Category
|
||||
category_ui_class = CategoryUI
|
||||
formdef_class = FormDef
|
||||
object_class = FormDef
|
||||
usage_title = _('Forms in this category')
|
||||
empty_message = _('No form associated to this category.')
|
||||
_q_exports = ['', 'edit', 'delete', 'description']
|
||||
|
@ -138,7 +149,7 @@ class CategoryPage(Directory):
|
|||
)
|
||||
|
||||
def get_formdefs(self):
|
||||
formdefs = self.formdef_class.select(order_by='name')
|
||||
formdefs = self.object_class.select(order_by='name')
|
||||
return [x for x in formdefs if x.category_id == self.category.id]
|
||||
|
||||
def edit(self):
|
||||
|
@ -208,11 +219,19 @@ class CategoryPage(Directory):
|
|||
class CardDefCategoryPage(CategoryPage):
|
||||
category_class = CardDefCategory
|
||||
category_ui_class = CardDefCategoryUI
|
||||
formdef_class = CardDef
|
||||
object_class = CardDef
|
||||
usage_title = _('Card models in this category')
|
||||
empty_message = _('No card model associated to this category.')
|
||||
|
||||
|
||||
class WorkflowCategoryPage(CategoryPage):
|
||||
category_class = WorkflowCategory
|
||||
category_ui_class = WorkflowCategoryUI
|
||||
object_class = Workflow
|
||||
usage_title = _('Workflows in this category')
|
||||
empty_message = _('No workflow associated to this category.')
|
||||
|
||||
|
||||
class CategoriesDirectory(Directory):
|
||||
_q_exports = ['', 'new', 'update_order']
|
||||
category_class = Category
|
||||
|
@ -291,3 +310,10 @@ class CardDefCategoriesDirectory(CategoriesDirectory):
|
|||
category_ui_class = CardDefCategoryUI
|
||||
category_page_class = CardDefCategoryPage
|
||||
category_explanation = _('Categories are used to sort the different card models.')
|
||||
|
||||
|
||||
class WorkflowCategoriesDirectory(CategoriesDirectory):
|
||||
category_class = WorkflowCategory
|
||||
category_ui_class = WorkflowCategoryUI
|
||||
category_page_class = WorkflowCategoryPage
|
||||
category_explanation = _('Categories are used to sort the different workflows.')
|
||||
|
|
|
@ -56,17 +56,12 @@ from wcs.workflows import Workflow
|
|||
|
||||
from . import utils
|
||||
from .blocks import BlocksDirectory
|
||||
from .categories import CategoriesDirectory
|
||||
from .categories import CategoriesDirectory, get_categories
|
||||
from .data_sources import NamedDataSourcesDirectory
|
||||
from .fields import FieldDefPage, FieldsDirectory
|
||||
from .logged_errors import LoggedErrorsDirectory
|
||||
|
||||
|
||||
def get_categories(category_class):
|
||||
t = sorted((misc.simplify(x.name), x.id, x.name, x.id) for x in category_class.select())
|
||||
return [x[1:] for x in t]
|
||||
|
||||
|
||||
class FormDefUI:
|
||||
formdef_class = FormDef
|
||||
category_class = Category
|
||||
|
|
|
@ -25,9 +25,11 @@ from quixote import get_publisher, get_request, get_response, get_session, redir
|
|||
from quixote.directory import Directory
|
||||
from quixote.html import TemplateIO, htmltext
|
||||
|
||||
from wcs.admin.categories import WorkflowCategoriesDirectory, get_categories
|
||||
from wcs.backoffice.snapshots import SnapshotsDirectory
|
||||
from wcs.backoffice.studio import StudioDirectory
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.categories import WorkflowCategory
|
||||
from wcs.formdata import Evolution
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.qommon import _, errors, force_str, get_logger, misc, template
|
||||
|
@ -296,6 +298,14 @@ class WorkflowUI:
|
|||
def form_new(self):
|
||||
form = Form(enctype='multipart/form-data')
|
||||
form.add(StringWidget, 'name', title=_('Workflow Name'), required=True, size=30)
|
||||
category_options = get_categories(WorkflowCategory)
|
||||
if category_options:
|
||||
form.add(
|
||||
SingleSelectWidget,
|
||||
'category_id',
|
||||
title=_('Category'),
|
||||
options=[(None, '---', '')] + category_options,
|
||||
)
|
||||
form.add_submit('submit', _('Submit'))
|
||||
form.add_submit('cancel', _('Cancel'))
|
||||
return form
|
||||
|
@ -1430,6 +1440,7 @@ class WorkflowPage(Directory):
|
|||
_q_exports = [
|
||||
'',
|
||||
'edit',
|
||||
'category',
|
||||
'delete',
|
||||
'newstatus',
|
||||
('status', 'status_dir'),
|
||||
|
@ -1475,6 +1486,40 @@ class WorkflowPage(Directory):
|
|||
def html_top(self, title):
|
||||
return html_top('workflows', title)
|
||||
|
||||
def category(self):
|
||||
categories = sorted((misc.simplify(x.name), x.id, x.name, x.id) for x in WorkflowCategory.select())
|
||||
|
||||
form = Form(enctype='multipart/form-data')
|
||||
form.widgets.append(HtmlWidget('<p>%s</p>' % _('Select a category for this workflow.')))
|
||||
form.add(
|
||||
SingleSelectWidget,
|
||||
'category_id',
|
||||
title=_('Category'),
|
||||
value=self.workflow.category_id,
|
||||
options=[(None, '---', '')] + [x[1:] for x in categories],
|
||||
)
|
||||
|
||||
if not self.workflow.is_readonly():
|
||||
form.add_submit('submit', _('Submit'))
|
||||
form.add_submit('cancel', _('Cancel'))
|
||||
if form.get_widget('cancel').parse():
|
||||
return redirect('..')
|
||||
|
||||
if form.is_submitted() and not form.has_errors():
|
||||
widget = form.get_widget('category_id')
|
||||
old_value = self.workflow.category_id
|
||||
new_value = widget.parse()
|
||||
if new_value != old_value:
|
||||
self.workflow.category_id = new_value
|
||||
self.workflow.store(comment=_('Changed category'))
|
||||
return redirect('.')
|
||||
|
||||
html_top('workflows', title=self.workflow.name)
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<h2>%s</h2>') % _('Category')
|
||||
r += form.render()
|
||||
return r.getvalue()
|
||||
|
||||
def last_modification_block(self):
|
||||
return utils.last_modification_block(obj=self.workflow)
|
||||
|
||||
|
@ -1779,6 +1824,7 @@ class WorkflowsDirectory(Directory):
|
|||
_q_exports = [
|
||||
'',
|
||||
'new',
|
||||
'categories',
|
||||
('import', 'p_import'),
|
||||
('data-sources', 'data_sources'),
|
||||
('mail-templates', 'mail_templates'),
|
||||
|
@ -1786,6 +1832,8 @@ class WorkflowsDirectory(Directory):
|
|||
|
||||
data_sources = NamedDataSourcesDirectoryInWorkflows()
|
||||
mail_templates = MailTemplatesDirectory()
|
||||
category_class = WorkflowCategory
|
||||
categories = WorkflowCategoriesDirectory()
|
||||
|
||||
def html_top(self, title):
|
||||
return html_top('workflows', title)
|
||||
|
@ -1804,6 +1852,8 @@ class WorkflowsDirectory(Directory):
|
|||
if get_publisher().has_site_option('mail-templates'):
|
||||
r += htmltext('<a href="mail-templates/">%s</a>') % _('Mail Templates')
|
||||
r += htmltext('<a href="data-sources/">%s</a>') % _('Data sources')
|
||||
if get_publisher().get_backoffice_root().is_accessible('categories'):
|
||||
r += htmltext('<a href="categories/">%s</a>') % _('Categories')
|
||||
r += htmltext('<a href="import" rel="popup">%s</a>') % _('Import')
|
||||
r += htmltext('<a class="new-item" rel="popup" href="new">%s</a>') % _('New Workflow')
|
||||
r += htmltext('</span>')
|
||||
|
@ -1839,8 +1889,18 @@ class WorkflowsDirectory(Directory):
|
|||
else:
|
||||
unused_workflows.append(workflow)
|
||||
|
||||
categories = sorted(WorkflowCategory.select(), key=lambda x: misc.simplify(x.name))
|
||||
|
||||
if categories:
|
||||
default_category = WorkflowCategory('Default')
|
||||
default_category.id = '_default_category'
|
||||
for workflow in workflows:
|
||||
if workflow.id in ('_default', '_carddef_default'):
|
||||
workflow.category_id = default_category.id
|
||||
categories = [default_category] + categories
|
||||
|
||||
def workflow_section(r, workflows):
|
||||
r += htmltext('<div class="bo-block"><ul class="biglist">')
|
||||
r += htmltext('<ul class="objects-list single-links">')
|
||||
for workflow in workflows:
|
||||
if workflow in shared_workflows:
|
||||
css_class = 'shared-workflow'
|
||||
|
@ -1853,21 +1913,36 @@ class WorkflowsDirectory(Directory):
|
|||
usage_label = _('Card models')
|
||||
else:
|
||||
css_class = 'unused-workflow'
|
||||
usage_label = None
|
||||
usage_label = _('Unused')
|
||||
r += htmltext('<li class="%s">' % css_class)
|
||||
r += htmltext('<strong class="label"><a href="%s/">%s</a></strong>') % (
|
||||
r += htmltext('<a href="%s/">%s</a>') % (
|
||||
workflow.id,
|
||||
workflow.name,
|
||||
)
|
||||
if usage_label and carddef_workflows:
|
||||
r += htmltext('<p class="details badge">%s</p>') % usage_label
|
||||
r += htmltext('<span class="badge">%s</span>') % usage_label
|
||||
r += htmltext('</li>')
|
||||
r += htmltext('</ul></div>')
|
||||
r += htmltext('</ul>')
|
||||
|
||||
workflow_section(r, workflows)
|
||||
if unused_workflows:
|
||||
r += htmltext('<h2>%s</h2>') % _('Unused workflows')
|
||||
workflow_section(r, unused_workflows)
|
||||
for category in categories + [None]:
|
||||
if category is None:
|
||||
category_workflows = [x for x in workflows + unused_workflows if not x.category_id]
|
||||
else:
|
||||
category_workflows = [
|
||||
x for x in workflows + unused_workflows if x.category_id == str(category.id)
|
||||
]
|
||||
if category_workflows:
|
||||
if len(categories) > 1:
|
||||
r += htmltext('<div class="section">')
|
||||
if category is None:
|
||||
r += htmltext('<h2>%s</h2>') % _('Uncategorised')
|
||||
elif category.id == '_default_category':
|
||||
pass # no title
|
||||
else:
|
||||
r += htmltext('<h2>%s</h2>') % category.name
|
||||
workflow_section(r, category_workflows)
|
||||
if len(categories) > 1:
|
||||
r += htmltext('</div>')
|
||||
|
||||
return r.getvalue()
|
||||
|
||||
|
|
|
@ -158,6 +158,24 @@ class CardDefCategory(Category):
|
|||
return CardDef
|
||||
|
||||
|
||||
class WorkflowCategory(Category):
|
||||
_names = 'workflow_categories'
|
||||
xml_root_node = 'workflow_category'
|
||||
|
||||
# declarations for serialization
|
||||
XML_NODES = [
|
||||
('name', 'str'),
|
||||
('url_name', 'str'),
|
||||
('description', 'str'),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_object_class(cls):
|
||||
from .workflows import Workflow
|
||||
|
||||
return Workflow
|
||||
|
||||
|
||||
Substitutions.register('category_name', category=_('General'), comment=_('Category Name'))
|
||||
Substitutions.register('category_description', category=_('General'), comment=_('Category Description'))
|
||||
Substitutions.register('category_id', category=_('General'), comment=_('Category Identifier'))
|
||||
|
|
|
@ -107,7 +107,8 @@ td.time {
|
|||
}
|
||||
|
||||
ul.biglist li.disabled, ul.biglist li.disabled .details,
|
||||
li.biglistitem.disabled, li.biglistitem.disabled .details {
|
||||
li.biglistitem.disabled, li.biglistitem.disabled .details,
|
||||
ul.objects-list li.unused-workflow {
|
||||
color: #999;
|
||||
background: #eee;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
{% block appbar-actions %}
|
||||
{% if not workflow.is_readonly %}
|
||||
<a rel="popup" href="category">{% trans "change category" %}</a>
|
||||
<a rel="popup" href="edit">{% trans "change title" %}</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -30,6 +30,7 @@ from quixote import get_publisher, get_request, get_response
|
|||
from quixote.html import TemplateIO, htmltext
|
||||
|
||||
from .carddef import CardDef
|
||||
from .categories import WorkflowCategory
|
||||
from .conditions import Condition
|
||||
from .fields import FileField
|
||||
from .formdata import Evolution
|
||||
|
@ -453,6 +454,7 @@ class Workflow(StorableObject):
|
|||
backoffice_fields_formdef = None
|
||||
global_actions = None
|
||||
criticality_levels = None
|
||||
category_id = None
|
||||
|
||||
def __init__(self, name=None):
|
||||
StorableObject.__init__(self)
|
||||
|
@ -482,6 +484,17 @@ class Workflow(StorableObject):
|
|||
if changed:
|
||||
self.store()
|
||||
|
||||
@property
|
||||
def category(self):
|
||||
return WorkflowCategory.get(self.category_id, ignore_errors=True)
|
||||
|
||||
@category.setter
|
||||
def category(self, category):
|
||||
if category:
|
||||
self.category_id = category.id
|
||||
elif self.category_id:
|
||||
self.category_id = None
|
||||
|
||||
def store(self, comment=None, *args, **kwargs):
|
||||
assert not self.is_readonly()
|
||||
must_update = False
|
||||
|
@ -683,6 +696,12 @@ class Workflow(StorableObject):
|
|||
root.attrib['id'] = str(self.id)
|
||||
ET.SubElement(root, 'name').text = force_text(self.name, charset)
|
||||
|
||||
if self.category:
|
||||
elem = ET.SubElement(root, 'category')
|
||||
elem.text = force_text(self.category.name, charset)
|
||||
if include_id:
|
||||
elem.attrib['category_id'] = str(self.category.id)
|
||||
|
||||
roles_node = ET.SubElement(root, 'roles')
|
||||
if self.roles:
|
||||
for role_id, role_label in sorted(self.roles.items()):
|
||||
|
@ -750,6 +769,19 @@ class Workflow(StorableObject):
|
|||
|
||||
workflow.name = xml_node_text(tree.find('name'))
|
||||
|
||||
if tree.find('category') is not None:
|
||||
category_node = tree.find('category')
|
||||
if include_id and category_node.attrib.get('category_id'):
|
||||
category_id = str(category_node.attrib.get('category_id'))
|
||||
if WorkflowCategory.has_key(category_id):
|
||||
workflow.category_id = category_id
|
||||
else:
|
||||
category = xml_node_text(category_node)
|
||||
for c in WorkflowCategory.select():
|
||||
if c.name == category:
|
||||
workflow.category_id = c.id
|
||||
break
|
||||
|
||||
if tree.find('roles') is not None:
|
||||
workflow.roles = {}
|
||||
for role_node in tree.findall('roles/role'):
|
||||
|
|
Loading…
Reference in New Issue