Passed
Push — master ( 33d86a...af7c24 )
by Ramon
03:54
created

bika.lims.utils.dicts_to_dict()   A

Complexity

Conditions 2

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 11
rs 10
c 0
b 0
f 0
cc 2
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-2020 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
import mimetypes
22
import os
23
import re
24
import tempfile
25
import urllib2
26
from AccessControl import ModuleSecurityInfo
27
from AccessControl import allow_module
28
from AccessControl import getSecurityManager
29
from Acquisition import aq_inner
30
from Acquisition import aq_parent
31
from email import Encoders
32
from time import time
33
34
import types
35
from DateTime import DateTime
36
from Products.Archetypes.interfaces.field import IComputedField
37
from Products.Archetypes.public import DisplayList
38
from Products.CMFCore.WorkflowCore import WorkflowException
39
from Products.CMFCore.utils import getToolByName
40
from Products.CMFPlone.utils import safe_unicode
41
from Products.DCWorkflow.events import AfterTransitionEvent
42
from bika.lims import api
43
from bika.lims import logger
44
from bika.lims.browser import BrowserView
45
from email.MIMEBase import MIMEBase
46
from plone.memoize import ram
47
from plone.registry.interfaces import IRegistry
48
from plone.subrequest import subrequest
49
from weasyprint import CSS, HTML
50
from weasyprint import default_url_fetcher
51
from zope.component import queryUtility
52
from zope.event import notify
53
from zope.i18n import translate
54
from zope.i18n.locales import locales
55
56
from bika.lims.interfaces import IClient
57
from bika.lims.interfaces import IClientAwareMixin
58
59
ModuleSecurityInfo('email.Utils').declarePublic('formataddr')
60
allow_module('csv')
61
62
63
def to_utf8(text):
64
    if text is None:
65
        text = ''
66
    unicode_obj = safe_unicode(text)
67
    # If it receives a dictionary or list, it will not work
68
    if isinstance(unicode_obj, unicode):
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable unicode does not seem to be defined.
Loading history...
69
        return unicode_obj.encode('utf-8')
70
    return unicode_obj
71
72
73
def to_unicode(text):
74
    if text is None:
75
        text = ''
76
    return safe_unicode(text)
77
78
79
def t(i18n_msg):
80
    """Safely translate and convert to UTF8, any zope i18n msgid returned from
81
    a bikaMessageFactory _
82
    """
83
    text = to_unicode(i18n_msg)
84
    try:
85
        request = api.get_request()
86
        domain = getattr(i18n_msg, "domain", "senaite.core")
87
        text = translate(text, domain=domain, context=request)
88
    except UnicodeDecodeError:
89
        # TODO: This is only a quick fix
90
        logger.warn("{} couldn't be translated".format(text))
91
    return to_utf8(text)
92
93
94
# Wrapper for PortalTransport's sendmail - don't know why there sendmail
95
# method is marked private
96
ModuleSecurityInfo('Products.bika.utils').declarePublic('sendmail')
97
# Protected( Publish, 'sendmail')
98
99
100
def sendmail(portal, from_addr, to_addrs, msg):
101
    mailspool = portal.portal_mailspool
102
    mailspool.sendmail(from_addr, to_addrs, msg)
103
104
105
class js_log(BrowserView):
106
107
    def __call__(self, message):
108
        """Javascript sends a string for us to place into the log.
109
        """
110
        self.logger.info(message)
111
112
113
class js_err(BrowserView):
114
115
    def __call__(self, message):
116
        """Javascript sends a string for us to place into the error log
117
        """
118
        self.logger.error(message)
119
120
121
class js_warn(BrowserView):
122
123
    def __call__(self, message):
124
        """Javascript sends a string for us to place into the warn log
125
        """
126
        self.logger.warning(message)
127
128
129
ModuleSecurityInfo('Products.bika.utils').declarePublic('printfile')
130
131
132
def printfile(portal, from_addr, to_addrs, msg):
133
134
    """ set the path, then the cmd 'lpr filepath'
135
    temp_path = 'C:/Zope2/Products/Bika/version.txt'
136
137
    os.system('lpr "%s"' %temp_path)
138
    """
