Passed
Push — master ( d8e2ec...90ae0b )
by Jordi
10:07 queued 04:19
created

build.bika.lims.utils.get_strings()   A

Complexity

Conditions 4

Size

Total Lines 22
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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