Passed
Push — 2.x ( ee4c2a...00630e )
by Ramon
06:59
created

bika.lims.api.is_valid_id()   B

Complexity

Conditions 7

Size

Total Lines 43
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 16
dl 0
loc 43
rs 8
c 0
b 0
f 0
cc 7
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-2025 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
import copy
22
import json
23
import re
24
from collections import OrderedDict
25
from datetime import datetime
26
from datetime import timedelta
27
from itertools import groupby
28
29
import Missing
30
import six
31
from AccessControl.PermissionRole import rolesForPermissionOn
32
from AccessControl.Permissions import copy_or_move as CopyOrMove
33
from Acquisition import aq_base
34
from Acquisition import aq_inner
35
from Acquisition import aq_parent
36
from bika.lims import logger
37
from bika.lims.interfaces import IClient
38
from bika.lims.interfaces import IContact
39
from bika.lims.interfaces import ILabContact
40
from DateTime import DateTime
41
from OFS.event import ObjectWillBeMovedEvent
42
from plone import api as ploneapi
43
from plone.api.exc import InvalidParameterError
44
from plone.app.layout.viewlets.content import ContentHistoryView
45
from plone.behavior.interfaces import IBehaviorAssignable
46
from plone.behavior.registration import lookup_behavior_registration
47
from plone.dexterity.interfaces import IDexterityContent
48
from plone.dexterity.schema import SchemaInvalidatedEvent
49
from plone.dexterity.utils import addContentToContainer
50
from plone.dexterity.utils import createContent
51
from plone.dexterity.utils import resolveDottedName
52
from plone.i18n.normalizer.interfaces import IFileNameNormalizer
53
from plone.i18n.normalizer.interfaces import IIDNormalizer
54
from plone.memoize.volatile import DontCache
55
from Products.Archetypes.atapi import DisplayList
56
from Products.Archetypes.BaseObject import BaseObject
57
from Products.Archetypes.event import ObjectInitializedEvent
58
from Products.Archetypes.utils import mapply
59
from Products.CMFCore.interfaces import IFolderish
60
from Products.CMFCore.interfaces import ISiteRoot
61
from Products.CMFCore.permissions import DeleteObjects
62
from Products.CMFCore.permissions import ModifyPortalContent
63
from Products.CMFCore.permissions import View
64
from Products.CMFCore.utils import getToolByName
65
from Products.CMFCore.WorkflowCore import WorkflowException
66
from Products.CMFPlone.RegistrationTool import get_member_by_login_name
67
from Products.CMFPlone.utils import _createObjectByType
68
from Products.CMFPlone.utils import base_hasattr
69
from Products.CMFPlone.utils import safe_unicode
70
from Products.PlonePAS.tools.memberdata import MemberData
71
from Products.ZCatalog.interfaces import ICatalogBrain
72
from senaite.core.interfaces import ITemporaryObject
73
from zope import globalrequest
74
from zope.annotation.interfaces import IAttributeAnnotatable
75
from zope.component import getUtility
76
from zope.component import queryMultiAdapter
77
from zope.container.contained import notifyContainerModified
78
from zope.event import notify
79
from zope.i18n import translate
80
from zope.interface import alsoProvides
81
from zope.interface import directlyProvides
82
from zope.interface import noLongerProvides
83
from zope.interface import Invalid
84
from zope.lifecycleevent import ObjectMovedEvent
85
from zope.publisher.browser import TestRequest
86
from zope.schema import getFieldsInOrder
87
from zope.schema.interfaces import RequiredMissing
88
from zope.schema.interfaces import WrongType
89
from zope.security.interfaces import Unauthorized
90
91
"""SENAITE LIMS Framework API
92
93
Please see tests/doctests/API.rst for documentation.
94
95
Architecural Notes:
96
97
Please add only functions that do a single thing for a single object.
98
99
Good: `def get_foo(brain_or_object)`
100
Bad:  `def get_foos(list_of_brain_objects)`
101
102
Why?
103
104
Because it makes things more complex. You can always use a pattern like this to
105
achieve the same::
106
107
    >>> foos = map(get_foo, list_of_brain_objects)
108
109
Please add for all functions a descriptive test in tests/doctests/API.rst.
110
111
Thanks.
112
"""
113
114
_marker = object()
115
116
UID_RX = re.compile("[a-z0-9]{32}$")
117
118
UID_CATALOG = "uid_catalog"
119
PORTAL_CATALOG = "portal_catalog"
120
121
122
class APIError(Exception):
123
    """Base exception class for bika.lims errors."""
124
125
126
def get_portal():
127
    """Get the portal object
128
129
    :returns: Portal object
130
    """
131
    return ploneapi.portal.getSite()
132
133
134
def get_setup():
135
    """Fetch the `bika_setup` folder.
136
    """
137
    portal = get_portal()
138
    return portal.get("bika_setup")
139
140
141
def get_bika_setup():
142
    """Fetch the `bika_setup` folder.
143
    """
144
    return get_setup()
145
146
147
def get_senaite_setup():
148
    """Fetch the new DX `setup` folder.
149
    """
150
    portal = get_portal()
151
    return portal.get("setup")
152
153
154
def create(container, portal_type, *args, **kwargs):
155
    """Creates an object in Bika LIMS
156
157
    This code uses most of the parts from the TypesTool
158
    see: `Products.CMFCore.TypesTool._constructInstance`
159
160
    :param container: container
161
    :type container: ATContentType/DexterityContentType/CatalogBrain
162
    :param portal_type: The portal type to create, e.g. "Client"
163
    :type portal_type: string
164
    :param title: The title for the new content object
165
    :type title: string
166
    :returns: The new created object
167
    """
168
    from bika.lims.utils import tmpID
169
170
    tmp_id = tmpID()
171
    id = kwargs.pop("id", "")
172
    title = kwargs.pop("title", "")
173
174
    # get the fti
175
    types_tool = get_tool("portal_types")
176
    fti = types_tool.getTypeInfo(portal_type)
177
178
    if fti.product:
179
        # create the AT object
180
        obj = _createObjectByType(portal_type, container, id or tmp_id)
181
        # update the object with values
182
        edit(obj, check_permissions=False, title=title, **kwargs)
183
        # auto-id if required
184
        if obj._at_rename_after_creation:
185
            obj._renameAfterCreation(check_auto_id=True)
186
        # we are no longer under creation
187
        obj.unmarkCreationFlag()
188
        # notify that the object was created
189
        notify(ObjectInitializedEvent(obj))
190
    else:
191
        content = createContent(portal_type, **kwargs)
192
        content.id = id
193
        content.title = title
194
        obj = addContentToContainer(container, content)
195
196
    return obj
197
198
199
def copy_object(source, container=None, portal_type=None, *args, **kwargs):
200
    """Creates a copy of the source object. If container is None, creates the
201
    copy inside the same container as the source. If portal_type is specified,
202
    creates a new object of this type, and copies the values from source fields
203
    to the destination object. Field values sent as kwargs have priority over
204
    the field values from source.
205
206
    :param source: object from which create a copy
207
    :type source: ATContentType/DexterityContentType/CatalogBrain
208
    :param container: destination container
209
    :type container: ATContentType/DexterityContentType/CatalogBrain
210
    :param portal_type: destination portal type
211
    :returns: The new created object
212
    """
213
    # Prevent circular dependencies
214
    from security import check_permission
215
    # Use same container as source unless explicitly set
216
    source = get_object(source)
217
    if not container:
218
        container = get_parent(source)
219
220
    # Use same portal type as source unless explicitly set
221
    if not portal_type:
222
        portal_type = get_portal_type(source)
223
224
    # Extend the fields to skip with defaults
225
    skip = kwargs.pop("skip", [])
226
    skip = set(skip)
227
    skip.update([
228
        "Products.Archetypes.Field.ComputedField",
229
        "UID",
230
        "id",
231
        "allowDiscussion",
232
        "contributors",
233
        "creation_date",
234
        "creators",
235
        "effectiveDate",
236
        "expirationDate",
237
        "language",
238
        "location",
239
        "modification_date",
240
        "rights",
241
        "subject",
242
    ])
243
    # Build a dict for complexity reduction
244
    skip = dict([(item, True) for item in skip])
245
246
    # Update kwargs with the field values to copy from source
247
    fields = get_fields(source)
248
    for field_name, field in fields.items():
249
        # Prioritize field values passed as kwargs
250
        if field_name in kwargs:
251
            continue
252
        # Skip framework internal fields by name
253
        if skip.get(field_name, False):
254
            continue
255
        # Skip fields of non-suitable types
256
        if hasattr(field, "getType") and skip.get(field.getType(), False):
257
            continue
258
        # Skip readonly fields
259
        if getattr(field, "readonly", False):
260
            continue
261
        # Skip non-readable fields
262
        perm = getattr(field, "read_permission", View)
263
        if perm and not check_permission(perm, source):
264
            continue
265
266
        # do not wake-up objects unnecessarily
267
        if hasattr(field, "getRaw"):
268
            field_value = field.getRaw(source)
269
        elif hasattr(field, "get_raw"):
270
            field_value = field.get_raw(source)
