From 58684d54cd1f8d53635ca27342ccd3645b8c8bf2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Fri, 14 May 2021 14:55:38 +0200
Subject: [PATCH 2/3] categories: add new export_roles and statistics_roles
 attributes (#53667)

---
 tests/admin_pages/test_carddefcategory.py  |  2 +
 tests/admin_pages/test_category.py         | 33 +++++++++++++
 wcs/admin/categories.py                    | 56 ++++++++++++++++++----
 wcs/api_access.py                          | 27 -----------
 wcs/categories.py                          | 13 ++++-
 wcs/qommon/xml_storage.py                  | 23 ++++++++-
 wcs/templates/wcs/backoffice/category.html | 24 ++++++++++
 7 files changed, 139 insertions(+), 39 deletions(-)

diff --git a/tests/admin_pages/test_carddefcategory.py b/tests/admin_pages/test_carddefcategory.py
index c95655be1..e8dda91fc 100644
--- a/tests/admin_pages/test_carddefcategory.py
+++ b/tests/admin_pages/test_carddefcategory.py
@@ -191,6 +191,8 @@ def test_categories_new_duplicate_name(pub):
 
 
 def test_categories_reorder(pub):
+    create_superuser(pub)
+
     CardDefCategory.wipe()
     category = CardDefCategory(name='foo')
     category.store()
diff --git a/tests/admin_pages/test_category.py b/tests/admin_pages/test_category.py
index 9901dff9a..27d1a7b52 100644
--- a/tests/admin_pages/test_category.py
+++ b/tests/admin_pages/test_category.py
@@ -202,6 +202,8 @@ def test_categories_new_duplicate_name(pub):
 
 
 def test_categories_reorder(pub):
+    create_superuser(pub)
+
     Category.wipe()
     category = Category(name='foo')
     category.store()
@@ -220,3 +222,34 @@ def test_categories_reorder(pub):
     categories = Category.select()
     Category.sort_by_position(categories)
     assert [x.id for x in categories] == ['3', '1', '2']
+
+
+def test_categories_edit_roles(pub):
+    create_superuser(pub)
+
+    pub.role_class.wipe()
+    role_a = pub.role_class(name='a')
+    role_a.store()
+    role_b = pub.role_class(name='b')
+    role_b.store()
+
+    Category.wipe()
+    category = Category(name='foobar')
+    category.store()
+
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/forms/categories/1/edit')
+
+    resp.form['export_roles$element0'] = role_a.id
+    resp = resp.form.submit('export_roles$add_element')
+    resp.form['export_roles$element1'] = role_b.id
+
+    resp.form['statistics_roles$element0'] = role_a.id
+    resp = resp.form.submit('submit')
+
+    category = Category.get(category.id)
+    assert set(x.id for x in category.export_roles) == {role_a.id, role_b.id}
+    assert set(x.id for x in category.statistics_roles) == {role_a.id}
+
+    resp = app.get('/backoffice/forms/categories/1/edit')
+    assert resp.form['export_roles$element0'].value == role_a.id
diff --git a/wcs/admin/categories.py b/wcs/admin/categories.py
index 65194624f..3d856e533 100644
--- a/wcs/admin/categories.py
+++ b/wcs/admin/categories.py
@@ -14,7 +14,7 @@
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, see <http://www.gnu.org/licenses/>.
 
-from quixote import get_request, get_response, redirect
+from quixote import get_publisher, get_request, get_response, redirect
 from quixote.directory import Directory
 from quixote.html import TemplateIO, htmltext
 
@@ -23,7 +23,7 @@ from wcs.categories import CardDefCategory, Category
 from wcs.formdef import FormDef
 from wcs.qommon import _, template
 from wcs.qommon.backoffice.menu import html_top
-from wcs.qommon.form import Form, HtmlWidget, StringWidget, WysiwygTextWidget
+from wcs.qommon.form import Form, HtmlWidget, SingleSelectWidget, StringWidget, WidgetList, WysiwygTextWidget
 
 
 class CategoryUI:
@@ -34,7 +34,7 @@ class CategoryUI:
         if self.category is None:
             self.category = self.category_class()
 
-    def get_form(self):
+    def get_form(self, new=False):
         form = Form(enctype='multipart/form-data')
         form.add(
             StringWidget, 'name', title=_('Category Name'), required=True, size=30, value=self.category.name
@@ -47,7 +47,7 @@ class CategoryUI:
             rows=10,
             value=self.category.description,
         )
-        if self.category_class == Category:
+        if self.category_class is Category:
             form.add(
                 StringWidget,
                 'redirect_url',
@@ -56,6 +56,40 @@ class CategoryUI:
                 hint=_('If set, redirect the site category page to the given URL.'),
                 value=self.category.redirect_url,
             )
+
+        if not new:
+            # include permission fields
+            roles = list(get_publisher().role_class.select(order_by='name'))
+            form.add(
+                WidgetList,
+                'export_roles',
+                title=_('Export Roles'),
+                element_type=SingleSelectWidget,
+                value=self.category.export_roles,
+                add_element_label=_('Add Role'),
+                element_kwargs={
+                    'render_br': False,
+                    'options': [(None, '---', None)]
+                    + [(x, x.name, x.id) for x in roles if not x.is_internal()],
+                },
+                hint=_('Roles allowed to export data'),
+            )
+            if self.category_class is Category:
+                form.add(
+                    WidgetList,
+                    'statistics_roles',
+                    title=_('Statistics Roles'),
+                    element_type=SingleSelectWidget,
+                    value=self.category.statistics_roles,
+                    add_element_label=_('Add Role'),
+                    element_kwargs={
+                        'render_br': False,
+                        'options': [(None, '---', None)]
+                        + [(x, x.name, x.id) for x in roles if not x.is_internal()],
+                    },
+                    hint=_('Roles with access to the statistics page'),
+                )
+
         form.add_submit('submit', _('Submit'))
         form.add_submit('cancel', _('Cancel'))
         return form
@@ -69,9 +103,11 @@ class CategoryUI:
             form.get_widget('name').set_error(_('This name is already used'))
             raise ValueError()
 
-        self.category.description = form.get_widget('description').parse()
-        if form.get_widget('redirect_url'):
-            self.category.redirect_url = form.get_widget('redirect_url').parse()
+        for attribute in ('description', 'redirect_url', 'export_roles', 'statistics_roles'):
+            widget = form.get_widget(attribute)
+            if widget:
+                setattr(self.category, attribute, widget.parse())
+
         self.category.store()
 
 
@@ -107,10 +143,10 @@ class CategoryPage(Directory):
 
     def edit(self):
         form = self.category_ui.get_form()
-        if form.get_widget('cancel').parse():
+        if form.get_submit() == 'cancel':
             return redirect('..')
 
-        if form.is_submitted() and not form.has_errors():
+        if form.get_submit() == 'submit' and not form.has_errors():
             try:
                 self.category_ui.submit_form(form)
             except ValueError:
@@ -224,7 +260,7 @@ class CategoriesDirectory(Directory):
     def new(self):
         get_response().breadcrumb.append(('new', _('New')))
         category_ui = self.category_ui_class(None)
-        form = category_ui.get_form()
+        form = category_ui.get_form(new=True)
         if form.get_widget('cancel').parse():
             return redirect('.')
 
diff --git a/wcs/api_access.py b/wcs/api_access.py
index c74cd597f..73f041795 100644
--- a/wcs/api_access.py
+++ b/wcs/api_access.py
@@ -14,12 +14,6 @@
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, see <http://www.gnu.org/licenses/>.
 
-import xml.etree.ElementTree as ET
-
-from quixote import get_publisher
-
-from wcs.qommon.misc import xml_node_text
-from wcs.qommon.storage import Equal, Or
 from wcs.qommon.xml_storage import XmlStorableObject
 
 
@@ -60,27 +54,6 @@ class ApiAccess(XmlStorableObject):
     def get_roles(self):
         return self.roles or []
 
-    def export_roles_to_xml(self, element, attribute_name, include_id=False, **kwargs):
-        for role in self.get_roles():
-            sub = ET.SubElement(element, 'role')
-            if include_id:
-                sub.attrib['role-id'] = role.id
-            sub.attrib['role-slug'] = role.slug
-            sub.text = role.name
-
-    def import_roles_from_xml(self, element, include_id=False, **kwargs):
-        criterias = []
-        for sub in element:
-            if include_id and 'role-id' in sub.attrib:
-                criterias.append(Equal('id', sub.attrib['role-id']))
-            elif 'role-slug' in sub.attrib:
-                criterias.append(Equal('slug', sub.attrib['role-slug']))
-            else:
-                role_name = xml_node_text(sub)
-                if role_name:
-                    criterias.append(Equal('name', role_name))
-        return get_publisher().role_class.select([Or(criterias)], order_by='name')
-
     def get_as_api_user(self):
         class RestrictedApiUser:
             # kept as inner class so cannot be pickled
diff --git a/wcs/categories.py b/wcs/categories.py
index 1d03b19b1..c9bc3001b 100644
--- a/wcs/categories.py
+++ b/wcs/categories.py
@@ -33,6 +33,9 @@ class Category(XmlStorableObject):
     position = None
     redirect_url = None
 
+    export_roles = None
+    statistics_roles = None
+
     # declarations for serialization
     XML_NODES = [
         ('name', 'str'),
@@ -40,6 +43,8 @@ class Category(XmlStorableObject):
         ('description', 'str'),
         ('redirect_url', 'str'),
         ('position', 'int'),
+        ('export_roles', 'roles'),
+        ('statistics_roles', 'roles'),
     ]
 
     def __init__(self, name=None):
@@ -125,7 +130,13 @@ class CardDefCategory(Category):
     xml_root_node = 'carddef_category'
 
     # declarations for serialization
-    XML_NODES = [('name', 'str'), ('url_name', 'str'), ('description', 'str'), ('position', 'int')]
+    XML_NODES = [
+        ('name', 'str'),
+        ('url_name', 'str'),
+        ('description', 'str'),
+        ('position', 'int'),
+        ('export_roles', 'roles'),
+    ]
 
 
 Substitutions.register('category_name', category=_('General'), comment=_('Category Name'))
diff --git a/wcs/qommon/xml_storage.py b/wcs/qommon/xml_storage.py
index 2dfcef9e4..5508b8b00 100644
--- a/wcs/qommon/xml_storage.py
+++ b/wcs/qommon/xml_storage.py
@@ -21,7 +21,7 @@ from django.utils.encoding import force_text
 from quixote import get_publisher
 
 from .misc import indent_xml, xml_node_text
-from .storage import StorableObject
+from .storage import Equal, Or, StorableObject
 
 
 class XmlStorableObject(StorableObject):
@@ -75,6 +75,14 @@ class XmlStorableObject(StorableObject):
         indent_xml(x)
         return ET.tostring(x)
 
+    def export_roles_to_xml(self, element, attribute_name, include_id=False, **kwargs):
+        for role in getattr(self, attribute_name, None) or []:
+            sub = ET.SubElement(element, 'role')
+            if include_id:
+                sub.attrib['role-id'] = role.id
+            sub.attrib['role-slug'] = role.slug
+            sub.text = role.name
+
     @classmethod
     def import_from_xml(cls, fd, charset=None, include_id=False):
         try:
@@ -127,3 +135,16 @@ class XmlStorableObject(StorableObject):
         for item in element.findall('item'):
             value.append(item.text)
         return value
+
+    def import_roles_from_xml(self, element, include_id=False, **kwargs):
+        criterias = []
+        for sub in element:
+            if include_id and 'role-id' in sub.attrib:
+                criterias.append(Equal('id', sub.attrib['role-id']))
+            elif 'role-slug' in sub.attrib:
+                criterias.append(Equal('slug', sub.attrib['role-slug']))
+            else:
+                role_name = xml_node_text(sub)
+                if role_name:
+                    criterias.append(Equal('name', role_name))
+        return get_publisher().role_class.select([Or(criterias)], order_by='name')
diff --git a/wcs/templates/wcs/backoffice/category.html b/wcs/templates/wcs/backoffice/category.html
index 1a9c2dc72..0f23b1e92 100644
--- a/wcs/templates/wcs/backoffice/category.html
+++ b/wcs/templates/wcs/backoffice/category.html
@@ -29,4 +29,28 @@
   {% endif %}
 {% endwith %}
 </div>
+
+{% if category.export_roles or category.statistics_roles %}
+<div class="section">
+<h3>{% trans "Permissions" %}</h3>
+<div>
+  <ul>
+    {% if category.export_roles %}
+    <li>{% trans "Export roles:" %}
+      <ul>
+      {% for role in category.export_roles %}<li>{{ role.name }}</li>{% endfor %}
+      </ul>
+    </li>
+    {% endif %}
+    {% if category.statistics_roles %}
+    <li>{% trans "Statistics roles:" %}
+      <ul>
+      {% for role in category.statistics_roles %}<li>{{ role.name }}</li>{% endfor %}
+      </ul>
+    </li>
+    {% endif %}
+  </ul>
+</div>
+</div>
+{% endif %}
 {% endblock %}
-- 
2.31.1

