Passed
Push — 2.x ( 353ef7...3874f5 )
by Jordi
05:23
created

bika.lims.api.get_cache_key()   A

Complexity

Conditions 2

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 18
rs 10
c 0
b 0
f 0
cc 2
nop 1
1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of SENAITE.CORE.
4
#
5
# SENAITE.CORE is free software: you can redistribute it and/or modify it under
6
# the terms of the GNU General Public License as published by the Free Software
7
# Foundation, version 2.
8
#
9
# This program is distributed in the hope that it will be useful, but WITHOUT
10
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12
# details.
13
#
14
# You should have received a copy of the GNU General Public License along with
15
# this program; if not, write to the Free Software Foundation, Inc., 51
16
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
#
18
# Copyright 2018-2021 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
import Missing
22
import re
23
import six
24
from AccessControl.PermissionRole import rolesForPermissionOn
25
from Acquisition import aq_base
26
from Acquisition import aq_inner
27
from Acquisition import aq_parent
28
from bika.lims import logger
29
from bika.lims.interfaces import IClient
30
from bika.lims.interfaces import IContact
31
from bika.lims.interfaces import ILabContact
32
from collections import OrderedDict
33
from datetime import datetime
34
from DateTime import DateTime
35
from datetime import timedelta
36
from DateTime.interfaces import DateTimeError
37
from itertools import groupby
38
from plone import api as ploneapi
39
from plone.api.exc import InvalidParameterError
40
from plone.app.layout.viewlets.content import ContentHistoryView
41
from plone.behavior.interfaces import IBehaviorAssignable
42
from plone.dexterity.interfaces import IDexterityContent
43
from plone.i18n.normalizer.interfaces import IFileNameNormalizer
44
from plone.i18n.normalizer.interfaces import IIDNormalizer
45
from plone.memoize.volatile import DontCache
46
from Products.Archetypes.atapi import DisplayList
47
from Products.Archetypes.BaseObject import BaseObject
48
from Products.CMFCore.interfaces import IFolderish
49
from Products.CMFCore.interfaces import ISiteRoot
50
from Products.CMFCore.utils import getToolByName
51
from Products.CMFCore.WorkflowCore import WorkflowException
52
from Products.CMFPlone.RegistrationTool import get_member_by_login_name
53
from Products.CMFPlone.utils import _createObjectByType
54
from Products.CMFPlone.utils import base_hasattr
55
from Products.CMFPlone.utils import safe_unicode
56
from Products.PlonePAS.tools.memberdata import MemberData
57
from Products.ZCatalog.interfaces import ICatalogBrain
58
from zope import globalrequest
59
from zope.annotation.interfaces import IAttributeAnnotatable
60
from zope.component import getUtility
61
from zope.component import queryMultiAdapter
62
from zope.component.interfaces import IFactory
63
from zope.event import notify
64
from zope.interface import directlyProvides
65
from zope.lifecycleevent import ObjectCreatedEvent
66
from zope.publisher.browser import TestRequest
67
from zope.schema import getFieldsInOrder
68
from zope.security.interfaces import Unauthorized
69
70
"""SENAITE LIMS Framework API
71
72
Please see bika.lims/docs/API.rst for documentation.
73
74
Architecural Notes:
75
76
Please add only functions that do a single thing for a single object.
77
78
Good: `def get_foo(brain_or_object)`
79
Bad:  `def get_foos(list_of_brain_objects)`
80
81
Why?
82
83
Because it makes things more complex. You can always use a pattern like this to
84
achieve the same::
85
86
    >>> foos = map(get_foo, list_of_brain_objects)
87
88
Please add for all of your functions a descriptive test in docs/API.rst.
89
90
Thanks.
91
"""
92
93
_marker = object()
94
95
UID_RX = re.compile("[a-z0-9]{32}$")
96
97
98
class APIError(Exception):
99
    """Base exception class for bika.lims errors."""
100
101
102
def get_portal():
103
    """Get the portal object
104
105
    :returns: Portal object
106
    """
107
    return ploneapi.portal.getSite()
108
109
110
def get_setup():
111
    """Fetch the `bika_setup` folder.
112
    """
113
    portal = get_portal()
114
    return portal.get("bika_setup")
115
116
117
def get_bika_setup():
118
    """Fetch the `bika_setup` folder.
119
    """
120
    return get_setup()
121
122
123
def get_senaite_setup():
124
    """Fetch the new DX `setup` folder.
125
    """
126
    portal = get_portal()
127
    return portal.get("setup")
128
129
130
def create(container, portal_type, *args, **kwargs):
131
    """Creates an object in Bika LIMS
132
133
    This code uses most of the parts from the TypesTool
134
    see: `Products.CMFCore.TypesTool._constructInstance`
135
136
    :param container: container
137
    :type container: ATContentType/DexterityContentType/CatalogBrain
138
    :param portal_type: The portal type to create, e.g. "Client"
139
    :type portal_type: string
140
    :param title: The title for the new content object
141
    :type title: string
142
    :returns: The new created object
143
    """
144
    from bika.lims.utils import tmpID
145
146
    id = kwargs.pop("id", tmpID())
147
    title = kwargs.pop("title", "New {}".format(portal_type))
148
149
    # get the fti
150
    types_tool = get_tool("portal_types")
151
    fti = types_tool.getTypeInfo(portal_type)
152
153
    if fti.product:
154
        obj = _createObjectByType(portal_type, container, id)
155
        obj.processForm()
156
        obj.edit(title=title, **kwargs)
157
    else:
158
        # newstyle factory
159
        factory = getUtility(IFactory, fti.factory)
160
        obj = factory(id, *args, **kwargs)
161
        if hasattr(obj, '_setPortalTypeName'):
162
            obj._setPortalTypeName(fti.getId())
163
        # set the title
164
        obj.title = safe_unicode(title)
165
        # notify that the object was created
166
        notify(ObjectCreatedEvent(obj))
167
        # notifies ObjectWillBeAddedEvent, ObjectAddedEvent and
168
        # ContainerModifiedEvent
169
        container._setObject(id, obj)
170
        # we get the object here with the current object id, as it might be
171
        # renamed already by an event handler
172
        obj = container._getOb(obj.getId())
173
174
    return obj
