cards: add shared and datasource custom views to carddef export (#42571)

This commit is contained in:
Lauréline Guérin 2020-10-16 11:42:10 +02:00
parent 24ef0f870c
commit f5fe99d347
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
5 changed files with 257 additions and 3 deletions

View File

@ -77,13 +77,86 @@ def test_xml_export_import(pub):
data_source={'type': 'carddef:foo'}) data_source={'type': 'carddef:foo'})
] ]
carddef.store() carddef.store()
# define also custom views
pub.custom_view_class.wipe()
custom_view = pub.custom_view_class()
custom_view.title = 'datasource card view'
custom_view.formdef = carddef
custom_view.columns = {'list': [{'id': 'id'}, {'id': 'time'}, {'id': 'status'}, {'id': '1'}, {'id': '2'}]}
custom_view.filters = {'filter': 'recorded', 'filter-1': 'on', 'filter-status': 'on', 'filter-1-value': 'a'}
custom_view.visibility = 'datasource'
custom_view.order_by = '-receipt_time'
custom_view.store()
custom_view = pub.custom_view_class()
custom_view.title = 'shared card view'
custom_view.formdef = carddef
custom_view.columns = {'list': [{'id': 'id'}, {'id': 'time'}, {'id': 'status'}]}
custom_view.filters = {'filter': 'done', 'filter-1': 'on', 'filter-status': 'on', 'filter-1-value': 'b'}
custom_view.visibility = 'any'
custom_view.order_by = 'receipt_time'
custom_view.store()
custom_view = pub.custom_view_class()
custom_view.title = 'private card view'
custom_view.formdef = carddef
custom_view.columns = {'list': [{'id': 'id'}]}
custom_view.filters = {}
custom_view.visibility = 'owner'
custom_view.usier_id = 42
custom_view.order_by = 'id'
custom_view.store()
carddef_xml = carddef.export_to_xml() carddef_xml = carddef.export_to_xml()
assert carddef_xml.tag == 'carddef' assert carddef_xml.tag == 'carddef'
carddef.data_class().wipe() carddef.data_class().wipe()
pub.custom_view_class.wipe()
carddef2 = CardDef.import_from_xml(BytesIO(ET.tostring(carddef_xml))) carddef2 = CardDef.import_from_xml(BytesIO(ET.tostring(carddef_xml)))
assert carddef2.name == 'foo' assert carddef2.name == 'foo'
assert carddef2.fields[1].data_source == {'type': 'carddef:foo'} assert carddef2.fields[1].data_source == {'type': 'carddef:foo'}
assert carddef2._custom_views
custom_views = sorted(carddef2._custom_views, key=lambda a: a.visibility)
assert len(custom_views) == 2
assert custom_views[0].title == 'shared card view'
assert custom_views[0].slug == 'shared-card-view'
assert custom_views[0].columns == {'list': [{'id': 'id'}, {'id': 'time'}, {'id': 'status'}]}
assert custom_views[0].filters == {'filter': 'done', 'filter-1': 'on', 'filter-status': 'on', 'filter-1-value': 'b'}
assert custom_views[0].visibility == 'any'
assert custom_views[0].order_by == 'receipt_time'
assert custom_views[0].formdef_id is None
assert custom_views[0].formdef_type is None
assert custom_views[1].title == 'datasource card view'
assert custom_views[1].slug == 'datasource-card-view'
assert custom_views[1].columns == {'list': [{'id': 'id'}, {'id': 'time'}, {'id': 'status'}, {'id': '1'}, {'id': '2'}]}
assert custom_views[1].filters == {'filter': 'recorded', 'filter-1': 'on', 'filter-status': 'on', 'filter-1-value': 'a'}
assert custom_views[1].visibility == 'datasource'
assert custom_views[1].order_by == '-receipt_time'
assert custom_views[1].formdef_id is None
assert custom_views[1].formdef_type is None
carddef2.store()
custom_views = sorted(pub.custom_view_class.select(), key=lambda a: a.visibility)
assert len(custom_views) == 2
assert custom_views[0].title == 'shared card view'
assert custom_views[0].slug == 'shared-card-view'
assert custom_views[0].columns == {'list': [{'id': 'id'}, {'id': 'time'}, {'id': 'status'}]}
assert custom_views[0].filters == {'filter': 'done', 'filter-1': 'on', 'filter-status': 'on', 'filter-1-value': 'b'}
assert custom_views[0].visibility == 'any'
assert custom_views[0].order_by == 'receipt_time'
assert custom_views[0].formdef_id == carddef2.id
assert custom_views[0].formdef_type == 'carddef'
assert custom_views[1].title == 'datasource card view'
assert custom_views[1].slug == 'datasource-card-view'
assert custom_views[1].columns == {'list': [{'id': 'id'}, {'id': 'time'}, {'id': 'status'}, {'id': '1'}, {'id': '2'}]}
assert custom_views[1].filters == {'filter': 'recorded', 'filter-1': 'on', 'filter-status': 'on', 'filter-1-value': 'a'}
assert custom_views[1].visibility == 'datasource'
assert custom_views[1].order_by == '-receipt_time'
assert custom_views[1].formdef_id == carddef2.id
assert custom_views[1].formdef_type == 'carddef'
def test_template_access(pub): def test_template_access(pub):

View File

@ -697,3 +697,67 @@ def test_field_prefill():
f2 = FormDef.import_from_xml_tree(formdef_xml) f2 = FormDef.import_from_xml_tree(formdef_xml)
assert len(f2.fields) == len(formdef.fields) assert len(f2.fields) == len(formdef.fields)
assert f2.fields[0].prefill == {'type': 'string', 'value': None} assert f2.fields[0].prefill == {'type': 'string', 'value': None}
def test_custom_views():
formdef = FormDef()
formdef.name = 'foo'
formdef.fields = [
fields.StringField(id='1', label='Foo', type='string', varname='foo'),
fields.StringField(id='2', label='Bar', type='string', varname='bar'),
]
formdef.store()
# define also custom views
pub.custom_view_class.wipe()
custom_view = pub.custom_view_class()
custom_view.title = 'shared form view'
custom_view.formdef = formdef
custom_view.columns = {'list': [{'id': 'id'}, {'id': 'time'}, {'id': 'status'}]}
custom_view.filters = {'filter': 'done', 'filter-1': 'on', 'filter-status': 'on', 'filter-1-value': 'b'}
custom_view.visibility = 'any'
custom_view.order_by = 'receipt_time'
custom_view.store()
custom_view = pub.custom_view_class()
custom_view.title = 'private form view'
custom_view.formdef = formdef
custom_view.columns = {'list': [{'id': 'id'}]}
custom_view.filters = {}
custom_view.visibility = 'owner'
custom_view.usier_id = 42
custom_view.order_by = 'id'
custom_view.store()
formdef_xml = formdef.export_to_xml()
assert formdef_xml.tag == 'formdef'
formdef.data_class().wipe()
pub.custom_view_class.wipe()
formdef2 = FormDef.import_from_xml(BytesIO(ET.tostring(formdef_xml)))
assert formdef2.name == 'foo'
assert formdef2._custom_views
custom_views = formdef2._custom_views
assert len(custom_views) == 1
assert custom_views[0].title == 'shared form view'
assert custom_views[0].slug == 'shared-form-view'
assert custom_views[0].columns == {'list': [{'id': 'id'}, {'id': 'time'}, {'id': 'status'}]}
assert custom_views[0].filters == {'filter': 'done', 'filter-1': 'on', 'filter-status': 'on', 'filter-1-value': 'b'}
assert custom_views[0].visibility == 'any'
assert custom_views[0].order_by == 'receipt_time'
assert custom_views[0].formdef_id is None
assert custom_views[0].formdef_type is None
formdef2.store()
custom_views = pub.custom_view_class.select()
assert len(custom_views) == 1
assert custom_views[0].title == 'shared form view'
assert custom_views[0].slug == 'shared-form-view'
assert custom_views[0].columns == {'list': [{'id': 'id'}, {'id': 'time'}, {'id': 'status'}]}
assert custom_views[0].filters == {'filter': 'done', 'filter-1': 'on', 'filter-status': 'on', 'filter-1-value': 'b'}
assert custom_views[0].visibility == 'any'
assert custom_views[0].order_by == 'receipt_time'
assert custom_views[0].formdef_id == formdef2.id
assert custom_views[0].formdef_type == 'formdef'

