Create a product to import existing photos as external resources
Import existing photo album as external resources by creating a ExPhotoAlbum product
Original article can be found here: http://www.len.ro/work/cms/import-existing-photo-album-as-external-resources
The goal
After migrating my website at www.len.ro 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
The UML schema


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.xmiThis 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"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:
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>
- 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"Some observations apply:
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>
- 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"The metadata for this controller template defines the validators and the actions:
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 existing 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>
[validators]
validators = importPhotoList_validate
[actions]
action.success=redirect_to:python:folder.absolute_url()
action.error=traverse_to:string:importPhotoList
- 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)
import urllib, reThis parses the original albums pages and generates the new content in plone:
from Products.CMFCore.utils import getToolByName
url = context.REQUEST.get('url')
baseurl = url
if url.endswith(".html"):
baseurl = '/'.join(url.split('/')[:-1])
#import 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
- 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
- you can download the product here
- you can download the UML here