175
176
177
def get_tool(name, context=None, default=_marker):
178
    """Get a portal tool by name
179
180
    :param name: The name of the tool, e.g. `portal_catalog`
181
    :type name: string
182
    :param context: A portal object
183
    :type context: ATContentType/DexterityContentType/CatalogBrain
184
    :returns: Portal Tool
185
    """
186
187
    # Try first with the context
188
    if context is not None:
189
        try:
190
            context = get_object(context)
191
            return getToolByName(context, name)
192
        except (APIError, AttributeError) as e:
193
            # https://github.com/senaite/bika.lims/issues/396
194
            logger.warn("get_tool::getToolByName({}, '{}') failed: {} "
195
                        "-> falling back to plone.api.portal.get_tool('{}')"
196
                        .format(repr(context), name, repr(e), name))
197
            return get_tool(name, default=default)
198
199
    # Try with the plone api
200
    try:
201
        return ploneapi.portal.get_tool(name)
202
    except InvalidParameterError:
203
        if default is not _marker:
204
            if isinstance(default, six.string_types):
205
                return get_tool(default)
206
            return default
207
        fail("No tool named '%s' found." % name)
208
209
210
def fail(msg=None):
211
    """API LIMS Error
212
    """
213
    if msg is None:
214
        msg = "Reason not given."
215
    raise APIError("{}".format(msg))
216
217
218
def is_object(brain_or_object):
219
    """Check if the passed in object is a supported portal content object
220
221
    :param brain_or_object: A single catalog brain or content object
222
    :type brain_or_object: Portal Object
223
    :returns: True if the passed in object is a valid portal content
224
    """
225
    if is_portal(brain_or_object):
226
        return True
227
    if is_supermodel(brain_or_object):
228
        return True
229
    if is_at_content(brain_or_object):
230
        return True
231
    if is_dexterity_content(brain_or_object):
232
        return True
233
    if is_brain(brain_or_object):
234
        return True
235
    return False
236
237
238
def get_object(brain_object_uid, default=_marker):
239
    """Get the full content object
240
241
    :param brain_object_uid: A catalog brain or content object or uid
242
    :type brain_object_uid: PortalObject/ATContentType/DexterityContentType
243
    /CatalogBrain/basestring
244
    :returns: The full object
245
    """
246
    if is_uid(brain_object_uid):
247
        return get_object_by_uid(brain_object_uid)
248
    elif is_supermodel(brain_object_uid):
249
        return brain_object_uid.instance
250
    if not is_object(brain_object_uid):
251
        if default is _marker:
252
            fail("{} is not supported.".format(repr(brain_object_uid)))
253
        return default
254
    if is_brain(brain_object_uid):
255
        return brain_object_uid.getObject()
256
    return brain_object_uid
257
258
259
def is_portal(brain_or_object):
260
    """Checks if the passed in object is the portal root object
261
262
    :param brain_or_object: A single catalog brain or content object
263
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
264
    :returns: True if the object is the portal root object
265
    :rtype: bool
266
    """
267
    return ISiteRoot.providedBy(brain_or_object)
268
269
270
def is_brain(brain_or_object):
271
    """Checks if the passed in object is a portal catalog brain
272
273
    :param brain_or_object: A single catalog brain or content object
274
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
275
    :returns: True if the object is a catalog brain
276
    :rtype: bool
277
    """
278
    return ICatalogBrain.providedBy(brain_or_object)
279
280
281
def is_supermodel(brain_or_object):
282
    """Checks if the passed in object is a supermodel
283
284
    :param brain_or_object: A single catalog brain or content object
285
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
286
    :returns: True if the object is a catalog brain
287
    :rtype: bool
288
    """
289
    # avoid circular imports
290
    from senaite.app.supermodel.interfaces import ISuperModel
291
    return ISuperModel.providedBy(brain_or_object)
292
293
294
def is_dexterity_content(brain_or_object):
295
    """Checks if the passed in object is a dexterity content type
296
297
    :param brain_or_object: A single catalog brain or content object
298
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
299
    :returns: True if the object is a dexterity content type
300
    :rtype: bool
301
    """
302
    return IDexterityContent.providedBy(brain_or_object)
303
304
305
def is_at_content(brain_or_object):
306
    """Checks if the passed in object is an AT content type
307
308
    :param brain_or_object: A single catalog brain or content object
309
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
310
    :returns: True if the object is an AT content type
311
    :rtype: bool
312
    """
313
    return isinstance(brain_or_object, BaseObject)
314
315
316
def is_folderish(brain_or_object):
317
    """Checks if the passed in object is folderish
318
319
    :param brain_or_object: A single catalog brain or content object
320
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
321
    :returns: True if the object is folderish
322
    :rtype: bool
323
    """
324
    if hasattr(brain_or_object, "is_folderish"):
325
        if callable(brain_or_object.is_folderish):
326
            return brain_or_object.is_folderish()
327
        return brain_or_object.is_folderish
328
    return IFolderish.providedBy(get_object(brain_or_object))
329
330
331
def get_portal_type(brain_or_object):
332
    """Get the portal type for this object
333
334
    :param brain_or_object: A single catalog brain or content object
335
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
336
    :returns: Portal type
337
    :rtype: string
338
    """
339
    if not is_object(brain_or_object):
340
        fail("{} is not supported.".format(repr(brain_or_object)))
341
    return brain_or_object.portal_type
342
343
344
def get_schema(brain_or_object):
345
    """Get the schema of the content
346
347
    :param brain_or_object: A single catalog brain or content object
348
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
349
    :returns: Schema object
350
    """
351
    obj = get_object(brain_or_object)
352
    if is_portal(obj):
353
        fail("get_schema can't return schema of portal root")
354
    if is_dexterity_content(obj):
355
        pt = get_tool("portal_types")
356
        fti = pt.getTypeInfo(obj.portal_type)
357
        return fti.lookupSchema()
358
    if is_at_content(obj):
359
        return obj.Schema()
360
    fail("{} has no Schema.".format(brain_or_object))
361
362
363
def get_fields(brain_or_object):
364
    """Get a name to field mapping of the object
365
366
    :param brain_or_object: A single catalog brain or content object
367
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
368
    :returns: Mapping of name -> field
369
    :rtype: OrderedDict
370
    """
371
    obj = get_object(brain_or_object)
372
    schema = get_schema(obj)
373
    if is_dexterity_content(obj):
374
        # get the fields directly provided by the interface
375
        fields = getFieldsInOrder(schema)
376
        # append the fields coming from behaviors
377
        behavior_assignable = IBehaviorAssignable(obj)
378
        if behavior_assignable:
379
            behaviors = behavior_assignable.enumerateBehaviors()
380
            for behavior in behaviors:
381
                fields.extend(getFieldsInOrder(behavior.interface))
382
        return OrderedDict(fields)
383
    return OrderedDict(schema)
384
385
386
def get_id(brain_or_object):
387
    """Get the Plone ID for this object
388
389
    :param brain_or_object: A single catalog brain or content object
390
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
391
    :returns: Plone ID
392
    :rtype: string
393
    """
394
    if is_brain(brain_or_object):
395
        if base_hasattr(brain_or_object, "getId"):
396
            return brain_or_object.getId
397
        if base_hasattr(brain_or_object, "id"):
398
            return brain_or_object.id
399
    return get_object(brain_or_object).getId()
400
401
402
def get_title(brain_or_object):
403
    """Get the Title for this object
404
405
    :param brain_or_object: A single catalog brain or content object
406
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
407
    :returns: Title
408
    :rtype: string
409
    """
410
    if is_brain(brain_or_object) and base_hasattr(brain_or_object, "Title"):
411
        return brain_or_object.Title
412
    return get_object(brain_or_object).Title()
413
414
415
def get_description(brain_or_object):
416
    """Get the Title for this object
417
418
    :param brain_or_object: A single catalog brain or content object
419
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
420
    :returns: Title
421
    :rtype: string
422
    """
423
    if is_brain(brain_or_object) \
424
            and base_hasattr(brain_or_object, "Description"):
425
        return brain_or_object.Description
426
    return get_object(brain_or_object).Description()
427
428
429
def get_uid(brain_or_object):
430
    """Get the Plone UID for this object
431
432
    :param brain_or_object: A single catalog brain or content object or an UID
433
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
434
    :returns: Plone UID
435
    :rtype: string
436
    """
437
    if is_uid(brain_or_object):
438
        return brain_or_object
439
    if is_portal(brain_or_object):
440
        return '0'
441
    if is_brain(brain_or_object) and base_hasattr(brain_or_object, "UID"):
442
        return brain_or_object.UID
443
    return get_object(brain_or_object).UID()
444
445
446
def get_url(brain_or_object):
447
    """Get the absolute URL for this object
448
449
    :param brain_or_object: A single catalog brain or content object
450
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
451
    :returns: Absolute URL
452
    :rtype: string
453
    """
454
    if is_brain(brain_or_object) and base_hasattr(brain_or_object, "getURL"):
455
        return brain_or_object.getURL()
456
    return get_object(brain_or_object).absolute_url()
457
458
459
def get_icon(brain_or_object, html_tag=True):
460
    """Get the icon of the content object
461
462
    :param brain_or_object: A single catalog brain or content object
463
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
464
    :param html_tag: A value of 'True' returns the HTML tag, else the image url
465
    :type html_tag: bool
466
    :returns: HTML '<img>' tag if 'html_tag' is True else the image url
467
    :rtype: string
468
    """
469
    # Manual approach, because `plone.app.layout.getIcon` does not reliable
470
    # work for Contents coming from other catalogs than the
471
    # `portal_catalog`
472
    portal_types = get_tool("portal_types")
473
    fti = portal_types.getTypeInfo(brain_or_object.portal_type)
474
    icon = fti.getIcon()
475
    if not icon:
476
        return ""
477
    url = "%s/%s" % (get_url(get_portal()), icon)
478
    if not html_tag:
479
        return url
480
    tag = '<img width="16" height="16" src="{url}" title="{title}" />'.format(
481
        url=url, title=get_title(brain_or_object))
482
    return tag
483
484
485
def get_object_by_uid(uid, default=_marker):
486
    """Find an object by a given UID
487
488
    :param uid: The UID of the object to find
489
    :type uid: string
490
    :returns: Found Object or None
491
    """
492
493
    # nothing to do here
494
    if not uid:
495
        if default is not _marker:
496
            return default
497
        fail("get_object_by_uid requires UID as first argument; got {} instead"
498
             .format(uid))
499
500
    # we defined the portal object UID to be '0'::
501
    if uid == '0':
502
        return get_portal()
503
504
    brain = get_brain_by_uid(uid)
505
506
    if brain is None:
507
        if default is not _marker:
508
            return default
509
        fail("No object found for UID {}".format(uid))
510
511
    return get_object(brain)
512
513
514
def get_brain_by_uid(uid, default=None):
515
    """Query a brain by a given UID
516
517
    :param uid: The UID of the object to find
518
    :type uid: string
519
    :returns: ZCatalog brain or None
520
    """
521
    if not is_uid(uid):
522
        return default
523
524
    # we try to find the object with the UID catalog
525
    uc = get_tool("uid_catalog")
526
527
    # try to find the object with the reference catalog first
528
    brains = uc(UID=uid)
529
    if len(brains) != 1:
530
        return default
531
    return brains[0]
532
533
534
def get_object_by_path(path, default=_marker):
535
    """Find an object by a given physical path or absolute_url
536
537
    :param path: The physical path of the object to find
538
    :type path: string
539
    :returns: Found Object or None
540
    """
541
542
    # nothing to do here
543
    if not path:
544
        if default is not _marker:
545
            return default
546
        fail("get_object_by_path first argument must be a path; {} received"
547
             .format(path))
548
549
    portal = get_portal()
550
    portal_path = get_path(portal)
551
    portal_url = get_url(portal)
552
553
    # ensure we have a physical path
554
    if path.startswith(portal_url):
555
        request = get_request()
556
        path = "/".join(request.physicalPathFromURL(path))
557
558
    if not path.startswith(portal_path):
559
        if default is not _marker:
560
            return default
561
        fail("Not a physical path inside the portal.")
562
563
    if path == portal_path:
564
        return portal
565
566
    return portal.restrictedTraverse(path, default)
567
568
569
def get_path(brain_or_object):
570
    """Calculate the physical path of this object
571
572
    :param brain_or_object: A single catalog brain or content object
573
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
574
    :returns: Physical path of the object
575
    :rtype: string
576
    """