271
        elif hasattr(field, "getAccessor"):
272
            accessor = field.getAccessor(source)
273
            field_value = accessor()
274
        else:
275
            field_value = field.get(source)
276
277
        # Do a hard copy of value if mutable type
278
        if isinstance(field_value, (list, dict, set)):
279
            field_value = copy.deepcopy(field_value)
280
        kwargs.update({field_name: field_value})
281
282
    # Create a copy
283
    return create(container, portal_type, *args, **kwargs)
284
285
286
def edit(obj, check_permissions=True, **kwargs):
287
    """Updates the values of object fields with the new values passed-in
288
    """
289
    # Prevent circular dependencies
290
    from security import check_permission
291
    fields = get_fields(obj)
292
    for name, value in kwargs.items():
293
        field = fields.get(name, None)
294
        if not field:
295
            continue
296
297
        # cannot update readonly fields
298
        readonly = getattr(field, "readonly", False)
299
        if readonly:
300
            raise ValueError("Field '{}' is readonly".format(name))
301
302
        # check field writable permission
303
        if check_permissions:
304
            perm = getattr(field, "write_permission", ModifyPortalContent)
305
            if perm and not check_permission(perm, obj):
306
                raise Unauthorized("Field '{}' is not writeable".format(name))
307
308
        # Set the value
309
        if hasattr(field, "getMutator"):
310
            mutator = field.getMutator(obj)
311
            mapply(mutator, value)
312
        else:
313
            field.set(obj, value)
314
315
316
def move_object(obj, destination, check_constraints=True):
317
    """Moves the object to the destination folder
318
319
    This function has the same effect as:
320
321
        id = obj.getId()
322
        cp = origin.manage_cutObjects(id)
323
        destination.manage_pasteObjects(cp)
324
325
    but with slightly better performance. The code is mostly grabbed from
326
    OFS.CopySupport.CopyContainer_pasteObjects
327
328
    :param obj: object to move to destination
329
    :type obj: ATContentType/DexterityContentType/CatalogBrain/UID
330
    :param destination: destination container
331
    :type destination: ATContentType/DexterityContentType/CatalogBrain/UID
332
    :param check_constraints: constraints and permissions must be checked
333
    :type check_constraints: bool
334
    :returns: The moved object
335
    """
336
    # prevent circular dependencies
337
    from bika.lims.api.security import check_permission
338
339
    obj = get_object(obj)
340
    destination = get_object(destination)
341
342
    # make sure the object is not moved into itself
343
    if obj == destination:
344
        raise ValueError("Cannot move object into itself: {}".format(obj))
345
346
    # do nothing if destination is the same as origin
347
    origin = get_parent(obj)
348
    if origin == destination:
349
        return obj
350
351
    if check_constraints:
352
353
        # check origin object has CopyOrMove permission
354
        if not check_permission(CopyOrMove, obj):
355
            raise Unauthorized("Cannot move {}".format(obj))
356
357
        # check if portal type is allowed in destination object
358
        portal_type = get_portal_type(obj)
359
        pt = get_tool("portal_types")
360
        ti = pt.getTypeInfo(destination)
361
        if not ti.allowType(portal_type):
362
            raise ValueError("Disallowed subobject type: %s" % portal_type)
363
364
    id = get_id(obj)
365
366
    # notify that the object will be copied to destination
367
    obj._notifyOfCopyTo(destination, op=1)  # noqa
368
369
    # notify that the object will be moved to destination
370
    notify(ObjectWillBeMovedEvent(obj, origin, id, destination, id))
371
372
    # effectively move the object from origin to destination
373
    delete(obj, check_permissions=check_constraints, suppress_events=True)
374
    obj = aq_base(obj)
375
    destination._setObject(id, obj, set_owner=0, suppress_events=True)  # noqa
376
    obj = destination._getOb(id)  # noqa
377
378
    # since we used "suppress_events=True", we need to manually notify that the
379
    # object was moved and containers modified. This also makes the objects to
380
    # be re-catalogued
381
    notify(ObjectMovedEvent(obj, origin, id, destination, id))
382
    notifyContainerModified(origin)
383
    notifyContainerModified(destination)
384
385
    # make ownership implicit if possible, so it acquires the permissions from
386
    # the container
387
    obj.manage_changeOwnershipType(explicit=0)
388
389
    return obj
390
391
392
def uncatalog_object(obj, recursive=False):
393
    """Un-catalog the object from all catalogs
394
395
    :param obj: object to un-catalog
396
    :param recursive: recursively uncatalog all child objects
397
    :type obj: ATContentType/DexterityContentType
398
    """
399
    # un-catalog from registered catalogs
400
    obj.unindexObject()
401
    # explicitly un-catalog from uid_catalog
402
    uid_catalog = get_tool("uid_catalog")
403
    # the uids of uid_catalog are relative paths to portal root
404
    # see Products.Archetypes.UIDCatalog.UIDResolver.catalog_object
405
    url = "/".join(obj.getPhysicalPath()[2:])
406
    uid_catalog.uncatalog_object(url)
407
408
    if recursive:
409
        for child in obj.objectValues():
410
            uncatalog_object(child, recursive=recursive)
411
412
413
def catalog_object(obj, recursive=False):
414
    """Re-catalog the object
415
416
    :param obj: object to catalog
417
    :param recursive: recursively catalog all child objects
418
    :type obj: ATContentType/DexterityContentType
419
    """
420
    if is_at_content(obj):
421
        # explicitly re-catalog AT types at uid_catalog (DX types are
422
        # automatically reindexed in UID catalog on reindexObject)
423
        uc = get_tool("uid_catalog")
424
        # the uids of uid_catalog are relative paths to portal root
425
        # see Products.Archetypes.UIDCatalog.UIDResolver.catalog_object
426
        url = "/".join(obj.getPhysicalPath()[2:])
427
        uc.catalog_object(obj, url)
428
    obj.reindexObject()
429
430
    if recursive:
431
        for child in obj.objectValues():
432
            catalog_object(child, recursive=recursive)
433
434
435
def delete(obj, check_permissions=True, suppress_events=False):
436
    """Deletes the given object
437
438
    :param obj: object to un-catalog
439
    :param check_permissions: whether delete permission must be checked
440
    :param suppress_events: whether ondelete events have to be fired
441
    :type obj: ATContentType/DexterityContentType
442
    """
443
    from security import check_permission
444
    if check_permissions and not check_permission(DeleteObjects, obj):
445
        raise Unauthorized("Do not have permissions to remove this object")
446
447
    # un-catalog the object from all catalogs (uid_catalog included)
448
    uncatalog_object(obj)
449
    # delete the object
450
    parent = get_parent(obj)
451
    parent._delObject(obj.getId(), suppress_events=suppress_events)
452
453
454
def get_tool(name, context=None, default=_marker):
455
    """Get a portal tool by name
456
457
    :param name: The name of the tool, e.g. `senaite_setup_catalog`
458
    :type name: string
459
    :param context: A portal object
460
    :type context: ATContentType/DexterityContentType/CatalogBrain
461
    :returns: Portal Tool
462
    """
463
464
    # Try first with the context
465
    if context is not None:
466
        try:
467
            context = get_object(context)
468
            return getToolByName(context, name)
469
        except (APIError, AttributeError) as e:
470
            # https://github.com/senaite/bika.lims/issues/396
471
            logger.warn("get_tool::getToolByName({}, '{}') failed: {} "
472
                        "-> falling back to plone.api.portal.get_tool('{}')"
473
                        .format(repr(context), name, repr(e), name))
474
            return get_tool(name, default=default)
475
476
    # Try with the plone api
477
    try:
478
        return ploneapi.portal.get_tool(name)
479
    except InvalidParameterError:
480
        if default is not _marker:
481
            if isinstance(default, six.string_types):
482
                return get_tool(default)
483
            return default
484
        fail("No tool named '%s' found." % name)
485
486
487
def fail(msg=None):
488
    """API LIMS Error
489
    """
490
    if msg is None:
491
        msg = "Reason not given."
492
    raise APIError("{}".format(msg))
493
494
495
def is_object(brain_or_object):
496
    """Check if the passed in object is a supported portal content object
497
498
    :param brain_or_object: A single catalog brain or content object
499
    :type brain_or_object: Portal Object
500
    :returns: True if the passed in object is a valid portal content
501
    """
502
    if is_portal(brain_or_object):
503
        return True
504
    if is_supermodel(brain_or_object):
505
        return True
506
    if is_at_content(brain_or_object):
507
        return True
508
    if is_dexterity_content(brain_or_object):
509
        return True
510
    if is_brain(brain_or_object):
511
        return True
512
    return False
513
514
515
def get_object(brain_object_uid, default=_marker):
516
    """Get the full content object
517
518
    :param brain_object_uid: A catalog brain or content object or uid
519
    :type brain_object_uid: PortalObject/ATContentType/DexterityContentType
520
    /CatalogBrain/basestring
521
    :returns: The full object
522
    """
523
    if is_uid(brain_object_uid):
524
        return get_object_by_uid(brain_object_uid, default=default)
525
    elif is_supermodel(brain_object_uid):
526
        return brain_object_uid.instance
527
    if not is_object(brain_object_uid):
528
        if default is _marker:
529
            fail("{} is not supported.".format(repr(brain_object_uid)))
530
        return default
531
    if is_brain(brain_object_uid):
532
        return brain_object_uid.getObject()
533
    return brain_object_uid
534
535
536
def is_portal(brain_or_object):
537
    """Checks if the passed in object is the portal root object
538
539
    :param brain_or_object: A single catalog brain or content object
540
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
541
    :returns: True if the object is the portal root object
542
    :rtype: bool
543
    """
544
    return ISiteRoot.providedBy(brain_or_object)
545
546
547
def is_brain(brain_or_object):
548
    """Checks if the passed in object is a portal catalog brain
549
550
    :param brain_or_object: A single catalog brain or content object
551
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
552
    :returns: True if the object is a catalog brain
553
    :rtype: bool
554
    """
555
    return ICatalogBrain.providedBy(brain_or_object)
556
557
558
def is_supermodel(brain_or_object):
559
    """Checks if the passed in object is a supermodel
560
561
    :param brain_or_object: A single catalog brain or content object
562
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
563
    :returns: True if the object is a catalog brain
564
    :rtype: bool
565
    """
566
    # avoid circular imports
567
    from senaite.app.supermodel.interfaces import ISuperModel
568
    return ISuperModel.providedBy(brain_or_object)
569
570
571
def is_dexterity_content(brain_or_object):
572
    """Checks if the passed in object is a dexterity content type
573
574
    :param brain_or_object: A single catalog brain or content object
575
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
576
    :returns: True if the object is a dexterity content type
577
    :rtype: bool
578
    """
579
    return IDexterityContent.providedBy(brain_or_object)
580
581
582
def is_at_content(brain_or_object):
583
    """Checks if the passed in object is an AT content type
584
585
    :param brain_or_object: A single catalog brain or content object
586
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
587
    :returns: True if the object is an AT content type
588
    :rtype: bool
589
    """
590
    return isinstance(brain_or_object, BaseObject)
591
592
593
def is_dx_type(portal_type):
594
    """Checks if the portal type is DX based
595
596
    :param portal_type: The portal type name to check
597
    :returns: True if the portal type is DX based
598
    """
599
    portal_types = get_tool("portal_types")
600
    fti = portal_types.getTypeInfo(portal_type)
601
    if fti.product:
602
        return False
603
    return True
604
605
606
def is_at_type(portal_type):
607
    """Checks if the portal type is AT based
608
609
    :param portal_type: The portal type name to check
610
    :returns: True if the portal type is AT based
611
    """
612
    return not is_dx_type(portal_type)
613
614
615
def is_folderish(brain_or_object):
616
    """Checks if the passed in object is folderish
617
618
    :param brain_or_object: A single catalog brain or content object
619
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
620
    :returns: True if the object is folderish
621
    :rtype: bool
622
    """
623
    if hasattr(brain_or_object, "is_folderish"):
624
        if callable(brain_or_object.is_folderish):
625
            return brain_or_object.is_folderish()
626
        return brain_or_object.is_folderish
627
    return IFolderish.providedBy(get_object(brain_or_object))
628
629
630
def get_portal_type(brain_or_object):
631
    """Get the portal type for this object
632
633
    :param brain_or_object: A single catalog brain or content object
634
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
635
    :returns: Portal type
636
    :rtype: string
637
    """
638
    if not is_object(brain_or_object):
639
        fail("{} is not supported.".format(repr(brain_or_object)))
640
    return brain_or_object.portal_type
641
642
643
def get_schema(brain_or_object):
644
    """Get the schema of the content
645
646
    :param brain_or_object: A single catalog brain or content object
647
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
648
    :returns: Schema object
649
    """
650
    obj = get_object(brain_or_object)
651
    if is_portal(obj):
652
        fail("get_schema can't return schema of portal root")
653
    if is_dexterity_content(obj):
654
        pt = get_tool("portal_types")
655
        fti = pt.getTypeInfo(obj.portal_type)
656
        return fti.lookupSchema()
657
    if is_at_content(obj):
658
        return obj.Schema()
659
    fail("{} has no Schema.".format(brain_or_object))
660
661
662
def get_behaviors(portal_type):
663
    """List all behaviors
664
665
    :param portal_type: DX portal type name
666
    """
667
    portal_types = get_tool("portal_types")
668
    fti = portal_types.getTypeInfo(portal_type)
669
    if fti.product:
670
        raise TypeError("Expected DX type, got AT type instead.")
671
    return fti.behaviors
672
673
674
def enable_behavior(portal_type, behavior_id):
675
    """Enable behavior
676
677
    :param portal_type: DX portal type name
678
    :param behavior_id: The behavior to enable
679
    """
680
    portal_types = get_tool("portal_types")
681
    fti = portal_types.getTypeInfo(portal_type)
682
    if fti.product:
683
        raise TypeError("Expected DX type, got AT type instead.")
684
685
    if behavior_id not in fti.behaviors:
686
        fti.behaviors += (behavior_id, )
687
        # invalidate schema cache
688
        notify(SchemaInvalidatedEvent(portal_type))
689
690
691
def disable_behavior(portal_type, behavior_id):
692
    """Disable behavior
693
694
    :param portal_type: DX portal type name
695
    :param behavior_id: The behavior to disable
696
    """
697
    portal_types = get_tool("portal_types")
698
    fti = portal_types.getTypeInfo(portal_type)
699
    if fti.product:
700
        raise TypeError("Expected DX type, got AT type instead.")
701
    fti.behaviors = tuple(filter(lambda b: b != behavior_id, fti.behaviors))
702
    # invalidate schema cache
703
    notify(SchemaInvalidatedEvent(portal_type))
704
705
706
def get_fields(brain_or_object):
707
    """Get a name to field mapping of the object
708
709
    :param brain_or_object: A single catalog brain or content object
710
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
711
    :returns: Mapping of name -> field
712
    :rtype: OrderedDict
713
    """
714
    obj = get_object(brain_or_object)
715
    schema = get_schema(obj)
716
    if is_dexterity_content(obj):
717
        # get the fields directly provided by the interface
718
        fields = getFieldsInOrder(schema)
719
        # append the fields coming from behaviors
720
        behavior_assignable = IBehaviorAssignable(obj)
721
        if behavior_assignable:
722
            behaviors = behavior_assignable.enumerateBehaviors()
723
            for behavior in behaviors:
724
                fields.extend(getFieldsInOrder(behavior.interface))
725
        return OrderedDict(fields)
726
    return OrderedDict(schema)
727
728
729
def get_id(brain_or_object):
730
    """Get the Plone ID for this object
731
732
    :param brain_or_object: A single catalog brain or content object
733
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
734
    :returns: Plone ID
735
    :rtype: string
736
    """
737
    if is_brain(brain_or_object):
738
        if base_hasattr(brain_or_object, "getId"):
739
            return brain_or_object.getId
740
        if base_hasattr(brain_or_object, "id"):
741
            return brain_or_object.id
742
    return get_object(brain_or_object).getId()
743
744
745
def get_title(brain_or_object):
746
    """Get the Title for this object
747
748
    :param brain_or_object: A single catalog brain or content object
749
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
750
    :returns: Title
751
    :rtype: string
752
    """
753
    if is_brain(brain_or_object) and base_hasattr(brain_or_object, "Title"):
754
        return brain_or_object.Title
755
    return get_object(brain_or_object).Title()
756
757
758
def get_description(brain_or_object):
759
    """Get the Title for this object
760
761
    :param brain_or_object: A single catalog brain or content object
762
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
763
    :returns: Title
764
    :rtype: string
765
    """
766
    if is_brain(brain_or_object) \
767
            and base_hasattr(brain_or_object, "Description"):
768
        return brain_or_object.Description
769
    return get_object(brain_or_object).Description()
770
771
772
def get_uid(brain_or_object):
773
    """Get the Plone UID for this object
774
775
    :param brain_or_object: A single catalog brain or content object or an UID
776
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
777
    :returns: Plone UID
778
    :rtype: string
779
    """
780
    if is_uid(brain_or_object):
781
        return brain_or_object
782
    if is_portal(brain_or_object):
783
        return '0'
784
    if is_brain(brain_or_object) and base_hasattr(brain_or_object, "UID"):
785
        return brain_or_object.UID
786
    return get_object(brain_or_object).UID()
787
788
789
def get_url(brain_or_object):
790
    """Get the absolute URL for this object
791
792
    :param brain_or_object: A single catalog brain or content object
793
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
794
    :returns: Absolute URL
795
    :rtype: string
796
    """
797
    if is_brain(brain_or_object) and base_hasattr(brain_or_object, "getURL"):
798
        return brain_or_object.getURL()
799
    return get_object(brain_or_object).absolute_url()
800
801
802
def get_icon(thing, html_tag=True):
803
    """Get the icon of the content object
804
805
    :param thing: A single catalog brain, content object or portal_type
806
    :type thing: ATContentType/DexterityContentType/CatalogBrain/String
807
    :param html_tag: A value of 'True' returns the HTML tag, else the image url
808
    :type html_tag: bool
809
    :returns: HTML '<img>' tag if 'html_tag' is True else the image url
810
    :rtype: string
811
    """
812
    portal_type = thing
813
    if is_object(thing):
814
        portal_type = get_portal_type(thing)
815
816
    # Manual approach, because `plone.app.layout.getIcon` does not reliable
817
    # work for Contents coming from other catalogs than the
818
    # `portal_catalog`
819
    portal_types = get_tool("portal_types")
820
    fti = portal_types.getTypeInfo(portal_type)
821
    if not fti:
822
        fail("No type info for {}".format(repr(thing)))
823
    icon = fti.getIcon()
824
    if not icon:
825
        return ""
826
    url = "%s/%s" % (get_url(get_portal()), icon)
827
    if not html_tag:
828
        return url
829
830
    # build the img element
831
    if is_object(thing):
832
        title = get_title(thing)
833
    else:
834
        title = fti.Title()
835
    tag = '<img width="16" height="16" src="{url}" title="{title}" />'
836
    return tag.format(url=url, title=title)
837
838
839
def get_object_by_uid(uid, default=_marker):
840
    """Find an object by a given UID
841
842
    :param uid: The UID of the object to find
843
    :type uid: string
844
    :returns: Found Object or None
845
    """
846
847
    # nothing to do here
848
    if not uid:
849
        if default is not _marker:
850
            return default
851
        fail("get_object_by_uid requires UID as first argument; got {} instead"
852
             .format(uid))
853
854
    # we defined the portal object UID to be '0'::
855
    if uid == '0':
856
        return get_portal()
857
858
    brain = get_brain_by_uid(uid)
859
860
    if brain is None:
861
        if default is not _marker:
862
            return default
863
        fail("No object found for UID {}".format(uid))
864
865
    return get_object(brain)
866
867
868
def get_brain_by_uid(uid, default=None):
869
    """Query a brain by a given UID
870
871
    :param uid: The UID of the object to find
872
    :type uid: string
873
    :returns: ZCatalog brain or None
874
    """
875
    if not is_uid(uid):
876
        return default
877
878
    # we try to find the object with the UID catalog
879
    uc = get_tool(UID_CATALOG)
880
881
    # try to find the object with the reference catalog first
882
    brains = uc(UID=uid)
883
    if len(brains) != 1:
884
        return default
885
    return brains[0]
886
887
888
def get_object_by_path(path, default=_marker):
889
    """Find an object by a given physical path or absolute_url
890
891
    :param path: The physical path of the object to find
892
    :type path: string
893
    :returns: Found Object or None
894
    """
895
896
    # nothing to do here
897
    if not path:
898
        if default is not _marker:
899
            return default
900
        fail("get_object_by_path first argument must be a path; {} received"
901
             .format(path))
902
903
    portal = get_portal()
904
    portal_path = get_path(portal)
905
    portal_url = get_url(portal)
906
907
    # ensure we have a physical path
908
    if path.startswith(portal_url):
909
        request = get_request()
910
        path = "/".join(request.physicalPathFromURL(path))
911
912
    if not path.startswith(portal_path):
913
        if default is not _marker:
914
            return default
915
        fail("Not a physical path inside the portal.")
916
917
    if path == portal_path:
918
        return portal
919
920
    return portal.restrictedTraverse(path, default)
921
922
923
def get_path(brain_or_object):
924
    """Calculate the physical path of this object
925
926
    :param brain_or_object: A single catalog brain or content object
927
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
928
    :returns: Physical path of the object
929
    :rtype: string
930
    """
931
    if is_brain(brain_or_object):
932
        return brain_or_object.getPath()
933
    return "/".join(get_object(brain_or_object).getPhysicalPath())
934
935
936
def get_parent_path(brain_or_object):
937
    """Calculate the physical parent path of this object
938
939
    :param brain_or_object: A single catalog brain or content object
940
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
941
    :returns: Physical path of the parent object
942
    :rtype: string
943
    """
944
    if is_portal(brain_or_object):
945
        return get_path(get_portal())
946
    if is_brain(brain_or_object):
947
        path = get_path(brain_or_object)
948
        return path.rpartition("/")[0]
949
    return get_path(get_object(brain_or_object).aq_parent)
950
951
952
def get_parent(brain_or_object, **kw):
953
    """Locate the parent object of the content/catalog brain
954
955
    :param brain_or_object: A single catalog brain or content object
956
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
957
    :param catalog_search: Use a catalog query to find the parent object
958
    :type catalog_search: bool
959
    :returns: parent object
960
    :rtype: ATContentType/DexterityContentType/PloneSite/CatalogBrain
961
    """
962
963
    if is_portal(brain_or_object):
964
        return get_portal()
965
966
    # BBB: removed `catalog_search` keyword
967
    if kw:
968
        logger.warn("API function `get_parent` no longer support keywords.")
969
970
    return get_object(brain_or_object).aq_parent
971
972
973
def search(query, catalog=_marker):
974
    """Search for objects.
975
976
    :param query: A suitable search query.
977
    :type query: dict
978
    :param catalog: A single catalog id or a list of catalog ids
979
    :type catalog: str/list
980
    :returns: Search results
981
    :rtype: List of ZCatalog brains
982
    """
983
984
    # query needs to be a dictionary
985
    if not isinstance(query, dict):
986
        fail("Catalog query needs to be a dictionary")
987
988
    # Portal types to query
989
    portal_types = query.get("portal_type", [])
990
    # We want the portal_type as a list
991
    if not isinstance(portal_types, (tuple, list)):
992
        portal_types = [portal_types]
993
994
    # The catalogs used for the query
995
    catalogs = []
996
997
    # The user did **not** specify a catalog
998
    if catalog is _marker:
999
        # Find the registered catalogs for the queried portal types
1000
        for portal_type in portal_types:
1001
            # Just get the first registered/default catalog
1002
            catalogs.append(get_catalogs_for(
1003
                portal_type, default=UID_CATALOG)[0])
1004
    else:
1005
        # User defined catalogs
1006
        if isinstance(catalog, (list, tuple)):
1007
            catalogs.extend(map(get_tool, catalog))
1008
        else:
1009
            catalogs.append(get_tool(catalog))
1010
1011
    # Cleanup: Avoid duplicate catalogs
1012
    catalogs = list(set(catalogs)) or [get_uid_catalog()]
1013
1014
    # We only support **single** catalog queries
1015
    if len(catalogs) > 1:
1016
        fail("Multi Catalog Queries are not supported!")
1017
1018
    return catalogs[0](query)
1019
1020
1021
def safe_getattr(brain_or_object, attr, default=_marker):
1022
    """Return the attribute value
1023
1024
    :param brain_or_object: A single catalog brain or content object
1025
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
1026
    :param attr: Attribute name
1027
    :type attr: str
1028
    :returns: Attribute value
1029
    :rtype: obj
1030
    """
1031
    try:
1032
        value = getattr(brain_or_object, attr, _marker)
1033
        if value is _marker:
1034
            if default is not _marker:
1035
                return default
1036
            fail("Attribute '{}' not found.".format(attr))
1037
        if callable(value):
1038
            return value()
1039
        return value
1040
    except Unauthorized:
1041
        if default is not _marker:
1042
            return default
1043
        fail("You are not authorized to access '{}' of '{}'.".format(
1044
            attr, repr(brain_or_object)))
1045
1046
1047
def get_uid_catalog():
1048
    """Get the UID catalog tool
1049
1050
    :returns: UID Catalog Tool
1051
    """
1052
    return get_tool(UID_CATALOG)
1053
1054
1055
def get_portal_catalog():
1056
    """Get the portal catalog tool
1057
1058
    :returns: Portal Catalog Tool
1059
    """
1060
    return get_tool(PORTAL_CATALOG)
1061
1062
1063
def get_review_history(brain_or_object, rev=True):
1064
    """Get the review history for the given brain or context.
1065
1066
    :param brain_or_object: A single catalog brain or content object
1067
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
1068
    :returns: Workflow history
1069
    :rtype: [{}, ...]
1070
    """
1071
    obj = get_object(brain_or_object)
1072
    review_history = []
1073
    try:
1074
        workflow = get_tool("portal_workflow")
1075
        review_history = workflow.getInfoFor(obj, 'review_history')
1076
    except WorkflowException as e:
1077
        message = str(e)
1078
        logger.error("Cannot retrieve review_history on {}: {}".format(
1079
            obj, message))
1080
    if not isinstance(review_history, (list, tuple)):
1081
        logger.error("get_review_history: expected list, received {}".format(
1082
            review_history))
1083
        review_history = []
1084
1085
    if isinstance(review_history, tuple):
1086
        # Products.CMFDefault.DefaultWorkflow.getInfoFor always returns a
1087
        # tuple when "review_history" is passed in:
1088
        # https://github.com/zopefoundation/Products.CMFDefault/blob/master/Products/CMFDefault/DefaultWorkflow.py#L244
1089
        #
1090
        # On the other hand, Products.DCWorkflow.getInfoFor relies on
1091
        # Expression.StateChangeInfo, that always returns a list, except when no
