Passed
Push — master ( babb16...96e596 )
by Jordi
04:46
created

build.bika.lims.utils   F

Complexity

Total Complexity 148

Size/Duplication

Total Lines 875
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 148
eloc 465
dl 0
loc 875
rs 2
c 0
b 0
f 0

45 Functions

Rating   Name   Duplication   Size   Complexity  
B formatDateQuery() 0 20 7
A getUsers() 0 16 4
B encode_header() 0 27 6
A formatDateParms() 0 16 5
A senaite_url_fetcher() 0 68 3
A _cache_key_getUsers() 0 3 1
C createPdf() 0 50 9
A changeWorkflowState() 0 29 2
A printfile() 0 8 1
A zero_fill() 0 2 1
A to_utf8() 0 8 3
A isnumber() 0 2 1
A sendmail() 0 3 1
A sortable_title() 0 23 5
A formatDecimalMark() 0 20 2
A t() 0 13 2
A to_unicode() 0 4 2
A currency_format() 0 8 1
A attachPdf() 0 7 1
A getHiddenAttributesForClass() 0 13 5
A logged_in_client() 0 2 1
A get_invoice_item_description() 0 12 3
A tmpID() 0 3 1
A to_int() 0 12 2
F format_supsub() 0 62 16
A get_image() 0 12 2
A get_strings() 0 22 4
A render_html_attributes() 0 8 2
A get_unicode() 0 22 4
A get_registry_value() 0 10 1
A get_email_link() 0 13 2
A dicts_to_dict() 0 11 2
C copy_field_values() 0 20 10
A user_email() 0 12 3
A is_bika_installed() 0 5 1
A drop_trailing_zeros_decimal() 0 6 2
A to_choices() 0 11 3
A get_progress_bar_html() 0 6 1
A checkPermissions() 0 19 4
A check_permission() 0 11 1
A measure_time() 0 17 1
A user_fullname() 0 12 3
B get_display_list() 0 27 6
A get_link() 0 13 2
B getFromString() 0 13 6

3 Methods

Rating   Name   Duplication   Size   Complexity  
A js_warn.__call__() 0 4 1
A js_err.__call__() 0 4 1
A js_log.__call__() 0 4 1

How to fix   Complexity   

Complexity

Complex classes like build.bika.lims.utils often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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