Back to index

plone3  3.1.7
utils.py
Go to the documentation of this file.
00001 import re
00002 import warnings
00003 from types import ClassType
00004 from os.path import join, abspath, split
00005 from cStringIO import StringIO
00006 from PIL import Image
00007 
00008 from plone.i18n.normalizer.interfaces import IIDNormalizer
00009 from plone.i18n.normalizer.interfaces import IFileNameNormalizer
00010 from plone.i18n.normalizer.interfaces import IUserPreferredFileNameNormalizer
00011 
00012 import zope.interface
00013 from zope.interface import implementedBy
00014 from zope.component import getMultiAdapter
00015 from zope.component import queryMultiAdapter
00016 from zope.component import queryUtility
00017 
00018 import OFS
00019 import Globals
00020 from Acquisition import aq_base, aq_inner, aq_parent
00021 from DateTime import DateTime
00022 from Products.Five import BrowserView as BaseView
00023 from Products.Five.bridge import fromZ2Interface
00024 from Products.CMFCore.utils import ToolInit as CMFCoreToolInit
00025 from Products.CMFCore.utils import getToolByName
00026 from Products.CMFPlone.interfaces.Translatable import ITranslatable
00027 import transaction
00028 
00029 from Products.PageTemplates.GlobalTranslationService import \
00030      getGlobalTranslationService
00031 
00032 # Canonical way to get at CMFPlone directory
00033 PACKAGE_HOME = Globals.package_home(globals())
00034 WWW_DIR = join(PACKAGE_HOME, 'www')
00035 
00036 # Log methods
00037 from log import log
00038 from log import log_exc
00039 from log import log_deprecated
00040 
00041 # Settings for member image resize quality
00042 PIL_SCALING_ALGO = Image.ANTIALIAS
00043 PIL_QUALITY = 88
00044 MEMBER_IMAGE_SCALE = (75, 100)
00045 IMAGE_SCALE_PARAMS = {'scale': MEMBER_IMAGE_SCALE,
00046                       'quality': PIL_QUALITY,
00047                       'algorithm': PIL_SCALING_ALGO,
00048                       'default_format': 'PNG'}
00049 
00050 _marker = []
00051 
00052 class BrowserView(BaseView):
00053 
00054     def __init__(self, context, request):
00055         self.context = [context]
00056         self.request = request
00057 
00058 def parent(obj):
00059     return aq_parent(aq_inner(obj))
00060 
00061 def context(view):
00062     return view.context[0]
00063 
00064 def createBreadCrumbs(context, request):
00065     view = getMultiAdapter((context, request), name='breadcrumbs_view')
00066     return view.breadcrumbs()
00067 
00068 def createNavTree(context, request, sitemap=False):
00069     view = getMultiAdapter((context, request), name='navtree_builder_view')
00070     return view.navigationTree()
00071 
00072 def createSiteMap(context, request, sitemap=False):
00073     view = getMultiAdapter((context, request), name='sitemap_builder_view')
00074     return view.siteMap()
00075 
00076 def _getDefaultPageView(obj, request):
00077     """This is a nasty hack because the view lookup fails when it occurs too
00078        early in the publishing process because the request isn't marked with
00079        the default skin.  Explicitly marking the request appears to cause
00080        connection errors, so we just instantiate the view manually.
00081     """
00082     view = queryMultiAdapter((obj, request), name='default_page')
00083     if view is None:
00084         # XXX: import here to avoid a circular dependency
00085         from plone.app.layout.navigation.defaultpage import DefaultPage
00086         view = DefaultPage(obj, request)
00087     return view
00088 
00089 def isDefaultPage(obj, request, context=None):
00090     if context is not None:
00091         warnings.warn("The context parameter for isDefaultPage is "
00092                       "deprecated and will be removed in Plone 4.",
00093                       DeprecationWarning, 1)
00094     container = parent(obj)
00095     if container is None:
00096         return False
00097     view = _getDefaultPageView(container, request)
00098     return view.isDefaultPage(obj)
00099 
00100 def getDefaultPage(obj, request, context=None):
00101     if context is not None:
00102         warnings.warn("The context parameter for getDefaultPage is "
00103                       "deprecated and will be removed in Plone 4.",
00104                       DeprecationWarning, 1)
00105     # Short circuit if we are not looking at a Folder
00106     if not obj.isPrincipiaFolderish:
00107         return None
00108     view = _getDefaultPageView(obj, request)
00109     return view.getDefaultPage()
00110 
00111 def isIDAutoGenerated(context, id):
00112     # In 2.1 non-autogenerated is the common case, caught exceptions are
00113     # expensive, so let's make a cheap check first
00114     if id.count('.') < 2:
00115         return False
00116 
00117     pt = getToolByName(context, 'portal_types')
00118     portaltypes = pt.listContentTypes()
00119     portaltypes.extend([pt.lower() for pt in portaltypes])
00120 
00121     try:
00122         parts = id.split('.')
00123         random_number = parts.pop()
00124         date_created = parts.pop()
00125         obj_type = '.'.join(parts)
00126         type = ' '.join(obj_type.split('_'))
00127         # New autogenerated ids may have a lower case portal type
00128         if ((type in portaltypes or obj_type in portaltypes) and
00129             DateTime(date_created) and
00130             float(random_number)):
00131             return True
00132     except (ValueError, AttributeError, IndexError, DateTime.DateTimeError):
00133         pass
00134 
00135     return False
00136 
00137 def lookupTranslationId(obj, page, ids):
00138     implemented = ITranslatable.isImplementedBy(obj)
00139     if not implemented or implemented and not obj.isTranslation():
00140         pageobj = getattr(obj, page, None)
00141         if (pageobj is not None and
00142             ITranslatable.isImplementedBy(pageobj)):
00143             translation = pageobj.getTranslation()
00144             if (translation is not None and
00145                 ids.has_key(translation.getId())):
00146                 page = translation.getId()
00147     return page
00148 
00149 def pretty_title_or_id(context, obj, empty_value=_marker):
00150     """Return the best possible title or id of an item, regardless
00151        of whether obj is a catalog brain or an object, but returning an
00152        empty title marker if the id is not set (i.e. it's auto-generated).
00153     """
00154     #if safe_hasattr(obj, 'aq_explicit'):
00155     #    obj = obj.aq_explicit
00156     #title = getattr(obj, 'Title', None)
00157     title = None
00158     if base_hasattr(obj, 'Title'):
00159         title = getattr(obj, 'Title', None)
00160     if safe_callable(title):
00161         title = title()
00162     if title:
00163         return title
00164     item_id = getattr(obj, 'getId', None)
00165     if safe_callable(item_id):
00166         item_id = item_id()
00167     if item_id and not isIDAutoGenerated(context, item_id):
00168         return item_id
00169     if empty_value is _marker:
00170         empty_value = getEmptyTitle(context)
00171     return empty_value
00172 
00173 def getSiteEncoding(context):
00174     default = 'utf-8'
00175     pprop = getToolByName(context, 'portal_properties')
00176     site_props = getToolByName(pprop, 'site_properties', None)
00177     if site_props is None:
00178         return default
00179     return site_props.getProperty('default_charset', default)
00180 
00181 def portal_utf8(context, str, errors='strict'):
00182     charset = getSiteEncoding(context)
00183     if charset.lower() in ('utf-8', 'utf8'):
00184         # Test
00185         unicode(str, 'utf-8', errors)
00186         return str
00187     else:
00188         return unicode(str, charset, errors).encode('utf-8', errors)
00189 
00190 def utf8_portal(context, str, errors='strict'):
00191     charset = getSiteEncoding(context)
00192     if charset.lower() in ('utf-8', 'utf8'):
00193         # Test
00194         unicode(str, 'utf-8', errors)
00195         return str
00196     else:
00197         return unicode(str, 'utf-8', errors).encode(charset, errors)
00198 
00199 def getEmptyTitle(context, translated=True):
00200     """Returns string to be used for objects with no title or id"""
00201     # The default is an extra fancy unicode elipsis
00202     empty = unicode('\x5b\xc2\xb7\xc2\xb7\xc2\xb7\x5d', 'utf-8')
00203     if translated:
00204         service = getGlobalTranslationService()
00205         empty = service.translate('plone', 'title_unset', context=context, default=empty)
00206     return empty
00207 
00208 def typesToList(context):
00209     ntp = getToolByName(context, 'portal_properties').navtree_properties
00210     ttool = getToolByName(context, 'portal_types')
00211     bl = ntp.getProperty('metaTypesNotToList', ())
00212     bl_dict = {}
00213     for t in bl:
00214         bl_dict[t] = 1
00215     all_types = ttool.listContentTypes()
00216     wl = [t for t in all_types if not bl_dict.has_key(t)]
00217     return wl
00218 
00219 def normalizeString(text, context=None, encoding=None, relaxed=False):
00220     assert (context is not None) or (encoding is not None), \
00221            'Either context or encoding must be provided'
00222     # Make sure we are dealing with a stringish type
00223     if not isinstance(text, basestring):
00224         # This most surely ends up in something the user does not expect
00225         # to see. But at least it does not break.
00226         text = repr(text)
00227 
00228     # Make sure we are dealing with a unicode string
00229     if not isinstance(text, unicode):
00230         if encoding is None:
00231             encoding = getSiteEncoding(context)
00232         text = unicode(text, encoding)
00233 
00234     if not relaxed:
00235         return queryUtility(IIDNormalizer).normalize(text)
00236 
00237     # BBB To be removed in Plone 4.0
00238     log_deprecated("The relaxed mode of normalizeString is deprecated and will "
00239                    "be removed in Plone 4.0. Please use either the url or file "
00240                    "name normalizer from the plone.i18n package instead.")
00241 
00242     request = getattr(context, 'REQUEST', None)
00243     # If we have a request, get the preferred user normalizer
00244     if request is not None:
00245         return IUserPreferredFileNameNormalizer(request).normalize(text)
00246 
00247     return queryUtility(IFileNameNormalizer).normalize(text)
00248 
00249 class IndexIterator(object):
00250     """BBB: This iterator was us ed for tabindex use, but for accessibility 
00251        reasons, we have deprecated it, and it now returns None always. Should 
00252        be removed in Plone 4.0.
00253        
00254        Below are the different use cases we used to have, all return None now:
00255 
00256         >>> i = IndexIterator(pos=10, mainSlot=True)
00257         >>> i.next() is None
00258         True
00259 
00260        The default start value gets "None"
00261 
00262         >>> i = IndexIterator(mainSlot=True)
00263         >>> i.next() is None
00264         True
00265 
00266        Subsequent iterations will get None (thus removing the tabindex
00267        attribute):
00268 
00269         >>> i.next() is None
00270         True
00271 
00272        Outside the mainSlot all iterations will get None:
00273 
00274         >>> i = IndexIterator(pos=10, mainSlot=False)
00275         >>> i.next() is None
00276         True
00277 
00278         >>> i.next() is None
00279         True
00280 
00281         >>> i = IndexIterator(mainSlot=False)
00282         >>> i.next() is None
00283         True
00284     """
00285     __allow_access_to_unprotected_subobjects__ = 1
00286 
00287     def __init__(self, upper=100000, pos=1, mainSlot=True):
00288         self.upper=upper
00289         self.pos=pos
00290         self.mainSlot=mainSlot
00291 
00292     def next(self):
00293         return None
00294 
00295 class RealIndexIterator(object):
00296     """The 'real' version of the IndexIterator class, that's actually 
00297     used to generate unique indexes. 
00298     """ 
00299     __allow_access_to_unprotected_subobjects__ = 1 
00300 
00301     def __init__(self, pos=0): 
00302         self.pos=pos
00303 
00304     def next(self): 
00305         result=self.pos 
00306         self.pos=self.pos+1 
00307         return result 
00308 
00309 class ToolInit(CMFCoreToolInit):
00310 
00311     def getProductContext(self, context):
00312         name = '_ProductContext__prod'
00313         return getattr(context, name, getattr(context, '__prod', None))
00314 
00315     def getPack(self, context):
00316         name = '_ProductContext__pack'
00317         return getattr(context, name, getattr(context, '__pack__', None))
00318 
00319     def getIcon(self, context, path):
00320         pack = self.getPack(context)
00321         icon = None
00322         # This variable is just used for the log message
00323         icon_path = path
00324         try:
00325             icon = Globals.ImageFile(path, pack.__dict__)
00326         except (IOError, OSError):
00327             # Fallback:
00328             # Assume path is relative to CMFPlone directory
00329             path = abspath(join(PACKAGE_HOME, path))
00330             try:
00331                 icon = Globals.ImageFile(path, pack.__dict__)
00332             except (IOError, OSError):
00333                 # if there is some problem loading the fancy image
00334                 # from the tool then  tell someone about it
00335                 log(('The icon for the product: %s which was set to: %s, '
00336                      'was not found. Using the default.' %
00337                      (self.product_name, icon_path)))
00338         return icon
00339 
00340     def initialize(self, context):
00341         """ Wrap the CMFCore Tool Init method """
00342         CMFCoreToolInit.initialize(self, context)
00343         for tool in self.tools:
00344             # Get the icon path from the tool
00345             path = getattr(tool, 'toolicon', None)
00346             if path is not None:
00347                 pc = self.getProductContext(context)
00348                 if pc is not None:
00349                     pid = pc.id
00350                     name = split(path)[1]
00351                     icon = self.getIcon(context, path)
00352                     if icon is None:
00353                         # Icon was not found
00354                         return
00355                     icon.__roles__ = None
00356                     tool.icon = 'misc_/%s/%s' % (self.product_name, name)
00357                     misc = OFS.misc_.misc_
00358                     Misc = OFS.misc_.Misc_
00359                     if not hasattr(misc, pid):
00360                         setattr(misc, pid, Misc(pid, {}))
00361                     getattr(misc, pid)[name] = icon
00362 
00363 
00364 def _createObjectByType(type_name, container, id, *args, **kw):
00365     """Create an object without performing security checks
00366 
00367     invokeFactory and fti.constructInstance perform some security checks
00368     before creating the object. Use this function instead if you need to
00369     skip these checks.
00370 
00371     This method uses some code from
00372     CMFCore.TypesTool.FactoryTypeInformation.constructInstance
00373     to create the object without security checks.
00374     """
00375     id = str(id)
00376     typesTool = getToolByName(container, 'portal_types')
00377     fti = typesTool.getTypeInfo(type_name)
00378     if not fti:
00379         raise ValueError, 'Invalid type %s' % type_name
00380 
00381     # we have to do it all manually :(
00382     p = container.manage_addProduct[fti.product]
00383     m = getattr(p, fti.factory, None)
00384     if m is None:
00385         raise ValueError, ('Product factory for %s was invalid' %
00386                            fti.getId())
00387 
00388     # construct the object
00389     m(id, *args, **kw)
00390     ob = container._getOb( id )
00391 
00392     return fti._finishConstruction(ob)
00393 
00394 
00395 def safeToInt(value):
00396     """Convert value to integer or just return 0 if we can't"""
00397     try:
00398         return int(value)
00399     except ValueError:
00400         return 0
00401 
00402 release_levels = ('alpha', 'beta', 'candidate', 'final')
00403 rl_abbr = {'a':'alpha', 'b':'beta', 'rc':'candidate'}
00404 
00405 def versionTupleFromString(v_str):
00406     """Returns version tuple from passed in version string
00407 
00408         >>> versionTupleFromString('1.2.3')
00409         (1, 2, 3, 'final', 0)
00410 
00411         >>> versionTupleFromString('2.1-final1 (SVN)')
00412         (2, 1, 0, 'final', 1)
00413 
00414         >>> versionTupleFromString('3-beta')
00415         (3, 0, 0, 'beta', 0)
00416 
00417         >>> versionTupleFromString('2.0a3')
00418         (2, 0, 0, 'alpha', 3)
00419 
00420         >>> versionTupleFromString('foo') is None
00421         True
00422         """
00423     regex_str = "(^\d+)[.]?(\d*)[.]?(\d*)[- ]?(alpha|beta|candidate|final|a|b|rc)?(\d*)"
00424     v_regex = re.compile(regex_str)
00425     match = v_regex.match(v_str)
00426     if match is None:
00427         v_tpl = None
00428     else:
00429         groups = list(match.groups())
00430         for i in (0, 1, 2, 4):
00431             groups[i] = safeToInt(groups[i])
00432         if groups[3] is None:
00433             groups[3] = 'final'
00434         elif groups[3] in rl_abbr.keys():
00435             groups[3] = rl_abbr[groups[3]]
00436         v_tpl = tuple(groups)
00437     return v_tpl
00438 
00439 def getFSVersionTuple():
00440     """Reads version.txt and returns version tuple"""
00441     vfile = "%s/version.txt" % PACKAGE_HOME
00442     v_str = open(vfile, 'r').read().lower()
00443     return versionTupleFromString(v_str)
00444 
00445 
00446 def transaction_note(note):
00447     """Write human legible note"""
00448     T=transaction.get()
00449     if isinstance(note, unicode):
00450         # Convert unicode to a regular string for the backend write IO.
00451         # UTF-8 is the only reasonable choice, as using unicode means
00452         # that Latin-1 is probably not enough.
00453         note = note.encode('utf-8', 'replace')
00454 
00455     if (len(T.description)+len(note))>=65535:
00456         log('Transaction note too large omitting %s' % str(note))
00457     else:
00458         T.note(str(note))
00459 
00460 
00461 def base_hasattr(obj, name):
00462     """Like safe_hasattr, but also disables acquisition."""
00463     return safe_hasattr(aq_base(obj), name)
00464 
00465 
00466 def safe_hasattr(obj, name, _marker=object()):
00467     """Make sure we don't mask exceptions like hasattr().
00468 
00469     We don't want exceptions other than AttributeError to be masked,
00470     since that too often masks other programming errors.
00471     Three-argument getattr() doesn't mask those, so we use that to
00472     implement our own hasattr() replacement.
00473     """
00474     return getattr(obj, name, _marker) is not _marker
00475 
00476 
00477 def safe_callable(obj):
00478     """Make sure our callable checks are ConflictError safe."""
00479     if safe_hasattr(obj, '__class__'):
00480         if safe_hasattr(obj, '__call__'):
00481             return True
00482         else:
00483             return isinstance(obj, ClassType)
00484     else:
00485         return callable(obj)
00486 
00487 
00488 def safe_unicode(value, encoding='utf-8'):
00489     """Converts a value to unicode, even it is already a unicode string.
00490 
00491         >>> from Products.CMFPlone.utils import safe_unicode
00492 
00493         >>> safe_unicode('spam')
00494         u'spam'
00495         >>> safe_unicode(u'spam')
00496         u'spam'
00497         >>> safe_unicode(u'spam'.encode('utf-8'))
00498         u'spam'
00499         >>> safe_unicode('\xc6\xb5')
00500         u'\u01b5'
00501         >>> safe_unicode(u'\xc6\xb5'.encode('iso-8859-1'))
00502         u'\u01b5'
00503         >>> safe_unicode('\xc6\xb5', encoding='ascii')
00504         u'\u01b5'
00505         >>> safe_unicode(1)
00506         1
00507         >>> print safe_unicode(None)
00508         None
00509     """
00510     if isinstance(value, unicode):
00511         return value
00512     elif isinstance(value, basestring):
00513         try:
00514             value = unicode(value, encoding)
00515         except (UnicodeDecodeError):
00516             value = value.decode('utf-8', 'replace')
00517     return value
00518 
00519 
00520 def tuplize(value):
00521     if isinstance(value, tuple):
00522         return value
00523     if isinstance(value, list):
00524         return tuple(value)
00525     return (value,)
00526 
00527 def _detuplize(interfaces, append):
00528     if isinstance(interfaces, (tuple, list)):
00529         for sub in interfaces:
00530             _detuplize(sub, append)
00531     else:
00532         append(interfaces)
00533 
00534 def flatten(interfaces):
00535     flattened = []
00536     _detuplize(interfaces, flattened.append)
00537     return tuple(flattened)
00538 
00539 def directlyProvides(obj, *interfaces):
00540     # convert any Zope 2 interfaces to Zope 3 using fromZ2Interface
00541     interfaces = flatten(interfaces)
00542     normalized_interfaces = []
00543     for i in interfaces:
00544         try:
00545             i = fromZ2Interface(i)
00546         except ValueError: # already a Zope 3 interface
00547             pass
00548         assert issubclass(i, zope.interface.Interface)
00549         normalized_interfaces.append(i)
00550     return zope.interface.directlyProvides(obj, *normalized_interfaces)
00551 
00552 def classImplements(class_, *interfaces):
00553     # convert any Zope 2 interfaces to Zope 3 using fromZ2Interface
00554     interfaces = flatten(interfaces)
00555     normalized_interfaces = []
00556     for i in interfaces:
00557         try:
00558             i = fromZ2Interface(i)
00559         except ValueError: # already a Zope 3 interface
00560             pass
00561         assert issubclass(i, zope.interface.Interface)
00562         normalized_interfaces.append(i)
00563     return zope.interface.classImplements(class_, *normalized_interfaces)
00564 
00565 def classDoesNotImplement(class_, *interfaces):
00566     # convert any Zope 2 interfaces to Zope 3 using fromZ2Interface
00567     interfaces = flatten(interfaces)
00568     normalized_interfaces = []
00569     for i in interfaces:
00570         try:
00571             i = fromZ2Interface(i)
00572         except ValueError: # already a Zope 3 interface
00573             pass
00574         assert issubclass(i, zope.interface.Interface)
00575         normalized_interfaces.append(i)
00576     implemented = implementedBy(class_)
00577     for iface in normalized_interfaces:
00578         implemented = implemented - iface
00579     return zope.interface.classImplementsOnly(class_, implemented)
00580 
00581 def webdav_enabled(obj, container):
00582     """WebDAV check used in externalEditorEnabled.py"""
00583 
00584     # Object implements lock interface
00585     interface_tool = getToolByName(container, 'portal_interface')
00586     if not interface_tool.objectImplements(obj, 'webdav.WriteLockInterface.WriteLockInterface'):
00587         return False
00588 
00589     # Backwards compatibility code for AT < 1.3.6
00590     if safe_hasattr(obj, '__dav_marshall__'):
00591         if obj.__dav_marshall__ == False:
00592             return False
00593     return True
00594 
00595 
00596 # Copied 'unrestricted_rename' from ATCT migrations to avoid
00597 # a dependency.
00598 
00599 from App.Dialogs import MessageDialog
00600 from OFS.CopySupport import CopyError
00601 from OFS.CopySupport import eNotSupported
00602 from cgi import escape
00603 import sys
00604 
00605 def _unrestricted_rename(container, id, new_id):
00606     """Rename a particular sub-object
00607 
00608     Copied from OFS.CopySupport
00609 
00610     Less strict version of manage_renameObject:
00611         * no write lock check
00612         * no verify object check from PortalFolder so it's allowed to rename
00613           even unallowed portal types inside a folder
00614     """
00615     try:
00616         container._checkId(new_id)
00617     except:
00618         raise CopyError, MessageDialog(
00619               title='Invalid Id',
00620               message=sys.exc_info()[1],
00621               action ='manage_main')
00622     ob=container._getOb(id)
00623     if not ob.cb_isMoveable():
00624         raise CopyError, eNotSupported % escape(id)
00625     try:
00626         ob._notifyOfCopyTo(container, op=1)
00627     except:
00628         raise CopyError, MessageDialog(
00629               title='Rename Error',
00630               message=sys.exc_info()[1],
00631               action ='manage_main')
00632     container._delObject(id)
00633     ob = aq_base(ob)
00634     ob._setId(new_id)
00635 
00636     # Note - because a rename always keeps the same context, we
00637     # can just leave the ownership info unchanged.
00638     container._setObject(new_id, ob, set_owner=0)
00639     ob = container._getOb(new_id)
00640     ob._postCopy(container, op=1)
00641 
00642     return None
00643 
00644 
00645 # Copied '_getSecurity' from Archetypes.utils to avoid a dependency.
00646 
00647 from AccessControl import ClassSecurityInfo
00648 
00649 def _getSecurity(klass, create=True):
00650     # a Zope 2 class can contain some attribute that is an instance
00651     # of ClassSecurityInfo. Zope 2 scans through things looking for
00652     # an attribute that has the name __security_info__ first
00653     info = vars(klass)
00654     security = None
00655     for k, v in info.items():
00656         if hasattr(v, '__security_info__'):
00657             security = v
00658             break
00659     # Didn't found a ClassSecurityInfo object
00660     if security is None:
00661         if not create:
00662             return None
00663         # we stuff the name ourselves as __security__, not security, as this
00664         # could theoretically lead to name clashes, and doesn't matter for
00665         # zope 2 anyway.
00666         security = ClassSecurityInfo()
00667         setattr(klass, '__security__', security)
00668     return security
00669 
00670 def scale_image(image_file, max_size=None, default_format=None):
00671     """Scales an image down to at most max_size preserving aspect ratio
00672     from an input file
00673 
00674         >>> import Products.CMFPlone
00675         >>> import os
00676         >>> from StringIO import StringIO
00677         >>> from Products.CMFPlone.utils import scale_image
00678         >>> from PIL import Image
00679 
00680     Let's make a couple test images and see how it works (all are
00681     100x100), the gif is palletted mode::
00682 
00683         >>> plone_path = os.path.dirname(Products.CMFPlone.__file__)
00684         >>> pjoin = os.path.join
00685         >>> path = pjoin(plone_path, 'tests', 'images')
00686         >>> orig_jpg = open(pjoin(path, 'test.jpg'), 'rb')
00687         >>> orig_png = open(pjoin(path, 'test.png'), 'rb')
00688         >>> orig_gif = open(pjoin(path, 'test.gif'), 'rb')
00689 
00690     We'll also make some evil non-images, including one which
00691     masquerades as a jpeg (which would trick OFS.Image)::
00692 
00693         >>> invalid = StringIO('<div>Evil!!!</div>')
00694         >>> sneaky = StringIO('\377\330<div>Evil!!!</div>')
00695 
00696     OK, let's get to it, first check that our bad images fail:
00697 
00698         >>> scale_image(invalid, (50, 50))
00699         Traceback (most recent call last):
00700         ...
00701         IOError: cannot identify image file
00702         >>> scale_image(sneaky, (50, 50))
00703         Traceback (most recent call last):
00704         ...
00705         IOError: cannot identify image file
00706 
00707     Now that that's out of the way we check on our real images to make
00708     sure the format and mode are preserved, that they are scaled, and that they
00709     return the correct mimetype::
00710 
00711         >>> new_jpg, mimetype = scale_image(orig_jpg, (50, 50))
00712         >>> img = Image.open(new_jpg)
00713         >>> img.size
00714         (50, 50)
00715         >>> img.format
00716         'JPEG'
00717         >>> mimetype
00718         'image/jpeg'
00719 
00720         >>> new_png, mimetype = scale_image(orig_png, (50, 50))
00721         >>> img = Image.open(new_png)
00722         >>> img.size
00723         (50, 50)
00724         >>> img.format
00725         'PNG'
00726         >>> mimetype
00727         'image/png'
00728 
00729         >>> new_gif, mimetype = scale_image(orig_gif, (50, 50))
00730         >>> img = Image.open(new_gif)
00731         >>> img.size
00732         (50, 50)
00733         >>> img.format
00734         'GIF'
00735         >>> img.mode
00736         'P'
00737         >>> mimetype
00738         'image/gif'
00739 
00740     We should also preserve the aspect ratio by scaling to the given
00741     width only unless told not to (we need to reset out files before
00742     trying again though::
00743 
00744         >>> orig_jpg.seek(0)
00745         >>> new_jpg, mimetype = scale_image(orig_jpg, (70, 100))
00746         >>> img = Image.open(new_jpg)
00747         >>> img.size
00748         (70, 70)
00749 
00750         >>> orig_jpg.seek(0)
00751         >>> new_jpg, mimetype = scale_image(orig_jpg, (70, 50))
00752         >>> img = Image.open(new_jpg)
00753         >>> img.size
00754         (50, 50)
00755 
00756     """
00757     if max_size is None:
00758         max_size = IMAGE_SCALE_PARAMS['scale']
00759     if default_format is None:
00760         default_format = IMAGE_SCALE_PARAMS['default_format']
00761     # Make sure we have ints
00762     size = (int(max_size[0]), int(max_size[1]))
00763     # Load up the image, don't try to catch errors, we want to fail miserably
00764     # on invalid images
00765     image = Image.open(image_file)
00766     # When might image.format not be true?
00767     format = image.format
00768     mimetype = 'image/%s'%format.lower()
00769     cur_size = image.size
00770     # from Archetypes ImageField
00771     # consider image mode when scaling
00772     # source images can be mode '1','L,','P','RGB(A)'
00773     # convert to greyscale or RGBA before scaling
00774     # preserve palletted mode (but not pallette)
00775     # for palletted-only image formats, e.g. GIF
00776     # PNG compression is OK for RGBA thumbnails
00777     original_mode = image.mode
00778     if original_mode == '1':
00779         image = image.convert('L')
00780     elif original_mode == 'P':
00781         image = image.convert('RGBA')
00782     # Rescale in place with an method that will not alter the aspect ratio
00783     # and will only shrink the image not enlarge it.
00784     image.thumbnail(size, resample=IMAGE_SCALE_PARAMS['algorithm'])
00785     # preserve palletted mode for GIF and PNG
00786     if original_mode == 'P' and format in ('GIF', 'PNG'):
00787         image = image.convert('P')
00788     # Save
00789     new_file = StringIO()
00790     image.save(new_file, format, quality=IMAGE_SCALE_PARAMS['quality'])
00791     new_file.seek(0)
00792     # Return the file data and the new mimetype
00793     return new_file, mimetype
00794 
00795 def isLinked(obj):
00796     """ check if the given content object is linked from another one
00797         WARNING: don't use this function in your code!!
00798             it is a helper for the link integrity code and will potentially
00799             abort the ongoing transaction, giving you unexpected results...
00800     """
00801     # first check to see if link integrity handling has been enabled at all
00802     # and if so, if the removal of the object was already confirmed, i.e.
00803     # while replaying the request;  unfortunately this makes it necessary
00804     # to import from plone.app.linkintegrity here, hence the try block...
00805     try:
00806         from plone.app.linkintegrity.interfaces import ILinkIntegrityInfo
00807         info = ILinkIntegrityInfo(obj.REQUEST)
00808     except (ImportError, TypeError):
00809         # if p.a.li isn't installed the following check can be cut short...
00810         return False
00811     if not info.integrityCheckingEnabled():
00812         return False
00813     if info.isConfirmedItem(obj):
00814         return True
00815     # otherwise, when not replaying the request already, it is tried to
00816     # delete the object, making it possible to find out if it was referenced,
00817     # i.e. in case a link integrity exception was raised
00818     linked = False
00819     parent = obj.aq_inner.aq_parent
00820     try:
00821         parent.manage_delObjects(obj.getId())
00822     except OFS.ObjectManager.BeforeDeleteException, e:
00823         linked = True
00824     except: # ignore other exceptions, not useful to us at this point
00825         pass
00826     # since this function is called first thing in `delete_confirmation.cpy`
00827     # and therefore nothing can possibly have changed yet at this point, we
00828     # might as well "begin" a new transaction instead of using a savepoint,
00829     # which creates a funny exception when using zeo (see #6666)
00830     transaction.begin()
00831     return linked
00832 
00833 # BBB Plone 4.0: Cyclic import errors are bad, deprecate these import locations.
00834 # Put these at the end to avoid an ImportError for safe_unicode
00835 from i18nl10n import utranslate
00836 from i18nl10n import ulocalized_time
00837 
00838 import zope.deprecation
00839 zope.deprecation.deprecated(
00840     ('getGlobalTranslationService'),
00841     "This reference to getGlobalTranslationService will be removed in Plone 4.0"
00842     ". Please import it from Products.PageTemplates.GlobalTranslationService.")
00843 zope.deprecation.deprecated(
00844     ('utranslate'),
00845     "This reference to the utranslate method has been deprecated will be "
00846     "removed in Plone 4.0. Please use the translate method of the "
00847     "GlobalTranslationService instead.")
00848 zope.deprecation.deprecated(
00849     ('ulocalized_time'),
00850     "This reference to the ulocalized_time method has been deprecated will be "
00851     "removed in Plone 4.0. Please import it from Products.CMFPlone.i18nl10n.")
00852 zope.deprecation.deprecated(
00853     ('BrowserView'),
00854     "Products.CMFPlone.utils.BrowserView will be removed in Plone 4.0. "
00855     "Please use Products.Five.BrowserView instead.")
00856 zope.deprecation.deprecated(
00857     ('context'),
00858     "This method is only useful for classes derived from the deprectaed "
00859     "Products.CMFPlone.utils.BrowserView class. Please use "
00860     "Products.Five.BrowserView as base class and aq_inner(self.context) "
00861     "to get the current context.")
00862 
00863