1092
        # review_history is found:
1093
        # https://github.com/zopefoundation/Products.DCWorkflow/blob/master/Products/DCWorkflow/DCWorkflow.py#L310
1094
        # https://github.com/zopefoundation/Products.DCWorkflow/blob/master/Products/DCWorkflow/Expression.py#L94
1095
        review_history = list(review_history)
1096
1097
    if rev is True:
1098
        review_history.reverse()
1099
    return review_history
1100
1101
1102
def get_revision_history(brain_or_object):
1103
    """Get the revision history for the given brain or context.
1104
1105
    :param brain_or_object: A single catalog brain or content object
1106
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
1107
    :returns: Workflow history
1108
    :rtype: obj
1109
    """
1110
    obj = get_object(brain_or_object)
1111
    chv = ContentHistoryView(obj, safe_getattr(obj, "REQUEST", None))
1112
    return chv.fullHistory()
1113
1114
1115
def get_workflows_for(brain_or_object):
1116
    """Get the assigned workflows for the given brain or context.
1117
1118
    Note: This function supports also the portal_type as parameter.
1119
1120
    :param brain_or_object: A single catalog brain or content object
1121
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
1122
    :returns: Assigned Workflows
1123
    :rtype: tuple
1124
    """
1125
    workflow = ploneapi.portal.get_tool("portal_workflow")
1126
    if isinstance(brain_or_object, six.string_types):
1127
        return workflow.getChainFor(brain_or_object)
1128
    obj = get_object(brain_or_object)
1129
    return workflow.getChainFor(obj)
1130
1131
1132
def get_workflow_status_of(brain_or_object, state_var="review_state"):
1133
    """Get the current workflow status of the given brain or context.
1134
1135
    :param brain_or_object: A single catalog brain or content object
1136
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
1137
    :param state_var: The name of the state variable
1138
    :type state_var: string
1139
    :returns: Status
1140
    :rtype: str
1141
    """
1142
    # Try to get the state from the catalog brain first
1143
    if is_brain(brain_or_object):
1144
        if state_var in brain_or_object.schema():
1145
            return brain_or_object[state_var]
1146
1147
    # Retrieve the sate from the object
1148
    workflow = get_tool("portal_workflow")
1149
    obj = get_object(brain_or_object)
1150
    return workflow.getInfoFor(ob=obj, name=state_var, default='')
1151
1152
1153
def get_previous_worfklow_status_of(brain_or_object, skip=None, default=None):
1154
    """Get the previous workflow status of the object
1155
1156
    :param brain_or_object: A single catalog brain or content object
1157
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
1158
    :param skip: Workflow states to skip
1159
    :type skip: tuple/list
1160
    :returns: status
1161
    :rtype: str
1162
    """
1163
1164
    skip = isinstance(skip, (list, tuple)) and skip or []
1165
    history = get_review_history(brain_or_object)
1166
1167
    # Remove consecutive duplicates, some transitions might happen more than
1168
    # once consecutively (e.g. publish)
1169
    history = map(lambda i: i[0], groupby(history))
1170
1171
    for num, item in enumerate(history):
1172
        # skip the current history entry
1173
        if num == 0:
1174
            continue
1175
        status = item.get("review_state")
1176
        if status in skip:
1177
            continue
1178
        return status
1179
    return default
1180
1181
1182
def get_creation_date(brain_or_object):
1183
    """Get the creation date of the brain or object
1184
1185
    :param brain_or_object: A single catalog brain or content object
1186
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
1187
    :returns: Creation date
1188
    :rtype: DateTime
1189
    """
1190
    created = getattr(brain_or_object, "created", None)
1191
    if created is None:
1192
        fail("Object {} has no creation date ".format(
1193
             repr(brain_or_object)))
1194
    if callable(created):
1195
        return created()
1196
    return created
1197
1198
1199
def get_modification_date(brain_or_object):
1200
    """Get the modification date of the brain or object
1201
1202
    :param brain_or_object: A single catalog brain or content object
1203
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
1204
    :returns: Modification date
1205
    :rtype: DateTime
1206
    """
1207
    modified = getattr(brain_or_object, "modified", None)
1208
    if modified is None:
1209
        fail("Object {} has no modification date ".format(
1210
             repr(brain_or_object)))
1211
    if callable(modified):
1212
        return modified()
1213
    return modified
1214
1215
1216
def get_review_status(brain_or_object):
1217
    """Get the `review_state` of an object
1218
1219
    :param brain_or_object: A single catalog brain or content object
1220
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
1221
    :returns: Value of the review_status variable
1222
    :rtype: String
1223
    """
1224
    if is_brain(brain_or_object) \
1225
       and base_hasattr(brain_or_object, "review_state"):
1226
        return brain_or_object.review_state
1227
    return get_workflow_status_of(brain_or_object, state_var="review_state")
1228
1229
1230
def is_active(brain_or_object):
1231
    """Check if the workflow state of the object is 'inactive' or 'cancelled'.
1232
1233
    :param brain_or_object: A single catalog brain or content object
1234
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
1235
    :returns: False if the object is in the state 'inactive' or 'cancelled'
1236
    :rtype: bool
1237
    """
1238
    if get_review_status(brain_or_object) in ["cancelled", "inactive"]:
1239
        return False
1240
    return True
1241
1242
1243
def get_fti(portal_type, default=None):
1244
    """Lookup the Dynamic Filetype Information for the given portal_type
1245
1246
    :param portal_type: The portal type to get the FTI for
1247
    :returns: FTI or default value
1248
    """
1249
    if not is_string(portal_type):
1250
        return default
1251
    portal_types = get_tool("portal_types")
1252
    fti = portal_types.getTypeInfo(portal_type)
1253
    return fti or default
1254
1255
1256
def get_catalogs_for(brain_or_object, default=PORTAL_CATALOG):
1257
    """Get all registered catalogs for the given portal_type, catalog brain or
1258
    content object
1259
1260
    NOTE: We pass in the `portal_catalog` as default in subsequent calls to
1261
          work around the missing `uid_catalog` during snapshot creation when
1262
          installing a fresh site!
1263
1264
    :param brain_or_object: The portal_type, a catalog brain or content object
1265
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
1266
    :returns: List of supported catalogs
1267
    :rtype: list
1268
    """
1269
1270
    # only handle catalog lookups by portal_type internally
1271
    if is_uid(brain_or_object) or is_object(brain_or_object):
1272
        obj = get_object(brain_or_object)
1273
        portal_type = get_portal_type(obj)
1274
        return get_catalogs_for(portal_type)
1275
1276
    catalogs = []
1277
1278
    if not is_string(brain_or_object):
1279
        raise APIError("Expected a portal_type string, got <%s>"
1280
                       % type(brain_or_object))
1281
1282
    # at this point the brain_or_object is a portal_type
1283
    portal_type = brain_or_object
1284
1285
    # check static portal_type -> catalog mapping first
1286
    from senaite.core.catalog import get_catalogs_by_type
1287
    catalogs = get_catalogs_by_type(portal_type)
1288
1289
    # no catalogs in static mapping
1290
    # => Lookup catalogs by FTI
1291
    if len(catalogs) == 0:
1292
        fti = get_fti(portal_type)
1293
        if fti.product:
1294
            # AT content type
1295
            # => Looup via archetype_tool
1296
            archetype_tool = get_tool("archetype_tool")
1297
            catalogs = archetype_tool.catalog_map.get(portal_type) or []
1298
        else:
1299
            # DX content type
1300
            # => resolve the `_catalogs` attribute from the class
1301
            klass = resolveDottedName(fti.klass)
1302
            # XXX: Refactor multi-catalog behavior to not rely
1303
            #      on this hidden `_catalogs` attribute!
1304
            catalogs = getattr(klass, "_catalogs", [])
1305
1306
    # fetch the catalog objects
1307
    catalogs = filter(None, map(lambda cid: get_tool(cid, None), catalogs))
1308
1309
    if len(catalogs) == 0:
1310
        return [get_tool(default, default=PORTAL_CATALOG)]
1311
1312
    return list(catalogs)
1313
1314
1315
def get_transitions_for(brain_or_object):
1316
    """List available workflow transitions for all workflows
1317
1318
    :param brain_or_object: A single catalog brain or content object
1319
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
1320
    :returns: All possible available and allowed transitions
1321
    :rtype: list[dict]
1322
    """
1323
    workflow = get_tool('portal_workflow')
1324
    transitions = []
1325
    instance = get_object(brain_or_object)
1326
    for wfid in get_workflows_for(brain_or_object):
1327
        wf = workflow[wfid]
1328
        tlist = wf.getTransitionsFor(instance)
1329
        transitions.extend([t for t in tlist if t not in transitions])
1330
    return transitions
1331
1332
1333
def do_transition_for(brain_or_object, transition):
1334
    """Performs a workflow transition for the passed in object.
1335
1336
    :param brain_or_object: A single catalog brain or content object
1337
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
1338
    :returns: The object where the transtion was performed
1339
    """
1340
    if not isinstance(transition, six.string_types):
1341
        fail("Transition type needs to be string, got '%s'" % type(transition))
1342
    obj = get_object(brain_or_object)
1343
    try:
1344
        ploneapi.content.transition(obj, transition)
1345
    except ploneapi.exc.InvalidParameterError as e:
1346
        fail("Failed to perform transition '{}' on {}: {}".format(
1347
             transition, obj, str(e)))
1348
    return obj
1349
1350
1351
def get_roles_for_permission(permission, brain_or_object):
1352
    """Get a list of granted roles for the given permission on the object.
1353
1354
    :param brain_or_object: A single catalog brain or content object
1355
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
1356
    :returns: Roles for the given Permission
1357
    :rtype: list
1358
    """
1359
    obj = get_object(brain_or_object)
1360
    allowed = set(rolesForPermissionOn(permission, obj))
1361
    return sorted(allowed)
1362
1363
1364
def is_versionable(brain_or_object, policy='at_edit_autoversion'):
1365
    """Checks if the passed in object is versionable.
1366
1367
    :param brain_or_object: A single catalog brain or content object
1368
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
1369
    :returns: True if the object is versionable
1370
    :rtype: bool
1371
    """
1372
    pr = get_tool("portal_repository")
1373
    obj = get_object(brain_or_object)
1374
    return pr.supportsPolicy(obj, 'at_edit_autoversion') \
1375
        and pr.isVersionable(obj)
1376
1377
1378
def get_version(brain_or_object):
1379
    """Get the version of the current object
1380
1381
    :param brain_or_object: A single catalog brain or content object
1382
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
1383
    :returns: The current version of the object, or None if not available
1384
    :rtype: int or None
1385
    """
1386
    obj = get_object(brain_or_object)
1387
    if not is_versionable(obj):
1388
        return None
1389
    return getattr(aq_base(obj), "version_id", 0)
1390
1391
1392
def get_view(name, context=None, request=None, default=None):
1393
    """Get the view by name
1394
1395
    :param name: The name of the view
1396
    :type name: str
1397
    :param context: The context to query the view
1398
    :type context: ATContentType/DexterityContentType/CatalogBrain
1399
    :param request: The request to query the view
1400
    :type request: HTTPRequest object
1401
    :returns: HTTP Request
1402
    :rtype: Products.Five.metaclass View object
1403
    """
1404
    context = context or get_portal()
1405
    request = request or get_request() or None
1406
    view = queryMultiAdapter((get_object(context), request), name=name)
1407
    if view is None:
1408
        return default
1409
    return view
1410
1411
1412
def get_request():
1413
    """Get the global request object
1414
1415
    :returns: HTTP Request
1416
    :rtype: HTTPRequest object
1417
    """
1418
    return globalrequest.getRequest()
1419
1420
1421
def get_test_request():
1422
    """Get the TestRequest object
1423
    """
1424
    request = TestRequest()
1425
    directlyProvides(request, IAttributeAnnotatable)
1426
    return request
1427
1428
1429
def get_group(group_or_groupname):
1430
    """Return Plone Group
1431
1432
    :param group_or_groupname: Plone group or the name of the group
1433
    :type groupname:  GroupData/str
1434
    :returns: Plone GroupData
1435
    """
1436
    if not group_or_groupname:
1437
1438
        return None
1439
    if hasattr(group_or_groupname, "_getGroup"):
1440
        return group_or_groupname
1441
    gtool = get_tool("portal_groups")
1442
    return gtool.getGroupById(group_or_groupname)
1443
1444
1445
def get_user(user_or_username):
1446
    """Return Plone User
1447
1448
    :param user_or_username: Plone user or user id
1449
    :returns: Plone MemberData
1450
    """
1451
    user = None
1452
    if isinstance(user_or_username, MemberData):
1453
        user = user_or_username
1454
    if isinstance(user_or_username, six.string_types):
1455
        user = get_member_by_login_name(get_portal(), user_or_username, False)
1456
    return user
1457
1458
1459
def get_user_properties(user_or_username):
1460
    """Return User Properties
1461
1462
    :param user_or_username: Plone group identifier
1463
    :returns: Plone MemberData
1464
    """
1465
    user = get_user(user_or_username)
1466
    if user is None:
1467
        return {}
1468
    if not callable(user.getUser):
1469
        return {}
1470
    out = {}
1471
    plone_user = user.getUser()
1472
    for sheet in plone_user.listPropertysheets():
1473
        ps = plone_user.getPropertysheet(sheet)
1474
        out.update(dict(ps.propertyItems()))
1475
    return out
1476
1477
1478
def get_users_by_roles(roles=None):
1479
    """Search Plone users by their roles
1480
1481
    :param roles: Plone role name or list of roles
1482
    :type roles:  list/str
1483
    :returns: List of Plone users having the role(s)
1484
    """
1485
    if not isinstance(roles, (tuple, list)):
1486
        roles = [roles]
1487
    mtool = get_tool("portal_membership")
1488
    return mtool.searchForMembers(roles=roles)
1489
1490
1491
def get_current_user():
1492
    """Returns the current logged in user
1493
1494
    :returns: Current User
1495
    """
1496
    return ploneapi.user.get_current()
1497
1498
1499
def get_user_contact(user, contact_types=['Contact', 'LabContact']):
1500
    """Returns the associated contact of a Plone user
1501
1502
    If the user passed in has no contact associated, return None.
1503
    The `contact_types` parameter filter the portal types for the search.
1504
1505
    :param: Plone user
1506
    :contact_types: List with the contact portal types to search
1507
    :returns: Contact associated to the Plone user or None
1508
    """
1509
    if not user:
1510
        return None
1511
1512
    from senaite.core.catalog import CONTACT_CATALOG  # Avoid circular import
1513
    query = {"portal_type": contact_types, "getUsername": user.getId()}
1514
    brains = search(query, catalog=CONTACT_CATALOG)
1515
    if not brains:
1516
        return None
1517
1518
    if len(brains) > 1:
1519
        # Oops, the user has multiple contacts assigned, return None
1520
        contacts = map(lambda c: c.Title, brains)
1521
        err_msg = "User '{}' is bound to multiple Contacts '{}'"
1522
        err_msg = err_msg.format(user.getId(), ','.join(contacts))
1523
        logger.error(err_msg)
1524
        return None
1525
1526
    return get_object(brains[0])
1527
1528
1529
def get_user_client(user_or_contact):
1530
    """Returns the client of the contact of a Plone user
1531
1532
    If the user passed in has no contact or does not belong to any client,
1533
    returns None.
1534
1535
    :param: Plone user or contact
1536
    :returns: Client the contact of the Plone user belongs to
1537
    """
1538
    if not user_or_contact or ILabContact.providedBy(user_or_contact):
1539
        # Lab contacts cannot belong to a client
1540
        return None
1541
1542
    if not IContact.providedBy(user_or_contact):
1543
        contact = get_user_contact(user_or_contact, contact_types=['Contact'])
1544
        if IContact.providedBy(contact):
1545
            return get_user_client(contact)
1546
        return None
1547
1548
    client = get_parent(user_or_contact)
1549
    if client and IClient.providedBy(client):
1550
        return client
1551
1552
    return None
1553
1554
1555
def get_user_fullname(user_or_contact):
1556
    """Returns the fullname of the contact or Plone user.
1557
1558
    If the user has a linked contact, the fullname of the contact has priority
1559
    over the value of the fullname property from the user
1560
1561
    :param: Plone user or contact
1562
    :returns: Fullname of the contact or user
1563
    """
1564
    if IContact.providedBy(user_or_contact):
1565
        return user_or_contact.getFullname()
1566
1567
    user = get_user(user_or_contact)
1568
    if not user:
1569
        return ""
1570
1571
    # contact's fullname has priority over user's
1572
    contact = get_user_contact(user)
1573
    if not contact:
1574
        return user.getProperty("fullname")
1575
1576
    return contact.getFullname()
1577
1578
1579
def get_user_email(user_or_contact):
1580
    """Returns the email of the contact or Plone user.
1581
    If the user has a linked contact, the email of the contact has priority
1582
    over the value of the email property from the user
1583
    :param: Plone user or contact
1584
    :returns: Fullname of the contact or user
1585
    """
1586
    if IContact.providedBy(user_or_contact):
1587
        return user_or_contact.getEmailAddress()
1588
1589
    user = get_user(user_or_contact)
1590
    if not user:
1591
        return ""
1592
1593
    # contact's email has priority over user's
1594
    contact = get_user_contact(user)
1595
    if not contact:
1596
        return user.getProperty("email", default="")
1597
1598
    return contact.getEmailAddress()
1599
1600
1601
def get_current_client():
1602
    """Returns the current client the current logged in user belongs to, if any
1603
1604
    :returns: Client the current logged in user belongs to or None
1605
    """
1606
    return get_user_client(get_current_user())
1607
1608
1609
def get_cache_key(brain_or_object):
1610
    """Generate a cache key for a common brain or object
1611
1612
    :param brain_or_object: A single catalog brain or content object
1613
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
1614
    :returns: Cache Key
1615
    :rtype: str
1616
    """