139
    pass
140
141
142
def _cache_key_getUsers(method, context, roles=[], allow_empty=True):
143
    key = time() // (60 * 60), roles, allow_empty
144
    return key
145
146
147
@ram.cache(_cache_key_getUsers)
148
def getUsers(context, roles, allow_empty=True):
149
    """ Present a DisplayList containing users in the specified
150
        list of roles
151
    """
152
    mtool = getToolByName(context, 'portal_membership')
153
    pairs = allow_empty and [['', '']] or []
154
    users = mtool.searchForMembers(roles=roles)
155
    for user in users:
156
        uid = user.getId()
157
        fullname = user.getProperty('fullname')
158
        if not fullname:
159
            fullname = uid
160
        pairs.append((uid, fullname))
161
    pairs.sort(lambda x, y: cmp(x[1], y[1]))
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable cmp does not seem to be defined.
Loading history...
162
    return DisplayList(pairs)
163
164
165
def formatDateQuery(context, date_id):
166
    """ Obtain and reformat the from and to dates
167
        into a date query construct
168
    """
169
    from_date = context.REQUEST.get('%s_fromdate' % date_id, None)
170
    if from_date:
171
        from_date = from_date + ' 00:00'
172
    to_date = context.REQUEST.get('%s_todate' % date_id, None)
173
    if to_date:
174
        to_date = to_date + ' 23:59'
175
176
    date_query = {}
177
    if from_date and to_date:
178
        date_query = {'query': [from_date, to_date],
179
                      'range': 'min:max'}
180
    elif from_date or to_date:
181
        date_query = {'query': from_date or to_date,
182
                      'range': from_date and 'min' or 'max'}
183
184
    return date_query
185
186
187
def formatDateParms(context, date_id):
188
    """ Obtain and reformat the from and to dates
189
        into a printable date parameter construct
190
    """
191
    from_date = context.REQUEST.get('%s_fromdate' % date_id, None)
192
    to_date = context.REQUEST.get('%s_todate' % date_id, None)
193
194
    date_parms = {}
195
    if from_date and to_date:
196
        date_parms = 'from %s to %s' % (from_date, to_date)
197
    elif from_date:
198
        date_parms = 'from %s' % (from_date)
199
    elif to_date:
200
        date_parms = 'to %s' % (to_date)
201
202
    return date_parms
203
204
205
def formatDecimalMark(value, decimalmark='.'):
206
    """
207
        Dummy method to replace decimal mark from an input string.
208
        Assumes that 'value' uses '.' as decimal mark and ',' as
209
        thousand mark.
210
        ::value:: is a string
211
        ::returns:: is a string with the decimal mark if needed
212
    """
213
    # We have to consider the possibility of working with decimals such as
214
    # X.000 where those decimals are important because of the precission
215
    # and significant digits matters
216
    # Using 'float' the system delete the extre desimals with 0 as a value
217
    # Example: float(2.00) -> 2.0
218
    # So we have to save the decimal length, this is one reason we are usnig
219
    # strings for results
220
    rawval = str(value)
221
    try:
222
        return decimalmark.join(rawval.split('.'))
223
    except:
224
        return rawval
225
226
227
# encode_header function copied from roundup's rfc2822 package.
228
hqre = re.compile(r'^[A-z0-9!"#$%%&\'()*+,-./:;<=>?@\[\]^_`{|}~ ]+$')
229
230
ModuleSecurityInfo('Products.bika.utils').declarePublic('encode_header')
231
232
233
def encode_header(header, charset='utf-8'):
234
    """ Will encode in quoted-printable encoding only if header
235
    contains non latin characters
236
    """
237
238
    # Return empty headers unchanged
239
    if not header:
240
        return header
241
242
    # return plain header if it does not contain non-ascii characters
243
    if hqre.match(header):
244
        return header
245
246
    quoted = ''
247
    # max_encoded = 76 - len(charset) - 7
248
    for c in header:
249
        # Space may be represented as _ instead of =20 for readability
250
        if c == ' ':
251
            quoted += '_'
