cards: add shared and datasource custom views to carddef export (#42571)
This commit is contained in:
parent
24ef0f870c
commit
f5fe99d347
|
@ -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):
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue