The goal

After migrating my website to plone I found it difficult to also import the ~ 1G of photos organized into JAlbum photo albums. The first approach was to create a separate mapping for the photo albums using:

ProxyPass /photo !
ProxyPassReverse /photo !

in the apache configuration.

This approach is not ok for the following reasons:

  • photos are not managed into the CMS thus I will have the same problems as before for adding comments and modifying albums
  • there is a different layout for the photo and other part of the site

One initial approach I thought about is to just dump all the photos in CMS but this also presents another problems:

  • very large ZODB since photos are not really ment to go in a database
  • the default thumbnail view for a folder does not allow base navigation from a photo to the next because the photos are not not really aware they are in an album

The ideea

The ideea was to create a new product using ArchGenXML which could:

  • show the album
  • show a photo with basic navigation and information (EXIF, comments)
  • import data from existing albums
  • use all the photos without storing them in the ZODB

Well, the ideea was simple, but the implementation tricky. Anyway, if you want, check the result before reading further.

The UML schema

The ExPhotoAlbum UML schema

UML elements defined

The ideea was to create:

  • An ExPhotoAlbum ordered folder (OrderedFolder) which contains
    • some description and the baseURL for all the images
    • an import function from JAlbum form which takes a remote url of an existing album and parses all the entries inserting links to the images in this album
    • the parseAlbum which is a function containing all the code which cannot be found in a controller template (url parsing, etc.) due to restricted python
  • An ExPhoto (BaseContent) which can be contained in the photo album (and nowhere else due to the global_allow=0 tagged value) which:
    • contains the fileName of the image
    • some description
    • exif data, extracted from the remote file
    • the functions which extract exif and comment data from the .jpg image
  • Some global configuration items (not used in the end)
  • All items will have discussions (comments) enabled (the allow_discussions=1 tagged value)

Create the product:

unzip  photoAlbum.zargo photoAlbum.xmi; $ARCHGENXML_PATH/ArchGenXML/ArchGenXML.py photoAlbum.xmi

This generates an ExPhotoAlbum directory.

Overwrite the ExPhotoAlbum view

Overwritting the view is really easy. I just had to create a exphotoalbum_view.pt in the skins directory which contains a “body” macro. This is the recomended way of creating a custom view for your content. Initially I tried to use a <<view>> stereotype but this generated a new tab for the content.

<html xmlns="http://www.w3.org/1999/xhtml" 
	  xmlns:tal="http://xml.zope.org/namespaces/tal" 
	  xmlns:metal="http://xml.zope.org/namespaces/metal"
	  xml:lang="en-US"
	  lang="en-US">
  <metal:block use-macro="context/global_defines/macros/defines" />
  <body>
	<metal:block metal:define-macro="body">
		<label tal:condition="context/description"><span>Description</span>:</label>
		<br/>
        <p
           tal:content="context/description"
           tal:condition="context/description">
          Description
        </p>
		<tal:exphotoalbum 
			tal:define="folderContents context/getFolderContents"
			tal:repeat="item folderContents">
		<div class="photoAlbumEntry photoAlbumFolder"
			tal:define="row python:item.getObject()">
		  <a tal:attributes="href row/id">
			<span class="photoAlbumEntryWrapper">
			  <img tal:attributes="src python:here.baseUrl + '/thumbs/' + row.fileName;title row/title;alt row/title"/>
			</span>
			<span class="photoAlbumEntryTitle">
               <tal:title content="row/fileName">Title</tal:title>
            </span>
		  </a>
		</div>
	  </tal:exphotoalbum>

	  <div class="visualClear"><!-- --></div>
	</metal:block>
  </body>
</html>

One of the things which costed me a lot of time at this template was to understand why not to use the listFolderContents function and what are the brains objects. Suffice to say that:

  • you list a folder with the getFolderContents
  • the resulted items are brains objects
  • you get your wrapped type with the getObject method or if you use metadata
  • as you see the images are in fact to the original location (on the same server) so they are not stored in the ZODB

Overwrite the ExPhoto view

<html xmlns="http://www.w3.org/1999/xhtml"
	  xmlns:tal="http://xml.zope.org/namespaces/tal" 
	  xmlns:metal="http://xml.zope.org/namespaces/metal"
	  xml:lang="en-US"
	  lang="en-US">
  <metal:block use-macro="context/global_defines/macros/defines" />
  <body>
	<metal:block metal:define-macro="body">

	  <div tal:define="idx python: context.getObjPositionInParent();max python:len(context.aq_parent.getFolderContents())">
		<a 
			class="exPhoto"
			tal:attributes="href python:here.baseUrl + '/' + context.fileName">
		  <img tal:attributes="src python:here.baseUrl + '/slides/' + context.fileName"/>
		</a>

		<div class="visualClear"></div>
		<br/>

		<div id="exPhotoAlbumNav">
		  <a 
			  tal:condition="python:idx > 0"
			  tal:define="prev python:context.aq_parent.getFolderContents()[idx - 1]"
			  tal:attributes="href prev/id"><img src="previous.gif" border="0"/></a>

		  <img
			  tal:condition="python:idx == 0"
			  src="previous_disabled.gif" border="0"/>

		  <a 
			  tal:attributes="href context/aq_parent/absolute_url" title="index">
			<img src="index.gif" alt="index"/>
		  </a>
		.......		
	  </div>
	</metal:block>
  </body>