577
    if is_brain(brain_or_object):
578
        return brain_or_object.getPath()
579
    return "/".join(get_object(brain_or_object).getPhysicalPath())
580
581
582
def get_parent_path(brain_or_object):
583
    """Calculate the physical parent path of this object
584
585
    :param brain_or_object: A single catalog brain or content object
586
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
587
    :returns: Physical path of the parent object
588
    :rtype: string
589
    """
590
    if is_portal(brain_or_object):
591
        return get_path(get_portal())
592
    if is_brain(brain_or_object):
593
        path = get_path(brain_or_object)
594
        return path.rpartition("/")[0]
595
    return get_path(get_object(brain_or_object).aq_parent)
596
597
598
def get_parent(brain_or_object, catalog_search=False):
599
    """Locate the parent object of the content/catalog brain
600
601
    The `catalog_search` switch uses the `portal_catalog` to do a search return
602
    a brain instead of the full parent object. However, if the search returned
603
    no results, it falls back to return the full parent object.
604
605
    :param brain_or_object: A single catalog brain or content object
606
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
607
    :param catalog_search: Use a catalog query to find the parent object
608
    :type catalog_search: bool
609
    :returns: parent object
610
    :rtype: ATContentType/DexterityContentType/PloneSite/CatalogBrain
611
    """
612
613
    if is_portal(brain_or_object):
614
        return get_portal()
615
616
    # Do a catalog search and return the brain
617
    if catalog_search:
618
        parent_path = get_parent_path(brain_or_object)
619
620
        # parent is the portal object
621
        if parent_path == get_path(get_portal()):
622
            return get_portal()
623
624
        # get the catalog tool
625
        pc = get_portal_catalog()
626
627
        # query for the parent path
628
        results = pc(path={
629
            "query": parent_path,
630
            "depth": 0})
631
632
        # No results fallback: return the parent object
633
        if not results:
634
            return get_object(brain_or_object).aq_parent
635
636
        # return the brain
637
        return results[0]
638
639
    return get_object(brain_or_object).aq_parent
640
641
642
def search(query, catalog=_marker):
643
    """Search for objects.
644
645
    :param query: A suitable search query.
646
    :type query: dict
647
    :param catalog: A single catalog id or a list of catalog ids
648
    :type catalog: str/list
649
    :returns: Search results
650
    :rtype: List of ZCatalog brains
651
    """
652
653
    # query needs to be a dictionary
654
    if not isinstance(query, dict):
655
        fail("Catalog query needs to be a dictionary")
656
657
    # Portal types to query
658
    portal_types = query.get("portal_type", [])
659
    # We want the portal_type as a list
660
    if not isinstance(portal_types, (tuple, list)):
661
        portal_types = [portal_types]
662
663
    # The catalogs used for the query
664
    catalogs = []
665
666
    # The user did **not** specify a catalog
667
    if catalog is _marker:
668
        # Find the registered catalogs for the queried portal types
669
        for portal_type in portal_types:
670
            # Just get the first registered/default catalog
671
            catalogs.append(get_catalogs_for(
672
                portal_type, default="portal_catalog")[0])
673
    else:
674
        # User defined catalogs
675
        if isinstance(catalog, (list, tuple)):
676
            catalogs.extend(map(get_tool, catalog))
677
        else:
678
            catalogs.append(get_tool(catalog))
679
680
    # Cleanup: Avoid duplicate catalogs
681
    catalogs = list(set(catalogs)) or [get_portal_catalog()]
682
683
    # We only support **single** catalog queries
684
    if len(catalogs) > 1:
685
        fail("Multi Catalog Queries are not supported!")
686
687
    return catalogs[0](query)
688
689
690
def safe_getattr(brain_or_object, attr, default=_marker):
691
    """Return the attribute value
692
693
    :param brain_or_object: A single catalog brain or content object
694
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
695
    :param attr: Attribute name
696
    :type attr: str
697
    :returns: Attribute value
698
    :rtype: obj
699
    """
700
    try:
701
        value = getattr(brain_or_object, attr, _marker)
702
        if value is _marker:
703
            if default is not _marker:
704
                return default
705
            fail("Attribute '{}' not found.".format(attr))
706
        if callable(value):
707
            return value()
708
        return value
709
    except Unauthorized:
710
        if default is not _marker:
711
            return default
712
        fail("You are not authorized to access '{}' of '{}'.".format(
713
            attr, repr(brain_or_object)))
714
715
716
def get_portal_catalog():
717
    """Get the portal catalog tool
718
719
    :returns: Portal Catalog Tool
720
    """
721
    return get_tool("portal_catalog")
722
723
724
def get_review_history(brain_or_object, rev=True):
725
    """Get the review history for the given brain or context.
726
727
    :param brain_or_object: A single catalog brain or content object
728
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
729
    :returns: Workflow history
730
    :rtype: [{}, ...]
731
    """
732
    obj = get_object(brain_or_object)
733
    review_history = []
734
    try:
735
        workflow = get_tool("portal_workflow")
736
        review_history = workflow.getInfoFor(obj, 'review_history')
737
    except WorkflowException as e:
738
        message = str(e)
739
        logger.error("Cannot retrieve review_history on {}: {}".format(
740
            obj, message))
741
    if not isinstance(review_history, (list, tuple)):
742
        logger.error("get_review_history: expected list, received {}".format(
743
            review_history))
744
        review_history = []
745
746
    if isinstance(review_history, tuple):
747
        # Products.CMFDefault.DefaultWorkflow.getInfoFor always returns a
748
        # tuple when "review_history" is passed in:
749
        # https://github.com/zopefoundation/Products.CMFDefault/blob/master/Products/CMFDefault/DefaultWorkflow.py#L244
750
        #
751
        # On the other hand, Products.DCWorkflow.getInfoFor relies on
752
        # Expression.StateChangeInfo, that always returns a list, except when no
753
        # review_history is found:
754
        # https://github.com/zopefoundation/Products.DCWorkflow/blob/master/Products/DCWorkflow/DCWorkflow.py#L310
755
        # https://github.com/zopefoundation/Products.DCWorkflow/blob/master/Products/DCWorkflow/Expression.py#L94
756
        review_history = list(review_history)
757
758
    if rev is True:
759
        review_history.reverse()
760
    return review_history
761
762
763
def get_revision_history(brain_or_object):
764
    """Get the revision history for the given brain or context.
765
766
    :param brain_or_object: A single catalog brain or content object
767
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
768
    :returns: Workflow history
769
    :rtype: obj
770
    """
771
    obj = get_object(brain_or_object)
772
    chv = ContentHistoryView(obj, safe_getattr(obj, "REQUEST", None))
773
    return chv.fullHistory()
774
775
776
def get_workflows_for(brain_or_object):
777
    """Get the assigned workflows for the given brain or context.
778
779
    Note: This function supports also the portal_type as parameter.
780
781
    :param brain_or_object: A single catalog brain or content object
782
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
783
    :returns: Assigned Workflows
784
    :rtype: tuple
785
    """
786
    workflow = ploneapi.portal.get_tool("portal_workflow")
787
    if isinstance(brain_or_object, six.string_types):
788
        return workflow.getChainFor(brain_or_object)
789
    obj = get_object(brain_or_object)
790
    return workflow.getChainFor(obj)
791
792
793
def get_workflow_status_of(brain_or_object, state_var="review_state"):
794
    """Get the current workflow status of the given brain or context.
795
796
    :param brain_or_object: A single catalog brain or content object
797
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
798
    :param state_var: The name of the state variable
799
    :type state_var: string
800
    :returns: Status
801
    :rtype: str
802
    """
803
    # Try to get the state from the catalog brain first
804
    if is_brain(brain_or_object):
805
        if state_var in brain_or_object.schema():
806
            return brain_or_object[state_var]
807
808
    # Retrieve the sate from the object
809
    workflow = get_tool("portal_workflow")
810
    obj = get_object(brain_or_object)
811
    return workflow.getInfoFor(ob=obj, name=state_var, default='')
812
813
814
def get_previous_worfklow_status_of(brain_or_object, skip=None, default=None):
815
    """Get the previous workflow status of the object
816
817
    :param brain_or_object: A single catalog brain or content object
818
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
819
    :param skip: Workflow states to skip
820
    :type skip: tuple/list
821
    :returns: status
822
    :rtype: str
823
    """
824
825
    skip = isinstance(skip, (list, tuple)) and skip or []
826
    history = get_review_history(brain_or_object)
827
828
    # Remove consecutive duplicates, some transitions might happen more than
829
    # once consecutively (e.g. publish)
830
    history = map(lambda i: i[0], groupby(history))
831
832
    for num, item in enumerate(history):
833
        # skip the current history entry
834
        if num == 0:
835
            continue
836
        status = item.get("review_state")
837
        if status in skip:
838
            continue
839
        return status
840
    return default
841
842
843
def get_creation_date(brain_or_object):
844
    """Get the creation date of the brain or object
845
846
    :param brain_or_object: A single catalog brain or content object
847
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
848
    :returns: Creation date
849
    :rtype: DateTime
850
    """
851
    created = getattr(brain_or_object, "created", None)
852
    if created is None:
853
        fail("Object {} has no creation date ".format(
854
             repr(brain_or_object)))
855
    if callable(created):
856
        return created()
857
    return created
858
859
860
def get_modification_date(brain_or_object):
861
    """Get the modification date of the brain or object
862
863
    :param brain_or_object: A single catalog brain or content object
864
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
865
    :returns: Modification date
866
    :rtype: DateTime
867
    """
868
    modified = getattr(brain_or_object, "modified", None)
869
    if modified is None:
870
        fail("Object {} has no modification date ".format(
871
             repr(brain_or_object)))
872
    if callable(modified):
873
        return modified()
874
    return modified
875
876
877
def get_review_status(brain_or_object):
878
    """Get the `review_state` of an object
879
880
    :param brain_or_object: A single catalog brain or content object
881
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
882
    :returns: Value of the review_status variable
883
    :rtype: String
884
    """
885
    if is_brain(brain_or_object):
886
        return brain_or_object.review_state
887
    return get_workflow_status_of(brain_or_object, state_var="review_state")
888
889
890
def is_active(brain_or_object):
891
    """Check if the workflow state of the object is 'inactive' or 'cancelled'.
892
893
    :param brain_or_object: A single catalog brain or content object
894
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
895
    :returns: False if the object is in the state 'inactive' or 'cancelled'
896
    :rtype: bool
897
    """
898
    if get_review_status(brain_or_object) in ["cancelled", "inactive"]:
899
        return False
900
    return True
901
902
903
def get_catalogs_for(brain_or_object, default="portal_catalog"):
904
    """Get all registered catalogs for the given portal_type, catalog brain or
905
    content object
906
907
    :param brain_or_object: The portal_type, a catalog brain or content object
908
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
909
    :returns: List of supported catalogs
910
    :rtype: list
911
    """
912
    archetype_tool = get_tool("archetype_tool", default=None)
913
    if archetype_tool is None:
914
        # return the default catalog
915
        return [get_tool(default, default="portal_catalog")]
916
917
    catalogs = []
918
919
    # get the registered catalogs for portal_type
920
    if is_object(brain_or_object):
921
        catalogs = archetype_tool.getCatalogsByType(
922
            get_portal_type(brain_or_object))
923
    if isinstance(brain_or_object, six.string_types):
924
        catalogs = archetype_tool.getCatalogsByType(brain_or_object)
925
926
    if not catalogs:
927
        return [get_tool(default)]
928
    return catalogs
929
930
931
def get_transitions_for(brain_or_object):
932
    """List available workflow transitions for all workflows
933
934
    :param brain_or_object: A single catalog brain or content object
935
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
936
    :returns: All possible available and allowed transitions
937
    :rtype: list[dict]
938
    """
939
    workflow = get_tool('portal_workflow')
940
    transitions = []
941
    instance = get_object(brain_or_object)
942
    for wfid in get_workflows_for(brain_or_object):
943
        wf = workflow[wfid]
944
        tlist = wf.getTransitionsFor(instance)
945
        transitions.extend([t for t in tlist if t not in transitions])
946
    return transitions
947
948
949
def do_transition_for(brain_or_object, transition):
950
    """Performs a workflow transition for the passed in object.
951
952
    :param brain_or_object: A single catalog brain or content object
953
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
954
    :returns: The object where the transtion was performed
955
    """