252
        # These characters can be included verbatim
253
        elif hqre.match(c):
254
            quoted += c
255
        # Otherwise, replace with hex value like =E2
256
        else:
257
            quoted += "=%02X" % ord(c)
258
259
    return '=?%s?q?%s?=' % (charset, quoted)
260
261
262
def zero_fill(matchobj):
263
    return matchobj.group().zfill(8)
264
265
266
num_sort_regex = re.compile('\d+')
267
268
ModuleSecurityInfo('Products.bika.utils').declarePublic('sortable_title')
269
270
271
def sortable_title(portal, title):
272
    """Convert title to sortable title
273
    """
274
    if not title:
275
        return ''
276
277
    def_charset = portal.plone_utils.getSiteEncoding()
278
    sortabletitle = str(title.lower().strip())
279
    # Replace numbers with zero filled numbers
280
    sortabletitle = num_sort_regex.sub(zero_fill, sortabletitle)
281
    # Truncate to prevent bloat
282
    for charset in [def_charset, 'latin-1', 'utf-8']:
283
        try:
284
            sortabletitle = safe_unicode(sortabletitle, charset)[:30]
285
            sortabletitle = sortabletitle.encode(def_charset or 'utf-8')
286
            break
287
        except UnicodeError:
288
            pass
289
        except TypeError:
290
            # If we get a TypeError if we already have a unicode string
291
            sortabletitle = sortabletitle[:30]
292
            break
293
    return sortabletitle
294
295
# TODO Remove this function
296
def logged_in_client(context, member=None):
297
    return api.get_current_client()
298
299
def changeWorkflowState(content, wf_id, state_id, **kw):
300
    """Change the workflow state of an object
301
    @param content: Content obj which state will be changed
302
    @param state_id: name of the state to put on content
303
    @param kw: change the values of same name of the state mapping
304
    @return: True if succeed. Otherwise, False
305
    """
306
    portal_workflow = api.get_tool("portal_workflow")
307
    workflow = portal_workflow.getWorkflowById(wf_id)
308
    if not workflow:
309
        logger.error("%s: Cannot find workflow id %s" % (content, wf_id))
310
        return False
311
312
    wf_state = {
313
        'action': kw.get("action", None),
314
        'actor': kw.get("actor", api.get_current_user().id),
315
        'comments': "Setting state to %s" % state_id,
316
        'review_state': state_id,
317
        'time': DateTime()
318
    }
319
320
    # Get old and new state info
321
    old_state = workflow._getWorkflowStateOf(content)
322
    new_state = workflow.states.get(state_id, None)
323
    if new_state is None:
324
        raise WorkflowException("Destination state undefined: {}"
325
                                .format(state_id))
326
327
    # Change status and update permissions
328
    portal_workflow.setStatusOf(wf_id, content, wf_state)
329
    workflow.updateRoleMappingsFor(content)
330
331
    # Notify the object has been transitioned
332
    notify(AfterTransitionEvent(content, workflow, old_state, new_state, None,
333
                                wf_state, None))
334
335
    # Map changes to catalog
336
    indexes = ["allowedRolesAndUsers", "review_state", "is_active"]
337
    content.reindexObject(idxs=indexes)
338
    return True
339
340
341
def tmpID():
342
    import binascii
343
    return binascii.hexlify(os.urandom(16))
344
345
346
def isnumber(s):
347
    return api.is_floatable(s)
348
349
350
def senaite_url_fetcher(url):
351
    """Uses plone.subrequest to fetch an internal image resource.
352
353
    If the URL points to an external resource, the URL is handed
354
    to weasyprint.default_url_fetcher.
355
356
    Please see these links for details:
357
358
        - https://github.com/plone/plone.subrequest
359
        - https://pypi.python.org/pypi/plone.subrequest
360
        - https://github.com/senaite/senaite.core/issues/538
361
362
    :returns: A dict with the following keys:
363
364
        * One of ``string`` (a byte string) or ``file_obj``
365
          (a file-like object)
366
        * Optionally: ``mime_type``, a MIME type extracted e.g. from a
367
          *Content-Type* header. If not provided, the type is guessed from the
368
          file extension in the URL.
369
        * Optionally: ``encoding``, a character encoding extracted e.g. from a
370
          *charset* parameter in a *Content-Type* header
371
        * Optionally: ``redirected_url``, the actual URL of the resource
372
          if there were e.g. HTTP redirects.
373
        * Optionally: ``filename``, the filename of the resource. Usually
374
          derived from the *filename* parameter in a *Content-Disposition*
375
          header
376
377
        If a ``file_obj`` key is given, it is the caller’s responsibility
378
        to call ``file_obj.close()``.
379
    """
380
381
    logger.info("Fetching URL '{}' for WeasyPrint".format(url))
382
383
    # get the pyhsical path from the URL
384
    request = api.get_request()
385
    host = request.get_header("HOST")
386
    path = "/".join(request.physicalPathFromURL(url))
387
388
    # fetch the object by sub-request
389
    portal = api.get_portal()
390
    context = portal.restrictedTraverse(path, None)
391
392
    # We double check here to avoid an edge case, where we have the same path
393
    # as well in our local site, e.g. we have `/senaite/img/systems/senaite.png`,
394
    # but the user requested http://www.ridingbytes.com/img/systems/senaite.png:
395
    #
396
    # "/".join(request.physicalPathFromURL("http://www.ridingbytes.com/img/systems/senaite.png"))
397
    # '/senaite/img/systems/senaite.png'
398
    if context is None or host not in url:
399
        logger.info("URL is external, passing over to the default URL fetcher...")
400
        return default_url_fetcher(url)
401
402
    logger.info("URL is local, fetching data by path '{}' via subrequest".format(path))
403
404
    # get the data via an authenticated subrequest
405
    response = subrequest(path)
406
407
    # Prepare the return data as required by WeasyPrint
408
    string = response.getBody()
409
    filename = url.split("/")[-1]
410
    mime_type = mimetypes.guess_type(url)[0]
411
    redirected_url = url
412
413
    return {
414
        "string": string,
415
        "filename": filename,
416
        "mime_type": mime_type,
417
        "redirected_url": redirected_url,
418
    }
419
420
421
def createPdf(htmlreport, outfile=None, css=None, images={}):
422
    """create a PDF from some HTML.
423
    htmlreport: rendered html
424
    outfile: pdf filename; if supplied, caller is responsible for creating
425
             and removing it.
426
    css: remote URL of css file to download
427
    images: A dictionary containing possible URLs (keys) and local filenames
428
            (values) with which they may to be replaced during rendering.
429
    # WeasyPrint will attempt to retrieve images directly from the URL
430
    # referenced in the HTML report, which may refer back to a single-threaded
431
    # (and currently occupied) zeoclient, hanging it.  All image source
432
    # URL's referenced in htmlreport should be local files.
433
    """
434
    # A list of files that should be removed after PDF is written
435
    cleanup = []
436
    css_def = ''
437
    if css:
438
        if css.startswith("http://") or css.startswith("https://"):
439
            # Download css file in temp dir
440
            u = urllib2.urlopen(css)
441
            _cssfile = tempfile.mktemp(suffix='.css')
442
            localFile = open(_cssfile, 'w')
443
            localFile.write(u.read())
444
            localFile.close()
445
            cleanup.append(_cssfile)
446
        else:
447
            _cssfile = css
448
        cssfile = open(_cssfile, 'r')
449
        css_def = cssfile.read()
450
451
    htmlreport = to_utf8(htmlreport)
452
453
    for (key, val) in images.items():
454
        htmlreport = htmlreport.replace(key, val)
455
456
    # render
457
    htmlreport = to_utf8(htmlreport)
458
    renderer = HTML(string=htmlreport, url_fetcher=senaite_url_fetcher, encoding='utf-8')
459
    pdf_fn = outfile if outfile else tempfile.mktemp(suffix=".pdf")
460
    if css:
461
        renderer.write_pdf(pdf_fn, stylesheets=[CSS(string=css_def)])
462
    else:
463
        renderer.write_pdf(pdf_fn)
