backoffice: add history/ paths to list snapshots (#4960)
This commit is contained in:
parent
8384cedeec
commit
b415ed66de
|
@ -63,6 +63,63 @@ def test_snapshot_basics(pub):
|
|||
assert '>testcard2<' in snapshot.serialization
|
||||
|
||||
|
||||
def test_snapshot_instance(pub):
|
||||
formdef = FormDef()
|
||||
formdef.name = 'testform'
|
||||
formdef.fields = []
|
||||
formdef.store()
|
||||
|
||||
carddef = CardDef()
|
||||
carddef.name = 'testcard'
|
||||
carddef.fields = []
|
||||
carddef.store()
|
||||
|
||||
# remove existing snapshots as they may be duplicated if table_name was
|
||||
# generated in a different second.
|
||||
pub.snapshot_class.wipe()
|
||||
|
||||
carddef.name = 'testcard2'
|
||||
carddef.store()
|
||||
|
||||
for i in range(10):
|
||||
formdef.name = 'testform %s' % i
|
||||
formdef.store()
|
||||
|
||||
assert pub.snapshot_class.count() == 11
|
||||
|
||||
snapshots = pub.snapshot_class.select_object_history(formdef)
|
||||
assert len(snapshots) == 10
|
||||
for i in range(10):
|
||||
assert snapshots[i].instance.name == 'testform %s' % (9 - i)
|
||||
|
||||
snapshots = pub.snapshot_class.select_object_history(carddef)
|
||||
assert len(snapshots) == 1
|
||||
|
||||
|
||||
def test_snapshot_user(pub):
|
||||
user = pub.user_class()
|
||||
user.name = 'User Name'
|
||||
user.email = 'foo@localhost'
|
||||
user.store()
|
||||
|
||||
carddef = CardDef()
|
||||
carddef.name = 'testcard'
|
||||
carddef.fields = []
|
||||
carddef.store()
|
||||
snapshot = pub.snapshot_class.select_object_history(carddef)[0]
|
||||
assert snapshot.user is None
|
||||
|
||||
snapshot.user_id = user.id
|
||||
snapshot.store()
|
||||
snapshot = pub.snapshot_class.select_object_history(carddef)[0]
|
||||
assert str(snapshot.user) == 'User Name'
|
||||
|
||||
snapshot.user_id = 'nope'
|
||||
snapshot.store()
|
||||
snapshot = pub.snapshot_class.select_object_history(carddef)[0]
|
||||
assert str(snapshot.user) == 'unknown user'
|
||||
|
||||
|
||||
def test_form_snapshot_comments(pub):
|
||||
create_superuser(pub)
|
||||
create_role()
|
||||
|
@ -87,3 +144,21 @@ def test_form_snapshot_comments(pub):
|
|||
resp.forms[0]['type'] = 'string'
|
||||
resp = resp.forms[0].submit().follow()
|
||||
assert pub.snapshot_class.select(order_by='-timestamp')[0].comment == 'New field "foobar"'
|
||||
|
||||
|
||||
def test_form_snapshot_history(pub):
|
||||
formdef = FormDef()
|
||||
formdef.name = 'testform'
|
||||
formdef.fields = []
|
||||
formdef.store()
|
||||
|
||||
for i in range(5):
|
||||
formdef.name = 'testform %s' % i
|
||||
formdef.store()
|
||||
|
||||
create_superuser(pub)
|
||||
create_role()
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/%s/history/' % formdef.id)
|
||||
assert [x.attrib['class'] for x in resp.pyquery.find('ul.snapshots-list li')] == [
|
||||
'new-day', 'collapsed', 'collapsed', 'collapsed', 'collapsed', 'collapsed']
|
||||
|
|
|
@ -28,6 +28,7 @@ from wcs.qommon.backoffice.menu import html_top
|
|||
|
||||
from wcs.admin.fields import FieldDefPage, FieldsDirectory
|
||||
from wcs.admin import utils
|
||||
from wcs.backoffice.snapshots import SnapshotsDirectory
|
||||
|
||||
|
||||
class BlockFieldDefPage(FieldDefPage):
|
||||
|
@ -39,7 +40,8 @@ class BlockFieldDefPage(FieldDefPage):
|
|||
|
||||
|
||||
class BlockDirectory(FieldsDirectory):
|
||||
_q_exports = ['', 'update_order', 'new', 'delete', 'export', 'settings']
|
||||
_q_exports = ['', 'update_order', 'new', 'delete', 'export', 'settings',
|
||||
('history', 'snapshots_dir'),]
|
||||
field_def_page_class = BlockFieldDefPage
|
||||
blacklisted_types = ['page', 'table', 'table-select', 'tablerows', 'ranked-items', 'blocks']
|
||||
support_import = False
|
||||
|
@ -48,6 +50,7 @@ class BlockDirectory(FieldsDirectory):
|
|||
def __init__(self, section, *args, **kwargs):
|
||||
self.section = section
|
||||
super().__init__(*args, **kwargs)
|
||||
self.snapshots_dir = SnapshotsDirectory(self.objectdef)
|
||||
|
||||
def _q_traverse(self, path):
|
||||
get_response().breadcrumb.append(('%s/' % self.objectdef.id, self.objectdef.name))
|
||||
|
|
|
@ -33,6 +33,7 @@ from wcs.qommon.backoffice.menu import html_top
|
|||
from wcs.data_sources import (NamedDataSource, DataSourceSelectionWidget,
|
||||
get_structured_items)
|
||||
from wcs.formdef import FormDef, get_formdefs_of_all_kinds
|
||||
from wcs.backoffice.snapshots import SnapshotsDirectory
|
||||
|
||||
|
||||
class NamedDataSourceUI(object):
|
||||
|
@ -155,7 +156,8 @@ class NamedDataSourceUI(object):
|
|||
|
||||
|
||||
class NamedDataSourcePage(Directory):
|
||||
_q_exports = ['', 'edit', 'delete', 'export']
|
||||
_q_exports = ['', 'edit', 'delete', 'export',
|
||||
('history', 'snapshots_dir'),]
|
||||
do_not_call_in_templates = True
|
||||
|
||||
def __init__(self, component):
|
||||
|
@ -165,6 +167,7 @@ class NamedDataSourcePage(Directory):
|
|||
raise errors.TraversalError()
|
||||
self.datasource_ui = NamedDataSourceUI(self.datasource)
|
||||
get_response().breadcrumb.append((component + '/', self.datasource.name))
|
||||
self.snapshots_dir = SnapshotsDirectory(self.datasource)
|
||||
|
||||
def _q_index(self):
|
||||
html_top('datasources', title=self.datasource.name)
|
||||
|
|
|
@ -51,6 +51,8 @@ from .categories import CategoriesDirectory
|
|||
from .data_sources import NamedDataSourcesDirectory
|
||||
from .logged_errors import LoggedErrorsDirectory
|
||||
|
||||
from wcs.backoffice.snapshots import SnapshotsDirectory
|
||||
|
||||
|
||||
def get_categories():
|
||||
t = sorted([(misc.simplify(x.name), x.id, x.name, x.id) for x in Category.select()])
|
||||
|
@ -365,7 +367,8 @@ class FormDefPage(Directory):
|
|||
'overwrite', 'qrcode', 'information',
|
||||
('public-url', 'public_url'),
|
||||
('backoffice-submission-roles', 'backoffice_submission_roles'),
|
||||
('logged-errors', 'logged_errors_dir'),]
|
||||
('logged-errors', 'logged_errors_dir'),
|
||||
('history', 'snapshots_dir'),]
|
||||
|
||||
formdef_class = FormDef
|
||||
formdef_export_prefix = 'form'
|
||||
|
@ -394,6 +397,7 @@ class FormDefPage(Directory):
|
|||
self.role.html_top = self.html_top
|
||||
self.options = OptionsDirectory(self.formdef)
|
||||
self.logged_errors_dir = LoggedErrorsDirectory(parent_dir=self, formdef_id=self.formdef.id)
|
||||
self.snapshots_dir = SnapshotsDirectory(self.formdef)
|
||||
|
||||
def html_top(self, title):
|
||||
return html_top('forms', title)
|
||||
|
|
|
@ -49,6 +49,7 @@ from .fields import FieldDefPage, FieldsDirectory
|
|||
from .data_sources import NamedDataSourcesDirectory
|
||||
from .logged_errors import LoggedErrorsDirectory
|
||||
from .mail_templates import MailTemplatesDirectory
|
||||
from wcs.backoffice.snapshots import SnapshotsDirectory
|
||||
from wcs.backoffice.studio import StudioDirectory
|
||||
|
||||
|
||||
|
@ -1369,6 +1370,7 @@ class WorkflowPage(Directory):
|
|||
('functions', 'functions_dir'), ('global-actions', 'global_actions_dir'),
|
||||
('criticality-levels', 'criticality_levels_dir'),
|
||||
('logged-errors', 'logged_errors_dir'),
|
||||
('history', 'snapshots_dir'),
|
||||
]
|
||||
|
||||
def __init__(self, component, html_top):
|
||||
|
@ -1388,6 +1390,7 @@ class WorkflowPage(Directory):
|
|||
self.global_actions_dir = GlobalActionsDirectory(self.workflow, html_top)
|
||||
self.criticality_levels_dir = CriticalityLevelsDirectory(self.workflow)
|
||||
self.logged_errors_dir = LoggedErrorsDirectory(parent_dir=self, workflow_id=self.workflow.id)
|
||||
self.snapshots_dir = SnapshotsDirectory(self.workflow)
|
||||
get_response().breadcrumb.append((component + '/', self.workflow.name))
|
||||
|
||||
def _q_index(self):
|
||||
|
|
|
@ -25,6 +25,7 @@ from wcs.qommon import misc
|
|||
from wcs.qommon.form import *
|
||||
from wcs.qommon.backoffice.menu import html_top
|
||||
from wcs.wscalls import NamedWsCall, WsCallRequestWidget
|
||||
from wcs.backoffice.snapshots import SnapshotsDirectory
|
||||
|
||||
|
||||
class NamedWsCallUI(object):
|
||||
|
@ -95,7 +96,8 @@ class NamedWsCallUI(object):
|
|||
|
||||
|
||||
class NamedWsCallPage(Directory):
|
||||
_q_exports = ['', 'edit', 'delete', 'export']
|
||||
_q_exports = ['', 'edit', 'delete', 'export',
|
||||
('history', 'snapshots_dir'),]
|
||||
|
||||
def __init__(self, component):
|
||||
try:
|
||||
|
@ -104,6 +106,7 @@ class NamedWsCallPage(Directory):
|
|||
raise errors.TraversalError()
|
||||
self.wscall_ui = NamedWsCallUI(self.wscall)
|
||||
get_response().breadcrumb.append((component + '/', self.wscall.name))
|
||||
self.snapshots_dir = SnapshotsDirectory(self.wscall)
|
||||
|
||||
def _q_index(self):
|
||||
html_top('wscalls', title=self.wscall.name)
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
# w.c.s. - web application for online forms
|
||||
# Copyright (C) 2005-2020 Entr'ouvert
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# 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_publisher, get_response
|
||||
from quixote.directory import Directory
|
||||
|
||||
from wcs.qommon import _, errors, misc, template
|
||||
from wcs.qommon.backoffice.menu import html_top
|
||||
from wcs.qommon.storage import Equal
|
||||
|
||||
|
||||
class SnapshotsDirectory(Directory):
|
||||
_q_exports = ['']
|
||||
do_not_call_in_templates = True
|
||||
|
||||
def __init__(self, instance):
|
||||
self.obj = instance
|
||||
self.object_type = instance.xml_root_node
|
||||
self.object_id = instance.id
|
||||
|
||||
def _q_traverse(self, path):
|
||||
get_response().breadcrumb.append(('history/', _('History')))
|
||||
return super()._q_traverse(path)
|
||||
|
||||
def _q_index(self):
|
||||
html_top('', _('History'))
|
||||
return template.QommonTemplateResponse(
|
||||
templates=['wcs/backoffice/snapshots.html'],
|
||||
context={'view': self})
|
||||
|
||||
def snapshots(self):
|
||||
current_date = None
|
||||
snapshots = get_publisher().snapshot_class.select_object_history(self.obj)[1:]
|
||||
day_snapshot = None
|
||||
for i, snapshot in enumerate(snapshots):
|
||||
if snapshot.timestamp.date() != current_date:
|
||||
current_date = snapshot.timestamp.date()
|
||||
snapshot.new_day = True
|
||||
snapshot.day_other_count = 0
|
||||
day_snapshot = snapshot
|
||||
else:
|
||||
day_snapshot.day_other_count += 1
|
||||
return snapshots
|
||||
|
||||
def _q_lookup(self, component):
|
||||
snapshots = get_publisher().snapshot_class.select_object_history(self.obj,
|
||||
clause=[Equal('id', component)])
|
||||
if not snapshots:
|
||||
raise errors.TraversalError()
|
||||
return SnapshotDirectory(self.obj, snapshots[0])
|
||||
|
||||
|
||||
class SnapshotDirectory(Directory):
|
||||
_q_exports = ['']
|
||||
|
||||
def __init__(self, instance, snapshot):
|
||||
self.obj = instance
|
||||
self.snapshot = snapshot
|
||||
|
||||
def _q_traverse(self, path):
|
||||
get_response().breadcrumb.append((
|
||||
'%s/' % self.snapshot.id,
|
||||
misc.localstrftime(self.snapshot.timestamp))
|
||||
)
|
||||
return super()._q_traverse(path)
|
||||
|
||||
def _q_index(self):
|
||||
return ''
|
|
@ -1937,3 +1937,11 @@ div.mail-body {
|
|||
#sidebar-custom-views .active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
ul.snapshots-list .collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
ul.snapshots-list .new-day, ul.snapshots-list .has-label {
|
||||
display: block;
|
||||
}
|
||||
|
|
|
@ -18,7 +18,14 @@ import xml.etree.ElementTree as ET
|
|||
|
||||
from django.utils.timezone import now
|
||||
|
||||
from quixote import get_session
|
||||
from quixote import get_publisher, get_session
|
||||
|
||||
from wcs.qommon import _
|
||||
|
||||
|
||||
class UnknownUser:
|
||||
def __str__(self):
|
||||
return _('unknown user')
|
||||
|
||||
|
||||
class Snapshot:
|
||||
|
@ -31,6 +38,10 @@ class Snapshot:
|
|||
serialization = None
|
||||
label = None # (named snapshot)
|
||||
|
||||
# cache
|
||||
_instance = None
|
||||
_user = None
|
||||
|
||||
@classmethod
|
||||
def snap(cls, instance, comment):
|
||||
obj = cls()
|
||||
|
@ -45,3 +56,33 @@ class Snapshot:
|
|||
if latest is None or obj.serialization != latest.serialization:
|
||||
# save snapshot if there are changes
|
||||
obj.store()
|
||||
|
||||
def get_object_class(self):
|
||||
from wcs.blocks import BlockDef
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.data_sources import NamedDataSource
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.workflows import Workflow
|
||||
from wcs.wscalls import NamedWsCall
|
||||
for klass in (BlockDef, CardDef, NamedDataSource, FormDef, Workflow, NamedWsCall):
|
||||
if klass.xml_root_node == self.object_type:
|
||||
return klass
|
||||
raise KeyError('no class for object type: %s' % self.object_type)
|
||||
|
||||
@property
|
||||
def instance(self):
|
||||
if self._instance is None:
|
||||
tree = ET.fromstring(self.serialization)
|
||||
self._instance = self.get_object_class().import_from_xml_tree(tree, include_id=True)
|
||||
return self._instance
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
if not self.user_id:
|
||||
return None
|
||||
if self._user is None:
|
||||
try:
|
||||
self._user = get_publisher().user_class.get(self.user_id)
|
||||
except KeyError:
|
||||
self._user = UnknownUser()
|
||||
return self._user
|
||||
|
|
|
@ -2404,6 +2404,13 @@ class Snapshot(SqlMixin, wcs.snapshots.Snapshot):
|
|||
conn.commit()
|
||||
cur.close()
|
||||
|
||||
@classmethod
|
||||
def select_object_history(cls, obj, clause=None):
|
||||
return cls.select([
|
||||
Equal('object_type', obj.xml_root_node),
|
||||
Equal('object_id', obj.id)] + (clause or []),
|
||||
order_by='-timestamp')
|
||||
|
||||
@classmethod
|
||||
def _row2ob(cls, row, **kwargs):
|
||||
o = cls()
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block body %}
|
||||
<div id="appbar">
|
||||
<h2>{% trans "History" %}</h2>
|
||||
</div>
|
||||
|
||||
{% with snapshots=view.snapshots %}
|
||||
{% if snapshots %}
|
||||
<div class="section">
|
||||
<ul class="objects-list snapshots-list">
|
||||
{% for snapshot in snapshots %}
|
||||
<li data-day="{{ snapshot.timestamp|date:"Y-m-d" }}" class="{% if snapshot.new_day %}new-day{% elif snapshot.label %}has-label{% else %}collapsed{% endif %}"
|
||||
>{{ snapshot.timestamp }}{% if snapshot.label %} <strong>{{ snapshot.label }}</strong>{% elif snapshot.comment %}, {{ snapshot.comment }}{% endif %}
|
||||
{% if snapshot.user_id %}({{ snapshot.user }}){% endif %}
|
||||
{% if snapshot.new_day and snapshot.day_other_count %}(<a class="reveal"
|
||||
href="#day-{{ snapshot.timestamp|date:"Y-m-d"}}">
|
||||
{% blocktrans trimmed count counter=snapshot.day_other_count %}
|
||||
1 other this day
|
||||
{% plural %}
|
||||
{{ counter }} others this day{% endblocktrans %}
|
||||
</a>){% endif %}
|
||||
—
|
||||
<a href="{{snapshot.id}}/view/">{% trans "View" %}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="infonotice"><p>{% trans "No changes history" %}</p></div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
$('li.new-day a.reveal').on('click', function() {
|
||||
var day = $(this).parents('li.new-day').data('day');
|
||||
$('.snapshots-list li[data-day="' + day + '"]').toggleClass('collapsed');
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
Loading…
Reference in New Issue