vendor/CMF/1.6.3/CMFCore

view utils.py @ 2:4c712d7bd1d7

Added tag 1.6.3 for changeset 1babb9d61518
author Georges Racinet on purity.racinet.fr <georges@racinet.fr>
date Fri, 09 Sep 2011 12:44:00 +0200
parents
children
line source
1 ##############################################################################
2 #
3 # Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
4 #
5 # This software is subject to the provisions of the Zope Public License,
6 # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
7 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
8 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
9 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
10 # FOR A PARTICULAR PURPOSE.
11 #
12 ##############################################################################
13 """ Utility functions.
15 $Id$
16 """
18 from os import path as os_path
19 from os.path import abspath
20 import re
21 from warnings import warn
22 from copy import deepcopy
24 from AccessControl import ClassSecurityInfo
25 from AccessControl import getSecurityManager
26 from AccessControl import ModuleSecurityInfo
27 from AccessControl.Permission import Permission
28 from AccessControl.PermissionRole import rolesForPermissionOn
29 from AccessControl.Role import gather_permissions
30 from Acquisition import aq_get
31 from Acquisition import aq_inner
32 from Acquisition import aq_parent
33 from Acquisition import Implicit
34 from DateTime import DateTime
35 from ExtensionClass import Base
36 from Globals import HTMLFile
37 from Globals import ImageFile
38 from Globals import InitializeClass
39 from Globals import MessageDialog
40 from Globals import package_home
41 from Globals import UNIQUE
42 from OFS.misc_ import misc_ as misc_images
43 from OFS.misc_ import Misc_ as MiscImage
44 from OFS.PropertyManager import PropertyManager
45 from OFS.PropertySheets import PropertySheets
46 from OFS.SimpleItem import SimpleItem
47 from Products.PageTemplates.Expressions import getEngine
48 from Products.PageTemplates.Expressions import SecureModuleImporter
49 from StructuredText.StructuredText import HTML
50 from thread import allocate_lock
51 from webdav.common import rfc1123_date
52 from exceptions import AccessControl_Unauthorized
53 from exceptions import NotFound
55 security = ModuleSecurityInfo( 'Products.CMFCore.utils' )
57 _dtmldir = os_path.join( package_home( globals() ), 'dtml' )
58 _wwwdir = os_path.join( package_home( globals() ), 'www' )
60 #
61 # Simple utility functions, callable from restricted code.
62 #
63 _marker = [] # Create a new marker object.
65 security.declarePublic('getToolByName')
66 def getToolByName(obj, name, default=_marker):
68 """ Get the tool, 'toolname', by acquiring it.
70 o Application code should use this method, rather than simply
71 acquiring the tool by name, to ease forward migration (e.g.,
72 to Zope3).
73 """
74 try:
75 tool = aq_get(obj, name, default, 1)
76 except AttributeError:
77 if default is _marker:
78 raise
79 return default
80 else:
81 if tool is _marker:
82 raise AttributeError, name
83 return tool
85 security.declarePublic('cookString')
86 def cookString(text):
88 """ Make a Zope-friendly ID from 'text'.
90 o Remove any spaces
92 o Lowercase the ID.
93 """
94 rgx = re.compile(r'(^_|[^a-zA-Z0-9-_~\,\.])')
95 cooked = re.sub(rgx, "",text).lower()
96 return cooked
98 security.declarePublic('tuplize')
99 def tuplize( valueName, value ):
101 """ Make a tuple from 'value'.
103 o Use 'valueName' to generate appropriate error messages.
104 """
105 if isinstance(value, tuple):
106 return value
107 if isinstance(value, list):
108 return tuple( value )
109 if isinstance(value, basestring):
110 return tuple( value.split() )
111 raise ValueError, "%s of unsupported type" % valueName
113 #
114 # Security utilities, callable only from unrestricted code.
115 #
116 security.declarePrivate('_getAuthenticatedUser')
117 def _getAuthenticatedUser(self):
118 return getSecurityManager().getUser()
120 security.declarePrivate('_checkPermission')
121 def _checkPermission(permission, obj):
122 return getSecurityManager().checkPermission(permission, obj)
124 security.declarePrivate('_verifyActionPermissions')
125 def _verifyActionPermissions(obj, action):
126 # _verifyActionPermissions is deprecated and will be removed in CMF 2.0.
127 # This was only used by the deprecated _getViewFor function.
128 pp = action.getPermissions()
129 if not pp:
130 return 1
131 for p in pp:
132 if _checkPermission(p, obj):
133 return 1
134 return 0
136 security.declarePublic( 'getActionContext' )
137 def getActionContext( self ):
138 # getActionContext is deprecated and will be removed as soon as the
139 # backwards compatibility code in TypeInformation._guessMethodAliases is
140 # removed.
141 data = { 'object_url' : ''
142 , 'folder_url' : ''
143 , 'portal_url' : ''
144 , 'object' : None
145 , 'folder' : None
146 , 'portal' : None
147 , 'nothing' : None
148 , 'request' : getattr( self, 'REQUEST', None )
149 , 'modules' : SecureModuleImporter
150 , 'member' : None
151 }
152 return getEngine().getContext( data )
154 security.declarePrivate('_getViewFor')
155 def _getViewFor(obj, view='view'):
156 warn('__call__() and view() methods using _getViewFor() as well as '
157 '_getViewFor() itself are deprecated and will be removed in CMF 2.0. '
158 'Bypass these methods by defining \'(Default)\' and \'view\' Method '
159 'Aliases.',
160 DeprecationWarning)
161 ti = obj.getTypeInfo()
163 if ti is not None:
165 context = getActionContext( obj )
166 actions = ti.listActions()
168 for action in actions:
169 if action.getId() == view:
170 if _verifyActionPermissions( obj, action ):
171 target = action.action(context).strip()
172 if target.startswith('/'):
173 target = target[1:]
174 __traceback_info__ = ( ti.getId(), target )
175 return obj.restrictedTraverse( target )
177 # "view" action is not present or not allowed.
178 # Find something that's allowed.
179 for action in actions:
180 if _verifyActionPermissions(obj, action):
181 target = action.action(context).strip()
182 if target.startswith('/'):
183 target = target[1:]
184 __traceback_info__ = ( ti.getId(), target )
185 return obj.restrictedTraverse( target )
187 raise AccessControl_Unauthorized( 'No accessible views available for '
188 '%s' % '/'.join( obj.getPhysicalPath() ) )
189 else:
190 raise NotFound('Cannot find default view for "%s"' %
191 '/'.join(obj.getPhysicalPath()))
193 def _FSCacheHeaders(obj):
195 REQUEST = getattr(obj, 'REQUEST', None)
196 if REQUEST is None:
197 return False
199 RESPONSE = REQUEST.RESPONSE
200 header = REQUEST.get_header('If-Modified-Since', None)
201 last_mod = obj._file_mod_time
203 if header is not None:
204 header = header.split(';')[0]
205 # Some proxies seem to send invalid date strings for this
206 # header. If the date string is not valid, we ignore it
207 # rather than raise an error to be generally consistent
208 # with common servers such as Apache (which can usually
209 # understand the screwy date string as a lucky side effect
210 # of the way they parse it).
211 try:
212 mod_since=DateTime(header)
213 mod_since=long(mod_since.timeTime())
214 except TypeError:
215 mod_since=None
217 if mod_since is not None:
218 if last_mod > 0 and last_mod <= mod_since:
219 RESPONSE.setStatus(304)
220 return True
222 #Last-Modified will get stomped on by a cache policy if there is
223 #one set....
224 RESPONSE.setHeader('Last-Modified', rfc1123_date(last_mod))
226 # If Zope ever provides a call to getRolesInContext() through
227 # the SecurityManager API, the method below needs to be updated.
228 security.declarePrivate('_limitGrantedRoles')
229 def _limitGrantedRoles(roles, context, special_roles=()):
230 # Only allow a user to grant roles already possessed by that user,
231 # with the exception that all special_roles can also be granted.
232 user = _getAuthenticatedUser(context)
233 if user is None:
234 user_roles = ()
235 else:
236 user_roles = user.getRolesInContext(context)
237 if 'Manager' in user_roles:
238 # Assume all other roles are allowed.
239 return
240 for role in roles:
241 if role not in special_roles and role not in user_roles:
242 raise AccessControl_Unauthorized('Too many roles specified.')
244 limitGrantedRoles = _limitGrantedRoles # XXX: Deprecated spelling
246 security.declarePrivate('_mergedLocalRoles')
247 def _mergedLocalRoles(object):
248 """Returns a merging of object and its ancestors'
249 __ac_local_roles__."""
250 # Modified from AccessControl.User.getRolesInContext().
251 merged = {}
252 object = getattr(object, 'aq_inner', object)
253 while 1:
254 if hasattr(object, '__ac_local_roles__'):
255 dict = object.__ac_local_roles__ or {}
256 if callable(dict): dict = dict()
257 for k, v in dict.items():
258 if merged.has_key(k):
259 merged[k] = merged[k] + v
260 else:
261 merged[k] = v
262 if hasattr(object, 'aq_parent'):
263 object=object.aq_parent
264 object=getattr(object, 'aq_inner', object)
265 continue
266 if hasattr(object, 'im_self'):
267 object=object.im_self
268 object=getattr(object, 'aq_inner', object)
269 continue
270 break
272 return deepcopy(merged)
274 mergedLocalRoles = _mergedLocalRoles # XXX: Deprecated spelling
276 security.declarePrivate('_ac_inherited_permissions')
277 def _ac_inherited_permissions(ob, all=0):
278 # Get all permissions not defined in ourself that are inherited
279 # This will be a sequence of tuples with a name as the first item and
280 # an empty tuple as the second.
281 d = {}
282 perms = getattr(ob, '__ac_permissions__', ())
283 for p in perms: d[p[0]] = None
284 r = gather_permissions(ob.__class__, [], d)
285 if all:
286 if hasattr(ob, '_subobject_permissions'):
287 for p in ob._subobject_permissions():
288 pname=p[0]
289 if not d.has_key(pname):
290 d[pname]=1
291 r.append(p)
292 r = list(perms) + r
293 return r
295 security.declarePrivate('_modifyPermissionMappings')
296 def _modifyPermissionMappings(ob, map):
297 """
298 Modifies multiple role to permission mappings.
299 """
300 # This mimics what AccessControl/Role.py does.
301 # Needless to say, it's crude. :-(
302 something_changed = 0
303 perm_info = _ac_inherited_permissions(ob, 1)
304 for name, settings in map.items():
305 cur_roles = rolesForPermissionOn(name, ob)
306 if isinstance(cur_roles, basestring):
307 cur_roles = [cur_roles]
308 else:
309 cur_roles = list(cur_roles)
310 changed = 0
311 for (role, allow) in settings.items():
312 if not allow:
313 if role in cur_roles:
314 changed = 1
315 cur_roles.remove(role)
316 else:
317 if role not in cur_roles:
318 changed = 1
319 cur_roles.append(role)
320 if changed:
321 data = () # The list of methods using this permission.
322 for perm in perm_info:
323 n, d = perm[:2]
324 if n == name:
325 data = d
326 break
327 p = Permission(name, data, ob)
328 p.setRoles(tuple(cur_roles))
329 something_changed = 1
330 return something_changed
333 # Parse a string of etags from an If-None-Match header
334 # Code follows ZPublisher.HTTPRequest.parse_cookie
335 parse_etags_lock=allocate_lock()
336 def parse_etags( text
337 , result=None
338 # quoted etags (assumed separated by whitespace + a comma)
339 , etagre_quote = re.compile('(\s*\"([^\"]*)\"\s*,{0,1})')
340 # non-quoted etags (assumed separated by whitespace + a comma)
341 , etagre_noquote = re.compile('(\s*([^,]*)\s*,{0,1})')
342 , acquire=parse_etags_lock.acquire
343 , release=parse_etags_lock.release
344 ):
346 if result is None: result=[]
347 if not len(text):
348 return result
350 acquire()
351 try:
352 m = etagre_quote.match(text)
353 if m:
354 # Match quoted etag (spec-observing client)
355 l = len(m.group(1))
356 value = m.group(2)
357 else:
358 # Match non-quoted etag (lazy client)
359 m = etagre_noquote.match(text)
360 if m:
361 l = len(m.group(1))
362 value = m.group(2)
363 else:
364 return result
365 finally: release()
367 if value:
368 result.append(value)
369 return apply(parse_etags,(text[l:],result))
372 def _checkConditionalGET(obj, extra_context):
373 """A conditional GET is done using one or both of the request
374 headers:
376 If-Modified-Since: Date
377 If-None-Match: list ETags (comma delimited, sometimes quoted)
379 If both conditions are present, both must be satisfied.
381 This method checks the caching policy manager to see if
382 a content object's Last-modified date and ETag satisfy
383 the conditional GET headers.
385 Returns the tuple (last_modified, etag) if the conditional
386 GET requirements are met and None if not.
388 It is possible for one of the tuple elements to be None.
389 For example, if there is no If-None-Match header and
390 the caching policy does not specify an ETag, we will
391 just return (last_modified, None).
392 """
394 REQUEST = getattr(obj, 'REQUEST', None)
395 if REQUEST is None:
396 return False
398 if_modified_since = REQUEST.get_header('If-Modified-Since', None)
399 if_none_match = REQUEST.get_header('If-None-Match', None)
401 if if_modified_since is None and if_none_match is None:
402 # not a conditional GET
403 return False
405 manager = getToolByName(obj, 'caching_policy_manager', None)
406 if manager is None:
407 return False
409 ret = manager.getModTimeAndETag(aq_parent(obj), obj.getId(), extra_context)
410 if ret is None:
411 # no appropriate policy or 304s not enabled
412 return False
414 (content_mod_time, content_etag, set_last_modified_header) = ret
415 if content_mod_time:
416 mod_time_secs = long(content_mod_time.timeTime())
417 else:
418 mod_time_secs = None
420 if if_modified_since:
421 # from CMFCore/FSFile.py:
422 if_modified_since = if_modified_since.split(';')[0]
423 # Some proxies seem to send invalid date strings for this
424 # header. If the date string is not valid, we ignore it
425 # rather than raise an error to be generally consistent
426 # with common servers such as Apache (which can usually
427 # understand the screwy date string as a lucky side effect
428 # of the way they parse it).
429 try:
430 if_modified_since=long(DateTime(if_modified_since).timeTime())
431 except:
432 if_mod_since=None
434 client_etags = None
435 if if_none_match:
436 client_etags = parse_etags(if_none_match)
438 if not if_modified_since and not client_etags:
439 # not a conditional GET, or headers are messed up
440 return False
442 if if_modified_since:
443 if ( not content_mod_time or
444 mod_time_secs < 0 or
445 mod_time_secs > if_modified_since ):
446 return False
448 if client_etags:
449 if ( not content_etag or
450 (content_etag not in client_etags and '*' not in client_etags) ):
451 return False
452 else:
453 # If we generate an ETag, don't validate the conditional GET unless
454 # the client supplies an ETag
455 # This may be more conservative than the spec requires, but we are
456 # already _way_ more conservative.
457 if content_etag:
458 return False
460 response = REQUEST.RESPONSE
461 if content_mod_time and set_last_modified_header:
462 response.setHeader('Last-modified', str(content_mod_time))
463 if content_etag:
464 response.setHeader('ETag', content_etag, literal=1)
465 response.setStatus(304)
467 return True
470 security.declarePrivate('_setCacheHeaders')
471 def _setCacheHeaders(obj, extra_context):
472 """Set cache headers according to cache policy manager for the obj."""
473 REQUEST = getattr(obj, 'REQUEST', None)
475 if REQUEST is not None:
476 content = aq_parent(obj)
477 manager = getToolByName(obj, 'caching_policy_manager', None)
478 if manager is not None:
479 view_name = obj.getId()
480 headers = manager.getHTTPCachingHeaders(
481 content, view_name, extra_context
482 )
483 RESPONSE = REQUEST['RESPONSE']
484 for key, value in headers:
485 if key == 'ETag':
486 RESPONSE.setHeader(key, value, literal=1)
487 else:
488 RESPONSE.setHeader(key, value)
489 if headers:
490 RESPONSE.setHeader('X-Cache-Headers-Set-By',
491 'CachingPolicyManager: %s' %
492 '/'.join(manager.getPhysicalPath()))
494 class _ViewEmulator(Implicit):
495 """Auxiliary class used to adapt FSFile and FSImage
496 for caching_policy_manager
497 """
498 def __init__(self, view_name=''):
499 self._view_name = view_name
501 def getId(self):
502 return self._view_name
505 #
506 # Base classes for tools
507 #
508 class ImmutableId(Base):
510 """ Base class for objects which cannot be renamed.
511 """
512 def _setId(self, id):
514 """ Never allow renaming!
515 """
516 if id != self.getId():
517 raise MessageDialog(
518 title='Invalid Id',
519 message='Cannot change the id of this object',
520 action ='./manage_main',)
522 class UniqueObject (ImmutableId):
524 """ Base class for objects which cannot be "overridden" / shadowed.
525 """
526 __replaceable__ = UNIQUE
529 class SimpleItemWithProperties (PropertyManager, SimpleItem):
530 """
531 A common base class for objects with configurable
532 properties in a fixed schema.
533 """
534 manage_options = (
535 PropertyManager.manage_options
536 + SimpleItem.manage_options)
539 security = ClassSecurityInfo()
540 security.declarePrivate('manage_addProperty')
541 security.declarePrivate('manage_delProperties')
542 security.declarePrivate('manage_changePropertyTypes')
544 def manage_propertiesForm(self, REQUEST, *args, **kw):
545 """ An override that makes the schema fixed.
546 """
547 my_kw = kw.copy()
548 my_kw['property_extensible_schema__'] = 0
549 form = PropertyManager.manage_propertiesForm.__of__(self)
550 return form(self, REQUEST, *args, **my_kw)
552 InitializeClass( SimpleItemWithProperties )
555 #
556 # "Omnibus" factory framework for tools.
557 #
558 class ToolInit:
560 """ Utility class for generating the factories for several tools.
561 """
562 __name__ = 'toolinit'
564 security = ClassSecurityInfo()
565 security.declareObjectPrivate() # equivalent of __roles__ = ()
567 def __init__(self, meta_type, tools, product_name=None, icon=None):
568 self.meta_type = meta_type
569 self.tools = tools
570 if product_name is not None:
571 warn("The product_name parameter of ToolInit is deprecated and "
572 "will be ignored in CMF 2.0: %s" % product_name,
573 DeprecationWarning, stacklevel=2)
574 self.product_name = product_name
575 self.icon = icon
577 def initialize(self, context):
578 # Add only one meta type to the folder add list.
579 if self.product_name is None:
580 productObject = context._ProductContext__prod
581 self.product_name = productObject.id
582 context.registerClass(
583 meta_type = self.meta_type,
584 # This is a little sneaky: we add self to the
585 # FactoryDispatcher under the name "toolinit".
586 # manage_addTool() can then grab it.
587 constructors = (manage_addToolForm,
588 manage_addTool,
589 self,),
590 icon = self.icon
591 )
593 if self.icon:
594 icon = os_path.split(self.icon)[1]
595 else:
596 icon = None
597 for tool in self.tools:
598 tool.__factory_meta_type__ = self.meta_type
599 tool.icon = 'misc_/%s/%s' % (self.product_name, icon)
601 InitializeClass( ToolInit )
603 addInstanceForm = HTMLFile('dtml/addInstance', globals())
605 def manage_addToolForm(self, REQUEST):
607 """ Show the add tool form.
608 """
609 # self is a FactoryDispatcher.
610 toolinit = self.toolinit
611 tl = []
612 for tool in toolinit.tools:
613 tl.append(tool.meta_type)
614 return addInstanceForm(addInstanceForm, self, REQUEST,
615 factory_action='manage_addTool',
616 factory_meta_type=toolinit.meta_type,
617 factory_product_name=toolinit.product_name,
618 factory_icon=toolinit.icon,
619 factory_types_list=tl,
620 factory_need_id=0)
622 def manage_addTool(self, type, REQUEST=None):
624 """ Add the tool specified by name.
625 """
626 # self is a FactoryDispatcher.
627 toolinit = self.toolinit
628 obj = None
629 for tool in toolinit.tools:
630 if tool.meta_type == type:
631 obj = tool()
632 break
633 if obj is None:
634 raise NotFound(type)
635 self._setObject(obj.getId(), obj)
636 if REQUEST is not None:
637 return self.manage_main(self, REQUEST)
640 #
641 # Now, do the same for creating content factories.
642 #
643 class ContentInit:
645 """ Utility class for generating factories for several content types.
646 """
647 __name__ = 'contentinit'
649 security = ClassSecurityInfo()
650 security.declareObjectPrivate()
652 def __init__( self
653 , meta_type
654 , content_types
655 , permission=None
656 , extra_constructors=()
657 , fti=()
658 ):
659 self.meta_type = meta_type
660 self.content_types = content_types
661 self.permission = permission
662 self.extra_constructors = extra_constructors
663 self.fti = fti
665 def initialize(self, context):
666 # Add only one meta type to the folder add list.
667 context.registerClass(
668 meta_type = self.meta_type
669 # This is a little sneaky: we add self to the
670 # FactoryDispatcher under the name "contentinit".
671 # manage_addContentType() can then grab it.
672 , constructors = ( manage_addContentForm
673 , manage_addContent
674 , self
675 , ('factory_type_information', self.fti)
676 ) + self.extra_constructors
677 , permission = self.permission
678 )
680 for ct in self.content_types:
681 ct.__factory_meta_type__ = self.meta_type
683 InitializeClass( ContentInit )
685 def manage_addContentForm(self, REQUEST):
687 """ Show the add content type form.
688 """
689 # self is a FactoryDispatcher.
690 ci = self.contentinit
691 tl = []
692 for t in ci.content_types:
693 tl.append(t.meta_type)
694 return addInstanceForm(addInstanceForm, self, REQUEST,
695 factory_action='manage_addContent',
696 factory_meta_type=ci.meta_type,
697 factory_icon=None,
698 factory_types_list=tl,
699 factory_need_id=1)
701 def manage_addContent( self, id, type, REQUEST=None ):
703 """ Add the content type specified by name.
704 """
705 # self is a FactoryDispatcher.
706 contentinit = self.contentinit
707 obj = None
708 for content_type in contentinit.content_types:
709 if content_type.meta_type == type:
710 obj = content_type( id )
711 break
712 if obj is None:
713 raise NotFound(type)
714 self._setObject( id, obj )
715 if REQUEST is not None:
716 return self.manage_main(self, REQUEST)
719 def initializeBasesPhase1(base_classes, module):
721 """ Execute the first part of initialization of ZClass base classes.
723 Stuffs a _ZClass_for_x class in the module for each base.
724 """
725 rval = []
726 for base_class in base_classes:
727 d={}
728 zclass_name = '_ZClass_for_%s' % base_class.__name__
729 exec 'class %s: pass' % zclass_name in d
730 Z = d[ zclass_name ]
731 Z.propertysheets = PropertySheets()
732 Z._zclass_ = base_class
733 Z.manage_options = ()
734 Z.__module__ = module.__name__
735 setattr( module, zclass_name, Z )
736 rval.append(Z)
737 return rval
739 def initializeBasesPhase2(zclasses, context):
741 """ Finishes ZClass base initialization.
743 o 'zclasses' is the list returned by initializeBasesPhase1().
745 o 'context' is a ProductContext object.
746 """
747 for zclass in zclasses:
748 context.registerZClass(zclass)
750 def registerIcon(klass, iconspec, _prefix=None):
752 """ Make an icon available for a given class.
754 o 'klass' is the class being decorated.
756 o 'iconspec' is the path within the product where the icon lives.
757 """
758 modname = klass.__module__
759 pid = modname.split('.')[1]
760 name = os_path.split(iconspec)[1]
761 klass.icon = 'misc_/%s/%s' % (pid, name)
762 icon = ImageFile(iconspec, _prefix)
763 icon.__roles__=None
764 if not hasattr(misc_images, pid):
765 setattr(misc_images, pid, MiscImage(pid, {}))
766 getattr(misc_images, pid)[name]=icon
768 security.declarePublic('format_stx')
769 def format_stx( text, level=1 ):
770 """ Render STX to HTML.
771 """
772 warn('format_stx() will be removed in CMF 2.0. Please use '
773 'StructuredText.StructuredText.HTML instead.',
774 DeprecationWarning, stacklevel=2)
775 return HTML(text, level=level, header=0)
777 #
778 # Metadata Keyword splitter utilities
779 #
780 KEYSPLITRE = re.compile(r'[,;]')
782 security.declarePublic('keywordsplitter')
783 def keywordsplitter( headers
784 , names=('Subject', 'Keywords',)
785 , splitter=KEYSPLITRE.split
786 ):
787 """ Split keywords out of headers, keyed on names. Returns list.
788 """
789 out = []
790 for head in names:
791 keylist = splitter(headers.get(head, ''))
792 keylist = map(lambda x: x.strip(), keylist)
793 out.extend( [key for key in keylist if key] )
794 return out
796 #
797 # Metadata Contributors splitter utilities
798 #
799 CONTRIBSPLITRE = re.compile(r';')
801 security.declarePublic('contributorsplitter')
802 def contributorsplitter( headers
803 , names=('Contributors',)
804 , splitter=CONTRIBSPLITRE.split
805 ):
806 """ Split contributors out of headers, keyed on names. Returns list.
807 """
808 return keywordsplitter( headers, names, splitter )
810 #
811 # Directory-handling utilities
812 #
813 security.declarePublic('normalize')
814 def normalize(p):
815 # the first .replace is needed to help normpath when dealing with Windows
816 # paths under *nix, the second to normalize to '/'
817 return os_path.normpath(p.replace('\\','/')).replace('\\','/')
819 import Products
820 ProductsPath = [ abspath(ppath) for ppath in Products.__path__ ]
822 security.declarePublic('expandpath')
823 def expandpath(p):
824 """ Convert minimal filepath to (expanded) filepath.
826 The (expanded) filepath is the valid absolute path on the current platform
827 and setup.
828 """
829 p = os_path.normpath(p)
830 if os_path.isabs(p):
831 return p
833 for ppath in ProductsPath:
834 abs = os_path.join(ppath, p)
835 if os_path.exists(abs):
836 return abs
838 # return the last one, errors will happen else where as as result
839 # and be caught
840 return abs
842 security.declarePublic('minimalpath')
843 def minimalpath(p):
844 """ Convert (expanded) filepath to minimal filepath.
846 The minimal filepath is the cross-platform / cross-setup path stored in
847 persistent objects and used as key in the directory registry.
849 Returns a slash-separated path relative to the Products path. If it can't
850 be found, a normalized path is returned.
851 """
852 p = abspath(p)
853 for ppath in ProductsPath:
854 if p.startswith(ppath):
855 p = p[len(ppath)+1:]
856 break
857 return p.replace('\\','/')
860 class SimpleRecord:
861 """ record-like class """
863 def __init__(self, **kw):
864 self.__dict__.update(kw)