464
    # return file data
465
    pdf_data = open(pdf_fn, "rb").read()
466
    if outfile is None:
467
        os.remove(pdf_fn)
468
    for fn in cleanup:
469
        os.remove(fn)
470
    return pdf_data
471
472
473
def attachPdf(mimemultipart, pdfreport, filename=None):
474
    part = MIMEBase('application', "pdf")
475
    part.add_header('Content-Disposition',
476
                    'attachment; filename="%s.pdf"' % (filename or tmpID()))
477
    part.set_payload(pdfreport)
478
    Encoders.encode_base64(part)
479
    mimemultipart.attach(part)
480
481
482
def get_invoice_item_description(obj):
483
    if obj.portal_type == 'AnalysisRequest':
484
        samplepoint = obj.getSamplePoint()
485
        samplepoint = samplepoint and samplepoint.Title() or ''
486
        sampletype = obj.getSampleType()
487
        sampletype = sampletype and sampletype.Title() or ''
488
        description = sampletype + ' ' + samplepoint
489
    elif obj.portal_type == 'SupplyOrder':
490
        products = obj.folderlistingFolderContents()
491
        products = [o.getProduct().Title() for o in products]
492
        description = ', '.join(products)
493
    return description
0 ignored issues
show
introduced by
The variable description does not seem to be defined for all execution paths.
Loading history...
494
495
496
def currency_format(context, locale):
497
    locale = locales.getLocale(locale)
498
    currency = context.bika_setup.getCurrency()
499
    symbol = locale.numbers.currencies[currency].symbol
500
501
    def format(val):
502
        return '%s %0.2f' % (symbol, val)
503
    return format
504
505
506
def getHiddenAttributesForClass(classname):
507
    try:
508
        registry = queryUtility(IRegistry)
509
        hiddenattributes = registry.get('bika.lims.hiddenattributes', ())
510
        if hiddenattributes is not None:
511
            for alist in hiddenattributes:
512
                if alist[0] == classname:
513
                    return alist[1:]
514
    except:
515
        logger.warning(
516
            'Probem accessing optionally hidden attributes in registry')
517
518
    return []
519
520
521
def dicts_to_dict(dictionaries, key_subfieldname):
522
    """Convert a list of dictionaries into a dictionary of dictionaries.
523
524
    key_subfieldname must exist in each Record's subfields and have a value,
525
    which will be used as the key for the new dictionary. If a key is duplicated,
526
    the earlier value will be overwritten.
527
    """
528
    result = {}
529
    for d in dictionaries:
530
        result[d[key_subfieldname]] = d
531
    return result
532
533
534
def format_supsub(text):
535
    """
536
    Mainly used for Analysis Service's unit. Transform the text adding
537
    sub and super html scripts:
538
    For super-scripts, use ^ char
539
    For sub-scripts, use _ char
540
    The expression "cm^2" will be translated to "cm²" and the
541
    expression "b_(n-1)" will be translated to "b n-1".
542
    The expression "n_(fibras)/cm^3" will be translated as
543
    "n fibras / cm³"
544
    :param text: text to be formatted
545
    """
546
    out = []
547
    subsup = []
548
    clauses = []
549
    insubsup = True
550
    for c in str(text):
551
        if c == '(':
552
            if insubsup is False:
553
                out.append(c)
554
                clauses.append(')')
555
            else:
556
                clauses.append('')
557
558
        elif c == ')':
559
            if len(clauses) > 0:
560
                out.append(clauses.pop())
561
                if len(subsup) > 0:
562
                    out.append(subsup.pop())
563
564
        elif c == '^':
565
            subsup.append('</sup>')
566
            out.append('<sup>')
567
            insubsup = True
568
            continue
569
570
        elif c == '_':
571
            subsup.append('</sub>')
572
            out.append('<sub>')
573
            insubsup = True
574
            continue
575
576
        elif c == ' ':
577
            if insubsup is True:
578
                out.append(subsup.pop())
579
            else:
580
                out.append(c)
581
        elif c in ['+', '-']:
582
            if len(clauses) == 0 and len(subsup) > 0:
583
                out.append(subsup.pop())
584
            out.append(c)
585
        else:
586
            out.append(c)
587
588
        insubsup = False
589
590
    while True:
591
        if len(subsup) == 0:
592
            break
593
        out.append(subsup.pop())
594
595
    return ''.join(out)
596
597
598
def drop_trailing_zeros_decimal(num):
599
    """ Drops the trailinz zeros from decimal value.
600
        Returns a string
601
    """
602
    out = str(num)
603
    return out.rstrip('0').rstrip('.') if '.' in out else out
604
605
606
def checkPermissions(permissions=[], obj=None):
607
    """
608
    Checks if a user has permissions for a given object.
609
610
    Args:
611
        permissions: The permissions the current user must be compliant with
612
        obj: The object for which the permissions apply
613
614
    Returns:
615
        1 if the user complies with all the permissions for the given object.
616
        Otherwise, it returns empty.
617
    """
618
    if not obj:
619
        return False
620
    sm = getSecurityManager()
621
    for perm in permissions:
622
        if not sm.checkPermission(perm, obj):
623
            return ''
624
    return True
625
626
627
def getFromString(obj, string):
628
    attrobj = obj
629
    attrs = string.split('.')
630
    for attr in attrs:
631
        if hasattr(attrobj, attr):
632
            attrobj = getattr(attrobj, attr)
633
            if isinstance(attrobj, types.MethodType) \
634
               and callable(attrobj):
635
                attrobj = attrobj()
636
        else:
637
            attrobj = None
638
            break
639
    return attrobj if attrobj else None
640
641
642
def user_fullname(obj, userid):
643
    """
644
    Returns the user full name as string.
645
    """
646
    member = obj.portal_membership.getMemberById(userid)
647
    if member is None:
648
        return userid
649
    member_fullname = member.getProperty('fullname')
650
    portal_catalog = getToolByName(obj, 'portal_catalog')
651
    c = portal_catalog(portal_type='Contact', getUsername=userid)
652
    contact_fullname = c[0].getObject().getFullname() if c else None
653
    return contact_fullname or member_fullname or userid
654
655
656
def user_email(obj, userid):
657
    """
658
    This function returns the user email as string.
659
    """
660
    member = obj.portal_membership.getMemberById(userid)
661
    if member is None:
662
        return userid
663
    member_email = member.getProperty('email')
664
    portal_catalog = getToolByName(obj, 'portal_catalog')
665
    c = portal_catalog(portal_type='Contact', getUsername=userid)
666
    contact_email = c[0].getObject().getEmailAddress() if c else None
667
    return contact_email or member_email or ''
668
669
670
def measure_time(func_to_measure):
671
    """
672
    This decorator allows to measure the execution time
673
    of a function and prints it to the console.
674
    :param func_to_measure: function to be decorated
675
    """
676
    def wrap(*args, **kwargs):
677
        start_time = time()
678
        return_value = func_to_measure(*args, **kwargs)
679
        finish_time = time()
680
        log = "%s took %0.4f seconds. start_time = %0.4f - finish_time = %0.4f\n" % (func_to_measure.func_name,
681
                                                                                     finish_time - start_time,
682
                                                                                     start_time,
683
                                                                                     finish_time)
684
        print log
685
        return return_value
686
    return wrap
687
688
689
def copy_field_values(src, dst, ignore_fieldnames=None, ignore_fieldtypes=None):
690
    ignore_fields = ignore_fieldnames if ignore_fieldnames else []
691
    ignore_types = ignore_fieldtypes if ignore_fieldtypes else []
692
    if 'id' not in ignore_fields:
693
        ignore_fields.append('id')
694
695
    src_schema = src.Schema()
696
    dst_schema = dst.Schema()
697
698
    for field in src_schema.fields():
699
        if IComputedField.providedBy(field):
700
            continue
701
        fieldname = field.getName()
702
        if fieldname in ignore_fields \
703
                or field.type in ignore_types \
704
                or fieldname not in dst_schema:
705
            continue
706
        value = field.get(src)
707
        if value:
708
            dst_schema[fieldname].set(dst, value)
