general: add categories for blocks (#59256)
This commit is contained in:
parent
a578bb646d
commit
cd70082556
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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.')
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue