source: main/waeup.kofa/trunk/src/waeup/kofa/utils/helpers.py @ 8048

Last change on this file since 8048 was 7968, checked in by uli, 13 years ago

Add helper function to sort lists with preferred values first.

  • Property svn:keywords set to Id
File size: 17.3 KB
Line 
1## $Id: helpers.py 7968 2012-03-23 03:02:21Z uli $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""General helper functions for Kofa.
19"""
20import os
21import re
22import shutil
23import grok
24from cStringIO import StringIO
25from docutils.core import publish_string
26from zope.component import getUtility
27from zope.component.interfaces import IFactory
28from zope.interface import implementedBy
29from zope.interface.interface import Method, Attribute
30from zope.schema import getFieldNames
31from zope.schema.fieldproperty import FieldProperty
32from zope.security.interfaces import NoInteraction
33from zope.security.management import getInteraction
34from zope.pluggableauth.interfaces import IAuthenticatorPlugin
35
36BUFSIZE = 8 * 1024
37
38def remove_file_or_directory(filepath):
39    """Remove a file or directory.
40
41    Different to :func:`shutil.rmtree` we also accept not existing
42    paths (returning silently) and if a dir turns out to be a regular
43    file, we remove that.
44    """
45    filepath = os.path.abspath(filepath)
46    if not os.path.exists(filepath):
47        return
48    if os.path.isdir(filepath):
49        shutil.rmtree(filepath)
50    else:
51        os.unlink(filepath)
52    return
53
54def copy_filesystem_tree(src, dst, overwrite=False, del_old=False):
55    """Copy contents of directory src to directory dst.
56
57    Both directories must exists.
58
59    If `overwrite` is true, any same named objects will be
60    overwritten. Otherwise these files will not be touched.
61
62    If `del_old` is true, copied files and directories will be removed
63    from the src directory.
64
65    This functions returns a list of non-copied files.
66
67    Unix hidden files and directories (starting with '.') are not
68    processed by this function.
69    """
70    if not os.path.exists(src):
71        raise ValueError('source path does not exist: %s' % src)
72    if not os.path.exists(dst):
73        raise ValueError('destination path does not exist: %s' % dst)
74    if not os.path.isdir(src):
75        raise ValueError('source path is not a directory: %s' % src)
76    if not os.path.isdir(dst):
77        raise ValueError('destination path is not a directory: %s' % dst)
78    not_copied = []
79    for item in os.listdir(src):
80        if item.startswith('.'):
81            continue # We do not copy hidden stuff...
82        itemsrc = os.path.join(src, item)
83        itemdst = os.path.join(dst, item)
84
85        if os.path.exists(itemdst):
86            if overwrite is True:
87                remove_file_or_directory(itemdst)
88            else:
89                not_copied.append(item)
90                continue
91
92        if os.path.isdir(itemsrc):
93            shutil.copytree(itemsrc, itemdst)
94        else:
95            shutil.copy2(itemsrc, itemdst)
96        if del_old:
97            remove_file_or_directory(itemsrc)
98    return not_copied
99
100
101def get_inner_HTML_part(html_code):
102    """Return the 'inner' part of a complete HTML snippet.
103
104    If there is a form part, get this.
105
106    If there is no form part, try to return the body part contents.
107
108    If there is no body, return as-is.
109
110    Let's see how that works. If we deliver some doc with form, we
111    will get that form only:
112
113       >>> doc = '<html><form>My Form</form>Outside the form</html>'
114       >>> get_inner_HTML_part(doc)
115       '<form>My Form</form>'
116
117    No form? Then seek for a body part and get the contents:
118
119       >>> doc = '<html><body>My Body</body>Trailing Trash</html>'
120       >>> get_inner_HTML_part(doc)
121       'My Body'
122
123    If none of these is included, return what we got:
124
125       >>> doc = '<html>without body nor form</html>'
126       >>> get_inner_HTML_part(doc)
127       '<html>without body nor form</html>'
128
129    """
130
131    try:
132        result = re.match('^.+(<form[^\>]*>.*</form>).+$', html_code,
133                          re.DOTALL).groups()[0]
134        return result
135    except AttributeError:
136        # No <form> part included
137        try:
138            result = re.match('^.+<body[^\>]*>(.*)</body>.*$', html_code,
139                              re.DOTALL).groups()[0]
140            return result
141        except AttributeError:
142            # No <form> and no <body> tag...
143            pass
144    return html_code
145
146class FactoryBase(grok.GlobalUtility):
147    """A factory for things.
148
149    This is a baseclass for easier creation of factories. Factories
150    are utilities that are registered under a certain name and return
151    instances of certain classes when called.
152
153    In :mod:`waeup.kofa` we use factories extensively for
154    batching. While processing a batch some processors looks up a
155    factory to create real-world instances that then get filled with
156    data from imported CSV files.
157
158    To get rid of reimplementing the same stuff over and over again,
159    most notably the methods defined here, we offer this base class
160    (which will *not* be registered as a factory itself).
161
162    Real factories can then be created like this:
163
164       >>> import grok
165       >>> from waeup.kofa.utils.helpers import FactoryBase
166       >>> class MyObject(object):
167       ...   # Some class we want to get instances of.
168       ...   pass
169       >>> class MyObjectFactory(FactoryBase):
170       ...   # This is the factory for MyObject instances
171       ...   grok.name(u'waeup.kofa.factory.MyObject')
172       ...   factory = MyObject
173
174    That's it. It is essential to set the ``factory`` attribute, which
175    will determine the class of which instances should be created when
176    called. The given name must even be unique amongst all utilities
177    registered during runtime. While you can pick any name you like
178    you might want to prepend ``waeup.kofa.factory.`` to the name
179    string to make sure it does not clash with names of other
180    utilities one day.
181
182    Before all this works we have to grok the baseclass once and our
183    freshly defined factory. This executes all the component
184    registration stuff we don't want to do ourselves. In daily use
185    this is done automatically on startup of a :mod:`waeup.kofa`
186    system.
187
188       >>> grok.testing.grok('waeup.kofa.utils.helpers')
189       >>> grok.testing.grok_component(
190       ...    'MyObjectFactory', MyObjectFactory
191       ...  )
192       True
193
194    After grokking we (and processors) can create objects without
195    knowing about the location of the real class definition, just by
196    the factory name:
197
198       >>> from zope.component import createObject
199       >>> obj = createObject('waeup.kofa.factory.MyObject')
200       >>> isinstance(obj, MyObject)
201       True
202
203    We can also use the regular utility lookups to find our new
204    factory:
205
206       >>> from zope.component import getUtility
207       >>> from zope.component.interfaces import IFactory
208       >>> factory = getUtility(
209       ...   IFactory, name='waeup.kofa.factory.MyObject'
210       ...   )
211       >>> isinstance(factory, MyObjectFactory)
212       True
213
214    And this factory generates `MyObject` instances:
215
216       >>> obj = factory()
217       >>> isinstance(obj, MyObject)
218       True
219
220    """
221    grok.baseclass() # Do not grok this class, do not register us.
222    grok.implements(IFactory)
223    # You can override any of the following attributes in derived
224    # classes. The `grok.name` setting *must* even be set to some
225    # unique value.
226    grok.name(u'waeup.Factory')
227    title = u"Create instances of ``factory``.",
228    description = u"This factory instantiates new applicant instances."
229    factory = None
230
231    def __call__(self, *args, **kw):
232        """The main factory function.
233
234        Returns an instance of the requested object.
235        """
236        return self.factory()
237
238    def getInterfaces(self):
239        # Required by IFactory
240        return implementedBy(self.factory)
241
242def ReST2HTML_w_warnings(source_string):
243    """Convert a reStructuredText string to HTML preserving warnings.
244
245    Returns a tuple ``(<HTML_CODE>, <WARNINGS>)``, both being
246    strings. Where ``<HTML_CODE>`` is the HTML code generated from the
247    source string (in unicode), ``<WARNINGS>`` is a string containing
248    any warning messages or ``None``.
249
250    Regular multi-line ReStructuredText strings will be returned as
251    HTML code:
252
253        >>> from waeup.kofa.utils.helpers import ReST2HTML
254        >>> source = '''
255        ... Headline
256        ... ========
257        ...
258        ... - A list item
259        ... - Another item
260        ...
261        ... Thanks for watching!
262        ... '''
263        >>> html, warnings = ReST2HTML_w_warnings(source)
264        >>> print html
265        <div class="document" id="headline">
266        <h1 class="title">Headline</h1>
267        <BLANKLINE>
268        <ul class="simple">
269        <li>A list item</li>
270        <li>Another item</li>
271        </ul>
272        <p>Thanks for watching!</p>
273        </div>
274
275    Here no warnings happened, so the `warnings` are ``None``:
276
277        >>> warnings is None
278        True
279
280    If warnings happen then they can be retrieved in the returned
281    ``warnings``. We try to render an erraneous document:
282
283        >>> source = '''
284        ... Headline
285        ... ======
286        ...
287        ... Thanks for watching!
288        ... '''
289        >>> html, warnings = ReST2HTML_w_warnings(source)
290        >>> print html
291        <div class="document" id="headline">
292        <h1 class="title">Headline</h1>
293        <BLANKLINE>
294        <p>Thanks for watching!</p>
295        </div>
296
297        >>> print warnings
298        <string>:3: (WARNING/2) Title underline too short.
299        <BLANKLINE>
300        Headline
301        ======
302        <BLANKLINE>
303
304    As you can see, the warnings are not displayed inline the document
305    but can be retrieved from the returned warnings, which is a string
306    or ``None``.
307    """
308    warnings = StringIO()
309    fulldoc = publish_string(
310        source_string, writer_name='html4css1',
311        settings_overrides={
312            'report_level': 0,
313            'warning_stream': warnings,
314            })
315    warnings.seek(0)
316    warning_msgs = warnings.read()
317    if warning_msgs:
318        # Render again, this time with no warnings inline...
319        fulldoc =  publish_string(
320        source_string, writer_name='html4css1',
321        settings_overrides={
322            'report_level': 10000,
323            'halt_level': 10000,
324            'warning_stream': warnings,
325            })
326    if warning_msgs == '':
327        warning_msgs = None
328    result = get_inner_HTML_part(fulldoc).strip()
329    if not isinstance(result, unicode):
330        result = result.decode('utf-8')
331    return result, warning_msgs
332
333def ReST2HTML(source_string):
334    """Render a string containing ReStructuredText to HTML.
335
336    Any warnings about too short headings, etc. are silently
337    discarded. Use :func:`ReST2HTML_w_warnings` if you want to get any
338    warnings.
339
340    The returned string will be unicode.
341
342    A regular document will be rendered like this:
343
344        >>> source = '''
345        ... Headline
346        ... ========
347        ...
348        ... Thanks for watching!
349        ... '''
350        >>> html = ReST2HTML(source)
351        >>> print html
352        <div class="document" id="headline">
353        <h1 class="title">Headline</h1>
354        <BLANKLINE>
355        <p>Thanks for watching!</p>
356        </div>
357
358    A document with markup problems (here: the underline is too short)
359    will look similar:
360
361        >>> source = '''
362        ... Headline
363        ... ======
364        ...
365        ... Thanks for watching!
366        ... '''
367        >>> html = ReST2HTML(source)
368        >>> print html
369        <div class="document" id="headline">
370        <h1 class="title">Headline</h1>
371        <BLANKLINE>
372        <p>Thanks for watching!</p>
373        </div>
374
375    """
376    html, warnings = ReST2HTML_w_warnings(source_string)
377    return html
378
379def attrs_to_fields(cls):
380    """Turn the attributes of a class into FieldProperty instances.
381
382    With Python >= 2.6 we can even use this function as a class decorator.
383    """
384    iface = list(implementedBy(cls))[0]
385    for field_name in getFieldNames(iface):
386        setattr(cls, field_name, FieldProperty(iface[field_name]))
387    return cls
388
389def get_current_principal():
390    """Get the 'current' principal.
391
392    This method works without a request. Examining a request is the
393    regular (and recommended) way to get a principal involved
394    'currently'.
395
396    Use this method only if you really have no access to the current
397    request.
398
399    Returns ``None`` when no principal is involved (for instance
400    during tests).
401    """
402    try:
403        principal = getInteraction().participations[0].principal
404    except NoInteraction:
405        return None
406    except IndexError: # No participations present
407        return None
408    return principal
409
410def cmp_files(file_descr1, file_descr2):
411    """Compare two files by their file descriptors.
412
413    Returns ``True`` if both are equal, ``False`` otherwise.
414    """
415    file_descr1.seek(0)
416    file_descr2.seek(0)
417    while True:
418        b1 = file_descr1.read(BUFSIZE)
419        b2 = file_descr2.read(BUFSIZE)
420        if b1 != b2:
421            return False
422        if not b1:
423            return True
424
425def string_from_bytes(number):
426    """Turn a number into some textual representation.
427
428      Examples:
429
430        >>> string_from_bytes(1)
431        u'1 byte(s)'
432
433        >>> string_from_bytes(1025)
434        u'1 KB'
435
436        >>> string_from_bytes(1.5 * 1024*1024)
437        u'1.50 MB'
438
439        >>> string_from_bytes(673.286 * 1024**3)
440        u'673.29 GB'
441
442    """
443    if number < 1024:
444        return u'%s byte(s)' % (str(number),)
445    elif number < 1024**2:
446        return u'%s KB' % (number / 1024,)
447    elif number < 1024**3:
448        return u'%.2f MB' % (number / 1024**2,)
449    return u'%.2f GB' % (number / 1024**3,)
450
451def file_size(file_like_obj):
452    """Determine file size in most effective manner.
453
454    Returns the number of bytes in a file. This function works for
455    both, real files as well as file-like objects like cStringIO based
456    'files'.
457
458    Example:
459
460      >>> from cStringIO import StringIO
461      >>> file_size(StringIO('my file content'))
462      15
463
464    Please note that this function expects the file-like object passed
465    in to be at first reading position (it does no seek(0)) and that
466    when finished the file pointer might be at end of file.
467    """
468    if hasattr(file_like_obj, 'fileno'):
469        return os.fstat(file_like_obj.fileno())[6]
470    file_like_obj.seek(0, 2) # seek to last position in file
471    return file_like_obj.tell()
472
473def get_user_account(request):
474    """Return local user account.
475    """
476    principal_id = request.principal.id
477    authenticator = getUtility(IAuthenticatorPlugin, name='users')
478    account = authenticator.getAccount(principal_id)
479    return account
480
481def iface_names(iface, omit=[], exclude_attribs=True, exclude_methods=True):
482    """Get all attribute names of an interface.
483
484    Searches also base interfaces.
485
486    Names of fields that are pure attributes
487    (i.e. zope.interface.Attribute) or methods are excluded by
488    default.
489
490    Names of typical fields derived from zope.schema are included.
491
492    The `omit` paramter can give a list of names to exclude.
493
494    Returns an unsorted list of strings.
495    """
496    ifaces = [iface] + list(iface.getBases())
497    names = []
498    for item in ifaces:
499        names += item.names()
500    names = [x for x in names if x not in omit]
501    result = []
502    for name in names:
503        cls = iface.get(name).__class__
504        if exclude_attribs and cls is Attribute:
505            continue
506        if exclude_methods and cls is Method:
507            continue
508        result.append(name)
509    return result
510
511def get_sorted_preferred(tuples_iterable, preferred_list):
512    """Get a list of tuples (<TITLE>,<TOKEN>) with values in
513    `preferred_list` put in front.
514
515    The rest of the tuples iterable is returned in orginal order. This
516    is useful for putting default entries on top of (already sorted)
517    lists of choice values, for instance when sorting countries and
518    their code.
519
520    Sample:
521
522    We have a list of tuples with uppercase 'titles' and lowercase
523    'tokens'. This list is already sorted but we want certain values
524    of this list to show up before other values. For instance we want
525    to see the 'C' entry to come first.
526
527      >>> get_sorted_preferred([('A','a'), ('B','b'), ('C','c')],
528      ...                       ['c'])
529      (('C', 'c'), ('A', 'a'), ('B', 'b'))
530
531    i.e. the entry with 'c' as second value moved to head of result.
532
533    We can also require multiple entries at head of list:
534
535      >>> get_sorted_preferred([('A','a'), ('B','b'), ('C','c')],
536      ...                       ['b', 'c'])
537      (('B', 'b'), ('C', 'c'), ('A', 'a'))
538
539    We required the 'b' entry to come before the 'c' entry and then
540    the rest of the input list. That's what we got.
541
542    The result is returned as a tuple of tuples to keep order of values.
543    """
544    result = [None for x in preferred_list]
545    for title, code in tuples_iterable:
546        if code in preferred_list:
547            index = preferred_list.index(code)
548            result[index] = (title, code)
549        else:
550            result.append((title, code))
551    return tuple(result)
Note: See TracBrowser for help on using the repository browser.