general: add categories for blocks (#59256)

This commit is contained in:
Frédéric Péters 2021-12-13 15:14:34 +01:00
parent a578bb646d
commit cd70082556
7 changed files with 252 additions and 7 deletions

View File

@ -5,6 +5,7 @@ from webtest import Upload
from wcs import fields
from wcs.blocks import BlockDef
from wcs.categories import BlockCategory
from wcs.formdef import FormDef
from wcs.qommon.http_request import HTTPRequest
@ -263,3 +264,69 @@ def test_block_use_in_formdef(pub):
assert 'a block field' in resp.text
resp = resp.click('Edit', href='1/')
assert resp.form['max_items'].value == '1'
def test_blocks_category(pub):
create_superuser(pub)
BlockCategory.wipe()
BlockDef.wipe()
app = login(get_app(pub))
resp = app.get('/backoffice/forms/blocks/new')
assert 'category_id' not in resp.form.fields
block = BlockDef(name='foo')
block.store()
resp = app.get('/backoffice/forms/blocks/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 BlockCategory.count() == 1
category = BlockCategory.select()[0]
assert category.name == 'a new category'
resp = app.get('/backoffice/forms/blocks/new')
assert 'category_id' in resp.form.fields
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
resp = resp.click(href=re.compile('^settings$'))
resp.forms[0]['category_id'] = str(category.id)
resp = resp.forms[0].submit('cancel').follow()
block.refresh_from_storage()
assert block.category_id is None
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
resp = resp.click(href=re.compile('^settings$'))
resp.forms[0]['category_id'] = str(category.id)
resp = resp.forms[0].submit('submit').follow()
block.refresh_from_storage()
assert str(block.category_id) == str(category.id)
resp = app.get('/backoffice/forms/blocks/categories/')
resp = resp.click('New Category')
resp.forms[0]['name'] = 'a second category'
resp.forms[0]['description'] = 'description of the category'
resp = resp.forms[0].submit('submit')
assert BlockCategory.count() == 2
category2 = [x for x in BlockCategory.select() if x.id != category.id][0]
assert category2.name == 'a second category'
app.get('/backoffice/forms/blocks/categories/update_order?order=%s;%s;' % (category2.id, category.id))
categories = BlockCategory.select()
BlockCategory.sort_by_position(categories)
assert [x.id for x in categories] == [str(category2.id), str(category.id)]
app.get('/backoffice/forms/blocks/categories/update_order?order=%s;%s;' % (category.id, category2.id))
categories = BlockCategory.select()
BlockCategory.sort_by_position(categories)
assert [x.id for x in categories] == [str(category.id), str(category2.id)]
resp = app.get('/backoffice/forms/blocks/categories/')
resp = resp.click('a new category')
resp = resp.click('Delete')
resp = resp.forms[0].submit()
block.refresh_from_storage()
assert not block.category_id

54
tests/test_block.py Normal file
View File

@ -0,0 +1,54 @@
import xml.etree.ElementTree as ET
import pytest
from wcs.blocks import BlockDef
from wcs.categories import BlockCategory
from wcs.qommon.misc import indent_xml as indent
from .utilities import clean_temporary_pub, create_temporary_pub
@pytest.fixture
def pub(request):
return create_temporary_pub()
def teardown_module(module):
clean_temporary_pub()
def export_to_indented_xml(block, include_id=False):
block_xml = block.export_to_xml(include_id=include_id)
indent(block_xml)
return block_xml
def assert_import_export_works(block, include_id=False):
block2 = BlockDef.import_from_xml_tree(
ET.fromstring(ET.tostring(block.export_to_xml(include_id))), include_id
)
assert ET.tostring(export_to_indented_xml(block)) == ET.tostring(export_to_indented_xml(block2))
return block2
def test_block(pub):
block = BlockDef(name='test')
assert_import_export_works(block, include_id=True)
def test_block_with_category(pub):
category = BlockCategory(name='test category')
category.store()
block = BlockDef(name='test category')
block.category_id = category.id
block.store()
block2 = assert_import_export_works(block, include_id=True)
assert block2.category_id == block.category_id
# import with non existing category
BlockCategory.wipe()
export = ET.tostring(block.export_to_xml(include_id=True))
block3 = BlockDef.import_from_xml_tree(ET.fromstring(export), include_id=True)
assert block3.category_id is None

View File

@ -19,13 +19,15 @@ from quixote.directory import Directory
from quixote.html import TemplateIO, htmltext
from wcs.admin import utils
from wcs.admin.categories import BlockCategoriesDirectory, get_categories
from wcs.admin.fields import FieldDefPage, FieldsDirectory
from wcs.backoffice.snapshots import SnapshotsDirectory
from wcs.blocks import BlockDef, BlockdefImportError
from wcs.categories import BlockCategory
from wcs.qommon import _, misc, template
from wcs.qommon.backoffice.menu import html_top
from wcs.qommon.errors import AccessForbiddenError, TraversalError
from wcs.qommon.form import FileWidget, Form, HtmlWidget, SlugWidget, StringWidget
from wcs.qommon.form import FileWidget, Form, HtmlWidget, SingleSelectWidget, SlugWidget, StringWidget
class BlockFieldDefPage(FieldDefPage):
@ -147,6 +149,17 @@ class BlockDirectory(FieldsDirectory):
)
if disabled_slug:
widget.hint = _('The identifier can not be modified as the block is in use.')
category_options = get_categories(BlockCategory)
if category_options:
category_options = [(None, '---', '')] + list(category_options)
form.add(
SingleSelectWidget,
'category_id',
title=_('Category'),
options=category_options,
)
form.add(
StringWidget,
'digest_template',
@ -166,6 +179,8 @@ class BlockDirectory(FieldsDirectory):
self.objectdef.name = form.get_widget('name').parse()
if form.get_widget('slug'):
self.objectdef.slug = form.get_widget('slug').parse()
if form.get_widget('category_id'):
self.objectdef.category_id = form.get_widget('category_id').parse()
widget_template = form.get_widget('digest_template')
if widget_template.parse() and 'form_var_' in widget_template.parse():
widget_template.set_error(
@ -185,8 +200,9 @@ class BlockDirectory(FieldsDirectory):
class BlocksDirectory(Directory):
_q_exports = ['', 'new', ('import', 'p_import')]
_q_exports = ['', 'new', 'categories', ('import', 'p_import')]
do_not_call_in_templates = True
categories = BlockCategoriesDirectory()
def __init__(self, section):
super().__init__()
@ -207,14 +223,31 @@ class BlocksDirectory(Directory):
def _q_index(self):
html_top(self.section, title=_('Fields Blocks'))
categories = BlockCategory.select()
BlockCategory.sort_by_position(categories)
blocks = BlockDef.select(order_by='name')
if categories:
categories.append(BlockCategory(_('Misc')))
for category in categories:
category.blocks = [x for x in blocks if x.category_id == category.id]
return template.QommonTemplateResponse(
templates=['wcs/backoffice/blocks.html'],
context={'view': self, 'blocks': BlockDef.select(order_by='name')},
context={'view': self, 'blocks': blocks, 'categories': categories},
)
def new(self):
form = Form(enctype='multipart/form-data')
form.add(StringWidget, 'name', title=_('Name'), required=True, size=50)
category_options = get_categories(BlockCategory)
if category_options:
category_options = [(None, '---', '')] + list(category_options)
form.add(
SingleSelectWidget,
'category_id',
title=_('Category'),
options=category_options,
)
form.add_submit('submit', _('Add'))
form.add_submit('cancel', _('Cancel'))
if form.get_widget('cancel').parse():
@ -222,6 +255,8 @@ class BlocksDirectory(Directory):
if form.is_submitted() and not form.has_errors():
block = BlockDef(name=form.get_widget('name').parse())
if form.get_widget('category_id'):
block.category_id = form.get_widget('category_id').parse()
block.store()
return redirect('%s/' % block.id)

View File

@ -18,8 +18,9 @@ from quixote import get_publisher, get_request, get_response, redirect
from quixote.directory import Directory
from quixote.html import TemplateIO, htmltext
from wcs.blocks import BlockDef
from wcs.carddef import CardDef, get_cards_graph
from wcs.categories import CardDefCategory, Category, WorkflowCategory
from wcs.categories import BlockCategory, CardDefCategory, Category, WorkflowCategory
from wcs.formdef import FormDef
from wcs.qommon import _, misc, template
from wcs.qommon.backoffice.menu import html_top
@ -28,9 +29,11 @@ from wcs.qommon.form import Form, HtmlWidget, SingleSelectWidget, StringWidget,
from wcs.workflows import Workflow
def get_categories(category_class, filter_function):
def get_categories(category_class, filter_function=None):
t = sorted(
(misc.simplify(x.name), x.id, x.name, x.id) for x in category_class.select() if filter_function(x)
(misc.simplify(x.name), x.id, x.name, x.id)
for x in category_class.select()
if not filter_function or filter_function(x)
)
return [x[1:] for x in t]
@ -153,6 +156,11 @@ class WorkflowCategoryUI(CategoryUI):
management_roles_hint_text = _('Roles allowed to create, edit and delete workflows.')
class BlockCategoryUI(CategoryUI):
category_class = BlockCategory
management_roles_hint_text = None
class CategoryPage(Directory):
category_class = Category
category_ui_class = CategoryUI
@ -270,6 +278,14 @@ class WorkflowCategoryPage(CategoryPage):
empty_message = _('No workflow associated to this category.')
class BlockCategoryPage(CategoryPage):
category_class = BlockCategory
category_ui_class = BlockCategoryUI
object_class = BlockDef
usage_title = _('Blocks in this category')
empty_message = _('No block associated to this category.')
class CategoriesDirectory(Directory):
_q_exports = ['', 'new', 'update_order']
@ -361,3 +377,11 @@ class WorkflowCategoriesDirectory(CategoriesDirectory):
category_ui_class = WorkflowCategoryUI
category_page_class = WorkflowCategoryPage
category_explanation = _('Categories are used to sort the different workflows.')
class BlockCategoriesDirectory(CategoriesDirectory):
base_section = 'forms'
category_class = BlockCategory
category_ui_class = BlockCategoryUI
category_page_class = BlockCategoryPage
category_explanation = _('Categories are used to sort the different blocks.')

View File

@ -21,6 +21,7 @@ from quixote import get_publisher, get_request
from quixote.html import htmltag, htmltext
from . import data_sources, fields
from .categories import BlockCategory
from .qommon import _, misc
from .qommon.form import CompositeWidget, WidgetList
from .qommon.storage import StorableObject
@ -42,6 +43,7 @@ class BlockDef(StorableObject):
slug = None
fields = None
digest_template = None
category_id = None
# declarations for serialization
TEXT_ATTRIBUTES = ['name', 'slug', 'digest_template']
@ -51,6 +53,17 @@ class BlockDef(StorableObject):
self.name = name
self.fields = []
@property
def category(self):
return BlockCategory.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()
if self.slug is None:
@ -104,6 +117,12 @@ class BlockDef(StorableObject):
continue
ET.SubElement(root, text_attribute).text = getattr(self, text_attribute)
if self.category:
elem = ET.SubElement(root, 'category')
elem.text = self.category.name
if include_id:
elem.attrib['category_id'] = str(self.category.id)
fields = ET.SubElement(root, 'fields')
for field in self.fields or []:
fields.append(field.export_to_xml(charset='utf-8', include_id=True))
@ -173,6 +192,19 @@ class BlockDef(StorableObject):
field_o.init_with_xml(field, charset, include_id=True)
blockdef.fields.append(field_o)
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 BlockCategory.has_key(category_id):
blockdef.category_id = category_id
else:
category = misc.xml_node_text(category_node)
for c in BlockCategory.select():
if c.name == category:
blockdef.category_id = c.id
break
return blockdef
def get_usage_formdefs(self):

View File

@ -181,6 +181,25 @@ class WorkflowCategory(Category):
return Workflow
class BlockCategory(Category):
_names = 'block_categories'
xml_root_node = 'block_category'
# declarations for serialization
XML_NODES = [
('name', 'str'),
('url_name', 'str'),
('description', 'str'),
('position', 'int'),
]
@classmethod
def get_object_class(cls):
from .blocks import BlockDef
return BlockDef
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'))

View File

@ -4,12 +4,26 @@
{% block appbar-title %}{% trans "Field Blocks" %}{% endblock %}
{% block appbar-actions %}
<a href="categories/">{% trans "Categories" %}</a>
<a rel="popup" href="import">{% trans "Import" %}</a>
<a rel="popup" href="new">{% trans "New field block" %}</a>
{% endblock %}
{% block content %}
{% if blocks %}
{% if categories %}
{% for category in categories %}
{% if category.blocks %}
<div class="section">
<h2>{{ category.name }}</h2>
<ul class="objects-list single-links">
{% for block in category.blocks %}
<li><a href="{{ block.id }}/">{{ block.name }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endfor %}
{% elif blocks %}
<ul class="objects-list single-links">
{% for block in blocks %}
<li><a href="{{ block.id }}/">{{ block.name }}</a></li>