cards: add category (#48111)

This commit is contained in:
Lauréline Guérin 2020-11-02 17:40:49 +01:00
parent 6a0c783587
commit 27b8916657
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
17 changed files with 641 additions and 157 deletions

View File

@ -23,7 +23,7 @@ from wcs.qommon.ident.password_accounts import PasswordAccount
from wcs.qommon.http_request import HTTPRequest
from wcs.qommon.template import get_current_theme
from wcs.admin.settings import UserFieldsFormDef
from wcs.categories import Category
from wcs.categories import Category, CardDefCategory
from wcs.data_sources import NamedDataSource
from wcs.wscalls import NamedWsCall
from wcs.roles import Role
@ -641,6 +641,7 @@ def test_settings_export_import(pub, studio):
Workflow.wipe()
Role.wipe()
Category.wipe()
CardDefCategory.wipe()
NamedDataSource.wipe()
NamedWsCall.wipe()
@ -672,6 +673,7 @@ def test_settings_export_import(pub, studio):
carddef.name = 'bar'
carddef.store()
Category(name='baz').store()
CardDefCategory(name='foobar').store()
Role(name='qux').store()
NamedDataSource(name='quux').store()
NamedWsCall(name='corge').store()
@ -708,10 +710,11 @@ def test_settings_export_import(pub, studio):
assert 'models/export_to_model-1.upload' not in filelist
assert 'roles/1' in filelist
assert 'categories/1' in filelist
assert 'carddef_categories/1' in filelist
assert 'datasources/1' in filelist
assert 'wscalls/corge' in filelist
for filename in filelist:
assert not '.indexes' in filename
assert '.indexes' not in filename
wipe()
assert FormDef.count() == 0

View File

@ -4,6 +4,7 @@ import pytest
from wcs import fields
from wcs.admin.settings import UserFieldsFormDef
from wcs.categories import CardDefCategory
from wcs.carddef import CardDef
from wcs.formdef import FormDef
from wcs.qommon.http_request import HTTPRequest
@ -42,6 +43,47 @@ def teardown_module(module):
clean_temporary_pub()
def test_cards_list(pub, studio):
create_superuser(pub)
CardDef.wipe()
carddef = CardDef()
carddef.name = 'card title'
carddef.fields = []
carddef.store()
carddef2 = CardDef()
carddef2.name = 'card title 2'
carddef2.fields = []
carddef2.store()
CardDefCategory.wipe()
cat = CardDefCategory(name='Foo')
cat.store()
cat2 = CardDefCategory(name='Bar')
cat2.store()
app = login(get_app(pub))
resp = app.get('/backoffice/cards/')
assert '<h2>Misc</h2>' not in resp.text
assert '<h2>Foo</h2>' not in resp.text
assert '<h2>Bar</h2>' not in resp.text
carddef.category = cat2
carddef.store()
resp = app.get('/backoffice/cards/')
assert '<h2>Misc</h2>' in resp.text
assert '<h2>Foo</h2>' not in resp.text
assert '<h2>Bar</h2>' in resp.text
carddef2.category = cat
carddef2.store()
resp = app.get('/backoffice/cards/')
assert '<h2>Misc</h2>' not in resp.text
assert '<h2>Foo</h2>' in resp.text
assert '<h2>Bar</h2>' in resp.text
def test_cards_new(pub, studio):
CardDef.wipe()
create_superuser(pub)
@ -197,6 +239,45 @@ def test_card_digest_template(pub, studio):
assert 'Existing cards will be updated in the background.' not in resp.text
def test_card_category(pub, studio):
create_superuser(pub)
CardDef.wipe()
carddef = CardDef()
carddef.name = 'card title'
carddef.fields = []
carddef.store()
CardDefCategory.wipe()
cat = CardDefCategory(name='Foo')
cat.store()
cat = CardDefCategory(name='Bar')
cat.store()
app = login(get_app(pub))
resp = app.get('/backoffice/cards/1/')
assert '<span class="label">Category</span> <span class="value">None</span>' in resp.text
assert '<span class="label">Category</span> <span class="value">Foo</span>' not in resp.text
assert '<span class="label">Category</span> <span class="value">Bar</span>' not in resp.text
resp = resp.click(href='category')
resp.forms[0].submit('cancel')
assert CardDef.get(carddef.id).category_id is None
resp = app.get('/backoffice/cards/1/')
assert '<span class="label">Category</span> <span class="value">None</span>' in resp.text
assert '<span class="label">Category</span> <span class="value">Foo</span>' not in resp.text
assert '<span class="label">Category</span> <span class="value">Bar</span>' not in resp.text
resp = resp.click(href='category')
resp.forms[0]['category_id'] = cat.id
resp.forms[0].submit('submit')
assert CardDef.get(carddef.id).category_id == cat.id
resp = app.get('/backoffice/cards/1/')
assert '<span class="label">Category</span> <span class="value">None</span>' not in resp.text
assert '<span class="label">Category</span> <span class="value">Foo</span>' not in resp.text
assert '<span class="label">Category</span> <span class="value">Bar</span>' in resp.text
def test_card_custom_view_data_source(pub, studio):
user = create_superuser(pub)
Role.wipe()

View File

@ -0,0 +1,212 @@
# -*- coding: utf-8 -*-
import pytest
from wcs.qommon.http_request import HTTPRequest
from wcs.carddef import CardDef
from wcs.categories import CardDefCategory
from utilities import get_app, login, create_temporary_pub, clean_temporary_pub
from .test_all import create_superuser
def pytest_generate_tests(metafunc):
if 'pub' in metafunc.fixturenames:
metafunc.parametrize('pub', ['pickle', 'sql', 'pickle-templates'], indirect=True)
@pytest.fixture
def pub(request):
pub = create_temporary_pub(
sql_mode=bool('sql' in request.param),
templates_mode=bool('templates' in request.param)
)
req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
pub.set_app_dir(req)
pub.cfg['identification'] = {'methods': ['password']}
pub.cfg['language'] = {'language': 'en'}
pub.write_cfg()
return pub
def teardown_module(module):
clean_temporary_pub()
def test_categories(pub):
create_superuser(pub)
app = login(get_app(pub))
app.get('/backoffice/cards/categories/')
def test_categories_new(pub):
create_superuser(pub)
CardDefCategory.wipe()
app = login(get_app(pub))
# go to the page and cancel
resp = app.get('/backoffice/cards/categories/')
resp = resp.click('New Category')
resp = resp.forms[0].submit('cancel')
assert resp.location == 'http://example.net/backoffice/cards/categories/'
# go to the page and add a category
resp = app.get('/backoffice/cards/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 resp.location == 'http://example.net/backoffice/cards/categories/'
resp = resp.follow()
assert 'a new category' in resp.text
resp = resp.click('a new category')
assert '<h2>a new category' in resp.text
assert CardDefCategory.get(1).name == 'a new category'
assert CardDefCategory.get(1).description == 'description of the category'
def test_categories_edit(pub):
create_superuser(pub)
CardDefCategory.wipe()
category = CardDefCategory(name='foobar')
category.store()
app = login(get_app(pub))
resp = app.get('/backoffice/cards/categories/1/')
assert 'no card model associated to this category' in resp.text
resp = resp.click(href='edit')
assert resp.forms[0]['name'].value == 'foobar'
resp.forms[0]['description'] = 'category description'
resp = resp.forms[0].submit('submit')
assert resp.location == 'http://example.net/backoffice/cards/categories/'
resp = resp.follow()
resp = resp.click('foobar')
assert '<h2>foobar' in resp.text
assert CardDefCategory.get(1).description == 'category description'
def test_categories_edit_duplicate_name(pub):
CardDefCategory.wipe()
category = CardDefCategory(name='foobar')
category.store()
category = CardDefCategory(name='foobar2')
category.store()
app = login(get_app(pub))
resp = app.get('/backoffice/cards/categories/1/')
resp = resp.click(href='edit')
assert resp.forms[0]['name'].value == 'foobar'
resp.forms[0]['name'] = 'foobar2'
resp = resp.forms[0].submit('submit')
assert 'This name is already used' in resp.text
resp = resp.forms[0].submit('cancel')
assert resp.location == 'http://example.net/backoffice/cards/categories/'
def test_categories_with_carddefs(pub):
CardDefCategory.wipe()
category = CardDefCategory(name='foobar')
category.store()
CardDef.wipe()
app = login(get_app(pub))
resp = app.get('/backoffice/cards/categories/1/')
assert 'form bar' not in resp.text
formdef = CardDef()
formdef.name = 'form bar'
formdef.fields = []
formdef.category_id = category.id
formdef.store()
resp = app.get('/backoffice/cards/categories/1/')
assert 'form bar' in resp.text
assert 'no card model associated to this category' not in resp.text
def test_categories_delete(pub):
CardDefCategory.wipe()
category = CardDefCategory(name='foobar')
category.store()
CardDef.wipe()
app = login(get_app(pub))
resp = app.get('/backoffice/cards/categories/1/')
resp = resp.click(href='delete')
resp = resp.forms[0].submit('cancel')
assert resp.location == 'http://example.net/backoffice/cards/categories/1/'
assert CardDefCategory.count() == 1
resp = app.get('/backoffice/cards/categories/1/')
resp = resp.click(href='delete')
resp = resp.forms[0].submit()
assert resp.location == 'http://example.net/backoffice/cards/categories/'
resp = resp.follow()
assert CardDefCategory.count() == 0
def test_categories_edit_description(pub):
CardDefCategory.wipe()
category = CardDefCategory(name='foobar')
category.description = 'category description'
category.store()
app = login(get_app(pub))
# this URL is used for editing from the frontoffice, there's no link
# pointing to it in the admin.
resp = app.get('/backoffice/cards/categories/1/description')
assert resp.forms[0]['description'].value == 'category description'
resp.forms[0]['description'] = 'updated description'
# check cancel doesn't save the change
resp2 = resp.forms[0].submit('cancel')
assert resp2.location == 'http://example.net/backoffice/cards/categories/1/'
assert CardDefCategory.get(1).description == 'category description'
# check submit does it properly
resp2 = resp.forms[0].submit('submit')
assert resp2.location == 'http://example.net/backoffice/cards/categories/1/'
resp2 = resp2.follow()
assert CardDefCategory.get(1).description == 'updated description'
def test_categories_new_duplicate_name(pub):
CardDefCategory.wipe()
category = CardDefCategory(name='foobar')
category.store()
app = login(get_app(pub))
resp = app.get('/backoffice/cards/categories/')
resp = resp.click('New Category')
resp.forms[0]['name'] = 'foobar'
resp = resp.forms[0].submit('submit')
assert 'This name is already used' in resp.text
def test_categories_reorder(pub):
CardDefCategory.wipe()
category = CardDefCategory(name='foo')
category.store()
category = CardDefCategory(name='bar')
category.store()
category = CardDefCategory(name='baz')
category.store()
app = login(get_app(pub))
app.get('/backoffice/cards/categories/update_order?order=1;2;3;')
categories = CardDefCategory.select()
CardDefCategory.sort_by_position(categories)
assert [x.id for x in categories] == ['1', '2', '3']
app.get('/backoffice/cards/categories/update_order?order=3;1;2;')
categories = CardDefCategory.select()
CardDefCategory.sort_by_position(categories)
assert [x.id for x in categories] == ['3', '1', '2']

View File

@ -46,7 +46,7 @@ from wcs.wf.resubmit import ResubmitWorkflowStatusItem
from wcs.wf.create_formdata import CreateFormdataWorkflowStatusItem, Mapping
from wcs.wf.create_carddata import CreateCarddataWorkflowStatusItem
from wcs.carddef import CardDef
from wcs.categories import Category
from wcs.categories import Category, CardDefCategory
from wcs.formdef import FormDef
from wcs.logged_errors import LoggedError
from wcs import fields
@ -6220,7 +6220,7 @@ def test_carddata_management(pub, studio):
user = create_user(pub)
app = login(get_app(pub))
resp = app.get('/backoffice/')
assert not 'Cards' in resp.text
assert 'Cards' not in resp.text
carddef = CardDef()
carddef.name = 'foo'
carddef.fields = [
@ -6232,7 +6232,7 @@ def test_carddata_management(pub, studio):
carddef.data_class().wipe()
resp = app.get('/backoffice/')
assert not 'Cards' in resp.text
assert 'Cards' not in resp.text
carddef.backoffice_submission_roles = user.roles
carddef.store()
@ -6288,6 +6288,51 @@ def test_carddata_management(pub, studio):
assert resp.text.count('<tr') == 2 # header + row of data
def test_carddata_management_categories(pub, studio):
user = create_user(pub)
CardDef.wipe()
carddef = CardDef()
carddef.name = 'foo'
carddef.fields = []
carddef.backoffice_submission_roles = None
carddef.workflow_roles = {'_editor': user.roles[0]}
carddef.store()
carddef2 = CardDef()
carddef2.name = 'card title 2'
carddef2.fields = []
carddef2.backoffice_submission_roles = None
carddef2.workflow_roles = {'_editor': user.roles[0]}
carddef2.store()
CardDefCategory.wipe()
cat = CardDefCategory(name='Foo')
cat.store()
cat2 = CardDefCategory(name='Bar')
cat2.store()
app = login(get_app(pub))
resp = app.get('/backoffice/data/')
assert '<h3>Misc</h3>' not in resp.text
assert '<h3>Foo</h3>' not in resp.text
assert '<h3>Bar</h3>' not in resp.text
carddef.category = cat2
carddef.store()
resp = app.get('/backoffice/data/')
assert '<h3>Misc</h3>' in resp.text
assert '<h3>Foo</h3>' not in resp.text
assert '<h3>Bar</h3>' in resp.text
carddef2.category = cat
carddef2.store()
resp = app.get('/backoffice/data/')
assert '<h3>Misc</h3>' not in resp.text
assert '<h3>Foo</h3>' in resp.text
assert '<h3>Bar</h3>' in resp.text
def test_studio_card_item_link(pub, studio):
user = create_user(pub)
CardDef.wipe()

View File

@ -31,7 +31,7 @@ from wcs.roles import Role
from wcs.carddef import CardDef
from wcs.formdef import FormDef
from wcs.formdata import Evolution
from wcs.categories import Category
from wcs.categories import Category, CardDefCategory
from wcs.data_sources import NamedDataSource
from wcs.workflows import Workflow, EditableWorkflowStatusItem, WorkflowBackofficeFieldsFormDef, WorkflowVariablesFieldsFormDef
from wcs.wf.jump import JumpWorkflowStatusItem
@ -3312,6 +3312,11 @@ def test_cards(pub, local_user):
local_user.roles = [role.id]
local_user.store()
CardDefCategory.wipe()
category = CardDefCategory()
category.name = 'Category A'
category.store()
CardDef.wipe()
carddef = CardDef()
carddef.name = 'test'
@ -3355,11 +3360,21 @@ def test_cards(pub, local_user):
resp = get_app(pub).get(sign_uri('/api/cards/@list'))
assert len(resp.json['data']) == 1
assert resp.json['data'][0]['slug'] == 'test'
assert resp.json['data'][0]['category_slug'] is None
assert resp.json['data'][0]['category_name'] is None
assert resp.json['data'][0]['custom_views'] == [
{'id': 'datasource-carddef-custom-view', 'text': 'datasource carddef custom view'},
{'id': 'shared-carddef-custom-view', 'text': 'shared carddef custom view'},
]
carddef.category = category
carddef.store()
resp = get_app(pub).get(sign_uri('/api/cards/@list'))
assert len(resp.json['data']) == 1
assert resp.json['data'][0]['slug'] == 'test'
assert resp.json['data'][0]['category_slug'] == 'category-a'
assert resp.json['data'][0]['category_name'] == 'Category A'
resp = get_app(pub).get(sign_uri('/api/cards/test/list'), status=403)
resp = get_app(pub).get(sign_uri(

View File

@ -4,7 +4,9 @@ import xml.etree.ElementTree as ET
from django.utils.six import BytesIO
from wcs.qommon.http_request import HTTPRequest
from wcs.qommon.misc import indent_xml as indent
from wcs.qommon.template import Template
from wcs.categories import CardDefCategory
from wcs.carddef import CardDef
from wcs.fields import ItemField
from wcs.fields import StringField
@ -31,6 +33,28 @@ def teardown_module(module):
clean_temporary_pub()
def export_to_indented_xml(carddef, include_id=False):
carddef_xml = ET.fromstring(ET.tostring(carddef.export_to_xml(include_id=include_id)))
indent(carddef_xml)
return carddef_xml
def assert_compare_carddef(carddef1, carddef2, include_id=False):
assert (
ET.tostring(export_to_indented_xml(carddef1, include_id=include_id)) ==
ET.tostring(export_to_indented_xml(carddef2, include_id=include_id)))
assert (
carddef1.export_to_json(include_id=include_id, indent=2) ==
carddef2.export_to_json(include_id=include_id, indent=2))
def assert_xml_import_export_works(carddef, include_id=False):
carddef_xml = carddef.export_to_xml(include_id=include_id)
carddef2 = CardDef.import_from_xml_tree(carddef_xml, include_id=include_id)
assert_compare_carddef(carddef, carddef2, include_id=include_id)
return carddef2
def test_basics(pub):
carddef = CardDef()
carddef.name = 'foo'
@ -159,6 +183,40 @@ def test_xml_export_import(pub):
assert custom_views[1].formdef_type == 'carddef'
def test_xml_export_import_category_reference(pub):
CardDefCategory.wipe()
CardDef.wipe()
cat = CardDefCategory()
cat.name = 'test category'
cat.store()
carddef = CardDef()
carddef.name = 'foo'
carddef.category_id = cat.id
f2 = assert_xml_import_export_works(carddef)
assert f2.category_id == carddef.category_id
f2 = assert_xml_import_export_works(carddef, include_id=True)
assert f2.category_id == carddef.category_id
carddef_xml_with_id = carddef.export_to_xml(include_id=True)
# check there's no reference to a non-existing category
CardDefCategory.wipe()
assert CardDef.import_from_xml_tree(carddef_xml_with_id, include_id=False).category_id is None
assert CardDef.import_from_xml_tree(carddef_xml_with_id, include_id=True).category_id is None
# check an import that is not using id fields will find the category by its
# name
cat = CardDefCategory()
cat.id = '2'
cat.name = 'test category'
cat.store()
assert CardDef.import_from_xml_tree(carddef_xml_with_id, include_id=False).category_id == '2'
assert CardDef.import_from_xml_tree(carddef_xml_with_id, include_id=True).category_id is None
def test_template_access(pub):
CardDef.wipe()
carddef = CardDef()

View File

@ -6,9 +6,8 @@ import pytest
from django.utils.six import BytesIO
from quixote import cleanup
from wcs import publisher
from wcs.categories import Category
from wcs.categories import Category, CardDefCategory
from utilities import create_temporary_pub
@ -25,49 +24,53 @@ def teardown_module(module):
shutil.rmtree(pub.APP_DIR)
def test_store():
Category.wipe()
test = Category()
@pytest.mark.parametrize('category_class', [Category, CardDefCategory])
def test_store(category_class):
category_class.wipe()
test = category_class()
test.name = 'Test'
test.description = 'Hello world'
test.store()
test2 = Category.get(1)
test2 = category_class.get(1)
assert test.id == test2.id
assert test.name == test2.name
assert test.description == test2.description
def test_urlname():
Category.wipe()
test = Category()
@pytest.mark.parametrize('category_class', [Category, CardDefCategory])
def test_urlname(category_class):
category_class.wipe()
test = category_class()
test.name = 'Test'
test.description = 'Hello world'
test.store()
test = Category.get(1)
test = category_class.get(1)
assert test.url_name == 'test'
def test_duplicate_urlname():
Category.wipe()
test = Category()
@pytest.mark.parametrize('category_class', [Category, CardDefCategory])
def test_duplicate_urlname(category_class):
category_class.wipe()
test = category_class()
test.name = 'Test'
test.store()
test = Category.get(1)
test = category_class.get(1)
assert test.url_name == 'test'
test2 = Category()
test2 = category_class()
test2.name = 'Test'
test2.store()
test2 = Category.get(2)
test2 = category_class.get(2)
assert test2.url_name == 'test-2'
def test_sort_positions():
Category.wipe()
@pytest.mark.parametrize('category_class', [Category, CardDefCategory])
def test_sort_positions(category_class):
category_class.wipe()
categories = []
for i in range(10):
test = Category()
test = category_class()
test.name = 'Test %s' % i
test.position = 10-i
categories.append(test)
@ -76,35 +79,37 @@ def test_sort_positions():
for i in range(8, 10):
categories[i].position = None
Category.sort_by_position(categories)
category_class.sort_by_position(categories)
assert categories[0].name == 'Test 7'
assert categories[-1].name in ('Test 8', 'Test 9')
def test_xml_export():
Category.wipe()
test = Category()
@pytest.mark.parametrize('category_class', [Category, CardDefCategory])
def test_xml_export(category_class):
category_class.wipe()
test = category_class()
test.id = 1
test.name = 'Test'
test.description = 'Hello world'
test.store()
test = Category.get(1)
test = category_class.get(1)
assert b'<name>Test</name>' in test.export_to_xml_string(include_id=True)
assert b' id="1"' in test.export_to_xml_string(include_id=True)
assert b' id="1"' not in test.export_to_xml_string(include_id=False)
def test_xml_import():
Category.wipe()
test = Category()
@pytest.mark.parametrize('category_class', [Category, CardDefCategory])
def test_xml_import(category_class):
category_class.wipe()
test = category_class()
test.name = 'Test'
test.description = 'Hello world'
test.store()
test = Category.get(1)
test = category_class.get(1)
fd = BytesIO(test.export_to_xml_string(include_id=True))
test2 = Category.import_from_xml(fd, include_id=True)
test2 = category_class.import_from_xml(fd, include_id=True)
assert test.id == test2.id
assert test.name == test2.name
assert test.description == test2.description
@ -129,40 +134,43 @@ def test_load_old_pickle():
assert test.description == test2.description
def test_get_by_urlname():
Category.wipe()
test = Category()
@pytest.mark.parametrize('category_class', [Category, CardDefCategory])
def test_get_by_urlname(category_class):
category_class.wipe()
test = category_class()
test.id = 1
test.name = 'Test'
test.description = 'Hello world'
test.store()
test = Category.get(1)
test2 = Category.get_by_urlname('test')
test = category_class.get(1)
test2 = category_class.get_by_urlname('test')
assert test.id == test2.id
def test_has_urlname():
Category.wipe()
test = Category()
@pytest.mark.parametrize('category_class', [Category, CardDefCategory])
def test_has_urlname(category_class):
category_class.wipe()
test = category_class()
test.id = 1
test.name = 'Test'
test.description = 'Hello world'
test.store()
test = Category.get(1)
test = category_class.get(1)
assert Category.has_urlname('test')
assert not Category.has_urlname('foobar')
assert category_class.has_urlname('test')
assert not category_class.has_urlname('foobar')
def test_remove_self():
Category.wipe()
test = Category()
@pytest.mark.parametrize('category_class', [Category, CardDefCategory])
def test_remove_self(category_class):
category_class.wipe()
test = category_class()
test.id = 1
test.name = 'Test'
test.description = 'Hello world'
test.store()
test = Category.get(1)
test = category_class.get(1)
test.remove_self()
with pytest.raises(KeyError):
Category.get(1)
category_class.get(1)

View File

@ -18,19 +18,22 @@ from quixote import redirect
from quixote.directory import Directory
from quixote.html import TemplateIO, htmltext
from wcs.qommon import _
from wcs.categories import Category
from wcs.qommon import _, N_
from wcs.categories import Category, CardDefCategory
from wcs.qommon.form import *
from wcs.qommon.backoffice.menu import html_top
from wcs.carddef import CardDef
from wcs.formdef import FormDef
class CategoryUI(object):
category_class = Category
def __init__(self, category):
self.category = category
if self.category is None:
self.category = Category()
self.category = self.category_class()
def get_form(self):
form = Form(enctype='multipart/form-data')
@ -39,10 +42,11 @@ class CategoryUI(object):
form.add(WysiwygTextWidget, 'description', title=_('Description'),
cols=80, rows=10,
value=self.category.description)
form.add(StringWidget, 'redirect_url', size=32,
title=_('URL Redirection'),
hint=_('If set, redirect the site category page to the given URL.'),
value=self.category.redirect_url)
if self.category_class == Category:
form.add(StringWidget, 'redirect_url', size=32,
title=_('URL Redirection'),
hint=_('If set, redirect the site category page to the given URL.'),
value=self.category.redirect_url)
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
return form
@ -51,22 +55,32 @@ class CategoryUI(object):
self.category.name = form.get_widget('name').parse()
name = form.get_widget('name').parse()
category_names = [x.name for x in Category.select() if x.id != self.category.id]
category_names = [x.name for x in self.category_class.select() if x.id != self.category.id]
if name in category_names:
form.get_widget('name').set_error(_('This name is already used'))
raise ValueError()
self.category.description = form.get_widget('description').parse()
self.category.redirect_url = form.get_widget('redirect_url').parse()
if form.get_widget('redirect_url'):
self.category.redirect_url = form.get_widget('redirect_url').parse()
self.category.store()
class CardDefCategoryUI(CategoryUI):
category_class = CardDefCategory
class CategoryPage(Directory):
category_class = Category
category_ui_class = CategoryUI
formdef_class = FormDef
usage_title = N_('Forms in this category')
empty_message = N_('no form associated to this category')
_q_exports = ['', 'edit', 'delete', 'description']
def __init__(self, component):
self.category = Category.get(component)
self.category_ui = CategoryUI(self.category)
self.category = self.category_class.get(component)
self.category_ui = self.category_ui_class(self.category)
get_response().breadcrumb.append((component + '/', self.category.name))
def _q_index(self):
@ -86,17 +100,17 @@ class CategoryPage(Directory):
r += self.category.get_description_html_text()
r += htmltext('</div>')
formdefs = FormDef.select(order_by='name')
formdefs = self.formdef_class.select(order_by='name')
formdefs = [x for x in formdefs if x.category_id == self.category.id]
r += htmltext('<div class="bo-block">')
r += htmltext('<h3>%s</h3>') % _('Forms in this category')
r += htmltext('<h3>%s</h3>') % _(self.usage_title)
r += htmltext('<ul>')
for formdef in formdefs:
r += htmltext('<li><a href="../../%s/">') % str(formdef.id)
r += formdef.name
r += htmltext('</a></li>')
if not formdefs:
r += htmltext('<li>%s</li>') % _('no form associated to this category')
r += htmltext('<li>%s</li>') % _(self.empty_message)
r += htmltext('</ul>')
r += htmltext('</div>')
return r.getvalue()
@ -165,8 +179,20 @@ class CategoryPage(Directory):
return r.getvalue()
class CardDefCategoryPage(CategoryPage):
category_class = CardDefCategory
category_ui_class = CardDefCategoryUI
formdef_class = CardDef
usage_title = N_('Card models in this category')
empty_message = N_('no card model associated to this category')
class CategoriesDirectory(Directory):
_q_exports = ['', 'new', 'update_order']
category_class = Category
category_ui_class = CategoryUI
category_page_class = CategoryPage
category_explanation = N_('Categories are used to sort the different forms.')
def _q_index(self):
get_response().add_javascript(['jquery.js', 'jquery-ui.js', 'biglist.js',
@ -180,11 +206,10 @@ class CategoriesDirectory(Directory):
r += htmltext('<a class="new-item" href="new" rel="popup">%s</a>') % _('New Category')
r += htmltext('</span>')
r += htmltext('</div>')
r += htmltext('<div class="explanation bo-block"><p>%s</p></div>') % \
_('Categories are used to sort the different forms.')
categories = Category.select()
r += htmltext('<div class="explanation bo-block"><p>%s</p></div>') % _(self.category_explanation)
categories = self.category_class.select()
r += htmltext('<ul class="biglist sortable" id="category-list">')
Category.sort_by_position(categories)
self.category_class.sort_by_position(categories)
for category in categories:
r += htmltext('<li class="biglistitem" id="itemId_%s">') % category.id
r += htmltext('<strong class="label"><a href="%s/">%s</a></strong>') % (
@ -196,7 +221,7 @@ class CategoriesDirectory(Directory):
def update_order(self):
request = get_request()
new_order = request.form['order'].strip(';').split(';')
categories = Category.select()
categories = self.category_class.select()
dict = {}
for l in categories:
dict[str(l.id)] = l
@ -207,7 +232,7 @@ class CategoriesDirectory(Directory):
def new(self):
get_response().breadcrumb.append( ('new', _('New')) )
category_ui = CategoryUI(None)
category_ui = self.category_ui_class(None)
form = category_ui.get_form()
if form.get_widget('cancel').parse():
return redirect('.')
@ -227,8 +252,15 @@ class CategoriesDirectory(Directory):
return r.getvalue()
def _q_lookup(self, component):
return CategoryPage(component)
return self.category_page_class(component)
def _q_traverse(self, path):
get_response().breadcrumb.append( ('categories/', _('Categories')) )
return super(CategoriesDirectory, self)._q_traverse(path)
return super()._q_traverse(path)
class CardDefCategoriesDirectory(CategoriesDirectory):
category_class = CardDefCategory
category_ui_class = CardDefCategoryUI
category_page_class = CardDefCategoryPage
category_explanation = N_('Categories are used to sort the different card models.')

View File

@ -60,19 +60,20 @@ from .logged_errors import LoggedErrorsDirectory
from wcs.backoffice.snapshots import SnapshotsDirectory
def get_categories():
t = sorted([(misc.simplify(x.name), x.id, x.name, x.id) for x in Category.select()])
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(object):
formdef_class = FormDef
category_class = Category
def __init__(self, formdef):
self.formdef = formdef
def get_categories(self):
return get_categories()
return get_categories(self.category_class)
@classmethod
def get_workflows(cls, condition=lambda x: True):
@ -165,6 +166,8 @@ class FieldsDirectory(FieldsDirectory):
class OptionsDirectory(Directory):
category_class = Category
category_empty_choice = N_('Select a category for this form')
_q_exports = ['confirmation', 'only_allow_one',
'always_advertise', 'tracking_code', 'online_status', 'captcha',
'description', 'keywords', 'category', 'management',
@ -265,9 +268,9 @@ class OptionsDirectory(Directory):
return self.handle(form, _('Keywords'))
def category(self):
categories = get_categories()
categories = get_categories(self.category_class)
form = Form(enctype='multipart/form-data')
form.widgets.append(HtmlWidget('<p>%s</p>' % _('Select a category for this form')))
form.widgets.append(HtmlWidget('<p>%s</p>' % _(self.category_empty_choice)))
form.add(SingleSelectWidget, 'category_id', title=_('Category'),
value=self.formdef.category_id,
options=[(None, '---', '')] + categories)
@ -421,6 +424,7 @@ class FormDefPage(Directory):
formdef_export_prefix = 'form'
formdef_ui_class = FormDefUI
formdef_default_workflow = '_default'
options_directory_class = OptionsDirectory
delete_message = N_('You are about to irrevocably delete this form.')
delete_title = N_('Deleting Form:')
@ -442,7 +446,7 @@ class FormDefPage(Directory):
self.fields.html_top = self.html_top
self.role = WorkflowRoleDirectory(self.formdef)
self.role.html_top = self.html_top
self.options = OptionsDirectory(self.formdef)
self.options = self.options_directory_class(self.formdef)
self.logged_errors_dir = LoggedErrorsDirectory(parent_dir=self, formdef_id=self.formdef.id)
self.snapshots_dir = SnapshotsDirectory(self.formdef)
@ -714,31 +718,6 @@ class FormDefPage(Directory):
r += htmltext('</div>')
return r.getvalue()
def category(self):
categories = get_categories()
form = Form(enctype='multipart/form-data')
form.add(SingleSelectWidget, 'category_id',
value=self.formdef.category_id,
options=[(None, '---', '')] + categories)
if not self.formdef.is_readonly():
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
if form.get_widget('cancel').parse():
return redirect('.')
if not form.is_submitted() or form.has_errors():
get_response().breadcrumb.append( ('category', _('Category')) )
self.html_top(title = self.formdef.name)
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('Category')
r += htmltext('<p>%s</p>') % _('Select a category for this form')
r += form.render()
return r.getvalue()
else:
self.formdef.category_id = form.get_widget('category_id').parse()
self.formdef.store(comment=_('Change of category'))
return redirect('.')
def _roles_selection(self, title, attribute, description=None,
include_logged_users_role=True):
form = Form(enctype='multipart/form-data')
@ -1564,6 +1543,7 @@ class FormsDirectory(AccessControlled, Directory):
_q_exports = ['', 'new', ('import', 'p_import'),
'blocks', 'categories', ('data-sources', 'data_sources')]
category_class = Category
categories = CategoriesDirectory()
blocks = BlocksDirectory(section='forms')
data_sources = NamedDataSourcesDirectoryInForms()
@ -1571,6 +1551,7 @@ class FormsDirectory(AccessControlled, Directory):
formdef_page_class = FormDefPage
formdef_ui_class = FormDefUI
top_title = N_('Forms')
import_title = N_('Import Form')
import_submit_label = N_('Import Form')
import_paragraph = N_(
@ -1593,30 +1574,13 @@ class FormsDirectory(AccessControlled, Directory):
return super()._q_traverse(path)
def _q_index(self):
self.html_top(title = _('Forms'))
self.html_top(title=_(self.top_title))
r = TemplateIO(html=True)
get_response().add_javascript(['jquery.js', 'widget_list.js'])
has_roles = bool(Role.count())
r += self.form_actions()
r += htmltext('<div id="appbar">')
r += htmltext('<h2>%s</h2>') % _('Forms')
if has_roles:
r += htmltext('<span class="actions">')
r += htmltext('<a href="data-sources/">%s</a>') % _('Data sources')
if get_publisher().has_site_option('fields-blocks'):
r += htmltext('<a href="blocks/">%s</a>') % _('Fields blocks')
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" href="new" rel="popup">%s</a>') % _('New Form')
r += htmltext('</span>')
r += htmltext('</div>')
if not has_roles:
r += htmltext('<p>%s</p>') % _('You first have to define roles.')
cats = Category.select()
Category.sort_by_position(cats)
cats = self.category_class.select()
self.category_class.sort_by_position(cats)
one = False
formdefs = self.formdef_class.select(order_by='name', ignore_errors=True, lightweight=True)
for c in cats:
@ -1638,6 +1602,27 @@ class FormsDirectory(AccessControlled, Directory):
r += self.form_list(l2, title = title)
return r.getvalue()
def form_actions(self):
r = TemplateIO(html=True)
has_roles = bool(Role.count())
r += htmltext('<div id="appbar">')
r += htmltext('<h2>%s</h2>') % _('Forms')
if has_roles:
r += htmltext('<span class="actions">')
r += htmltext('<a href="data-sources/">%s</a>') % _('Data sources')
if get_publisher().has_site_option('fields-blocks'):
r += htmltext('<a href="blocks/">%s</a>') % _('Fields blocks')
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" href="new" rel="popup">%s</a>') % _('New Form')
r += htmltext('</span>')
r += htmltext('</div>')
if not has_roles:
r += htmltext('<p>%s</p>') % _('You first have to define roles.')
return r.getvalue()
def form_list(self, formdefs, title):
r = TemplateIO(html=True)
if title:

View File

@ -867,6 +867,7 @@ class SettingsDirectory(QommonSettingsDirectory):
if not get_cfg('sp', {}).get('idp-manage-roles'):
form.add(CheckboxWidget, 'roles', title = _('Roles'), value = True)
form.add(CheckboxWidget, 'categories', title = _('Categories'), value = True)
form.add(CheckboxWidget, 'carddef_categories', title = _('Card Model Categories'), value = True)
form.add(CheckboxWidget, 'settings', title = _('Settings'), value = False)
form.add(CheckboxWidget, 'datasources', title=_('Data sources'), value=True)
form.add(CheckboxWidget, 'mail-templates', title=_('Mail templates'), value=True)
@ -894,7 +895,7 @@ class SettingsDirectory(QommonSettingsDirectory):
c = BytesIO()
z = zipfile.ZipFile(c, 'w')
for d in self.dirs:
if d not in ('roles', 'categories', 'datasources', 'wscalls', 'mail-templates'):
if d not in ('roles', 'categories', 'carddef_categories', 'datasources', 'wscalls', 'mail-templates'):
continue
path = os.path.join(self.app_dir, d)
if not os.path.exists(path):
@ -942,8 +943,8 @@ class SettingsDirectory(QommonSettingsDirectory):
job.store()
dirs = []
for w in ('formdefs', 'carddefs', 'workflows', 'roles', 'categories',
'datasources', 'wscalls', 'mail-templates', 'blockdefs'):
for w in ('formdefs', 'carddefs', 'workflows', 'roles', 'categories', 'carddef_categories',
'datasources', 'wscalls', 'mail-templates', 'blockdefs'):
if form.get_widget(w) and form.get_widget(w).parse():
dirs.append(w)
if not dirs and not form.get_widget('settings').parse():

View File

@ -364,6 +364,8 @@ class ApiCardsDirectory(Directory):
'title': x.name,
'slug': x.url_name,
'url': x.get_url(),
'category_slug': x.category.url_name if x.category else None,
'category_name': x.category.name if x.category else None,
'description': x.description or '',
'keywords': x.keywords_list,
'custom_views': get_custom_views(x),

View File

@ -16,29 +16,32 @@
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.
import time
from quixote import get_publisher, get_request, get_response, get_session, redirect
from quixote import get_publisher, get_response, get_session, redirect
from quixote.directory import Directory
from quixote.html import TemplateIO, htmltext
from ..qommon import _, N_, misc
from ..qommon import _, N_
from ..qommon.misc import C_
from ..qommon.storage import NotEqual, Null
from wcs.carddef import CardDef
from wcs.categories import CardDefCategory
from wcs.roles import Role
from wcs.workflows import Workflow
from wcs.admin.forms import FormsDirectory, FormDefPage, FormDefUI, html_top
from wcs.admin.categories import CardDefCategoriesDirectory
from wcs.admin.forms import FormsDirectory, FormDefPage, FormDefUI, html_top, OptionsDirectory
from wcs.admin.logged_errors import LoggedErrorsDirectory
from wcs.admin import utils
class CardDefUI(FormDefUI):
formdef_class = CardDef
category_class = CardDefCategory
def get_categories(self):
return []
class CardDefOptionsDirectory(OptionsDirectory):
category_class = CardDefCategory
category_empty_choice = N_('Select a category for this card model')
class CardDefPage(FormDefPage):
@ -47,6 +50,8 @@ class CardDefPage(FormDefPage):
formdef_ui_class = CardDefUI
formdef_default_workflow = '_carddef_default'
options_directory_class = CardDefOptionsDirectory
delete_message = N_('You are about to irrevocably delete this card model.')
delete_title = N_('Deleting Card Model:')
overwrite_message = N_(
@ -86,6 +91,17 @@ class CardDefPage(FormDefPage):
'label': label,
'current_value': current_value})
r += htmltext('<div class="bo-block">')
r += htmltext('<h3>%s</h3>') % _('Information')
r += htmltext('<ul class="biglist optionslist">')
r += add_option_line(
'options/category', _('Category'),
self.formdef.category_id and self.formdef.category and
self.formdef.category.name or C_('category|None'))
r += htmltext('</ul>')
r += htmltext('</div>')
r += htmltext('<div class="splitcontent-left">')
r += htmltext('<div class="bo-block">')
r += htmltext('<h3>%s</h3>') % _('Workflow')
@ -214,11 +230,15 @@ class CardDefPage(FormDefPage):
class CardsDirectory(FormsDirectory):
_q_exports = ['', 'new', ('import', 'p_import')]
_q_exports = ['', 'new', ('import', 'p_import'), 'categories']
category_class = CardDefCategory
categories = CardDefCategoriesDirectory()
formdef_class = CardDef
formdef_page_class = CardDefPage
formdef_ui_class = CardDefUI
top_title = N_('Card Models')
import_title = N_('Import Card Model')
import_submit_label = N_('Import Card Model')
import_paragraph = N_(
@ -238,20 +258,16 @@ class CardsDirectory(FormsDirectory):
get_response().breadcrumb.append(('cards/', _('Card Models')))
return Directory._q_traverse(self, path)
def _q_index(self):
self.html_top(title=_('Card Models'))
def form_actions(self):
r = TemplateIO(html=True)
r += htmltext('<div id="appbar">')
r += htmltext('<h2>%s</h2>') % _('Card Models')
r += htmltext('<span class="actions">')
r += htmltext('<a href="categories/">%s</a>') % _('Categories')
r += htmltext('<a href="import" rel="popup">%s</a>') % _('Import')
r += htmltext('<a class="new-item" href="new" rel="popup">%s</a>') % _('New Card Model')
r += htmltext('</span>')
r += htmltext('</div>')
formdefs = self.formdef_class.select(order_by='name', ignore_errors=True, lightweight=True)
r += self.form_list(formdefs, title='')
return r.getvalue()
def new(self):

View File

@ -33,6 +33,7 @@ from ..qommon import template
from ..qommon.afterjobs import AfterJob
from wcs.carddef import CardDef
from wcs.categories import CardDefCategory
from wcs import fields
from .management import ManagementDirectory, FormPage, FormFillPage, FormBackOfficeStatusPage
@ -60,16 +61,23 @@ class DataManagementDirectory(ManagementDirectory):
def get_carddefs(self):
user = get_request().user
if user:
for formdef in CardDef.select(order_by='name', ignore_errors=True, lightweight=True):
if user.is_admin or formdef.is_of_concern_for_user(user):
yield formdef
if not user:
return
carddefs = CardDef.select(order_by='name', ignore_errors=True, lightweight=True)
carddefs = [c for c in carddefs if user.is_admin or c.is_of_concern_for_user(user)]
cats = CardDefCategory.select()
CardDefCategory.sort_by_position(cats)
for c in cats + [None]:
for carddef in carddefs:
if c is None and not carddef.category_id:
yield carddef
if c is not None and carddef.category_id == c.id:
yield carddef
def _q_index(self):
html_top('data_management', _('Cards'))
if CardDef.count() == 0:
return self.empty_site_message(_('Cards'))
return template.QommonTemplateResponse(
templates=['wcs/backoffice/data-management.html'],
context={'view': self})

View File

@ -23,6 +23,7 @@ from .qommon.storage import Contains, Equal, NotEqual, ILike
from .qommon.template import Template
from wcs.carddata import CardData
from wcs.categories import CardDefCategory
from wcs.formdef import FormDef, get_formdefs_of_all_kinds
if not hasattr(types, 'ClassType'):
@ -39,6 +40,8 @@ class CardDef(FormDef):
confirmation = False
category_class = CardDefCategory
def data_class(self, mode=None):
if not 'carddef' in sys.modules:
sys.modules['carddef'] = sys.modules[__name__]

View File

@ -14,7 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.
from quixote import get_publisher, get_response
from quixote import get_publisher
from quixote.html import htmltext
from .qommon import N_
@ -34,10 +34,11 @@ class Category(XmlStorableObject):
redirect_url = None
# declarations for serialization
XML_NODES = [('name', 'str'), ('url_name', 'str'), ('description', 'str'),
('redirect_url', 'str'), ('position', 'int')]
XML_NODES = [
('name', 'str'), ('url_name', 'str'), ('description', 'str'),
('redirect_url', 'str'), ('position', 'int')]
def __init__(self, name = None):
def __init__(self, name=None):
StorableObject.__init__(self)
self.name = name
@ -110,6 +111,14 @@ class Category(XmlStorableObject):
return htmltext(text)
class CardDefCategory(Category):
_names = 'carddef_categories'
xml_root_node = 'carddef_category'
# declarations for serialization
XML_NODES = [('name', 'str'), ('url_name', 'str'), ('description', 'str'), ('position', 'int')]
Substitutions.register('category_name', category=N_('General'), comment=N_('Category Name'))
Substitutions.register('category_description', category=N_('General'), comment=N_('Category Description'))
Substitutions.register('category_id', category=N_('General'), comment=N_('Category Identifier'))

View File

@ -142,6 +142,8 @@ class FormDef(StorableObject):
'always_advertise', 'include_download_all_button',
'has_captcha', 'skip_from_360_view']
category_class = Category
def __init__(self, *args, **kwargs):
super(FormDef, self).__init__(*args, **kwargs)
self.fields = []
@ -416,7 +418,7 @@ class FormDef(StorableObject):
def get_category(self):
if self.category_id:
try:
return Category.get(self.category_id)
return self.category_class.get(self.category_id)
except KeyError:
return None
else:
@ -1147,11 +1149,11 @@ class FormDef(StorableObject):
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 Category.has_key(category_id):
if cls.category_class.has_key(category_id):
formdef.category_id = category_id
else:
category = xml_node_text(category_node)
for c in Category.select():
for c in cls.category_class.select():
if c.name == category:
formdef.category_id = c.id
break

View File

@ -4,9 +4,13 @@
{% block appbar-title %}{% trans "Cards" %}{% endblock %}
{% block content %}
<ul class="objects-list single-links">
{% for carddef in view.get_carddefs %}
<li><a href="{{ carddef.url_name }}/">{{ carddef.name }}</a></li>
{% regroup view.get_carddefs by category.id as category_list %}
<ul class="biglist">
{% for category_id, object_list in category_list %}
{% if not forloop.first or category_id %}<li><h3>{{ object_list.0.category.name|default:_('Misc') }}</h3></li>{% endif %}
{% for carddef in object_list %}
<li><a href="{{ carddef.url_name }}/">{{ carddef.name }}</a></li>
{% endfor %}
{% endfor %}
</ul>
{% endblock %}