diff --git a/tests/test_snapshots.py b/tests/test_snapshots.py
index 90387a567..933dc498b 100644
--- a/tests/test_snapshots.py
+++ b/tests/test_snapshots.py
@@ -317,3 +317,25 @@ def test_wscall_snapshot_browse(pub):
assert 'This webservice call is readonly' in resp
with pytest.raises(IndexError):
resp = resp.click('Edit')
+
+
+def test_form_snapshot_save(pub, formdef_with_history):
+ create_superuser(pub)
+ create_role()
+ app = login(get_app(pub))
+
+ resp = app.get('/backoffice/forms/%s/' % formdef_with_history.id)
+ resp = resp.click('Save snapshot')
+ resp.form['label'] = 'test snapshot'
+ resp = resp.form.submit('submit')
+
+ # add more snapshots
+ formdef = FormDef.get(id=formdef_with_history.id)
+ for i in range(10, 15):
+ formdef.description = 'this is a description (%s)' % i
+ formdef.store()
+
+ resp = app.get('/backoffice/forms/%s/history/' % formdef_with_history.id)
+ assert [x.attrib['class'] for x in resp.pyquery.find('ul.snapshots-list li')] == [
+ 'new-day', 'collapsed', 'collapsed', 'collapsed', 'collapsed', 'has-label',
+ 'collapsed', 'collapsed', 'collapsed', 'collapsed', 'collapsed', 'collapsed']
diff --git a/wcs/admin/blocks.py b/wcs/admin/blocks.py
index cac3fe71f..bf7c06d23 100644
--- a/wcs/admin/blocks.py
+++ b/wcs/admin/blocks.py
@@ -90,6 +90,7 @@ class BlockDirectory(FieldsDirectory):
r += htmltext('
%s') % _('Delete')
r += htmltext('%s') % _('Export')
if get_publisher().snapshot_class:
+ r += htmltext('%s') % _('Save snapshot')
r += htmltext('%s') % _('History')
r += htmltext('%s') % _('Settings')
r += htmltext('')
diff --git a/wcs/admin/data_sources.py b/wcs/admin/data_sources.py
index c248f4fea..f0aefeae4 100644
--- a/wcs/admin/data_sources.py
+++ b/wcs/admin/data_sources.py
@@ -178,6 +178,7 @@ class NamedDataSourcePage(Directory):
r += htmltext('%s') % _('Delete')
r += htmltext('%s') % _('Export')
if get_publisher().snapshot_class:
+ r += htmltext('%s') % _('Save snapshot')
r += htmltext('%s') % _('History')
r += htmltext('')
return r.getvalue()
diff --git a/wcs/admin/forms.py b/wcs/admin/forms.py
index 368a174fa..1930476eb 100644
--- a/wcs/admin/forms.py
+++ b/wcs/admin/forms.py
@@ -623,6 +623,7 @@ class FormDefPage(Directory):
'Overwrite with new import')
r += htmltext('%s') % _('Export')
if get_publisher().snapshot_class:
+ r += htmltext('%s') % _('Save snapshot')
r += htmltext('%s') % _('History')
r += htmltext('%s') % _('Anonymise forms')
if not get_publisher().is_using_postgresql():
diff --git a/wcs/admin/workflows.py b/wcs/admin/workflows.py
index 64c39709f..b62381eaa 100644
--- a/wcs/admin/workflows.py
+++ b/wcs/admin/workflows.py
@@ -1581,6 +1581,7 @@ class WorkflowPage(Directory):
r += htmltext('%s') % _('Duplicate')
r += htmltext('%s') % _('Export')
if get_publisher().snapshot_class:
+ r += htmltext('%s') % _('Save snapshot')
r += htmltext('%s') % _('History')
r += htmltext('')
if not self.workflow.is_readonly():
diff --git a/wcs/admin/wscalls.py b/wcs/admin/wscalls.py
index d3d091bf9..b366bf757 100644
--- a/wcs/admin/wscalls.py
+++ b/wcs/admin/wscalls.py
@@ -117,6 +117,7 @@ class NamedWsCallPage(Directory):
r += htmltext('%s') % _('Export')
r += htmltext('%s') % _('Delete')
if get_publisher().snapshot_class:
+ r += htmltext('%s') % _('Save snapshot')
r += htmltext('%s') % _('History')
r += htmltext('')
return r.getvalue()
diff --git a/wcs/backoffice/cards.py b/wcs/backoffice/cards.py
index c22963d41..487bff4af 100644
--- a/wcs/backoffice/cards.py
+++ b/wcs/backoffice/cards.py
@@ -184,6 +184,7 @@ class CardDefPage(FormDefPage):
'Overwrite with new import')
r += htmltext('%s') % _('Export')
if get_publisher().snapshot_class:
+ r += htmltext('%s') % _('Save snapshot')
r += htmltext('%s') % _('History')
r += htmltext('')
diff --git a/wcs/backoffice/snapshots.py b/wcs/backoffice/snapshots.py
index 8c114c324..59f52cc72 100644
--- a/wcs/backoffice/snapshots.py
+++ b/wcs/backoffice/snapshots.py
@@ -20,12 +20,12 @@ from quixote.html import TemplateIO, htmltext
from wcs.qommon import _, errors, misc, template
from wcs.qommon.backoffice.menu import html_top
-from wcs.qommon.form import Form, RadiobuttonsWidget
+from wcs.qommon.form import Form, RadiobuttonsWidget, StringWidget
from wcs.qommon.storage import Equal
class SnapshotsDirectory(Directory):
- _q_exports = ['']
+ _q_exports = ['', 'save']
do_not_call_in_templates = True
def __init__(self, instance):
@@ -43,9 +43,28 @@ class SnapshotsDirectory(Directory):
templates=['wcs/backoffice/snapshots.html'],
context={'view': self})
+ def save(self):
+ form = Form(enctype='multipart/form-data')
+ label = form.add(StringWidget, 'label', title=_('Label'), required=True)
+ form.add_submit('submit', _('Submit'))
+ form.add_submit('cancel', _('Cancel'))
+
+ if form.get_widget('cancel').parse():
+ return redirect('../')
+
+ if form.is_submitted() and not form.has_errors():
+ get_publisher().snapshot_class.snap(instance=self.obj, label=label.parse())
+ return redirect('../')
+
+ html_top('', _('History'))
+ r = TemplateIO(html=True)
+ r += htmltext('%s
') % _('Save snapshot')
+ r += form.render()
+ return r.getvalue()
+
def snapshots(self):
current_date = None
- snapshots = get_publisher().snapshot_class.select_object_history(self.obj)[1:]
+ snapshots = get_publisher().snapshot_class.select_object_history(self.obj)
day_snapshot = None
for i, snapshot in enumerate(snapshots):
if snapshot.timestamp.date() != current_date:
diff --git a/wcs/snapshots.py b/wcs/snapshots.py
index 066ba2569..bcffb58fb 100644
--- a/wcs/snapshots.py
+++ b/wcs/snapshots.py
@@ -43,7 +43,7 @@ class Snapshot:
_user = None
@classmethod
- def snap(cls, instance, comment):
+ def snap(cls, instance, comment=None, label=None):
obj = cls()
obj.object_type = instance.xml_root_node
obj.object_id = instance.id
@@ -52,9 +52,11 @@ class Snapshot:
obj.user_id = get_session().user
obj.serialization = ET.tostring(instance.export_to_xml(include_id=True)).decode('utf-8')
obj.comment = comment
+ obj.label = label
latest = cls.get_latest(obj.object_type, obj.object_id)
- if latest is None or obj.serialization != latest.serialization:
- # save snapshot if there are changes
+ if label is not None or latest is None or obj.serialization != latest.serialization:
+ # save snapshot if there are changes or an explicit label was
+ # given.
obj.store()
def get_object_class(self):