9cd4d8b1ee755fe456b2d28ae03667a0bacc79d1
[Plinn.git] / Folder.py
1 # -*- coding: utf-8 -*-
2 #######################################################################################
3 # Plinn - http://plinn.org #
4 # Copyright (C) 2005-2007 BenoƮt PIN <benoit.pin@ensmp.fr> #
5 # #
6 # This program is free software; you can redistribute it and/or #
7 # modify it under the terms of the GNU General Public License #
8 # as published by the Free Software Foundation; either version 2 #
9 # of the License, or (at your option) any later version. #
10 # #
11 # This program is distributed in the hope that it will be useful, #
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of #
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
14 # GNU General Public License for more details. #
15 # #
16 # You should have received a copy of the GNU General Public License #
17 # along with this program; if not, write to the Free Software #
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. #
19 #######################################################################################
20 """ Plinn portal folder implementation
21
22 $Id: Folder.py 1459 2009-02-02 00:57:24Z pin $
23 $URL: http://svn.cri.ensmp.fr/svn/Plinn/branches/CMF-2.1/Folder.py $
24 """
25
26 from OFS.CopySupport import CopyError, eNoData, _cb_decode, eInvalid, eNotFound,\
27 eNotSupported, sanity_check, cookie_path
28 from App.Dialogs import MessageDialog
29 import sys
30 import warnings
31 from cgi import escape
32 from OFS import Moniker
33 from ZODB.POSException import ConflictError
34 import OFS.subscribers
35 from zope.event import notify
36 from zope.lifecycleevent import ObjectCopiedEvent
37 from zope.app.container.contained import ObjectMovedEvent
38 from zope.app.container.contained import notifyContainerModified
39 from OFS.event import ObjectClonedEvent
40 from OFS.event import ObjectWillBeMovedEvent
41 from zope.component.factory import Factory
42 from Acquisition import aq_base, aq_inner, aq_parent
43
44 from types import StringType
45 from Products.CMFCore.permissions import ListFolderContents, View, ManageProperties, AddPortalFolders, AddPortalContent, ManagePortal
46 from permissions import DeletePortalContents, DeleteObjects, DeleteOwnedObjects, SetLocalRoles, CheckMemberPermission
47 from Products.CMFCore.utils import _checkPermission, getToolByName
48 from Products.CMFCore.CMFCatalogAware import CMFCatalogAware
49 from Products.CMFCore.PortalFolder import PortalFolder, ContentFilter
50 from Products.CMFDefault.DublinCore import DefaultDublinCoreImpl
51
52 from zope.interface import implements
53 from Products.CMFCore.interfaces import IContentish
54
55 from utils import _checkMemberPermission
56 from Globals import InitializeClass
57 from AccessControl import ClassSecurityInfo
58
59
60 class PlinnFolder(CMFCatalogAware, PortalFolder, DefaultDublinCoreImpl) :
61 """ Plinn Folder """
62
63 implements(IContentish)
64
65 security = ClassSecurityInfo()
66
67 manage_options = PortalFolder.manage_options
68
69 ## change security for inherited methods
70 security.declareProtected(AddPortalContent, 'manage_pasteObjects')
71
72 def __init__( self, id, title='' ) :
73 PortalFolder.__init__(self, id)
74 DefaultDublinCoreImpl.__init__(self, title = title)
75
76 security.declarePublic('allowedContentTypes')
77 def allowedContentTypes(self):
78 """
79 List type info objects for types which can be added in this folder.
80 Types can be filtered using the localContentTypes attribute.
81 """
82 allowedTypes = PortalFolder.allowedContentTypes(self)
83 if hasattr(self, 'localContentTypes'):
84 allowedTypes = [t for t in allowedTypes if t.title in self.localContentTypes]
85 return allowedTypes
86
87 security.declareProtected(View, 'objectIdCanBeDeleted')
88 def objectIdCanBeDeleted(self, id) :
89 """ Check permissions and ownership and return True
90 if current user can delete object id.
91 """
92 if _checkPermission(DeleteObjects, self) : # std zope perm
93 return True
94
95 elif _checkPermission(DeletePortalContents, self):
96 mtool = getToolByName(self, 'portal_membership')
97 authMember = mtool.getAuthenticatedMember()
98 ob = getattr(self, id)
99 if authMember.allowed(ob, object_roles=['Owner'] ) and \
100 _checkPermission(DeleteOwnedObjects, ob) : return True
101
102 else :
103 return False
104
105
106 security.declareProtected(DeletePortalContents, 'manage_delObjects')
107 def manage_delObjects(self, ids=[], REQUEST=None):
108 """Delete a subordinate object.
109 A member can delete his owned contents (if he has the 'Delete Portal Contents' permission)
110 without 'Delete objects' permission in this folder.
111 Return skipped object ids.
112 """
113 notOwned = []
114 if _checkPermission(DeleteObjects, self) : # std zope perm
115 PortalFolder.manage_delObjects(self, ids=ids, REQUEST=REQUEST)
116 else :
117 mtool = getToolByName(self, 'portal_membership')
118 authMember = mtool.getAuthenticatedMember()
119 owned = []
120 if type(ids) == StringType :
121 ids = [ids]
122 for id in ids :
123 ob = self._getOb(id)
124 if authMember.allowed(ob, object_roles=['Owner'] ) and \
125 _checkPermission(DeleteOwnedObjects, ob) : owned.append(id)
126 else : notOwned.append(id)
127 if owned :
128 PortalFolder.manage_delObjects(self, ids=owned, REQUEST=REQUEST)
129
130 if REQUEST is not None:
131 return self.manage_main(
132 self, REQUEST,
133 manage_tabs_message='Object(s) deleted.',
134 update_menu=1)
135 return notOwned
136
137
138 security.declareProtected(ListFolderContents, 'listFolderContents')
139 def listFolderContents( self, contentFilter=None ):
140 """ List viewable contentish and folderish sub-objects.
141 """
142 items = self.contentItems(filter=contentFilter)
143 l = []
144 for id, obj in items:
145 if _checkPermission(View, obj) :
146 l.append(obj)
147
148 return l
149
150
151 security.declareProtected(ListFolderContents, 'listNearestFolderContents')
152 def listNearestFolderContents(self, contentFilter=None, userid=None, sorted=False) :
153 """ Return folder contents and traverse
154 recursively unaccessfull sub folders to find
155 accessible contents.
156 """
157
158 filt = {}
159 if contentFilter :
160 filt = contentFilter.copy()
161 ctool = getToolByName(self, 'portal_catalog')
162 mtool = getToolByName(self, 'portal_membership')
163
164 if userid and _checkPermission(CheckMemberPermission, getToolByName(self, 'portal_url').getPortalObject()) :
165 checkFunc = lambda perm, ob : _checkMemberPermission(userid, View, ob)
166 filt['allowedRolesAndUsers'] = ctool._listAllowedRolesAndUsers( mtool.getMemberById(userid) )
167 else :
168 checkFunc = _checkPermission
169 filt['allowedRolesAndUsers'] = ctool._listAllowedRolesAndUsers( mtool.getAuthenticatedMember() )
170
171
172 # copy from CMFCore.PortalFolder.PortalFolder._filteredItems
173 pt = filt.get('portal_type', [])
174 if type(pt) is type(''):
175 pt = [pt]
176 types_tool = getToolByName(self, 'portal_types')
177 allowed_types = types_tool.listContentTypes()
178 if not pt:
179 pt = allowed_types
180 else:
181 pt = [t for t in pt if t in allowed_types]
182 if not pt:
183 # After filtering, no types remain, so nothing should be
184 # returned.
185 return []
186 filt['portal_type'] = pt
187 #---
188
189 query = ContentFilter(**filt)
190 nearestObjects = []
191
192 for o in self.objectValues() :
193 if query(o) :
194 if checkFunc(View, o):
195 nearestObjects.append(o)
196 elif getattr(o.aq_self,'isAnObjectManager', False):
197 nearestObjects.extend(_getDeepObjects(self, ctool, o, filter=filt))
198
199 if sorted and len(nearestObjects) > 0 :
200 key, reverse = self.getDefaultSorting()
201 if key != 'position' :
202 indexCallable = callable(getattr(nearestObjects[0], key))
203 if indexCallable :
204 sortfunc = lambda a, b : cmp(getattr(a, key)(), getattr(b, key)())
205 else :
206 sortfunc = lambda a, b : cmp(getattr(a, key), getattr(b, key))
207 nearestObjects.sort(cmp=sortfunc, reverse=reverse)
208
209 return nearestObjects
210
211 security.declareProtected(ListFolderContents, 'listCatalogedContents')
212 def listCatalogedContents(self, contentFilter={}):
213 """ query catalog and returns brains of contents.
214 Requires ExtendedPathIndex
215 """
216 ctool = getToolByName(self, 'portal_catalog')
217 contentFilter['path'] = {'query':'/'.join(self.getPhysicalPath()),
218 'depth':1}
219 return ctool(sort_on='position', **contentFilter)
220
221
222 security.declarePublic('synContentValues')
223 def synContentValues(self):
224 # value for syndication
225 return self.listNearestFolderContents()
226
227 security.declareProtected(View, 'SearchableText')
228 def SearchableText(self) :
229 """ for full text indexation
230 """
231 return '%s %s' % (self.title, self.description)
232
233 security.declareProtected(AddPortalFolders, 'manage_addPlinnFolder')
234 def manage_addPlinnFolder(self, id, title='', REQUEST=None):
235 """Add a new PortalFolder object with id *id*.
236 """
237 ob=PlinnFolder(id, title)
238 # from CMFCore.PortalFolder.PortalFolder :-)
239 self._setObject(id, ob)
240 if REQUEST is not None:
241 return self.folder_contents( # XXX: ick!
242 self, REQUEST, portal_status_message="Folder added")
243
244
245 ## overload to maintain ownership if authenticated user has 'Manage portal' permission
246 def manage_pasteObjects(self, cb_copy_data=None, REQUEST=None):
247 """Paste previously copied objects into the current object.
248
249 If calling manage_pasteObjects from python code, pass the result of a
250 previous call to manage_cutObjects or manage_copyObjects as the first
251 argument.
252
253 Also sends IObjectCopiedEvent and IObjectClonedEvent
254 or IObjectWillBeMovedEvent and IObjectMovedEvent.
255 """
256 if cb_copy_data is not None:
257 cp = cb_copy_data
258 elif REQUEST is not None and REQUEST.has_key('__cp'):
259 cp = REQUEST['__cp']
260 else:
261 cp = None
262 if cp is None:
263 raise CopyError, eNoData
264
265 try:
266 op, mdatas = _cb_decode(cp)
267 except:
268 raise CopyError, eInvalid
269
270 oblist = []
271 app = self.getPhysicalRoot()
272 for mdata in mdatas:
273 m = Moniker.loadMoniker(mdata)
274 try:
275 ob = m.bind(app)
276 except ConflictError:
277 raise
278 except:
279 raise CopyError, eNotFound
280 self._verifyObjectPaste(ob, validate_src=op+1)
281 oblist.append(ob)
282
283 result = []
284 if op == 0:
285 # Copy operation
286 mtool = getToolByName(self, 'portal_membership')
287 utool = getToolByName(self, 'portal_url')
288 portal = utool.getPortalObject()
289 userIsPortalManager = mtool.checkPermission(ManagePortal, portal)
290
291 for ob in oblist:
292 orig_id = ob.getId()
293 if not ob.cb_isCopyable():
294 raise CopyError, eNotSupported % escape(orig_id)
295
296 try:
297 ob._notifyOfCopyTo(self, op=0)
298 except ConflictError:
299 raise
300 except:
301 raise CopyError, MessageDialog(
302 title="Copy Error",
303 message=sys.exc_info()[1],
304 action='manage_main')
305
306 id = self._get_id(orig_id)
307 result.append({'id': orig_id, 'new_id': id})
308
309 orig_ob = ob
310 ob = ob._getCopy(self)
311 ob._setId(id)
312 notify(ObjectCopiedEvent(ob, orig_ob))
313
314 if not userIsPortalManager :
315 self._setObject(id, ob, suppress_events=True)
316 else :
317 self._setObject(id, ob, suppress_events=True, set_owner=0)
318 ob = self._getOb(id)
319 ob.wl_clearLocks()
320
321 ob._postCopy(self, op=0)
322
323 OFS.subscribers.compatibilityCall('manage_afterClone', ob, ob)
324
325 notify(ObjectClonedEvent(ob))
326
327 if REQUEST is not None:
328 return self.manage_main(self, REQUEST, update_menu=1,
329 cb_dataValid=1)
330
331 elif op == 1:
332 # Move operation
333 for ob in oblist:
334 orig_id = ob.getId()
335 if not ob.cb_isMoveable():
336 raise CopyError, eNotSupported % escape(orig_id)
337
338 try:
339 ob._notifyOfCopyTo(self, op=1)
340 except ConflictError:
341 raise
342 except:
343 raise CopyError, MessageDialog(
344 title="Move Error",
345 message=sys.exc_info()[1],
346 action='manage_main')
347
348 if not sanity_check(self, ob):
349 raise CopyError, "This object cannot be pasted into itself"
350
351 orig_container = aq_parent(aq_inner(ob))
352 if aq_base(orig_container) is aq_base(self):
353 id = orig_id
354 else:
355 id = self._get_id(orig_id)
356 result.append({'id': orig_id, 'new_id': id})
357
358 notify(ObjectWillBeMovedEvent(ob, orig_container, orig_id,
359 self, id))
360
361 # try to make ownership explicit so that it gets carried
362 # along to the new location if needed.
363 ob.manage_changeOwnershipType(explicit=1)
364
365 try:
366 orig_container._delObject(orig_id, suppress_events=True)
367 except TypeError:
368 orig_container._delObject(orig_id)
369 warnings.warn(
370 "%s._delObject without suppress_events is discouraged."
371 % orig_container.__class__.__name__,
372 DeprecationWarning)
373 ob = aq_base(ob)
374 ob._setId(id)
375
376 try:
377 self._setObject(id, ob, set_owner=0, suppress_events=True)
378 except TypeError:
379 self._setObject(id, ob, set_owner=0)
380 warnings.warn(
381 "%s._setObject without suppress_events is discouraged."
382 % self.__class__.__name__, DeprecationWarning)
383 ob = self._getOb(id)
384
385 notify(ObjectMovedEvent(ob, orig_container, orig_id, self, id))
386 notifyContainerModified(orig_container)
387 if aq_base(orig_container) is not aq_base(self):
388 notifyContainerModified(self)
389
390 ob._postCopy(self, op=1)
391 # try to make ownership implicit if possible
392 ob.manage_changeOwnershipType(explicit=0)
393
394 if REQUEST is not None:
395 REQUEST['RESPONSE'].setCookie('__cp', 'deleted',
396 path='%s' % cookie_path(REQUEST),
397 expires='Wed, 31-Dec-97 23:59:59 GMT')
398 REQUEST['__cp'] = None
399 return self.manage_main(self, REQUEST, update_menu=1,
400 cb_dataValid=0)
401
402 return result
403
404
405 InitializeClass(PlinnFolder)
406 PlinnFolderFactory = Factory(PlinnFolder)
407
408 def _getDeepObjects(self, ctool, o, filter={}):
409 res = ctool.unrestrictedSearchResults(path = '/'.join(o.getPhysicalPath()), **filter)
410
411 if not res :
412 return []
413 else :
414 deepObjects = []
415 res = list(res)
416 res.sort(lambda a, b: cmp(a.getPath(), b.getPath()))
417 previousPath = res[0].getPath()
418
419 deepObjects.append(res[0].getObject())
420 for b in res[1:] :
421 currentPath = b.getPath()
422 if currentPath.startswith(previousPath) and len(currentPath) > len(previousPath):
423 continue
424 else :
425 deepObjects.append(b.getObject())
426 previousPath = currentPath
427
428 return deepObjects
429
430
431 manage_addPlinnFolder = PlinnFolder.manage_addPlinnFolder.im_func