956
    if not isinstance(transition, six.string_types):
957
        fail("Transition type needs to be string, got '%s'" % type(transition))
958
    obj = get_object(brain_or_object)
959
    try:
960
        ploneapi.content.transition(obj, transition)
961
    except ploneapi.exc.InvalidParameterError as e:
962
        fail("Failed to perform transition '{}' on {}: {}".format(
963
             transition, obj, str(e)))
964
    return obj
965
966
967
def get_roles_for_permission(permission, brain_or_object):
968
    """Get a list of granted roles for the given permission on the object.
969
970
    :param brain_or_object: A single catalog brain or content object
971
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
972
    :returns: Roles for the given Permission
973
    :rtype: list
974
    """
975
    obj = get_object(brain_or_object)
976
    allowed = set(rolesForPermissionOn(permission, obj))
977
    return sorted(allowed)
978
979
980
def is_versionable(brain_or_object, policy='at_edit_autoversion'):
981
    """Checks if the passed in object is versionable.
982
983
    :param brain_or_object: A single catalog brain or content object
984
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
985
    :returns: True if the object is versionable
986
    :rtype: bool
987
    """
988
    pr = get_tool("portal_repository")
989
    obj = get_object(brain_or_object)
990
    return pr.supportsPolicy(obj, 'at_edit_autoversion') \
991
        and pr.isVersionable(obj)
992
993
994
def get_version(brain_or_object):
995
    """Get the version of the current object
996
997
    :param brain_or_object: A single catalog brain or content object
998
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
999
    :returns: The current version of the object, or None if not available
1000
    :rtype: int or None
1001
    """
1002
    obj = get_object(brain_or_object)
1003
    if not is_versionable(obj):
1004
        return None
1005
    return getattr(aq_base(obj), "version_id", 0)
1006
1007
1008
def get_view(name, context=None, request=None, default=None):
1009
    """Get the view by name
1010
1011
    :param name: The name of the view
1012
    :type name: str
1013
    :param context: The context to query the view
1014
    :type context: ATContentType/DexterityContentType/CatalogBrain
1015
    :param request: The request to query the view
1016
    :type request: HTTPRequest object
1017
    :returns: HTTP Request
1018
    :rtype: Products.Five.metaclass View object
1019
    """
1020
    context = context or get_portal()
1021
    request = request or get_request() or None
1022
    view = queryMultiAdapter((get_object(context), request), name=name)
1023
    if view is None:
1024
        return default
1025
    return view
1026
1027
1028
def get_request():
1029
    """Get the global request object
1030
1031
    :returns: HTTP Request
1032
    :rtype: HTTPRequest object
1033
    """
1034
    return globalrequest.getRequest()
1035
1036
1037
def get_test_request():
1038
    """Get the TestRequest object
1039
    """
1040
    request = TestRequest()
1041
    directlyProvides(request, IAttributeAnnotatable)
1042
    return request
1043
1044
1045
def get_group(group_or_groupname):
1046
    """Return Plone Group
1047
1048
    :param group_or_groupname: Plone group or the name of the group
1049
    :type groupname:  GroupData/str
1050
    :returns: Plone GroupData
1051
    """
1052
    if not group_or_groupname:
1053
1054
        return None
1055
    if hasattr(group_or_groupname, "_getGroup"):
1056
        return group_or_groupname
1057
    gtool = get_tool("portal_groups")
1058
    return gtool.getGroupById(group_or_groupname)
1059
1060
1061
def get_user(user_or_username):
1062
    """Return Plone User
1063
1064
    :param user_or_username: Plone user or user id
1065
    :returns: Plone MemberData
1066
    """
1067
    user = None
1068
    if isinstance(user_or_username, MemberData):
1069
        user = user_or_username
1070
    if isinstance(user_or_username, six.string_types):
1071
        user = get_member_by_login_name(get_portal(), user_or_username, False)
1072
    return user
1073
1074
1075
def get_user_properties(user_or_username):
1076
    """Return User Properties
1077
1078
    :param user_or_username: Plone group identifier
1079
    :returns: Plone MemberData
1080
    """
1081
    user = get_user(user_or_username)
1082
    if user is None:
1083
        return {}
1084
    if not callable(user.getUser):
1085
        return {}
1086
    out = {}
1087
    plone_user = user.getUser()
1088
    for sheet in plone_user.listPropertysheets():
1089
        ps = plone_user.getPropertysheet(sheet)
1090
        out.update(dict(ps.propertyItems()))
1091
    return out
1092
1093
1094
def get_users_by_roles(roles=None):
1095
    """Search Plone users by their roles
1096
1097
    :param roles: Plone role name or list of roles
1098
    :type roles:  list/str
1099
    :returns: List of Plone users having the role(s)
1100
    """
1101
    if not isinstance(roles, (tuple, list)):
1102
        roles = [roles]
1103
    mtool = get_tool("portal_membership")
1104
    return mtool.searchForMembers(roles=roles)
1105
1106
1107
def get_current_user():
1108
    """Returns the current logged in user
1109
1110
    :returns: Current User
1111
    """
1112
    return ploneapi.user.get_current()
1113
1114
1115
def get_user_contact(user, contact_types=['Contact', 'LabContact']):
1116
    """Returns the associated contact of a Plone user
1117
1118
    If the user passed in has no contact associated, return None.
1119
    The `contact_types` parameter filter the portal types for the search.
1120
1121
    :param: Plone user
1122
    :contact_types: List with the contact portal types to search
1123
    :returns: Contact associated to the Plone user or None
1124
    """
1125
    if not user:
1126
        return None
1127
1128
    query = {'portal_type': contact_types, 'getUsername': user.id}
1129
    brains = search(query, catalog='portal_catalog')
1130
    if not brains:
1131
        return None
1132
1133
    if len(brains) > 1:
1134
        # Oops, the user has multiple contacts assigned, return None
1135
        contacts = map(lambda c: c.Title, brains)
1136
        err_msg = "User '{}' is bound to multiple Contacts '{}'"
1137
        err_msg = err_msg.format(user.id, ','.join(contacts))
1138
        logger.error(err_msg)
1139
        return None
1140
1141
    return get_object(brains[0])