</html>

Some observations apply:

  • to get this item position the folder you use: context.getObjPositionInParent
  • to get the parent you use: context.aq_parent
  • the image is referred to it’s original location but in the current page
  • some basic navigation is added

External methods

One of the things rather difficult to grasp is the security model. You cannot execute any python code anywhere since this might represent a security risk. The solution consist of adding public methods to your content type.

security.declarePublic('parseImage')
def parseImage(self,url):
        """
	...

By default a method from a content type can call any python module, in my case urllib. The security declaration allows for the function to be called from a template or controller template.

The controller template, importing from JAlbum files

If you add a method with <<form>> stereotype , ArchGenXML generates a .cpt file for you to fill.

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
      lang="en"
      metal:use-macro="here/main_template/macros/master"
      i18n:domain="plone">
<body>

<div metal:fill-slot="main"
	tal:condition="python:not isAnon">
	Enter URL of <span class="highlightedSearchTerm">existing</span> album:
    <form method="post"
		tal:define="errors options/state/getErrors;"
 		tal:attributes="action template/id;">
	<input type="hidden" name="form.submitted" value="1" />
	<input type="text" name="url" 
		   tal:attributes="tabindex tabindex/next;
           value request/url|nothing" />
	<input type="submit" value="Submit"/>
	<p tal:define="err errors/url|nothing" tal:condition="err" tal:content="err" />
    </form>
</div>

</body>
</html>

The metadata for this controller template defines the validators and the actions:

[validators]
validators = <span class="highlightedSearchTerm">import</span>PhotoList_validate

[actions]
action.success=redirect_to:python:folder.absolute_url()
action.error=traverse_to:string:<span class="highlightedSearchTerm">import</span>PhotoList
  • upon succes we are redirected to the parent folder
  • upon failure the error is displayed in the original form

The validator does all the work (a bit ugly, must I admit)

<span class="highlightedSearchTerm">import</span> urllib, re

from Products.CMFCore.utils <span class="highlightedSearchTerm">import</span> getToolByName

url = context.REQUEST.get('url')
baseurl = url

if url.endswith(".html"):
    baseurl = '/'.join(url.split('/')[:-1])

#<span class="highlightedSearchTerm">import</span> at most 4 pages of the album
for i in ['','2','3','4']:
    try:
        context.plone_log(url)
        folder = context.aq_inner
        folder.edit(baseUrl = baseurl)

        myType=container.portal_types.getTypeInfo(folder)
        #context.plone_log(myType.allowType('ExPhoto'))

        if myType.allowType('ExPhoto'):
            photos = folder.parseAlbum(baseurl + '/index%s.html' % i)
            #context.plone_log(photos)
            for p in photos:
                #context.plone_log(p)
                folder.invokeFactory('ExPhoto', id=p)
                data = folder[p].parseImage(baseurl + '/' + p)
                folder[p].edit(id=p, title=p, fileName=p, exifInfo = data[1], excludeFromNav=True)
                ##don't know how to do that yet
                #folder[p].setExcludeFromNav(True)
                #folder[p].update()
                if data[0]:
                    folder[p].edit(description=data[0])
                    folder[p].discussion_reply('comment', data[0])
    except ValueError, inst:
        context.plone_log(inst)
        state.setError('url', inst)
        state.set(status='error')
return state

This parses the original albums pages and generates the new content in plone:

  • in calls the methods from the content which fetch the url and returns a list of images
  • logging can be done for debuging purposes using context.plone_log(…)
  • the content is created using the normal invokeFactory method
  • did not managed to get the photos excluded from navigation after all. I guess it’s a question of which schema it’s inherited, in my case BaseScheme. The excludeFromNav appears in ATDefaultContentType which also inherits from BaseSchema. I tried using it but without success, the property appears but does not work
  • comments are extracted from the image and added as comments to the corresponding image.

A very good tutorial on form was the one here: http://plone.org/documentation/how-to/forms/

About the image data

As far as I understood the JPEG image can contain a lot of information segments and could not find something uniform to extract all:

  • the APP0 segment contains EXIF entries. I used the EXIF library from here to extract EXIF data. Thank you for this code.
  • the COM segment contain comentaries. JAlbum stores comentaries in this part and I wanted to read them also. I achieved this using the PIL library which even if it does not read the EXIF properly reads the comentaries ok
  • IPTC segment contains IPTC data, did not need that

Results

### Comments:

roulette house rules -

I would like to appreciate the efforts you have made in writing this article and i am hoping the same good work from you in the future as well.