From 0fa25a767bec2d8b7703d76de8f646291d7cd038 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Tue, 13 Oct 2020 15:10:49 +0200
Subject: [PATCH 3/3] backoffice: set default view as default (#47492)

---
 tests/backoffice_pages/test_custom_view.py | 195 +++++++++++++++++++++
 wcs/api.py                                 |   4 +-
 wcs/backoffice/management.py               |  83 ++++++++-
 wcs/custom_views.py                        |   5 +-
 wcs/qommon/static/js/wcs.listing.js        |   5 +
 wcs/sql.py                                 |  14 +-
 6 files changed, 292 insertions(+), 14 deletions(-)

diff --git a/tests/backoffice_pages/test_custom_view.py b/tests/backoffice_pages/test_custom_view.py
index fcce97a1..ca50063f 100644
--- a/tests/backoffice_pages/test_custom_view.py
+++ b/tests/backoffice_pages/test_custom_view.py
@@ -13,6 +13,7 @@ from wcs.qommon.ident.password_accounts import PasswordAccount
 from wcs.qommon.http_request import HTTPRequest
 from wcs.carddef import CardDef
 from wcs.formdef import FormDef
+from wcs.roles import Role
 from wcs import fields
 
 from utilities import (get_app, login, create_temporary_pub, clean_temporary_pub)
@@ -291,6 +292,169 @@ def test_backoffice_custom_view_visibility(pub):
             [('custom-test-view', 'owner'), ('shared-view', 'any'), ('shared-view-2', 'any')])
 
 
