Passed
Push — master ( caa188...9d0ad3 )
by Ramon
11:25 queued 07:12
created

bika.lims.utils.formatDuration()   A

Complexity

Conditions 3

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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