Projet

Général

Profil

0001-general-add-category-based-management-access-forms-c.patch

Frédéric Péters, 10 août 2021 08:31

Télécharger (52,6 ko)

Voir les différences:

Subject: [PATCH] general: add category-based management access
 (forms/cards/workflows) (#21991)

 tests/admin_pages/test_card.py             |  76 ++++++++++++
 tests/admin_pages/test_form.py             |  80 ++++++++++++
 tests/admin_pages/test_workflow.py         |  66 ++++++++++
 tests/conftest.py                          |  32 +++++
 wcs/admin/blocks.py                        |   4 +-
 wcs/admin/categories.py                    |  43 ++++++-
 wcs/admin/data_sources.py                  |   7 ++
 wcs/admin/forms.py                         | 118 +++++++++++++-----
 wcs/admin/mail_templates.py                |   2 +
 wcs/admin/workflows.py                     | 134 +++++++++++++++++----
 wcs/backoffice/cards.py                    |  27 ++---
 wcs/backoffice/management.py               |  29 +++--
 wcs/backoffice/root.py                     |  17 ++-
 wcs/carddef.py                             |   1 +
 wcs/categories.py                          |   4 +
 wcs/formdef.py                             |   1 +
 wcs/templates/wcs/backoffice/category.html |   9 +-
 17 files changed, 565 insertions(+), 85 deletions(-)
tests/admin_pages/test_card.py
1 1
import re
2
import xml.etree.ElementTree as ET
2 3

  
3 4
import pytest
5
from webtest import Upload
4 6

  
5 7
from wcs import fields
6 8
from wcs.admin.settings import UserFieldsFormDef
......
516 518
    user_formdef = UserFieldsFormDef(pub)
517 519
    user_formdef.fields = []
518 520
    user_formdef.store()
521

  
522

  
523
def test_card_category_management_roles(pub, backoffice_user, backoffice_role):
524
    app = login(get_app(pub), username='backoffice-user', password='backoffice-user')
525
    app.get('/backoffice/cards/', status=403)
526

  
527
    CardDefCategory.wipe()
528
    cat = CardDefCategory(name='Foo')
529
    cat.store()
530

  
531
    CardDef.wipe()
532
    carddef = CardDef()
533
    carddef.name = 'card title'
534
    carddef.category_id = cat.id
535
    carddef.fields = []
536
    carddef.store()
537

  
538
    cat = CardDefCategory(name='Bar')
539
    cat.management_roles = [backoffice_role]
540
    cat.store()
541

  
542
    resp = app.get('/backoffice/cards/')
543
    assert 'Foo' not in resp.text  # not a category managed by user
544
    assert 'card title' not in resp.text  # carddef in that category
545
    assert 'Bar' not in resp.text  # not yet any form in this category
546

  
547
    resp = resp.click('New Card')
548
    resp.forms[0]['name'] = 'card in category'
549
    assert len(resp.forms[0]['category_id'].options) == 1  # single option
550
    assert resp.forms[0]['category_id'].value == cat.id  # the category managed by user
551
    resp = resp.forms[0].submit().follow()
552
    new_carddef = CardDef.get_by_urlname('card-in-category')
553

  
554
    # check category select only let choose one
555
    resp = resp.click(href='/category')
556
    assert len(resp.forms[0]['category_id'].options) == 1  # single option
557
    assert resp.forms[0]['category_id'].value == cat.id  # the category managed by user
558

  
559
    resp = app.get('/backoffice/cards/')
560
    assert 'Bar' in resp.text  # now there's a form in this category
561
    assert 'card in category' in resp.text
562

  
563
    # no access to subdirectories
564
    assert 'href="categories/"' not in resp.text
565
    app.get('/backoffice/cards/categories/', status=403)
566

  
567
    # no import into other category
568
    carddef_xml = ET.tostring(carddef.export_to_xml(include_id=True))
569
    resp = resp.click(href='import')
570
    resp.forms[0]['file'] = Upload('carddef.wcs', carddef_xml)
571
    resp = resp.forms[0].submit()
572
    assert 'Invalid File (unauthorized category)' in resp.text
573

  
574
    # check access to inspect page
575
    carddef.workflow_roles = {'_viewer': str(backoffice_role.id)}
576
    carddef.store()
577

  
578
    carddata = carddef.data_class()()
579
    carddata.just_created()
580
    carddata.store()
581

  
582
    resp = app.get(carddata.get_backoffice_url())
583
    assert 'inspect' not in resp.text
584
    resp = app.get(carddata.get_backoffice_url() + 'inspect', status=403)
585

  
586
    new_carddef.workflow_roles = {'_viewer': str(backoffice_role.id)}
587
    new_carddef.store()
588

  
589
    carddata = new_carddef.data_class()()
590
    carddata.just_created()
591
    carddata.store()
592
    resp = app.get(carddata.get_backoffice_url())
593
    assert 'inspect' in resp.text
594
    resp = app.get(carddata.get_backoffice_url() + 'inspect')
tests/admin_pages/test_form.py
2611 2611
    assert FormDef.get(1).fields[0].key == 'computed'
2612 2612
    assert FormDef.get(1).fields[0].label == 'foobar'
2613 2613
    assert FormDef.get(1).fields[0].varname == 'foobar'
2614

  
2615

  
2616
def test_form_category_management_roles(pub, backoffice_user, backoffice_role):
2617
    app = login(get_app(pub), username='backoffice-user', password='backoffice-user')
2618
    app.get('/backoffice/forms/', status=403)
2619

  
2620
    Category.wipe()
2621
    cat = Category(name='Foo')
2622
    cat.store()
2623

  
2624
    FormDef.wipe()
2625
    formdef = FormDef()
2626
    formdef.name = 'form title'
2627
    formdef.category_id = cat.id
2628
    formdef.fields = []
2629
    formdef.store()
2630

  
2631
    cat = Category(name='Bar')
2632
    cat.management_roles = [backoffice_role]
2633
    cat.store()
2634

  
2635
    resp = app.get('/backoffice/forms/')
2636
    assert 'Foo' not in resp.text  # not a category managed by user
2637
    assert 'form title' not in resp.text  # formdef in that category
2638
    assert 'Bar' not in resp.text  # not yet any form in this category
2639

  
2640
    app.get('/backoffice/forms/%s/' % formdef.id, status=403)
2641

  
2642
    resp = resp.click('New Form')
2643
    resp.forms[0]['name'] = 'form in category'
2644
    assert len(resp.forms[0]['category_id'].options) == 1  # single option
2645
    assert resp.forms[0]['category_id'].value == cat.id  # the category managed by user
2646
    resp = resp.forms[0].submit().follow()
2647
    new_formdef = FormDef.get_by_urlname('form-in-category')
2648

  
2649
    # check category select only let choose one
2650
    resp = resp.click(href='/category')
2651
    assert len(resp.forms[0]['category_id'].options) == 1  # single option
2652
    assert resp.forms[0]['category_id'].value == cat.id  # the category managed by user
2653

  
2654
    resp = app.get('/backoffice/forms/')
2655
    assert 'Bar' in resp.text  # now there's a form in this category
2656
    assert 'form in category' in resp.text
2657

  
2658
    # no access to subdirectories
2659
    assert 'href="categories/"' not in resp.text
2660
    assert 'href="data-sources/"' not in resp.text
2661
    assert 'href="blocks/"' not in resp.text
2662
    app.get('/backoffice/forms/categories/', status=403)
2663
    app.get('/backoffice/forms/data-sources/', status=403)
2664
    app.get('/backoffice/forms/blocks/', status=403)
2665

  
2666
    # no import into other category
2667
    formdef_xml = ET.tostring(formdef.export_to_xml(include_id=True))
2668
    resp = resp.click(href='import')
2669
    resp.forms[0]['file'] = Upload('formdef.wcs', formdef_xml)
2670
    resp = resp.forms[0].submit()
2671
    assert 'Invalid File (unauthorized category)' in resp.text
2672

  
2673
    # check access to inspect page
2674
    formdef.workflow_roles = {'_receiver': int(backoffice_role.id)}
2675
    formdef.store()
2676

  
2677
    formdata = formdef.data_class()()
2678
    formdata.just_created()
2679
    formdata.store()
2680

  
2681
    resp = app.get(formdata.get_backoffice_url())
2682
    assert 'inspect' not in resp.text
2683
    resp = app.get(formdata.get_backoffice_url() + 'inspect', status=403)
2684

  
2685
    new_formdef.workflow_roles = {'_receiver': int(backoffice_role.id)}
2686
    new_formdef.store()
2687

  
2688
    formdata = new_formdef.data_class()()
2689
    formdata.just_created()
2690
    formdata.store()
2691
    resp = app.get(formdata.get_backoffice_url())
2692
    assert 'inspect' in resp.text
2693
    resp = app.get(formdata.get_backoffice_url() + 'inspect')
tests/admin_pages/test_workflow.py
1 1
import io
2 2
import os
3 3
import re
4
import xml.etree.ElementTree as ET
4 5
from unittest import mock
5 6

  
6 7
import pytest
......
2683 2684
    resp = app.get('/backoffice/workflows/')
2684 2685
    assert 'Uncategorised' in resp.text
2685 2686
    assert 'XcategoryY' in resp.text
2687

  
2688

  
2689
def test_workflow_category_management_roles(pub, backoffice_user, backoffice_role):
2690
    app = login(get_app(pub), username='backoffice-user', password='backoffice-user')
2691
    app.get('/backoffice/workflows/', status=403)
2692

  
2693
    WorkflowCategory.wipe()
2694
    cat = WorkflowCategory(name='Foo')
2695
    cat.store()
2696

  
2697
    Workflow.wipe()
2698
    workflow = Workflow()
2699
    workflow.name = 'workflow title'
2700
    workflow.category_id = cat.id
2701
    workflow.store()
2702

  
2703
    cat = WorkflowCategory(name='Bar')
2704
    cat.management_roles = [backoffice_role]
2705
    cat.store()
2706

  
2707
    resp = app.get('/backoffice/workflows/')
2708
    assert 'Foo' not in resp.text  # not a category managed by user
2709
    assert 'workflow title' not in resp.text  # workflow in that category
2710
    assert 'Bar' not in resp.text  # not yet any form in this category
2711

  
2712
    resp = resp.click('New Workflow')
2713
    resp.forms[0]['name'] = 'workflow in category'
2714
    assert len(resp.forms[0]['category_id'].options) == 1  # single option
2715
    assert resp.forms[0]['category_id'].value == cat.id  # the category managed by user
2716
    resp = resp.forms[0].submit().follow()
2717

  
2718
    # check category select only let choose one
2719
    resp = resp.click(href='category')
2720
    assert len(resp.forms[0]['category_id'].options) == 1  # single option
2721
    assert resp.forms[0]['category_id'].value == cat.id  # the category managed by user
2722

  
2723
    resp = app.get('/backoffice/workflows/')
2724
    assert 'Bar' in resp.text  # now there's a form in this category
2725
    assert 'workflow in category' in resp.text
2726

  
2727
    # no access to subdirectories
2728
    assert 'href="categories/"' not in resp.text
2729
    assert 'href="data-sources/"' not in resp.text
2730
    assert 'href="mail-templates/"' not in resp.text
2731
    app.get('/backoffice/workflows/categories/', status=403)
2732
    app.get('/backoffice/workflows/data-sources/', status=403)
2733
    app.get('/backoffice/workflows/mail-templates/', status=403)
2734

  
2735
    # no import into other category
2736
    workflow_xml = ET.tostring(workflow.export_to_xml(include_id=True))
2737
    resp = resp.click(href='import')
2738
    resp.forms[0]['file'] = Upload('workflow.wcs', workflow_xml)
2739
    resp = resp.forms[0].submit()
2740
    assert 'Invalid File (unauthorized category)' in resp.text
2741

  
2742
    # access to default workflows
2743
    app.get('/backoffice/workflows/_carddef_default/')
2744
    resp = app.get('/backoffice/workflows/_default/')
2745

  
2746
    # duplicate on default workflows should open a dialog
2747
    resp = resp.click(href='duplicate')
2748
    assert len(resp.forms[0]['category_id'].options) == 1  # single option
2749
    resp = resp.forms[0].submit('cancel').follow()
2750
    resp = resp.click(href='duplicate')
2751
    resp = resp.forms[0].submit('submit').follow()
tests/conftest.py
4 4

  
5 5
import pytest
6 6

  
7
from wcs.qommon.ident.password_accounts import PasswordAccount
8

  
7 9
from .utilities import EmailsMocking, HttpRequestsMocking, SMSMocking
8 10

  
9 11

  
......
115 117
    monkeypatch.setattr(psycopg2, 'connect', connect)
116 118
    yield queries
117 119
    wcs.sql.cleanup_connection()
120

  
121

  
122
@pytest.fixture
123
def backoffice_role(pub):
124
    role = pub.role_class.get_on_index('backoffice-role', 'slug', ignore_errors=True)
125
    if not role:
126
        role = pub.role_class(name='backoffice role')
127
        role.allows_backoffice_access = True
128
        role.store()
129
        assert role.slug == 'backoffice-role'
130
    return role
131

  
132

  
133
@pytest.fixture
134
def backoffice_user(pub, backoffice_role):
135
    try:
136
        user = pub.user_class.get_users_with_email('backoffice-user@example.net')[0]
137
    except IndexError:
138
        user = pub.user_class()
139
        user.name = 'backoffice user'
140
        user.email = 'backoffice-user@example.net'
141
        user.roles = [backoffice_role.id]
142
        user.store()
143

  
144
    account1 = PasswordAccount(id='backoffice-user')
145
    account1.set_password('backoffice-user')
146
    account1.user_id = user.id
147
    account1.store()
148

  
149
    return user
wcs/admin/blocks.py
24 24
from wcs.blocks import BlockDef, BlockdefImportError
25 25
from wcs.qommon import _, misc, template
26 26
from wcs.qommon.backoffice.menu import html_top
27
from wcs.qommon.errors import TraversalError
27
from wcs.qommon.errors import AccessForbiddenError, TraversalError
28 28
from wcs.qommon.form import FileWidget, Form, HtmlWidget, StringWidget
29 29

  
30 30

  
......
195 195
        self.section = section
196 196

  
197 197
    def _q_traverse(self, path):
198
        if not get_publisher().get_backoffice_root().is_global_accessible('forms'):
199
            raise AccessForbiddenError()
198 200
        get_response().breadcrumb.append(('blocks/', _('Fields Blocks')))
199 201
        return super()._q_traverse(path)
200 202

  
wcs/admin/categories.py
23 23
from wcs.formdef import FormDef
24 24
from wcs.qommon import _, misc, template
25 25
from wcs.qommon.backoffice.menu import html_top
26
from wcs.qommon.errors import AccessForbiddenError
26 27
from wcs.qommon.form import Form, HtmlWidget, SingleSelectWidget, StringWidget, WidgetList, WysiwygTextWidget
27 28
from wcs.workflows import Workflow
28 29

  
29 30

  
30
def get_categories(category_class):
31
    t = sorted((misc.simplify(x.name), x.id, x.name, x.id) for x in category_class.select())
31
def get_categories(category_class, filter_function):
32
    t = sorted(
33
        (misc.simplify(x.name), x.id, x.name, x.id) for x in category_class.select() if filter_function(x)
34
    )
32 35
    return [x[1:] for x in t]
33 36

  
34 37

  
35 38
class CategoryUI:
36 39
    category_class = Category
40
    management_roles_hint_text = _('Roles allowed to create, edit and delete forms.')
37 41

  
38 42
    def __init__(self, category):
39 43
        self.category = category
......
66 70
        if not new:
67 71
            # include permission fields
68 72
            roles = list(get_publisher().role_class.select(order_by='name'))
73
            if 'management_roles' in [x[0] for x in self.category_class.XML_NODES]:
74
                form.add(
75
                    WidgetList,
76
                    'management_roles',
77
                    title=_('Management Roles'),
78
                    element_type=SingleSelectWidget,
79
                    value=self.category.management_roles,
80
                    add_element_label=_('Add Role'),
81
                    element_kwargs={
82
                        'render_br': False,
83
                        'options': [(None, '---', None)]
84
                        + [(x, x.name, x.id) for x in roles if not x.is_internal()],
85
                    },
86
                    hint=self.management_roles_hint_text,
87
                )
69 88
            if 'export_roles' in [x[0] for x in self.category_class.XML_NODES]:
70 89
                form.add(
71 90
                    WidgetList,
......
79 98
                        'options': [(None, '---', None)]
80 99
                        + [(x, x.name, x.id) for x in roles if not x.is_internal()],
81 100
                    },
82
                    hint=_('Roles allowed to export data'),
101
                    hint=_('Roles allowed to export data.'),
83 102
                )
