Commit 7815161c authored by Vincent Michel's avatar Vincent Michel
Browse files

[uicfg] Add new features for uicfg, closes #3489097

We can now use uicfg to add specific callback or rql/vid html into
the 'attributes' or 'relations' sections of the primary view.

The DisplayCtrlRelationTags is monkeypatch to add a new function "display_rset".
parent 64b7fca29d23
Summary
-------
Cube for the Brainomics Project, see http://www.brainomics.net/
Uicfg documentation
~~~~~~~~~~~~~~~~~~~
In Brainomics, we have experimented a new function for uicfg that
allows to display specific rset/information within the 'attributes'
or 'relations' sections of the primary view.
The display_rset method of uicfg take 3 arguments:
* the name of the etype concerned by the rule;
* the section where the information is displayed (attributes or relations);
* a dictionnary of properties.
The dictionnary of properties must have:
* a 'callback' item OR a 'rql' and a 'vid' items.
The 'callback' is a function that takes the entity and render an HTML snipet.
The 'rql' is a rql query where the variable 'X' is the current entity. The 'vid'
is the __regid__ of the view that will be applied to the result of the rql query.
It could also have:
* a 'label' ('' if not given);
* an 'order' (9999 if not given);
It should be used as follows, e.g. using an etype property:
>>> _pvdc = uicfg.primaryview_display_ctrl
>>> _pvdc.display_rset('MyEtype', 'attributes',
{'callback': lambda x: x.formatted_description,
'label': _('description')})
or with a 'rql' and 'vid' attributes:
>>> _pvdc = uicfg.primaryview_display_ctrl
>>> _pvdc.display_rset('MyEtype', 'relations',
{'rql': 'Any Z WHERE X relation1 Y, Y relation2 Z',
'vid': 'my-view',
'label': _('My label')})
or with a 'callback' using a view:
>>> _pvdc = uicfg.primaryview_display_ctrl
>>> _pvdc.display_rset('MyEtype', 'relations',
{'callback': lambda x: x.view('my-secondary-view'),
'order': 2,
'label': _('My label')})
# copyright 2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr -- mailto:contact@logilab.fr
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
""" Schema test """
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.web.views import uicfg
from cubes.brainomics.views.primary import BrainomicsPrimaryView
class BrainomicsUICFGTC(CubicWebTC):
""" Test proper uicfg behavior, see https://www.cubicweb.org/ticket/3489097 """
def setup_database(self):
""" Several entities involving composite relations are created,
according to the schema.
"""
# Clean uicfg for test
uicfg.primaryview_display_ctrl._relrsetdefs = {}
uicfg.primaryview_display_ctrl._attrrsetdefs = {}
# Add entities
req = self.request()
t_study = req.create_entity('Study', name=u'Test study',
data_filepath=u'test/data/filepath')
t_card = req.create_entity('Card', title=u'This is a card.',
reverse_wiki=t_study)
t_subj = req.create_entity('Subject', identifier=u'test_subj',
gender=u'unknown', handedness=u'mixed')
t_center = req.create_entity('Center', identifier=u'center_id',
name=u'Test center name')
t_assessment = req.create_entity('Assessment', identifier=u'Test assess ID',
related_study=t_study,
reverse_holds=t_center,
reverse_concerned_by=t_subj)
def get_display_rset(self, req, entity, type='relations'):
pview = BrainomicsPrimaryView(req)
uicfg_reg = req.vreg['uicfg']
pview.display_ctrl = uicfg_reg.select('primaryview_display_ctrl',
req, entity=entity)
return list(pview.iterate_display_rset(entity, type))
def add_center_uicfg(self):
_pvdc = uicfg.primaryview_display_ctrl
_pvdc.display_rset('Center', 'relations',
{'rql': 'Any S WHERE S concerned_by Y, X holds Y',
'vid': 'table',
'order': 1,
'label': _('Subjects')})
def test_uihelper_setdefs(self):
# Add uicfg display_rset
_pvdc = uicfg.primaryview_display_ctrl
self.add_center_uicfg()
_pvdc.display_rset('Gene', 'relations',
{'rql': 'Any CGH WHERE CGH genomic_region GR, GR genes X',
'vid': 'region-genmeas-table-view',
'order': 1,
'label': _('CGH results')})
self.assertEqual(uicfg.primaryview_display_ctrl._attrrsetdefs, {})
self.assertEqual(uicfg.primaryview_display_ctrl._relrsetdefs['Center'],
[{'rql': 'Any S WHERE S concerned_by Y, X holds Y',
'order': 1, 'vid': 'table', 'label': u'Subjects'}])
self.assertEqual(uicfg.primaryview_display_ctrl._relrsetdefs['Gene'],
[{'rql': 'Any CGH WHERE CGH genomic_region GR, GR genes X',
'vid': 'region-genmeas-table-view',
'order': 1,
'label': _('CGH results')}])
def test_uihelper_iterate_rset_relations(self):
# Add uicfg display_rset
self.add_center_uicfg()
req = self.request()
entity = req.find_one_entity('Center', identifier=u'center_id')
display_rset = self.get_display_rset(req, entity)
self.assertEqual(len(display_rset), 1)
self.assertEqual(display_rset[0][0], 1)
self.assertEqual(display_rset[0][1], u'Subjects')
self.assertTrue(display_rset[0][2].startswith('<div id'))
def test_uihelper_iterate_rset_attributes(self):
# Add uicfg display_rset
_pvdc = uicfg.primaryview_display_ctrl
_pvdc.display_rset('Center', 'attributes',
{'callback': lambda x:x.identifier,
'order': 2,
'label': _('Test callback')})
self.add_center_uicfg()
req = self.request()
entity = req.find_one_entity('Center', identifier=u'center_id')
display_rset = self.get_display_rset(req, entity, 'attributes')
self.assertEqual(len(display_rset), 1)
self.assertEqual(display_rset, [(2, u'Test callback', u'center_id')])
def test_uihelper_iterate_rset_callback(self):
self.add_center_uicfg()
req = self.request()
_pvdc = uicfg.primaryview_display_ctrl
_pvdc.display_rset('Center', 'relations',
{'callback': lambda x:x.identifier,
'order': 2,
'label': _('Test callback')})
entity = req.find_one_entity('Center', identifier=u'center_id')
display_rset = self.get_display_rset(req, entity)
self.assertEqual(len(display_rset), 2)
self.assertEqual(display_rset[1], (2, u'Test callback', u'center_id'))
def test_uihelper_order(self):
req = self.request()
_pvdc = uicfg.primaryview_display_ctrl
_pvdc.display_rset('Center', 'relations',
{'rql': 'Any S WHERE S concerned_by Y, X holds Y',
'vid': 'table',
'order': 2,
'label': _('Subjects 2')})
_pvdc.display_rset('Center', 'relations',
{'rql': 'Any S WHERE S concerned_by Y, X holds Y',
'vid': 'table',
'order': 0,
'label': _('Subjects 0')})
_pvdc.display_rset('Center', 'relations',
{'callback': lambda x:x.identifier,
'order': 1,
'label': _('Subjects 1')})
entity = req.find_one_entity('Center', identifier=u'center_id')
pview = BrainomicsPrimaryView(req)
html = []
html_rel = []
pview.w = html.append
pview.render_relation = lambda x,y: html_rel.append((x,y))
pview.render_entity(entity)
self.assertEqual(html_rel[0][0], 'Subjects 0')
self.assertEqual(html_rel[1][0], 'Subjects 1')
self.assertEqual(html_rel[2][0], 'Subjects 2')
if __name__ == '__main__':
from logilab.common.testlib import unittest_main
unittest_main()
......@@ -18,9 +18,11 @@
"""cubicweb-suivimp views/forms/actions/components for web ui"""
from logilab.mtconverter import xml_escape
from logilab.common.decorators import monkeypatch
from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES
from cubicweb.selectors import is_instance
from cubicweb.web.views import uicfg
from cubicweb.web.views.primary import PrimaryView
from cubes.brainomics.entities import MEASURES
......@@ -47,17 +49,94 @@ class BrainomicsPrimaryView(PrimaryView):
value = self._cw.view(vid, rset)
else:
value = None
# pylint: disable=E1101
label = self._rel_label(entity, rschema, role, dispctrl)
if value is not None and value != '':
display_attributes.append( (rschema, role, dispctrl, value) )
# BRAINOMICS MODIFICATIONS
display_attributes.append((dispctrl.get('order', 9999), label, value))
# BRAINOMICS MODIFICATIONS - Add specific display rset
for order, label, value in self.iterate_display_rset(entity, 'attributes'):
display_attributes.append((order, label, value))
if display_attributes:
self.w(u'<dl class="dl-horizontal">')
for rschema, role, dispctrl, value in display_attributes:
# pylint: disable=E1101
label = self._rel_label(entity, rschema, role, dispctrl)
for order, label, value in sorted(display_attributes, key=lambda x: x[0]):
self.render_attribute(label, value)
self.w(u'</dl>')
def iterate_display_rset(self, entity, section):
""" Iterate over the display rset from uicfg """
# Add specific uicfg rset views
eschema = entity.e_schema
if section == 'attributes':
rsetdefs = self.display_ctrl._attrrsetdefs.get(eschema, [])
elif section == 'relations':
rsetdefs = self.display_ctrl._relrsetdefs.get(eschema, [])
else:
self.warning('Bad section %s for uicfg display rsets' % section)
return
for dispctrl in rsetdefs:
label = dispctrl.get('label')
if label:
label = self._cw._(label)
else:
label = u''
# Get order
order = dispctrl['order'] if 'order' in dispctrl else 9999
# Get value
if 'rql' in dispctrl:
rset = self._cw.execute(dispctrl['rql'] + ', X eid %(e)s', {'e': entity.eid})
if rset:
value = self._cw.view(dispctrl['vid'], rset=rset)
if value:
yield order, label, value
elif 'callback' in dispctrl:
value = dispctrl.get('callback')(entity)
if value:
yield order, label, value
def render_entity_relations(self, entity):
"""Renders all relations in the 'relations' section."""
defaultlimit = self._cw.property_value('navigation.related-limit')
display_relations = []
for rschema, tschemas, role, dispctrl in self._section_def(entity, 'relations'):
if rschema.final or dispctrl.get('rtypevid'):
vid = dispctrl.get('vid', 'reledit')
try:
rview = self._cw.vreg['views'].select(
vid, self._cw, rset=entity.cw_rset, row=entity.cw_row,
col=entity.cw_col, dispctrl=dispctrl,
rtype=rschema, role=role)
except NoSelectableObject:
continue
value = rview.render(row=entity.cw_row, col=entity.cw_col,
rtype=rschema.type, role=role)
else:
vid = dispctrl.get('vid', 'autolimited')
limit = defaultlimit if vid == 'autolimited' else None
rset = self._relation_rset(entity, rschema, role, dispctrl, limit=limit)
if not rset:
continue
if hasattr(self, '_render_relation'):
# pylint: disable=E1101
self._render_relation(dispctrl, rset, 'autolimited')
warn('[3.9] _render_relation prototype has changed and has '
'been renamed to render_relation, please update %s'
% self.__class__, DeprecationWarning)
continue
try:
rview = self._cw.vreg['views'].select(
vid, self._cw, rset=rset, dispctrl=dispctrl)
except NoSelectableObject:
continue
value = rview.render()
label = self._rel_label(entity, rschema, role, dispctrl)
display_relations.append((dispctrl.get('order', 9999), label, value))
# BRAINOMICS MODIFICATIONS - Add specific display rset
for order, label, value in self.iterate_display_rset(entity, 'relations'):
display_relations.append((order, label, value))
if display_relations:
for order, label, value in sorted(display_relations, key=lambda x: x[0]):
self.render_relation(label, value)
def render_attribute(self, label, value):
self.w(u'<dt>%(label)s</dt><dd>%(value)s</dd>'
% {'label': xml_escape(self._cw._(label)),
......@@ -70,8 +149,8 @@ class BrainomicsPrimaryView(PrimaryView):
self.w(value)
self.w(u'</div>')
def render_entity(self, rset=None):
entity = self.cw_rset.get_entity(0,0)
def render_entity(self, entity=None):
entity = entity if entity else self.cw_rset.get_entity(0,0)
w = self.w
# define rsection for similar behavior than CW's primary view
uicfg_reg = self._cw.vreg['uicfg']
......@@ -101,6 +180,81 @@ class BrainomicsPrimaryView(PrimaryView):
tab.render(self.w)
w(u'</div>')
###############################################################################
### UICFG MODIFICATIONS #######################################################
###############################################################################
uicfg.primaryview_display_ctrl._relrsetdefs = {}
uicfg.primaryview_display_ctrl._attrrsetdefs = {}
@monkeypatch(uicfg.DisplayCtrlRelationTags)
def __init__(self, *args, **kwargs):
super(DisplayCtrlRelationTags, self).__init__(*args, **kwargs)
self.counter = 0
self._attrrsetdefs = {}
self._relrsetdefs = {}
@monkeypatch(uicfg.DisplayCtrlRelationTags)
def display_rset(self, etype, section, props):
"""
The display_rset method of uicfg take 3 arguments:
* the name of the etype concerned by the rule;
* the section where the information is displayed (attributes or relations);
* a dictionnary of properties.
The dictionnary of properties must have:
* a 'callback' item OR a 'rql' and a 'vid' items.
The 'callback' is a function that takes the entity and render an HTML snipet.
The 'rql' is a rql query where the variable 'X' is the current entity. The 'vid'
is the __regid__ of the view that will be applied to the result of the rql query.
It could also have:
* a 'label' ('' if not given);
* an 'order' (9999 if not given);
It should be used as follows, e.g. using an etype property:
>>> _pvdc = uicfg.primaryview_display_ctrl
>>> _pvdc.display_rset('MyEtype', 'attributes',
{'callback': lambda x: x.formatted_description,
'label': _('description')})
or with a 'rql' and 'vid' attributes (in this case 'X' is the current entity):
>>> _pvdc = uicfg.primaryview_display_ctrl
>>> _pvdc.display_rset('MyEtype', 'relations',
{'rql': 'Any Z WHERE X relation1 Y, Y relation2 Z',
'vid': 'my-view',
'label': _('My label')})
or with a 'callback' using a view:
>>> _pvdc = uicfg.primaryview_display_ctrl
>>> _pvdc.display_rset('MyEtype', 'relations',
{'callback': lambda x: x.view('my-secondary-view'),
'order': 2,
'label': _('My label')})
"""
assert ('rql' in props and 'vid' in props) or 'callback' in props
if section == 'attributes':
self._attrrsetdefs.setdefault(etype, []).append(props)
elif section == 'relations':
self._relrsetdefs.setdefault(etype, []).append(props)
else:
self.warning('display_rset for %s should be in section '
'"attributes" or "relations"' % etype)
###############################################################################
### REGISTRATION CALLBACK #####################################################
###############################################################################
......
......@@ -152,6 +152,9 @@ class AnswerInContextTableView(EntityTableView):
__regid__ = 'answer-incontext-table-view'
columns = ['answer', 'value', 'datetime']
def render_value(w, entity):
w(u'%s' % entity.computed_value)
def render_text(w, entity):
w(u'%s' % entity.question[0].text)
......@@ -159,7 +162,8 @@ class AnswerInContextTableView(EntityTableView):
w(u'<a href="%s">%s</a>' % (xml_escape(entity.absolute_url()),
xml_escape(entity.dc_title())))
column_renderers = {'answer': EntityTableColRenderer(renderfunc=render_answer)}
column_renderers = {'answer': EntityTableColRenderer(renderfunc=render_answer),
'value': EntityTableColRenderer(renderfunc=render_value)}
class AnswerOutOfContextTableView(EntityTableView):
......
......@@ -27,11 +27,6 @@ _pvdc = uicfg.primaryview_display_ctrl
###############################################################################
### SUBJECT ###################################################################
###############################################################################
# TODO : For Subject, could not handle:
# - age_of_subjects(),
# - external_resources (Subject concerned_by X X external_resources E),
# - order of the attributes / relations.
# Studies
_pvs.tag_subject_of(('Subject', 'related_studies', 'Study'), 'attributes')
# Related therapies
......@@ -65,31 +60,34 @@ _pvdc.tag_object_of(('Subject', 'related_study', 'Study'),
###############################################################################
### THERAPY ###################################################################
###############################################################################
_pvs.tag_object_of(('*', 'taken_in_therapy', 'Therapy'), 'attributes')
_pvs.tag_subject_of(('*', 'therapy_for', 'Disease'), 'attributes')
_pvs.tag_object_of(('*', 'related_therapies', 'Therapy'), 'attributes')
_pvs.tag_subject_of(('Therapy', 'therapy_for', '*'), 'attributes')
_pvs.tag_object_of(('*', 'taken_in_therapy', 'Therapy'), 'relations')
_pvdc.tag_object_of(('*', 'taken_in_therapy', 'Therapy'), {'vid': 'drugtake-table-view'})
###############################################################################
### CENTER ####################################################################
###############################################################################
# TODO : For Center: cannot:
# - display indirectly linked entities, viz. Subjects where
# Subject -- concerned_by -- Assessment and Center -- holds -- Assessment
# Name
_pvs.tag_attribute(('Center', 'name'), 'hidden')
# Assessment
_pvs.tag_subject_of(('Center', 'holds', 'Assessment'), 'relations')
_pvdc.tag_subject_of(('Center', 'holds', 'Assessment'), {'label': _('Assessment'), 'vid': 'list'})
_pvdc.tag_subject_of(('Center', 'holds', 'Assessment'), {'label': _('Assessment'), 'vid': 'table'})
# Device
_pvdc.tag_object_of(('Device', 'hosted_by', 'Center'), {'label': _('Devices'), 'vid': 'list'})
# subjects
_pvdc.display_rset('Center', 'relations',
{'rql': 'Any S WHERE S concerned_by Y, X holds Y',
'vid': 'table',
'order': 1,
'label': _('Subjects')})
###############################################################################
### DEVICE ####################################################################
###############################################################################
# TODO : for Device: cannot:
# - enable the DISTINCT on the generated measures which use the device
# Name
_pvs.tag_attribute(('Device', 'name'), 'hidden')
# Center
......@@ -102,9 +100,6 @@ _pvdc.tag_object_of(('*', 'uses_device', 'Device'),
###############################################################################
### ASSESSMENT ################################################################
###############################################################################
# TODO : for Assessment: cannot:
# - get the object type and put it into the 'label'
# - make synthetic counted table, via COUNT
# Protocol
_pvs.tag_subject_of(('Assessment', 'protocols', 'Protocol'), 'attributes')
# Subject
......@@ -147,10 +142,6 @@ _pvdc.tag_object_of(('Assessment', 'conducted_by', 'Investigator'),
###############################################################################
### GENERICTERUN ##############################################################
###############################################################################
# TODO : for GenericTestRun : could not:
# - handle ScoreDefinition, ScoreValue tuples in the same rset
# - handle indirect external_resources, i.e. A -- generates -- GenericTestRun and
# A -- external_resources -- E
# Instance
_pvs.tag_subject_of(('GenericTestRun', 'instance_of', '*'), 'attributes')
# Concerns
......@@ -193,16 +184,17 @@ _pvdc.tag_object_of(('*', 'related_measure', 'GenomicMeasure'),
###############################################################################
### GENE ######################################################################
###############################################################################
# TODO for Gene: could not:
# - properly reach CghResults; we reach GenomicRegion instead
# Chromosomes
_pvs.tag_subject_of(('Gene', 'chromosomes', '*'), 'attributes')
_pvdc.tag_subject_of(('Gene', 'chromosomes', '*'),
{'vid': 'incontext', 'label': _('Chromosomes')})
# CGH results
_pvs.tag_object_of(('*', 'genes', 'Gene'), 'relations') # GenomicRegion
_pvdc.tag_object_of(('*', 'genes', 'Gene'),
{'label': _('CGH results'), 'vid': 'table'})
_pvs.tag_object_of(('*', 'genes', 'Gene'), 'hidden')
_pvdc.display_rset('Gene', 'relations',
{'rql': 'Any CGH WHERE CGH genomic_region GR, GR genes X',
'vid': 'region-genmeas-table-view',
'order': 1,
'label': _('CGH results')})
# Sequencing results
_pvs.tag_object_of(('*', 'related_gene', 'Gene'), 'relations') # Mutation
_pvdc.tag_object_of(('*', 'related_gene', 'Gene'),
......@@ -213,8 +205,6 @@ _pvdc.tag_object_of(('*', 'related_gene', 'Gene'),
###############################################################################
### GENOMICREGION #############################################################
###############################################################################
# TODO for GenomicRegion: could not:
# - use build_url to build custom RQL-defined URL to a set of entities
# Genes
_pvs.tag_subject_of(('GenomicRegion', 'genes', '*'), 'attributes')
# CGH results
......@@ -227,10 +217,6 @@ _pvdc.tag_object_of(('*', 'genomic_region', 'GenomicRegion'),
###############################################################################
### QUESTIONNAIRERUN ##########################################################
###############################################################################
# TODO for QuestionnaireRun, could not:
# - directly apply a view on an entity itself, e.g. self.w(entity.view('vid'))
# - have a fine-grained control over various entities related to the Subject
# (viz. the Assessment, in this case)
# Instance of
_pvs.tag_subject_of(('QuestionnaireRun', 'instance_of', '*'), 'attributes')
_pvdc.tag_subject_of(('QuestionnaireRun', 'instance_of', '*'), {'vid': 'incontext'})
......@@ -255,9 +241,6 @@ _pvdc.tag_object_of(('*', 'questionnaire_run', 'QuestionnaireRun'),
###############################################################################
### QUESTIONNAIRE #############################################################
###############################################################################
# TODO for Questionnaire, could not:
# - directly call a specific view (viz. 'results-view') on the entity
# - have a fine-grained control over what we display from related entities
# Questionnaire runs
_pvs.tag_object_of(('*', 'instance_of', 'Questionnaire'), 'relations')
_pvdc.tag_object_of(('*', 'instance_of', 'Questionnaire'), {'vid': 'table'})
......@@ -269,10 +252,6 @@ _pvdc.tag_object_of(('*', 'questionnaire', 'Questionnaire'), {'vid': 'question-t
###############################################################################
### QUESTION ##################################################################
###############################################################################
# TODO for Question, could not:
# - have access to ScoreValue's computed_value property
# - same as above, provide fine-grained control over the
# various related entities
# Questionnaire
_pvs.tag_subject_of(('Question', 'questionnaire', '*'), 'attributes')
_pvdc.tag_subject_of(('Question', 'questionnaire', '*'), {'vid': 'incontext'})
......@@ -291,3 +270,24 @@ _pvs.tag_subject_of(('Scan', 'has_data', '*'), 'relations')
_pvdc.tag_subject_of(('Scan', 'has_data', '*'),
{'vid': 'scan-data-view', 'label': _(' ')})
_pvs.tag_object_of(('*', 'external_resources', 'ExternalResource'), 'relations')
###############################################################################
### DRUG ######################################################################
###############################################################################
_pvs.tag_object_of(('DrugTake', 'drug', '*'), 'relations')
_pvdc.tag_object_of(('DrugTake', 'drug', '*'), {'vid': 'drugtake-table-view'})
###############################################################################
### DISEASE ###################################################################
###############################################################################
_pvs.tag_object_of(('*', 'related_diseases', 'Disease'), 'relations')
_pvdc.tag_object_of(('*', 'related_diseases', 'Disease'), {'vid': 'table'})
###############################################################################
### BODYLOCATION ##############################################################
###############################################################################
_pvs.tag_subject_of(('Subject', 'related_lesions', '*'), 'attributes')
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment