Passed
Push — master ( 30348e...354e15 )
by Ramon
06:32 queued 01:08
created

ReferenceWidget.process_form()   C

Complexity

Conditions 9

Size

Total Lines 18
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 15
dl 0
loc 18
rs 6.6666
c 0
b 0
f 0
cc 9
nop 6
1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of SENAITE.CORE.
4
#
5
# SENAITE.CORE is free software: you can redistribute it and/or modify it under
6
# the terms of the GNU General Public License as published by the Free Software
7
# Foundation, version 2.
8
#
9
# This program is distributed in the hope that it will be useful, but WITHOUT
10
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12
# details.
13
#
14
# You should have received a copy of the GNU General Public License along with
15
# this program; if not, write to the Free Software Foundation, Inc., 51
16
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
#
18
# Copyright 2018-2019 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
import json
22
23
import plone
24
from AccessControl import ClassSecurityInfo
25
from Products.Archetypes.Registry import registerWidget
26
from Products.Archetypes.Widget import StringWidget
27
from bika.lims import api
28
from bika.lims import logger
29
from bika.lims import bikaMessageFactory as _
30
from bika.lims.browser import BrowserView
31
from bika.lims.interfaces import IReferenceWidgetVocabulary
32
from bika.lims.utils import to_unicode as _u
33
from senaite.core.supermodel.model import SuperModel
34
from zope.component import getAdapters
35
from plone import protect
36
37
38
class ReferenceWidget(StringWidget):
39
    _properties = StringWidget._properties.copy()
40
    _properties.update({
41
        'macro': "bika_widgets/referencewidget",
42
        'helper_js': ("bika_widgets/referencewidget.js",),
43
        'helper_css': ("bika_widgets/referencewidget.css",),
44
45
        'url': 'referencewidget_search',
46
        'catalog_name': 'portal_catalog',
47
48
        # base_query can be a dict or a callable returning a dict
49
        'base_query': {},
50
51
        # This will be faster if the columnNames are catalog indexes
52
        'colModel': [
53
            {'columnName': 'Title', 'width': '30', 'label': _(
54
                'Title'), 'align': 'left'},
55
            {'columnName': 'Description', 'width': '70', 'label': _(
56
                'Description'), 'align': 'left'},
57
            # UID is required in colModel
58
            {'columnName': 'UID', 'hidden': True},
59
        ],
60
61
        # Default field to put back into input elements
62
        'ui_item': 'Title',
63
        'search_fields': ('Title',),
64
        'discard_empty': [],
65
        'popup_width': '550px',
66
        'showOn': False,
67
        'searchIcon': True,
68
        'minLength': '0',
69
        'delay': '500',
70
        'resetButton': False,
71
        'sord': 'asc',
72
        'sidx': 'Title',
73
        'force_all': False,
74
        'portal_types': {},
75
        'add_button': {
76
            'visible': False,
77
            'url': '',
78
            'js_controllers': [],
79
            'return_fields': [],
80
            'overlay_options': {},
81
            },
82
        'edit_button': {
83
            'visible': False,
84
            'url': '',
85
            'js_controllers': [],
86
            'return_fields': [],
87
            'overlay_options': {},
88
        },
89
    })
90
    security = ClassSecurityInfo()
91
92
    security.declarePublic('process_form')
93
94
    def process_form(self, instance, field, form, empty_marker=None,
95
                     emptyReturnsMarker=False):
96
        """Return a UID so that ReferenceField understands.
97
        """
98
        fieldName = field.getName()
99
        if fieldName + "_uid" in form:
100
            uid = form.get(fieldName + "_uid", '')
101
            if field.multiValued and\
102
                    (isinstance(uid, str) or isinstance(uid, unicode)):
103
                uid = uid.split(",")
104
        elif fieldName in form:
105
            uid = form.get(fieldName, '')
106
            if field.multiValued and\
107
                    (isinstance(uid, str) or isinstance(uid, unicode)):
108
                uid = uid.split(",")
109
        else:
110
            uid = None
111
        return uid, {}
112
113
    def get_combogrid_options(self, context, fieldName):
114
        colModel = self.colModel
115
        if 'UID' not in [x['columnName'] for x in colModel]:
116
            colModel.append({'columnName': 'UID', 'hidden': True})
117
        options = {
118
            'url': self.url,
119
            'colModel': colModel,
120
            'showOn': self.showOn,
121
            'width': self.popup_width,
122
            'sord': self.sord,
123
            'sidx': self.sidx,
124
            'force_all': self.force_all,
125
            'search_fields': self.search_fields,
126
            'discard_empty': self.discard_empty,
127
            'minLength': self.minLength,
128
            'resetButton': self.resetButton,
129
            'searchIcon': self.searchIcon,
130
            'delay': self.delay,
131
        }
132
        return json.dumps(options)
133
134
    def get_base_query(self, context, fieldName):
135
        base_query = self.base_query
136
        if callable(base_query):
137
            base_query = base_query()
138
        if base_query and isinstance(base_query, basestring):
139
            base_query = json.loads(base_query)
140
141
        # portal_type: use field allowed types
142
        field = context.Schema().getField(fieldName)
143
        allowed_types = getattr(field, 'allowed_types', None)
144
        allowed_types_method = getattr(field, 'allowed_types_method', None)
145
        if allowed_types_method:
146
            meth = getattr(context, allowed_types_method)
147
            allowed_types = meth(field)
148
        # If field has no allowed_types defined, use widget's portal_type prop