1142
1143
1144
def get_user_client(user_or_contact):
1145
    """Returns the client of the contact of a Plone user
1146
1147
    If the user passed in has no contact or does not belong to any client,
1148
    returns None.
1149
1150
    :param: Plone user or contact
1151
    :returns: Client the contact of the Plone user belongs to
1152
    """
1153
    if not user_or_contact or ILabContact.providedBy(user_or_contact):
1154
        # Lab contacts cannot belong to a client
1155
        return None
1156
1157
    if not IContact.providedBy(user_or_contact):
1158
        contact = get_user_contact(user_or_contact, contact_types=['Contact'])
1159
        if IContact.providedBy(contact):
1160
            return get_user_client(contact)
1161
        return None
1162
1163
    client = get_parent(user_or_contact)
1164
    if client and IClient.providedBy(client):
1165
        return client
1166
1167
    return None
1168
1169
1170
def get_current_client():
1171
    """Returns the current client the current logged in user belongs to, if any
1172
1173
    :returns: Client the current logged in user belongs to or None
1174
    """
1175
    return get_user_client(get_current_user())
1176
1177
1178
def get_cache_key(brain_or_object):
1179
    """Generate a cache key for a common brain or object
1180
1181
    :param brain_or_object: A single catalog brain or content object
1182
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
1183
    :returns: Cache Key
1184
    :rtype: str
1185
    """
1186
    key = [
1187
        get_portal_type(brain_or_object),
1188
        get_id(brain_or_object),
1189
        get_uid(brain_or_object),
1190
        # handle different domains gracefully
1191
        get_url(brain_or_object),
1192
        # Return the microsecond since the epoch in GMT
1193
        get_modification_date(brain_or_object).micros(),
1194
    ]
1195
    return "-".join(map(lambda x: str(x), key))
1196
1197
1198
def bika_cache_key_decorator(method, self, brain_or_object):
1199
    """Bika cache key decorator usable for
1200
1201
    :param brain_or_object: A single catalog brain or content object
1202
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
1203
    :returns: Cache Key
1204
    :rtype: str
1205
    """
1206
    if brain_or_object is None:
1207
        raise DontCache
1208
    return get_cache_key(brain_or_object)
1209
1210
1211
def normalize_id(string):
1212
    """Normalize the id
1213
1214
    :param string: A string to normalize
1215
    :type string: str
1216
    :returns: Normalized ID
1217
    :rtype: str
1218
    """
1219
    if not isinstance(string, six.string_types):
1220
        fail("Type of argument must be string, found '{}'"
1221
             .format(type(string)))
1222
    # get the id nomalizer utility
1223
    normalizer = getUtility(IIDNormalizer).normalize
1224
    return normalizer(string)
1225
1226
1227
def normalize_filename(string):
1228
    """Normalize the filename
1229
1230
    :param string: A string to normalize
1231
    :type string: str
1232
    :returns: Normalized ID
1233
    :rtype: str
1234
    """
1235
    if not isinstance(string, six.string_types):
1236
        fail("Type of argument must be string, found '{}'"
1237
             .format(type(string)))
1238
    # get the file nomalizer utility
1239
    normalizer = getUtility(IFileNameNormalizer).normalize
1240
    return normalizer(string)
1241
1242
1243
def is_uid(uid, validate=False):
1244
    """Checks if the passed in uid is a valid UID
1245
1246
    :param uid: The uid to check
1247
    :param validate: If False, checks if uid is a valid 23 alphanumeric uid. If
1248
    True, also verifies if a brain exists for the uid passed in
1249
    :type uid: string
1250
    :return: True if a valid uid
1251
    :rtype: bool
1252
    """
1253
    if not isinstance(uid, six.string_types):
1254
        return False
1255
    if uid == '0':
1256
        return True
1257
    if len(uid) != 32:
1258
        return False
1259
    if not UID_RX.match(uid):
1260
        return False
1261
    if not validate:
1262
        return True
1263
1264
    # Check if a brain for this uid exists
1265
    uc = get_tool('uid_catalog')
1266
    brains = uc(UID=uid)
1267
    if brains:
1268
        assert (len(brains) == 1)
1269
    return len(brains) > 0
1270
1271
1272
def is_date(date):
1273
    """Checks if the passed in value is a valid Zope's DateTime
1274
1275
    :param date: The date to check
1276
    :type date: DateTime
1277
    :return: True if a valid date
1278
    :rtype: bool
1279
    """
1280
    if not date:
1281
        return False
1282
    return isinstance(date, (DateTime, datetime))
1283
1284
1285
def to_date(value, default=None):
1286
    """Tries to convert the passed in value to Zope's DateTime
1287
1288
    :param value: The value to be converted to a valid DateTime
1289
    :type value: str, DateTime or datetime
1290
    :return: The DateTime representation of the value passed in or default
1291
    """
1292
    if isinstance(value, DateTime):
1293
        return value
1294
    if not value:
1295
        if default is None:
1296
            return None
1297
        return to_date(default)
1298
    try:
1299
        if isinstance(value, str) and '.' in value:
1300
            # https://docs.plone.org/develop/plone/misc/datetime.html#datetime-problems-and-pitfalls
1301
            return DateTime(value, datefmt='international')
1302
        return DateTime(value)
1303
    except (TypeError, ValueError, DateTimeError):
1304
        return to_date(default)
1305
1306
1307
def to_minutes(days=0, hours=0, minutes=0, seconds=0, milliseconds=0,
1308
               round_to_int=True):
1309
    """Returns the computed total number of minutes
1310
    """
1311
    total = float(days)*24*60 + float(hours)*60 + float(minutes) + \
1312
        float(seconds)/60 + float(milliseconds)/1000/60
1313
    return int(round(total)) if round_to_int else total
1314
1315
1316
def to_dhm_format(days=0, hours=0, minutes=0, seconds=0, milliseconds=0):
1317
    """Returns a representation of time in a string in xd yh zm format
1318
    """
1319
    minutes = to_minutes(days=days, hours=hours, minutes=minutes,
1320
                         seconds=seconds, milliseconds=milliseconds)
1321
    delta = timedelta(minutes=int(round(minutes)))
1322
    d = delta.days
1323
    h = delta.seconds // 3600
