[PATCH 8 of 9 seda] [profile gen] Support for multiple concepts on mime type and format id

Sylvain Thenault sylvain.thenault at logilab.fr
Thu Oct 12 16:59:53 CEST 2017


# HG changeset patch
# User Sylvain Thénault <sylvain.thenault at logilab.fr>
# Date 1507805550 -7200
#      Thu Oct 12 12:52:30 2017 +0200
# Node ID cc3b3e0f6d427c8413ddea9dd7dd69749b678859
# Parent  a9d8ac0426d6e57e255be6c3b01ad5d108a86994
# Available At https://hg.logilab.org/review/cubes/seda
#              hg pull https://hg.logilab.org/review/cubes/seda -r cc3b3e0f6d42
[profile gen] Support for multiple concepts on mime type and format id

since this may now occurs with values deducted from file category. In such case,
generates proper rng:choices / xsd:enumeration.

Related to #36331831

diff --git a/cubicweb_seda/entities/custom.py b/cubicweb_seda/entities/custom.py
--- a/cubicweb_seda/entities/custom.py
+++ b/cubicweb_seda/entities/custom.py
@@ -343,12 +343,12 @@ class SEDASeqStorageRuleRule(RuleRuleMix
 
 
 class SEDAFormatId(generated.SEDAFormatId):
 
     @property
-    def concept(self):
-        return self.seda_format_id_to[0] if self.seda_format_id_to else None
+    def concepts(self):
+        return self.seda_format_id_to
 
 
 class SEDAEncoding(generated.SEDAEncoding):
 
     @property
@@ -357,12 +357,12 @@ class SEDAEncoding(generated.SEDAEncodin
 
 
 class SEDAMimeType(generated.SEDAMimeType):
 
     @property
-    def concept(self):
-        return self.seda_mime_type_to[0] if self.seda_mime_type_to else None
+    def concepts(self):
+        return self.seda_mime_type_to
 
 
 class SEDAType(generated.SEDAType):
 
     @property
diff --git a/cubicweb_seda/entities/profile_generation.py b/cubicweb_seda/entities/profile_generation.py
--- a/cubicweb_seda/entities/profile_generation.py
+++ b/cubicweb_seda/entities/profile_generation.py
@@ -228,11 +228,16 @@ class RNGMixin(object):
             # every custom type to string, supposing transfer are also checked against the original
             # schema (as agape v1 was doing).
             datatype = 'string'
         type_attrs = {'type': datatype}
         if fixed_value is not None:
-            self.element('rng:value', element, type_attrs, text=fixed_value)
+            if isinstance(fixed_value, list):
+                choice = self.element('rng:choice', element)
+                for value in fixed_value:
+                    self.element('rng:value', choice, type_attrs, text=value)
+            else:
+                self.element('rng:value', element, type_attrs, text=fixed_value)
         elif default_value is not None:
             element.attrib[self.qname('a:defaultValue')] = default_value
             self.element('rng:data', element, type_attrs)
         else:
             self.element('rng:data', element, type_attrs)
@@ -546,10 +551,12 @@ class SEDA2RelaxNGExport(RNGMixin, SEDA2
                     self._rng_attribute(occ.target, parent_element)
         # special case for KeywordReference content, the only known case where we want URL instead
         # of label of its concept value
         if value is not None and xselement.local_name == 'KeywordReference':
             fixed_value = self.cwuri_url(value)
+        elif isinstance(value, list):
+            fixed_value = [serialize(val, self.cwuri_url) for val in value]
         else:
             fixed_value = serialize(value, self.cwuri_url)
         if fixed_value is not None:
             if _internal_reference(value):
                 profile_element.attrib[self.qname('a:defaultValue')] = fixed_value
@@ -558,11 +565,16 @@ class SEDA2RelaxNGExport(RNGMixin, SEDA2
                 if (len(profile_element)
                         and profile_element[-1].tag == '{http://relaxng.org/ns/structure/1.0}data'):
                     xstype = profile_element[-1].attrib.get('type')
                     profile_element.remove(profile_element[-1])
                 attrs = {'type': xstype} if xstype else {}
-                self.element('rng:value', profile_element, attrs, text=fixed_value)
+                if isinstance(fixed_value, list):
+                    choice = self.element('rng:choice', profile_element)
+                    for val in fixed_value:
+                        self.element('rng:value', choice, attrs, text=val)
+                else:
+                    self.element('rng:value', profile_element, attrs, text=fixed_value)
         elif xstype is not None:
             self.element('rng:data', profile_element, {'type': xstype})
 
     def concept_scheme_attribute(self, xselement, type_element, scheme):
         try:
@@ -637,12 +649,16 @@ class XAttr(namedtuple('_XAttr', ['name'
     * `fixed_value`, optional fixed value for the attribute.
 
     """
     def __new__(cls, name, qualified_type, cardinality='0..1', fixed_value=None):
         assert cardinality in (None, '1', '0..1'), cardinality
-        if fixed_value is not None:
+        if fixed_value:
             cardinality = '1'
+            if isinstance(fixed_value, list) and len(fixed_value) == 1:
+                fixed_value = fixed_value[0]
+        else:
+            fixed_value = None
         return super(XAttr, cls).__new__(cls, name, qualified_type, cardinality, fixed_value)
 
 
 LIST_VERSION_ID_2009 = XAttr('listVersionID', 'xsd:token', '1', 'edition 2009')
 LIST_VERSION_ID_2011 = XAttr('listVersionID', 'xsd:token', '1', 'edition 2011')
@@ -715,20 +731,28 @@ class SEDA1XSDExport(SEDA2ExportAdapter)
         for xattr in xsd_attributes:
             self.attribute_schema(attributes_parent, xattr)
         return children_parent
 
     def attribute_schema(self, parent, xattr):
-        attrs = {'name': xattr.name, 'type': xattr.qualified_type}
+        attrs = {'name': xattr.name}
         if xattr.cardinality is None:
             attrs['use'] = 'prohibited'
         elif xattr.cardinality == '1':
             attrs['use'] = 'required'
         else:
             attrs['use'] = 'optional'
-        if xattr.fixed_value is not None:
-            attrs['fixed'] = text_type(xattr.fixed_value)
-        self.element('xsd:attribute', parent, attrs)
+        if not isinstance(xattr.fixed_value, list):
+            attrs['type'] = xattr.qualified_type
+            if isinstance(xattr.fixed_value, string_types):
+                attrs['fixed'] = text_type(xattr.fixed_value)
+        attribute_element = self.element('xsd:attribute', parent, attrs)
+        if isinstance(xattr.fixed_value, list):
+            type_element = self.element('xsd:simpleType', attribute_element)
+            restriction_element = self.element('xsd:restriction', type_element,
+                                               {'base': 'xsd:token'})
+            for value in xattr.fixed_value:
+                self.element('xsd:enumeration', restriction_element, {'value': value})
 
     # business visit methods #######################################################################
 
     def dump_etree(self):
         """Return an XSD etree for the adapted SEDA profile."""
@@ -856,24 +880,28 @@ class SEDA1XSDExport(SEDA2ExportAdapter)
 
     def xsd_attachment(self, parent, data_object):
         _safe_concept = partial(_safe_concept_value, concepts_language=self.concepts_language)
 
         format_id = data_object.format_id
+        format_ids = [_concept_value(concept, self.concepts_language)
+                      for concept in format_id.concepts]
+        mime_type = data_object.mime_type
+        mime_types = [_concept_value(concept, self.concepts_language)
+                      for concept in mime_type.concepts]
         encoding = data_object.encoding
-        mime_type = data_object.mime_type
         self.element_schema(parent, 'Attachment', 'qdt:ArchivesBinaryObjectType',
                             xsd_attributes=[
                                 XAttr('format', 'clmDAFFileTypeCode:FileTypeCodeType',
-                                      cardinality=_safe_cardinality(format_id),
-                                      fixed_value=_safe_concept(format_id)),
+                                      cardinality=format_id.user_cardinality,
+                                      fixed_value=format_ids),
                                 XAttr('encodingCode',
                                       'clm60133:CharacterSetEncodingCodeContentType',
                                       cardinality=_safe_cardinality(encoding),
                                       fixed_value=_safe_concept(encoding)),
                                 XAttr('mimeCode', 'clmIANAMIMEMediaType:MIMEMediaTypeContentType',
-                                      cardinality=_safe_cardinality(mime_type),
-                                      fixed_value=_safe_concept(mime_type)),
+                                      cardinality=mime_type.user_cardinality,
+                                      fixed_value=mime_types),
                                 XAttr('filename', 'xsd:string',
                                       cardinality='0..1',
                                       fixed_value=data_object.filename),
                                 # hard-coded attributes
                                 XAttr('characterSetCode',
@@ -1382,11 +1410,16 @@ def _complex_path_target_values(entity, 
                     # the relation is not defined in the schema: element is not modelized but should
                     # be added in the XSD
                     rtype_targets.append((entity, None))
                     continue
                 if targets:
-                    rtype_targets += [(entity, t) for t in targets]
+                    if len(targets) > 1 and targets[0].cw_etype == 'Concept':
+                        # same element with several allowed values
+                        rtype_targets += [(entity, targets)]
+                    else:
+                        # different children
+                        rtype_targets += [(entity, t) for t in targets]
                 # if relation is not composite, that means it's a "value" relation, hence we should
                 # always emit a value (its associated XSD element must be defined)
                 elif not rdefschema.composite:
                     rtype_targets.append((entity, None))
         entities = [v for e, v in rtype_targets]
diff --git a/test/data/seda_02_export.rng b/test/data/seda_02_export.rng
--- a/test/data/seda_02_export.rng
+++ b/test/data/seda_02_export.rng
@@ -382,11 +382,14 @@
                   <rng:data type="ID"/>
                 </rng:attribute>
               </rng:optional>
               <rng:element name="Attachment">
                 <rng:attribute name="format">
-                  <rng:value type="string">fmt/123</rng:value>
+                  <rng:choice>
+                    <rng:value type="string">fmt/123</rng:value>
+                    <rng:value type="string">fmt/987</rng:value>
+                  </rng:choice>
                 </rng:attribute>
                 <rng:attribute name="encodingCode">
                   <rng:value type="string">6</rng:value>
                 </rng:attribute>
                 <rng:optional>
diff --git a/test/data/seda_02_export.xsd b/test/data/seda_02_export.xsd
--- a/test/data/seda_02_export.xsd
+++ b/test/data/seda_02_export.xsd
@@ -282,11 +282,18 @@
                   <xsd:sequence>
                     <xsd:element name="Attachment">
                       <xsd:complexType>
                         <xsd:simpleContent>
                           <xsd:extension base="qdt:ArchivesBinaryObjectType">
-                            <xsd:attribute fixed="fmt/123" name="format" type="clmDAFFileTypeCode:FileTypeCodeType" use="required"/>
+                            <xsd:attribute name="format" use="required">
+                              <xsd:simpleType>
+                                <xsd:restriction base="xsd:token">
+			                <xsd:enumeration value="fmt/123"/>
+			                <xsd:enumeration value="fmt/987"/>
+                                </xsd:restriction>
+                              </xsd:simpleType>
+                            </xsd:attribute>
                             <xsd:attribute fixed="6" name="encodingCode" type="clm60133:CharacterSetEncodingCodeContentType" use="required"/>
                             <xsd:attribute name="mimeCode" type="clmIANAMIMEMediaType:MIMEMediaTypeContentType" use="optional"/>
                             <xsd:attribute fixed="this_is_the_filename.pdf" name="filename" type="xsd:string" use="required"/>
                             <xsd:attribute name="characterSetCode" type="clmIANACharacterSetCode:CharacterSetCodeContentType" use="prohibited"/>
                             <xsd:attribute name="uri" type="xsd:anyURI" use="prohibited"/>
diff --git a/test/data/seda_1_export.rng b/test/data/seda_1_export.rng
--- a/test/data/seda_1_export.rng
+++ b/test/data/seda_1_export.rng
@@ -529,11 +529,14 @@
                   <rng:data type="ID"/>
                 </rng:attribute>
               </rng:optional>
               <rng:element name="Attachment">
                 <rng:attribute name="format">
-                  <rng:value type="string">fmt/123</rng:value>
+                  <rng:choice>
+                    <rng:value type="string">fmt/123</rng:value>
+                    <rng:value type="string">fmt/987</rng:value>
+                  </rng:choice>
                 </rng:attribute>
                 <rng:attribute name="encodingCode">
                   <rng:value type="string">6</rng:value>
                 </rng:attribute>
                 <rng:optional>
diff --git a/test/data/seda_1_export.xsd b/test/data/seda_1_export.xsd
--- a/test/data/seda_1_export.xsd
+++ b/test/data/seda_1_export.xsd
@@ -427,11 +427,18 @@
                   <xsd:sequence>
                     <xsd:element name="Attachment">
                       <xsd:complexType>
                         <xsd:simpleContent>
                           <xsd:extension base="qdt:ArchivesBinaryObjectType">
-                            <xsd:attribute fixed="fmt/123" name="format" type="clmDAFFileTypeCode:FileTypeCodeType" use="required"/>
+                            <xsd:attribute name="format" use="required">
+                              <xsd:simpleType>
+                                <xsd:restriction base="xsd:token">
+			                <xsd:enumeration value="fmt/123"/>
+			                <xsd:enumeration value="fmt/987"/>
+                                </xsd:restriction>
+                              </xsd:simpleType>
+                            </xsd:attribute>
                             <xsd:attribute fixed="6" name="encodingCode" type="clm60133:CharacterSetEncodingCodeContentType" use="required"/>
                             <xsd:attribute name="mimeCode" type="clmIANAMIMEMediaType:MIMEMediaTypeContentType" use="optional"/>
                             <xsd:attribute fixed="this_is_the_filename.pdf" name="filename" type="xsd:string" use="required"/>
                             <xsd:attribute name="characterSetCode" type="clmIANACharacterSetCode:CharacterSetCodeContentType" use="prohibited"/>
                             <xsd:attribute name="uri" type="xsd:anyURI" use="prohibited"/>
diff --git a/test/test_profile_generation.py b/test/test_profile_generation.py
--- a/test/test_profile_generation.py
+++ b/test/test_profile_generation.py
@@ -531,10 +531,31 @@ class SEDA2RNGExportTC(RelaxNGTestMixin,
                  {'name': 'listName', 'use': 'optional', 'type': 'xsd:string'},
                  {'name': 'listSchemeURI', 'use': 'optional', 'type': 'xsd:anyURI'},
                  {'name': 'listURI', 'use': 'optional', 'type': 'xsd:anyURI'},
                  {'name': 'listVersionID', 'use': 'optional', 'type': 'xsd:token'}])
 
+    def test_multiple_concepts(self):
+        with self.admin_access.cnx() as cnx:
+            transfer = cnx.create_entity('SEDAArchiveTransfer', title=u'test profile')
+            scheme = testutils.scheme_for_type(cnx, None, None,
+                                               u'application/msword', u'application/pdf')
+            cnx.create_entity('SEDAMimeTypeCodeListVersion',
+                              seda_mime_type_code_list_version_from=transfer,
+                              seda_mime_type_code_list_version_to=scheme)
+            bdo = testutils.create_data_object(transfer)
+            bdo.mime_type.cw_set(user_cardinality=u'1',
+                                 seda_mime_type_to=scheme.reverse_in_scheme)
+
+            profile = self.profile_etree(transfer)
+            mt = self.get_element(profile, 'MimeType')
+            self.assertEqual('\n'.join(etree.tostring(mt, pretty_print=True).splitlines()[1:-1]),
+                             '''\
+  <rng:choice>
+    <rng:value type="token">application/msword</rng:value>
+    <rng:value type="token">application/pdf</rng:value>
+  </rng:choice>''')
+
     def test_seda2_concept(self):
         with self.admin_access.cnx() as cnx:
             create = cnx.create_entity
             scheme = create('ConceptScheme', title=u'Digest algorithm')
             some_concept = scheme.add_concept(label=u'md5 algorithm', language_code=u'en')
@@ -708,22 +729,26 @@ class OldSEDAExportMixin(object):
     def setup_database(self):
         with self.admin_access.cnx() as cnx:
             create = cnx.create_entity
 
             concepts = {}
-            for rtype, etype, value in [
-                    ('seda_format_id_to', None, u'fmt/123'),
-                    ('seda_encoding_to', None, u'6'),
-                    ('seda_type_to', None, u'CDO'),
-                    ('seda_description_level', None, u'file'),
-                    ('seda_algorithm', 'SEDABinaryDataObject', u'md5'),
-                    ('seda_rule', 'SEDASeqAppraisalRuleRule', u'P10Y'),
-                    ('seda_rule', 'SEDASeqAccessRuleRule', u'AR038'),
-                    ('seda_final_action', 'SEDAAppraisalRule', u'detruire'),
+            for rtype, etype, labels in [
+                    ('seda_format_id_to', None, [u'fmt/123', u'fmt/987']),
+                    ('seda_encoding_to', None, [u'6']),
+                    ('seda_type_to', None, [u'CDO']),
+                    ('seda_description_level', None, [u'file']),
+                    ('seda_algorithm', 'SEDABinaryDataObject', [u'md5']),
+                    ('seda_rule', 'SEDASeqAppraisalRuleRule', [u'P10Y']),
+                    ('seda_rule', 'SEDASeqAccessRuleRule', [u'AR038']),
+                    ('seda_final_action', 'SEDAAppraisalRule', [u'detruire']),
             ]:
-                scheme = testutils.scheme_for_type(cnx, rtype, etype, value)
-                concepts[value] = scheme.reverse_in_scheme[0]
+                scheme = testutils.scheme_for_type(cnx, rtype, etype, *labels)
+                if len(labels) == 1:
+                    concepts[labels[0]] = scheme.reverse_in_scheme[0]
+                else:
+                    for concept in scheme.reverse_in_scheme:
+                        concepts[concept.label()] = concept
 
             # ensure we're able to export concept with unexpected language code
             concepts['md5'].preferred_label[0].cw_set(language_code=u'de')
 
             agent = testutils.create_authority_record(cnx, u'bob')
@@ -818,11 +843,11 @@ class OldSEDAExportMixin(object):
                                                seda_algorithm=concepts['md5'],
                                                reverse_seda_data_object_reference_id=ref)
 
             bdo.format_id.cw_set(
                 user_cardinality=u'1',
-                seda_format_id_to=concepts['fmt/123'])
+                seda_format_id_to=[concepts['fmt/123'], concepts['fmt/987']])
             create('SEDAEncoding',
                    user_cardinality=u'1',
                    seda_encoding_from=bdo,
                    seda_encoding_to=concepts['6'])
 


More information about the saem-devel mailing list