View File

@ -251,6 +251,22 @@ def test_card_snapshot_browse(pub):
carddef.fields = [] carddef.fields = []
carddef.store() carddef.store()
pub.custom_view_class.wipe()
custom_view = pub.custom_view_class()
custom_view.title = 'shared form view'
custom_view.formdef = carddef
custom_view.columns = {'list': [{'id': 'id'}]}
custom_view.filters = {}
custom_view.visibility = 'any'
custom_view.store()
# new version has custom views
carddef.name = 'test 1'
carddef.store()
# delete custom views
pub.custom_view_class.wipe()
app = login(get_app(pub)) app = login(get_app(pub))
resp = app.get('/backoffice/cards/%s/history/' % carddef.id) resp = app.get('/backoffice/cards/%s/history/' % carddef.id)
@ -259,6 +275,7 @@ def test_card_snapshot_browse(pub):
assert 'This card model is readonly' in resp assert 'This card model is readonly' in resp
resp = resp.click('Geolocation') resp = resp.click('Geolocation')
assert [x[0].name for x in resp.form.fields.values() if x[0].tag == 'button'] == ['cancel'] 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
def test_datasource_snapshot_browse(pub): def test_datasource_snapshot_browse(pub):
@ -286,13 +303,31 @@ def test_form_snapshot_browse(pub, formdef_with_history):
create_role() create_role()
app = login(get_app(pub)) app = login(get_app(pub))
pub.custom_view_class.wipe()
custom_view = pub.custom_view_class()
custom_view.title = 'shared form view'
custom_view.formdef = formdef_with_history
custom_view.columns = {'list': [{'id': 'id'}]}
custom_view.filters = {}
custom_view.visibility = 'any'
custom_view.store()
# version 5 has custom views
formdef_with_history.name = 'testform 5'
formdef_with_history.description = 'this is a description (5)'
formdef_with_history.store()
# delete custom views
pub.custom_view_class.wipe()
resp = app.get('/backoffice/forms/%s/history/' % formdef_with_history.id) resp = app.get('/backoffice/forms/%s/history/' % formdef_with_history.id)
snapshot = pub.snapshot_class.select_object_history(formdef_with_history)[2] snapshot = pub.snapshot_class.select_object_history(formdef_with_history)[0]
resp = resp.click(href='%s/view/' % snapshot.id) resp = resp.click(href='%s/view/' % snapshot.id)
assert 'This form is readonly' in resp assert 'This form is readonly' in resp
resp = resp.click('Description') resp = resp.click('Description')
assert resp.form['description'].value == 'this is a description (2)' 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'] 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
def test_workflow_snapshot_browse(pub): def test_workflow_snapshot_browse(pub):

View File

@ -14,13 +14,17 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>. # along with this program; if not, see <http://www.gnu.org/licenses/>.
import xml.etree.ElementTree as ET
from django.utils.six.moves.urllib import parse as urlparse from django.utils.six.moves.urllib import parse as urlparse
from django.utils.encoding import force_text
from quixote import get_publisher from quixote import get_publisher
from wcs.carddef import CardDef from wcs.carddef import CardDef
from wcs.formdef import FormDef from wcs.formdef import FormDef
from wcs.qommon.storage import StorableObject, Equal from wcs.qommon.storage import StorableObject, Equal
from wcs.qommon.misc import simplify from wcs.qommon.misc import simplify
from .qommon.misc import xml_node_text
class CustomView(StorableObject): class CustomView(StorableObject):
@ -37,6 +41,8 @@ class CustomView(StorableObject):
filters = None filters = None
order_by = None order_by = None
xml_root_node = 'custom_view'
@property @property
def user(self): def user(self):
return get_publisher().user_class.get(self.user_id) return get_publisher().user_class.get(self.user_id)
@ -62,7 +68,7 @@ class CustomView(StorableObject):
return False return False
if self.formdef_id != str(formdef.id): if self.formdef_id != str(formdef.id):
return False return False
if self.visibility == 'owner' and self.user_id != str(user.id): if self.visibility == 'owner' and (user is None or self.user_id != str(user.id)):
return False return False
return True return True
@ -135,3 +141,60 @@ class CustomView(StorableObject):
def get_default_filters(self): def get_default_filters(self):
return [key[7:] for key in self.filters if key.startswith('filter-')] return [key[7:] for key in self.filters if key.startswith('filter-')]
def export_to_xml(self, charset=None):
root = ET.Element(self.xml_root_node)
fields = [
'title',
'slug',
'visibility',
'filters',
'columns',
'order_by',
]
for attribute in fields:
if getattr(self, attribute, None) is not None:
val = getattr(self, attribute)
el = ET.SubElement(root, attribute)
if attribute == 'columns':
for field_dict in self.columns.get('list') or []:
if not isinstance(field_dict, dict):
continue
for k, v in sorted(field_dict.items()):
text_value = force_text(v, charset, errors='replace')
ET.SubElement(el, k).text = text_value
elif type(val) is dict:
for k, v in sorted(val.items()):
text_value = force_text(v, charset, errors='replace')
ET.SubElement(el, k).text = text_value
elif isinstance(val, str):
el.text = force_text(val, charset, errors='replace')
else:
el.text = str(val)
return root
def init_with_xml(self, elem, charset):
fields = [
'title',
'slug',
'visibility',
'filters',
'columns',
'order_by',
]
for attribute in fields:
el = elem.find(attribute)
if el is None:
continue
if attribute == 'filters':
v = {}
for e in el:
v[e.tag] = xml_node_text(e)
setattr(self, attribute, v)
elif attribute == 'columns':
v = []
for e in el:
v.append({e.tag: xml_node_text(e)})
setattr(self, attribute, {'list': v})
else:
setattr(self, attribute, xml_node_text(el))

