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