+def test_backoffice_custom_view_is_default(pub):
+    create_superuser(pub)
+
+    FormDef.wipe()
+    pub.custom_view_class.wipe()
+    formdef = FormDef()
+    formdef.name = 'form title'
+    formdef.fields = []
+    formdef.workflow_roles = {'_receiver': 1}
+    formdef.store()
+
+    # private custom view
+    agent = pub.user_class(name='agent')
+    agent.roles = [formdef.workflow_roles['_receiver']]
+    agent.store()
+    account = PasswordAccount(id='agent')
+    account.set_password('agent')
+    account.user_id = agent.id
+    account.store()
+    app = login(get_app(pub), username='agent', password='agent')
+    resp = app.get('/backoffice/management/form-title/')
+    resp = resp.forms['listing-settings'].submit()
+    resp.forms['save-custom-view']['title'] = 'view 1'
+    resp.forms['save-custom-view']['is_default'] = True
+    resp = resp.forms['save-custom-view'].submit()
+
+    # other private custom view
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/management/form-title/')
+    resp = resp.forms['listing-settings'].submit()
+    resp.forms['save-custom-view']['title'] = 'view 2'
+    resp.forms['save-custom-view']['visibility'] = 'owner'
+    resp.forms['save-custom-view']['is_default'] = True
+    resp = resp.forms['save-custom-view'].submit()
+
+    # shared custom view
+    resp = app.get('/backoffice/management/form-title/')
+    resp = resp.forms['listing-settings'].submit()
+    resp.forms['save-custom-view']['title'] = 'view 3'
+    resp.forms['save-custom-view']['visibility'] = 'any'
+    resp.forms['save-custom-view']['is_default'] = True
+    resp = resp.forms['save-custom-view'].submit()
+
+    assert pub.custom_view_class.count() == 3
+    assert pub.custom_view_class.get(1).is_default is True  # simple user - private
+    assert pub.custom_view_class.get(2).is_default is True  # super user - private
+    assert pub.custom_view_class.get(3).is_default is True  # super user - shared
+
+    # not possible to define more than one default private view
+    resp = app.get('/backoffice/management/form-title/')
+    resp = resp.forms['listing-settings'].submit()
+    resp.forms['save-custom-view']['title'] = 'view 4'
+    resp.forms['save-custom-view']['visibility'] = 'owner'
+    resp.forms['save-custom-view']['is_default'] = True
+    resp = resp.forms['save-custom-view'].submit()
+    assert pub.custom_view_class.count() == 4
+    assert pub.custom_view_class.get(1).is_default is True  # simple user - private
+    assert pub.custom_view_class.get(2).is_default is False  # super user - private
+    assert pub.custom_view_class.get(3).is_default is True  # super user - shared
+    assert pub.custom_view_class.get(4).is_default is True  # super user - private 2
+
+    # not possible to define more than one default shared view
+    resp = app.get('/backoffice/management/form-title/')
+    resp = resp.forms['listing-settings'].submit()
+    resp.forms['save-custom-view']['title'] = 'view 5'
+    resp.forms['save-custom-view']['visibility'] = 'any'
+    resp.forms['save-custom-view']['is_default'] = True
+    resp = resp.forms['save-custom-view'].submit()
+    assert pub.custom_view_class.count() == 5
+    assert pub.custom_view_class.get(1).is_default is True  # simple user - private
+    assert pub.custom_view_class.get(2).is_default is False  # super user - private
+    assert pub.custom_view_class.get(3).is_default is False  # super user - shared
+    assert pub.custom_view_class.get(4).is_default is True  # super user - private 2
+    assert pub.custom_view_class.get(5).is_default is True  # super user - shared 2
+
+
+def test_backoffice_default_custom_view(pub):
+    create_superuser(pub)
+
+    FormDef.wipe()
+    pub.custom_view_class.wipe()
+    formdef = FormDef()
+    formdef.name = 'form title'
+    formdef.fields = [
+        fields.ItemField(
+            id='1', label='field 1', type='item',
+            items=['foo', 'bar', 'baz'],
+            display_locations=['validation', 'summary', 'listings']),
+    ]
+    formdef.workflow_roles = {'_receiver': 1}
+    formdef.store()
+
+    formdef.data_class().wipe()
+    for i in range(3):
+        formdata = formdef.data_class()()
+        formdata.data = {}
+        if i == 0:
+            formdata.data['1'] = 'foo'
+            formdata.data['1_display'] = 'foo'
+        else:
+            formdata.data['1'] = 'baz'
+            formdata.data['1_display'] = 'baz'
+        if i < 2:
+            formdata.jump_status('new')
+        formdata.store()
+
+    # define a shared defautl view
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/management/form-title/')
+    # columns
+    resp.forms['listing-settings']['user-label'].checked = False
+    resp.forms['listing-settings']['1'].checked = False
+    # filters
+    resp.forms['listing-settings']['filter-1'].checked = True
+    resp = resp.forms['listing-settings'].submit()
+    resp.forms['listing-settings']['filter-1-value'] = 'baz'
+    resp.forms['listing-settings']['filter'] = 'all'
+    resp = resp.forms['listing-settings'].submit()
+    # view
+    resp.forms['save-custom-view']['title'] = 'shared custom test view'
+    resp.forms['save-custom-view']['visibility'] = 'any'
+    resp.forms['save-custom-view']['is_default'] = True
+    resp = resp.forms['save-custom-view'].submit()
+    assert resp.location.endswith('/shared-custom-test-view/')
+    resp = resp.follow()
+    assert resp.text.count('<span>User Label</span>') == 0
+    assert resp.text.count('<span>field 1</span>') == 0
+    assert resp.text.count('<tr') == 3
+
+    # check that default view is applied
+    resp = app.get('/backoffice/management/form-title/')
+    assert resp.text.count('<span>User Label</span>') == 0
+    assert resp.text.count('<span>field 1</span>') == 0
+    assert resp.text.count('<tr') == 3
+
+    # define a user default view
+    # columns
+    resp.forms['listing-settings']['1'].checked = True
+    # filters
+    resp.forms['listing-settings']['filter-1'].checked = True
+    resp = resp.forms['listing-settings'].submit()
+    resp.forms['listing-settings']['filter-1-value'] = 'foo'
+    resp.forms['listing-settings']['filter'] = 'all'
+    resp = resp.forms['listing-settings'].submit()
+    # view
+    resp.forms['save-custom-view']['title'] = 'private custom test view'
+    resp.forms['save-custom-view']['visibility'] = 'owner'
+    resp.forms['save-custom-view']['is_default'] = True
+
+    resp = resp.forms['save-custom-view'].submit()
+    assert resp.location.endswith('/user-private-custom-test-view/')
+    resp = resp.follow()
+    assert resp.text.count('<span>User Label</span>') == 0
+    assert resp.text.count('<span>field 1</span>') == 1
+    assert resp.text.count('<tr') == 2
+
+    # check that private default view is applied, and not shared default view
+    resp = app.get('/backoffice/management/form-title/')
+    assert resp.text.count('<span>User Label</span>') == 0
+    assert resp.text.count('<span>field 1</span>') == 1
+    assert resp.text.count('<tr') == 2
+
+
 def test_backoffice_missing_custom_view(pub):
     create_superuser(pub)
 
@@ -348,6 +512,37 @@ def test_carddata_custom_view(pub, studio):
     resp = resp.follow()
 
 
+def test_carddata_custom_view_is_default(pub):
+    user = create_superuser(pub)
+    Role.wipe()
+    role = Role(name='foobar')
+    role.store()
+    user.roles = [role.id]
+    user.store()
+
+    CardDef.wipe()
+    pub.custom_view_class.wipe()
+    carddef = CardDef()
+    carddef.name = 'foo'
+    carddef.fields = [
+        fields.StringField(id='1', label='Test', type='string', varname='foo'),
+    ]
+    carddef.backoffice_submission_roles = user.roles
+    carddef.digest_template = '{{ form_var_foo }}'
+    carddef.store()
+    carddef.data_class().wipe()
+
+    # datasource custom view
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/data/foo/')
+    resp = resp.forms['listing-settings'].submit()
+    resp.forms['save-custom-view']['title'] = 'view 3'
+    resp.forms['save-custom-view']['visibility'] = 'datasource'
+    resp.forms['save-custom-view']['is_default'] = True
+    resp = resp.forms['save-custom-view'].submit()
+    assert pub.custom_view_class.get(1).is_default is False  # not for datasource
+
+
 def test_backoffice_custom_view_keep_filters(pub):
     create_superuser(pub)
     datasource = {
diff --git a/wcs/api.py b/wcs/api.py
index 4ad41419..04575929 100644
--- a/wcs/api.py
+++ b/wcs/api.py
@@ -216,12 +216,12 @@ class ApiFormPage(BackofficeFormPage):
                 else:
                     # custom view
                     for view in self.get_custom_views([Equal('visibility', 'any'), Equal('slug', path[1])]):
-                        self.view = view
+                        self._view = view
                         path = [mode]
 
         if len(path) >= 2 and path[1] == 'ics':
             for view in self.get_custom_views([Equal('visibility', 'any'), Equal('slug', path[0])]):
-                self.view = view
+                self._view = view
                 path = path[1:]
 
         self.is_webhook = False
diff --git a/wcs/backoffice/management.py b/wcs/backoffice/management.py
index 911f8330..39a31c05 100644
--- a/wcs/backoffice/management.py
+++ b/wcs/backoffice/management.py
@@ -1027,7 +1027,9 @@ class FormPage(Directory):
     _q_exports = ['', 'csv', 'stats', 'xls', 'ods', 'json', 'export', 'map',
                   'geojson', ('filter-options', 'filter_options'),
                   ('save-view', 'save_view'), ('delete-view', 'delete_view'),]
-    view = None
+    _view = None
+    default_view = None
+    use_default_view = False
     admin_permission = 'forms'
     formdef_class = FormDef
     search_label = N_('Search in form content')
@@ -1043,9 +1045,40 @@ class FormPage(Directory):
                 get_response().breadcrumb.append((component + '/', self.formdef.name))
         else:
             self.formdef = formdef
-            self.view = view
+            self._view = view
             if update_breadcrumbs:
                 get_response().breadcrumb.append((view.get_url_slug() + '/', view.title))
+        self.set_default_view()
+
+    def set_default_view(self):
+        if not get_request():
+            return
+        custom_views = list(self.get_custom_views())
+
+        # search for first default user custom view
+        for view in custom_views:
+            if view.visibility != 'owner':
+                continue
+            if not view.is_default:
+                continue
+            self.default_view = view
+            return
+
+        # default user custom view not found, search in 'any' custom views
+        for view in custom_views:
+            if view.visibility != 'any':
+                continue
+            if not view.is_default:
+                continue
+            self.default_view = view
+            return
+
+    @property
+    def view(self):
+        view = self._view
+        if self.use_default_view:
+            view = view or self.default_view
+        return view
 
     def check_access(self, api_name=None):
         session = get_session()
@@ -1093,14 +1126,16 @@ class FormPage(Directory):
             r += htmltext('<ul id="sidebar-custom-views">')
             view_type = 'map' if self.view_type == 'map' else ''
             for view in sorted(views, key=lambda x: getattr(x, 'title')):
-                if self.view:
-                    active = bool(self.view.get_url_slug() == view.get_url_slug())
+                if self._view:
+                    active = bool(self._view.get_url_slug() == view.get_url_slug())
                     r += htmltext('<li class="active">' if active else '<li>')
                     r += htmltext('<a href="../%s/%s">%s</a>') % (view.get_url_slug(), view_type, view.title)
                 else:
                     r += htmltext('<li><a href="%s/%s">%s</a>') % (view.get_url_slug(), view_type, view.title)
                 if view.visibility == 'datasource':
                     r += htmltext(' <span class="as-data-source">(%s)</span>') % _('for data sources')
+                elif self.default_view and view.id == self.default_view.id:
+                    r += htmltext(' <span class="default-custom-view">(%s)</span>') % _('default')
                 r += htmltext('</li>')
             r += htmltext('</ul>')
         return r.getvalue()
@@ -1479,9 +1514,28 @@ class FormPage(Directory):
             if isinstance(self.formdef, CardDef) and self.formdef.digest_template:
                 options.append(('datasource', _('as data source'), 'datasource'))
 
-            form.add(RadiobuttonsWidget, 'visibility', title=_('Visibility'),
-                    value=self.view.visibility if self.view else 'owner',
-                    options=options)
+            form.add(
+                RadiobuttonsWidget,
+                'visibility',
+                title=_('Visibility'),
+                value=self.view.visibility if self.view else 'owner',
+                options=options,
+                attrs={'data-dynamic-display-parent': 'true'})
+            form.add(
+                CheckboxWidget,
+                'is_default',
+                title=_('Set as default view'),
+                value=self.view.is_default if self.view else False,
+                attrs={
+                    'data-dynamic-display-child-of': 'visibility',
+                    'data-dynamic-display-value-in': 'owner|any',
+                })
+        else:
+            form.add(
+                CheckboxWidget,
+                'is_default',
+                title=_('Set as default view'),
+                value=self.view.is_default if self.view else False)
         if self.view and (self.view.user_id == get_request().user.id or
                           get_publisher().get_backoffice_root().is_accessible(self.admin_permission)):
             form.add(CheckboxWidget, 'update', title=_('Update existing view settings'), value=True)
@@ -1505,9 +1559,23 @@ class FormPage(Directory):
         if not custom_view.columns['list']:
             get_session().message = ('error', _('Views must have at least one column.'))
             return redirect('.')
+        if form.get_widget('is_default'):
+            custom_view.is_default = form.get_widget('is_default').parse()
         if form.get_widget('visibility'):
             custom_view.visibility = form.get_widget('visibility').parse()
+            if custom_view.visibility == 'datasource':
+                custom_view.is_default = False
         custom_view.store()
+
+        if custom_view.is_default and custom_view.visibility != 'datasource':
+            # need to clean other views to have only one default per owner/any visibility
+            for view in self.get_custom_views():
+                if view.id == custom_view.id:
+                    continue
+                if custom_view.visibility == view.visibility and view.is_default:
+                    view.is_default = False
+                    view.store()
+
         if self.view:
             return redirect('../' + custom_view.get_url_slug() + '/')
         else:
@@ -1788,6 +1856,7 @@ class FormPage(Directory):
         if 'job' in get_request().form:
             return self.job_multi()
 
+        self.use_default_view = True
         fields = self.get_fields_from_query()
         selected_filter = self.get_filter_from_query()
         criterias = self.get_criterias_from_query()
diff --git a/wcs/custom_views.py b/wcs/custom_views.py
index 26de1fbe..7ae95e72 100644
--- a/wcs/custom_views.py
+++ b/wcs/custom_views.py
@@ -32,6 +32,7 @@ class CustomView(StorableObject):
     visibility = 'owner'
     formdef_type = None
     formdef_id = None
+    is_default = False
     columns = None
     filters = None
     order_by = None
@@ -57,12 +58,12 @@ class CustomView(StorableObject):
         self.formdef_type = value.xml_root_node
 
     def match(self, user, formdef):
-        if self.visibility == 'owner' and self.user_id != str(user.id):
-            return False
         if self.formdef_type != formdef.xml_root_node:
             return False
         if self.formdef_id != str(formdef.id):
             return False
+        if self.visibility == 'owner' and self.user_id != str(user.id):
+            return False
         return True
 
     def set_from_qs(self, qs):
diff --git a/wcs/qommon/static/js/wcs.listing.js b/wcs/qommon/static/js/wcs.listing.js
index f9984382..28cb8f41 100644
--- a/wcs/qommon/static/js/wcs.listing.js
+++ b/wcs/qommon/static/js/wcs.listing.js
@@ -334,6 +334,7 @@ $(function() {
               }
             ]
     });