84 103
            if 'statistics_roles' in [x[0] for x in self.category_class.XML_NODES]:
85 104
                form.add(
......
94 113
                        'options': [(None, '---', None)]
95 114
                        + [(x, x.name, x.id) for x in roles if not x.is_internal()],
96 115
                    },
97
                    hint=_('Roles with access to the statistics page'),
116
                    hint=_('Roles with access to the statistics page.'),
98 117
                )
99 118

  
100 119
        form.add_submit('submit', _('Submit'))
......
110 129
            form.get_widget('name').set_error(_('This name is already used'))
111 130
            raise ValueError()
112 131

  
113
        for attribute in ('description', 'redirect_url', 'export_roles', 'statistics_roles'):
132
        for attribute in (
133
            'description',
134
            'redirect_url',
135
            'management_roles',
136
            'export_roles',
137
            'statistics_roles',
138
        ):
114 139
            widget = form.get_widget(attribute)
115 140
            if widget:
116 141
                setattr(self.category, attribute, widget.parse())
......
120 145

  
121 146
class CardDefCategoryUI(CategoryUI):
122 147
    category_class = CardDefCategory
148
    management_roles_hint_text = _('Roles allowed to create, edit and delete card models.')
123 149

  
124 150

  
125 151
class WorkflowCategoryUI(CategoryUI):
126 152
    category_class = WorkflowCategory
