From 06877712274b97f7435ec1777cc71fc49f44cd66 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Wed, 29 Jun 2022 22:11:19 +0200
Subject: [PATCH 2/2] studio: admin can see all changes (#62953)

---
 tests/admin_pages/test_studio.py          | 25 +++++++++--
 wcs/backoffice/studio.py                  | 54 ++++++++++++++++++++++-
 wcs/snapshots.py                          |  4 +-
 wcs/sql.py                                | 42 ++++++++++++++----
 wcs/templates/wcs/backoffice/changes.html | 19 ++++++++
 wcs/templates/wcs/backoffice/studio.html  |  3 ++
 6 files changed, 130 insertions(+), 17 deletions(-)
 create mode 100644 wcs/templates/wcs/backoffice/changes.html

diff --git a/tests/admin_pages/test_studio.py b/tests/admin_pages/test_studio.py
index f6db5de75..3a0d27b7b 100644
--- a/tests/admin_pages/test_studio.py
+++ b/tests/admin_pages/test_studio.py
@@ -218,7 +218,6 @@ def test_studio_home_recent_changes(sql_pub):
     pub.cfg['admin-permissions'].update({'settings': ['x']})
     pub.write_cfg()
 
-    app = login(get_app(pub))
     resp = app.get('/backoffice/studio/')
     # no access to settings
     for i in range(6):
@@ -256,7 +255,6 @@ def test_studio_home_recent_changes(sql_pub):
     pub.cfg['admin-permissions'].update({'settings': ['x'], 'forms': ['x']})
     pub.write_cfg()
 
-    app = login(get_app(pub))
     resp = app.get('/backoffice/studio/')
     # no access to settings or forms
     for i in range(6):
@@ -292,7 +290,6 @@ def test_studio_home_recent_changes(sql_pub):
     pub.cfg['admin-permissions'].update({'settings': ['x'], 'forms': ['x'], 'workflows': ['x']})
     pub.write_cfg()
 
-    app = login(get_app(pub))
     resp = app.get('/backoffice/studio/')
     # no access to settings, forms or workflows
     for i in range(6):
@@ -317,7 +314,6 @@ def test_studio_home_recent_changes(sql_pub):
         assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][i].id in resp
 
     objects[CardDef.xml_root_node][5].remove_self()
-    app = login(get_app(pub))
     resp = app.get('/backoffice/studio/')
     # too old
     assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][0].id not in resp
@@ -327,6 +323,27 @@ def test_studio_home_recent_changes(sql_pub):
         # deleted
     assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][5].id not in resp
 
+    # all changes page: adin user can see all changes (dependig on permissions)
+    resp = resp.click(href='all-changes/')
+    assert '(1-6/6)' in resp
+    # he can also see changes from other users
+    for snapshot in pub.snapshot_class.select():
+        snapshot.user_id = other_user.id
+        snapshot.store()
+
+    pub.cfg['admin-permissions'] = {}
+    pub.write_cfg()
+    resp = app.get('/backoffice/studio/all-changes/')
+    assert '(1-20/42)' in resp
+    resp = resp.click('<!--Next Page-->')
+    assert '21-40/42' in resp.text
+    resp = resp.click('<!--Next Page-->')
+    assert '41-42/42' in resp.text
+
+    user.is_admin = False
+    user.store()
+    app.get('/backoffice/studio/all-changes/', status=403)
+
 
 def test_studio_workflows(pub):
     create_superuser(pub)
diff --git a/wcs/backoffice/studio.py b/wcs/backoffice/studio.py
index 913b04f3c..1c44eb223 100644
--- a/wcs/backoffice/studio.py
+++ b/wcs/backoffice/studio.py
@@ -24,17 +24,65 @@ from wcs.carddef import CardDef
 from wcs.data_sources import NamedDataSource
 from wcs.formdef import FormDef
 from wcs.mail_templates import MailTemplate