1617
    key = [
1618
        get_portal_type(brain_or_object),
1619
        get_id(brain_or_object),
1620
        get_uid(brain_or_object),
1621
        # handle different domains gracefully
1622
        get_url(brain_or_object),
1623
        # Return the microsecond since the epoch in GMT
1624
        get_modification_date(brain_or_object).micros(),
1625
    ]
1626
    return "-".join(map(lambda x: str(x), key))
1627
1628
1629
def bika_cache_key_decorator(method, self, brain_or_object):
1630
    """Bika cache key decorator usable for
1631
1632
    :param brain_or_object: A single catalog brain or content object
1633
    :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain
1634
    :returns: Cache Key
1635
    :rtype: str
1636
    """
1637
    if brain_or_object is None:
1638
        raise DontCache
1639
    return get_cache_key(brain_or_object)
1640
1641
1642
def normalize_id(string):
1643
    """Normalize the id
1644
1645
    :param string: A string to normalize
1646
    :type string: str
1647
    :returns: Normalized ID
1648
    :rtype: str
1649
    """
1650
    if not isinstance(string, six.string_types):
1651
        fail("Type of argument must be string, found '{}'"
1652
             .format(type(string)))
1653
    # get the id nomalizer utility
1654
    normalizer = getUtility(IIDNormalizer).normalize
1655
    return normalizer(string)
1656
1657
1658
def normalize_filename(string):
1659
    """Normalize the filename
1660
1661
    :param string: A string to normalize
1662
    :type string: str
1663
    :returns: Normalized ID
1664
    :rtype: str
1665
    """
1666
    if not isinstance(string, six.string_types):
1667
        fail("Type of argument must be string, found '{}'"
1668
             .format(type(string)))
1669
    # get the file nomalizer utility
1670
    normalizer = getUtility(IFileNameNormalizer).normalize
1671
    return normalizer(string)
1672
1673
1674
def is_uid(uid, validate=False):
1675
    """Checks if the passed in uid is a valid UID
1676
1677
    :param uid: The uid to check
1678
    :param validate: If False, checks if uid is a valid 23 alphanumeric uid. If
1679
    True, also verifies if a brain exists for the uid passed in
1680
    :type uid: string
1681
    :return: True if a valid uid
1682
    :rtype: bool
1683
    """
1684
    if not isinstance(uid, six.string_types):
1685
        return False
1686
    if uid == '0':
1687
        return True
1688
    if len(uid) != 32:
1689
        return False
1690
    if not UID_RX.match(uid):
1691
        return False
1692
    if not validate:
1693
        return True
1694
1695
    # Check if a brain for this uid exists
1696
    uc = get_tool('uid_catalog')
1697
    brains = uc(UID=uid)
1698
    if brains:
1699
        assert (len(brains) == 1)
1700
    return len(brains) > 0
1701
1702
1703
def is_date(date):
1704
    """Checks if the passed in value is a valid Zope's DateTime
1705
1706
    :param date: The date to check
1707
    :type date: DateTime
1708
    :return: True if a valid date
1709
    :rtype: bool
1710
    """
1711
    if not date:
1712
        return False
1713
    return isinstance(date, (DateTime, datetime))
1714
1715
1716
def to_date(value, default=None):
1717
    """Tries to convert the passed in value to Zope's DateTime
1718
1719
    :param value: The value to be converted to a valid DateTime
1720
    :type value: str, DateTime or datetime
1721
    :return: The DateTime representation of the value passed in or default
1722
    """
1723
1724
    # cannot use bika.lims.deprecated (circular dependencies)
1725
    import warnings
1726
    warnings.simplefilter("always", DeprecationWarning)
1727
    warn = "Deprecated: use senaite.core.api.dtime.to_DT instead"
1728
    warnings.warn(warn, category=DeprecationWarning, stacklevel=2)
1729
    warnings.simplefilter("default", DeprecationWarning)
1730
1731
    # prevent circular dependencies
1732
    from senaite.core.api.dtime import to_DT
1733
    date = to_DT(value)
1734
    if not date:
1735
        return to_DT(default)
1736
    return date
1737
1738
1739
def to_minutes(days=0, hours=0, minutes=0, seconds=0, milliseconds=0,
1740
               round_to_int=True):
1741
    """Returns the computed total number of minutes
1742
    """
1743
    total = float(days)*24*60 + float(hours)*60 + float(minutes) + \
1744
        float(seconds)/60 + float(milliseconds)/1000/60
1745
    return int(round(total)) if round_to_int else total
1746
1747
1748
def to_dhm_format(days=0, hours=0, minutes=0, seconds=0, milliseconds=0):
1749
    """Returns a representation of time in a string in xd yh zm format
1750
    """
1751
    minutes = to_minutes(days=days, hours=hours, minutes=minutes,
1752
                         seconds=seconds, milliseconds=milliseconds)
1753
    delta = timedelta(minutes=int(round(minutes)))
1754
    d = delta.days
1755
    h = delta.seconds // 3600
