bugfix : on vérifie que le parent implémente bien l'interface d'ordonancement.
[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
23
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 from zExceptions import BadRequest
30 import sys
31 import warnings
32 from cgi import escape
33 from OFS import Moniker
34 from ZODB.POSException import ConflictError
35 import OFS.subscribers
36 from webdav.NullResource import NullResource
37 from zope.event import notify
38 from zope.lifecycleevent import ObjectCopiedEvent
39 try :
40 from zope.app.container.contained import notifyContainerModified
41 from zope.app.container.contained import ObjectMovedEvent
42 except ImportError :
43 ## Zope-2.13 compat
44 from zope.container.contained import notifyContainerModified
45 from zope.container.contained import ObjectMovedEvent
46 from OFS.event import ObjectClonedEvent
47 from OFS.event import ObjectWillBeMovedEvent
48 from zope.component.factory import Factory
49 from Acquisition import aq_base, aq_inner, aq_parent
50
51 from types import StringType, NoneType
52 from Products.CMFCore.permissions import ListFolderContents, View, ViewManagementScreens,\
53 ManageProperties, AddPortalFolders, AddPortalContent,\
54 ManagePortal, ModifyPortalContent
55 from permissions import DeletePortalContents, DeleteObjects, DeleteOwnedObjects, SetLocalRoles, CheckMemberPermission
56 from Products.CMFCore.utils import _checkPermission, getToolByName
57 from Products.CMFCore.utils import getUtilityByInterfaceName
58 from Products.CMFCore.CMFCatalogAware import CMFCatalogAware
59 from Products.CMFCore.PortalFolder import PortalFolder, ContentFilter
60 from Products.CMFDefault.DublinCore import DefaultDublinCoreImpl
61
62 from zope.interface import implements
63 from Products.CMFCore.interfaces import IContentish
64
65 from utils import _checkMemberPermission
66 from utils import Message as _
67 from utils import makeValidId
68 from Globals import InitializeClass
69 from AccessControl import ClassSecurityInfo
70
71
72 class PlinnFolder(CMFCatalogAware, PortalFolder, DefaultDublinCoreImpl) :
73 """ Plinn Folder """
74
75 implements(IContentish)
76
77 security = ClassSecurityInfo()
78
79 manage_options = PortalFolder.manage_options
80
81 ## change security for inherited methods
82 security.declareProtected(AddPortalContent, 'manage_pasteObjects')
83
84 def __init__( self, id, title='' ) :
85 PortalFolder.__init__(self, id)
86 DefaultDublinCoreImpl.__init__(self, title = title)
87
88 def __getitem__(self, key):
89 if key in self:
90 return self._getOb(key, None)
91 request = getattr(self, 'REQUEST', None)
92 if not isinstance(request, (str, NoneType)):
93 method=request.get('REQUEST_METHOD', 'GET')
94 if (request.maybe_webdav_client and
95 method not in ('GET', 'POST')):
96 id = makeValidId(self, key)
97 return NullResource(self, id, request).__of__(self)
98 raise KeyError, key
99
100
101 security.declarePublic('allowedContentTypes')
102 def allowedContentTypes(self):
103 """
104 List type info objects for types which can be added in this folder.
105 Types can be filtered using the localContentTypes attribute.
106 """
107 allowedTypes = PortalFolder.allowedContentTypes(self)
108 if hasattr(self, 'localContentTypes'):
109 allowedTypes = [t for t in allowedTypes if t.title in self.localContentTypes]
110 return allowedTypes
111
112 security.declareProtected(View, 'objectIdCanBeDeleted')
113 def objectIdCanBeDeleted(self, id) :
114 """ Check permissions and ownership and return True
115 if current user can delete object id.
116 """
117 if _checkPermission(DeleteObjects, self) : # std zope perm
118 return True
119
120 elif _checkPermission(DeletePortalContents, self):
121 mtool = getToolByName(self, 'portal_membership')
122 authMember = mtool.getAuthenticatedMember()
123 ob = getattr(self, id)
124 if authMember.allowed(ob, object_roles=['Owner'] ) and \
125 _checkPermission(DeleteOwnedObjects, ob) : return True
126
127 else :
128 return False
129
130
131 security.declareProtected(DeletePortalContents, 'manage_delObjects')
132 def manage_delObjects(self, ids=[], REQUEST=None):
133 """Delete subordinate objects.
134 A member can delete his owned contents (if he has the 'Delete Portal Contents' permission)
135 without 'Delete objects' permission in this folder.
136 Return skipped object ids.
137 """
138 notOwned = []
139 if _checkPermission(DeleteObjects, self) : # std zope perm
140 PortalFolder.manage_delObjects(self, ids=ids, REQUEST=REQUEST)
141 else :
142 mtool = getToolByName(self, 'portal_membership')
143 authMember = mtool.getAuthenticatedMember()
144 owned = []
145 if type(ids) == StringType :
146 ids = [ids]
147 for id in ids :
148 ob = self._getOb(id)
149 if authMember.allowed(ob, object_roles=['Owner'] ) and \
150 _checkPermission(DeleteOwnedObjects, ob) : owned.append(id)
151 else : notOwned.append(id)
152 if owned :
153 PortalFolder.manage_delObjects(self, ids=owned, REQUEST=REQUEST)
154
155 if REQUEST is not None:
156 return self.manage_main(
157 self, REQUEST,
158 manage_tabs_message='Object(s) deleted.',
159 update_menu=1)
160 return notOwned
161
162
163 security.declareProtected(AddPortalContent, 'manage_renameObjects')
164 def manage_renameObjects(self, ids=[], new_ids=[], REQUEST=None) :
165 """ Rename subordinate objects
166 A member can rename his owned contents if he has the 'Modify Portal Content' permission.
167 Returns skippend object ids.
168 """
169 if len(ids) != len(new_ids):
170 raise BadRequest(_('Please rename each listed object.'))
171
172 if _checkPermission(ViewManagementScreens, self) : # std zope perm
173 return super(PlinnFolder, self).manage_renameObjects(ids, new_ids, REQUEST)
174
175 mtool = getToolByName(self, 'portal_membership')
176 authMember = mtool.getAuthenticatedMember()
177 skiped = []
178 for id, new_id in zip(ids, new_ids) :
179 if id == new_id : continue
180
181 ob = self._getOb(id)
182 if authMember.allowed(ob, object_roles=['Owner'] ) and \
183 _checkPermission(ModifyPortalContent, ob) :
184 self.manage_renameObject(id, new_id)
185 else :
186 skiped.append(id)
187
188 if REQUEST is not None :
189 return self.manage_main(self, REQUEST, update_menu=1)
190
191 return skiped
192
193
194 security.declareProtected(ListFolderContents, 'listFolderContents')
195 def listFolderContents( self, contentFilter=None ):
196 """ List viewable contentish and folderish sub-objects.
197 """
198 items = self.contentItems(filter=contentFilter)
199 l = []
200 for id, obj in items:
201 if _checkPermission(View, obj) :
202 l.append(obj)
203
204 return l
205
206
207 security.declareProtected(ListFolderContents, 'listNearestFolderContents')
208 def listNearestFolderContents(self, contentFilter=None, userid=None, sorted=False) :
209 """ Return folder contents and traverse
210 recursively unaccessfull sub folders to find
211 accessible contents.
212 """
213
214 filt = {}
215 if contentFilter :
216 filt = contentFilter.copy()
217 ctool = getToolByName(self, 'portal_catalog')
218 mtool = getToolByName(self, 'portal_membership')
219
220 if userid and _checkPermission(CheckMemberPermission, getToolByName(self, 'portal_url').getPortalObject()) :
221 checkFunc = lambda perm, ob : _checkMemberPermission(userid, View, ob)
222 filt['allowedRolesAndUsers'] = ctool._listAllowedRolesAndUsers( mtool.getMemberById(userid) )
223 else :
224 checkFunc = _checkPermission
225 filt['allowedRolesAndUsers'] = ctool._listAllowedRolesAndUsers( mtool.getAuthenticatedMember() )
226
227
228 # copy from CMFCore.PortalFolder.PortalFolder._filteredItems
229 pt = filt.get('portal_type', [])
230 if type(pt) is type(''):
231 pt = [pt]
232 types_tool = getToolByName(self, 'portal_types')
233 allowed_types = types_tool.listContentTypes()
234 if not pt:
235 pt = allowed_types
236 else:
237 pt = [t for t in pt if t in allowed_types]
238 if not pt:
239 # After filtering, no types remain, so nothing should be
240 # returned.
241 return []
242 filt['portal_type'] = pt
243 #---
244
245 query = ContentFilter(**filt)
246 nearestObjects = []
247
248 for o in self.objectValues() :
249 if query(o) :
250 if checkFunc(View, o):
251 nearestObjects.append(o)
252 elif getattr(o.aq_self,'isAnObjectManager', False):
253 nearestObjects.extend(_getDeepObjects(self, ctool, o, filter=filt))
254
255 if sorted and len(nearestObjects) > 0 :
256 key, reverse = self.getDefaultSorting()
257 if key != 'position' :
258 indexCallable = callable(getattr(nearestObjects[0], key))
259 if indexCallable :
260 sortfunc = lambda a, b : cmp(getattr(a, key)(), getattr(b, key)())
261 else :
262 sortfunc = lambda a, b : cmp(getattr(a, key), getattr(b, key))
263 nearestObjects.sort(cmp=sortfunc, reverse=reverse)
264
265 return nearestObjects
266
267 security.declareProtected(ListFolderContents, 'listCatalogedContents')
268 def listCatalogedContents(self, contentFilter={}):
269 """ query catalog and returns brains of contents.
270 Requires ExtendedPathIndex
271 """
272 ctool = getUtilityByInterfaceName('Products.CMFCore.interfaces.ICatalogTool')
273 contentFilter['path'] = {'query':'/'.join(self.getPhysicalPath()),
274 'depth':1}
275 return ctool(sort_on='position', **contentFilter)
276
277 security.declarePublic('synContentValues')
278 def synContentValues(self):
279 # value for syndication
280 return self.listNearestFolderContents()
281
282 security.declareProtected(View, 'SearchableText')
283 def SearchableText(self) :
284 """ for full text indexation
285 """
286 return '%s %s' % (self.title, self.description)
287
288 security.declareProtected(AddPortalFolders, 'manage_addPlinnFolder')
289 def manage_addPlinnFolder(self, id, title='', REQUEST=None):
290 """Add a new PortalFolder object with id *id*.
291 """
292 ob=PlinnFolder(id, title)
293 # from CMFCore.PortalFolder.PortalFolder :-)
294 self._setObject(id, ob)
295 if REQUEST is not None:
296 return self.folder_contents( # XXX: ick!
297 self, REQUEST, portal_status_message="Folder added")
298
299
300 # ## overload to maintain ownership if authenticated user has 'Manage portal' permission
301 # def manage_pasteObjects(self, cb_copy_data=None, REQUEST=None):
302 # """Paste previously copied objects into the current object.
303 #
304 # If calling manage_pasteObjects from python code, pass the result of a
305 # previous call to manage_cutObjects or manage_copyObjects as the first
306 # argument.
307 #
308 # Also sends IObjectCopiedEvent and IObjectClonedEvent
309 # or IObjectWillBeMovedEvent and IObjectMovedEvent.
310 # """
311 # if cb_copy_data is not None:
312 # cp = cb_copy_data
313 # elif REQUEST is not None and REQUEST.has_key('__cp'):
314 # cp = REQUEST['__cp']
315 # else:
316 # cp = None
317 # if cp is None:
318 # raise CopyError, eNoData
319 #
320 # try:
321 # op, mdatas = _cb_decode(cp)
322 # except:
323 # raise CopyError, eInvalid
324 #
325 # oblist = []
326 # app = self.getPhysicalRoot()
327 # for mdata in mdatas:
328 # m = Moniker.loadMoniker(mdata)
329 # try:
330 # ob = m.bind(app)
331 # except ConflictError:
332 # raise
333 # except:
334 # raise CopyError, eNotFound
335 # self._verifyObjectPaste(ob, validate_src=op+1)
336 # oblist.append(ob)
337 #
338 # result = []
339 # if op == 0:
340 # # Copy operation
341 # mtool = getToolByName(self, 'portal_membership')
342 # utool = getToolByName(self, 'portal_url')
343 # portal = utool.getPortalObject()
344 # userIsPortalManager = mtool.checkPermission(ManagePortal, portal)
345 #
346 # for ob in oblist:
347 # orig_id = ob.getId()
348 # if not ob.cb_isCopyable():
349 # raise CopyError, eNotSupported % escape(orig_id)
350 #
351 # try:
352 # ob._notifyOfCopyTo(self, op=0)
353 # except ConflictError:
354 # raise
355 # except:
356 # raise CopyError, MessageDialog(
357 # title="Copy Error",
358 # message=sys.exc_info()[1],
359 # action='manage_main')
360 #
361 # id = self._get_id(orig_id)
362 # result.append({'id': orig_id, 'new_id': id})
363 #
364 # orig_ob = ob
365 # ob = ob._getCopy(self)
366 # ob._setId(id)
367 # notify(ObjectCopiedEvent(ob, orig_ob))
368 #
369 # if not userIsPortalManager :
370 # self._setObject(id, ob, suppress_events=True)
371 # else :
372 # self._setObject(id, ob, suppress_events=True, set_owner=0)
373 # ob = self._getOb(id)
374 # ob.wl_clearLocks()
375 #
376 # ob._postCopy(self, op=0)
377 #
378 # OFS.subscribers.compatibilityCall('manage_afterClone', ob, ob)
379 #
380 # notify(ObjectClonedEvent(ob))
381 #
382 # if REQUEST is not None:
383 # return self.manage_main(self, REQUEST, update_menu=1,
384 # cb_dataValid=1)
385 #
386 # elif op == 1:
387 # # Move operation
388 # for ob in oblist:
389 # orig_id = ob.getId()
390 # if not ob.cb_isMoveable():
391 # raise CopyError, eNotSupported % escape(orig_id)
392 #
393 # try:
394 # ob._notifyOfCopyTo(self, op=1)
395 # except ConflictError:
396 # raise
397 # except:
398 # raise CopyError, MessageDialog(
399 # title="Move Error",
400 # message=sys.exc_info()[1],
401 # action='manage_main')
402 #
403 # if not sanity_check(self, ob):
404 # raise CopyError, "This object cannot be pasted into itself"
405 #
406 # orig_container = aq_parent(aq_inner(ob))
407 # if aq_base(orig_container) is aq_base(self):
408 # id = orig_id
409 # else:
410 # id = self._get_id(orig_id)
411 # result.append({'id': orig_id, 'new_id': id})
412 #
413 # notify(ObjectWillBeMovedEvent(ob, orig_container, orig_id,
414 # self, id))
415 #
416 # # try to make ownership explicit so that it gets carried
417 # # along to the new location if needed.
418 # ob.manage_changeOwnershipType(explicit=1)
419 #
420 # try:
421 # orig_container._delObject(orig_id, suppress_events=True)
422 # except TypeError:
423 # orig_container._delObject(orig_id)
424 # warnings.warn(
425 # "%s._delObject without suppress_events is discouraged."
426 # % orig_container.__class__.__name__,
427 # DeprecationWarning)
428 # ob = aq_base(ob)
429 # ob._setId(id)
430 #
431 # try:
432 # self._setObject(id, ob, set_owner=0, suppress_events=True)
433 # except TypeError:
434 # self._setObject(id, ob, set_owner=0)
435 # warnings.warn(
436 # "%s._setObject without suppress_events is discouraged."
437 # % self.__class__.__name__, DeprecationWarning)
438 # ob = self._getOb(id)
439 #
440 # notify(ObjectMovedEvent(ob, orig_container, orig_id, self, id))
441 # notifyContainerModified(orig_container)
442 # if aq_base(orig_container) is not aq_base(self):
443 # notifyContainerModified(self)
444 #
445 # ob._postCopy(self, op=1)
446 # # try to make ownership implicit if possible
447 # ob.manage_changeOwnershipType(explicit=0)
448 #
449 # if REQUEST is not None:
450 # REQUEST['RESPONSE'].setCookie('__cp', 'deleted',
451 # path='%s' % cookie_path(REQUEST),
452 # expires='Wed, 31-Dec-97 23:59:59 GMT')
453 # REQUEST['__cp'] = None
454 # return self.manage_main(self, REQUEST, update_menu=1,
455 # cb_dataValid=0)
456 #
457 # return result
458
459
460 InitializeClass(PlinnFolder)
461 PlinnFolderFactory = Factory(PlinnFolder)
462
463 def _getDeepObjects(self, ctool, o, filter={}):
464 res = ctool.unrestrictedSearchResults(path = '/'.join(o.getPhysicalPath()), **filter)
465
466 if not res :
467 return []
468 else :
469 deepObjects = []
470 res = list(res)
471 res.sort(lambda a, b: cmp(a.getPath(), b.getPath()))
472 previousPath = res[0].getPath()
473
474 deepObjects.append(res[0].getObject())
475 for b in res[1:] :
476 currentPath = b.getPath()
477 if currentPath.startswith(previousPath) and len(currentPath) > len(previousPath):
478 continue
479 else :
480 deepObjects.append(b.getObject())
481 previousPath = currentPath
482
483 return deepObjects
484
485
486 manage_addPlinnFolder = PlinnFolder.manage_addPlinnFolder.im_func