87f959e5c5c6ba42560f1a563b7059d04a974c90
[Portfolio.git] / deprecated / Portfolio.py
1 # -*- coding: utf-8 -*-
2 ############################################################
3 # Copyright © 2005-2008 Benoît PIN <benoit.pin@ensmp.fr> #
4 # Plinn - http://plinn.org #
5 # #
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
12
13
14 """
15
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
30
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
35
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
40
41 from DateTime import DateTime
42 from zipfile import ZipFile, ZIP_DEFLATED
43 from cStringIO import StringIO
44 from unicodedata import normalize
45 NFC = 'NFC'
46 from random import randrange
47 from logging import getLogger
48 console = getLogger('Portfolio')
49
50 _marker = []
51
52 class Portfolio(PlinnFolder) :
53 """ Container for photos """
54
55 meta_type = "Portfolio"
56
57 security = ClassSecurityInfo()
58
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 = []
64
65
66 security.declareProtected(ViewRawImage, 'exportAsZipFile')
67 def exportAsZipFile(self, REQUEST, RESPONSE) :
68 " Export all photos in one zip file "
69
70 photos = self.listNearestFolderContents(contentFilter={'portal_type' : 'Photo'})
71 ids = REQUEST.form.get('ids')
72 if ids :
73 photos = [ photo for photo in photos if photo.id in ids ]
74
75 photos = [ photo for photo in photos if not photo.id.startswith('._')]
76
77 if not photos :
78 return
79
80 sio = StringIO()
81 z = ZipFile(sio, mode='w', compression=ZIP_DEFLATED)
82
83 for photo in photos :
84 id = photo.id
85 lid = id.lower()
86 if not (lid.endswith('.jpg') or lid.endswith('.jpeg')) and photo.content_type == 'image/jpeg' :
87 id += '.jpg'
88 z.writestr('%s/%s' % (self.getId(), id), str(photo.data))
89 z.close()
90 sio.seek(0)
91
92 RESPONSE.setHeader('Content-Disposition',
93 'attachment; filename=%s' % self.title_or_id().replace(' ', '_') + '.zip')
94
95 _v_zopeFile = File('id', 'title', str(sio.read()), content_type='application/zip')
96
97 return _v_zopeFile.index_html( REQUEST, RESPONSE)
98
99 security.declareProtected(AddPortalContent, 'importZipFile')
100 def importZipFile(self, file) :
101 " Extracts zip file and constructs recursively Portfolios and Photos "
102
103 z = ZipFile(file)
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)
108
109 if filepath[0] == '__MACOSX' :
110 continue
111
112 elif filepath[-1] == '' :
113 container = self
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)
117
118 elif not filepath[-1].startswith('.') or filepath[-1] == 'Thumbs.db' :
119 container = self
120 for part in normalizedPath[0:-1] :
121 container = getattr(container, part)
122
123 container.invokeFactory('Photo',
124 normalizedPath[-1],
125 title=filepath[-1],
126 file=z.read(zi.filename))
127
128 security.declareProtected(View, 'randomPhoto')
129 def randomPhoto(self):
130 " return a ramdom photo or None "
131
132 length = len(self._randomCandidates)
133 if length :
134 rid = self._randomCandidates[randrange(length)]
135 return getattr(self, rid)
136 else :
137 portfolios = self.listNearestFolderContents(contentFilter={'portal_type' : 'Portfolio'})
138 while portfolios :
139 p = portfolios.pop(randrange(len(portfolios)))
140 rphoto = p.randomPhoto()
141 if rphoto :
142 return rphoto
143 return None
144
145 security.declareProtected(ModifyPortalContent, 'setSamplePhoto')
146 def setSamplePhoto(self, photoPath):
147 """ set photo used to represents portfolio content.
148 """
149 self.samplePhotoPath = photoPath
150 return True
151
152 security.declareProtected(View, 'samplePhoto')
153 def samplePhoto(self):
154 """ returns sample photo or random photo if not found.
155 """
156 if self.samplePhotoPath is None :
157 return self.randomPhoto()
158 else :
159 try :
160 return self.restrictedTraverse(self.samplePhotoPath)
161 except (KeyError, NotFound, Unauthorized) :
162 self.samplePhotoPath = None
163 return self.randomPhoto()
164
165 security.declareProtected(View, 'hasPresentationPage')
166 def hasPresentationPage(self):
167 return self.presentation_page is not None
168
169
170 security.declareProtected(ModifyPortalContent, 'createPresentationPage')
171 def createPresentationPage(self):
172 #create a presentation page
173 self.presentation_page = ''
174 return True
175
176 security.declareProtected(ModifyPortalContent, 'deletePresentationPage')
177 def deletePresentationPage(self):
178 self.presentation_page = None
179 return True
180
181
182 security.declareProtected(ModifyPortalContent, 'editPresentationPage')
183 def editPresentationPage(self, text):
184 """editPresentationPage documentation
185 """
186 self.presentation_page = text
187 self.reindexObject()
188 return True
189
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)
195 else :
196 return base
197
198
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)
204
205 if object.meta_type == 'Photo':
206 self._randomCandidates.append(id)
207
208 return id
209
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)
213 try :
214 self._randomCandidates.remove(id)
215 except ValueError:
216 pass
217
218 InitializeClass(Portfolio)
219
220 def addPortfolio(dispatcher, id, title='', REQUEST=None) :
221 """ Add a new Portfolio """
222
223 container = dispatcher.Destination()
224 pf = Portfolio(id, title=title)
225 container._setObject(id, pf)
226 if REQUEST :
227 REQUEST.RESPONSE.redirect(dispatcher.DestinationURL() + 'manage_main')
228
229 class Photo(DynamicType, CMFCatalogAware, BasePhoto, DefaultDublinCoreImpl) :
230 """ Photo CMF aware """
231
232 implements(IContentish)
233 __implements__ = (z2IContentish, z2IWriteLock, DynamicType.__implements__)
234
235 meta_type = BasePhoto.meta_type
236 manage_options = BasePhoto.manage_options
237 security = ClassSecurityInfo()
238
239 security.declareProtected(ViewRawImage, 'index_html')
240 security.declareProtected(ViewRawImage, 'getJpegImage')
241
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)
244 self.id = id
245 self.title = title
246
247 now = DateTime()
248 self.creation_date = now
249 self.modification_date = now
250
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)
253 self.reindexObject()
254
255
256 def _getAfterResizingHooks(self) :
257 pim = getToolByName(self, 'portal_image_manipulation')
258 return pim.image.objectValues(['Script (Python)'])
259
260 def _getAfterTilingHooks(self) :
261 pim = getToolByName(self, 'portal_image_manipulation')
262 return pim.tile.objectValues(['Script (Python)'])
263
264 #
265 # Dublin Core interface
266 #
267
268 security.declareProtected(View, 'Title')
269 @memoizedmethod()
270 def Title(self):
271 """ returns dc:title from xmp
272 """
273 photoshopHeadline = self.getXmpValue('photoshop:Headline')
274 dcTitle = self.getXmpValue('dc:title')
275
276 return dcTitle or photoshopHeadline
277
278
279 security.declareProtected(View, 'listCreators')
280 @memoizedmethod()
281 def listCreators(self):
282 """ returns creator from dc:creator from xmp
283 """
284 return self.getXmpValue('dc:creator')
285
286
287 security.declareProtected(View, 'Description')
288 @memoizedmethod()
289 def Description(self) :
290 """ returns dc:description from xmp """
291 return self.getXmpValue('dc:description')
292
293
294 security.declareProtected(View, 'Subject')
295 @memoizedmethod()
296 def Subject(self):
297 """ returns subject from dc:subject from xmp
298 """
299 return self.getXmpValue('dc:subject')
300
301 security.declareProtected(View, 'Rights')
302 @memoizedmethod()
303 def Rights(self):
304 """ returns rights from dc:rights from xmp
305 """
306 return self.getXmpValue('dc:rights')
307
308 security.declareProtected(ModifyPortalContent, 'editMetadata')
309 def editMetadata(self, **kw):
310 """
311 Need to add check for webDAV locked resource for TTW methods.
312 """
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'):
316 self.failIfLocked()
317
318 self.setXmpFields(**kw)
319 for name in ('Title', 'listCreators', 'Description', 'Subject', 'Rights') :
320 self._clearCacheFor(name)
321 self.reindexObject()
322
323
324 def _clearCacheFor(self, name) :
325 try :
326 del self._methodResultsCache[name]
327 except KeyError : pass
328
329
330 security.declareProtected(View, 'SearchableText')
331 def SearchableText(self):
332 """ Return textuals metadata"""
333 return '%s %s %s' % ( self.Title()
334 , self.Description()
335 , ' '.join(self.Subject()))
336
337 security.declareProtected(View, 'DateTimeOriginal')
338 @memoizedmethod()
339 def DateTimeOriginal(self) :
340 """ return DateTimeOriginal exif tag value or created """
341 dto = self.getXmpValue('exif:DateTimeOriginal')
342 if dto :
343 return DateTime(dto)
344 else :
345 return self.created()
346
347
348 CreationDate = DefaultDublinCoreImpl.CreationDate
349
350 Format = BasePhoto.getContentType
351
352 #
353 # SimpleItem interface
354 #
355
356 def title_or_id(self):
357 """Return the title if it is not blank and the id otherwise.
358 """
359 return self.Title().strip() or self.getId()
360
361 def title_and_id(self):
362 """Return the title if it is not blank and the id otherwise.
363
364 If the title is not blank, then the id is included in parens.
365 """
366 title = self.Title()
367 id = self.getId()
368 return title and ("%s (%s)" % (title,id)) or id
369
370
371 InitializeClass(Photo)
372
373 def addPhoto(dispatcher, id, title='', file='', content_type='', REQUEST=None) :
374 """Add new Photo"""
375
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)
384
385 if file :
386 p.manage_upload(file)
387
388 if REQUEST :
389 REQUEST.RESPONSE.redirect(dispatcher.DestinationURL() + 'manage_main')
390
391
392 def _normalizeId(id) :
393 return makeValidId(None, id, allow_dup=1)