+    $(document).trigger('wcs:dialog-loaded', $(dialog));
     return false;
   });
 
@@ -376,4 +377,8 @@ $(function() {
   prepare_page_links();
   prepare_row_links();
   prepare_column_headers();
+
+  $('a[data-base-href]').each(function(idx, elem) {
+    $(elem).attr('href', $(elem).data('base-href') + '?' + $('form#listing-settings').serialize());
+  });
 });
diff --git a/wcs/sql.py b/wcs/sql.py
index aa326b17..85ccd986 100644
--- a/wcs/sql.py
+++ b/wcs/sql.py
@@ -803,6 +803,7 @@ def do_custom_views_table():
                                         visibility varchar,
                                         formdef_type varchar,
                                         formdef_id varchar,
+                                        is_default boolean,
                                         order_by varchar,
                                         columns jsonb,
                                         filters jsonb
@@ -814,6 +815,10 @@ def do_custom_views_table():
 
     needed_fields = set([x[0] for x in CustomView._table_static_fields])
 
+    # migrations
+    if 'is_default' not in existing_fields:
+        cur.execute('''ALTER TABLE %s ADD COLUMN is_default boolean DEFAULT FALSE''' % table_name)
+
     # delete obsolete fields
     for field in (existing_fields - needed_fields):
         cur.execute('''ALTER TABLE %s DROP COLUMN %s''' % (table_name, field))