153
    management_roles_hint_text = _('Roles allowed to create, edit and delete workflows.')
127 154

  
128 155

  
129 156
class CategoryPage(Directory):
......
234 261

  
235 262
class CategoriesDirectory(Directory):
236 263
    _q_exports = ['', 'new', 'update_order']
264

  
265
    base_section = 'forms'
237 266
    category_class = Category
238 267
    category_ui_class = CategoryUI
239 268
    category_page_class = CategoryPage
......
301 330
        return self.category_page_class(component)
302 331

  
303 332
    def _q_traverse(self, path):
333
        if not get_publisher().get_backoffice_root().is_global_accessible(self.base_section):
334
            raise AccessForbiddenError()
304 335
        get_response().breadcrumb.append(('categories/', _('Categories')))
305 336
        return super()._q_traverse(path)
306 337

  
307 338

  
308 339
class CardDefCategoriesDirectory(CategoriesDirectory):
340
    base_section = 'cards'
309 341
    category_class = CardDefCategory
310 342
    category_ui_class = CardDefCategoryUI
311 343
    category_page_class = CardDefCategoryPage
......
313 345

  
314 346

  
315 347
class WorkflowCategoriesDirectory(CategoriesDirectory):
348
    base_section = 'workflows'
316 349
    category_class = WorkflowCategory
317 350
    category_ui_class = WorkflowCategoryUI
318 351
    category_page_class = WorkflowCategoryPage
wcs/admin/data_sources.py
31 31
from wcs.formdef import get_formdefs_of_all_kinds
32 32
from wcs.qommon import _, errors, misc, template
33 33
from wcs.qommon.backoffice.menu import html_top
34
from wcs.qommon.errors import AccessForbiddenError
34 35
from wcs.qommon.form import (
35 36
    CheckboxWidget,
36 37
    DurationWidget,
......
421 422
    ]
422 423

  
423 424
    def _q_traverse(self, path):
