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