From cda0cc878ce924a9861fef4494bde5bce879bdd2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Tue, 12 Jan 2021 14:55:54 +0100
Subject: [PATCH] snapshots: add label, comment, timestamp and user in sidebar
 (#50009)

---
 tests/test_snapshots.py   | 21 ++++++++++++++-------
 wcs/admin/data_sources.py |  2 ++
 wcs/admin/fields.py       |  4 +++-
 wcs/admin/forms.py        |  1 +
 wcs/admin/utils.py        | 17 +++++++++++++++++
 wcs/admin/workflows.py    |  1 +
 wcs/admin/wscalls.py      |  2 ++
 wcs/backoffice/cards.py   |  1 +
 wcs/snapshots.py          |  1 +
 9 files changed, 42 insertions(+), 8 deletions(-)

diff --git a/tests/test_snapshots.py b/tests/test_snapshots.py
index 341d8d57..0f137b16 100644
--- a/tests/test_snapshots.py
+++ b/tests/test_snapshots.py
@@ -12,6 +12,7 @@ from wcs.carddef import CardDef
 from wcs.data_sources import NamedDataSource
 from wcs.formdef import FormDef
 from wcs.qommon.form import UploadedFile
+from wcs.qommon.misc import localstrftime
 from wcs.workflows import Workflow
 from wcs.workflows import ExportToModel
 from wcs.wscalls import NamedWsCall
@@ -236,7 +237,8 @@ def test_block_snapshot_browse(pub, blocks_feature):
     resp = app.get('/backoffice/forms/blocks/%s/history/' % blockdef.id)
     snapshot = pub.snapshot_class.select_object_history(blockdef)[0]
     resp = resp.click(href='%s/view/' % snapshot.id)
-    assert 'This block of fields is readonly.' in resp
+    assert 'This block of fields is readonly.' in resp.text
+    assert '<p>%s</p>' % localstrftime(snapshot.timestamp) in resp.text
 
 
 def test_card_snapshot_browse(pub):
@@ -274,7 +276,8 @@ def test_card_snapshot_browse(pub):
     resp = app.get('/backoffice/cards/%s/history/' % carddef.id)
     snapshot = pub.snapshot_class.select_object_history(carddef)[0]
     resp = resp.click(href='%s/view/' % snapshot.id)
-    assert 'This card model is readonly' in resp
+    assert 'This card model is readonly' in resp.text
+    assert '<p>%s</p>' % localstrftime(snapshot.timestamp) in resp.text
     resp = resp.click('Geolocation')
     assert [x[0].name for x in resp.form.fields.values() if x[0].tag == 'button'] == ['cancel']
     assert pub.custom_view_class.count() == 0  # custom views are not restore on preview
@@ -299,7 +302,8 @@ def test_datasource_snapshot_browse(pub):
     resp = app.get('/backoffice/forms/data-sources/%s/history/' % datasource.id)
     snapshot = pub.snapshot_class.select_object_history(datasource)[0]
     resp = resp.click(href='%s/view/' % snapshot.id)
-    assert 'This data source is readonly' in resp
+    assert 'This data source is readonly' in resp.text
+    assert '<p>%s</p>' % localstrftime(snapshot.timestamp) in resp.text
     with pytest.raises(IndexError):
         resp = resp.click('Edit')
 
@@ -333,7 +337,8 @@ def test_form_snapshot_browse(pub, formdef_with_history):
     resp = app.get('/backoffice/forms/%s/history/' % formdef_with_history.id)
     snapshot = pub.snapshot_class.select_object_history(formdef_with_history)[0]
     resp = resp.click(href='%s/view/' % snapshot.id)
-    assert 'This form is readonly' in resp
+    assert 'This form is readonly' in resp.text
+    assert '<p>%s</p>' % localstrftime(snapshot.timestamp) in resp.text
     resp = resp.click('Description')
     assert resp.form['description'].value == 'this is a description (5)'
     assert [x[0].name for x in resp.form.fields.values() if x[0].tag == 'button'] == ['cancel']
@@ -357,7 +362,8 @@ def test_workflow_snapshot_browse(pub):
     resp = app.get('/backoffice/workflows/%s/history/' % workflow.id)
     snapshot = pub.snapshot_class.select_object_history(workflow)[0]
     resp = resp.click(href='%s/view/' % snapshot.id)
-    assert 'This workflow is readonly' in resp
+    assert 'This workflow is readonly' in resp.text
+    assert '<p>%s</p>' % localstrftime(snapshot.timestamp) in resp.text
 
 
 def test_workflow_with_model_snapshot_browse(pub):
@@ -397,7 +403,7 @@ def test_workflow_with_model_snapshot_browse(pub):
         resp = app.get('/backoffice/workflows/%s/history/' % workflow.id)
         snapshot = pub.snapshot_class.select_object_history(workflow)[0]
         resp = resp.click(href='%s/view/' % snapshot.id)
-        assert 'This workflow is readonly' in resp
+        assert 'This workflow is readonly' in resp.text
         assert len(os.listdir(os.path.join(pub.app_dir, 'models'))) == 3 + i
 
 
@@ -418,7 +424,8 @@ def test_wscall_snapshot_browse(pub):
     resp = app.get('/backoffice/settings/wscalls/%s/history/' % wscall.id)
     snapshot = pub.snapshot_class.select_object_history(wscall)[0]
     resp = resp.click(href='%s/view/' % snapshot.id)
-    assert 'This webservice call is readonly' in resp
+    assert 'This webservice call is readonly' in resp.text
+    assert '<p>%s</p>' % localstrftime(snapshot.timestamp) in resp.text
     with pytest.raises(IndexError):
         resp = resp.click('Edit')
 
diff --git a/wcs/admin/data_sources.py b/wcs/admin/data_sources.py
index b688d573..fe6625e9 100644
--- a/wcs/admin/data_sources.py
+++ b/wcs/admin/data_sources.py
@@ -20,6 +20,7 @@ from quixote import get_publisher, redirect
 from quixote.directory import Directory
 from quixote.html import TemplateIO, htmltext
 
+from wcs.admin import utils
 from wcs.qommon import _, force_str
 from wcs.qommon import errors, template
 from wcs.qommon.form import *
@@ -207,6 +208,7 @@ class NamedDataSourcePage(Directory):
         r = TemplateIO(html=True)
         if self.datasource.is_readonly():
             r += htmltext('<div class="infonotice"><p>%s</p></div>') % _('This data source is readonly.')
+            r += utils.snapshot_info_block(snapshot=self.datasource.snapshot_object)
         r += htmltext('<ul id="sidebar-actions">')
         if not self.datasource.is_readonly():
             r += htmltext('<li><a href="delete" rel="popup">%s</a></li>') % _('Delete')
diff --git a/wcs/admin/fields.py b/wcs/admin/fields.py
index 6a6b32dc..bd2b434f 100644
--- a/wcs/admin/fields.py
+++ b/wcs/admin/fields.py
@@ -22,6 +22,7 @@ from quixote import redirect
 from quixote.directory import Directory
 from quixote.html import TemplateIO, htmltext, htmlescape
 
+from wcs.admin import utils
 from wcs.qommon import _
 from wcs.qommon.form import *
 from wcs.qommon import errors, misc
@@ -337,7 +338,8 @@ class FieldsDirectory(Directory):
             r += htmltext('</ul>')
 
         if self.objectdef.is_readonly():
-            get_response().filter['sidebar'] = '<div class="infonotice"><p>%s</p></div>' % _(self.readonly_message)
+            get_response().filter['sidebar'] = htmltext('<div class="infonotice"><p>%s</p></div>') % _(self.readonly_message)
+            get_response().filter['sidebar'] += utils.snapshot_info_block(snapshot=self.objectdef.snapshot_object)
         else:
             get_response().filter['sidebar'] = str(self.get_new_field_form(self.page_id))
         r += self.index_bottom()
diff --git a/wcs/admin/forms.py b/wcs/admin/forms.py
index ad0f471b..dd3bdd13 100644
--- a/wcs/admin/forms.py
+++ b/wcs/admin/forms.py
@@ -673,6 +673,7 @@ class FormDefPage(Directory):
         r = TemplateIO(html=True)
         if self.formdef.is_readonly():
             r += htmltext('<div class="infonotice"><p>%s</p></div>') % _('This form is readonly.')
+            r += utils.snapshot_info_block(snapshot=self.formdef.snapshot_object)
             return r.getvalue()
         r += htmltext('<ul id="sidebar-actions">')
         r += htmltext('<li><a href="delete" rel="popup">%s</a></li>') % _('Delete')
diff --git a/wcs/admin/utils.py b/wcs/admin/utils.py
index 17f7f713..7a5b3136 100644
--- a/wcs/admin/utils.py
+++ b/wcs/admin/utils.py
@@ -46,3 +46,20 @@ def last_modification_block(obj):
         r += htmltext('</p>')
 
     return r.getvalue()
+
+
+def snapshot_info_block(snapshot):
+    r = TemplateIO(html=True)
+    r += htmltext('<p>')
+    parts = []
+    if snapshot.label:
+        parts.append(htmltext('<strong>%s</strong>') % snapshot.label)
+    elif snapshot.comment:
+        parts.append(snapshot.comment)
+    if snapshot.user_id:
+        parts.append('%s (%s)' % (misc.localstrftime(snapshot.timestamp), snapshot.user))
+    else:
+        parts.append(misc.localstrftime(snapshot.timestamp))
+    r += htmltext('<br />').join(parts)
+    r += htmltext('</p>')
+    return r.getvalue()
diff --git a/wcs/admin/workflows.py b/wcs/admin/workflows.py
index 81d1e1dd..f42d7b8d 100644
--- a/wcs/admin/workflows.py
+++ b/wcs/admin/workflows.py
@@ -1585,6 +1585,7 @@ class WorkflowPage(Directory):
             r += htmltext('</p>')
         elif self.workflow.is_readonly():
             r += htmltext('<div class="infonotice"><p>%s</p></div>') % _('This workflow is readonly.')
+            r += utils.snapshot_info_block(snapshot=self.workflow.snapshot_object)
             return r.getvalue()
 
         r += htmltext('<ul id="sidebar-actions">')
diff --git a/wcs/admin/wscalls.py b/wcs/admin/wscalls.py
index ac8e32d5..26a04a70 100644
--- a/wcs/admin/wscalls.py
+++ b/wcs/admin/wscalls.py
@@ -20,6 +20,7 @@ from quixote import get_publisher, get_response, redirect
 from quixote.directory import Directory
 from quixote.html import TemplateIO, htmltext
 
+from wcs.admin import utils
 from wcs.qommon import _, errors, template
 from wcs.qommon import misc
 from wcs.qommon.form import *
@@ -112,6 +113,7 @@ class NamedWsCallPage(Directory):
         r = TemplateIO(html=True)
         if self.wscall.is_readonly():
             r += htmltext('<div class="infonotice"><p>%s</p></div>') % _('This webservice call is readonly.')
+            r += utils.snapshot_info_block(snapshot=self.wscall.snapshot_object)
         r += htmltext('<ul id="sidebar-actions">')
         if not self.wscall.is_readonly():
             r += htmltext('<li><a href="export">%s</a></li>') % _('Export')
diff --git a/wcs/backoffice/cards.py b/wcs/backoffice/cards.py
index 1e91b3a8..00c47294 100644
--- a/wcs/backoffice/cards.py
+++ b/wcs/backoffice/cards.py
@@ -208,6 +208,7 @@ class CardDefPage(FormDefPage):
         r = TemplateIO(html=True)
         if self.formdef.is_readonly():
             r += htmltext('<div class="infonotice"><p>%s</p></div>') % _('This card model is readonly.')
+            r += utils.snapshot_info_block(snapshot=self.formdef.snapshot_object)
             return r.getvalue()
         r += htmltext('<ul id="sidebar-actions">')
         r += htmltext('<li><a href="delete" rel="popup">%s</a></li>') % _('Delete')
diff --git a/wcs/snapshots.py b/wcs/snapshots.py
index 7c89e789..dd4bbffa 100644
--- a/wcs/snapshots.py
+++ b/wcs/snapshots.py
@@ -88,6 +88,7 @@ class Snapshot:
                     include_id=True,
                     snapshot=True)
             self._instance.readonly = True
+            self._instance.snapshot_object = self
         return self._instance
 
     @property
-- 
2.29.2