425
        if (
426
            not get_publisher().get_backoffice_root().is_global_accessible('forms')
427
            and not get_publisher().get_backoffice_root().is_global_accessible('workflows')
428
            and not get_publisher().get_backoffice_root().is_global_accessible('cards')
429
        ):
430
            raise AccessForbiddenError()
424 431
        get_response().breadcrumb.append(('data-sources/', _('Data Sources')))
425 432
        return super()._q_traverse(path)
426 433

  
wcs/admin/forms.py
32 32
from wcs.qommon import _, force_str, get_logger, misc, template
33 33
from wcs.qommon.afterjobs import AfterJob
34 34
from wcs.qommon.backoffice.menu import html_top
35
from wcs.qommon.errors import TraversalError
35
from wcs.qommon.errors import AccessForbiddenError, TraversalError
36 36
from wcs.qommon.form import (
37 37
    CheckboxesWidget,
38 38
    CheckboxWidget,
......
62 62
from .logged_errors import LoggedErrorsDirectory
63 63

  
64 64

  
65
def is_global_accessible(section):
66
    return get_publisher().get_backoffice_root().is_global_accessible(section)
67

  
68

  
65 69
class FormDefUI:
66 70
    formdef_class = FormDef
67 71
    category_class = Category
72
    section = 'forms'
68 73

  
69 74
    def __init__(self, formdef):
70 75
        self.formdef = formdef
71 76

  
72 77
    def get_categories(self):
73
        return get_categories(self.category_class)
78
        global_access = is_global_accessible(self.section)
79
        user_roles = set(get_request().user.get_roles())
80

  
81
        def filter_function(category):
82
            if global_access:
83
                return True
84
            management_roles = {x.id for x in getattr(category, 'management_roles') or []}
85
            return bool(user_roles.intersection(management_roles))
86

  
87
        return get_categories(self.category_class, filter_function=filter_function)
74 88

  
75 89
    @classmethod
76 90
    def get_workflows(cls, condition=lambda x: True):
......
87 101
        form.add(StringWidget, 'name', title=_('Name'), required=True, size=40, value=formdef.name)
88 102
        categories = self.get_categories()
89 103
        if categories:
104
            if is_global_accessible(self.section):
105
                categories = [(None, '---', '')] + list(categories)
90 106
            form.add(
91 107
                SingleSelectWidget,
92 108
                'category_id',
93 109
                title=_('Category'),
94 110
                value=formdef.category_id,
95
                options=[(None, '---', '')] + categories,
111
                options=categories,
96 112
            )
97 113
        workflows = self.get_workflows()
98 114
        if len(workflows) > 1:
......
182 198
class OptionsDirectory(Directory):
183 199
    category_class = Category
184 200
    category_empty_choice = _('Select a category for this form')
201
    section = 'forms'
202

  
185 203
    _q_exports = [
186 204
        'confirmation',
187 205
        'only_allow_one',
......
199 217
        'user_support',
200 218
    ]
201 219

  
202
    def __init__(self, formdef):
220
    def __init__(self, formdef, formdefui):
203 221
        self.formdef = formdef
204 222
        self.changed = False
223
        self.formdefui = formdefui
205 224

  
206 225
    def confirmation(self):
207 226
        form = Form(enctype='multipart/form-data')
......
331 350
        return self.handle(form, _('Keywords'))
332 351

  
333 352
    def category(self):
334
        categories = get_categories(self.category_class)
353
        categories = self.formdefui.get_categories()
354
        if is_global_accessible(self.section):
355
            categories = [(None, '---', '')] + list(categories)
335 356
        form = Form(enctype='multipart/form-data')
336 357
        form.widgets.append(HtmlWidget('<p>%s</p>' % self.category_empty_choice))
337 358
        form.add(
......
339 360
            'category_id',
340 361
            title=_('Category'),
341 362
            value=self.formdef.category_id,
342
            options=[(None, '---', '')] + categories,
363
            options=list(categories),
343 364
        )
344 365
        return self.handle(form, _('Category'))
345 366

  
......
572 593
    formdef_export_prefix = 'form'
573 594
    formdef_ui_class = FormDefUI
574 595
    formdef_default_workflow = '_default'
596
    section = 'forms'
575 597
    options_directory_class = OptionsDirectory
576 598

  
577 599
    delete_message = _('You are about to irrevocably delete this form.')
......
593 615
        self.fields.html_top = self.html_top
594 616
        self.role = WorkflowRoleDirectory(self.formdef)
595 617
        self.role.html_top = self.html_top
596
        self.options = self.options_directory_class(self.formdef)
618
        self.options = self.options_directory_class(self.formdef, self.formdefui)
597 619
        self.logged_errors_dir = LoggedErrorsDirectory(
598 620
            parent_dir=self, formdef_class=self.formdef_class, formdef_id=self.formdef.id
599 621
        )
600 622
        self.snapshots_dir = SnapshotsDirectory(self.formdef)
601 623

  
602 624
    def html_top(self, title):
603
        return html_top('forms', title)
625
        return html_top(self.section, title)
604 626

  
605 627
    def add_option_line(self, link, label, current_value, popup=True):
606 628
        return htmltext(
......
1672 1694
    formdef_page_class = FormDefPage
1673 1695
    formdef_ui_class = FormDefUI
1674 1696

  
1697
    section = 'forms'
1675 1698
    top_title = _('Forms')
1676 1699
    import_title = _('Import Form')
1677 1700
    import_submit_label = _('Import Form')
......
1687 1710
    )
1688 1711

  
1689 1712
    def html_top(self, title):
1690
        return html_top('forms', title)
1713
        return html_top(self.section, title)
1691 1714

  
1692 1715
    def _q_traverse(self, path):
1693
        get_response().breadcrumb.append(('forms/', _('Forms')))
1716
        get_response().breadcrumb.append(('%s/' % self.section, self.top_title))
1694 1717
        return super()._q_traverse(path)
1695 1718

  
1719
    def is_accessible(self, user):
1720
        if is_global_accessible(self.section):
1721
            return True
1722

  
1723
        # check for access to specific categories
1724
        user_roles = set(user.get_roles())
1725
        for category in self.category_class.select():
1726
            management_roles = {x.id for x in getattr(category, 'management_roles') or []}
1727
            if management_roles and user_roles.intersection(management_roles):
1728
                return True
1729

  
1730
        return False
1731

  
1696 1732
    def _q_index(self):
1697 1733
        self.html_top(title=self.top_title)
1698 1734
        r = TemplateIO(html=True)
1699 1735
        get_response().add_javascript(['jquery.js', 'widget_list.js'])
1700 1736
        r += self.form_actions()
1701 1737

  
1738
        global_access = is_global_accessible(self.section)
1739

  
1702 1740
        cats = self.category_class.select()
1703 1741
        self.category_class.sort_by_position(cats)
1704 1742
        one = False
1705 1743
        formdefs = self.formdef_class.select(order_by='name', ignore_errors=True, lightweight=True)
1706 1744
        for c in cats:
1745
            if not global_access:
1746
                user_roles = set(get_request().user.get_roles())
1747
                management_roles = {x.id for x in getattr(c, 'management_roles') or []}
1748
                if not user_roles.intersection(management_roles):
1749
                    continue
1707 1750
            l2 = [x for x in formdefs if str(x.category_id) == str(c.id)]
1708 1751
            l2 = [x for x in l2 if not x.disabled or (x.disabled and x.disabled_redirection)] + [
1709 1752
                x for x in l2 if x.disabled and not x.disabled_redirection
......
1712 1755
                r += self.form_list(l2, title=c.name)
1713 1756
                one = True
1714 1757

  
1715
        l2 = [x for x in formdefs if not x.category]
1716
        if l2:
1717
            if one:
1718
                title = _('Misc')
1719
            else:
1720
                title = None
1721
            l2 = [x for x in l2 if not x.disabled or (x.disabled and x.disabled_redirection)] + [
1722
                x for x in l2 if x.disabled and not x.disabled_redirection
1723
            ]
1724
            r += self.form_list(l2, title=title)
1758
        if global_access:
1759
            l2 = [x for x in formdefs if not x.category]
1760
            if l2:
1761
                if one:
1762
                    title = _('Misc')
1763
                else:
1764
                    title = None
1765
                l2 = [x for x in l2 if not x.disabled or (x.disabled and x.disabled_redirection)] + [
1766
                    x for x in l2 if x.disabled and not x.disabled_redirection
1767
                ]
1768
                r += self.form_list(l2, title=title)
1725 1769
        return r.getvalue()
1726 1770

  
1727 1771
    def form_actions(self):
......
1731 1775
        r += htmltext('<h2>%s</h2>') % _('Forms')
1732 1776
        if has_roles:
1733 1777
            r += htmltext('<span class="actions">')
1734
            r += htmltext('<a href="data-sources/">%s</a>') % _('Data sources')
1735
            if get_publisher().has_site_option('fields-blocks'):
1736
                r += htmltext('<a href="blocks/">%s</a>') % _('Fields blocks')
1737
            if get_publisher().get_backoffice_root().is_accessible('categories'):
1738
                r += htmltext('<a href="categories/">%s</a>') % _('Categories')
1778
            if is_global_accessible('forms'):
1779
                r += htmltext('<a href="data-sources/">%s</a>') % _('Data sources')
1780
                if get_publisher().has_site_option('fields-blocks'):
1781
                    r += htmltext('<a href="blocks/">%s</a>') % _('Fields blocks')
1782
                if get_publisher().get_backoffice_root().is_accessible('categories'):
1783
                    r += htmltext('<a href="categories/">%s</a>') % _('Categories')
1739 1784
            r += htmltext('<a href="import" rel="popup">%s</a>') % _('Import')
1740 1785
            r += htmltext('<a class="new-item" href="new" rel="popup">%s</a>') % _('New Form')
1741 1786
            r += htmltext('</span>')
......
1768 1813
    def new(self):
1769 1814
        get_response().breadcrumb.append(('new', _('New')))
1770 1815
        if get_publisher().role_class.count() == 0:
1771
            return template.error_page('forms', _('You first have to define roles.'))
1816
            return template.error_page(self.section, _('You first have to define roles.'))
1772 1817
        formdefui = self.formdef_ui_class(None)
1773 1818
        form = formdefui.new_form_ui()
1774 1819
        if form.get_widget('cancel').parse():
......
1791 1836
        return r.getvalue()
1792 1837

  
1793 1838
    def _q_lookup(self, component):
1794
        return self.formdef_page_class(component)
1839
        directory = self.formdef_page_class(component)
1840
        global_access = is_global_accessible(self.section)
1841
        if not global_access:
1842
            user_roles = set(get_request().user.get_roles())
1843
            management_roles = set()
1844
            if directory.formdef.category:
1845
                management_roles = {
1846
                    x.id for x in getattr(directory.formdef.category, 'management_roles') or []
1847
                }
1848
            if not management_roles.intersection(user_roles):
1849
                raise AccessForbiddenError()
1850
        return directory
1795 1851

  
1796 1852
    def p_import(self):
1797 1853
        form = Form(enctype='multipart/form-data')
......
1850 1906
        except ValueError:
1851 1907
            error = True
1852 1908

  
1909
        global_access = is_global_accessible(self.section)
1910
        if not global_access:
1911
            management_roles = {x.id for x in getattr(formdef.category, 'management_roles', None) or []}
1912
            user_roles = set(get_request().user.get_roles())
1913
            if not user_roles.intersection(management_roles):
1914
                error = True
1915
                reason = _('unauthorized category')
1916

  
1853 1917
        if error:
1854 1918
            if reason:
1855 1919
                msg = _('Invalid File (%s)') % reason
wcs/admin/mail_templates.py
40 40
    do_not_call_in_templates = True
41 41

  
42 42
    def _q_traverse(self, path):
43
        if not get_publisher().get_backoffice_root().is_global_accessible('workflows'):
44
            raise errors.AccessForbiddenError()
43 45
        get_response().breadcrumb.append(('mail-templates/', _('Mail Templates')))
44 46
        return super()._q_traverse(path)
45 47

  
wcs/admin/workflows.py
66 66
from .mail_templates import MailTemplatesDirectory
67 67

  
68 68

  
69
def is_global_accessible():
70
    return get_publisher().get_backoffice_root().is_global_accessible('workflows')
71

  
72

  
69 73
def svg(tag):
70 74
    return '{http://www.w3.org/2000/svg}%s' % tag
71 75

  
......
295 299
    def __init__(self, workflow):
296 300
        self.workflow = workflow
297 301

  
302
    def get_categories(self):
303
        global_access = is_global_accessible()
304
        user_roles = set(get_request().user.get_roles())
305

  
306
        def filter_function(category):
307
            if global_access:
308
                return True
309
            management_roles = {x.id for x in getattr(category, 'management_roles') or []}
310
            return bool(user_roles.intersection(management_roles))
311

  
312
        return get_categories(WorkflowCategory, filter_function=filter_function)
313

  
298 314
    def form_new(self):
299 315
        form = Form(enctype='multipart/form-data')
300 316
        form.add(StringWidget, 'name', title=_('Workflow Name'), required=True, size=30)
301
        category_options = get_categories(WorkflowCategory)
317
        category_options = self.get_categories()
302 318
        if category_options:
319
            if is_global_accessible():
320
                category_options = [(None, '---', '')] + list(category_options)
303 321
            form.add(
304 322
                SingleSelectWidget,
305 323
                'category_id',
306 324
                title=_('Category'),
307
                options=[(None, '---', '')] + category_options,
325
                options=category_options,
308 326
            )
309 327
        form.add_submit('submit', _('Submit'))
310 328
        form.add_submit('cancel', _('Cancel'))
......
332 350
            form.get_widget('name').set_error(_('This name is already used'))
333 351
            raise ValueError()
334 352

  
335
        for f in ('name',):
336
            setattr(workflow, f, form.get_widget(f).parse())
353
        for f in ('name', 'category_id'):
354
            widget = form.get_widget(f)
355
            if widget:
356
                setattr(workflow, f, widget.parse())
337 357
        workflow.store()
338 358
        return workflow
339 359

  
......
1487 1507
        return html_top('workflows', title)
1488 1508

  
1489 1509
    def category(self):
1490
        categories = sorted((misc.simplify(x.name), x.id, x.name, x.id) for x in WorkflowCategory.select())
1510
        category_options = self.workflow_ui.get_categories()
1511
        if is_global_accessible():
1512
            category_options = [(None, '---', '')] + list(category_options)
1491 1513

  
1492 1514
        form = Form(enctype='multipart/form-data')
1493 1515
        form.widgets.append(HtmlWidget('<p>%s</p>' % _('Select a category for this workflow.')))
......
1496 1518
            'category_id',
1497 1519
            title=_('Category'),
1498 1520
            value=self.workflow.category_id,
1499
            options=[(None, '---', '')] + [x[1:] for x in categories],
1521
            options=category_options,
1500 1522
        )
1501 1523

  
1502 1524
        if not self.workflow.is_readonly():
......
1560 1582
        r += htmltext('<ul id="sidebar-actions">')
1561 1583
        if not self.workflow.is_readonly():
1562 1584
            r += htmltext('<li><a href="delete" rel="popup">%s</a></li>') % _('Delete')
1563
        r += htmltext('<li><a href="duplicate">%s</a></li>') % _('Duplicate')
1585
        if not is_global_accessible() and self.workflow.id in ('_default', '_carddef_default'):
1586
            r += htmltext('<li><a rel="popup" href="duplicate">%s</a></li>') % _('Duplicate')
1587
        else:
1588
            r += htmltext('<li><a href="duplicate">%s</a></li>') % _('Duplicate')
1564 1589
        r += htmltext('<li><a href="export">%s</a></li>') % _('Export')
1565 1590
        if get_publisher().snapshot_class:
1566 1591
            r += htmltext('<li><a rel="popup" href="history/save">%s</a></li>') % _('Save snapshot')
......
1746 1771

  
1747 1772
        return redirect('.')
1748 1773

  
1749
    def edit(self, duplicate=False):
1774
    def edit(self):
1750 1775
        form = self.workflow_ui.form_edit()
1751 1776
        if form.get_widget('cancel').parse():
1752 1777
            return redirect('.')
......
1761 1786

  
1762 1787
        self.html_top(title=_('Edit Workflow'))
1763 1788
        r = TemplateIO(html=True)
1764
        if duplicate:
1765
            get_response().breadcrumb.append(('edit', _('Duplicate')))
1766
            r += htmltext('<h2>%s</h2>') % _('Duplicate Workflow')
1767
        else:
1768
            get_response().breadcrumb.append(('edit', _('Edit')))
1769
            r += htmltext('<h2>%s</h2>') % _('Edit Workflow')
1789
        get_response().breadcrumb.append(('edit', _('Edit')))
1790
        r += htmltext('<h2>%s</h2>') % _('Edit Workflow')
1770 1791
        r += form.render()
1771 1792
        return r.getvalue()
1772 1793

  
......
1801 1822
            return redirect('..')
1802 1823

  
1803 1824
    def duplicate(self):
1825
        if not is_global_accessible() and self.workflow.id in ('_default', '_carddef_default'):
1826
            category_options = self.workflow_ui.get_categories()
1827
            form = Form(enctype='multipart/form-data')
1828
            form.widgets.append(HtmlWidget('<p>%s</p>' % _('Select a category for this workflow.')))
1829
            form.add(
1830
                SingleSelectWidget,
1831
                'category_id',
1832
                title=_('Category'),
1833
                options=category_options,
1834
            )
1835
            form.add_submit('submit', _('Submit'))
1836
            form.add_submit('cancel', _('Cancel'))
1837
            if form.get_widget('cancel').parse():
1838
                return redirect('.')
1839

  
1840
            if not form.is_submitted() or form.has_errors():
1841
                self.html_top(title=_('Duplicate Workflow'))
1842
                r = TemplateIO(html=True)
1843
                get_response().breadcrumb.append(('duplicate', _('Duplicate')))
1844
                r += htmltext('<h2>%s</h2>') % _('Duplicate Workflow')
1845
                r += form.render()
1846
                return r.getvalue()
1847

  
1848
            self.workflow_ui.workflow.category_id = form.get_widget('category_id').parse()
1849

  
1804 1850
        self.workflow_ui.workflow.id = None
1805 1851
        original_name = self.workflow_ui.workflow.name
1806 1852
        self.workflow_ui.workflow.name = '%s %s' % (self.workflow_ui.workflow.name, _('(copy)'))
......
1832 1878

  
1833 1879
    data_sources = NamedDataSourcesDirectoryInWorkflows()
1834 1880
    mail_templates = MailTemplatesDirectory()
1835
    category_class = WorkflowCategory
1836 1881
    categories = WorkflowCategoriesDirectory()
1837 1882

  
1838 1883
    def html_top(self, title):
......
1842 1887
        get_response().breadcrumb.append(('workflows/', _('Workflows')))
1843 1888
        return super()._q_traverse(path)
1844 1889

  
1890
    def is_accessible(self, user):
1891
        if is_global_accessible():
1892
            return True
1893

  
1894
        # check for access to specific categories
1895
        user_roles = set(user.get_roles())
1896
        for category in WorkflowCategory.select():
1897
            management_roles = {x.id for x in getattr(category, 'management_roles') or []}
1898
            if management_roles and user_roles.intersection(management_roles):
1899
                return True
1900

  
1901
        return False
1902

  
1845 1903
    def _q_index(self):
1846 1904
        self.html_top(title=_('Workflows'))
1847 1905
        r = TemplateIO(html=True)
......
1849 1907
        r += htmltext('<div id="appbar">')
1850 1908
        r += htmltext('<h2>%s</h2>') % _('Workflows')
1851 1909
        r += htmltext('<span class="actions">')
1852
        if get_publisher().has_site_option('mail-templates'):
1853
            r += htmltext('<a href="mail-templates/">%s</a>') % _('Mail Templates')
1854
        r += htmltext('<a href="data-sources/">%s</a>') % _('Data sources')
1855
        if get_publisher().get_backoffice_root().is_accessible('categories'):
1910
        if is_global_accessible():
1911
            if get_publisher().has_site_option('mail-templates'):
1912
                r += htmltext('<a href="mail-templates/">%s</a>') % _('Mail Templates')
1913
            r += htmltext('<a href="data-sources/">%s</a>') % _('Data sources')
1856 1914
            r += htmltext('<a href="categories/">%s</a>') % _('Categories')
1857 1915
        r += htmltext('<a href="import" rel="popup">%s</a>') % _('Import')
1858 1916
        r += htmltext('<a class="new-item" rel="popup" href="new">%s</a>') % _('New Workflow')
......
1889 1947
            else:
1890 1948
                unused_workflows.append(workflow)
1891 1949

  
1892
        categories = sorted(WorkflowCategory.select(), key=lambda x: misc.simplify(x.name))
1950
        if is_global_accessible():
1951
            categories = WorkflowCategory.select()
1952
        else:
1953
            categories = []
1954
            user_roles = set(get_request().user.get_roles())
1955
            for category in WorkflowCategory.select():
1956
                management_roles = {x.id for x in getattr(category, 'management_roles') or []}
1957
                if management_roles and user_roles.intersection(management_roles):
1958
                    categories.append(category)
1959

  
1960
        categories.sort(key=lambda x: misc.simplify(x.name))
1893 1961

  
1894 1962
        if categories:
1895 1963
            default_category = WorkflowCategory('Default')
......
1899 1967
                    workflow.category_id = default_category.id
1900 1968
            categories = [default_category] + categories
1901 1969

  
1970
        if is_global_accessible():
1971
            categories = categories + [None]
1972

  
1902 1973
        def workflow_section(r, workflows):
1903 1974
            r += htmltext('<ul class="objects-list single-links">')
1904 1975
            for workflow in workflows:
......
1924 1995
                r += htmltext('</li>')
1925 1996
            r += htmltext('</ul>')
1926 1997

  
1927
        for category in categories + [None]:
1998
        for category in categories:
1928 1999
            if category is None:
1929 2000
                category_workflows = [x for x in workflows + unused_workflows if not x.category_id]
1930 2001
            else:
......
1969 2040
        return r.getvalue()
1970 2041

  
1971 2042
    def _q_lookup(self, component):
1972
        return WorkflowPage(component)
2043
        directory = WorkflowPage(component)
2044
        global_access = is_global_accessible()
2045
        if directory.workflow.id not in ('_default', '_carddef_default') and not global_access:
2046
            user_roles = set(get_request().user.get_roles())
2047
            management_roles = set()
2048
            if directory.workflow.category:
2049
                management_roles = {
2050
                    x.id for x in getattr(directory.workflow.category, 'management_roles') or []
2051
                }
2052
            if not management_roles.intersection(user_roles):
2053
                raise errors.AccessForbiddenError()
2054
        return directory
1973 2055

  
1974 2056
    def p_import(self):
1975 2057
        form = Form(enctype='multipart/form-data')
......
2023 2105
        except ValueError:
2024 2106
            error = True
2025 2107

  
2108
        global_access = is_global_accessible()
2109
        if not global_access:
2110
            management_roles = {x.id for x in getattr(workflow.category, 'management_roles', None) or []}
2111
            user_roles = set(get_request().user.get_roles())
2112
            if not user_roles.intersection(management_roles):
2113
                error = True
2114
                reason = _('unauthorized category')
2115

  
2026 2116
        if error:
2027 2117
            if reason:
2028 2118
                msg = _('Invalid File (%s)') % reason
wcs/backoffice/cards.py
15 15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16 16

  
17 17
from quixote import get_publisher, get_response, get_session, redirect
18
from quixote.directory import Directory
19 18
from quixote.html import TemplateIO, htmltext
20 19

  
21 20
from wcs.admin import utils
22 21
from wcs.admin.categories import CardDefCategoriesDirectory
23
from wcs.admin.forms import FormDefPage, FormDefUI, FormsDirectory, OptionsDirectory, html_top
22
from wcs.admin.forms import FormDefPage, FormDefUI, FormsDirectory, OptionsDirectory
24 23
from wcs.admin.logged_errors import LoggedErrorsDirectory
25 24
from wcs.carddef import CardDef
26 25
from wcs.categories import CardDefCategory
......
34 33
class CardDefUI(FormDefUI):
35 34
    formdef_class = CardDef
36 35
    category_class = CardDefCategory
36
    section = 'cards'
37 37

  
38 38

  
39 39
class CardDefOptionsDirectory(OptionsDirectory):
40 40
    category_class = CardDefCategory
41 41
    category_empty_choice = _('Select a category for this card model')
42
    section = 'cards'
42 43

  
43 44

  
44 45
class CardDefPage(FormDefPage):
......
46 47
    formdef_export_prefix = 'card'
47 48
    formdef_ui_class = CardDefUI
48 49
    formdef_default_workflow = '_carddef_default'
50
    section = 'cards'
49 51

  
50 52
    options_directory_class = CardDefOptionsDirectory
51 53

  
......
59 61
        'Do note it kept its existing address and role and workflow parameters.'
60 62
    )
