Backport basic SEDA 1.0 export from the saem cube
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Thu, 29 Sep 2016 18:54:30 +0200
changeset 1609 8a899bb52c8b
parent 1608 c502200d190e
child 1621 b9560f846e58
Backport basic SEDA 1.0 export from the saem cube a few features are missing but most things are there. This has been the occasion to add properties to entity class, to make things easier to read (in xsd generation and tests, for now at least). Related to #15045341
entities/custom.py
entities/profile_generation.py
test/data/seda_1_export.xsd
test/test_profile_generation.py
--- a/entities/custom.py	Thu Sep 29 18:50:30 2016 +0200
+++ b/entities/custom.py	Thu Sep 29 18:54:30 2016 +0200
@@ -22,6 +22,22 @@
     def dc_title(self):
         return self.title
 
+    @property
+    def comments(self):
+        return self.reverse_seda_comment
+
+    @property
+    def archive_units(self):
+        return self.reverse_seda_archive_unit
+
+    @property
+    def physical_data_objects(self):
+        return self.reverse_seda_physical_data_object
+
+    @property
+    def binary_data_objects(self):
+        return self.reverse_seda_binary_data_object
+
 
 class SEDAArchiveUnit(generated.SEDAArchiveUnit):
 
@@ -33,6 +49,17 @@
         return self.related('seda_alt_archive_unit_archive_unit_ref_id', 'subject').one()
 
 
+class SEDABinaryDataObject(generated.SEDABinaryDataObject):
+
+    @property
+    def format_id(self):
+        return self.reverse_seda_format_id_from[0] if self.reverse_seda_format_id_from else None
+
+    @property
+    def encoding(self):
+        return self.reverse_seda_encoding_from[0]
+
+
 class SEDAAltArchiveUnitArchiveUnitRefId(generated.SEDAAltArchiveUnitArchiveUnitRefId):
 
     @property
@@ -53,14 +80,57 @@
         return None
 
 
+class SEDASeqAltArchiveUnitArchiveUnitRefIdManagement(
+        generated.SEDASeqAltArchiveUnitArchiveUnitRefIdManagement):
+
+    @property
+    def contents(self):
+        return self.reverse_seda_content
+
+
 class SEDAContent(generated.SEDAContent):
 
     def dc_title(self):
         seda_descr_lvl = self.seda_description_level
         title = (seda_descr_lvl[0].label() if seda_descr_lvl
                  else self._cw._('<no description level specified>'))
-        seda_title = self.reverse_seda_title[0]
-        return u'{0} ({1})'.format(title, seda_title.title or self._cw._('<no title specified>'))
+        return u'{0} ({1})'.format(title, self.title.title or self._cw._('<no title specified>'))
+
+    @property
+    def title(self):
+        return self.reverse_seda_title[0]
+
+    @property
+    def keywords(self):
+        return self.reverse_seda_keyword
+
+    @property
+    def description_level_concept(self):
+        return self.seda_description_level[0] if self.seda_description_level else None
+
+    @property
+    def description(self):
+        return self.reverse_seda_description[0] if self.reverse_seda_description else None
+
+    @property
+    def start_date(self):
+        return self.reverse_seda_start_date[0] if self.reverse_seda_start_date else None
+
+    @property
+    def end_date(self):
+        return self.reverse_seda_end_date[0] if self.reverse_seda_end_date else None
+
+
+class SEDAKeyword(generated.SEDAKeyword):
+
+    @property
+    def content_string(self):
+        return self.seda_keyword_content[0].keyword_content
+
+    @property
+    def reference(self):
+        return (self.reverse_seda_keyword_reference_from[0]
+                if self.reverse_seda_keyword_reference_from else None)
 
 
 class SEDAKeywordReference(generated.SEDAKeywordReference):
@@ -96,7 +166,12 @@
 
 
 class SEDAAppraisalRule(RuleMixIn, generated.SEDAAppraisalRule):
-    pass
+
+    @property
+    def final_action_concept(self):
+        if self.seda_final_action:
+            return self.seda_final_action[0]
+        return None
 
 
 class SEDAClassificationRule(RuleMixIn, generated.SEDAClassificationRule):
@@ -112,4 +187,58 @@
 
 
 class SEDAStorageRule(RuleMixIn, generated.SEDAStorageRule):
+
+    @property
+    def final_action_concept(self):
+        if self.seda_final_action:
+            return self.seda_final_action[0]
+        return None
+
+
+class RuleRuleMixIn(object):
+
+    @property
+    def start_date(self):
+        return self.reverse_seda_start_date[0] if self.reverse_seda_start_date else None
+
+    @property
+    def rule_concept(self):
+        return self.seda_rule[0] if self.seda_rule else None
+
+
+class SEDASeqAccessRuleRule(RuleRuleMixIn, generated.SEDASeqAccessRuleRule):
     pass
+
+
+class SEDASeqAppraisalRuleRule(RuleRuleMixIn, generated.SEDASeqAppraisalRuleRule):
+    pass
+
+
+class SEDASeqClassificationRuleRule(RuleRuleMixIn, generated.SEDASeqClassificationRuleRule):
+    pass
+
+
+class SEDASeqDisseminationRuleRule(RuleRuleMixIn, generated.SEDASeqDisseminationRuleRule):
+    pass
+
+
+class SEDASeqReuseRuleRule(RuleRuleMixIn, generated.SEDASeqReuseRuleRule):
+    pass
+
+
+class SEDASeqStorageRuleRule(RuleRuleMixIn, generated.SEDASeqStorageRuleRule):
+    pass
+
+
+class SEDAFormatId(generated.SEDAFormatId):
+
+    @property
+    def concept(self):
+        return self.seda_format_id_to[0] if self.seda_format_id_to else None
+
+
+class SEDAEncoding(generated.SEDAEncoding):
+
+    @property
+    def concept(self):
+        return self.seda_encoding_to[0] if self.seda_encoding_to else None
--- a/entities/profile_generation.py	Thu Sep 29 18:50:30 2016 +0200
+++ b/entities/profile_generation.py	Thu Sep 29 18:54:30 2016 +0200
@@ -29,6 +29,7 @@
 
 from cubes.seda.xsd import BASE_TYPES, XSDM_MAPPING, JUMP_ELEMENTS
 from cubes.seda.xsd2yams import SKIP_ATTRS
