Projet

Général

Profil

0002-cards-add-category-48111.patch

Lauréline Guérin, 03 novembre 2020 14:54

Télécharger (54,6 ko)

Voir les différences:

Subject: [PATCH 2/2] cards: add category (#48111)

 tests/admin_pages/test_all.py                 |   7 +-
 tests/admin_pages/test_card.py                |  81 +++++++
 tests/admin_pages/test_carddefcategory.py     | 212 ++++++++++++++++++
 tests/backoffice_pages/test_all.py            |  51 ++++-
 tests/test_api.py                             |  17 +-
 tests/test_carddef.py                         |  58 +++++
 tests/test_categories.py                      |  98 ++++----
 wcs/admin/categories.py                       |  76 +++++--
 wcs/admin/forms.py                            |  89 +++-----
 wcs/admin/settings.py                         |   7 +-
 wcs/api.py                                    |   2 +
 wcs/backoffice/cards.py                       |  44 ++--
 wcs/backoffice/data_management.py             |  17 +-
 wcs/carddef.py                                |   3 +
 wcs/categories.py                             |  17 +-
 wcs/formdef.py                                |   8 +-
 .../wcs/backoffice/data-management.html       |  10 +-
 17 files changed, 640 insertions(+), 157 deletions(-)
 create mode 100644 tests/admin_pages/test_carddefcategory.py
tests/admin_pages/test_all.py
23 23
from wcs.qommon.http_request import HTTPRequest
24 24
from wcs.qommon.template import get_current_theme
25 25
from wcs.admin.settings import UserFieldsFormDef
26
from wcs.categories import Category
26
from wcs.categories import Category, CardDefCategory
27 27
from wcs.data_sources import NamedDataSource
28 28
from wcs.wscalls import NamedWsCall
29 29
from wcs.roles import Role
......
641 641
        Workflow.wipe()
642 642
        Role.wipe()
643 643
        Category.wipe()
644
        CardDefCategory.wipe()
644 645
        NamedDataSource.wipe()
645 646
        NamedWsCall.wipe()
646 647

  
......
672 673
    carddef.name = 'bar'
673 674
    carddef.store()
674 675
    Category(name='baz').store()
676
    CardDefCategory(name='foobar').store()
675 677
    Role(name='qux').store()
676 678
    NamedDataSource(name='quux').store()
677 679
    NamedWsCall(name='corge').store()
......
708 710
    assert 'models/export_to_model-1.upload' not in filelist
709 711
    assert 'roles/1' in filelist
710 712
    assert 'categories/1' in filelist
713
    assert 'carddef_categories/1' in filelist
711 714
    assert 'datasources/1' in filelist
712 715
    assert 'wscalls/corge' in filelist
713 716
    for filename in filelist:
714
        assert not '.indexes' in filename
717
        assert '.indexes' not in filename
715 718

  
716 719
    wipe()
717 720
    assert FormDef.count() == 0
tests/admin_pages/test_card.py
4 4

  
5 5
from wcs import fields
6 6
from wcs.admin.settings import UserFieldsFormDef
7
from wcs.categories import CardDefCategory
7 8
from wcs.carddef import CardDef
8 9
from wcs.formdef import FormDef
9 10
from wcs.qommon.http_request import HTTPRequest
......
42 43
    clean_temporary_pub()
43 44

  
44 45

  
46
def test_cards_list(pub, studio):
47
    create_superuser(pub)
48

  
49
    CardDef.wipe()
50
    carddef = CardDef()
51
    carddef.name = 'card title'
52
    carddef.fields = []
53
    carddef.store()
54

  
55
    carddef2 = CardDef()
56
    carddef2.name = 'card title 2'
57
    carddef2.fields = []
58
    carddef2.store()
59

  
60
    CardDefCategory.wipe()
61
    cat = CardDefCategory(name='Foo')
62
    cat.store()
63
    cat2 = CardDefCategory(name='Bar')
64
    cat2.store()
65

  
66
    app = login(get_app(pub))
67
    resp = app.get('/backoffice/cards/')
68
    assert '<h2>Misc</h2>' not in resp.text
69
    assert '<h2>Foo</h2>' not in resp.text
70
    assert '<h2>Bar</h2>' not in resp.text
71

  
72
    carddef.category = cat2
73
    carddef.store()
74
    resp = app.get('/backoffice/cards/')
75
    assert '<h2>Misc</h2>' in resp.text
76
    assert '<h2>Foo</h2>' not in resp.text
77
    assert '<h2>Bar</h2>' in resp.text
78

  
79
    carddef2.category = cat
80
    carddef2.store()
81
    resp = app.get('/backoffice/cards/')
82
    assert '<h2>Misc</h2>' not in resp.text
83
    assert '<h2>Foo</h2>' in resp.text
84
    assert '<h2>Bar</h2>' in resp.text
85

  
86

  
45 87
def test_cards_new(pub, studio):
46 88
    CardDef.wipe()
47 89
    create_superuser(pub)
......
197 239
    assert 'Existing cards will be updated in the background.' not in resp.text
198 240

  
199 241

  
242
def test_card_category(pub, studio):
243
    create_superuser(pub)
244

  
245
    CardDef.wipe()
246
    carddef = CardDef()
247
    carddef.name = 'card title'
248
    carddef.fields = []
249
    carddef.store()
250

  
251
    CardDefCategory.wipe()
252
    cat = CardDefCategory(name='Foo')
253
    cat.store()
254
    cat = CardDefCategory(name='Bar')
255
    cat.store()
256

  
257
    app = login(get_app(pub))
258
    resp = app.get('/backoffice/cards/1/')
259
    assert '<span class="label">Category</span> <span class="value">None</span>' in resp.text
260
    assert '<span class="label">Category</span> <span class="value">Foo</span>' not in resp.text
261
    assert '<span class="label">Category</span> <span class="value">Bar</span>' not in resp.text
262
    resp = resp.click(href='category')
263
    resp.forms[0].submit('cancel')
264
    assert CardDef.get(carddef.id).category_id is None
265

  
266
    resp = app.get('/backoffice/cards/1/')
267
    assert '<span class="label">Category</span> <span class="value">None</span>' in resp.text
268
    assert '<span class="label">Category</span> <span class="value">Foo</span>' not in resp.text
269
    assert '<span class="label">Category</span> <span class="value">Bar</span>' not in resp.text
270
    resp = resp.click(href='category')
271
    resp.forms[0]['category_id'] = cat.id
272
    resp.forms[0].submit('submit')
273
    assert CardDef.get(carddef.id).category_id == cat.id
274

  
275
    resp = app.get('/backoffice/cards/1/')
276
    assert '<span class="label">Category</span> <span class="value">None</span>' not in resp.text
277
    assert '<span class="label">Category</span> <span class="value">Foo</span>' not in resp.text
278
    assert '<span class="label">Category</span> <span class="value">Bar</span>' in resp.text
279

  
280

  
200 281
def test_card_custom_view_data_source(pub, studio):
201 282
    user = create_superuser(pub)
202 283
    Role.wipe()
tests/admin_pages/test_carddefcategory.py
1
# -*- coding: utf-8 -*-
2

  
3
import pytest
4

  
5
from wcs.qommon.http_request import HTTPRequest
6
from wcs.carddef import CardDef
7
from wcs.categories import CardDefCategory
8

  
9
from utilities import get_app, login, create_temporary_pub, clean_temporary_pub
10
from .test_all import create_superuser
11

  
12

  
13
def pytest_generate_tests(metafunc):
14
    if 'pub' in metafunc.fixturenames:
15
        metafunc.parametrize('pub', ['pickle', 'sql', 'pickle-templates'], indirect=True)
16

  
17

  
18
@pytest.fixture
19
def pub(request):
20
    pub = create_temporary_pub(
21
            sql_mode=bool('sql' in request.param),
22
            templates_mode=bool('templates' in request.param)
23
            )
24

  
25
    req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
26
    pub.set_app_dir(req)
27
    pub.cfg['identification'] = {'methods': ['password']}
28
    pub.cfg['language'] = {'language': 'en'}
29
    pub.write_cfg()
30

  
31
    return pub
32

  
33

  
34
def teardown_module(module):
35
    clean_temporary_pub()
36

  
37

  
38
def test_categories(pub):
39
    create_superuser(pub)
40
    app = login(get_app(pub))
41
    app.get('/backoffice/cards/categories/')
42

  
43

  
44
def test_categories_new(pub):
45
    create_superuser(pub)
46
    CardDefCategory.wipe()
47
    app = login(get_app(pub))
48

  
49
    # go to the page and cancel
50
    resp = app.get('/backoffice/cards/categories/')
51
    resp = resp.click('New Category')
52
    resp = resp.forms[0].submit('cancel')
53
    assert resp.location == 'http://example.net/backoffice/cards/categories/'
54

  
55
    # go to the page and add a category
56
    resp = app.get('/backoffice/cards/categories/')
57
    resp = resp.click('New Category')
58
    resp.forms[0]['name'] = 'a new category'
59
    resp.forms[0]['description'] = 'description of the category'
60
    resp = resp.forms[0].submit('submit')
61
    assert resp.location == 'http://example.net/backoffice/cards/categories/'
62
    resp = resp.follow()
63
    assert 'a new category' in resp.text
64
    resp = resp.click('a new category')
65
    assert '<h2>a new category' in resp.text
66

  
67
    assert CardDefCategory.get(1).name == 'a new category'
68
    assert CardDefCategory.get(1).description == 'description of the category'
69

  
70

  
71
def test_categories_edit(pub):
72
    create_superuser(pub)
73
    CardDefCategory.wipe()
74
    category = CardDefCategory(name='foobar')
75
    category.store()
76

  
77
    app = login(get_app(pub))
78
    resp = app.get('/backoffice/cards/categories/1/')
79
    assert 'no card model associated to this category' in resp.text
80

  
81
    resp = resp.click(href='edit')
82
    assert resp.forms[0]['name'].value == 'foobar'
83
    resp.forms[0]['description'] = 'category description'
84
    resp = resp.forms[0].submit('submit')
85
    assert resp.location == 'http://example.net/backoffice/cards/categories/'
86
    resp = resp.follow()
87
    resp = resp.click('foobar')
88
    assert '<h2>foobar' in resp.text
89

  
90
    assert CardDefCategory.get(1).description == 'category description'
91

  
92

  
93
def test_categories_edit_duplicate_name(pub):
94
    CardDefCategory.wipe()
95
    category = CardDefCategory(name='foobar')
96
    category.store()
97
    category = CardDefCategory(name='foobar2')
98
    category.store()
99

  
100
    app = login(get_app(pub))
101
    resp = app.get('/backoffice/cards/categories/1/')
102

  
103
    resp = resp.click(href='edit')
104
    assert resp.forms[0]['name'].value == 'foobar'
105
    resp.forms[0]['name'] = 'foobar2'
106
    resp = resp.forms[0].submit('submit')
107
    assert 'This name is already used' in resp.text
108

  
109
    resp = resp.forms[0].submit('cancel')
110
    assert resp.location == 'http://example.net/backoffice/cards/categories/'
111

  
112

  
113
def test_categories_with_carddefs(pub):
114
    CardDefCategory.wipe()
115
    category = CardDefCategory(name='foobar')
116
    category.store()
117

  
118
    CardDef.wipe()
119
    app = login(get_app(pub))
120
    resp = app.get('/backoffice/cards/categories/1/')
121
    assert 'form bar' not in resp.text
122

  
123
    formdef = CardDef()
124
    formdef.name = 'form bar'
125
    formdef.fields = []
126
    formdef.category_id = category.id
127
    formdef.store()
128

  
129
    resp = app.get('/backoffice/cards/categories/1/')
130
    assert 'form bar' in resp.text
131
    assert 'no card model associated to this category' not in resp.text
132

  
133

  
134
def test_categories_delete(pub):
135
    CardDefCategory.wipe()
136
    category = CardDefCategory(name='foobar')
137
    category.store()
138

  
139
    CardDef.wipe()
140
    app = login(get_app(pub))
141
    resp = app.get('/backoffice/cards/categories/1/')
142

  
143
    resp = resp.click(href='delete')
144
    resp = resp.forms[0].submit('cancel')
145
    assert resp.location == 'http://example.net/backoffice/cards/categories/1/'
146
    assert CardDefCategory.count() == 1
147

  
148
    resp = app.get('/backoffice/cards/categories/1/')
149
    resp = resp.click(href='delete')
150
    resp = resp.forms[0].submit()
151
    assert resp.location == 'http://example.net/backoffice/cards/categories/'
152
    resp = resp.follow()
153
    assert CardDefCategory.count() == 0
154

  
155

  
156
def test_categories_edit_description(pub):
157
    CardDefCategory.wipe()
158
    category = CardDefCategory(name='foobar')
159
    category.description = 'category description'
160
    category.store()
161

  
162
    app = login(get_app(pub))
163
    # this URL is used for editing from the frontoffice, there's no link
164
    # pointing to it in the admin.
165
    resp = app.get('/backoffice/cards/categories/1/description')
166
    assert resp.forms[0]['description'].value == 'category description'
167
    resp.forms[0]['description'] = 'updated description'
168

  
169
    # check cancel doesn't save the change
170
    resp2 = resp.forms[0].submit('cancel')
171
    assert resp2.location == 'http://example.net/backoffice/cards/categories/1/'
172
    assert CardDefCategory.get(1).description == 'category description'
173

  
174
    # check submit does it properly
175
    resp2 = resp.forms[0].submit('submit')
176
    assert resp2.location == 'http://example.net/backoffice/cards/categories/1/'
177
    resp2 = resp2.follow()
178
    assert CardDefCategory.get(1).description == 'updated description'
179

  
180

  
181
def test_categories_new_duplicate_name(pub):
182
    CardDefCategory.wipe()
183
    category = CardDefCategory(name='foobar')
184
    category.store()
185

  
186
    app = login(get_app(pub))
187
    resp = app.get('/backoffice/cards/categories/')
188
    resp = resp.click('New Category')
189
    resp.forms[0]['name'] = 'foobar'
190
    resp = resp.forms[0].submit('submit')
191
    assert 'This name is already used' in resp.text
192

  
193

  
194
def test_categories_reorder(pub):
195
    CardDefCategory.wipe()
196
    category = CardDefCategory(name='foo')
197
    category.store()
198
    category = CardDefCategory(name='bar')
199
    category.store()
200
    category = CardDefCategory(name='baz')
201
    category.store()
202

  
203
    app = login(get_app(pub))
204
    app.get('/backoffice/cards/categories/update_order?order=1;2;3;')
205
    categories = CardDefCategory.select()
206
    CardDefCategory.sort_by_position(categories)
207
    assert [x.id for x in categories] == ['1', '2', '3']
208

  
209
    app.get('/backoffice/cards/categories/update_order?order=3;1;2;')
210
    categories = CardDefCategory.select()
211
    CardDefCategory.sort_by_position(categories)
212
    assert [x.id for x in categories] == ['3', '1', '2']
tests/backoffice_pages/test_all.py
46 46
from wcs.wf.create_formdata import CreateFormdataWorkflowStatusItem, Mapping
47 47
from wcs.wf.create_carddata import CreateCarddataWorkflowStatusItem
48 48
from wcs.carddef import CardDef
49
from wcs.categories import Category
49
from wcs.categories import Category, CardDefCategory
50 50
from wcs.formdef import FormDef
51 51
from wcs.logged_errors import LoggedError
52 52
from wcs import fields
......
6144 6144
    user = create_user(pub)
6145 6145
    app = login(get_app(pub))
6146 6146
    resp = app.get('/backoffice/')
6147
    assert not 'Cards' in resp.text
6147
    assert 'Cards' not in resp.text
6148 6148
    carddef = CardDef()
6149 6149
    carddef.name = 'foo'
6150 6150
    carddef.fields = [
......
6156 6156
    carddef.data_class().wipe()
6157 6157

  
6158 6158
    resp = app.get('/backoffice/')
6159
    assert not 'Cards' in resp.text
6159
    assert 'Cards' not in resp.text
6160 6160

  
6161 6161
    carddef.backoffice_submission_roles = user.roles
6162 6162
    carddef.store()
......
6212 6212
    assert resp.text.count('<tr') == 2  # header + row of data
6213 6213

  
6214 6214

  
6215
def test_carddata_management_categories(pub, studio):
6216
    user = create_user(pub)
6217

  
6218
    CardDef.wipe()
6219
    carddef = CardDef()
6220
    carddef.name = 'foo'
6221
    carddef.fields = []
6222
    carddef.backoffice_submission_roles = None
6223
    carddef.workflow_roles = {'_editor': user.roles[0]}
6224
    carddef.store()
6225

  
6226
    carddef2 = CardDef()
6227
    carddef2.name = 'card title 2'
6228
    carddef2.fields = []
6229
    carddef2.backoffice_submission_roles = None
6230
    carddef2.workflow_roles = {'_editor': user.roles[0]}
6231
    carddef2.store()
6232

  
6233
    CardDefCategory.wipe()
6234
    cat = CardDefCategory(name='Foo')
6235
    cat.store()
6236
    cat2 = CardDefCategory(name='Bar')
6237
    cat2.store()
6238

  
6239
    app = login(get_app(pub))
6240
    resp = app.get('/backoffice/data/')
6241
    assert '<h3>Misc</h3>' not in resp.text
6242
    assert '<h3>Foo</h3>' not in resp.text
6243
    assert '<h3>Bar</h3>' not in resp.text
6244

  
6245
    carddef.category = cat2
6246
    carddef.store()
6247
    resp = app.get('/backoffice/data/')
6248
    assert '<h3>Misc</h3>' in resp.text
6249
    assert '<h3>Foo</h3>' not in resp.text
6250
    assert '<h3>Bar</h3>' in resp.text
6251

  
6252
    carddef2.category = cat
6253
    carddef2.store()
6254
    resp = app.get('/backoffice/data/')
6255
    assert '<h3>Misc</h3>' not in resp.text
6256
    assert '<h3>Foo</h3>' in resp.text
6257
    assert '<h3>Bar</h3>' in resp.text
6258

  
6259

  
6215 6260
def test_studio_card_item_link(pub, studio):
6216 6261
    user = create_user(pub)
6217 6262
    CardDef.wipe()
tests/test_api.py
31 31
from wcs.carddef import CardDef
32 32
from wcs.formdef import FormDef
33 33
from wcs.formdata import Evolution
34
from wcs.categories import Category
34
from wcs.categories import Category, CardDefCategory
35 35
from wcs.data_sources import NamedDataSource
36 36
from wcs.workflows import Workflow, EditableWorkflowStatusItem, WorkflowBackofficeFieldsFormDef, WorkflowVariablesFieldsFormDef
37 37
from wcs.wf.jump import JumpWorkflowStatusItem
......
3312 3312
    local_user.roles = [role.id]
3313 3313
    local_user.store()
3314 3314

  
3315
    CardDefCategory.wipe()
3316
    category = CardDefCategory()
3317
    category.name = 'Category A'
3318
    category.store()
3319

  
3315 3320
    CardDef.wipe()
3316 3321
    carddef = CardDef()
3317 3322
    carddef.name = 'test'
......
3355 3360
    resp = get_app(pub).get(sign_uri('/api/cards/@list'))
3356 3361
    assert len(resp.json['data']) == 1
3357 3362
    assert resp.json['data'][0]['slug'] == 'test'
3363
    assert resp.json['data'][0]['category_slug'] is None
3364
    assert resp.json['data'][0]['category_name'] is None
3358 3365
    assert resp.json['data'][0]['custom_views'] == [
3359 3366
        {'id': 'datasource-carddef-custom-view', 'text': 'datasource carddef custom view'},
3360 3367
        {'id': 'shared-carddef-custom-view', 'text': 'shared carddef custom view'},
3361 3368
    ]
3362 3369

  
3370
    carddef.category = category
3371
    carddef.store()
3372
    resp = get_app(pub).get(sign_uri('/api/cards/@list'))
3373
    assert len(resp.json['data']) == 1
3374
    assert resp.json['data'][0]['slug'] == 'test'
3375
    assert resp.json['data'][0]['category_slug'] == 'category-a'
3376
    assert resp.json['data'][0]['category_name'] == 'Category A'
3377

  
3363 3378
    resp = get_app(pub).get(sign_uri('/api/cards/test/list'), status=403)
3364 3379

  
3365 3380
    resp = get_app(pub).get(sign_uri(
tests/test_carddef.py
4 4
from django.utils.six import BytesIO
5 5

  
6 6
from wcs.qommon.http_request import HTTPRequest
7
from wcs.qommon.misc import indent_xml as indent
7 8
from wcs.qommon.template import Template
9
from wcs.categories import CardDefCategory
8 10
from wcs.carddef import CardDef
9 11
from wcs.fields import ItemField
10 12
from wcs.fields import StringField
......
31 33
    clean_temporary_pub()
32 34

  
33 35

  
36
def export_to_indented_xml(carddef, include_id=False):
37
    carddef_xml = ET.fromstring(ET.tostring(carddef.export_to_xml(include_id=include_id)))
38
    indent(carddef_xml)
39
    return carddef_xml
40

  
41

  
42
def assert_compare_carddef(carddef1, carddef2, include_id=False):
43
    assert (
44
        ET.tostring(export_to_indented_xml(carddef1, include_id=include_id)) ==
45
        ET.tostring(export_to_indented_xml(carddef2, include_id=include_id)))
46
    assert (
47
        carddef1.export_to_json(include_id=include_id, indent=2) ==
48
        carddef2.export_to_json(include_id=include_id, indent=2))
49

  
50

  
51
def assert_xml_import_export_works(carddef, include_id=False):
52
    carddef_xml = carddef.export_to_xml(include_id=include_id)
53
    carddef2 = CardDef.import_from_xml_tree(carddef_xml, include_id=include_id)
54
    assert_compare_carddef(carddef, carddef2, include_id=include_id)
55
    return carddef2
56

  
57

  
34 58
def test_basics(pub):
35 59
    carddef = CardDef()
36 60
    carddef.name = 'foo'
......
159 183
    assert custom_views[1].formdef_type == 'carddef'
160 184

  
161 185

  
186
def test_xml_export_import_category_reference(pub):
187
    CardDefCategory.wipe()
188
    CardDef.wipe()
189

  
190
    cat = CardDefCategory()
191
    cat.name = 'test category'
192
    cat.store()
193

  
194
    carddef = CardDef()
195
    carddef.name = 'foo'
196
    carddef.category_id = cat.id
197
    f2 = assert_xml_import_export_works(carddef)
198
    assert f2.category_id == carddef.category_id
199

  
200
    f2 = assert_xml_import_export_works(carddef, include_id=True)
201
    assert f2.category_id == carddef.category_id
202

  
203
    carddef_xml_with_id = carddef.export_to_xml(include_id=True)
204

  
205
    # check there's no reference to a non-existing category
206
    CardDefCategory.wipe()
207
    assert CardDef.import_from_xml_tree(carddef_xml_with_id, include_id=False).category_id is None
208
    assert CardDef.import_from_xml_tree(carddef_xml_with_id, include_id=True).category_id is None
209

  
210
    # check an import that is not using id fields will find the category by its
211
    # name
212
    cat = CardDefCategory()
213
    cat.id = '2'
214
    cat.name = 'test category'
215
    cat.store()
216
    assert CardDef.import_from_xml_tree(carddef_xml_with_id, include_id=False).category_id == '2'
217
    assert CardDef.import_from_xml_tree(carddef_xml_with_id, include_id=True).category_id is None
218

  
219

  
162 220
def test_template_access(pub):
163 221
    CardDef.wipe()
164 222
    carddef = CardDef()
tests/test_categories.py
6 6

  
7 7
from django.utils.six import BytesIO
8 8
from quixote import cleanup
9
from wcs import publisher
10 9

  
11
from wcs.categories import Category
10
from wcs.categories import Category, CardDefCategory
12 11

  
13 12
from utilities import create_temporary_pub
14 13

  
......
25 24
    shutil.rmtree(pub.APP_DIR)
26 25

  
27 26

  
28
def test_store():
29
    Category.wipe()
30
    test = Category()
27
@pytest.mark.parametrize('category_class', [Category, CardDefCategory])
28
def test_store(category_class):
29
    category_class.wipe()
30
    test = category_class()
31 31
    test.name = 'Test'
32 32
    test.description = 'Hello world'
33 33
    test.store()
34
    test2 = Category.get(1)
34
    test2 = category_class.get(1)
35 35
    assert test.id == test2.id
36 36
    assert test.name == test2.name
37 37
    assert test.description == test2.description
38 38

  
39 39

  
40
def test_urlname():
41
    Category.wipe()
42
    test = Category()
40
@pytest.mark.parametrize('category_class', [Category, CardDefCategory])
41
def test_urlname(category_class):
42
    category_class.wipe()
43
    test = category_class()
43 44
    test.name = 'Test'
44 45
    test.description = 'Hello world'
45 46
    test.store()
46
    test = Category.get(1)
47
    test = category_class.get(1)
47 48
    assert test.url_name == 'test'
48 49

  
49 50

  
50
def test_duplicate_urlname():
51
    Category.wipe()
52
    test = Category()
51
@pytest.mark.parametrize('category_class', [Category, CardDefCategory])
52
def test_duplicate_urlname(category_class):
53
    category_class.wipe()
54
    test = category_class()
53 55
    test.name = 'Test'
54 56
    test.store()
55
    test = Category.get(1)
57
    test = category_class.get(1)
56 58
    assert test.url_name == 'test'
57 59

  
58
    test2 = Category()
60
    test2 = category_class()
59 61
    test2.name = 'Test'
60 62
    test2.store()
61
    test2 = Category.get(2)
63
    test2 = category_class.get(2)
62 64
    assert test2.url_name == 'test-2'
63 65

  
64 66

  
65
def test_sort_positions():
66
    Category.wipe()
67
@pytest.mark.parametrize('category_class', [Category, CardDefCategory])
68
def test_sort_positions(category_class):
69
    category_class.wipe()
67 70

  
68 71
    categories = []
69 72
    for i in range(10):
70
        test = Category()
73
        test = category_class()
71 74
        test.name = 'Test %s' % i
72 75
        test.position = 10-i
73 76
        categories.append(test)
......
76 79
    for i in range(8, 10):
77 80
        categories[i].position = None
78 81

  
79
    Category.sort_by_position(categories)
82
    category_class.sort_by_position(categories)
80 83
    assert categories[0].name == 'Test 7'
81 84
    assert categories[-1].name in ('Test 8', 'Test 9')
82 85

  
83 86

  
84
def test_xml_export():
85
    Category.wipe()
86
    test = Category()
87
@pytest.mark.parametrize('category_class', [Category, CardDefCategory])
88
def test_xml_export(category_class):
89
    category_class.wipe()
90
    test = category_class()
87 91
    test.id = 1
88 92
    test.name = 'Test'
89 93
    test.description = 'Hello world'
90 94
    test.store()
91
    test = Category.get(1)
95
    test = category_class.get(1)
92 96

  
93 97
    assert b'<name>Test</name>' in test.export_to_xml_string(include_id=True)
94 98
    assert b' id="1"' in test.export_to_xml_string(include_id=True)
95 99
    assert b' id="1"' not in test.export_to_xml_string(include_id=False)
96 100

  
97 101

  
98
def test_xml_import():
99
    Category.wipe()
100
    test = Category()
102
@pytest.mark.parametrize('category_class', [Category, CardDefCategory])
103
def test_xml_import(category_class):
104
    category_class.wipe()
105
    test = category_class()
101 106
    test.name = 'Test'
102 107
    test.description = 'Hello world'
103 108
    test.store()
104
    test = Category.get(1)
109
    test = category_class.get(1)
105 110

  
106 111
    fd = BytesIO(test.export_to_xml_string(include_id=True))
107
    test2 = Category.import_from_xml(fd, include_id=True)
112
    test2 = category_class.import_from_xml(fd, include_id=True)
108 113
    assert test.id == test2.id
109 114
    assert test.name == test2.name
110 115
    assert test.description == test2.description
......
129 134
    assert test.description == test2.description
130 135

  
131 136

  
132
def test_get_by_urlname():
133
    Category.wipe()
134
    test = Category()
137
@pytest.mark.parametrize('category_class', [Category, CardDefCategory])
138
def test_get_by_urlname(category_class):
139
    category_class.wipe()
140
    test = category_class()
135 141
    test.id = 1
136 142
    test.name = 'Test'
137 143
    test.description = 'Hello world'
138 144
    test.store()
139
    test = Category.get(1)
140
    test2 = Category.get_by_urlname('test')
145
    test = category_class.get(1)
146
    test2 = category_class.get_by_urlname('test')
141 147
    assert test.id == test2.id
142 148

  
143 149

  
144
def test_has_urlname():
145
    Category.wipe()
146
    test = Category()
150
@pytest.mark.parametrize('category_class', [Category, CardDefCategory])
151
def test_has_urlname(category_class):
152
    category_class.wipe()
153
    test = category_class()
147 154
    test.id = 1
148 155
    test.name = 'Test'
149 156
    test.description = 'Hello world'
150 157
    test.store()
151
    test = Category.get(1)
158
    test = category_class.get(1)
152 159

  
153
    assert Category.has_urlname('test')
154
    assert not Category.has_urlname('foobar')
160
    assert category_class.has_urlname('test')
161
    assert not category_class.has_urlname('foobar')
155 162

  
156 163

  
157
def test_remove_self():
158
    Category.wipe()
159
    test = Category()
164
@pytest.mark.parametrize('category_class', [Category, CardDefCategory])
165
def test_remove_self(category_class):
166
    category_class.wipe()
167
    test = category_class()
160 168
    test.id = 1
161 169
    test.name = 'Test'
162 170
    test.description = 'Hello world'
163 171
    test.store()
164
    test = Category.get(1)
172
    test = category_class.get(1)
165 173
    test.remove_self()
166 174

  
167 175
    with pytest.raises(KeyError):
168
        Category.get(1)
176
        category_class.get(1)
wcs/admin/categories.py
18 18
from quixote.directory import Directory
19 19
from quixote.html import TemplateIO, htmltext
20 20

  
21
from wcs.qommon import _
22
from wcs.categories import Category
21
from wcs.qommon import _, N_
22
from wcs.categories import Category, CardDefCategory
23 23
from wcs.qommon.form import *
24 24
from wcs.qommon.backoffice.menu import html_top
25 25

  
26
from wcs.carddef import CardDef
26 27
from wcs.formdef import FormDef
27 28

  
28 29

  
29 30
class CategoryUI(object):
31
    category_class = Category
32

  
30 33
    def __init__(self, category):
31 34
        self.category = category
32 35
        if self.category is None:
33
            self.category = Category()
36
            self.category = self.category_class()
34 37

  
35 38
    def get_form(self):
36 39
        form = Form(enctype='multipart/form-data')
......
39 42
        form.add(WysiwygTextWidget, 'description', title=_('Description'),
40 43
                cols=80, rows=10,
41 44
                value=self.category.description)
42
        form.add(StringWidget, 'redirect_url', size=32,
43
                title=_('URL Redirection'),
44
                hint=_('If set, redirect the site category page to the given URL.'),
45
                value=self.category.redirect_url)
45
        if self.category_class == Category:
46
            form.add(StringWidget, 'redirect_url', size=32,
47
                    title=_('URL Redirection'),
48
                    hint=_('If set, redirect the site category page to the given URL.'),
49
                    value=self.category.redirect_url)
46 50
        form.add_submit('submit', _('Submit'))
47 51
        form.add_submit('cancel', _('Cancel'))
48 52
        return form
......
51 55
        self.category.name = form.get_widget('name').parse()
52 56

  
53 57
        name = form.get_widget('name').parse()
54
        category_names = [x.name for x in Category.select() if x.id != self.category.id]
58
        category_names = [x.name for x in self.category_class.select() if x.id != self.category.id]
55 59
        if name in category_names:
56 60
            form.get_widget('name').set_error(_('This name is already used'))
57 61
            raise ValueError()
58 62

  
59 63
        self.category.description = form.get_widget('description').parse()
60
        self.category.redirect_url = form.get_widget('redirect_url').parse()
64
        if form.get_widget('redirect_url'):
65
            self.category.redirect_url = form.get_widget('redirect_url').parse()
61 66
        self.category.store()
62 67

  
63 68

  
69
class CardDefCategoryUI(CategoryUI):
70
    category_class = CardDefCategory
71

  
72

  
64 73
class CategoryPage(Directory):
74
    category_class = Category
75
    category_ui_class = CategoryUI
76
    formdef_class = FormDef
77
    usage_title = N_('Forms in this category')
78
    empty_message = N_('no form associated to this category')
65 79
    _q_exports = ['', 'edit', 'delete', 'description']
66 80

  
67 81
    def __init__(self, component):
68
        self.category = Category.get(component)
69
        self.category_ui = CategoryUI(self.category)
82
        self.category = self.category_class.get(component)
83
        self.category_ui = self.category_ui_class(self.category)
70 84
        get_response().breadcrumb.append((component + '/', self.category.name))
71 85

  
72 86
    def _q_index(self):
......
86 100
            r += self.category.get_description_html_text()
87 101
            r += htmltext('</div>')
88 102

  
89
        formdefs = FormDef.select(order_by='name')
103
        formdefs = self.formdef_class.select(order_by='name')
90 104
        formdefs = [x for x in formdefs if x.category_id == self.category.id]
91 105
        r += htmltext('<div class="bo-block">')
92
        r += htmltext('<h3>%s</h3>') % _('Forms in this category')
106
        r += htmltext('<h3>%s</h3>') % _(self.usage_title)
93 107
        r += htmltext('<ul>')
94 108
        for formdef in formdefs:
95 109
            r += htmltext('<li><a href="../../%s/">') % str(formdef.id)
96 110
            r += formdef.name
97 111
            r += htmltext('</a></li>')
98 112
        if not formdefs:
99
            r += htmltext('<li>%s</li>') % _('no form associated to this category')
113
            r += htmltext('<li>%s</li>') % _(self.empty_message)
100 114
        r += htmltext('</ul>')
101 115
        r += htmltext('</div>')
102 116
        return r.getvalue()
......
165 179
        return r.getvalue()
166 180

  
167 181

  
182
class CardDefCategoryPage(CategoryPage):
183
    category_class = CardDefCategory
184
    category_ui_class = CardDefCategoryUI
185
    formdef_class = CardDef
186
    usage_title = N_('Card models in this category')
187
    empty_message = N_('no card model associated to this category')
188

  
189

  
168 190
class CategoriesDirectory(Directory):
169 191
    _q_exports = ['', 'new', 'update_order']
192
    category_class = Category
193
    category_ui_class = CategoryUI
194
    category_page_class = CategoryPage
195
    category_explanation = N_('Categories are used to sort the different forms.')
170 196

  
171 197
    def _q_index(self):
172 198
        get_response().add_javascript(['jquery.js', 'jquery-ui.js', 'biglist.js',
......
180 206
        r += htmltext('<a class="new-item" href="new" rel="popup">%s</a>') % _('New Category')
181 207
        r += htmltext('</span>')
182 208
        r += htmltext('</div>')
183
        r += htmltext('<div class="explanation bo-block"><p>%s</p></div>') % \
184
                _('Categories are used to sort the different forms.')
185
        categories = Category.select()
209
        r += htmltext('<div class="explanation bo-block"><p>%s</p></div>') % _(self.category_explanation)
210
        categories = self.category_class.select()
186 211
        r += htmltext('<ul class="biglist sortable" id="category-list">')
187
        Category.sort_by_position(categories)
212
        self.category_class.sort_by_position(categories)
188 213
        for category in categories:
189 214
            r += htmltext('<li class="biglistitem" id="itemId_%s">') % category.id
190 215
            r += htmltext('<strong class="label"><a href="%s/">%s</a></strong>') % (
......
196 221
    def update_order(self):
197 222
        request = get_request()
198 223
        new_order = request.form['order'].strip(';').split(';')
199
        categories = Category.select()
224
        categories = self.category_class.select()
200 225
        dict = {}
201 226
        for l in categories:
202 227
            dict[str(l.id)] = l
......
207 232

  
208 233
    def new(self):
209 234
        get_response().breadcrumb.append( ('new', _('New')) )
210
        category_ui = CategoryUI(None)
235
        category_ui = self.category_ui_class(None)
211 236
        form = category_ui.get_form()
212 237
        if form.get_widget('cancel').parse():
213 238
            return redirect('.')
......
227 252
        return r.getvalue()
228 253

  
229 254
    def _q_lookup(self, component):
230
        return CategoryPage(component)
255
        return self.category_page_class(component)
231 256

  
232 257
    def _q_traverse(self, path):
233 258
        get_response().breadcrumb.append( ('categories/', _('Categories')) )
234
        return super(CategoriesDirectory, self)._q_traverse(path)
259
        return super()._q_traverse(path)
260

  
261

  
262
class CardDefCategoriesDirectory(CategoriesDirectory):
263
    category_class = CardDefCategory
264
    category_ui_class = CardDefCategoryUI
265
    category_page_class = CardDefCategoryPage
266
    category_explanation = N_('Categories are used to sort the different card models.')
wcs/admin/forms.py
60 60
from wcs.backoffice.snapshots import SnapshotsDirectory
61 61

  
62 62

  
63
def get_categories():
64
    t = sorted([(misc.simplify(x.name), x.id, x.name, x.id) for x in Category.select()])
63
def get_categories(category_class):
64
    t = sorted([(misc.simplify(x.name), x.id, x.name, x.id) for x in category_class.select()])
65 65
    return [x[1:] for x in t]
66 66

  
67 67

  
68 68
class FormDefUI(object):
69 69
    formdef_class = FormDef
70
    category_class = Category
70 71

  
71 72
    def __init__(self, formdef):
72 73
        self.formdef = formdef
73 74

  
74 75
    def get_categories(self):
75
        return get_categories()
76
        return get_categories(self.category_class)
76 77

  
77 78
    @classmethod
78 79
    def get_workflows(cls, condition=lambda x: True):
......
165 166

  
166 167

  
167 168
class OptionsDirectory(Directory):
169
    category_class = Category
170
    category_empty_choice = N_('Select a category for this form')
168 171
    _q_exports = ['confirmation', 'only_allow_one',
169 172
            'always_advertise', 'tracking_code', 'online_status', 'captcha',
170 173
            'description', 'keywords', 'category', 'management',
......
265 268
        return self.handle(form, _('Keywords'))
266 269

  
267 270
    def category(self):
268
        categories = get_categories()
271
        categories = get_categories(self.category_class)
269 272
        form = Form(enctype='multipart/form-data')
270
        form.widgets.append(HtmlWidget('<p>%s</p>' % _('Select a category for this form')))
273
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(self.category_empty_choice)))
271 274
        form.add(SingleSelectWidget, 'category_id', title=_('Category'),
272 275
               value=self.formdef.category_id,
273 276
               options=[(None, '---', '')] + categories)
......
421 424
    formdef_export_prefix = 'form'
422 425
    formdef_ui_class = FormDefUI
423 426
    formdef_default_workflow = '_default'
427
    options_directory_class = OptionsDirectory
424 428

  
425 429
    delete_message = N_('You are about to irrevocably delete this form.')
426 430
    delete_title = N_('Deleting Form:')
......
442 446
        self.fields.html_top = self.html_top
443 447
        self.role = WorkflowRoleDirectory(self.formdef)
444 448
        self.role.html_top = self.html_top
445
        self.options = OptionsDirectory(self.formdef)
449
        self.options = self.options_directory_class(self.formdef)
446 450
        self.logged_errors_dir = LoggedErrorsDirectory(parent_dir=self, formdef_id=self.formdef.id)
447 451
        self.snapshots_dir = SnapshotsDirectory(self.formdef)
448 452

  
......
714 718
        r += htmltext('</div>')
715 719
        return r.getvalue()
716 720

  
717
    def category(self):
718
        categories = get_categories()
719
        form = Form(enctype='multipart/form-data')
720
        form.add(SingleSelectWidget, 'category_id',
721
                        value=self.formdef.category_id,
722
                        options=[(None, '---', '')] + categories)
723
        if not self.formdef.is_readonly():
724
            form.add_submit('submit', _('Submit'))
725
        form.add_submit('cancel', _('Cancel'))
726
        if form.get_widget('cancel').parse():
727
            return redirect('.')
728

  
729
        if not form.is_submitted() or form.has_errors():
730
            get_response().breadcrumb.append( ('category', _('Category')) )
731
            self.html_top(title = self.formdef.name)
732
            r = TemplateIO(html=True)
733
            r += htmltext('<h2>%s</h2>') % _('Category')
734
            r += htmltext('<p>%s</p>') % _('Select a category for this form')
735
            r += form.render()
736
            return r.getvalue()
737
        else:
738
            self.formdef.category_id = form.get_widget('category_id').parse()
739
            self.formdef.store(comment=_('Change of category'))
740
            return redirect('.')
741

  
742 721
    def _roles_selection(self, title, attribute, description=None,
743 722
            include_logged_users_role=True):
744 723
        form = Form(enctype='multipart/form-data')
......
1564 1543
    _q_exports = ['', 'new', ('import', 'p_import'),
1565 1544
            'blocks', 'categories', ('data-sources', 'data_sources')]
1566 1545

  
1546
    category_class = Category
1567 1547
    categories = CategoriesDirectory()
1568 1548
    blocks = BlocksDirectory(section='forms')
1569 1549
    data_sources = NamedDataSourcesDirectoryInForms()
......
1571 1551
    formdef_page_class = FormDefPage
1572 1552
    formdef_ui_class = FormDefUI
1573 1553

  
1554
    top_title = N_('Forms')
1574 1555
    import_title = N_('Import Form')
1575 1556
    import_submit_label = N_('Import Form')
1576 1557
    import_paragraph = N_(
......
1593 1574
        return super()._q_traverse(path)
1594 1575

  
1595 1576
    def _q_index(self):
1596
        self.html_top(title = _('Forms'))
1577
        self.html_top(title=_(self.top_title))
1597 1578
        r = TemplateIO(html=True)
1598 1579
        get_response().add_javascript(['jquery.js', 'widget_list.js'])
1599
        has_roles = bool(Role.count())
1580
        r += self.form_actions()
1600 1581

  
1601
        r += htmltext('<div id="appbar">')
1602
        r += htmltext('<h2>%s</h2>') % _('Forms')
1603
        if has_roles:
1604
            r += htmltext('<span class="actions">')
1605
            r += htmltext('<a href="data-sources/">%s</a>') % _('Data sources')
1606
            if get_publisher().has_site_option('fields-blocks'):
1607
                r += htmltext('<a href="blocks/">%s</a>') % _('Fields blocks')
1608
            if get_publisher().get_backoffice_root().is_accessible('categories'):
1609
                r += htmltext('<a href="categories/">%s</a>') % _('Categories')
1610
            r += htmltext('<a href="import" rel="popup">%s</a>') % _('Import')
1611
            r += htmltext('<a class="new-item" href="new" rel="popup">%s</a>') % _('New Form')
1612
            r += htmltext('</span>')
1613
        r += htmltext('</div>')
1614

  
1615
        if not has_roles:
1616
            r += htmltext('<p>%s</p>') % _('You first have to define roles.')
1617

  
1618
        cats = Category.select()
1619
        Category.sort_by_position(cats)
1582
        cats = self.category_class.select()
1583
        self.category_class.sort_by_position(cats)
1620 1584
        one = False
1621 1585
        formdefs = self.formdef_class.select(order_by='name', ignore_errors=True, lightweight=True)
1622 1586
        for c in cats:
......
1638 1602
            r += self.form_list(l2, title = title)
1639 1603
        return r.getvalue()
1640 1604

  
1605
    def form_actions(self):
1606
        r = TemplateIO(html=True)
1607
        has_roles = bool(Role.count())
1608
        r += htmltext('<div id="appbar">')
1609
        r += htmltext('<h2>%s</h2>') % _('Forms')
1610
        if has_roles:
1611
            r += htmltext('<span class="actions">')
1612
            r += htmltext('<a href="data-sources/">%s</a>') % _('Data sources')
1613
            if get_publisher().has_site_option('fields-blocks'):
1614
                r += htmltext('<a href="blocks/">%s</a>') % _('Fields blocks')
1615
            if get_publisher().get_backoffice_root().is_accessible('categories'):
1616
                r += htmltext('<a href="categories/">%s</a>') % _('Categories')
1617
            r += htmltext('<a href="import" rel="popup">%s</a>') % _('Import')
1618
            r += htmltext('<a class="new-item" href="new" rel="popup">%s</a>') % _('New Form')
1619
            r += htmltext('</span>')
1620
        r += htmltext('</div>')
1621

  
1622
        if not has_roles:
1623
            r += htmltext('<p>%s</p>') % _('You first have to define roles.')
1624
        return r.getvalue()
1625

  
1641 1626
    def form_list(self, formdefs, title):
1642 1627
        r = TemplateIO(html=True)
1643 1628
        if title:
wcs/admin/settings.py
867 867
        if not get_cfg('sp', {}).get('idp-manage-roles'):
868 868
            form.add(CheckboxWidget, 'roles', title = _('Roles'), value = True)
869 869
        form.add(CheckboxWidget, 'categories', title = _('Categories'), value = True)
870
        form.add(CheckboxWidget, 'carddef_categories', title = _('Card Model Categories'), value = True)
870 871
        form.add(CheckboxWidget, 'settings', title = _('Settings'), value = False)
871 872
        form.add(CheckboxWidget, 'datasources', title=_('Data sources'), value=True)
872 873
        form.add(CheckboxWidget, 'mail-templates', title=_('Mail templates'), value=True)
......
894 895
                c = BytesIO()
895 896
                z = zipfile.ZipFile(c, 'w')
896 897
                for d in self.dirs:
897
                    if d not in ('roles', 'categories', 'datasources', 'wscalls', 'mail-templates'):
898
                    if d not in ('roles', 'categories', 'carddef_categories', 'datasources', 'wscalls', 'mail-templates'):
898 899
                        continue
899 900
                    path = os.path.join(self.app_dir, d)
900 901
                    if not os.path.exists(path):
......
942 943
                job.store()
943 944

  
944 945
        dirs = []
945
        for w in ('formdefs', 'carddefs', 'workflows', 'roles', 'categories',
946
                'datasources', 'wscalls', 'mail-templates', 'blockdefs'):
946
        for w in ('formdefs', 'carddefs', 'workflows', 'roles', 'categories', 'carddef_categories',
947
                  'datasources', 'wscalls', 'mail-templates', 'blockdefs'):
947 948
            if form.get_widget(w) and form.get_widget(w).parse():
948 949
                dirs.append(w)
949 950
        if not dirs and not form.get_widget('settings').parse():
wcs/api.py
342 342
                'title': x.name,
343 343
                'slug': x.url_name,
344 344
                'url': x.get_url(),
345
                'category_slug': x.category.url_name if x.category else None,
346
                'category_name': x.category.name if x.category else None,
345 347
                'description': x.description or '',
346 348
                'keywords': x.keywords_list,
347 349
                'custom_views': get_custom_views(x),
wcs/backoffice/cards.py
16 16
# You should have received a copy of the GNU General Public License
17 17
# along with this program; if not, see <http://www.gnu.org/licenses/>.
18 18

  
19
import time
20

  
21
from quixote import get_publisher, get_request, get_response, get_session, redirect
19
from quixote import get_publisher, get_response, get_session, redirect
22 20
from quixote.directory import Directory
23 21
from quixote.html import TemplateIO, htmltext
24 22

  
25
from ..qommon import _, N_, misc
23
from ..qommon import _, N_
26 24
from ..qommon.misc import C_
27 25
from ..qommon.storage import NotEqual, Null
28 26

  
29 27
from wcs.carddef import CardDef
28
from wcs.categories import CardDefCategory
30 29
from wcs.roles import Role
31 30
from wcs.workflows import Workflow
32
from wcs.admin.forms import FormsDirectory, FormDefPage, FormDefUI, html_top
31
from wcs.admin.categories import CardDefCategoriesDirectory
32
from wcs.admin.forms import FormsDirectory, FormDefPage, FormDefUI, html_top, OptionsDirectory
33 33
from wcs.admin.logged_errors import LoggedErrorsDirectory
34 34
from wcs.admin import utils
35 35

  
36 36

  
37 37
class CardDefUI(FormDefUI):
38 38
    formdef_class = CardDef
39
    category_class = CardDefCategory
40

  
39 41

  
40
    def get_categories(self):
41
        return []
42
class CardDefOptionsDirectory(OptionsDirectory):
43
    category_class = CardDefCategory
44
    category_empty_choice = N_('Select a category for this card model')
42 45

  
43 46

  
44 47
class CardDefPage(FormDefPage):
......
47 50
    formdef_ui_class = CardDefUI
48 51
    formdef_default_workflow = '_carddef_default'
49 52

  
53
    options_directory_class = CardDefOptionsDirectory
54

  
50 55
    delete_message = N_('You are about to irrevocably delete this card model.')
51 56
    delete_title = N_('Deleting Card Model:')
52 57
    overwrite_message = N_(
......
86 91
                              'label': label,
87 92
                              'current_value': current_value})
88 93

  
94
        r += htmltext('<div class="bo-block">')
95
        r += htmltext('<h3>%s</h3>') % _('Information')
96
        r += htmltext('<ul class="biglist optionslist">')
97

  
98
        r += add_option_line(
99
            'options/category', _('Category'),
100
            self.formdef.category_id and self.formdef.category and
101
            self.formdef.category.name or C_('category|None'))
102
        r += htmltext('</ul>')
103
        r += htmltext('</div>')
104

  
89 105
        r += htmltext('<div class="splitcontent-left">')
90 106
        r += htmltext('<div class="bo-block">')
91 107
        r += htmltext('<h3>%s</h3>') % _('Workflow')
......
214 230

  
215 231

  
216 232
class CardsDirectory(FormsDirectory):
217
    _q_exports = ['', 'new', ('import', 'p_import')]
233
    _q_exports = ['', 'new', ('import', 'p_import'), 'categories']
234

  
235
    category_class = CardDefCategory
236
    categories = CardDefCategoriesDirectory()
218 237
    formdef_class = CardDef
219 238
    formdef_page_class = CardDefPage
220 239
    formdef_ui_class = CardDefUI
221 240

  
241
    top_title = N_('Card Models')
222 242
    import_title = N_('Import Card Model')
223 243
    import_submit_label = N_('Import Card Model')
224 244
    import_paragraph = N_(
......
238 258
        get_response().breadcrumb.append(('cards/', _('Card Models')))
239 259
        return Directory._q_traverse(self, path)
240 260

  
241
    def _q_index(self):
242
        self.html_top(title=_('Card Models'))
261
    def form_actions(self):
243 262
        r = TemplateIO(html=True)
244

  
245 263
        r += htmltext('<div id="appbar">')
246 264
        r += htmltext('<h2>%s</h2>') % _('Card Models')
247 265
        r += htmltext('<span class="actions">')
266
        r += htmltext('<a href="categories/">%s</a>') % _('Categories')
248 267
        r += htmltext('<a href="import" rel="popup">%s</a>') % _('Import')
249 268
        r += htmltext('<a class="new-item" href="new" rel="popup">%s</a>') % _('New Card Model')
250 269
        r += htmltext('</span>')
251 270
        r += htmltext('</div>')
252

  
253
        formdefs = self.formdef_class.select(order_by='name', ignore_errors=True, lightweight=True)
254
        r += self.form_list(formdefs, title='')
255 271
        return r.getvalue()
256 272

  
257 273
    def new(self):
wcs/backoffice/data_management.py
33 33
from ..qommon.afterjobs import AfterJob
34 34

  
35 35
from wcs.carddef import CardDef
36
from wcs.categories import CardDefCategory
36 37
from wcs import fields
37 38

  
38 39
from .management import ManagementDirectory, FormPage, FormFillPage, FormBackOfficeStatusPage
......
60 61

  
61 62
    def get_carddefs(self):
62 63
        user = get_request().user
63
        if user:
64
            for formdef in CardDef.select(order_by='name', ignore_errors=True, lightweight=True):
65
                if user.is_admin or formdef.is_of_concern_for_user(user):
66
                    yield formdef
64
        if not user:
65
            return
66
        carddefs = CardDef.select(order_by='name', ignore_errors=True, lightweight=True)
67
        carddefs = [c for c in carddefs if user.is_admin or c.is_of_concern_for_user(user)]
68
        cats = CardDefCategory.select(order_by='name')
69
        for c in cats + [None]:
70
            for carddef in carddefs:
71
                if c is None and not carddef.category_id:
72
                    yield carddef
73
                if c is not None and carddef.category_id == c.id:
74
                    yield carddef
67 75

  
68 76
    def _q_index(self):
69 77
        html_top('data_management', _('Cards'))
70 78
        if CardDef.count() == 0:
71 79
            return self.empty_site_message(_('Cards'))
72

  
73 80
        return template.QommonTemplateResponse(
74 81
                templates=['wcs/backoffice/data-management.html'],
75 82
                context={'view': self})
wcs/carddef.py
23 23
from .qommon.template import Template
24 24

  
25 25
from wcs.carddata import CardData
26
from wcs.categories import CardDefCategory
26 27
from wcs.formdef import FormDef, get_formdefs_of_all_kinds
27 28

  
28 29
if not hasattr(types, 'ClassType'):
......
39 40

  
40 41
    confirmation = False
41 42

  
43
    category_class = CardDefCategory
44

  
42 45
    def data_class(self, mode=None):
43 46
        if not 'carddef' in sys.modules:
44 47
            sys.modules['carddef'] = sys.modules[__name__]
wcs/categories.py
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16 16

  
17
from quixote import get_publisher, get_response
17
from quixote import get_publisher
18 18
from quixote.html import htmltext
19 19

  
20 20
from .qommon import N_
......
34 34
    redirect_url = None
35 35

  
36 36
    # declarations for serialization
37
    XML_NODES = [('name', 'str'), ('url_name', 'str'), ('description', 'str'),
38
            ('redirect_url', 'str'), ('position', 'int')]
37
    XML_NODES = [
38
        ('name', 'str'), ('url_name', 'str'), ('description', 'str'),
39
        ('redirect_url', 'str'), ('position', 'int')]
39 40

  
40
    def __init__(self, name = None):
41
    def __init__(self, name=None):
41 42
        StorableObject.__init__(self)
42 43
        self.name = name
43 44

  
......
110 111
        return htmltext(text)
111 112

  
112 113

  
114
class CardDefCategory(Category):
115
    _names = 'carddef_categories'
116
    xml_root_node = 'carddef_category'
117

  
118
    # declarations for serialization
119
    XML_NODES = [('name', 'str'), ('url_name', 'str'), ('description', 'str'), ('position', 'int')]
120

  
121

  
113 122
Substitutions.register('category_name', category=N_('General'), comment=N_('Category Name'))
114 123
Substitutions.register('category_description', category=N_('General'), comment=N_('Category Description'))
115 124
Substitutions.register('category_id', category=N_('General'), comment=N_('Category Identifier'))
wcs/formdef.py
141 141
            'always_advertise', 'include_download_all_button',
142 142
            'has_captcha', 'skip_from_360_view']
143 143

  
144
    category_class = Category
145

  
144 146
    def __init__(self, *args, **kwargs):
145 147
        super(FormDef, self).__init__(*args, **kwargs)
146 148
        self.fields = []
......
415 417
    def get_category(self):
416 418
        if self.category_id:
417 419
            try:
418
                return Category.get(self.category_id)
420
                return self.category_class.get(self.category_id)
419 421
            except KeyError:
420 422
                return None
421 423
        else:
......
1183 1185
            category_node = tree.find('category')
1184 1186
            if include_id and category_node.attrib.get('category_id'):
1185 1187
                category_id = str(category_node.attrib.get('category_id'))
1186
                if Category.has_key(category_id):
1188
                if cls.category_class.has_key(category_id):
1187 1189
                    formdef.category_id = category_id
1188 1190
            else:
1189 1191
                category = xml_node_text(category_node)
1190
                for c in Category.select():
1192
                for c in cls.category_class.select():
1191 1193
                    if c.name == category:
1192 1194
                        formdef.category_id = c.id
1193 1195
                        break
wcs/templates/wcs/backoffice/data-management.html
4 4
{% block appbar-title %}{% trans "Cards" %}{% endblock %}
5 5

  
6 6
{% block content %}
7
<ul class="objects-list single-links">
8
  {% for carddef in view.get_carddefs %}
9
  <li><a href="{{ carddef.url_name }}/">{{ carddef.name }}</a></li>
7
{% regroup view.get_carddefs by category as category_list %}
8
<ul class="biglist">
9
  {% for category, object_list in category_list %}
10
    {% if not forloop.first or category %}<li><h3>{{ category.name|default:_('Misc') }}</h3></li>{% endif %}
11
    {% for carddef in object_list %}
12
    <li><a href="{{ carddef.url_name }}/">{{ carddef.name }}</a></li>
13
    {% endfor %}
10 14
  {% endfor %}
11 15
</ul>
12 16
{% endblock %}
13
-