61 63

  
62
    def html_top(self, title):
63
        return html_top('cards', title)
64

  
65 64
    def _q_index(self):
66 65
        self.html_top(title=self.formdef.name)
67 66
        r = TemplateIO(html=True)
......
248 247
    formdef_page_class = CardDefPage
249 248
    formdef_ui_class = CardDefUI
250 249

  
250
    section = 'cards'
251 251
    top_title = _('Card Models')
252 252
    import_title = _('Import Card Model')
253 253
    import_submit_label = _('Import Card Model')
......
261 261
        'you should nevertheless check everything is ok. '
262 262
    )
263 263

  
264
    def html_top(self, title):
265
        return html_top('cards', title)
266

  
267
    def _q_traverse(self, path):
268
        get_response().breadcrumb.append(('cards/', _('Card Models')))
269
        return Directory._q_traverse(self, path)
270

  
271 264
    def form_actions(self):
272 265
        r = TemplateIO(html=True)
273 266
        r += htmltext('<div id="appbar">')
274 267
        r += htmltext('<h2>%s</h2>') % _('Card Models')
275 268
        r += htmltext('<span class="actions">')
276
        r += htmltext('<a href="../forms/data-sources/">%s</a>') % _('Data sources')
277
        if get_publisher().has_site_option('fields-blocks'):