-from wcs.qommon import _, pgettext, template
+from wcs.qommon import _, misc, pgettext, template
+from wcs.qommon.backoffice.listing import pagination_links
 from wcs.qommon.backoffice.menu import html_top
 from wcs.qommon.form import get_response
 from wcs.workflows import Workflow
 from wcs.wscalls import NamedWsCall
 
 
+class ChangesDirectory(Directory):
+    _q_exports = ['']
+
+    def _q_index(self):
+        get_response().breadcrumb.append(('all-changes/', pgettext('studio', 'All changes')))
+        html_top(pgettext('studio', 'All Changes'))
+        limit = misc.get_int_or_400(
+            get_request().form.get('limit', get_publisher().get_site_option('default-page-size')) or 20
+        )
+        offset = misc.get_int_or_400(get_request().form.get('offset', 0))
+
+        backoffice_root = get_publisher().get_backoffice_root()
+        object_types = []
+        if backoffice_root.is_accessible('workflows'):
+            object_types += [Workflow, MailTemplate]
+        if backoffice_root.is_accessible('forms'):
+            object_types += [NamedDataSource, BlockDef, FormDef]
+        if backoffice_root.is_accessible('workflows'):
+            object_types += [NamedDataSource]
+        if backoffice_root.is_accessible('settings'):
+            object_types += [NamedDataSource, NamedWsCall]
+        if backoffice_root.is_accessible('cards'):
+            object_types += [CardDef]
+        object_types = [ot.xml_root_node for ot in object_types]
+
+        objects = []
+        links = ''
+        if get_publisher().snapshot_class:
+            objects = get_publisher().snapshot_class.get_recent_changes(
+                object_types=object_types, limit=limit, offset=offset
+            )
+            total_count = get_publisher().snapshot_class.count_recent_changes(object_types=object_types)
+            links = pagination_links(offset, limit, total_count, load_js=False)
+
+        return template.QommonTemplateResponse(
+            templates=['wcs/backoffice/changes.html'],
+            context={
+                'objects': objects,
+                'pagination_links': links,
+            },
+        )
+
+    def is_accessible(self, user):
+        return user.is_admin
+
+
 class StudioDirectory(Directory):
-    _q_exports = ['', 'deprecations', ('logged-errors', 'logged_errors_dir')]
+    _q_exports = ['', 'deprecations', ('logged-errors', 'logged_errors_dir'), ('all-changes', 'changes_dir')]
 
     deprecations = DeprecationsDirectory()
+    changes_dir = ChangesDirectory()
 
     def __init__(self):
         self.logged_errors_dir = LoggedErrorsDirectory(parent_dir=self)
@@ -71,10 +119,12 @@ class StudioDirectory(Directory):
         if backoffice_root.is_accessible('cards'):
             object_types += [CardDef]
 