+from cubes.seda.entities import simplified_profile
 
 
 JUMPED_OPTIONAL_ELEMENTS = set(('FileInfo', 'PhysicalDimensions', 'Coverage'))
@@ -693,6 +694,381 @@
         return parent
 
 
+class XSDAttr(dict):
+    """Simple object to define an xsd:attribute element.
+
+    Params:
+
+    * `name`, value for the 'name' attribute of the xsd:attribute
+
+    * `xsd_type`, value for the 'type' attribute of the xsd:attribute
+
+    * `cardinality`, optional cardinality that will be used to compute
+      the 'use' attribute of the xsd:attribute (default to 'prohibited')
+
+    * `value`, optional value for the 'fixed' attribute of the xsd:attribute
+    """
+    def __init__(self, name, xsd_type, cardinality=None, fixed_value=None):
+        assert cardinality in (None, '1', '0..1'), cardinality
+        if cardinality is None:
+            use = 'prohibited'
+        elif cardinality == '1':
+            use = 'required'
+        else:
+            use = 'optional'
+        super(XSDAttr, self).__init__(name=name, type=xsd_type, use=use)
+        if fixed_value is not None:
+            self['fixed'] = text_type(fixed_value)
+
+
+LIST_VERSION_ID_2009 = XSDAttr('listVersionID', 'xsd:token', '1', 'edition 2009')
+LIST_VERSION_ID_2011 = XSDAttr('listVersionID', 'xsd:token', '1', 'edition 2011')
+
+
+class SEDA1XSDExport(SEDA2XSDExport):
+    """Adapter to build an XSD representation of a simplified SEDA profile, using SEDA 1.0
+    specification.
+
+    The SEDA2XSDExport implementation may be driven by the SEDA 2.0 XSD model because it's used as
+    the basis for the Yams model generation. We can't do the same thing with lower version of SEDA,
+    hence the limitation to simplified profile, and a direct implementation of the export.
+    """
+    __regid__ = 'SEDA-1.0.xsd'
+    __select__ = SEDA2XSDExport.__select__ & simplified_profile()
+    extension = 'xsd'
+
+    namespaces = {
+        None: 'fr:gouv:culture:archivesdefrance:seda:v1.0',
+        'xsd': 'http://www.w3.org/2001/XMLSchema',
+        'qdt': 'fr:gouv:culture:archivesdefrance:seda:v1.0:QualifiedDataType:1',
+        'udt': 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:10',
+        'clmDAFFileTypeCode': 'urn:un:unece:uncefact:codelist:draft:DAF:fileTypeCode:2009-08-18',
+        'clmIANACharacterSetCode':
+        'urn:un:unece:uncefact:codelist:standard:IANA:CharacterSetCode:2007-05-14',
+        'clmIANAMIMEMediaType':
+        'urn:un:unece:uncefact:codelist:standard:IANA:MIMEMediaType:2008-11-12',
+        'clm60133': 'urn:un:unece:uncefact:codelist:standard:6:0133:40106',
+    }
+    root_attributes = {
+        'targetNamespace': 'fr:gouv:culture:archivesdefrance:seda:v1.0',
+        'attributeFormDefault': 'unqualified',
+        'elementFormDefault': 'qualified',
+        'version': '1.0',
+    }
+
+    concepts_language = 'seda-1'
+
+    def xsd_element(self, parent, name, xsd_type=None, fixed_value=None, cardinality='1',
+                    documentation=None, xsd_attributes=()):
+        attributes = {'name': name}
+        if fixed_value is not None:
+            attributes['fixed'] = text_type(fixed_value)
+        if xsd_type is not None and not xsd_attributes:
+            attributes['type'] = xsd_type
+        assert cardinality in ('0..1', '0..n', '1', '1..n')
+        if cardinality != '1':
+            if cardinality[0] == '0':
+                attributes['minOccurs'] = '0'
+            if cardinality[-1] == 'n':
+                attributes['maxOccurs'] = 'unbounded'
+        if documentation:
+            attributes['documentation'] = documentation
+        element = self.element('xsd:element', parent, attributes)
+        children_parent = None
+        if xsd_type is None:
+            attributes_parent = self.element('xsd:complexType', element)
+            children_parent = self.element('xsd:sequence', attributes_parent)
+        elif xsd_attributes:
+            ct = self.element('xsd:complexType', element)
+            scontent = self.element('xsd:simpleContent', ct)
+            attributes_parent = self.element('xsd:extension', scontent, {'base': xsd_type})
+        for xsd_attr in xsd_attributes:
+            self.element('xsd:attribute', attributes_parent, xsd_attr)
+        return children_parent
+
+    # business visit methods #######################################################################
+
+    def dump_etree(self):
+        """Return an XSD etree for the adapted SEDA profile."""
+        self.defined_content_types = set()
+        root = self.element('xsd:schema', attributes=self.root_attributes)
+        # self.element('xsd:import', parent=root,
+        #              attributes={'namespace': 'http://www.w3.org/XML/1998/namespace',
+        #                          'schemaLocation': 'http://www.w3.org/2001/xml.xsd'})
+        self.xsd_transfer(root, self.entity)
+        return root
+
+    def xsd_transfer(self, parent, archive_transfer):
+        """Append XSD elements for the archive transfer to the given parent node."""
+        transfer_node = self.xsd_element(parent, 'ArchiveTransfer',
+                                         documentation=archive_transfer.title,
+                                         xsd_attributes=[XSDAttr('Id', 'xsd:ID')])
+        for comment in archive_transfer.comments:
+            self.xsd_element(transfer_node, 'Comment', 'udt:TextType',
+                             fixed_value=comment.comment,
+                             cardinality=comment.user_cardinality,
+                             documentation=comment.user_annotation,
+                             xsd_attributes=[XSDAttr('languageID', 'xsd:language')])
+        self.xsd_element(transfer_node, 'Date', 'udt:DateTimeType')
+        self.xsd_element(transfer_node, 'TransferIdentifier', 'qdt:ArchivesIDType',
+                         xsd_attributes=self.xsd_attributes_scheme())
+        for archive_unit in archive_transfer.archive_units:
+            self.xsd_archive(transfer_node, archive_unit)
+
+    def xsd_archive(self, parent, archive_unit):
+        """Append XSD elements for an archive to the given parent node."""
+        archive_node = self.xsd_element(parent, 'Archive',
+                                        cardinality=archive_unit.user_cardinality,
+                                        documentation=archive_unit.user_annotation,
+                                        xsd_attributes=[XSDAttr('Id', 'xsd:ID')])
+        # hard-coded description's language XXX fine, content language may be specified
+        self.xsd_element(archive_node, 'DescriptionLanguage', 'qdt:CodeLanguageType',
+                         fixed_value='fra',
+                         xsd_attributes=[LIST_VERSION_ID_2011])
+        name_entity = self.archive_unit_name(archive_unit)
+        self.xsd_element(archive_node, 'Name', 'udt:TextType',
+                         fixed_value=name_entity.title,
+                         documentation=name_entity.user_annotation,
+                         xsd_attributes=[XSDAttr('languageID', 'xsd:language')])
+        appraisal_rule_entity = self.archive_unit_appraisal_rule(archive_unit)
+        if appraisal_rule_entity:
+            self.xsd_appraisal_rule(archive_node, appraisal_rule_entity)
+        # XXX not optional in seda < 2
+        access_rule_entity = self.archive_unit_access_rule(archive_unit)
+        self.xsd_access_rule(archive_node, access_rule_entity)
+        content_entity = self.archive_unit_content(archive_unit)
+        self.xsd_content_description(archive_node, content_entity)
+        self.xsd_children(archive_node, archive_unit)
+
+    def archive_unit_name(self, archive_unit):
+        seq = archive_unit.first_level_choice.content_sequence
+        return seq.contents[0].title
+
+    def archive_unit_appraisal_rule(self, archive_unit):
+        seq = archive_unit.first_level_choice.content_sequence
+        return seq.reverse_seda_appraisal_rule[0] if seq.reverse_seda_appraisal_rule else None
+
+    def xsd_appraisal_rule(self, parent, appraisal_rule):
+        # XXX cardinality 1 on rule, not multiple + element name : 'Appraisal' ou 'AppraisalRule'
+        # (cf http://www.archivesdefrance.culture.gouv.fr/seda/api/index.html)
+        ar_node = self.xsd_element(parent, 'Appraisal',
+                                   cardinality=appraisal_rule.user_cardinality,
+                                   documentation=appraisal_rule.user_annotation,
+                                   xsd_attributes=[XSDAttr('Id', 'xsd:ID')])
+        ar_code = appraisal_rule.final_action_concept
+        ar_code_value = _concept_value(ar_code, self.concepts_language)
+        self.xsd_element(ar_node, 'Code', 'qdt:CodeAppraisalType',
+                         fixed_value=ar_code_value,
+                         xsd_attributes=[LIST_VERSION_ID_2009])
+
+        rule = appraisal_rule.rules[0] if appraisal_rule.rules else None
+        value = _concept_value(rule.rule_concept, self.concepts_language) if rule else None
+        self.xsd_element(ar_node, 'Duration', 'qdt:ArchivesDurationType',
+                         fixed_value=value,
+                         documentation=rule.user_annotation if rule else None)
+        self.xsd_element(ar_node, 'StartDate', 'udt:DateType')
+
+    def archive_unit_access_rule(self, archive_unit):
+        seq = archive_unit.first_level_choice.content_sequence
+        return seq.reverse_seda_access_rule[0] if seq.reverse_seda_access_rule else None
+
+    access_restriction_tag_name = 'AccessRestrictionRule'
+
+    def xsd_access_rule(self, parent, access_rule):
+        """Append XSD elements for an access restriction to the given parent node."""
+        ar_node = self.xsd_element(parent, self.access_restriction_tag_name,
+                                   cardinality=access_rule.user_cardinality,
+                                   documentation=access_rule.user_annotation,
+                                   xsd_attributes=[XSDAttr('Id', 'xsd:ID')])
+        # XXX cardinality 1
+        rule = access_rule.rules[0] if access_rule.rules else None
+        value = _concept_value(rule.rule_concept, self.concepts_language) if rule else None
+        self.xsd_element(ar_node, 'Code', 'qdt:CodeAccessRestrictionType',
+                         fixed_value=value,
+                         documentation=rule.user_annotation if rule else None,
+                         xsd_attributes=[LIST_VERSION_ID_2009])
+        self.xsd_element(ar_node, 'StartDate', 'udt:DateType')
+
+    def archive_unit_content(self, archive_unit):
+        seq = archive_unit.first_level_choice.content_sequence
+        return seq.contents[0]
+
+    def xsd_content_description(self, parent, content):
+        """Append XSD elements for a description content to the given parent node"""
+        cd_node = self.xsd_element(parent, 'ContentDescription',
+                                   # XXX should always be 1
+                                   # cardinality=content.user_cardinality,
+                                   documentation=content.user_annotation,
+                                   xsd_attributes=[XSDAttr('Id', 'xsd:ID')])
+        self.xsd_description_level(cd_node, content.description_level_concept)
+        for seda2_name, seda1_name in (('start', 'oldest'), ('end', 'latest')):
+            date_entity = getattr(content, '%s_date' % seda2_name)
+            if date_entity:
+                self.xsd_element(cd_node, '%sDate' % seda1_name.capitalize(), 'udt:DateType',
+                                 cardinality=date_entity.user_cardinality,
+                                 documentation=date_entity.user_annotation)
+        if content.description:
+            self.xsd_description(cd_node, content.description)
+        for keyword in content.keywords:
+            self.xsd_keyword(cd_node, keyword)
+
+    def xsd_description_level(self, parent, concept):
+        """Append XSD elements for a description level to the given parent node"""
+        value = _concept_value(concept, self.concepts_language)
+        self.xsd_element(parent, 'DescriptionLevel', 'qdt:CodeDescriptionLevelType',
+                         fixed_value=value,
+                         xsd_attributes=[LIST_VERSION_ID_2009])
+
+    def xsd_description(self, parent, description):
+        """Append XSD elements for a description to the given parent node"""
+        self.xsd_element(parent, 'Description', 'udt:TextType',
+                         cardinality=description.user_cardinality,
+                         documentation=description.user_annotation,
+                         fixed_value=description.description,
+                         xsd_attributes=[XSDAttr('languageID', 'xsd:language')])
+
+    # extracted from xsd_keyword to allow parametrization for SEDA 1.0 vs 0.2 generation
+    kw_tag_name = 'Keyword'
+    kw_content_tag_type = 'qdt:KeywordContentType'
+    kw_content_tag_attributes = [XSDAttr('role', 'xsd:token'),
+                                 XSDAttr('languageID', 'xsd:language')]
+
+    def xsd_keyword(self, parent, keyword):
+        """Append XSD elements for a keyword to the given parent node"""
+        kw_node = self.xsd_element(parent, self.kw_tag_name,
+                                   cardinality=keyword.user_cardinality,
+                                   documentation=keyword.user_annotation,
+                                   xsd_attributes=[XSDAttr('Id', 'xsd:ID')])
+        content = keyword.content_string
+        url = None
+        if keyword.reference:
+            concept = keyword.reference.concept
+            if concept:
+                url = self.cwuri_url(concept)
+                if content is None:
+                    content = concept.label()
+        self.xsd_element(kw_node, 'KeywordContent', self.kw_content_tag_type,
+                         fixed_value=content,
+                         xsd_attributes=self.kw_content_tag_attributes)
+        scheme = keyword.reference.scheme if keyword.reference else None
+        self.xsd_element(kw_node, 'KeywordReference', 'qdt:ArchivesIDType',
+                         fixed_value=url,
+                         xsd_attributes=self.xsd_attributes_scheme(scheme))
+        # XXX keyword type
+
+    def xsd_children(self, parent, entity):
+        """Iter on archive/archive object children, which may be either
+        archive objects or documents, and append XSD elements for them to the given parent node.
+        """
+        for au_or_bdo in entity.cw_adapt_to('ITreeBase').iterchildren():
+            if au_or_bdo.cw_etype == 'SEDABinaryDataObject':
+                self.xsd_document(parent, au_or_bdo)
+            else:
+                assert au_or_bdo.cw_etype == 'SEDAArchiveUnit'
+                self.xsd_archive_object(parent, au_or_bdo)
+
+    archive_object_tag_name = 'ArchiveObject'
+
+    def xsd_archive_object(self, parent, archive_unit):
+        """Append XSD elements for the archive object to the given parent node."""
+        ao_node = self.xsd_element(parent, self.archive_object_tag_name,
+                                   cardinality=archive_unit.user_cardinality,
+                                   documentation=archive_unit.user_annotation,
+                                   xsd_attributes=[XSDAttr('Id', 'xsd:ID')])
+        content_entity = self.archive_unit_content(archive_unit)
+        self.xsd_element(ao_node, 'Name', 'udt:TextType',
+                         fixed_value=content_entity.title.title,
+                         documentation=content_entity.title.user_annotation,
+                         xsd_attributes=[XSDAttr('languageID', 'xsd:language')])
+        appraisal_rule_entity = self.archive_unit_appraisal_rule(archive_unit)
+        if appraisal_rule_entity:
+            self.xsd_appraisal_rule(ao_node, appraisal_rule_entity)
+        access_rule_entity = self.archive_unit_access_rule(archive_unit)
+        if access_rule_entity:
+            self.xsd_access_rule(ao_node, access_rule_entity)
+        self.xsd_content_description(ao_node, content_entity)
+        self.xsd_children(ao_node, archive_unit)
+
+        return ao_node
+
+    def xsd_document(self, parent, data_object):
+        """Append XSD elements for the document to the given parent node."""
+        document_node = self.xsd_element(parent, 'Document',
+                                         cardinality=data_object.user_cardinality,
+                                         documentation=data_object.user_annotation,
+                                         xsd_attributes=[XSDAttr('Id', 'xsd:ID')])
+
+        def safe_cardinality(entity):
+            if entity is None:
+                return None
+            return entity.user_cardinality
+
+        def safe_concept_value(entity):
+            if entity is None:
+                return None
+            return _concept_value(entity.concept, self.concepts_language)
+
+        format_id = data_object.format_id
+        encoding = data_object.encoding
+        self.xsd_element(document_node, 'Attachment', 'qdt:ArchivesBinaryObjectType',
+                         xsd_attributes=[
+                             XSDAttr('format', 'clmDAFFileTypeCode:FileTypeCodeType',
+                                     cardinality=safe_cardinality(format_id),
+                                     fixed_value=safe_concept_value(format_id)),
+                             XSDAttr('encodingCode',
+                                     'clm60133:CharacterSetEncodingCodeContentType',
+                                     cardinality=safe_cardinality(encoding),
+                                     fixed_value=safe_concept_value(encoding)),
+                             # hard-coded attributes
+                             XSDAttr('characterSetCode',
+                                     'clmIANACharacterSetCode:CharacterSetCodeContentType'),
+                             XSDAttr('mimeCode', 'clmIANAMIMEMediaType:MIMEMediaTypeContentType'),
+                             XSDAttr('uri', 'xsd:anyURI'),
+                             XSDAttr('filename', 'xsd:string'),
+                         ])
+        # XXX seda 2 equivalent?
+        # self.xsd_element(document_node, 'Type', 'qdt:CodeDocumentType',
+        #                  fixed_value=_concept_value(document.document_type_code,
+        #                                       'seda_document_type_code_value'),
+        #                  xsd_attributes=[LIST_VERSION_ID_2009])
+        # if document.description:
+        #     self.xsd_description(document_node, document.description)
+
+    @classmethod
+    def xsd_attributes_scheme(cls, scheme=None):
+        """Return a list of :class:`XSDAttr` for a scheme definition, with some proper values
+        specified if a scheme is given.
+        """
+        attributes = [
+            XSDAttr('schemeID', 'xsd:token'),
+            XSDAttr('schemeName', 'xsd:string'),
+            XSDAttr('schemeAgencyName', 'xsd:string'),
+            XSDAttr('schemeVersionID', 'xsd:token'),
+            XSDAttr('schemeDataURI', 'xsd:anyURI'),
+            XSDAttr('schemeURI', 'xsd:anyURI'),
+        ]
+        if scheme is not None:
+            # schemeURI
+            attributes[-1]['fixed'] = cls.cwuri_url(scheme)
+            attributes[-1]['use'] = 'required'
+            # # schemeID XXX move to saem
+            # attributes[0]['fixed'] = scheme.ark
+            # attributes[0]['use'] = 'required'
+            # schemeName
+            if scheme.title:
+                attributes[1]['fixed'] = scheme.title
+                attributes[1]['use'] = 'required'
+        return attributes
+
+    @staticmethod
+    def cwuri_url(entity):
+        """Return "public" URI for the given entity.
+
+        In a staticmethod to ease overriding in subclasses (eg saem).
+        """
+        return entity.cwuri
+
+
 def xsd_element_cardinality(occ, card_entity):
     """Return XSD element cardinality for the given pyxst Occurence. Cardinality may be overriden by
     the data model's user_cardinality value.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/seda_1_export.xsd	Thu Sep 29 18:54:30 2016 +0200
@@ -0,0 +1,285 @@
+<?xml version='1.0' encoding='utf-8' standalone='no'?>
+<xsd:schema attributeFormDefault="unqualified" elementFormDefault="qualified" targetNamespace="fr:gouv:culture:archivesdefrance:seda:v1.0" version="1.0" xmlns:clm60133="urn:un:unece:uncefact:codelist:standard:6:0133:40106" xmlns:clmDAFFileTypeCode="urn:un:unece:uncefact:codelist:draft:DAF:fileTypeCode:2009-08-18" xmlns:clmIANACharacterSetCode="urn:un:unece:uncefact:codelist:standard:IANA:CharacterSetCode:2007-05-14" xmlns:clmIANAMIMEMediaType="urn:un:unece:uncefact:codelist:standard:IANA:MIMEMediaType:2008-11-12" xmlns:qdt="fr:gouv:culture:archivesdefrance:seda:v1.0:QualifiedDataType:1" xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:10" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="fr:gouv:culture:archivesdefrance:seda:v1.0">
+  <xsd:element name="ArchiveTransfer">
+    <xsd:annotation>
+      <xsd:documentation>my profile title &amp;&amp;</xsd:documentation>
+    </xsd:annotation>
+    <xsd:complexType>
+      <xsd:sequence>
+        <xsd:element fixed="my profile description &amp;&amp;" name="Comment">
+          <xsd:complexType>
+            <xsd:simpleContent>
+              <xsd:extension base="udt:TextType">
+                <xsd:attribute name="languageID" type="xsd:language" use="prohibited"/>
+              </xsd:extension>
+            </xsd:simpleContent>
+          </xsd:complexType>
+        </xsd:element>
+        <xsd:element name="Date" type="udt:DateTimeType"/>
+        <xsd:element name="TransferIdentifier">
+          <xsd:complexType>
+            <xsd:simpleContent>
+              <xsd:extension base="qdt:ArchivesIDType">
+                <xsd:attribute name="schemeID" type="xsd:token" use="prohibited"/>
+                <xsd:attribute name="schemeName" type="xsd:string" use="prohibited"/>
+                <xsd:attribute name="schemeAgencyName" type="xsd:string" use="prohibited"/>
+                <xsd:attribute name="schemeVersionID" type="xsd:token" use="prohibited"/>
+                <xsd:attribute name="schemeDataURI" type="xsd:anyURI" use="prohibited"/>
+                <xsd:attribute name="schemeURI" type="xsd:anyURI" use="prohibited"/>
+              </xsd:extension>
+            </xsd:simpleContent>
+          </xsd:complexType>
+        </xsd:element>
+        <xsd:element maxOccurs="unbounded" name="Archive">
+          <xsd:complexType>
+            <xsd:sequence>
+              <xsd:element fixed="fra" name="DescriptionLanguage">
+                <xsd:complexType>
+                  <xsd:simpleContent>
+                    <xsd:extension base="qdt:CodeLanguageType">
+                      <xsd:attribute fixed="edition 2011" name="listVersionID" type="xsd:token" use="required"/>
+                    </xsd:extension>
+                  </xsd:simpleContent>
+                </xsd:complexType>
+              </xsd:element>
+              <xsd:element name="Name">
+                <xsd:complexType>
+                  <xsd:simpleContent>
+                    <xsd:extension base="udt:TextType">
+                      <xsd:attribute name="languageID" type="xsd:language" use="prohibited"/>
+                    </xsd:extension>
+                  </xsd:simpleContent>
+                </xsd:complexType>
+              </xsd:element>
+              <xsd:element minOccurs="0" name="Appraisal">
+                <xsd:annotation>
+                  <xsd:documentation>detruire le document</xsd:documentation>
+                </xsd:annotation>
+                <xsd:complexType>
+                  <xsd:sequence>
+                    <xsd:element fixed="detruire" name="Code">
+                      <xsd:complexType>
+                        <xsd:simpleContent>
+                          <xsd:extension base="qdt:CodeAppraisalType">
+                            <xsd:attribute fixed="edition 2009" name="listVersionID" type="xsd:token" use="required"/>
+                          </xsd:extension>
+                        </xsd:simpleContent>
+                      </xsd:complexType>
+                    </xsd:element>
+                    <xsd:element fixed="P10Y" name="Duration" type="qdt:ArchivesDurationType">
+                      <xsd:annotation>
+                        <xsd:documentation>C'est dans 10ans je m'en irai</xsd:documentation>
+                      </xsd:annotation>
+                    </xsd:element>
+                    <xsd:element name="StartDate" type="udt:DateType"/>
+                  </xsd:sequence>
+                  <xsd:attribute name="Id" type="xsd:ID" use="prohibited"/>
+                </xsd:complexType>
+              </xsd:element>
+              <xsd:element name="AccessRestrictionRule">
+                <xsd:complexType>
+                  <xsd:sequence>
+                    <xsd:element name="Code">
+                      <xsd:complexType>
+                        <xsd:simpleContent>
+                          <xsd:extension base="qdt:CodeAccessRestrictionType">
+                            <xsd:attribute fixed="edition 2009" name="listVersionID" type="xsd:token" use="required"/>
+                          </xsd:extension>
+                        </xsd:simpleContent>
+                      </xsd:complexType>
+                    </xsd:element>
+                    <xsd:element name="StartDate" type="udt:DateType"/>
+                  </xsd:sequence>
+                  <xsd:attribute name="Id" type="xsd:ID" use="prohibited"/>
+                </xsd:complexType>
+              </xsd:element>
+              <xsd:element name="ContentDescription">
+                <xsd:complexType>
+                  <xsd:sequence>
+                    <xsd:element fixed="file" name="DescriptionLevel">
+                      <xsd:complexType>
+                        <xsd:simpleContent>
+                          <xsd:extension base="qdt:CodeDescriptionLevelType">
+                            <xsd:attribute fixed="edition 2009" name="listVersionID" type="xsd:token" use="required"/>
+                          </xsd:extension>
+                        </xsd:simpleContent>
+                      </xsd:complexType>
+                    </xsd:element>
+                    <xsd:element minOccurs="0" name="OldestDate" type="udt:DateType"/>
+                    <xsd:element minOccurs="0" name="LatestDate" type="udt:DateType"/>
+                    <xsd:element minOccurs="0" name="Description">
+                      <xsd:complexType>
+                        <xsd:simpleContent>
+                          <xsd:extension base="udt:TextType">
+                            <xsd:attribute name="languageID" type="xsd:language" use="prohibited"/>
+                          </xsd:extension>
+                        </xsd:simpleContent>
+                      </xsd:complexType>
+                    </xsd:element>
+                    <xsd:element maxOccurs="unbounded" minOccurs="0" name="Keyword">
+                      <xsd:complexType>
+                        <xsd:sequence>
+                          <xsd:element fixed="file" name="KeywordContent">
+                            <xsd:complexType>
+                              <xsd:simpleContent>
+                                <xsd:extension base="qdt:KeywordContentType">
+                                  <xsd:attribute name="role" type="xsd:token" use="prohibited"/>
+                                  <xsd:attribute name="languageID" type="xsd:language" use="prohibited"/>
+                                </xsd:extension>
+                              </xsd:simpleContent>
+                            </xsd:complexType>
+                          </xsd:element>
+                          <xsd:element fixed="%(concept-uri)s" name="KeywordReference">
+                            <xsd:complexType>
+                              <xsd:simpleContent>
+                                <xsd:extension base="qdt:ArchivesIDType">
+                                  <xsd:attribute name="schemeID" type="xsd:token" use="prohibited"/>
+                                  <xsd:attribute fixed="seda_description_level/None vocabulary" name="schemeName" type="xsd:string" use="required"/>
+                                  <xsd:attribute name="schemeAgencyName" type="xsd:string" use="prohibited"/>
+                                  <xsd:attribute name="schemeVersionID" type="xsd:token" use="prohibited"/>
+                                  <xsd:attribute name="schemeDataURI" type="xsd:anyURI" use="prohibited"/>
+                                  <xsd:attribute fixed="%(scheme-uri)s" name="schemeURI" type="xsd:anyURI" use="required"/>
+                                </xsd:extension>
+                              </xsd:simpleContent>
+                            </xsd:complexType>
+                          </xsd:element>
+                        </xsd:sequence>
+                        <xsd:attribute name="Id" type="xsd:ID" use="prohibited"/>
+                      </xsd:complexType>
+                    </xsd:element>
+                  </xsd:sequence>
+                  <xsd:attribute name="Id" type="xsd:ID" use="prohibited"/>
+                </xsd:complexType>
+              </xsd:element>
+              <xsd:element maxOccurs="unbounded" name="ArchiveObject">
+                <xsd:complexType>
+                  <xsd:sequence>
+                    <xsd:element name="Name">
+                      <xsd:complexType>
+                        <xsd:simpleContent>
+                          <xsd:extension base="udt:TextType">
+                            <xsd:attribute name="languageID" type="xsd:language" use="prohibited"/>
+                          </xsd:extension>
+                        </xsd:simpleContent>
+                      </xsd:complexType>
+                    </xsd:element>
+                    <xsd:element minOccurs="0" name="Appraisal">
+                      <xsd:complexType>
+                        <xsd:sequence>
+                          <xsd:element name="Code">
+                            <xsd:complexType>
+                              <xsd:simpleContent>
+                                <xsd:extension base="qdt:CodeAppraisalType">
+                                  <xsd:attribute fixed="edition 2009" name="listVersionID" type="xsd:token" use="required"/>
+                                </xsd:extension>
+                              </xsd:simpleContent>
+                            </xsd:complexType>
+                          </xsd:element>
+                          <xsd:element name="Duration" type="qdt:ArchivesDurationType"/>
+                          <xsd:element name="StartDate" type="udt:DateType"/>
+                        </xsd:sequence>
+                        <xsd:attribute name="Id" type="xsd:ID" use="prohibited"/>
+                      </xsd:complexType>
+                    </xsd:element>
+                    <xsd:element name="AccessRestrictionRule">
+                      <xsd:annotation>
+                        <xsd:documentation>restrict</xsd:documentation>
+                      </xsd:annotation>
+                      <xsd:complexType>
+                        <xsd:sequence>
+                          <xsd:element fixed="AR038" name="Code">
+                            <xsd:complexType>
+                              <xsd:simpleContent>
+                                <xsd:extension base="qdt:CodeAccessRestrictionType">
+                                  <xsd:attribute fixed="edition 2009" name="listVersionID" type="xsd:token" use="required"/>
+                                </xsd:extension>
+                              </xsd:simpleContent>
+                            </xsd:complexType>
+                          </xsd:element>
+                          <xsd:element name="StartDate" type="udt:DateType"/>
+                        </xsd:sequence>
+                        <xsd:attribute name="Id" type="xsd:ID" use="prohibited"/>
+                      </xsd:complexType>
+                    </xsd:element>
+                    <xsd:element name="ContentDescription">
+                      <xsd:complexType>
+                        <xsd:sequence>
+                          <xsd:element name="DescriptionLevel">
+                            <xsd:complexType>
+                              <xsd:simpleContent>
+                                <xsd:extension base="qdt:CodeDescriptionLevelType">
+                                  <xsd:attribute fixed="edition 2009" name="listVersionID" type="xsd:token" use="required"/>
+                                </xsd:extension>
+                              </xsd:simpleContent>
+                            </xsd:complexType>
+                          </xsd:element>
+                        </xsd:sequence>
+                        <xsd:attribute name="Id" type="xsd:ID" use="prohibited"/>
+                      </xsd:complexType>
+                    </xsd:element>
+                  </xsd:sequence>
+                  <xsd:attribute name="Id" type="xsd:ID" use="prohibited"/>
+                </xsd:complexType>
+              </xsd:element>
+              <xsd:element maxOccurs="unbounded" name="ArchiveObject">
+                <xsd:complexType>
+                  <xsd:sequence>
+                    <xsd:element name="Name">
+                      <xsd:complexType>
+                        <xsd:simpleContent>
+                          <xsd:extension base="udt:TextType">
+                            <xsd:attribute name="languageID" type="xsd:language" use="prohibited"/>
+                          </xsd:extension>
+                        </xsd:simpleContent>
+                      </xsd:complexType>
+                    </xsd:element>
+                    <xsd:element name="ContentDescription">
+                      <xsd:complexType>
+                        <xsd:sequence>
+                          <xsd:element name="DescriptionLevel">
+                            <xsd:complexType>
+                              <xsd:simpleContent>
+                                <xsd:extension base="qdt:CodeDescriptionLevelType">
+                                  <xsd:attribute fixed="edition 2009" name="listVersionID" type="xsd:token" use="required"/>
+                                </xsd:extension>
+                              </xsd:simpleContent>
+                            </xsd:complexType>
+                          </xsd:element>
+                        </xsd:sequence>
+                        <xsd:attribute name="Id" type="xsd:ID" use="prohibited"/>
+                      </xsd:complexType>
+                    </xsd:element>
+                  </xsd:sequence>
+                  <xsd:attribute name="Id" type="xsd:ID" use="prohibited"/>
+                </xsd:complexType>
+              </xsd:element>
+              <xsd:element maxOccurs="unbounded" minOccurs="0" name="Document">
+                <xsd:complexType>
+                  <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 fixed="6" name="encodingCode" type="clm60133:CharacterSetEncodingCodeContentType" use="required"/>
+                            <xsd:attribute name="characterSetCode" type="clmIANACharacterSetCode:CharacterSetCodeContentType" use="prohibited"/>
+                            <xsd:attribute name="mimeCode" type="clmIANAMIMEMediaType:MIMEMediaTypeContentType" use="prohibited"/>
+                            <xsd:attribute name="uri" type="xsd:anyURI" use="prohibited"/>
+                            <xsd:attribute name="filename" type="xsd:string" use="prohibited"/>
+                          </xsd:extension>
+                        </xsd:simpleContent>
+                      </xsd:complexType>
+                    </xsd:element>
+                  </xsd:sequence>
+                  <xsd:attribute name="Id" type="xsd:ID" use="prohibited"/>
+                </xsd:complexType>
+              </xsd:element>
+            </xsd:sequence>
+            <xsd:attribute name="Id" type="xsd:ID" use="prohibited"/>
+          </xsd:complexType>
+        </xsd:element>
+      </xsd:sequence>
+      <xsd:attribute name="Id" type="xsd:ID" use="prohibited"/>
+    </xsd:complexType>
+  </xsd:element>
+</xsd:schema>
--- a/test/test_profile_generation.py	Thu Sep 29 18:50:30 2016 +0200
+++ b/test/test_profile_generation.py	Thu Sep 29 18:54:30 2016 +0200
@@ -15,9 +15,13 @@
 # with this program. If not, see <http://www.gnu.org/licenses/>.
 """cubicweb-seda unit tests for XSD profile generation"""
 
+from doctest import Example
 from itertools import chain, izip, repeat
 
+from six import binary_type
+
 from lxml import etree
+from lxml.doctestcompare import LXMLOutputChecker
 
 from logilab.common.decorators import cached
 
@@ -26,7 +30,7 @@
 from cubes.seda.xsd2yams import XSDMMapping
 from cubes.seda.entities.profile_generation import _path_target_values
 
-from testutils import create_archive_unit, scheme_for_rtype
+from testutils import create_archive_unit, scheme_for_rtype, scheme_for_type
 
 
 class XmlTestMixin(object):
@@ -83,6 +87,13 @@
         schema = self.schema(self.datapath(self.schema_file))
         schema.assert_(root)
 
+    def assertXmlEqual(self, actual, expected):
+        """Check that both XML strings represent the same XML tree."""
+        checker = LXMLOutputChecker()
+        if not checker.check_output(expected, actual, 0):
+            message = checker.output_difference(Example("", expected), actual, 0)
+            self.fail(message)
+
 
 class XMLSchemaTestMixin(XmlTestMixin):
     """Mixin extending XmlTestMixin with implementation of some assert methods for XMLSchema."""
@@ -655,6 +666,142 @@
         self.assertEqual(set(elmt.attrib['type'] for elmt in ddt), set(['date', 'dateTime']))
 
 
+class SEDA1XSDExportTC(XMLSchemaTestMixin, CubicWebTC):
+    def setup_database(self):
+        with self.admin_access.client_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_description_level', None, u'file'),
+                    ('seda_rule', 'SEDASeqAppraisalRuleRule', u'P10Y'),
+                    ('seda_rule', 'SEDASeqAccessRuleRule', u'AR038'),
+                    ('seda_final_action', 'SEDAAppraisalRule', u'detruire'),
+                    # XXX (u'seda_document_type_code', u'CDO', None),
+            ]:
+                scheme = scheme_for_type(cnx, rtype, etype, value)
+                concepts[value] = scheme.reverse_in_scheme[0]
+
+            transfer = create('SEDAArchiveTransfer', title=u'my profile title &&',
+                              simplified_profile=True)
+
+            create('SEDAComment',
+                   user_cardinality=u'1',
+                   comment=u'my profile description &&',
+                   seda_comment=transfer)
+
+            _, _, unit_alt_seq = create_archive_unit(transfer, id=u'au1',
+                                                     user_cardinality=u'1..n')
+            create('SEDAAccessRule',  # XXX mandatory for seda 1.0
+                   user_cardinality=u'1',
+                   seda_access_rule=unit_alt_seq,
+                   seda_seq_access_rule_rule=create('SEDASeqAccessRuleRule'))
+
+            content = create('SEDAContent',
+                             user_cardinality=u'1',
+                             seda_content=unit_alt_seq,
+                             seda_description_level=concepts['file'],
+                             reverse_seda_start_date=create('SEDAStartDate'),
+                             reverse_seda_end_date=create('SEDAEndDate'),
+                             # XXX, value=date(2015, 2, 24)),
+                             reverse_seda_description=create('SEDADescription'))
+            cnx.create_entity('SEDATitle', seda_title=content)
+
+            kw = create('SEDAKeyword',
+                        user_cardinality=u'0..n',
+                        seda_keyword=content,
+                        seda_keyword_content=create('SEDAKeywordContent'))  # XXX
+            create('SEDAKeywordReference',
+                   seda_keyword_reference_from=kw,
+                   seda_keyword_reference_to=concepts['file'],
+                   seda_keyword_reference_to_scheme=concepts['file'].scheme)
+
+            appraisal_rule_rule = create('SEDASeqAppraisalRuleRule',
+                                         seda_rule=concepts['P10Y'],
+                                         user_annotation=u"C'est dans 10ans je m'en irai")
+            create('SEDAAppraisalRule',
+                   seda_appraisal_rule=unit_alt_seq,
+                   seda_final_action=concepts['detruire'],
+                   seda_seq_appraisal_rule_rule=appraisal_rule_rule,
+                   user_annotation=u'detruire le document')
+
+            # Add sub archive unit
+            _, _, subunit_alt_seq = create_archive_unit(unit_alt_seq, id=u'au2',
+                                                        user_cardinality=u'1..n')
+            content = create('SEDAContent', seda_content=subunit_alt_seq)
+            cnx.create_entity('SEDATitle', seda_title=content)
+
+            create('SEDAAppraisalRule',
+                   seda_appraisal_rule=subunit_alt_seq)
+
+            create('SEDAAccessRule',
+                   user_cardinality=u'1',
+                   user_annotation=u'restrict',
+                   seda_access_rule=subunit_alt_seq,
+                   seda_seq_access_rule_rule=create('SEDASeqAccessRuleRule',
+                                                    seda_rule=concepts['AR038']))
+
+            # Add minimal document to first level archive
+            ref = create('SEDADataObjectReference', seda_data_object_reference=unit_alt_seq)
+            bdo_alt = cnx.create_entity('SEDAAltBinaryDataObjectAttachment')
+            cnx.create_entity('SEDAAttachment', seda_attachment=bdo_alt)
+            bdo = create('SEDABinaryDataObject', id=u'bdo1',
+                         user_cardinality=u'0..n',
+                         # seda_description=create('SEDADescription'),
+                         seda_binary_data_object=transfer,
+                         seda_alt_binary_data_object_attachment=bdo_alt,
+                         reverse_seda_data_object_reference_id=ref)
+
+            create('SEDAFormatId',
+                   user_cardinality=u'1',
+                   seda_format_id_from=bdo,
+                   seda_format_id_to=concepts['fmt/123'])
+            create('SEDAEncoding',
+                   user_cardinality=u'1',
+                   seda_encoding_from=bdo,
+                   seda_encoding_to=concepts['6'])
+            # document_type_code_value = testutils.concept(cnx, 'CDO')
+            # document_type_code = create(
+            #     'SEDADocumentTypeCode', seda_document_type_code_value=document_type_code_value)
+
+            # Add another sub archive unit
+            _, _, subunit2_alt_seq = create_archive_unit(unit_alt_seq, id=u'au3',
+                                                         user_cardinality=u'1..n')
+            content = create('SEDAContent', seda_content=subunit2_alt_seq)
+            cnx.create_entity('SEDATitle', seda_title=content)
+
+            cnx.commit()
+
+        self.transfer_eid = transfer.eid
+        self.file_concept_eid = concepts['file'].eid
+
+    def test_seda_1_0(self):
+        self._test_profile_xsd('SEDA-1.0.xsd', 'seda_1_export.xsd')
+
+    def _test_profile_xsd(self, adapter_id, expected_file):
+        with self.admin_access.client_cnx() as cnx:
+            transfer = cnx.entity_from_eid(self.transfer_eid)
+            adapter = transfer.cw_adapt_to(adapter_id)
+            generated_xsd = adapter.dump()
+            open('/tmp/x.xml', 'w').write(generated_xsd)
+            xml = etree.fromstring(generated_xsd)
+            self.assertXmlValid(xml)
+            file_concept = cnx.entity_from_eid(self.file_concept_eid)
+            with open(self.datapath(expected_file)) as expected:
+                self.assertXmlEqual(expected.read()
+                                    % {'concept-uri': binary_type(file_concept.cwuri),
+                                       'scheme-uri': binary_type(file_concept.scheme.cwuri)},
+                                    generated_xsd)
+            # ensure there is no element with @type but a complex type
+            namespaces = adapter.namespaces.copy()
+            namespaces.pop(None)
+            dates = xml.xpath('//xsd:element[@name="Date"]/xsd:complexType/xsd:sequence',
+                              namespaces=namespaces)
+            self.assertEqual(len(dates), 0)
+
+
 if __name__ == '__main__':
     import unittest
     unittest.main()