1 # -*- coding: utf-8 -*-
2 ############################################################
3 # Copyright © 2005-2008 Benoît PIN <benoit.pin@ensmp.fr> #
4 # Plinn - http://plinn.org #
6 # This program is free software; you can redistribute it #
7 # and/or modify it under the terms of the Creative Commons #
8 # "Attribution-Noncommercial 2.0 Generic" #
9 # http://creativecommons.org/licenses/by-nc/2.0/ #
10 ############################################################
11 """ Image and Portfolio classes
16 from OFS
.OrderSupport
import OrderSupport
17 from OFS
.Image
import File
18 from AccessControl
import ClassSecurityInfo
, Unauthorized
19 from zExceptions
import NotFound
20 from Products
.CMFDefault
.SkinnedFolder
import SkinnedFolder
21 from Products
.CMFDefault
.DublinCore
import DefaultDublinCoreImpl
22 from Products
.CMFCore
.permissions
import View
, ModifyPortalContent
, ManageProperties
,\
23 ListFolderContents
, AddPortalContent
24 from Products
.Portfolio
.permissions
import ViewRawImage
25 from Products
.CMFCore
.utils
import getToolByName
26 from Globals
import InitializeClass
27 from Products
.CMFCore
.CMFCatalogAware
import CMFCatalogAware
28 from Products
.CMFCore
.DynamicType
import DynamicType
29 from Products
.Photo
.Photo
import Photo
as BasePhoto
31 from webdav
.WriteLockInterface
import WriteLockInterface
as z2IWriteLock
32 from zope
.interface
import implements
33 from Products
.CMFCore
.interfaces
import IContentish
34 from Products
.CMFCore
.interfaces
.Contentish
import Contentish
as z2IContentish
36 from Products
.Photo
.cache
import memoizedmethod
37 from Products
.Photo
.standards
.xmp
import accessors
as xmpAccessors
38 from Products
.Plinn
.Folder
import PlinnFolder
39 from Products
.Plinn
.utils
import makeValidId
41 from DateTime
import DateTime
42 from zipfile
import ZipFile
, ZIP_DEFLATED
43 from cStringIO
import StringIO
44 from unicodedata
import normalize
46 from random
import randrange
47 from logging
import getLogger
48 console
= getLogger('Portfolio')
52 class Portfolio(PlinnFolder
) :
53 """ Container for photos """
55 meta_type
= "Portfolio"
57 security
= ClassSecurityInfo()
59 def __init__( self
, id, title
='' ) :
60 PlinnFolder
.__init
__(self
, id, title
=title
)
61 self
.samplePhotoPath
= None
62 self
.presentation_page
= None
63 self
._randomCandidates
= []
66 security
.declareProtected(ViewRawImage
, 'exportAsZipFile')
67 def exportAsZipFile(self
, REQUEST
, RESPONSE
) :
68 " Export all photos in one zip file "
70 photos
= self
.listNearestFolderContents(contentFilter
={'portal_type' : 'Photo'})
71 ids
= REQUEST
.form
.get('ids')
73 photos
= [ photo
for photo
in photos
if photo
.id in ids
]
75 photos
= [ photo
for photo
in photos
if not photo
.id.startswith('._')]
81 z
= ZipFile(sio
, mode
='w', compression
=ZIP_DEFLATED
)
86 if not (lid
.endswith('.jpg') or lid
.endswith('.jpeg')) and photo
.content_type
== 'image/jpeg' :
88 z
.writestr('%s/%s' % (self
.getId(), id), str(photo
.data
))
92 RESPONSE
.setHeader('Content-Disposition',
93 'attachment; filename=%s' % self
.title_or_id().replace(' ', '_') + '.zip')
95 _v_zopeFile
= File('id', 'title', str(sio
.read()), content_type
='application/zip')
97 return _v_zopeFile
.index_html( REQUEST
, RESPONSE
)
99 security
.declareProtected(AddPortalContent
, 'importZipFile')
100 def importZipFile(self
, file) :
101 " Extracts zip file and constructs recursively Portfolios and Photos "
104 for zi
in z
.filelist
:
105 filepath
= zi
.filename
.split('/')
106 filepath
= map(lambda part
: normalize(NFC
, part
.decode('utf-8')).encode('utf-8'), filepath
)
107 normalizedPath
= map(_normalizeId
, filepath
)
109 if filepath
[0] == '__MACOSX' :
112 elif filepath
[-1] == '' :
114 for nPart
, part
in [ (normalizedPath
[i
], filepath
[i
]) for i
in range(len(filepath
) - 1) ] :
115 container
.invokeFactory('Portfolio', nPart
, title
=part
)
116 container
= getattr(container
, nPart
)
118 elif not filepath
[-1].startswith('.') or filepath
[-1] == 'Thumbs.db' :
120 for part
in normalizedPath
[0:-1] :
121 container
= getattr(container
, part
)
123 container
.invokeFactory('Photo',
126 file=z
.read(zi
.filename
))
128 security
.declareProtected(View
, 'randomPhoto')
129 def randomPhoto(self
):
130 " return a ramdom photo or None "
132 length
= len(self
._randomCandidates
)
134 rid
= self
._randomCandidates
[randrange(length
)]
135 return getattr(self
, rid
)
137 portfolios
= self
.listNearestFolderContents(contentFilter
={'portal_type' : 'Portfolio'})
139 p
= portfolios
.pop(randrange(len(portfolios
)))
140 rphoto
= p
.randomPhoto()
145 security
.declareProtected(ModifyPortalContent
, 'setSamplePhoto')
146 def setSamplePhoto(self
, photoPath
):
147 """ set photo used to represents portfolio content.
149 self
.samplePhotoPath
= photoPath
152 security
.declareProtected(View
, 'samplePhoto')
153 def samplePhoto(self
):
154 """ returns sample photo or random photo if not found.
156 if self
.samplePhotoPath
is None :
157 return self
.randomPhoto()
160 return self
.restrictedTraverse(self
.samplePhotoPath
)
161 except (KeyError, NotFound
, Unauthorized
) :
162 self
.samplePhotoPath
= None
163 return self
.randomPhoto()
165 security
.declareProtected(View
, 'hasPresentationPage')
166 def hasPresentationPage(self
):
167 return self
.presentation_page
is not None
170 security
.declareProtected(ModifyPortalContent
, 'createPresentationPage')
171 def createPresentationPage(self
):
172 #create a presentation page
173 self
.presentation_page
= ''
176 security
.declareProtected(ModifyPortalContent
, 'deletePresentationPage')
177 def deletePresentationPage(self
):
178 self
.presentation_page
= None
182 security
.declareProtected(ModifyPortalContent
, 'editPresentationPage')
183 def editPresentationPage(self
, text
):
184 """editPresentationPage documentation
186 self
.presentation_page
= text
190 security
.declareProtected(View
, 'SearchableText')
191 def SearchableText(self
):
192 base
= PlinnFolder
.SearchableText(self
)
193 if self
.hasPresentationPage() :
194 return '%s %s' % (base
, self
.presentation_page
)
199 def _setObject(self
, id, object, roles
=None, user
=None, set_owner
=1,
200 suppress_events
=False):
201 super_setObject
= super(Portfolio
, self
)._setObject
202 id = super_setObject(id, object, roles
=roles
, user
=user
,
203 set_owner
=set_owner
, suppress_events
=suppress_events
)
205 if object.meta_type
== 'Photo':
206 self
._randomCandidates
.append(id)
210 def _delObject(self
, id, dp
=1, suppress_events
=False):
211 super_delObject
= super(Portfolio
, self
)._delObject
212 super_delObject(id, dp
=dp
, suppress_events
=suppress_events
)
214 self
._randomCandidates
.remove(id)
218 InitializeClass(Portfolio
)
220 def addPortfolio(dispatcher
, id, title
='', REQUEST
=None) :
221 """ Add a new Portfolio """
223 container
= dispatcher
.Destination()
224 pf
= Portfolio(id, title
=title
)
225 container
._setObject
(id, pf
)
227 REQUEST
.RESPONSE
.redirect(dispatcher
.DestinationURL() + 'manage_main')
229 class Photo(DynamicType
, CMFCatalogAware
, BasePhoto
, DefaultDublinCoreImpl
) :
230 """ Photo CMF aware """
232 implements(IContentish
)
233 __implements__
= (z2IContentish
, z2IWriteLock
, DynamicType
.__implements
__)
235 meta_type
= BasePhoto
.meta_type
236 manage_options
= BasePhoto
.manage_options
237 security
= ClassSecurityInfo()
239 security
.declareProtected(ViewRawImage
, 'index_html')
240 security
.declareProtected(ViewRawImage
, 'getJpegImage')
242 def __init__(self
, id, title
, file, content_type
='', precondition
='', **kw
) :
243 BasePhoto
.__init
__(self
, id, title
, file, content_type
=content_type
, precondition
=precondition
, **kw
)
248 self
.creation_date
= now
249 self
.modification_date
= now
251 def update_data(self
, data
, content_type
=None, size
=None, REQUEST
=None) :
252 BasePhoto
.update_data(self
, data
, content_type
=content_type
, size
=size
, REQUEST
=REQUEST
)
256 def _getAfterResizingHooks(self
) :
257 pim
= getToolByName(self
, 'portal_image_manipulation')
258 return pim
.image
.objectValues(['Script (Python)'])
260 def _getAfterTilingHooks(self
) :
261 pim
= getToolByName(self
, 'portal_image_manipulation')
262 return pim
.tile
.objectValues(['Script (Python)'])
265 # Dublin Core interface
268 security
.declareProtected(View
, 'Title')
271 """ returns dc:title from xmp
273 photoshopHeadline
= self
.getXmpValue('photoshop:Headline')
274 dcTitle
= self
.getXmpValue('dc:title')
276 return dcTitle
or photoshopHeadline
279 security
.declareProtected(View
, 'listCreators')
281 def listCreators(self
):
282 """ returns creator from dc:creator from xmp
284 return self
.getXmpValue('dc:creator')
287 security
.declareProtected(View
, 'Description')
289 def Description(self
) :
290 """ returns dc:description from xmp """
291 return self
.getXmpValue('dc:description')
294 security
.declareProtected(View
, 'Subject')
297 """ returns subject from dc:subject from xmp
299 return self
.getXmpValue('dc:subject')
301 security
.declareProtected(View
, 'Rights')
304 """ returns rights from dc:rights from xmp
306 return self
.getXmpValue('dc:rights')
308 security
.declareProtected(ModifyPortalContent
, 'editMetadata')
309 def editMetadata(self
, **kw
):
311 Need to add check for webDAV locked resource for TTW methods.
313 # as per bug #69, we cant assume they use the webdav
314 # locking interface, and fail gracefully if they dont
315 if hasattr(self
, 'failIfLocked'):
318 self
.setXmpFields(**kw
)
319 for name
in ('Title', 'listCreators', 'Description', 'Subject', 'Rights') :
320 self
._clearCacheFor
(name
)
324 def _clearCacheFor(self
, name
) :
326 del self
._methodResultsCache
[name
]
327 except KeyError : pass
330 security
.declareProtected(View
, 'SearchableText')
331 def SearchableText(self
):
332 """ Return textuals metadata"""
333 return '%s %s %s' % ( self
.Title()
335 , ' '.join(self
.Subject()))
337 security
.declareProtected(View
, 'DateTimeOriginal')
339 def DateTimeOriginal(self
) :
340 """ return DateTimeOriginal exif tag value or created """
341 dto
= self
.getXmpValue('exif:DateTimeOriginal')
345 return self
.created()
348 CreationDate
= DefaultDublinCoreImpl
.CreationDate
350 Format
= BasePhoto
.getContentType
353 # SimpleItem interface
356 def title_or_id(self
):
357 """Return the title if it is not blank and the id otherwise.
359 return self
.Title().strip() or self
.getId()
361 def title_and_id(self
):
362 """Return the title if it is not blank and the id otherwise.
364 If the title is not blank, then the id is included in parens.
368 return title
and ("%s (%s)" % (title
,id)) or id
371 InitializeClass(Photo
)
373 def addPhoto(dispatcher
, id, title
='', file='', content_type
='', REQUEST
=None) :
376 container
= dispatcher
.Destination()
377 portal
= getToolByName(container
, 'portal_url').getPortalObject()
378 thumb_height
= portal
.getProperty('thumb_height', 192)
379 thumb_width
= portal
.getProperty('thumb_width', 192)
380 p
= Photo(id, title
=title
, file='',
381 content_type
=content_type
,
382 thumb_height
=thumb_height
, thumb_width
=thumb_width
)
383 container
._setObject
(id, p
)
386 p
.manage_upload(file)
389 REQUEST
.RESPONSE
.redirect(dispatcher
.DestinationURL() + 'manage_main')
392 def _normalizeId(id) :
393 return makeValidId(None, id, allow_dup
=1)