Passed
Push — 2.x ( 30532a...211428 )
by Jordi
05:19
created

bika.lims.api.float_to_string()   B

Complexity

Conditions 6

Size

Total Lines 36
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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