Passed
Push — master ( f42093...226b52 )
by Ramon
04:24
created

bika.lims.utils.js_warn.__call__()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
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-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 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):
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]))
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_email_link(email, value=None):
727
    """
728
    Returns a well-formed link to an email address. If email is None/empty,
729
    returns an empty string
730
    :param email: email address
731
    :param link_text: text to be displayed. If None, the email itself is used
732
    :return: a well-formatted html anchor
733
    """
734
    if not email:
735
        return ""
736
    mailto = 'mailto:{}'.format(email)
737
    link_value = value and value or email
738
    return get_link(mailto, link_value)
739
740
741
def get_image(name, **kwargs):
742
    """Returns a well-formed image
743
    :param name: file name of the image
744
    :param kwargs: additional attributes and values
745
    :return: a well-formed html img
746
    """
747
    if not name:
748
        return ""
749
    portal_url = api.get_url(api.get_portal())
750
    attr = render_html_attributes(**kwargs)
751
    html = '<img src="{}/++resource++bika.lims.images/{}" {}/>'
752
    return html.format(portal_url, name, attr)
753
754
755
def get_progress_bar_html(percentage):
756
    """Returns an html that represents a progress bar
757
    """
758
    return '<div class="progress md-progress">' \
759
           '<div class="progress-bar" style="width: {0}%">{0}%</div>' \
760
           '</div>'.format(percentage or 0)
761
762
763
def render_html_attributes(**kwargs):
764
    """Returns a string representation of attributes for html entities
765
    :param kwargs: attributes and values
766
    :return: a well-formed string representation of attributes"""
767
    attr = list()
768
    if kwargs:
769
        attr = ['{}="{}"'.format(key, val) for key, val in kwargs.items()]
770
    return " ".join(attr).replace("css_class", "class")
771
772
773
def get_registry_value(key, default=None):
774
    """
775
    Gets the utility for IRegistry and returns the value for the key passed in.
776
    If there is no value for the key passed in, returns default value
777
    :param key: the key in the registry to look for
778
    :param default: default value if the key is not registered
779
    :return: value in the registry for the key passed in
780
    """
781
    registry = queryUtility(IRegistry)
782
    return registry.get(key, default)
783
784
785
def check_permission(permission, obj):
786
    """
787
    Returns if the current user has rights for the permission passed in against
788
    the obj passed in
789
    :param permission: name of the permission
790
    :param obj: the object to check the permission against for the current user
791
    :return: 1 if the user has rights for this permission for the passed in obj
792
    """
793
    mtool = api.get_tool('portal_membership')
794
    object = api.get_object(obj)
795
    return mtool.checkPermission(permission, object)
796
797
798
def to_int(value, default=0):
799
    """
800
    Tries to convert the value passed in as an int. If no success, returns the
801
    default value passed in
802
    :param value: the string to convert to integer
803
    :param default: the default fallback
804
    :return: int representation of the value passed in
805
    """
806
    try:
807
        return int(value)
808
    except (TypeError, ValueError):
809
        return to_int(default, default=0)
810
811
812
def get_strings(data):
813
    """
814
    Convert unicode values to strings even if they belong to lists or dicts.
815
    :param data: an object.
816
    :return: The object with all unicode values converted to string.
817
    """
818
    # if this is a unicode string, return its string representation
819
    if isinstance(data, unicode):
820
        return data.encode('utf-8')
821
822
    # if this is a list of values, return list of string values
823
    if isinstance(data, list):
824
        return [get_strings(item) for item in data]
825
826
    # if this is a dictionary, return dictionary of string keys and values
827
    if isinstance(data, dict):
828
        return {
829
            get_strings(key): get_strings(value)
830
            for key, value in data.iteritems()
831
        }
832
    # if it's anything else, return it in its original form
833
    return data
834
835
836
def get_unicode(data):
837
    """
838
    Convert string values to unicode even if they belong to lists or dicts.
839
    :param data: an object.
840
    :return: The object with all string values converted to unicode.
841
    """
842
    # if this is a common string, return its unicode representation
843
    if isinstance(data, str):
844
        return safe_unicode(data)
845
846
    # if this is a list of values, return list of unicode values
847
    if isinstance(data, list):
848
        return [get_unicode(item) for item in data]
849
850
    # if this is a dictionary, return dictionary of unicode keys and values
851
    if isinstance(data, dict):
852
        return {
853
            get_unicode(key): get_unicode(value)
854
            for key, value in data.iteritems()
855
        }
856
    # if it's anything else, return it in its original form
857
    return data
858
859
860
def is_bika_installed():
861
    """Check if Bika LIMS is installed in the Portal
862
    """
863
    qi = api.portal.get_tool("portal_quickinstaller")
864
    return qi.isProductInstalled("bika.lims")
865
866
867
def get_display_list(brains_or_objects=None, none_item=False):
868
    """
869
    Returns a DisplayList with the items sorted by Title
870
    :param brains_or_objects: list of brains or objects
871
    :param none_item: adds an item with empty uid and text "Select.." in pos 0
872
    :return: DisplayList (uid, title) sorted by title ascending
873
    :rtype: DisplayList
874
    """
875
    if brains_or_objects is None:
876
        return get_display_list(list(), none_item)
877
878
    items = list()
879
    for brain in brains_or_objects:
880
        uid = api.get_uid(brain)
881
        if not uid:
882
            continue
883
        title = api.get_title(brain)
884
        items.append((uid, title))
885
886
    # Sort items by title ascending
887
    items.sort(lambda x, y: cmp(x[1], y[1]))
888
889
    # Add the first item?
890
    if none_item:
891
        items.insert(0, ('', t('Select...')))
892
893
    return DisplayList(items)
894
895
896
def to_choices(display_list):
897
    """Converts a display list to a choices list
898
    """
899
    if not display_list:
900
        return []
901
902
    return map(
903
        lambda item: {
904
            "ResultValue": item[0],
905
            "ResultText": item[1]},
906
        display_list.items())
907
908
909
def chain(obj):
910
    """Generator to walk the acquistion chain of object, considering that it
911
    could be a function.
912
913
    If the thing we are accessing is actually a bound method on an instance,
914
    then after we've checked the method itself, get the instance it's bound to
915
    using im_self, so that we can continue to walk up the acquistion chain from
916
    it (incidentally, this is why we can't juse use aq_chain()).
917
    """
918
    context = aq_inner(obj)
919
920
    while context is not None:
921
        yield context
922
923
        func_object = getattr(context, "im_self", None)
924
        if func_object is not None:
925
            context = aq_inner(func_object)
926
        else:
927
            # Don't use aq_inner() since portal_factory (and probably other)
928
            # things, depends on being able to wrap itself in a fake context.
929
            context = aq_parent(context)
930
931
932
def get_client(obj):
933
    """Returns the client the object passed-in belongs to, if any
934
935
    This walks the acquisition chain up until we find something which provides
936
    either IClient or IClientAwareMixin
937
    """
938
    for obj in chain(obj):
939
        if IClient.providedBy(obj):
940
            return obj
941
        elif IClientAwareMixin.providedBy(obj):
942
            # ClientAwareMixin can return a Client, even if there is no client
943
            # in the acquisition chain
944
            return obj.getClient()
945
946
    return None
947