Passed
Push — 2.x ( f01285...cc9902 )
by Ramon
11:03
created

QuerySelectWidget.render_reference()   A

Complexity

Conditions 3

Size

Total Lines 18
Code Lines 13

Duplication

Lines 18
Ratio 100 %

Importance

Changes 0
Metric Value
eloc 13
dl 18
loc 18
rs 9.75
c 0
b 0
f 0
cc 3
nop 2
1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of SENAITE.CORE.
4
#
5
# 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-2023 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
import json
22
import string
23
24
from bika.lims import api
25
from bika.lims import logger
26
from Products.CMFPlone.utils import base_hasattr
27
from senaite.core.interfaces import ISenaiteFormLayer
28
from senaite.core.z3cform.interfaces import IQuerySelectWidget
29
from senaite.core.z3cform.widgets.basewidget import BaseWidget
30
from z3c.form.browser import widget
31
from z3c.form.converter import TextLinesConverter
32
from z3c.form.interfaces import IDataConverter
33
from z3c.form.interfaces import IFieldWidget
34
from z3c.form.widget import FieldWidget
35
from zope.component import adapter
36
from zope.interface import implementer
37
from zope.interface import implementer_only
38
from zope.schema.interfaces import IField
39
from zope.schema.interfaces import ISequence
40
41
# See IReferenceWidgetDataProvider for provided object data
42
DISPLAY_TEMPLATE = "<div>${Title}</div>"
43
44
# Search index placeholder for dynamic lookup by the search endpoint
45
SEARCH_INDEX_MARKER = "__search__"
46
47
48
@adapter(ISequence, IQuerySelectWidget)
49
class QuerySelectDataConverter(TextLinesConverter):
50
    """Converter for multi valued List fields
51
    """
52
    def toWidgetValue(self, value):
53
        """Return the value w/o changes
54
55
        Note:
56
57
        All widget templates use the `get_value` method,
58
        which ensures a list of UIDs.
59
60
        However, `toWidgetValue` is called by `widget.update()` implicitly for
61
        `self.value`, which is then used by the `get_value` method again.
62
        """
63
        return value
64
65
    def toFieldValue(self, value):
66
        """Converts a unicode string to a list of UIDs
67
        """
68
        # remove any blank lines at the end
69
        value = value.rstrip("\r\n")
70
        return super(QuerySelectDataConverter, self).toFieldValue(value)
71
72
73
@implementer_only(IQuerySelectWidget)
74
class QuerySelectWidget(widget.HTMLInputWidget, BaseWidget):
75
    """A widget to select one or more items from catalog search
76
    """
77
    klass = u"senaite-queryselect-widget-input"
78
79
    def update(self):
80
        super(QuerySelectWidget, self).update()
81
        widget.addFieldClass(self)
82
83
    def get_input_widget_attributes(self):
84
        """Return input widget attributes for the ReactJS component
85
86
        This method get called from the page template to populate the
87
        attributes that are used by the ReactJS widget component.
88
        """
89
        context = self.get_context()
90
        values = self.get_value()
91
        field = self.field
92
93
        attributes = {
94
            "data-id": self.id,
95
            "data-name": self.name,
96
            "data-values": values,
97
            "data-records": dict(zip(values, map(
98
                lambda ref: self.get_render_data(
99
                    context, field, ref), values))),
100
            "data-value_key": getattr(self, "value_key", "title"),
101
            "data-value_query_index": getattr(
102
                self, "value_query_index", "title"),
103
            "data-api_url": getattr(self, "api_url", "referencewidget_search"),
104
            "data-query": getattr(self, "query", {}),
105
            "data-catalog": getattr(self, "catalog", None),
106
            "data-search_index": getattr(
107
                self, "search_index", SEARCH_INDEX_MARKER),
108
            "data-search_wildcard": getattr(self, "search_wildcard", True),
109
            "data-allow_user_value": getattr(self, "allow_user_value", False),
110
            "data-columns": getattr(self, "columns", []),
111
            "data-display_template": getattr(self, "display_template", None),
112
            "data-limit": getattr(self, "limit", 5),
113
            "data-multi_valued": getattr(self, "multi_valued", True),
114
            "data-disabled": getattr(self, "disabled", False),
115
            "data-readonly": getattr(self, "readonly", False),
116
            "data-clear_results_after_select": getattr(
117
                self, "clear_results_after_select", False),
118
        }
119
120
        for key, value in attributes.items():
121
            # Remove the data prefix
122
            name = key.replace("data-", "", 1)
123
            # lookup attributes for overrides
124
            value = self.lookup(name, context, field, default=value)
125
            # convert all attributes to JSON
126
            attributes[key] = json.dumps(value)
127
128
        return attributes
129
130 View Code Duplication
    def lookup(self, name, context, field, default=None):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
131
        """Check if the context has an override for the given named property
132
133
        The context can either define an attribute or a method with the
134
        following naming convention (all lower case):
135
136
            get_widget_<fieldname>_<propertyname>
137
138
        If an attribute or method is found, this value will be returned,
139
        otherwise the lookup will return the default value
140
141
        :param name: The name of a method to lookup
142
        :param context: The current context of the field
143
        :param field: The current field of the widget
144
        :param default: The default property value for the given name
145
        :returns: New value for the named property
146
        """
