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>