Commit 1f51191c authored by Sylvain Thénault's avatar Sylvain Thénault
Browse files

[ui] Add proper validation and vocabulary on ref_non_rule_id_to

Validation is done in a custom hook due to cw'schema limitation: all constraints
must be checked and we need several one in our case (one for each possible
parent type).

A custom vocabulary function is introduced and reused for the 'seda_rule'
relation. There is still some limitation in case where the parent entity has not
yet been created, as we're missing information to go back to the container
holding the scheme definition.

Closes #13562327
parent 116036b73955
......@@ -24,6 +24,16 @@ from cubes.seda.xsd import XSDMMapping
XSDM_MAPPING = XSDMMapping('ArchiveTransfer')
def rule_type_from_etype(etype):
"""Return the rule type from an etype like SEDAAccessRule or SEDAAltAccessRulePreventInheritance
"""
if etype.startswith('SEDAAlt'):
rule_type = etype[len('SEDAAlt'):-len('RulePreventInheritance')]
else:
rule_type = etype[len('SEDA'):-len('Rule')]
return rule_type.lower()
def iter_rtype_role(xsd_element):
"""Given a XSD element name, yield (rtype, role, path) where `rtype` and `role` define a
relation of the yams data model for a subelement, also including the full `path` if you need
......
......@@ -17,8 +17,16 @@
from collections import defaultdict
from yams import ValidationError
from yams.schema import role_name
from cubicweb.server import hook
from cubes.seda.entities import rule_type_from_etype
_ = unicode
SEDA_PARENT_RTYPES = {}
......@@ -63,6 +71,39 @@ class SetContainerHook(hook.Hook):
SetContainerOp.get_instance(self._cw).add_data((self.eidto, self.eidfrom))
class CheckRefNonRuleIdCodeListHook(hook.Hook):
"""Watch for addition of concept through seda_ref_non_rule_id_to relation, to ensure it belongs
to the scheme specified on the transfer
This depends on the parent entity type and can not properly be handled with a rql constraint,
since it would require several disjoint constraints, while cw's semantic is that all constraints
should be matched.
"""
__regid__ = 'seda.schema.ref_non_rule_id'
__select__ = hook.Hook.__select__ & hook.match_rtype('seda_ref_non_rule_id_to')
events = ('after_add_relation',)
def __call__(self):
CheckRefNonRuleIdCodeListOp(self._cw, parent=self.eidfrom, concept=self.eidto)
# Late operation to be called once `container` relation is set
class CheckRefNonRuleIdCodeListOp(hook.LateOperation):
def precommit_event(self):
parent = self.cnx.entity_from_eid(self.parent, etype='SEDARefNonRuleId')
# generate constraint on the concept's scheme depending on the parent's parent type
parent_parent = parent.cw_adapt_to('IContained').parent
rule_type = rule_type_from_etype(parent_parent.cw_etype)
rql = ('Any C WHERE C eid %(c)s, C in_scheme CS, X eid %(x)s, X container AT, '
'CACLV seda_{0}_rule_code_list_version_from AT, '
'CACLV seda_{0}_rule_code_list_version_to CS'.format(rule_type))
if not self.cnx.execute(rql, {'c': self.concept, 'x': parent_parent.eid}):
msg = _("this concept doesn't belong to scheme specified on the profile")
raise ValidationError(parent.eid,
{role_name('seda_ref_non_rule_id_to', 'subject'): msg})
def registration_callback(vreg):
vreg.register_all(globals().values(), __name__)
from cubes.seda.entities import seda_profile_container_def
......
......@@ -7662,6 +7662,9 @@ msgctxt "SEDATemporal"
msgid "temporal"
msgstr ""
msgid "this concept doesn't belong to scheme specified on the profile"
msgstr ""
msgctxt "SEDAArchiveTransfer"
msgid "title"
msgstr ""
......@@ -8253,3 +8256,10 @@ msgstr ""
msgid "without start date"
msgstr ""
#, python-brace-format
msgid "you must specify a scheme for {0} to select a value"
msgstr ""
msgid "you must validate first to select a possible value"
msgstr ""
......@@ -7674,6 +7674,9 @@ msgctxt "SEDATemporal"
msgid "temporal"
msgstr ""
msgid "this concept doesn't belong to scheme specified on the profile"
msgstr "ce concept n'appartient pas au vocabulaire spécifier sur le profil"
msgctxt "SEDAArchiveTransfer"
msgid "title"
msgstr ""
......@@ -8265,3 +8268,10 @@ msgstr "sans date de réévaluation"
msgid "without start date"
msgstr "sans date de départ"
#, python-brace-format
msgid "you must specify a scheme for {0} to select a value"
msgstr "spécifiez d'abord un vocabulaire pour \"{0}\""
msgid "you must validate first to select a possible value"
msgstr "validez avant de pouvoir sélectionner une valeur"
# copyright 2016 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/>.
"""cubicweb-seda unit tests for hooks"""
from cubicweb import ValidationError
from cubicweb.devtools.testlib import CubicWebTC
from test_schema import create_transfer_to_bdo
class ValidationHooksTC(CubicWebTC):
def test_ref_non_rule_constraints(self):
with self.admin_access.client_cnx() as cnx:
create = cnx.create_entity
access_scheme = create('ConceptScheme', title=u'access')
access_concept = access_scheme.add_concept(label=u'anyone')
reuse_scheme = create('ConceptScheme', title=u'reuse')
reuse_concept = reuse_scheme.add_concept(label=u'share-alike')
cnx.commit()
bdo = create_transfer_to_bdo(cnx)
transfer = bdo.container[0]
create('SEDAAccessRuleCodeListVersion',
seda_access_rule_code_list_version_from=transfer,
seda_access_rule_code_list_version_to=access_scheme)
create('SEDAReuseRuleCodeListVersion',
seda_reuse_rule_code_list_version_from=transfer,
seda_reuse_rule_code_list_version_to=reuse_scheme)
cnx.commit()
rule_base = create('SEDAAccessRule', seda_access_rule=transfer)
rule_alt = create('SEDAAltAccessRulePreventInheritance',
reverse_seda_alt_access_rule_prevent_inheritance=rule_base)
non_rule = create('SEDARefNonRuleId', seda_ref_non_rule_id_from=rule_alt)
cnx.commit()
non_rule.cw_set(seda_ref_non_rule_id_to=reuse_concept)
with self.assertRaises(ValidationError) as cm:
cnx.commit()
self.assertIn('seda_ref_non_rule_id_to-subject', cm.exception.errors)
non_rule.cw_set(seda_ref_non_rule_id_to=access_concept)
cnx.commit()
if __name__ == '__main__':
import unittest
unittest.main()
......@@ -80,3 +80,8 @@ class SchemaTC(CubicWebTC):
create('SEDAMimeType', seda_mime_type_from=bdo, seda_mime_type_to=mt_concept)
cnx.commit()
if __name__ == '__main__':
import unittest
unittest.main()
# copyright 2016 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/>.
"""cubicweb-seda unit tests for schema"""
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.web import INTERNAL_FIELD_VALUE
from test_schema import create_transfer_to_bdo
class ManagementRulesTC(CubicWebTC):
def test_rule_ref_vocabulary(self):
from cubes.seda.views.mgmt_rules import _rule_ref_vocabulary
with self.admin_access.client_cnx() as cnx:
create = cnx.create_entity
access_scheme = create('ConceptScheme', title=u'access')
access_concept = access_scheme.add_concept(label=u'anyone')
cnx.commit()
bdo = create_transfer_to_bdo(cnx)
transfer = bdo.container[0]
rule_base = create('SEDAAccessRule', seda_access_rule=transfer)
rule_alt = create('SEDAAltAccessRulePreventInheritance',
reverse_seda_alt_access_rule_prevent_inheritance=rule_base)
cnx.commit()
self.assertEqual(_rule_ref_vocabulary(rule_base, transfer),
[('you must specify a scheme for seda_access_rule_code_list_version_to'
' to select a value', INTERNAL_FIELD_VALUE)])
self.assertEqual(_rule_ref_vocabulary(rule_alt, transfer),
[('you must specify a scheme for seda_access_rule_code_list_version_to'
' to select a value', INTERNAL_FIELD_VALUE)])
create('SEDAAccessRuleCodeListVersion',
seda_access_rule_code_list_version_from=transfer,
seda_access_rule_code_list_version_to=access_scheme)
cnx.commit()
self.assertEqual(_rule_ref_vocabulary(rule_base, transfer),
[(access_concept.label(), unicode(access_concept.eid))])
self.assertEqual(_rule_ref_vocabulary(rule_alt, transfer),
[(access_concept.label(), unicode(access_concept.eid))])
if __name__ == '__main__':
import unittest
unittest.main()
......@@ -14,6 +14,8 @@
# 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/>.
import json
from yams import BASE_TYPES
from cubicweb import tags, neg_role
......@@ -165,3 +167,33 @@ def dropdown_button(text, links):
w(u'</ul>')
w(u'</div>')
return data.getvalue()
def parent_and_container(entity):
"""Attempt to return the direct parent and container from the entity, handling case where entity
is being created and container information will be found through linkto or parent form
(partially supported)
"""
if entity.has_eid():
# entity is expected to be a contained entity, not the container itself
container = entity.cw_adapt_to('IContained').container
parent = entity.cw_adapt_to('IContained').parent
else:
# but parent entity, retrieved through linkto, may be the container itself or a
# contained entity
try:
parent_eid = int(entity._cw.form['__linkto'].split(':')[1])
except KeyError:
# ajax created form
try:
parent_eid = int(json.loads(entity._cw.form['arg'][0]))
except ValueError:
# unable to get parent eid for now :()
return None, None
parent = entity._cw.entity_from_eid(parent_eid)
icontainer = parent.cw_adapt_to('IContainer')
if icontainer is None:
container = parent.cw_adapt_to('IContained').container
else:
container = icontainer.container
return parent, container
......@@ -26,7 +26,8 @@ from cubicweb.web.views import uicfg, tabs
from cubes.relationwidget import views as rwdg
from cubes.seda.xsd2yams import RDEF_CONSTRAINTS
from cubes.seda.views import rtags_from_xsd_element, add_subobject_link, viewlib
from cubes.seda.views import rtags_from_xsd_element, add_subobject_link, parent_and_container
from cubes.seda.views import viewlib
_ = unicode
......@@ -36,35 +37,6 @@ afs = uicfg.autoform_section
affk = uicfg.autoform_field_kwargs
def _container_eid(entity):
"""Attempt to return the container eid from the entity, handling case where entity is being
created and container information will be find through linkto or parent form (partially
supported)
"""
if entity.has_eid():
# entity is expected to be a contained entity, not the container itself
container = entity.cw_adapt_to('IContained').container
else:
# but parent entity, retrieved through linkto, may be the container itself or a
# contained entity
try:
parent_eid = int(entity._cw.form['__linkto'].split(':')[1])
except KeyError:
# ajax created form
try:
parent_eid = int(json.loads(entity._cw.form['arg'][0]))
except ValueError:
# unable to get parent eid for now :()
return None
parent = entity._cw.entity_from_eid(parent_eid)
icontainer = parent.cw_adapt_to('IContainer')
if icontainer is None:
container = parent.cw_adapt_to('IContained').container
else:
container = icontainer.container
return container.eid
class ContainedRelationFacetWidget(rwdg.RelationFacetWidget):
def trigger_search_url(self, entity, url_params):
......@@ -73,10 +45,10 @@ class ContainedRelationFacetWidget(rwdg.RelationFacetWidget):
This information will be used later for proper vocabulary computation.
"""
# first retrieve the container entity
container_eid = _container_eid(entity)
_, container = parent_and_container(entity)
# and put it as an extra url param
if container_eid is not None:
url_params['container'] = unicode(container_eid)
if container is not None:
url_params['container'] = unicode(container.eid)
return super(ContainedRelationFacetWidget, self).trigger_search_url(entity, url_params)
......@@ -142,6 +114,8 @@ for key, rql_expr in RDEF_CONSTRAINTS.items():
except ValueError:
etype = '*'
rtype = key
if rtype == 'seda_rule':
continue # managed in mgmt_rules module
affk.tag_subject_of((etype, rtype, '*'),
{'widget': ContainedRelationFacetWidget(dialog_options={'width': 800})})
......
......@@ -18,8 +18,50 @@
from logilab.mtconverter import xml_escape
from cubicweb.predicates import is_instance
from cubicweb.web import INTERNAL_FIELD_VALUE
from cubicweb.web.views import uicfg
from cubes.seda.views import viewlib
from cubes.seda.entities import rule_type_from_etype
from cubes.seda.views import parent_and_container, viewlib
affk = uicfg.autoform_field_kwargs
def rule_ref_vocabulary(form, field):
"""Form vocabulary function, for fields referencing a management rules (i.e. a concept in a
scheme defined on the archive transfer)
"""
req = form._cw
parent, container = parent_and_container(form.edited_entity)
if container is None:
# missing parent information
msg = req._('you must validate first to select a possible value')
return [(msg, INTERNAL_FIELD_VALUE)]
return _rule_ref_vocabulary(parent, container)
def _rule_ref_vocabulary(parent, container):
req = parent._cw
rule_type = rule_type_from_etype(parent.cw_etype)
rql = ('Any C WHERE C in_scheme CS, AT eid %(at)s, '
'CACLV seda_{0}_rule_code_list_version_from AT, '
'CACLV seda_{0}_rule_code_list_version_to CS'.format(rule_type))
rset = req.execute(rql, {'at': container.eid})
if rset:
return [(concept.label(), unicode(concept.eid))
for concept in rset.entities()]
else:
scheme_relation = 'seda_{0}_rule_code_list_version_to'.format(rule_type)
scheme_relation = req._(scheme_relation)
msg = req._('you must specify a scheme for {0} to select a value').format(scheme_relation)
return [(msg, INTERNAL_FIELD_VALUE)]
affk.tag_subject_of(('*', 'seda_ref_non_rule_id_to', '*'),
{'choices': rule_ref_vocabulary})
affk.tag_subject_of(('*', 'seda_rule', '*'),
{'choices': rule_ref_vocabulary})
class RuleComplexLinkEntityAttributeView(viewlib.TextEntityAttributeView):
......
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