Passed
Push — 2.x ( e94308...699219 )
by Jordi
09:28
created

  A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 2
rs 10
c 0
b 0
f 0
cc 1
nop 1
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
23
import six
24
25
from bika.lims import api
26
from plone.z3cform.fieldsets.interfaces import IDescriptiveGroup
27
from Products.CMFPlone.utils import base_hasattr
28
from senaite.core.interfaces import ISenaiteFormLayer
29
from senaite.core.z3cform.interfaces import IQuerySelectWidget
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.interfaces import ISubForm
35
from z3c.form.widget import FieldWidget
36
from z3c.form.widget import Widget
37
from zope.component import adapter
38
from zope.component import getUtility
39
from zope.component.interfaces import IFactory
40
from zope.interface import implementer
41
from zope.interface import implementer_only
42
from zope.schema.interfaces import IField
43
from zope.schema.interfaces import ISequence
44
45
_marker = object
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, Widget):
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_form(self):
84
        """Return the current form of the widget
85
        """
86
        form = self.form
87
        # form is a fieldset group
88
        if IDescriptiveGroup.providedBy(form):
89
            form = form.parentForm
90
        # form is a subform (e.g. DataGridFieldObjectSubForm)
91
        if ISubForm.providedBy(form):
92
            form = form.parentForm
93
        return form
94
95 View Code Duplication
    def get_context(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
96
        """Get the current context
97
98
        NOTE: If we are in the ++add++ form, `self.context` is the container!
99
              Therefore, we create one here to have access to the methods.
100
        """
101
        schema_iface = self.field.interface
102
        if schema_iface and schema_iface.providedBy(self.context):
103
            return self.context
104
105
        # we might be in a subform, so try first to retrieve the object from
106
        # the base form itself first
107
        form = self.get_form()
108
        portal_type = getattr(form, "portal_type", None)
109
        context = getattr(form, "context", None)
110
        if api.is_object(context):
111
            if api.get_portal_type(context) == portal_type:
112
                return context
113
114
        # Hack alert!
115
        # we are in ++add++ form and have no context!
116
        # Create a temporary object to be able to access class methods
117
        if not portal_type:
118
            portal_type = api.get_portal_type(self.context)
119
        portal_types = api.get_tool("portal_types")
120
        fti = portal_types[portal_type]
121
        factory = getUtility(IFactory, fti.factory)
122
        context = factory("temporary")
123
        # hook into acquisition chain
124
        context = context.__of__(self.context)
125
        return context
126
127 View Code Duplication
    def lookup(self, name, field, context, default=None):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
128
        """Check if the context has an override for the given named property
129
130
        The context can either define an attribute or a method with the
131
        following naming convention:
132
133
            <fieldname>_<propertyname>
134
135
        If an attribute or method is found, this value will be returned,
136
        otherwise the lookup will return the default value
137
        """
138
139
        # check if the current context defines an attribute or method for the
140
        # given property
141
        key = "{}_{}".format(field.getName(), name)
142
        if base_hasattr(context, key):
143
            attr = getattr(context, key, default)
144
            if callable(attr):
145
                # call the context method with additional information
146
                attr = attr(name=name,
147
                            widget=self,
148
                            field=field,
149
                            context=context,
150
                            default=default)
151
            return attr
152
153
        # return the widget attribute
154
        return getattr(self, name, default)
155
156 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...
157
        """Extract the value from the request or get it from the field
158
        """
159
        # get the processed value from the `update` method
160
        value = self.value
161
        # the value might come from the request, e.g. on object creation
162
        if isinstance(value, six.string_types):
163
            value = IDataConverter(self).toFieldValue(value)
164
        # we handle always lists in the templates
165
        if value is None:
166
            return []
167
        if not isinstance(value, (list, tuple)):
168
            value = [value]
169
        return value
170
171 View Code Duplication
    def get_input_widget_attributes(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
172
        """Return input widget attributes for the ReactJS component
173
        """
174
        context = self.get_context()
175
        values = self.get_value()
176
        attributes = {
177
            "data-id": self.id,
178
            "data-name": self.name,
179
            "data-values": values,
180
            "data-value_key": getattr(self, "value_key", "uid"),
181
            "data-api_url": self.get_api_url(),
182
            "data-query": getattr(self, "query", {}),
183
            "data-catalog": getattr(self, "catalog", "portal_catalog"),
184
            "data-search_index": getattr(self, "search_index", "Title"),
185
            "data-search_wildcard": getattr(self, "search_wildcard", True),
186
            "data-allow_user_value": getattr(self, "allow_user_value", False),
187
            "data-columns": getattr(self, "columns", []),
188
            "data-display_template": getattr(self, "display_template", None),
189
            "data-limit": getattr(self, "limit", 5),
190
            "data-multi_valued": getattr(self, "multi_valued", True),
191
            "data-disabled": getattr(self, "disabled", False),
192
            "data-readonly": getattr(self, "readonly", False),
193
            "data-hide_input_after_select": getattr(
194
                self, "hide_user_input_after_select", False),
195
        }
196
197
        for key, value in attributes.items():
198
            # lookup attributes for overrides
199
            value = self.lookup(key, self.field, context, default=value)
200
            # convert all attributes to JSON
201
            attributes[key] = json.dumps(value)
202
203
        return attributes
204
205
    def get_api_url(self):
206
        """JSON API URL to use for this widget
207
        """
208
        portal = api.get_portal()
209
        portal_url = api.get_url(portal)
210
        api_url = "{}/@@API/senaite/v1".format(portal_url)
211
        return api_url
212
213
214
@adapter(IField, ISenaiteFormLayer)
215
@implementer(IFieldWidget)
216
def QuerySelectWidgetFactory(field, request):
217
    """Widget factory
218
    """
219
    return FieldWidget(field, QuerySelectWidget(request))
220