Passed
Push — master ( b60410...062e8c )
by Jordi
05:05
created

bika.lims.utils.get_progress_bar_html()   A

Complexity

Conditions 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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