278
            r += htmltext('<a href="../forms/blocks/">%s</a>') % _('Fields blocks')
279
        r += htmltext('<a href="categories/">%s</a>') % _('Categories')
269
        if get_publisher().get_backoffice_root().is_global_accessible('forms'):
270
            r += htmltext('<a href="../forms/data-sources/">%s</a>') % _('Data sources')
271
            if get_publisher().has_site_option('fields-blocks'):
272
                r += htmltext('<a href="../forms/blocks/">%s</a>') % _('Fields blocks')
273
        if get_publisher().get_backoffice_root().is_global_accessible('cards'):
274
            r += htmltext('<a href="categories/">%s</a>') % _('Categories')
280 275
        r += htmltext('<a href="import" rel="popup">%s</a>') % _('Import')
281 276
        r += htmltext('<a class="new-item" href="new" rel="popup">%s</a>') % _('New Card Model')
282 277
        r += htmltext('</span>')
wcs/backoffice/management.py
2905 2905

  
2906 2906
        return response
2907 2907

  
2908
    def can_go_in_inspector(self):
2909
        if get_publisher().get_backoffice_root().is_global_accessible('worflows'):
2910
            return True
2911
        if (
2912
            get_publisher()
2913
            .get_backoffice_root()
2914
            .is_global_accessible(self.formdata.formdef.backoffice_section)
2915
        ):
