Completed
Pull Request — master (#579)
by Pau
02:19
created

BaseAddView.get_default_value()   C

Complexity

Conditions 7

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
dl 0
loc 21
rs 6.4705
c 1
b 0
f 0
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
from datetime import datetime
9
10
from BTrees.OOBTree import OOBTree
11
from DateTime import DateTime
12
from Products.Five.browser import BrowserView
13
from plone import protect
14
from plone.memoize.volatile import cache
15
from zope.annotation.interfaces import IAnnotations
16
from zope.i18n.locales import locales
17
from zope.interface import implements
18
from zope.publisher.interfaces import IPublishTraverse
19
20
from bika.lims import api
21
from bika.lims import logger
22
from bika.lims.utils import cache_key
23
from bika.lims.utils import returns_json
24
25
26
class BaseManageAddView(BrowserView):
27
    """
28
    Base view to create a visibility and order manager for an Add View.
29
    """
30
    template = None
31
32
    def __init__(self, context, request):
33
        BrowserView.__init__(self, context, request)
34
        self.context = context
35
        self.request = request
36
        self.tmp_obj = None
37
        self.CONFIGURATION_STORAGE = None
38
        self.SKIP_FIELD_ON_COPY = []
39
40
    def __call__(self):
41
        protect.CheckAuthenticator(self.request.form)
42
        form = self.request.form
43
        if form.get("submitted", False) and form.get("save", False):
44
            order = form.get("order")
45
            self.set_field_order(order)
46
            visibility = form.get("visibility")
47
            self.set_field_visibility(visibility)
48
        if form.get("submitted", False) and form.get("reset", False):
49
            self.flush()
50
        return self.template()
51
52
    def get_obj(self):
53
        """
54
        This method should return a temporary object. This object should be
55
        of the same type as the one we are creating.
56
        :return: ATObjectType
57
        """
58
        raise NotImplementedError("get_obj is not implemented.")
59
60
    def get_annotation(self):
61
        bika_setup = api.get_bika_setup()
62
        return IAnnotations(bika_setup)
63
64
    @property
65
    def storage(self):
66
        annotation = self.get_annotation()
67
        if annotation.get(self.CONFIGURATION_STORAGE) is None:
68
            annotation[self.CONFIGURATION_STORAGE] = OOBTree()
69
        return annotation[self.CONFIGURATION_STORAGE]
70
71
    def flush(self):
72
        annotation = self.get_annotation()
73
        if annotation.get(self.CONFIGURATION_STORAGE) is not None:
74
            del annotation[self.CONFIGURATION_STORAGE]
75
76
    def set_field_order(self, order):
77
        self.storage.update({"order": order})
78
79
    def get_field_order(self):
80
        order = self.storage.get("order")
81
        if order is None:
82
            return map(lambda f: f.getName(), self.get_fields())
83
        return order
84
85
    def set_field_visibility(self, visibility):
86
        self.storage.update({"visibility": visibility})
87
88
    def get_field_visibility(self):
89
        return self.storage.get("visibility")
90
91
    def is_field_visible(self, field):
92
        if field.required:
93
            return True
94
        visibility = self.get_field_visibility()
95
        if visibility is None:
96
            return True
97
        return visibility.get(field.getName(), True)
98
99
    def get_field(self, name):
100
        """Get a field of the object by name
101
        """
102
        obj = self.get_obj()
103
        return obj.getField(name)
104
105
    def get_fields(self):
106
        """Return all Object fields
107
        """
108
        obj = self.get_obj()
109
        return obj.Schema().fields()
110
111
    def get_sorted_fields(self):
112
        """Return the sorted fields
113
        """
114
        inf = float("inf")
115
        order = self.get_field_order()
116
117
        def field_cmp(field1, field2):
118
            _n1 = field1.getName()
119
            _n2 = field2.getName()
120
            _i1 = _n1 in order and order.index(_n1) + 1 or inf
121
            _i2 = _n2 in order and order.index(_n2) + 1 or inf
122
            return cmp(_i1, _i2)
123
124
        return sorted(self.get_fields(), cmp=field_cmp)
125
126
    def get_fields_with_visibility(self, visibility="edit", mode="add"):
127
        """Return the fields with visibility
128
        """
129
        fields = self.get_sorted_fields()
130
131
        out = []
132
133
        for field in fields:
134
            v = field.widget.isVisible(
135
                self.context, mode, default='invisible', field=field)
136
137
            if self.is_field_visible(field) is False:
138
                v = "hidden"
139
140
            visibility_guard = True
141
            # visibility_guard is a widget field defined in the schema in order
142
            # to know the visibility of the widget when the field is related to
143
            # a dynamically changing content such as workflows. For instance
144
            # those fields related to the workflow will be displayed only if
145
            # the workflow is enabled, otherwise they should not be shown.
146
            if 'visibility_guard' in dir(field.widget):
147
                visibility_guard = eval(field.widget.visibility_guard)
148
            if v == visibility and visibility_guard:
149
                out.append(field)
150
151
        return out
152
153
154
class BaseAddView(BrowserView):
155
    """
156
    Base Object Add view
157
158
    This class offers the necessary methods to create a Add view.
159
    """
160
    template = None
161
162
    def __init__(self, context, request):
163
        BrowserView.__init__(self, context, request)
164
        self.request = request
165
        self.context = context
166
        self.fieldvalues = {}
167
        self.tmp_obj = None
168
        self.icon = None
169
        self.portal = None
170
        self.portal_url = None
171
        self.bika_setup = None
172
        self.came_from = None
173
        self.obj_count = None
174
        self.specifications = None
175
        self.ShowPrices = None
176
        self.SKIP_FIELD_ON_COPY = []
177
178
    def __call__(self):
179
        self.portal = api.get_portal()
180
        self.portal_url = self.portal.absolute_url()
181
        self.bika_setup = api.get_bika_setup()
182
        self.request.set('disable_plone.rightcolumn', 1)
183
        self.came_from = "add"
184
        self.tmp_obj = self.get_obj()
185
        self.obj_count = self.get_obj_count()
186
        self.ShowPrices = self.bika_setup.getShowPrices()
187
188
    def get_view_url(self):
189
        """Return the current view url including request parameters
190
        """
191
        request = self.request
192
        url = request.getURL()
193
        qs = request.getHeader("query_string")
194
        if not qs:
195
            return url
196
        return "{}?{}".format(url, qs)
197
198
    def get_object_by_uid(self, uid):
199
        """Get the object by UID
200
        """
201
        logger.debug("get_object_by_uid::UID={}".format(uid))
202
        obj = api.get_object_by_uid(uid, None)
203
        if obj is None:
204
            logger.warn("!! No object found for UID #{} !!")
205
        return obj
206
207
    def get_currency(self):
208
        """Returns the configured currency
209
        """
210
        bika_setup = api.get_bika_setup()
211
        currency = bika_setup.getCurrency()
212
        currencies = locales.getLocale('en').numbers.currencies
213
        return currencies[currency]
214
215
    def get_obj_count(self):
216
        """Return the obj_count request parameter
217
        """
218
        try:
219
            obj_count = int(self.request.form.get("obj_count", 1))
220
        except (TypeError, ValueError):
221
            obj_count = 1
222
        return obj_count
223
224
    def get_obj(self):
225
        """Create a temporary Object to fetch the fields from
226
        """
227
        raise NotImplementedError("get_obj is not implemented.")
228
229
    def get_obj_schema(self):
230
        """Return the Object schema
231
        """
232
        obj = self.get_obj()
233
        return obj.Schema()
234
235
    def get_obj_fields(self):
236
        """Return the Object schema fields (including extendend fields)
237
        """
238
        schema = self.get_obj_schema()
239
        return schema.fields()
240
241
    def get_fieldname(self, field, objnum):
242
        """Generate a new fieldname with a '-<objnum>' suffix
243
        """
244
        name = field.getName()
245
        # ensure we have only *one* suffix
246
        base_name = name.split("-")[0]
247
        suffix = "-{}".format(objnum)
248
        return "{}{}".format(base_name, suffix)
249
250
    def get_input_widget(self, fieldname, objnum=0, **kw):
251
        """Get the field widget of the Object in column <objnum>
252
253
        :param fieldname: The base fieldname
254
        :type fieldname: string
255
        """
256
257
        # temporary Object Context
258
        context = self.get_obj()
259
        # request = self.request
260
        schema = context.Schema()
261
262
        # get original field in the schema from the base_fieldname
263
        base_fieldname = fieldname.split("-")[0]
264
        field = context.getField(base_fieldname)
265
266
        # fieldname with -<objnum> suffix
267
        new_fieldname = self.get_fieldname(field, objnum)
268
        new_field = field.copy(name=new_fieldname)
269
270
        # get the default value for this field
271
        fieldvalues = self.fieldvalues
272
        field_value = fieldvalues.get(new_fieldname)
273
        # request_value = request.form.get(new_fieldname)
274
        # value = request_value or field_value
275
        value = field_value
276
277
        def getAccessor(instance):
278
            def accessor(**kw):
279
                return value
280
            return accessor
281
282
        # inject the new context for the widget renderer
283
        # see: Products.Archetypes.Renderer.render
284
        kw["here"] = context
285
        kw["context"] = context
286
        kw["fieldName"] = new_fieldname
287
288
        # make the field available with this name
289
        # XXX: This is actually a hack to make the widget
290
        # available in the template
291
        schema._fields[new_fieldname] = new_field
292
        new_field.getAccessor = getAccessor
293
294
        # set the default value
295
        form = dict()
296
        form[new_fieldname] = value
297
        self.request.form.update(form)
298
299
        logger.info(
300
            "get_input_widget: fieldname={} objnum={} -> "
301
            "new_fieldname={} value={}"
302
            .format(fieldname, objnum, new_fieldname, value))
303
        widget = context.widget(new_fieldname, **kw)
304
        return widget
305
306
    def get_copy_from(self):
307
        """Returns a mapping of UID index -> Object
308
        """
309
        # Create a mapping of source Objects for copy
310
        copy_from = self.request.form.get("copy_from", "").split(",")
311
        # clean out empty strings
312
        copy_from_uids = filter(lambda x: x, copy_from)
313
        out = dict().fromkeys(range(len(copy_from_uids)))
314
        for n, uid in enumerate(copy_from_uids):
315
            obj = self.get_object_by_uid(uid)
316
            if obj is None:
317
                continue
318
            out[n] = obj
319
        logger.info("get_copy_from: uids={}".format(copy_from_uids))
320
        return out
321
322
    def get_default_value(self, field, context):
323
        """Get the default value of the field
324
        """
325
        name = field.getName()
326
        default = field.getDefault(context)
327
        if name == "Batch":
328
            batch = self.get_batch()
329
            if batch is not None:
330
                default = batch
331
        if name == "Client":
332
            client = self.get_client()
333
            if client is not None:
334
                default = client
335
        if name == "Contact":
336
            contact = self.get_default_contact()
337
            if contact is not None:
338
                default = contact
339
        logger.info(
340
            "get_default_value: context={} field={} value={}"
341
            .format(context, name, default))
342
        return default
343
344
    def get_field_value(self, field, context):
345
        """Get the stored value of the field
346
        """
347
        name = field.getName()
348
        value = context.getField(name).get(context)
349
        logger.info(
350
            "get_field_value: context={} field={} value={}"
351
            .format(context, name, value))
352
        return value
353
354
    def get_client(self):
355
        """Returns the Client
356
        """
357
        context = self.context
358
        parent = api.get_parent(context)
359
        if context.portal_type == "Client":
360
            return context
361
        elif parent.portal_type == "Client":
362
            return parent
363
        elif context.portal_type == "Batch":
364
            return context.getClient()
365
        elif parent.portal_type == "Batch":
366
            return context.getClient()
367
        return None
368
369
    def get_batch(self):
370
        """Returns the Batch
371
        """
372
        context = self.context
373
        parent = api.get_parent(context)
374
        if context.portal_type == "Batch":
375
            return context
376
        elif parent.portal_type == "Batch":
377
            return parent
378
        return None
379
380
    def generate_fieldvalues(self, count=1):
381
        """Returns a mapping of '<fieldname>-<count>' to the default value
382
        of the field or the field value of the source Object (copy from)
383
        """
384
        raise NotImplementedError("generate_fieldvalues is not implemented.")
385
386
    def is_field_visible(self, field):
387
        """Check if the field is visible
388
        """
389
        context = self.context
390
        fieldname = field.getName()
391
392
        # hide the Client field on client and batch contexts
393
        if fieldname == "Client" and context.portal_type in ("Client", ):
394
            return False
395
396
        # hide the Batch field on batch contexts
397
        if fieldname == "Batch" and context.portal_type in ("Batch", ):
398
            return False
399
400
        return True
401
402
    def get_fields_with_visibility(self, visibility, mode="add"):
403
        """Return the Object fields with the current visibility.
404
405
        It is recommended to get those values from an AddManagerView
406
        """
407
        raise NotImplementedError(
408
            "get_fields_with_visibility is not implemented.")
409
410
411
class BaseAjaxAddView(BaseAddView):
412
    """Ajax helpers for an Object Add form
413
    """
414
    implements(IPublishTraverse)
415
416
    def __init__(self, context, request):
417
        BaseAddView.__init__(self, context, request)
418
        self.traverse_subpath = []
419
        # Errors are aggregated here, and returned together to the browser
420
        self.errors = {}
421
422
    def publishTraverse(self, request, name):
423
        """ get's called before __call__ for each path name
424
        """
425
        self.traverse_subpath.append(name)
426
        return self
427
428
    @returns_json
429
    def __call__(self):
430
        """Dispatch the path to a method and return JSON.
431
        """
432
        protect.CheckAuthenticator(self.request.form)
433
        protect.PostOnly(self.request.form)
434
435
        if len(self.traverse_subpath) != 1:
436
            return self.error("Not found", status=404)
437
        func_name = "ajax_{}".format(self.traverse_subpath[0])
438
        func = getattr(self, func_name, None)
439
        if func is None:
440
            return self.error("Invalid function", status=400)
441
        return func()
442
443
    def error(self, message, status=500, **kw):
444
        """Set a JSON error object and a status to the response
445
        """
446
        self.request.response.setStatus(status)
447
        result = {"success": False, "errors": message}
448
        result.update(kw)
449
        return result
450
451
    def to_iso_date(self, dt):
452
        """Return the ISO representation of a date object
453
        """
454
        if dt is None:
455
            return ""
456
        if isinstance(dt, DateTime):
457
            return dt.ISO8601()
458
        if isinstance(dt, datetime):
459
            return dt.isoformat()
460
        raise TypeError("{} is neither an instance of DateTime nor datetime"
461
                        .format(repr(dt)))
462
463
    def get_records(self):
464
        """Returns a list of Sample records
465
466
        All fields coming from `request.form` have a number prefix,
467
        e.g. `Contact-0`.
468
        All fields with the same suffix number are grouped together in a
469
        record.
470
        Each record represents the data for one column in the Sample Add
471
        form and contains a mapping of the fieldName (w/o prefix) -> value.
472
473
        Example:
474
        [{"Contact": "Rita Mohale", ...}, {Contact: "Neil Standard"} ...]
475
        """
476
        form = self.request.form
477
        obj_count = self.get_obj_count()
478
479
        records = []
480
        # Group belonging Object fields together
481
        for objenum in range(obj_count):
482
            record = {}
483
            s1 = "-{}".format(objenum)
484
            keys = filter(lambda key: s1 in key, form.keys())
485
            for key in keys:
486
                new_key = key.replace(s1, "")
487
                value = form.get(key)
488
                record[new_key] = value
489
            records.append(record)
490
        return records
491
492
    def get_objs_from_record(self, record, key):
493
        """Returns a mapping of UID -> object
494
        """
495
        uids = self.get_uids_from_record(record, key)
496
        objs = map(self.get_object_by_uid, uids)
497
        return dict(zip(uids, objs))
498
499
    def get_uids_from_record(self, record, key):
500
        """Returns a list of parsed UIDs from a single form field identified
501
        by the given key.
502
503
        A form field ending with `_uid` can contain an empty value, a
504
        single UID or multiple UIDs separated by a comma.
505
506
        This method parses the UID value and returns a list of non-empty UIDs.
507
        """
508
        value = record.get(key, None)
509
        if value is None:
510
            return []
511
        if isinstance(value, basestring):
512
            value = value.split(",")
513
        return filter(lambda uid: uid, value)
514
515
    @cache(cache_key)
516
    def get_base_info(self, obj):
517
        """Returns the base info of an object
518
        """
519
        if obj is None:
520
            return {}
521
522
        info = {
523
            "id": obj.getId(),
524
            "uid": obj.UID(),
525
            "title": obj.Title(),
526
            "description": obj.Description(),
527
            "url": obj.absolute_url(),
528
        }
529
530
        return info
531
532
    @cache(cache_key)
533
    def get_container_info(self, obj):
534
        """Returns the info for a Container
535
        """
536
        info = self.get_base_info(obj)
537
        info.update({})
538
        return info
539
540
    @cache(cache_key)
541
    def get_preservation_info(self, obj):
542
        """Returns the info for a Preservation
543
        """
544
        info = self.get_base_info(obj)
545
        info.update({})
546
        return info
547
548
    def ajax_get_global_settings(self):
549
        """Returns the global Bika settings
550
        """
551
        bika_setup = api.get_bika_setup()
552
        settings = {
553
            "show_prices": bika_setup.getShowPrices(),
554
        }
555
        return settings
556
557
    def ajax_recalculate_records(self):
558
        """Recalculate all Object records and dependencies
559
        """
560
        raise NotImplementedError(
561
            "ajax_recalculate_records is not implemented.")
562
563
    def ajax_submit(self):
564
        """Submit & create the Object
565
        """
566
        raise NotImplementedError(
567
            "ajax_recalculate_records is not implemented.")
568