Passed
Push — master ( 5dbef2...07531b )
by Ramon
05:13
created

bika.lims.utils.get_invoice_item_description()   A

Complexity

Conditions 3

Size

Total Lines 13
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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