@@ -2298,6 +2303,7 @@ class CustomView(SqlMixin, wcs.custom_views.CustomView):
         ('visibility', 'varchar'),
         ('formdef_type', 'varchar'),
         ('formdef_id', 'varchar'),
+        ('is_default', 'boolean'),
         ('order_by', 'varchar'),
         ('columns', 'jsonb'),
         ('filters', 'jsonb'),
@@ -2315,6 +2321,7 @@ class CustomView(SqlMixin, wcs.custom_views.CustomView):
             'visibility': self.visibility,
             'formdef_type': self.formdef_type,
             'formdef_id': self.formdef_id,
+            'is_default': self.is_default,
             'order_by': self.order_by,
             'columns': self.columns,
             'filters': self.filters,
@@ -2358,7 +2365,7 @@ class CustomView(SqlMixin, wcs.custom_views.CustomView):
         for field, value in zip(cls._table_static_fields, tuple(row)):
             if field[1] == 'varchar':
                 setattr(o, field[0], str_encode(value))
-            elif field[1] == 'jsonb':
+            elif field[1] in ('jsonb', 'boolean'):
                 setattr(o, field[0], value)
         return o
 
@@ -2817,8 +2824,9 @@ def migrate():
         # 25: create session_table
         # 32: add last_update_time column to session table
         do_session_table()
-    if sql_level < 37:
-        # 37: create custom_views tabl
+    if sql_level < 43:
+        # 37: create custom_views table
+        # 43: add is_default column to custom_views table
         do_custom_views_table()
     if sql_level < 30:
         # 30: actually remove evo.who on anonymised formdatas
-- 
2.28.0