1756
    m = (delta.seconds // 60) % 60
1757
    m = m and "{}m ".format(str(m)) or ""
1758
    d = d and "{}d ".format(str(d)) or ""
1759
    if m and d:
1760
        h = "{}h ".format(str(h))
1761
    else:
1762
        h = h and "{}h ".format(str(h)) or ""
1763
    return "".join([d, h, m]).strip()
1764
1765
1766
def to_int(value, default=_marker):
1767
    """Tries to convert the value to int.
1768
    Truncates at the decimal point if the value is a float
1769
1770
    :param value: The value to be converted to an int
1771
    :return: The resulting int or default
1772
    """
1773
    if is_floatable(value):
1774
        value = to_float(value)
1775
    try:
1776
        return int(value)
1777
    except (TypeError, ValueError):
1778
        if default is None:
1779
            return default
1780
        if default is not _marker:
1781
            return to_int(default)
1782
        fail("Value %s cannot be converted to int" % repr(value))
1783
1784
1785
def is_floatable(value):
1786
    """Checks if the passed in value is a valid floatable number
1787
1788
    :param value: The value to be evaluated as a float number
1789
    :type value: str, float, int
1790
    :returns: True if is a valid float number
1791
    :rtype: bool"""
1792
    try:
1793
        float(value)
1794
        return True
1795
    except (TypeError, ValueError):
1796
        return False
1797
1798
1799
def to_float(value, default=_marker):
1800
    """Converts the passed in value to a float number
1801
1802
    :param value: The value to be converted to a floatable number
1803
    :type value: str, float, int
1804
    :returns: The float number representation of the passed in value
1805
    :rtype: float
1806
    """
1807
    if not is_floatable(value):
1808
        if default is not _marker:
1809
            return to_float(default)
1810
        fail("Value %s is not floatable" % repr(value))
1811
    return float(value)
1812
1813
1814
def float_to_string(value, default=_marker):
1815
    """Convert a float value to string without exponential notation
1816
1817
    This function preserves the whole fraction
1818
1819
    :param value: The float value to be converted to a string
1820
    :type value: str, float, int
1821
    :returns: String representation of the float w/o exponential notation
1822
    :rtype: str
1823
    """
1824
    if not is_floatable(value):
1825
        if default is not _marker:
1826
            return default
1827
        fail("Value %s is not floatable" % repr(value))
1828
1829
    # Leave floatable string values unchanged
1830
    if isinstance(value, six.string_types):
1831
        return value
1832
1833
    value = float(value)
1834
    str_value = str(value)
1835
1836
    if "." in str_value:
1837
        # might be something like 1.23e-26
1838
        front, back = str_value.split(".")
1839
    else:
1840
        # or 1e-07 for 0.0000001
1841
        back = str_value
1842
1843
    if "e-" in back:
1844
        fraction, zeros = back.split("e-")
1845
        # we want to cover the faction and the zeros
1846
        precision = len(fraction) + int(zeros)
1847
        template = "{:.%df}" % precision
1848
        str_value = template.format(value)
1849
    elif "e+" in back:
1850
        # positive numbers, e.g. 1e+16 don't need a fractional part
1851
        str_value = "{:.0f}".format(value)
1852
1853
    # cut off trailing zeros
1854
    if "." in str_value:
1855
        str_value = str_value.rstrip("0").rstrip(".")
1856
1857
    return str_value
1858
1859
1860
def to_searchable_text_metadata(value):
1861
    """Parse the given metadata value to searchable text
1862
1863
    :param value: The raw value of the metadata column
1864
    :returns: Searchable and translated unicode value or None
1865
    """
1866
    if not value:
1867
        return u""
1868
    if value is Missing.Value:
1869
        return u""
1870
    if is_uid(value):
1871
        return u""
1872
    if isinstance(value, (bool)):
1873
        return u""
1874
    if isinstance(value, (list, tuple)):
1875
        values = map(to_searchable_text_metadata, value)
1876
        values = filter(None, values)
1877
        return " ".join(values)
1878
    if isinstance(value, dict):
1879
        return to_searchable_text_metadata(value.values())
1880
    if is_date(value):
1881
        from senaite.core.api.dtime import date_to_string
1882
        return date_to_string(value, "%Y-%m-%d")
1883
    if is_at_content(value):
1884
        return to_searchable_text_metadata(get_title(value))
1885
    if not isinstance(value, six.string_types):
1886
        value = str(value)
1887
    return safe_unicode(value)
1888
1889
1890
def get_registry_record(name, default=None):
1891
    """Returns the value of a registry record
1892
1893
    :param name: [required] name of the registry record
1894
    :type name: str
1895
    :param default: The value returned if the record is not found
1896
    :type default: anything
1897
    :returns: value of the registry record
1898
    """
1899
    return ploneapi.portal.get_registry_record(name, default=default)
1900
1901
1902
def to_display_list(pairs, sort_by="key", allow_empty=True):
1903
    """Create a Plone DisplayList from list items
1904
1905
    :param pairs: list of key, value pairs
1906
    :param sort_by: Sort the items either by key or value
1907
    :param allow_empty: Allow to select an empty value
1908
    :returns: Plone DisplayList
1909
    """
1910
    dl = DisplayList()
1911
1912
    if isinstance(pairs, six.string_types):
1913
        pairs = [pairs, pairs]
1914
    for pair in pairs:
1915
        # pairs is a list of lists -> add each pair
1916
        if isinstance(pair, (tuple, list)):
1917
            dl.add(*pair)
1918
        # pairs is just a single pair -> add it and stop
1919
        if isinstance(pair, six.string_types):
1920
            dl.add(*pairs)
1921
            break
1922
1923
    # add the empty option
1924
    if allow_empty:
1925
        dl.add("", "")
1926
1927
    # sort by key/value
1928
    if sort_by == "key":
1929
        dl = dl.sortedByKey()
1930
    elif sort_by == "value":
1931
        dl = dl.sortedByValue()
1932
1933
    return dl
1934
1935
1936
def text_to_html(text, wrap="p", encoding="utf8"):
1937
    """Convert `\n` sequences in the text to HTML `\n`
1938
1939
    :param text: Plain text to convert
1940
    :param wrap: Toggle to wrap the text in a
1941
    :returns: HTML converted and encoded text
1942
    """
1943
    if not text:
1944
        return ""
1945
    # handle text internally as unicode
1946
    text = safe_unicode(text)
1947
    # replace newline characters with HTML entities
1948
    html = text.replace("\n", "<br/>")
1949
    if wrap:
1950
        html = u"<{tag}>{html}</{tag}>".format(
1951
            tag=wrap, html=html)
1952
    # return encoded html
1953
    return html.encode(encoding)
1954
1955
1956
def to_utf8(string, default=_marker):
1957
    """Encode string to UTF8
1958
1959
    :param string: String to be encoded to UTF8
1960
    :returns: UTF8 encoded string
1961
    """
1962
    if not isinstance(string, six.string_types):
1963
        if default is _marker:
1964
            fail("Expected string type, got '%s'" % type(string))
1965
        return default
1966
    return safe_unicode(string).encode("utf8")
1967
1968
1969
def is_temporary(obj):
1970
    """Returns whether the given object is temporary or not
1971
1972
    :param obj: the object to evaluate
1973
    :returns: True if the object is temporary
1974
    """
1975
    if ITemporaryObject.providedBy(obj):
1976
        return True
1977
1978
    obj_id = getattr(aq_base(obj), "id", None)
1979
    if obj_id is None or UID_RX.match(obj_id):
1980
        return True
1981
1982
    parent = aq_parent(aq_inner(obj))
1983
    if not parent:
1984
        return True
1985
1986
    parent_id = getattr(aq_base(parent), "id", None)
1987
    if parent_id is None or UID_RX.match(parent_id):
1988
        return True
1989
1990
    # Checks to see if we are created inside the portal_factory.
1991
    # This might also happen for DX types in senaite.databox!
1992
    meta_type = getattr(aq_base(parent), "meta_type", "")
1993
    if meta_type == "TempFolder":
1994
        return True
1995
1996
    return False
1997
1998
1999
def mark_temporary(brain_or_object):
2000
    """Mark the object as temporary
2001
    """
2002
    obj = get_object(brain_or_object)
2003
    alsoProvides(obj, ITemporaryObject)
2004
2005
2006
def unmark_temporary(brain_or_object):
2007
    """Unmark the object as temporary
2008
    """
2009
    obj = get_object(brain_or_object)
2010
    noLongerProvides(obj, ITemporaryObject)
2011
2012
2013
def is_string(thing):
2014
    """Checks if the passed in object is a string type
2015
2016
    :param thing: object to test
2017
    :returns: True if the object is a string
2018
    """
2019
    return isinstance(thing, six.string_types)
2020
2021
2022
def is_list(thing):
2023
    """Checks if the passed in object is a list type
2024
2025
    :param thing: object to test
2026
    :returns: True if the object is a list
2027
    """
2028
    return isinstance(thing, list)
2029
2030
2031
def is_list_iterable(thing):
2032
    """Checks if the passed in object can be iterated like a list
2033
2034
    :param thing: object to test
2035
    :returns: True if the object is a list, tuple or set
2036
    """
2037
    return isinstance(thing, (list, tuple, set))
2038
2039
2040
def parse_json(thing, default=""):
2041
    """Parse from JSON
2042
2043
    :param thing: thing to parse
2044
    :param default: value to return if cannot parse
2045
    :returns: the object representing the JSON or default
2046
    """
2047
    try:
2048
        return json.loads(thing)
2049
    except (TypeError, ValueError):
2050
        return default
2051
2052
2053
def to_list(value):
2054
    """Converts the value to a list
2055
2056
    :param value: the value to be represented as a list
2057
    :returns: a list that represents or contains the value
2058
    """
2059
    if is_string(value):
2060
        val = parse_json(value)
2061
        if isinstance(val, (list, tuple, set)):
2062
            value = val
2063
    if not isinstance(value, (list, tuple, set)):
2064
        value = [value]
2065
    return list(value)
2066
2067
2068
def validate(obj, invariants=True):
2069
    """Validates the full object
2070
2071
    :param obj: the object to validate the data against
2072
    :type obj: ATContentType/DexterityContentType
2073
    :returns: a dict with field names as keys and errors as values
2074
    """
2075
    if is_at_content(obj):
2076
        return obj.validate(data=True)
2077
2078
    if not is_dexterity_content(obj):
2079
        raise TypeError("%r is not supported" % type(obj))
2080
2081
    errors = {}
2082
2083
    # iterate through object fields and validate each
2084
    fields = get_fields(obj)
2085
    for field_name, field in fields.items():
2086
        value = getattr(obj, field_name, None)
2087
        value = safe_unicode(value)
2088
        try:
2089
            field.validate(value)
2090
        except RequiredMissing:
2091
            errors[field_name] = "required field"
2092
        except WrongType:
2093
            errors[field_name] = "wrong type"
2094
        except Invalid as ex:
2095
            errors[field_name] = translate(ex.message)
2096
2097
    # validate invariants from schema
2098
    sch = get_schema(obj)
2099
    try:
2100
        sch.validateInvariants(obj)
2101
    except Invalid as ex:
2102
        errors[sch.getName()] = translate(ex.message)
2103
2104
    # validate invariants from behaviors
2105
    for behavior_id in get_behaviors(obj):
2106
        behavior = lookup_behavior_registration(behavior_id)
2107
        try:
2108
            behavior.interface.validateInvariants(obj)
2109
        except Invalid as ex:
2110
            errors[behavior_id] = translate(ex.message)
2111
2112
    return errors
2113
2114
2115
def get_portal_types():
2116
    """Returns a list with the registered portal types
2117
2118
    :returns: List of portal type names
2119
    :rtype: list
2120
    """
2121
    types_tool = get_tool("portal_types")
2122
    return types_tool.listContentTypes()
2123
2124
2125
def is_valid_id(thing, container=None):
2126
    """Checks if is a valid ID candidate based on the following conditions:
2127
2128
      - Contains only letters, numbers, hyphens ('-'), or underscores ('_').
2129
      - Starts with a letter or a number.
2130
      - Ends with a letter or a number.
2131
      - Has a minimum length of 3 characters.
2132
      - Does not match reserved words (e.g., 'REQUEST') or portal type names.
2133
2134
    If a container is provided, it also verifies the container does not have
2135
    any attribute or function with same id.
2136
2137
    :param id: the id to validate
2138
    :type id: str
2139
    :returns: True if the id meets all the conditions, False otherwise.
2140
    """
2141
    id_rx = re.compile(r"^[a-z0-9][a-z0-9_\-]+[a-z0-9]$")
2142
    illegal = re.compile(r"^(aq_|manage|request).*")
2143
2144
    if not is_string(thing):
2145
        return False
2146
2147
    # convert to lower to simplify regex
2148
    lower = thing.lower()
2149
2150
    # check length and characters
2151
    if not id_rx.match(lower):
2152
        return False
2153
2154
    # check for reserved/illegal word
2155
    if illegal.match(lower):
2156
        return False
2157
2158
    # check for portal type names
2159
    portal_types = get_portal_types()
2160
    if thing in portal_types:
2161
        return False
2162
2163
    # check for container attributes and functions
2164
    if container and hasattr(container, thing):
2165
        return False
2166
2167
    return True
2168