Use first line of annotation as title for archive units and data objects
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Wed, 11 Jan 2017 15:08:47 +0100
changeset 2265 aac19cc4a92e
parent 2264 5fb5bf38f962
child 2266 70cccf02d910
Use first line of annotation as title for archive units and data objects * make it mandatory ; * update migration script removing id to copy existing value to annotation before removal ; * regenerate test data file since this change adds exported annotations. Along the way, add missing escaping when displaying user_annotation, by using printable_value. Related to #16070476
entities/custom.py
i18n/en.po
i18n/fr.po
migration/0.6.0_Any.py
schema/__init__.py
test/data/seda_02_export.rng
test/data/seda_02_export.xsd
test/data/seda_1_export.rng
test/data/seda_1_export.xsd
test/test_entities.py
test/test_html_generation.py
test/test_profile_generation.py
test/test_views.py
test/testutils.py
views/viewlib.py
--- a/entities/custom.py	Tue Jan 10 17:51:45 2017 +0100
+++ b/entities/custom.py	Wed Jan 11 15:08:47 2017 +0100
@@ -17,6 +17,13 @@
 from cubes.seda.entities import generated
 
 
+def _extract_title(annotation):
+    """Return the first line in the annotation to use as a title"""
+    annotation = annotation.strip()
+    assert annotation
+    return annotation.splitlines()[0]
+
+
 class SEDAArchiveTransfer(generated.SEDAArchiveTransfer):
 
     def dc_title(self):
@@ -53,6 +60,9 @@
 
 class SEDAArchiveUnit(generated.SEDAArchiveUnit):
 
+    def dc_title(self):
+        return _extract_title(self.user_annotation)
+
     @property
     def is_archive_unit_ref(self):
         """Return true if this is a 'reference' archive unit, else false for 'description' archive
@@ -70,6 +80,9 @@
 
 class SEDABinaryDataObject(generated.SEDABinaryDataObject):
 
+    def dc_title(self):
+        return _extract_title(self.user_annotation)
+
     @property
     def format_id(self):
         return self.reverse_seda_format_id_from[0] if self.reverse_seda_format_id_from else None
@@ -96,6 +109,12 @@
                 yield seq
 
 
+class SEDAPhysicalDataObject(generated.SEDAPhysicalDataObject):
+
+    def dc_title(self):
+        return _extract_title(self.user_annotation)
+
+
 class SEDAAltArchiveUnitArchiveUnitRefId(generated.SEDAAltArchiveUnitArchiveUnitRefId):
 
     @property
--- a/i18n/en.po	Tue Jan 10 17:51:45 2017 +0100
+++ b/i18n/en.po	Wed Jan 11 15:08:47 2017 +0100
@@ -7681,6 +7681,10 @@
 msgstr ""
 
 msgid ""
+"the first line will be used to display this entity within the user interface"
+msgstr ""
+
+msgid ""
 "there is no scheme available for this relation. Contact the site "
 "administrator."
 msgstr ""
--- a/i18n/fr.po	Tue Jan 10 17:51:45 2017 +0100
+++ b/i18n/fr.po	Wed Jan 11 15:08:47 2017 +0100
@@ -7704,6 +7704,12 @@
 msgstr ""
 
 msgid ""
+"the first line will be used to display this entity within the user interface"
+msgstr ""
+"la première ligne doit être courte et descriptive : elle sera utilisée pour "
+"afficher cette entité dans l'interface"
+
+msgid ""
 "there is no scheme available for this relation. Contact the site "
 "administrator."
 msgstr ""
--- a/migration/0.6.0_Any.py	Tue Jan 10 17:51:45 2017 +0100
+++ b/migration/0.6.0_Any.py	Wed Jan 11 15:08:47 2017 +0100
@@ -6,10 +6,16 @@
     cnx.create_entity('Label', label_of=concept,
                       kind=u'preferred', language_code=u'seda-2',
                       label=seda2_label)
-
 commit()
 
 sync_schema_props_perms(('SEDAArchiveTransfer', 'title', 'String'))
 
+with cnx.deny_all_hooks_but():
+    rql('SET X user_annotation XI + "\n" + XUA WHERE X id XI, X user_annotation XUA, '
+        'NOT X user_annotation NULL')
+    rql('SET X user_annotation XI WHERE X id XI, '
+        'X user_annotation NULL')
+commit()
 for etype in ('SEDAArchiveUnit', 'SEDABinaryDataObject', 'SEDAPhysicalDataObject'):
     drop_attribute(etype, 'id')
+    sync_schema_props_perms((etype, 'user_annotation', 'String'))
--- a/schema/__init__.py	Tue Jan 10 17:51:45 2017 +0100
+++ b/schema/__init__.py	Wed Jan 11 15:08:47 2017 +0100
@@ -38,7 +38,15 @@
                                     internationalizable=True),
                              name='user_cardinality')
         if annotable:
-            cls.add_relation(String(fulltextindexed=True),
+            required = cls.__name__ in ('SEDAArchiveUnit',
+                                        'SEDABinaryDataObject', 'SEDAPhysicalDataObject')
+            if required:
+                description = _('the first line will be used to display this '
+                                'entity within the user interface')
+            else:
+                description = None
+            cls.add_relation(String(fulltextindexed=True,
+                                    required=required, description=description),
                              name='user_annotation')
         return cls
     return decorator
--- a/test/data/seda_02_export.rng	Tue Jan 10 17:51:45 2017 +0100
+++ b/test/data/seda_02_export.rng	Wed Jan 11 15:08:47 2017 +0100
@@ -1,5 +1,5 @@
 <?xml version='1.0' encoding='utf-8' standalone='no'?>
-<rng:grammar xmlns:clmDAFFileTypeCode="urn:un:unece:uncefact:codelist:draft:DAF:fileTypeCode:2009-08-18" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:clm60133="urn:un:unece:uncefact:codelist:standard:6:0133:40106" 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:clmIANACharacterSetCode="urn:un:unece:uncefact:codelist:standard:IANA:CharacterSetCode:2007-05-14" xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:10" xmlns:rng="http://relaxng.org/ns/structure/1.0" xmlns="fr:gouv:culture:archivesdefrance:seda:v1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes" ns="fr:gouv:ae:archive:draft:standard_echange_v0.2">
+<rng:grammar xmlns:rng="http://relaxng.org/ns/structure/1.0" xmlns:clmIANACharacterSetCode="urn:un:unece:uncefact:codelist:standard:IANA:CharacterSetCode:2007-05-14" xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:10" xmlns:clmDAFFileTypeCode="urn:un:unece:uncefact:codelist:draft:DAF:fileTypeCode:2009-08-18" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:qdt="fr:gouv:culture:archivesdefrance:seda:v1.0:QualifiedDataType:1" xmlns:clmIANAMIMEMediaType="urn:un:unece:uncefact:codelist:standard:IANA:MIMEMediaType:2008-11-12" xmlns:clm60133="urn:un:unece:uncefact:codelist:standard:6:0133:40106" xmlns="fr:gouv:culture:archivesdefrance:seda:v1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes" ns="fr:gouv:ae:archive:draft:standard_echange_v0.2">
   <rng:start>
     <rng:element name="ArchiveTransfer">
       <xsd:annotation>
@@ -136,6 +136,9 @@
       </rng:element>
       <rng:oneOrMore>
         <rng:element name="Contains">
+          <xsd:annotation>
+            <xsd:documentation>archive unit title</xsd:documentation>
+          </xsd:annotation>
           <rng:optional>
             <rng:attribute name="Id">
               <rng:data type="ID"/>
@@ -353,6 +356,9 @@
                 </rng:attribute>
                 <rng:data type="string"/>
               </rng:element>
+              <xsd:annotation>
+                <xsd:documentation>archive unit title</xsd:documentation>
+              </xsd:annotation>
               <rng:optional>
                 <rng:attribute name="Id">
                   <rng:data type="ID"/>
@@ -416,6 +422,9 @@
                 </rng:attribute>
                 <rng:data type="string"/>
               </rng:element>
+              <xsd:annotation>
+                <xsd:documentation>archive unit title</xsd:documentation>
+              </xsd:annotation>
               <rng:optional>
                 <rng:attribute name="Id">
                   <rng:data type="ID"/>
@@ -476,6 +485,9 @@
           </rng:oneOrMore>
           <rng:zeroOrMore>
             <rng:element name="Document">
+              <xsd:annotation>
+                <xsd:documentation>data object title</xsd:documentation>
+              </xsd:annotation>
               <rng:optional>
                 <rng:attribute name="Id">
                   <rng:data type="ID"/>
--- a/test/data/seda_02_export.xsd	Tue Jan 10 17:51:45 2017 +0100
+++ b/test/data/seda_02_export.xsd	Wed Jan 11 15:08:47 2017 +0100
@@ -1,5 +1,5 @@
 <?xml version='1.0' encoding='utf-8' standalone='no'?>
-<xsd:schema attributeFormDefault="unqualified" elementFormDefault="qualified" targetNamespace="fr:gouv:ae:archive:draft:standard_echange_v0.2" version="1.1" 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:ae:archive:draft:standard_echange_v0.2:QualifiedDataType:1" xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:6" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="fr:gouv:ae:archive:draft:standard_echange_v0.2">
+<xsd:schema xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:6" xmlns:qdt="fr:gouv:ae:archive:draft:standard_echange_v0.2:QualifiedDataType:1" xmlns:clm60133="urn:un:unece:uncefact:codelist:standard:6:0133:40106" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:clmIANACharacterSetCode="urn:un:unece:uncefact:codelist:standard:IANA:CharacterSetCode:2007-05-14" xmlns:clmDAFFileTypeCode="urn:un:unece:uncefact:codelist:draft:DAF:fileTypeCode:2009-08-18" xmlns:clmIANAMIMEMediaType="urn:un:unece:uncefact:codelist:standard:IANA:MIMEMediaType:2008-11-12" xmlns="fr:gouv:ae:archive:draft:standard_echange_v0.2" attributeFormDefault="unqualified" elementFormDefault="qualified" targetNamespace="fr:gouv:ae:archive:draft:standard_echange_v0.2" version="1.1">
   <xsd:element name="ArchiveTransfer">
     <xsd:annotation>
       <xsd:documentation>my profile title &amp;&amp;</xsd:documentation>
@@ -73,6 +73,9 @@
           </xsd:complexType>
         </xsd:element>
         <xsd:element maxOccurs="unbounded" name="Contains">
+          <xsd:annotation>
+            <xsd:documentation>archive unit title</xsd:documentation>
+          </xsd:annotation>
           <xsd:complexType>
             <xsd:sequence>
               <xsd:element fixed="fr" name="DescriptionLanguage">
@@ -167,7 +170,7 @@
                               </xsd:simpleContent>
                             </xsd:complexType>
                           </xsd:element>
-                          <xsd:element fixed="%(concept-uri)s" name="KeywordReference" minOccurs="0">
+                          <xsd:element fixed="%(concept-uri)s" minOccurs="0" name="KeywordReference">
                             <xsd:complexType>
                               <xsd:simpleContent>
                                 <xsd:extension base="qdt:ArchivesIDType">
@@ -241,6 +244,9 @@
                 </xsd:complexType>
               </xsd:element>
               <xsd:element maxOccurs="unbounded" name="Contains">
+                <xsd:annotation>
+                  <xsd:documentation>archive unit title</xsd:documentation>
+                </xsd:annotation>
                 <xsd:complexType>
                   <xsd:sequence>
                     <xsd:element name="DescriptionLevel">
@@ -304,6 +310,9 @@
                 </xsd:complexType>
               </xsd:element>
               <xsd:element maxOccurs="unbounded" name="Contains">
+                <xsd:annotation>
+                  <xsd:documentation>archive unit title</xsd:documentation>
+                </xsd:annotation>
                 <xsd:complexType>
                   <xsd:sequence>
                     <xsd:element name="DescriptionLevel">
@@ -371,6 +380,9 @@
                 </xsd:complexType>
               </xsd:element>
               <xsd:element maxOccurs="unbounded" minOccurs="0" name="Document">
+                <xsd:annotation>
+                  <xsd:documentation>data object title</xsd:documentation>
+                </xsd:annotation>
                 <xsd:complexType>
                   <xsd:sequence>
                     <xsd:element name="Attachment">
--- a/test/data/seda_1_export.rng	Tue Jan 10 17:51:45 2017 +0100
+++ b/test/data/seda_1_export.rng	Wed Jan 11 15:08:47 2017 +0100
@@ -1,14 +1,5 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<rng:grammar xmlns:rng="http://relaxng.org/ns/structure/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"
-             ns="fr:gouv:culture:archivesdefrance:seda:v1.0"
-             datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
+<?xml version='1.0' encoding='utf-8' standalone='no'?>
+<rng:grammar xmlns:clmIANACharacterSetCode="urn:un:unece:uncefact:codelist:standard:IANA:CharacterSetCode:2007-05-14" xmlns:clmDAFFileTypeCode="urn:un:unece:uncefact:codelist:draft:DAF:fileTypeCode:2009-08-18" xmlns:rng="http://relaxng.org/ns/structure/1.0" 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:clmIANAMIMEMediaType="urn:un:unece:uncefact:codelist:standard:IANA:MIMEMediaType:2008-11-12" xmlns:clm60133="urn:un:unece:uncefact:codelist:standard:6:0133:40106" xmlns="fr:gouv:culture:archivesdefrance:seda:v1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes" ns="fr:gouv:culture:archivesdefrance:seda:v1.0">
   <rng:start>
     <rng:element name="ArchiveTransfer">
       <xsd:annotation>
@@ -145,6 +136,9 @@
       </rng:element>
       <rng:oneOrMore>
         <rng:element name="Archive">
+          <xsd:annotation>
+            <xsd:documentation>archive unit title</xsd:documentation>
+          </xsd:annotation>
           <rng:optional>
             <rng:attribute name="Id">
               <rng:data type="ID"/>
@@ -370,6 +364,9 @@
           </rng:element>
           <rng:oneOrMore>
             <rng:element name="ArchiveObject">
+              <xsd:annotation>
+                <xsd:documentation>archive unit title</xsd:documentation>
+              </xsd:annotation>
               <rng:optional>
                 <rng:attribute name="Id">
                   <rng:data type="ID"/>
@@ -446,6 +443,9 @@
           </rng:oneOrMore>
           <rng:oneOrMore>
             <rng:element name="ArchiveObject">
+              <xsd:annotation>
+                <xsd:documentation>archive unit title</xsd:documentation>
+              </xsd:annotation>
               <rng:optional>
                 <rng:attribute name="Id">
                   <rng:data type="ID"/>
@@ -525,6 +525,9 @@
           </rng:oneOrMore>
           <rng:zeroOrMore>
             <rng:element name="Document">
+              <xsd:annotation>
+                <xsd:documentation>data object title</xsd:documentation>
+              </xsd:annotation>
               <rng:optional>
                 <rng:attribute name="Id">
                   <rng:data type="ID"/>
--- a/test/data/seda_1_export.xsd	Tue Jan 10 17:51:45 2017 +0100
+++ b/test/data/seda_1_export.xsd	Wed Jan 11 15:08:47 2017 +0100
@@ -1,5 +1,5 @@
 <?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:schema 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:qdt="fr:gouv:culture:archivesdefrance:seda:v1.0:QualifiedDataType:1" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:clm60133="urn:un:unece:uncefact:codelist:standard:6:0133:40106" xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:10" xmlns:clmIANAMIMEMediaType="urn:un:unece:uncefact:codelist:standard:IANA:MIMEMediaType:2008-11-12" xmlns="fr:gouv:culture:archivesdefrance:seda:v1.0" attributeFormDefault="unqualified" elementFormDefault="qualified" targetNamespace="fr:gouv:culture:archivesdefrance:seda:v1.0" version="1.0">
   <xsd:element name="ArchiveTransfer">
     <xsd:annotation>
       <xsd:documentation>my profile title &amp;&amp;</xsd:documentation>
@@ -73,6 +73,9 @@
           </xsd:complexType>
         </xsd:element>
         <xsd:element maxOccurs="unbounded" name="Archive">
+          <xsd:annotation>
+            <xsd:documentation>archive unit title</xsd:documentation>
+          </xsd:annotation>
           <xsd:complexType>
             <xsd:sequence>
               <xsd:element fixed="fra" name="DescriptionLanguage">
@@ -154,7 +157,7 @@
                               </xsd:simpleContent>
                             </xsd:complexType>
                           </xsd:element>
-                          <xsd:element fixed="%(concept-uri)s" name="KeywordReference" minOccurs="0">
+                          <xsd:element fixed="%(concept-uri)s" minOccurs="0" name="KeywordReference">
                             <xsd:complexType>
                               <xsd:simpleContent>
                                 <xsd:extension base="qdt:ArchivesIDType">
@@ -249,6 +252,9 @@
                 </xsd:complexType>
               </xsd:element>
               <xsd:element maxOccurs="unbounded" name="ArchiveObject">
+                <xsd:annotation>
+                  <xsd:documentation>archive unit title</xsd:documentation>
+                </xsd:annotation>
                 <xsd:complexType>
                   <xsd:sequence>
                     <xsd:element name="Name">
@@ -328,6 +334,9 @@
                 </xsd:complexType>
               </xsd:element>
               <xsd:element maxOccurs="unbounded" name="ArchiveObject">
+                <xsd:annotation>
+                  <xsd:documentation>archive unit title</xsd:documentation>
+                </xsd:annotation>
                 <xsd:complexType>
                   <xsd:sequence>
                     <xsd:element name="Name">
@@ -411,6 +420,9 @@
                 </xsd:complexType>
               </xsd:element>
               <xsd:element maxOccurs="unbounded" minOccurs="0" name="Document">
+                <xsd:annotation>
+                  <xsd:documentation>data object title</xsd:documentation>
+                </xsd:annotation>
                 <xsd:complexType>
                   <xsd:sequence>
                     <xsd:element name="Attachment">
--- a/test/test_entities.py	Tue Jan 10 17:51:45 2017 +0100
+++ b/test/test_entities.py	Wed Jan 11 15:08:47 2017 +0100
@@ -98,6 +98,7 @@
             # test clone with reparenting
             transfer = cnx.create_entity('SEDAArchiveTransfer', title=u'test profile')
             cloned = cnx.create_entity(unit.cw_etype,
+                                       user_annotation=u'I am mandatory',
                                        clone_of=unit,
                                        seda_archive_unit=transfer)
             cnx.commit()
@@ -244,5 +245,14 @@
             self.assertEqual(container.eid, transfer.eid)
 
 
+class CustomEntitiesTC(CubicWebTC):
+
+    def test_title(self):
+        with self.admin_access.client_cnx() as cnx:
+            for etype in ('SEDAArchiveUnit', 'SEDABinaryDataObject', 'SEDAPhysicalDataObject'):
+                ent = cnx.create_entity(etype, user_annotation=u'bla bla\nbli bli blo\n')
+                self.assertEqual(ent.dc_title(), u'bla bla')
+
+
 if __name__ == '__main__':
     unittest.main()
--- a/test/test_html_generation.py	Tue Jan 10 17:51:45 2017 +0100
+++ b/test/test_html_generation.py	Wed Jan 11 15:08:47 2017 +0100
@@ -76,8 +76,10 @@
             self.assertEqual(len(self.xpath(profile, '//title')), 1)
             self.assertEqual(len(self.xpath(profile, '//h1')), 1)
             # ensure annotation are serialized
-            self.assertEqual(self.xpath(profile, '//div[@class="description"]/text()'),
-                             ['Composant ISAD(G)'])
+            self.assertEqual(
+                [ann for ann in self.xpath(profile, '//div[@class="description"]/text()')
+                 if ann != 'data object title'],
+                ['Composant ISAD(G)'])
             # ensure all attributes have label, card and value defined
             attr_divs = self.xpath(profile, '//div[@class="attribute"]')
             attr_defs = set()
--- a/test/test_profile_generation.py	Tue Jan 10 17:51:45 2017 +0100
+++ b/test/test_profile_generation.py	Wed Jan 11 15:08:47 2017 +0100
@@ -382,6 +382,7 @@
         with self.admin_access.client_cnx() as cnx:
             transfer = cnx.create_entity('SEDAArchiveTransfer', title=u'test profile')
             bdo = cnx.create_entity('SEDABinaryDataObject',
+                                    user_annotation=u'I am mandatory',
                                     seda_binary_data_object=transfer)
             appname = cnx.create_entity('SEDACreatingApplicationName',
                                         seda_creating_application_name=bdo)
@@ -405,7 +406,8 @@
         with self.admin_access.client_cnx() as cnx:
             transfer = cnx.create_entity('SEDAArchiveTransfer', title=u'test profile')
             bdo = cnx.create_entity('SEDABinaryDataObject',
-                              seda_binary_data_object=transfer)
+                                    user_annotation=u'I am mandatory',
+                                    seda_binary_data_object=transfer)
 
             profile = self.profile_etree(transfer)
             fileinfo = self.get_element(profile, 'DataObjectPackage')
@@ -536,6 +538,7 @@
             transfer = create('SEDAArchiveTransfer', title=u'test profile',
                               seda_message_digest_algorithm_code_list_version=scheme)
             create('SEDABinaryDataObject', user_cardinality=u'0..n',
+                   user_annotation=u'I am mandatory',
                    seda_binary_data_object=transfer,
                    seda_algorithm=some_concept)
 
--- a/test/test_views.py	Tue Jan 10 17:51:45 2017 +0100
+++ b/test/test_views.py	Wed Jan 11 15:08:47 2017 +0100
@@ -120,6 +120,7 @@
         with self.admin_access.repo_cnx() as cnx:
             transfer = cnx.create_entity('SEDAArchiveTransfer', title=u'Test widget')
             bdo = cnx.create_entity('SEDABinaryDataObject',
+                                    user_annotation=u'I am mandatory',
                                     seda_binary_data_object=transfer)
             bdo_alt = cnx.create_entity('SEDAAltBinaryDataObjectAttachment',
                                         reverse_seda_alt_binary_data_object_attachment=bdo)
--- a/test/testutils.py	Tue Jan 10 17:51:45 2017 +0100
+++ b/test/testutils.py	Wed Jan 11 15:08:47 2017 +0100
@@ -33,6 +33,7 @@
     else (archive unit, alternative, reference).
     """
     cnx = kwargs.pop('cnx', getattr(parent, '_cw', None))
+    kwargs.setdefault('user_annotation', u'archive unit title')
     au = cnx.create_entity('SEDAArchiveUnit', seda_archive_unit=parent, **kwargs)
     alt = cnx.create_entity('SEDAAltArchiveUnitArchiveUnitRefId',
                             reverse_seda_alt_archive_unit_archive_unit_ref_id=au)
@@ -49,6 +50,7 @@
     cnx = getattr(parent, '_cw', None)
     if parent.cw_etype == 'SEDAArchiveTransfer':
         kwargs['seda_binary_data_object'] = parent
+    kwargs.setdefault('user_annotation', u'data object title')
     bdo = cnx.create_entity('SEDABinaryDataObject', **kwargs)
     choice = cnx.create_entity('SEDAAltBinaryDataObjectAttachment',
                                reverse_seda_alt_binary_data_object_attachment=bdo)
--- a/views/viewlib.py	Tue Jan 10 17:51:45 2017 +0100
+++ b/views/viewlib.py	Wed Jan 11 15:08:47 2017 +0100
@@ -32,10 +32,13 @@
         cardinality = getattr(entity, 'user_cardinality', '1')
         if cardinality != '1' or not skip_one_card:
             self.w(u' <span class="cardinality">[%s]</span>' % cardinality)
-        if with_annotation:
-            description = getattr(entity, 'user_annotation', None)
+        if with_annotation and getattr(entity, 'user_annotation', None):
+            description = entity.printable_value('user_annotation')
+            if entity.cw_etype in FIRST_LEVEL_ETYPES:
+                # skip first line, already displayed since it's used as title for the entity
+                description = description.splitlines()[1:]
             if description:
-                self.w(u' <span class="description text-muted">%s</span>' % description)
+                self.w(u' <span class="description text-muted">%s</span>' % '\n'.join(description))
 
 
 class TextEntityAttributeView(EntityView):
@@ -145,7 +148,7 @@
 
 class BusinessValueLinkEntityView(BusinessValueEntityView):
     """Similar to seda.business but value is enclosed in a link if some value is specified."""
-    __select__ = is_instance(*FIRST_LEVEL_ETYPES)
+    __abstract__ = True
 
     def entity_value(self, entity):
         value = super(BusinessValueLinkEntityView, self).entity_value(entity)
@@ -154,6 +157,15 @@
         return value
 
 
+class BusinessValueReferenceEntityView(BusinessValueEntityView):
+    """Similar to seda.business but value is enclosed in a link if some value is specified."""
+    __select__ = is_instance(*FIRST_LEVEL_ETYPES)
+
+    def entity_value(self, entity):
+        value = entity.dc_title()
+        return tags.a(value, href=entity.absolute_url())
+
+
 class TypeAndMetaEntityView(EntityView):
     """Glue entity's type, seda.business and seda.xsdmeta views together, for use within alternative
     """