+        user = get_request().user
         context = {
             'has_sidebar': False,
             'extra_links': extra_links,
             'recent_errors': LoggedErrorsDirectory.get_errors(offset=0, limit=5)[0],
+            'show_all_changes': user and user.is_admin,
         }
         if get_publisher().snapshot_class:
             context['recent_objects'] = get_publisher().snapshot_class.get_recent_changes(
diff --git a/wcs/snapshots.py b/wcs/snapshots.py
index 0f1bd599b..090b05d19 100644
--- a/wcs/snapshots.py
+++ b/wcs/snapshots.py
@@ -184,8 +184,8 @@ class Snapshot:
             obj.store()
 
     @classmethod
-    def get_recent_changes(cls, object_types, user):
-        elements = cls._get_recent_changes(object_types, user)
+    def get_recent_changes(cls, object_types=None, user=None, limit=5, offset=0):
+        elements = cls._get_recent_changes(object_types=object_types, user=user, limit=limit, offset=offset)
         instances = []
         for object_type, object_id, snapshot_timestamp in elements:
             klass = cls.get_class(object_type)
diff --git a/wcs/sql.py b/wcs/sql.py
index cc67699cf..5e6df9d3e 100644
--- a/wcs/sql.py
+++ b/wcs/sql.py
@@ -3625,22 +3625,46 @@ class Snapshot(SqlMixin, wcs.snapshots.Snapshot):
         return cls.get(row[0])
 
     @classmethod
-    def _get_recent_changes(cls, object_types, user):
+    def _get_recent_changes(cls, object_types, user=None, limit=5, offset=0):
         conn, cur = get_connection_and_cursor()
-        sql_statement = '''SELECT object_type, object_id, MAX(timestamp) AS m
-                           FROM snapshots
-                           WHERE object_type IN %(object_types)s
-                           AND user_id = %(user_id)s
-                           GROUP BY object_type, object_id
-                           ORDER BY m DESC
-                           LIMIT 5'''
-        parameters = {'object_types': tuple(object_types), 'user_id': str(user.id)}
+        clause = [Contains('object_type', object_types)]
+        if user is not None:
+            clause.append(Equal('user_id', str(user.id)))
+        where_clauses, parameters, dummy = parse_clause(clause)
+
+        sql_statement = 'SELECT object_type, object_id, MAX(timestamp) AS m FROM snapshots'
+        sql_statement += ' WHERE ' + ' AND '.join(where_clauses)
+        sql_statement += ' GROUP BY object_type, object_id ORDER BY m DESC'
+
+        if limit:
+            sql_statement += ' LIMIT %(limit)s'
+            parameters['limit'] = limit
+        if offset:
+            sql_statement += ' OFFSET %(offset)s'
+            parameters['offset'] = offset
+
         cur.execute(sql_statement, parameters)
         result = cur.fetchall()
         conn.commit()
         cur.close()
         return result
 
+    @classmethod
+    def count_recent_changes(cls, object_types):
+        conn, cur = get_connection_and_cursor()
+
+        clause = [Contains('object_type', object_types)]
+        where_clauses, parameters, dummy = parse_clause(clause)
+        sql_statement = 'SELECT COUNT(*) FROM (SELECT object_type, object_id FROM snapshots'
+        sql_statement += ' WHERE ' + ' AND '.join(where_clauses)
+        sql_statement += ' GROUP BY object_type, object_id) AS s'
+
+        cur.execute(sql_statement, parameters)
+        count = cur.fetchone()[0]
+        conn.commit()
+        cur.close()
+        return count
+
 
 class LoggedError(SqlMixin, wcs.logged_errors.LoggedError):
     _table_name = 'loggederrors'
diff --git a/wcs/templates/wcs/backoffice/changes.html b/wcs/templates/wcs/backoffice/changes.html
new file mode 100644
index 000000000..4818a8835
--- /dev/null
+++ b/wcs/templates/wcs/backoffice/changes.html
@@ -0,0 +1,19 @@
+{% extends "wcs/backoffice/base.html" %}
+{% load i18n %}
+
+{% block appbar-title %}{% trans "All changes" context 'studio' %}{% endblock %}
+
+{% block content %}
+<ul class="objects-list single-links">
+  {% for obj in objects %}
+  <li>
+    <a href="{{ obj.get_admin_url }}">
+      {{ obj.name }}
+      <span class="extra-info">{{ obj.snapshot_timestamp }}</span>
+      <span class="badge">{{ obj.verbose_name }}</span>
+    </a>
+  </li>
+  {% endfor %}
+</ul>
+{{ pagination_links|safe }}
+{% endblock %}
diff --git a/wcs/templates/wcs/backoffice/studio.html b/wcs/templates/wcs/backoffice/studio.html
index c9277d10e..f1680c8f6 100644
--- a/wcs/templates/wcs/backoffice/studio.html
+++ b/wcs/templates/wcs/backoffice/studio.html
@@ -40,6 +40,9 @@
                 <span class="timestamp">{{ obj.snapshot_timestamp }}</span></li>
         {% endfor %}
       </ul>
+      {% if show_all_changes %}
+        <p><a class="all-changes pk-button" href="all-changes/">{% trans "See all changes" %}</a></p>
+      {% endif %}
     </div>
 
     <div class="errors-and-deprecations">
-- 
2.35.1