147
148
        # check if the current context defines an attribute or a method for the
149
        # given property following our naming convention
150
        context_key = "get_widget_{}_{}".format(field.getName(), name).lower()
151
        if base_hasattr(context, context_key):
152
            attr = getattr(context, context_key, default)
153
            if callable(attr):
154
                # call the context method with additional information
155
                attr = attr(name=name,
156
                            widget=self,
157
                            field=field,
158
                            context=context,
159
                            default=default)
160
            return attr
161
162
        # Allow named methods for query/columns
163
        if name in ["query", "columns"]:
164
            value = getattr(self, name, None)
165
            # allow named methods from the context class
166
            if api.is_string(value):
167
                method = getattr(context, value, None)
168
                if callable(method):
169
                    return method()
170
            # allow function objects directly
171
            if callable(value):
172
                return value()
173
174
        # Call custom getter from the widget class
175
        getter = "get_{}".format(name)
176
        method = getattr(self, getter, None)
177
        if callable(method):
178
            return method(context, field, default=default)
179
180
        # return the widget attribute
181
        return getattr(self, name, default)
182
183
    def get_api_url(self, context, field, default=None):
184
        """JSON API URL to use for this widget
185
186
        NOTE: we need to call the search view on the correct context to allow
187
              context adapter registrations for IReferenceWidgetVocabulary!
188
189
        :param context: The current context of the field
190
        :param field: The current field of the widget
191
        :param default: The default property value
192
        :returns: API URL that is contacted when the search changed
193
        """
194
        # ensure we have an absolute url for the current context
195
        url = api.get_url(context)
196
197
        # NOTE: The temporary context created by `self.get_context` when if we
198
        #       are in the ++add++ creation form for Dexterity contents exists
199
        #       only for the current request and will be gone after response.
200
        #       Therefore, the search view called later will result in a 404!
201
        if getattr(context, "_temporary_", False):
202
            form = self.get_form()
203
            portal_type = getattr(form, "portal_type", "")
204
            parent_url = api.get_url(self.context)
205
            # provide a dynamically created context for the search view to call
206
            # the right search adapters.
207
            url = "{}/@@temporary_context/{}".format(parent_url, portal_type)
208
209
        # get the API URL
210
        api_url = getattr(self, "api_url", default)
211
        # ensure the search path does not contain already the url
212
        search_path = api_url.split(url)[-1]
213
        # return the absolute search url
214
        return "/".join([url, search_path])
215
216
    def get_display_template(self, context, field, default=None):
217
        """Return the display template to use
218
        """
219
        template = getattr(self, "display_template", None)
220
        if template is not None:
221
            return template
222
        return DISPLAY_TEMPLATE
223
224
    def get_multi_valued(self, context, field, default=None):
225
        """Returns if the field is multi valued or not
226
        """
227
        return getattr(self.field, "multi_valued", default)
228
229
    def get_catalog(self, context, field, default=None):
230
        """Lookup the catalog to query
231
232
        :param context: The current context of the field
233
        :param field: The current field of the widget
234
        :param default: The default property value
235
        :returns: Catalog name to query
236
        """
237
        # check if the new `catalog` property is set
238
        catalog_name = getattr(self, "catalog", None)
239
240
        if catalog_name is None:
241
            # try to lookup the catalog for the given object
242
            catalogs = api.get_catalogs_for(context)
243
            # function always returns at least one catalog object
244
            catalog_name = catalogs[0].getId()
245
246
        return catalog_name
247
248 View Code Duplication
    def get_value(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
249
        """Extract the value from the request or get it from the field
250
        """
251
        # get the processed value from the `update` method
252
        value = self.value
253
        # the value might come from the request, e.g. on object creation
254
        if api.is_string(value):
255
            value = IDataConverter(self).toFieldValue(value)
256
        # we handle always lists in the templates
257
        if value is None:
258
            return []
259
        if not isinstance(value, (list, tuple)):
260
            value = [value]
261
        return value
262
263
    def get_render_data(self, context, field, reference):
264
        """Provides the needed data to render the display template
265
266
        :returns: Dictionary with data needed to render the display template
267
        """
268
        if not reference:
269
            return {}
270
271
        return {
272
            "uid": "",
273
            "url": "",
274
            "Title": reference,
275
            "Description": "",
276
            "review_state": "active",
277
        }
278
279 View Code Duplication
    def render_reference(self, reference):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
280
        """Returns a rendered HTML element for the reference
281
        """
282
        context = self.get_context()
283
        field = self.field
284
        display_template = self.get_display_template(context, field)
285
        template = string.Template(display_template)
286
        try:
287
            data = self.get_render_data(context, field, reference)
288
        except ValueError as e:
289
            # Current user might not have privileges to view this object
290
            logger.error(e.message)
291
            return ""
292
293
        if not data:
294
            return ""
295
296
        return template.safe_substitute(data)
297
298
299
@adapter(IField, ISenaiteFormLayer)
300
@implementer(IFieldWidget)
301
def QuerySelectWidgetFactory(field, request):
302
    """Widget factory
303
    """
304
    return FieldWidget(field, QuerySelectWidget(request))
305