149
        base_query['portal_type'] = allowed_types \
150
            if allowed_types \
151
            else self.portal_types
152
153
        return json.dumps(self.base_query)
154
155
    def initial_uid_field_value(self, value):
156
        if type(value) in (list, tuple):
157
            ret = ",".join([v.UID() for v in value])
158
        elif type(value) in [str, ]:
159
            ret = value
160
        else:
161
            ret = value.UID() if value else value
162
        return ret
163
164
    def get_addbutton_options(self):
165
        # Return a dict with the options defined in the schema whose widget needs an add button.
166
        return {
167
            'visible': self.add_button.get('visible', False),
168
            'url': self.add_button.get('url'),
169
            'return_fields': json.dumps(self.add_button.get('return_fields')),
170
            'js_controllers': json.dumps(self.add_button.get('js_controllers',[])),
171
            'overlay_handler': self.add_button.get('overlay_handler', ''),
172
            'overlay_options': json.dumps(self.add_button.get('overlay_options',{
173
                'filter': 'head>*,#content>*:not(div.configlet),dl.portalMessage.error,dl.portalMessage.info',
174
                'formselector': 'form[id$="base-edit"]',
175
                'closeselector': '[name="form.button.cancel"]',
176
                'width': '70%',
177
                'noform': 'close',}))
178
            }
179
180
    def get_editbutton_options(self):
181
        # Return a dict with the options defined in the schema whose widget needs an edit button.
182
        return {
183
            'visible': self.edit_button.get('visible', False),
184
            'url': self.edit_button.get('url'),
185
            'return_fields': json.dumps(self.edit_button.get('return_fields')),
186
            'js_controllers': json.dumps(self.edit_button.get('js_controllers',[])),
187
            'overlay_handler': self.edit_button.get('overlay_handler', ''),
188
            'overlay_options': json.dumps(self.edit_button.get('overlay_options',{
189
                'filter': 'head>*,#content>*:not(div.configlet),dl.portalMessage.error,dl.portalMessage.info',
190
                'formselector': 'form[id$="base-edit"]',
191
                'closeselector': '[name="form.button.cancel"]',
192
                'width': '70%',
193
                'noform': 'close',}))
194
            }
195
196
registerWidget(ReferenceWidget, title='Reference Widget')
197
198
class ajaxReferenceWidgetSearch(BrowserView):
199
    """ Source for jquery combo dropdown box
200
    """
201
202
    @property
203
    def num_page(self):
204
        """Returns the number of page to render
205
        """
206
        return api.to_int(self.request.get("page", None), default=1)
207
208
    @property
209
    def num_rows_page(self):
210
        """Returns the number of rows per page to render
211
        """
212
        return api.to_int(self.request.get("rows", None), default=10)
213
214
    def get_field_names(self):
215
        """Return the field names to get values for
216
        """
217
        col_model = self.request.get("colModel", None)
218
        if not col_model:
219
            return ["UID",]
220
221
        names = []
222
        col_model = json.loads(_u(col_model))
223
        if isinstance(col_model, (list, tuple)):
224
            names = map(lambda c: c.get("columnName", "").strip(), col_model)
225
226
        # UID is used by reference widget to know the object that the user
227
        # selected from the popup list
228
        if "UID" not in names:
229
            names.append("UID")
230
231
        return filter(None, names)
232
233
    def get_data_record(self, brain, field_names):
234
        """Returns a dict with the column values for the given brain
235
        """
236
        record = {}
237
        model = None
238
239
        for field_name in field_names:
240
            # First try to get the value directly from the brain
241
            value = getattr(brain, field_name, None)
242
243
            # No metadata for this column name
244
            if value is None:
245
                logger.warn("Not a metadata field: {}".format(field_name))
246
                model = model or SuperModel(brain)
247
                value = model.get(field_name, None)
248
                if callable(value):
249
                    value = value()
250
251
            # ' ' instead of '' because empty div fields don't render
252
            # correctly in combo results table
253
            record[field_name] = value or " "
254
255
        return record
256
257
    def search(self):
258
        """Returns the list of brains that match with the request criteria
259
        """
260
        brains = []
261
        # TODO Legacy
262
        for name, adapter in getAdapters((self.context, self.request),
263
                                         IReferenceWidgetVocabulary):
264
            brains.extend(adapter())
265
        return brains
266
267
    def to_data_rows(self, brains):
268
        """Returns a list of dictionaries representing the values of each brain
269
        """
270
        fields = self.get_field_names()
271
        return map(lambda brain: self.get_data_record(brain, fields), brains)
272
273
    def to_json_payload(self, data_rows):
274
        """Returns the json payload
275
        """
276
        num_rows = len(data_rows)
277
        num_page = self.num_page
278
        num_rows_page = self.num_rows_page
279
280
        pages = num_rows / num_rows_page
281
        pages += divmod(num_rows, num_rows_page)[1] and 1 or 0
282
        start = (num_page - 1) * num_rows_page
283
        end = num_page * num_rows_page
284
        payload = {"page": num_page,
285
                   "total": pages,
286
                   "records": num_rows,
287
                   "rows": data_rows[start:end]}
288
        return json.dumps(payload)
289
290
    def __call__(self):
291
        protect.CheckAuthenticator(self.request)
292
293
        # Do the search
294
        brains = self.search()
295
296
        # Generate the data rows to display
297
        data_rows = self.to_data_rows(brains)
298
299
        # Return the payload
300
        return self.to_json_payload(data_rows)
301