1324
    m = (delta.seconds // 60) % 60
1325
    m = m and "{}m ".format(str(m)) or ""
1326
    d = d and "{}d ".format(str(d)) or ""
1327
    if m and d:
1328
        h = "{}h ".format(str(h))
1329
    else:
1330
        h = h and "{}h ".format(str(h)) or ""
1331
    return "".join([d, h, m]).strip()
1332
1333
1334
def to_int(value, default=_marker):
1335
    """Tries to convert the value to int.
1336
    Truncates at the decimal point if the value is a float
1337
1338
    :param value: The value to be converted to an int
1339
    :return: The resulting int or default
1340
    """
1341
    if is_floatable(value):
1342
        value = to_float(value)
1343
    try:
1344
        return int(value)
1345
    except (TypeError, ValueError):
1346
        if default is None:
1347
            return default
1348
        if default is not _marker:
1349
            return to_int(default)
1350
        fail("Value %s cannot be converted to int" % repr(value))
1351
1352
1353
def is_floatable(value):
1354
    """Checks if the passed in value is a valid floatable number
1355
1356
    :param value: The value to be evaluated as a float number
1357
    :type value: str, float, int
1358
    :returns: True if is a valid float number
1359
    :rtype: bool"""
1360
    try:
1361
        float(value)
1362
        return True
1363
    except (TypeError, ValueError):
1364
        return False
1365
1366
1367
def to_float(value, default=_marker):
1368
    """Converts the passed in value to a float number
1369
1370
    :param value: The value to be converted to a floatable number
1371
    :type value: str, float, int
1372
    :returns: The float number representation of the passed in value
1373
    :rtype: float
1374
    """
1375
    if not is_floatable(value):
1376
        if default is not _marker:
1377
            return to_float(default)
1378
        fail("Value %s is not floatable" % repr(value))
1379
    return float(value)
1380
1381
1382
def to_searchable_text_metadata(value):
1383
    """Parse the given metadata value to searchable text
1384
1385
    :param value: The raw value of the metadata column
1386
    :returns: Searchable and translated unicode value or None
1387
    """
1388
    if not value:
1389
        return u""
1390
    if value is Missing.Value:
1391
        return u""
1392
    if is_uid(value):
1393
        return u""
1394
    if isinstance(value, (bool)):
1395
        return u""
1396
    if isinstance(value, (list, tuple)):
1397
        values = map(to_searchable_text_metadata, value)
1398
        values = filter(None, values)
1399
        return " ".join(values)
1400
    if isinstance(value, dict):
1401
        return to_searchable_text_metadata(value.values())
1402
    if is_date(value):
1403
        return value.strftime("%Y-%m-%d")
1404
    if is_at_content(value):
1405
        return to_searchable_text_metadata(get_title(value))
1406
    if not isinstance(value, six.string_types):
1407
        value = str(value)
1408
    return safe_unicode(value)
1409
1410
1411
def get_registry_record(name, default=None):
1412
    """Returns the value of a registry record
1413
1414
    :param name: [required] name of the registry record
1415
    :type name: str
1416
    :param default: The value returned if the record is not found
1417
    :type default: anything
1418
    :returns: value of the registry record
1419
    """
1420
    return ploneapi.portal.get_registry_record(name, default=default)
1421
1422
1423
def to_display_list(pairs, sort_by="key", allow_empty=True):
1424
    """Create a Plone DisplayList from list items
1425
1426
    :param pairs: list of key, value pairs
1427
    :param sort_by: Sort the items either by key or value
1428
    :param allow_empty: Allow to select an empty value
1429
    :returns: Plone DisplayList
1430
    """
1431
    dl = DisplayList()
1432
1433
    if isinstance(pairs, six.string_types):
1434
        pairs = [pairs, pairs]
1435
    for pair in pairs:
1436
        # pairs is a list of lists -> add each pair
1437
        if isinstance(pair, (tuple, list)):
1438
            dl.add(*pair)
1439
        # pairs is just a single pair -> add it and stop
1440
        if isinstance(pair, six.string_types):
1441
            dl.add(*pairs)
1442
            break
1443
1444
    # add the empty option
1445
    if allow_empty:
1446
        dl.add("", "")
1447
1448
    # sort by key/value
1449
    if sort_by == "key":
1450
        dl = dl.sortedByKey()
1451
    elif sort_by == "value":
1452
        dl = dl.sortedByValue()
1453
1454
    return dl
1455
1456
1457
def text_to_html(text, wrap="p", encoding="utf8"):
1458
    """Convert `\n` sequences in the text to HTML `\n`
1459
1460
    :param text: Plain text to convert
1461
    :param wrap: Toggle to wrap the text in a
1462
    :returns: HTML converted and encoded text
1463
    """
1464
    if not text:
1465
        return ""
1466
    # handle text internally as unicode
1467
    text = safe_unicode(text)
1468
    # replace newline characters with HTML entities
1469
    html = text.replace("\n", "<br/>")
1470
    if wrap:
1471
        html = u"<{tag}>{html}</{tag}>".format(
1472
            tag=wrap, html=html)
1473
    # return encoded html
1474
    return html.encode(encoding)
1475
1476
1477
def to_utf8(string, default=_marker):
1478
    """Encode string to UTF8
1479
1480
    :param string: String to be encoded to UTF8
1481
    :returns: UTF8 encoded string
1482
    """
1483
    if not isinstance(string, six.string_types):
1484
        if default is _marker:
1485
            fail("Expected string type, got '%s'" % type(string))
1486
        return default
1487
    return safe_unicode(string).encode("utf8")
1488
1489
1490
def is_temporary(obj):
1491
    """Returns whether the given object is temporary or not
1492
1493
    :param obj: the object to evaluate
1494
    :returns: True if the object is temporary
1495
    """
1496
    if UID_RX.match(obj.id):
1497
        return True
1498
1499
    parent = aq_parent(aq_inner(obj))
1500
    if not parent:
1501
        return True
1502
1503
    if UID_RX.match(parent.id):
1504
        return True
1505
1506
    if is_at_content(obj):
1507
        # Checks to see if we are created inside the portal_factory. We don't
1508
        # rely here on AT's isFactoryContained because the function is patched
1509
        meta_type = getattr(aq_base(parent), "meta_type", "")
1510
        return meta_type == "TempFolder"
1511
1512
    return False
1513