View File

@ -389,8 +389,14 @@ class FormDef(StorableObject):
from . import sql from . import sql
sql.do_formdef_tables(self, rebuild_views=True, sql.do_formdef_tables(self, rebuild_views=True,
rebuild_global_views=True) rebuild_global_views=True)
self.store_related_custom_views()
return t return t
def store_related_custom_views(self):
for view in getattr(self, '_custom_views', []):
view.formdef = self
view.store()
def get_all_fields(self): def get_all_fields(self):
return (self.fields or []) + self.workflow.get_backoffice_fields() return (self.fields or []) + self.workflow.get_backoffice_fields()
@ -1000,6 +1006,11 @@ class FormDef(StorableObject):
else: else:
pass # TODO: extend support to other types pass # TODO: extend support to other types
custom_views = ET.SubElement(root, 'custom_views')
for view in get_publisher().custom_view_class.select():
if view.match(user=None, formdef=self):
custom_views.append(view.export_to_xml(charset=charset))
geolocations = ET.SubElement(root, 'geolocations') geolocations = ET.SubElement(root, 'geolocations')
for geoloc_key, geoloc_label in (self.geolocations or {}).items(): for geoloc_key, geoloc_label in (self.geolocations or {}).items():
element = ET.SubElement(geolocations, 'geolocation') element = ET.SubElement(geolocations, 'geolocation')
@ -1153,6 +1164,12 @@ class FormDef(StorableObject):
option_value.set_content(base64.decodebytes(force_bytes(option.find('content').text))) option_value.set_content(base64.decodebytes(force_bytes(option.find('content').text)))
formdef.workflow_options[option.attrib.get('varname')] = option_value formdef.workflow_options[option.attrib.get('varname')] = option_value
formdef._custom_views = []
for view in tree.findall('custom_views/%s' % get_publisher().custom_view_class.xml_root_node):
view_o = get_publisher().custom_view_class()
view_o.init_with_xml(view, charset)
formdef._custom_views.append(view_o)
if tree.find('last_modification') is not None: if tree.find('last_modification') is not None:
node = tree.find('last_modification') node = tree.find('last_modification')
formdef.last_modification_time = time.strptime(node.text, '%Y-%m-%d %H:%M:%S') formdef.last_modification_time = time.strptime(node.text, '%Y-%m-%d %H:%M:%S')
@ -1492,6 +1509,8 @@ class FormDef(StorableObject):
if self.lightweight and 'fields' in odict: if self.lightweight and 'fields' in odict:
# will be stored independently # will be stored independently
del odict['fields'] del odict['fields']
if '_custom_views' in odict:
del odict['_custom_views']
return odict return odict
def __setstate__(self, dict): def __setstate__(self, dict):