709
710
711
def get_link(href, value=None, **kwargs):
712
    """
713
    Returns a well-formed link. If href is None/empty, returns an empty string
714
    :param href: value to be set for attribute href
715
    :param value: the text to be displayed. If None, the href itself is used
716
    :param kwargs: additional attributes and values
717
    :return: a well-formed html anchor
718
    """
719
    if not href:
720
        return ""
721
    anchor_value = value and value or href
722
    attr = render_html_attributes(**kwargs)
723
    return '<a href="{}" {}>{}</a>'.format(href, attr, anchor_value)
724
725
726
def get_link_for(obj, **kwargs):
727
    """Returns a well-formed html anchor to the object
728
    """
729
    if not obj:
730
        return ""
731
    href = api.get_url(obj)
732
    value = api.get_title(obj)
733
    return get_link(href=href, value=value, **kwargs)
734
735
736
def get_email_link(email, value=None):
737
    """
738
    Returns a well-formed link to an email address. If email is None/empty,
739
    returns an empty string
740
    :param email: email address
741
    :param link_text: text to be displayed. If None, the email itself is used
742
    :return: a well-formatted html anchor
743
    """
744
    if not email:
745
        return ""
746
    mailto = 'mailto:{}'.format(email)
747
    link_value = value and value or email
748
    return get_link(mailto, link_value)
749
750
751
def get_image(name, **kwargs):
752
    """Returns a well-formed image
753
    :param name: file name of the image
754
    :param kwargs: additional attributes and values
755
    :return: a well-formed html img
756
    """
757
    if not name:
758
        return ""
759
    portal_url = api.get_url(api.get_portal())
760
    attr = render_html_attributes(**kwargs)
761
    html = '<img src="{}/++resource++bika.lims.images/{}" {}/>'
762
    return html.format(portal_url, name, attr)
763
764
765
def get_progress_bar_html(percentage):
766
    """Returns an html that represents a progress bar
767
    """
768
    return '<div class="progress md-progress">' \
769
           '<div class="progress-bar" style="width: {0}%">{0}%</div>' \
770
           '</div>'.format(percentage or 0)
771
772
773
def render_html_attributes(**kwargs):
774
    """Returns a string representation of attributes for html entities
775
    :param kwargs: attributes and values
776
    :return: a well-formed string representation of attributes"""
777
    attr = list()
778
    if kwargs:
779
        attr = ['{}="{}"'.format(key, val) for key, val in kwargs.items()]
780
    return " ".join(attr).replace("css_class", "class")
781
782
783
def get_registry_value(key, default=None):
784
    """
785
    Gets the utility for IRegistry and returns the value for the key passed in.
786
    If there is no value for the key passed in, returns default value
787
    :param key: the key in the registry to look for
788
    :param default: default value if the key is not registered
789
    :return: value in the registry for the key passed in
790
    """
791
    registry = queryUtility(IRegistry)
792
    return registry.get(key, default)
793
794
795
def check_permission(permission, obj):
796
    """
797
    Returns if the current user has rights for the permission passed in against
798
    the obj passed in
799
    :param permission: name of the permission
800
    :param obj: the object to check the permission against for the current user
801
    :return: 1 if the user has rights for this permission for the passed in obj
802
    """
803
    mtool = api.get_tool('portal_membership')
804
    object = api.get_object(obj)
805
    return mtool.checkPermission(permission, object)
806
807
808
def to_int(value, default=0):
809
    """
810
    Tries to convert the value passed in as an int. If no success, returns the
811
    default value passed in
812
    :param value: the string to convert to integer
813
    :param default: the default fallback
814
    :return: int representation of the value passed in
815
    """
816
    try:
817
        return int(value)
818
    except (TypeError, ValueError):
819
        return to_int(default, default=0)
820
821
822
def get_strings(data):
823
    """
824
    Convert unicode values to strings even if they belong to lists or dicts.
825
    :param data: an object.
826
    :return: The object with all unicode values converted to string.
827
    """
828
    # if this is a unicode string, return its string representation