2916
            return True
2917

  
2918
        user_roles = set(get_request().user.get_roles())
2919
        for category in (self.formdata.formdef.category, self.formdata.formdef.workflow.category):
2920
            if not category:
2921
                continue
2922
            management_roles = {x.id for x in getattr(category, 'management_roles') or []}
2923
            if user_roles.intersection(management_roles):
2924
                return True
2925
        return False
2926

  
2908 2927
    def get_extra_context_bar(self, parent=None):
2909 2928
        formdata = self.filled
2910 2929

  
......
3014 3033
                '<div data-async-url="%suser-pending-forms"></div>' % formdata.get_url(backoffice=True)
3015 3034
            )
3016 3035

  
3017
        if not formdata.is_draft() and (
3018
            get_publisher().get_backoffice_root().is_accessible('forms')
3019
            or get_publisher().get_backoffice_root().is_accessible('workflows')
3020
        ):
3036
        if not formdata.is_draft() and self.can_go_in_inspector():
3021 3037
            r += htmltext('<div class="extra-context">')
3022 3038
            r += htmltext('<p><a href="%sinspect">' % formdata.get_url(backoffice=True))
3023 3039
            r += htmltext('%s</a></p>') % _('Data Inspector')
......
3393 3409
        return r.getvalue()
3394 3410

  
3395 3411
    def inspect(self):
