Mercurial > hg > MPIWGWeb
changeset 81:975a8d88e315
new editable info blocks for projects.
removed RelatedDigitalSources. updateAllProjects converts to info block.
author | casties |
---|---|
date | Fri, 10 May 2013 17:32:53 +0200 |
parents | b1893c4c9d2c |
children | a6054bc6ad71 |
files | MPIWGHelper.py MPIWGProjects.py MPIWGProjects_removed.py __init__.py zpt/project/edit_infoblocks.zpt zpt/project/edit_template.zpt zpt/project/infoblock/edit_items.zpt zpt/project/project_template.zpt |
diffstat | 8 files changed, 364 insertions(+), 100 deletions(-) [+] |
line wrap: on
line diff
--- a/MPIWGHelper.py Wed May 08 19:59:25 2013 +0200 +++ b/MPIWGHelper.py Fri May 10 17:32:53 2013 +0200 @@ -1,12 +1,10 @@ from Products.PageTemplates.PageTemplateFile import PageTemplateFile import SrvTxtUtils +import time +import email import logging -definedFields=['WEB_title','xdata_01','xdata_02','xdata_03','xdata_04','xdata_05','xdata_06','xdata_07','xdata_08','xdata_09','xdata_10','xdata_11','xdata_12','xdata_13','WEB_project_header','WEB_project_description','WEB_related_pub'] - -checkFields = ['xdata_01'] - #ersetzt logging def logger(txt,method,txt2): """logging""" @@ -156,3 +154,18 @@ return '%s/%s' % (baseUrl, self.getId()) + +def redirect(self, RESPONSE, url): + """mache ein redirect mit einem angehaengten time stamp um ein reload zu erzwingen""" + timeStamp = time.time() + + if url.find("?") > -1: # giebt es schon parameter + addStr = "&time=%s" + else: + addStr = "?time=%s" + + RESPONSE.setHeader('Last-Modified', email.Utils.formatdate().split("-")[0] + 'GMT') + logging.debug(email.Utils.formatdate() + ' GMT') + RESPONSE.redirect(url + addStr % timeStamp) + +
--- a/MPIWGProjects.py Wed May 08 19:59:25 2013 +0200 +++ b/MPIWGProjects.py Fri May 10 17:32:53 2013 +0200 @@ -14,7 +14,6 @@ import urllib import re import os -import email import sys import logging import time @@ -50,7 +49,7 @@ definedFields = fieldLabels.keys() # TODO: should this be sorted? -editableFields = ('xdata_01', 'xdata_05', 'xdata_07', 'xdata_08', 'xdata_11', 'xdata_12', 'xdata_13') +editableFields = ('xdata_07', 'xdata_01', 'xdata_05', 'xdata_08', 'xdata_12', 'xdata_13') # die folgenden Klassen sind jetzt in einzelne Files ausgelagert aus Kompatibilitaetsgruenden, bleiben die Klassen hier noch drin. # Sonst funktionieren die alten Webseiten nicht mehr. @@ -79,21 +78,10 @@ bookId = None # templates - editDescription = PageTemplateFile('zpt/project/related_publication/edit_basic', globals()) + edit = PageTemplateFile('zpt/project/related_publication/edit_basic', globals()) - def redirect(self, RESPONSE, url): - """mache ein redirect mit einem angehaengten time stamp um ein reload zu erzwingen""" - timeStamp = time.time() - - if url.find("?") > -1: # giebt es schon parameter - addStr = "&time=%s" - else: - addStr = "?time=%s" - - RESPONSE.setHeader('Last-Modified', email.Utils.formatdate().split("-")[0] + 'GMT') - logging.debug(email.Utils.formatdate() + ' GMT') - RESPONSE.redirect(url + addStr % timeStamp) + redirect = MPIWGHelper.redirect def hasLinkToBookPage(self): @@ -132,7 +120,7 @@ """edit a publication""" if (not text) and (not description): - pt = self.editDescription + pt = self.edit return pt() if text: @@ -149,7 +137,7 @@ self.redirect(RESPONSE, "../managePublications") -class MPIWGProject_relatedProject(Folder): +class MPIWGProject_relatedProject(SimpleItem): """publications object fuer project""" meta_type = "MPIWGProject_relatedProject" @@ -158,21 +146,10 @@ projectLabel = None # templates - editDescription = PageTemplateFile('zpt/project/related_project/edit_basic', globals()) + edit = PageTemplateFile('zpt/project/related_project/edit_basic', globals()) - def redirect(self, RESPONSE, url): - """mache ein redirect mit einem angehaengten time stamp um ein reload zu erzwingen""" - - timeStamp = time.time() - - if url.find("?") > -1: # giebt es schon parameter - addStr = "&time=%s" - else: - addStr = "?time=%s" - - RESPONSE.setHeader('Last-Modified', email.Utils.formatdate().split("-")[0] + 'GMT') - logging.debug(email.Utils.formatdate() + ' GMT') - RESPONSE.redirect(url + addStr % timeStamp) + + redirect = MPIWGHelper.redirect def getProjectId(self): @@ -265,6 +242,102 @@ self.redirect(RESPONSE, "../manageImages") +class MPIWGProject_InfoBlock(SimpleItem): + """publications object fuer project""" + + meta_type = "MPIWGProject_InfoBlock" + + # templates + edit = PageTemplateFile('zpt/project/infoblock/edit_items', globals()) + + + redirect = MPIWGHelper.redirect + + + def __init__(self, id, title=None): + """Create info block.""" + self.id = id + self.title = title + self.place = 0 + self.items = [] + + + def getTitle(self): + """Return the title.""" + return self.title + + + def getItems(self): + """Return the list of items.""" + return self.items + + + def setItems(self, items): + """Set the list of items.""" + self.items = items + self._p_changed = True + + + def addItem(self, item=None, text=None, link=None, RESPONSE=None): + """Add an item to the InfoBox""" + if item is None: + item = {'text': text, 'link': link} + + self.items.append(item) + self._p_changed = True + if RESPONSE is not None: + self.redirect(RESPONSE, 'edit') + + + def deleteItem(self, idx, RESPONSE=None): + """Delete an item from the info block.""" + try: + del self.items[int(idx)] + self._p_changed = True + except: + logging.error("InfoBlock deleteItem: error deleting item %s!"%idx) + + if RESPONSE is not None: + self.redirect(RESPONSE, 'edit') + + + def moveItem(self, idx, op, RESPONSE=None): + """Move items up or down the list.""" + try: + idx = int(idx) + if op == 'up': + if idx > 0: + self.items[idx-1], self.items[idx] = self.items[idx], self.items[idx-1] + elif op == 'down': + if idx < len(self.items)-1: + self.items[idx], self.items[idx+1] = self.items[idx+1], self.items[idx] + + self._p_changed = True + except: + logging.error("InfoBlock moveItem: error moving item at %s!"%idx) + + if RESPONSE is not None: + self.redirect(RESPONSE, 'edit') + + + def editItems(self, REQUEST, RESPONSE=None): + """Change items from request form.""" + form = REQUEST.form + for k in form: + t, n = k.split('_') + if t in ['text', 'link']: + try: + logging.debug("editItems: change[%s].%s = %s"%(n,t,repr(form[k]))) + self.items[int(n)][t] = form[k] + except: + logging.error("InfoBlock editItems: error changing item %s!"%k) + + self._p_changed = True + if RESPONSE is not None: + self.redirect(RESPONSE, 'edit') + + + class MPIWGProject(Folder): """Class for Projects""" @@ -304,6 +377,7 @@ editRelatedProjectsError = PageTemplateFile('zpt/project/edit_related_projects_error', globals()) editImagesForm = PageTemplateFile('zpt/project/edit_images', globals()) editPublicationsForm = PageTemplateFile('zpt/project/edit_publications', globals()) + editInfoBlocksForm = PageTemplateFile('zpt/project/edit_infoblocks', globals()) editAdditionalPublicationsForm = PageTemplateFile('zpt/project/pubman/change_publications', globals()) editAddAdditionalPublications = PageTemplateFile('zpt/project/pubman/add_publications', globals()) security.declareProtected('View management screens', 'edit') @@ -344,19 +418,8 @@ # render template return pt() - def redirect(self, RESPONSE, url): - """mache ein redirect mit einem angehaengten time stamp um ein reload zu erzwingen""" - - timeStamp = time.time() - - if url.find("?") > -1: # giebt es schon parameter - addStr = "&time=%s" - else: - addStr = "?time=%s" - - RESPONSE.setHeader('Last-Modified', email.Utils.formatdate().split("-")[0] + 'GMT') - logging.debug(email.Utils.formatdate() + ' GMT') - RESPONSE.redirect(url + addStr % timeStamp) + + redirect = MPIWGHelper.redirect def getDefinedFields(self): @@ -784,6 +847,58 @@ return pt(link=link) + def getInfoBlockList(self): + """returns the list of related projects""" + items = self.objectValues(spec='MPIWGProject_InfoBlock') + # sort by place + items.sort(key=lambda x:int(getattr(x, 'place', 0))) + return items + + + def addInfoBlock(self, block_title=None, item_text=None, item_link=None, RESPONSE=None): + """add a MPIWGProject_InfoBlock""" + if block_title: + number = self._getLastInfoBlockNumber() + 1 + name = "infoblock_" + str(number) + while hasattr(self, name): + number += 1 + name = "infoblock_" + str(number) + + newBlock = MPIWGProject_InfoBlock(name, block_title) + # add block to project + self._setObject(name, newBlock) + obj = getattr(self, name) + obj.place = self._getLastInfoBlockNumber() + 1 + if item_text: + obj.addItem(text=item_text, link=item_link) + + if RESPONSE is not None: + self.redirect(RESPONSE, 'manageInfoBlocks') + + + def _getLastInfoBlockNumber(self): + items = self.getInfoBlockList() + if not items: + return 0 + else: + return getattr(items[-1], 'place', 0) + + + def manageInfoBlocks(self, name=None, op=None): + """manage related projects""" + self._moveObjectPlace(self.getInfoBlockList(), name, op) + + pt = self.editInfoBlocksForm + return pt() + + + def deleteInfoBlock(self, id, RESPONSE=None): + """delete Publication id""" + self.manage_delObjects([id]) + if RESPONSE: + self.redirect(RESPONSE, 'manageInfoBlocks') + + def getAdditionalPublicationList(self): """hole publications aus der datenbank""" query="select * from pubmanbiblio_projects where lower(key_main) = lower(%s) order by priority DESC" @@ -1181,8 +1296,6 @@ tmpPro.invisible = True pt = PageTemplateFile('zpt/previewFrame.zpt', globals()).__of__(self) return pt() - - # return self.REQUEST.RESPONSE.redirect(self.REQUEST['URL1']+"/previewTemplate") def isResponsibleScientist(self, key): @@ -1356,6 +1469,15 @@ return + def moveObjectDigitallibraryToInfoBlock(self): + """Move text from 'Object Digitallibrary' to InfoBlock.""" + text = self.getRelatedDigitalSources() + if text: + logging.debug("Moving 'Object Digitallibrary' to InfoBlock: %s"%repr(text)) + self.addInfoBlock(block_title='Related digital sources', item_text=text, item_link=None) + delattr(self, 'xdata_11') + + def hasRelatedPublicationsOldVersion(self): """teste ob es related publications gibt""" ret = True; @@ -1715,47 +1837,6 @@ RESPONSE.redirect(self.en.MPIWGrootURL()+'/admin/showTree') - # TODO: this is broken. is this used? - def getAllProjectsAndTagsAsCSV(self,archived=1,RESPONSE=None): - """alle projekte auch die nicht getaggten""" - retList=[] - headers=['projectId','sortingNumber','projectName','scholars','startedAt','completedAt','lastChangeThesaurusAt','lastChangeProjectAt','projectCreatedAt','persons','places','objects'] - headers.extend(list(self.thesaurus.tags.keys())) - retList.append("\t".join(headers)) - if not hasattr(self,'thesaurus'): - return "NON thesaurus (there have to be a MPIWGthesaurus object, with object ID thesaurus)" - - projectTags = self.thesaurus.getProjectsAndTags() - for project in self.getProjectFields('WEB_title_or_short'): - proj = project[0] - p_name = project[1] - retProj=[] - #if (not proj.isArchivedProject() and archived==1) or (proj.isArchivedProject() and archived==2): - retProj.append(self.utf8ify(proj.getId())) - retProj.append(self.utf8ify(proj.getContent('xdata_05'))) - retProj.append(self.utf8ify(p_name)) - retProj.append(self.utf8ify(proj.getContent('xdata_01'))) - retProj.append(self.utf8ify(proj.getStartedAt())) - retProj.append(self.utf8ify(proj.getCompletedAt())) - changeDate=self.thesaurus.lastChangeInThesaurus.get(proj.getId(),'') - n = re.sub("[:\- ]","",str(changeDate)) - retProj.append(n) - retProj.append(self.utf8ify(getattr(proj,'creationTime','20050101000000'))) - retProj.append("")#TODO: project created at - retProj.append(";".join([person[1] for person in self.thesaurus.getPersonsFromProject(proj.getId())])) - retProj.append(";".join([person[1] for person in self.thesaurus.getHistoricalPlacesFromProject(proj.getId())])) - retProj.append(";".join([person[1] for person in self.thesaurus.getObjectsFromProject(proj.getId())])) - retProj+=self.thesaurus.getTags(proj.getId(),projectTags) - retList.append("\t".join(retProj)) - - if RESPONSE: - - RESPONSE.setHeader('Content-Disposition','attachment; filename="ProjectsAndTags.tsv"') - RESPONSE.setHeader('Content-Type', "application/octet-stream") - - return "\n".join(retList); - - security.declareProtected('View management screens', 'updateAllProjectMembers') def updateAllProjectMembers(self, updateResponsibleScientistsList=False): """Re-create responsibleScientistsLists and projects_members table from all current projects.""" @@ -1870,6 +1951,12 @@ # hasLinkToBookPage updates the bookId pub.hasLinkToBookPage() + + # + # update RelatedDigitalSources + # + project.moveObjectDigitallibraryToInfoBlock() + # # unicodify #
--- a/MPIWGProjects_removed.py Wed May 08 19:59:25 2013 +0200 +++ b/MPIWGProjects_removed.py Fri May 10 17:32:53 2013 +0200 @@ -1,6 +1,8 @@ # # removed methods # + + class MPIWGProjects_notused: def decode(self, str): @@ -371,3 +373,45 @@ return ret + # TODO: this is broken. is this used? + def getAllProjectsAndTagsAsCSV(self,archived=1,RESPONSE=None): + """alle projekte auch die nicht getaggten""" + retList=[] + headers=['projectId','sortingNumber','projectName','scholars','startedAt','completedAt','lastChangeThesaurusAt','lastChangeProjectAt','projectCreatedAt','persons','places','objects'] + headers.extend(list(self.thesaurus.tags.keys())) + retList.append("\t".join(headers)) + if not hasattr(self,'thesaurus'): + return "NON thesaurus (there have to be a MPIWGthesaurus object, with object ID thesaurus)" + + projectTags = self.thesaurus.getProjectsAndTags() + for project in self.getProjectFields('WEB_title_or_short'): + proj = project[0] + p_name = project[1] + retProj=[] + #if (not proj.isArchivedProject() and archived==1) or (proj.isArchivedProject() and archived==2): + retProj.append(self.utf8ify(proj.getId())) + retProj.append(self.utf8ify(proj.getContent('xdata_05'))) + retProj.append(self.utf8ify(p_name)) + retProj.append(self.utf8ify(proj.getContent('xdata_01'))) + retProj.append(self.utf8ify(proj.getStartedAt())) + retProj.append(self.utf8ify(proj.getCompletedAt())) + changeDate=self.thesaurus.lastChangeInThesaurus.get(proj.getId(),'') + n = re.sub("[:\- ]","",str(changeDate)) + retProj.append(n) + retProj.append(self.utf8ify(getattr(proj,'creationTime','20050101000000'))) + retProj.append("")#TODO: project created at + retProj.append(";".join([person[1] for person in self.thesaurus.getPersonsFromProject(proj.getId())])) + retProj.append(";".join([person[1] for person in self.thesaurus.getHistoricalPlacesFromProject(proj.getId())])) + retProj.append(";".join([person[1] for person in self.thesaurus.getObjectsFromProject(proj.getId())])) + retProj+=self.thesaurus.getTags(proj.getId(),projectTags) + retList.append("\t".join(retProj)) + + if RESPONSE: + + RESPONSE.setHeader('Content-Disposition','attachment; filename="ProjectsAndTags.tsv"') + RESPONSE.setHeader('Content-Type', "application/octet-stream") + + return "\n".join(retList); + + +
--- a/__init__.py Wed May 08 19:59:25 2013 +0200 +++ b/__init__.py Fri May 10 17:32:53 2013 +0200 @@ -7,7 +7,6 @@ import MPIWGFolder import MPIWGRoot -from nameSplitter import nameSplitter def initialize(context): """initialize MPIWGWeb"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/zpt/project/edit_infoblocks.zpt Fri May 10 17:32:53 2013 +0200 @@ -0,0 +1,45 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html metal:use-macro="here/edit_template/macros/page"> +<head> +</head> +<body> +<tal:block metal:fill-slot="navsel" tal:define="global menusel string:infoblocks" /> +<tal:block metal:fill-slot="body"> + <h2>Additional info blocks</h2> + <table> + <tal:block tal:repeat="block here/getInfoBlockList"> + <tr tal:define="blockid block/getId"> + <td> + <a tal:attributes="href string:$root/manageInfoBlocks?name=$blockid&op=up">up</a><br> + <a tal:attributes="href string:$root/manageInfoBlocks?name=$blockid&op=down">down</a> + </td> + <td tal:content="string:[${block/place}]"/> + <td> + <div><b tal:content="block/getTitle" /></div> + <div tal:repeat="item block/getItems"> + <a tal:attributes="href item/link" tal:omit-tag="not:item/link" + tal:content="structure item/text"/> + </div> + <td> + <a tal:attributes="href string:$root/$blockid/edit">Edit</a><br/> + <a tal:attributes="href string:$root/deleteInfoBlock?id=$blockid">Delete</a> + </td> + </tr> + </tal:block> + </table> + + <h3>Add an info block</h3> + <form tal:attributes="action string:$root/addInfoBlock" method="post"> + <p><b>Info block title:</b><br/> + <input name="block_title" size="20"/> + </p> + <p><b>First item text:</b> <input name="item_text" size="60"/></p> + <p><b>First item link:</b> <input name="item_link" size="20"/> (optional)</p> + <p><input type="submit" value="submit"/></p> + <p>You can add more items to an info block after it's been created through the edit link above.</p> + </form> + +</tal:block> +</body> +</html>
--- a/zpt/project/edit_template.zpt Wed May 08 19:59:25 2013 +0200 +++ b/zpt/project/edit_template.zpt Fri May 10 17:32:53 2013 +0200 @@ -12,13 +12,22 @@ <h2 class="title">Edit project <i tal:content="here/getProjectTitle"/></h2> <metal:block metal:define-slot="navsel"/> <div class="mainnav"> - <span tal:attributes="class python:test('basic'==menusel, 'mainmenusel', 'mainmenu')"><a tal:attributes="href string:$root/editBasic">Basic information</a></span> - <span tal:attributes="class python:test('description'==menusel, 'mainmenusel', 'mainmenu')"><a tal:attributes="href string:$root/editDescription">Project description</a></span> - <span tal:attributes="class python:test('images'==menusel, 'mainmenusel', 'mainmenu')"><a tal:attributes="href string:$root/manageImages">Images</a></span> - <span tal:attributes="class python:test('publications'==menusel, 'mainmenusel', 'mainmenu')"><a tal:attributes="href string:$root/managePublications">Publications</a></span> - <span tal:attributes="class python:test('relatedProjects'==menusel, 'mainmenusel', 'mainmenu')"><a tal:attributes="href string:$root/manageRelatedProjects">Related Projects</a></span> - <span tal:attributes="class python:test('themes'==menusel, 'mainmenusel', 'mainmenu')"><a tal:attributes="href string:$root/tagTheProject">Tags</a></span> - <span class="mainmenu"><a target="_blank" tal:attributes="href python:here.getUrl(baseUrl=here.en.MPIWGrootURL()+'/research/projects')">View</a></span> + <span tal:attributes="class python:test('basic'==menusel, 'mainmenusel', 'mainmenu')"><a + tal:attributes="href string:$root/editBasic">Basic information</a></span> + <span tal:attributes="class python:test('description'==menusel, 'mainmenusel', 'mainmenu')"><a + tal:attributes="href string:$root/editDescription">Project description</a></span> + <span tal:attributes="class python:test('images'==menusel, 'mainmenusel', 'mainmenu')"><a + tal:attributes="href string:$root/manageImages">Images</a></span> + <span tal:attributes="class python:test('publications'==menusel, 'mainmenusel', 'mainmenu')"><a + tal:attributes="href string:$root/managePublications">Publications</a></span> + <span tal:attributes="class python:test('relatedProjects'==menusel, 'mainmenusel', 'mainmenu')"><a + tal:attributes="href string:$root/manageRelatedProjects">Related projects</a></span> + <span tal:attributes="class python:test('infoblocks'==menusel, 'mainmenusel', 'mainmenu')"><a + tal:attributes="href string:$root/manageInfoBlocks">Info blocks</a></span> + <span tal:attributes="class python:test('themes'==menusel, 'mainmenusel', 'mainmenu')"><a + tal:attributes="href string:$root/tagTheProject">Tags</a></span> + <span class="mainmenu"><a target="_blank" + tal:attributes="href python:here.getUrl(baseUrl=here.en.MPIWGrootURL()+'/research/projects')">View</a></span> </div> <div class="content"> <tal:block metal:define-slot="body"/>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/zpt/project/infoblock/edit_items.zpt Fri May 10 17:32:53 2013 +0200 @@ -0,0 +1,56 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html metal:use-macro="here/edit_template/macros/page"> +<head> +</head> +<body> + <tal:block metal:fill-slot="navsel" tal:define="global menusel string:infoblocks" /> + <tal:block metal:fill-slot="body" tal:define="blockid here/getId; items here/getItems"> + <h2>Edit info block</h2> + <form action="editItems" method="post"> + <h3> + Title: <input size="20" name="block_title" tal:attributes="value here/getTitle" /> + </h3> + + <table> + <tal:block tal:repeat="idx python:range(len(items))"> + <tr tal:define="item python:items[idx];"> + <td><a tal:attributes="href string:$root/$blockid/moveItem?idx=$idx&op=up">up</a><br> <a + tal:attributes="href string:$root/$blockid/moveItem?idx=$idx&op=down">down</a></td> + <td tal:content="string:[${idx}]" /> + <td> + <p> + <b>text:</b> <input tal:attributes="name string:text_$idx; value item/text" size="60" /> + </p> + <p> + <b>link:</b> <input tal:attributes="name string:link_$idx; value item/link" size="20" /> (optional) + </p> + </td> + <td><a tal:attributes="href string:$root/$blockid/deleteItem?idx=$idx">Delete</a></td> + </tr> + </tal:block> + </table> + <p> + <input type="submit" value="Change" /> + </p> + </form> + + <h3>Add an item</h3> + <form tal:attributes="action string:$root/$blockid/addItem" method="post"> + <p> + <b>text:</b> <input name="text" size="60" /> + </p> + <p> + <b>link:</b> <input name="link" size="20" /> (optional) + </p> + <p> + <input type="submit" value="Add" /> + </p> + </form> + + <h3> + <a tal:attributes="href string:$root/manageInfoBlocks">Back to info block list</a> + </h3> + </tal:block> +</body> +</html>
--- a/zpt/project/project_template.zpt Wed May 08 19:59:25 2013 +0200 +++ b/zpt/project/project_template.zpt Fri May 10 17:32:53 2013 +0200 @@ -163,9 +163,20 @@ </div> <!-- projects covered --> + <!-- custom info blocks --> + <div class="sideblock" tal:repeat="block here/getInfoBlockList"> + <h2 tal:content="block/getTitle">Info block</h2> + <div class="item" tal:repeat="item block/getItems"> + <a class="external" tal:attributes="href item/link" tal:omit-tag="not:item/link" + tal:content="structure item/text"> + info item + </a> + </div> + </div> + <!-- related digital sources --> <div class="sideblock" tal:define="sources here/getRelatedDigitalSources" tal:condition="sources"> - <h2>Related digital sources</h2> + <h2>OLD! Related digital sources</h2> <div class="item" tal:content="structure sources"> digital sources </div>