829
    if isinstance(data, unicode):
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable unicode does not seem to be defined.
Loading history...
830
        return data.encode('utf-8')
831
832
    # if this is a list of values, return list of string values
833
    if isinstance(data, list):
834
        return [get_strings(item) for item in data]
835
836
    # if this is a dictionary, return dictionary of string keys and values
837
    if isinstance(data, dict):
838
        return {
839
            get_strings(key): get_strings(value)
840
            for key, value in data.iteritems()
841
        }
842
    # if it's anything else, return it in its original form
843
    return data
844
845
846
def get_unicode(data):
847
    """
848
    Convert string values to unicode even if they belong to lists or dicts.
849
    :param data: an object.
850
    :return: The object with all string values converted to unicode.
851
    """
852
    # if this is a common string, return its unicode representation
853
    if isinstance(data, str):
854
        return safe_unicode(data)
855
856
    # if this is a list of values, return list of unicode values
857
    if isinstance(data, list):
858
        return [get_unicode(item) for item in data]
859
860
    # if this is a dictionary, return dictionary of unicode keys and values
861
    if isinstance(data, dict):
862
        return {
863
            get_unicode(key): get_unicode(value)
864
            for key, value in data.iteritems()
865
        }
866
    # if it's anything else, return it in its original form
867
    return data
868
869
870
def is_bika_installed():
871
    """Check if Bika LIMS is installed in the Portal
872
    """
873
    qi = api.portal.get_tool("portal_quickinstaller")
874
    return qi.isProductInstalled("bika.lims")
875
876
877
def get_display_list(brains_or_objects=None, none_item=False):
878
    """
879
    Returns a DisplayList with the items sorted by Title
880
    :param brains_or_objects: list of brains or objects
881
    :param none_item: adds an item with empty uid and text "Select.." in pos 0
882
    :return: DisplayList (uid, title) sorted by title ascending
883
    :rtype: DisplayList
884
    """
885
    if brains_or_objects is None:
886
        return get_display_list(list(), none_item)
887
888
    items = list()
889
    for brain in brains_or_objects:
890
        uid = api.get_uid(brain)
891
        if not uid:
892
            continue
893
        title = api.get_title(brain)
894
        items.append((uid, title))
895
896
    # Sort items by title ascending
897
    items.sort(lambda x, y: cmp(x[1], y[1]))
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable cmp does not seem to be defined.
Loading history...
898
899
    # Add the first item?
900
    if none_item:
901
        items.insert(0, ('', t('Select...')))
902
903
    return DisplayList(items)
904
905
906
def to_choices(display_list):
907
    """Converts a display list to a choices list
908
    """
909
    if not display_list:
910
        return []
911
912
    return map(
913
        lambda item: {
914
            "ResultValue": item[0],
915
            "ResultText": item[1]},
916
        display_list.items())
917
918
919
def chain(obj):
920
    """Generator to walk the acquistion chain of object, considering that it
921
    could be a function.
922
923
    If the thing we are accessing is actually a bound method on an instance,
924
    then after we've checked the method itself, get the instance it's bound to
925
    using im_self, so that we can continue to walk up the acquistion chain from
926
    it (incidentally, this is why we can't juse use aq_chain()).
927
    """
928
    context = aq_inner(obj)
929
930
    while context is not None:
931
        yield context
932
933
        func_object = getattr(context, "im_self", None)
934
        if func_object is not None:
935
            context = aq_inner(func_object)
936
        else:
937
            # Don't use aq_inner() since portal_factory (and probably other)
938
            # things, depends on being able to wrap itself in a fake context.
939
            context = aq_parent(context)
940
941
942
def get_client(obj):
943
    """Returns the client the object passed-in belongs to, if any
944
945
    This walks the acquisition chain up until we find something which provides
946
    either IClient or IClientAwareMixin
947
    """
948
    for obj in chain(obj):
949
        if IClient.providedBy(obj):
950
            return obj
951
        elif IClientAwareMixin.providedBy(obj):
952
            # ClientAwareMixin can return a Client, even if there is no client
953
            # in the acquisition chain
954
            return obj.getClient()
955
956
    return None
957