3396
        if not (
3397
            get_publisher().get_backoffice_root().is_accessible('forms')
3398
            or get_publisher().get_backoffice_root().is_accessible('workflows')
3399
        ):
3412
        if not self.can_go_in_inspector():
3400 3413
            raise errors.AccessForbiddenError()
3401 3414
        charset = get_publisher().site_charset
3402 3415
        get_response().breadcrumb.append(('inspect', _('Data Inspector')))
wcs/backoffice/root.py
98 98
                return subdirectory in ('settings', 'users')
99 99
            return False
100 100

  
101
        # if the directory defines a is_accessible method, use it.
102
        if hasattr(getattr(cls, subdirectory, None), 'is_accessible'):
103
            return getattr(cls, subdirectory).is_accessible(get_request().user)
104

  
105
        return cls.is_global_accessible(subdirectory)
106

  
107
    @classmethod
108
    def is_global_accessible(cls, subdirectory):
109
        if cls.check_admin_for_all():
110
            return True
101 111
        user_roles = set(get_request().user.get_roles())
102 112
        authorised_roles = set(get_cfg('admin-permissions', {}).get(subdirectory) or [])
103 113
        if authorised_roles:
104 114
            # access is governed by roles set in the settings panel
105 115
            return user_roles.intersection(authorised_roles)
106 116

  
107
        # if the directory defines a is_accessible method, use it.
108
        if hasattr(getattr(cls, subdirectory, None), 'is_accessible'):
109
            return getattr(cls, subdirectory).is_accessible(get_request().user)
110

  
111 117
        # as a last resort, for the other directories, the user needs to be
112 118
        # marked as admin
113 119
        return get_request().user.can_go_in_admin()
114 120

  
115
    def check_admin_for_all(self):
121
    @classmethod
122
    def check_admin_for_all(cls):
116 123
        admin_for_all_file_path = os.path.join(get_publisher().app_dir, 'ADMIN_FOR_ALL')
117 124
        if not os.path.exists(os.path.join(admin_for_all_file_path)):
118 125
            return False
wcs/carddef.py
33 33

  
34 34
class CardDef(FormDef):
35 35
    _names = 'carddefs'
36
    backoffice_section = 'cards'
36 37
    data_sql_prefix = 'carddata'
37 38
    pickle_module_name = 'carddef'
38 39
    xml_root_node = 'carddef'
wcs/categories.py
35 35

  
36 36
    export_roles = None
37 37
    statistics_roles = None
38
    management_roles = None
38 39

  
39 40
    # declarations for serialization
40 41
    XML_NODES = [
......
45 46
        ('position', 'int'),
46 47
        ('export_roles', 'roles'),
47 48
        ('statistics_roles', 'roles'),
49
        ('management_roles', 'roles'),
48 50
    ]
49 51

  
50 52
    def __init__(self, name=None):
......
149 151
        ('description', 'str'),
150 152
        ('position', 'int'),
151 153
        ('export_roles', 'roles'),
154
        ('management_roles', 'roles'),
152 155
    ]
153 156

  
154 157
    @classmethod
......
167 170
        ('name', 'str'),
168 171
        ('url_name', 'str'),
169 172
        ('description', 'str'),
173
        ('management_roles', 'roles'),
170 174
    ]
171 175

  
172 176
    @classmethod
wcs/formdef.py
85 85
    data_sql_prefix = 'formdata'
86 86
    pickle_module_name = 'formdef'
87 87
    xml_root_node = 'formdef'
88
    backoffice_section = 'forms'
88 89
    verbose_name = _('Form')
89 90
    verbose_name_plural = _('Forms')
90 91

  
wcs/templates/wcs/backoffice/category.html
30 30
{% endwith %}
31 31
</div>
32 32

  
33
{% if category.export_roles or category.statistics_roles %}
33
{% if category.export_roles or category.statistics_roles or category.management_roles %}
34 34
<div class="section">
35 35
<h3>{% trans "Permissions" %}</h3>
36 36
<div>
37 37
  <ul>
38
    {% if category.management_roles %}
39
    <li>{% trans "Management roles:" %}
40
      <ul>
41
      {% for role in category.management_roles %}<li>{{ role.name }}</li>{% endfor %}
42
      </ul>
43
    </li>
44
    {% endif %}
38 45
    {% if category.export_roles %}
39 46
    <li>{% trans "Export roles:" %}
40 47
      <ul>
41
-