Passed
Pull Request — 2.x (#1879)
by Jordi
06:43
created

UIDReferenceWidget.get_context()   B

Complexity

Conditions 6

Size

Total Lines 31
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 18
dl 0
loc 31
rs 8.5666
c 0
b 0
f 0
cc 6
nop 1
1
# -*- coding: utf-8 -*-
2
3
import json
4
import string
5
6
import six
7
8
from bika.lims import api
9
from Products.CMFPlone.utils import base_hasattr
10
from senaite.app.supermodel import SuperModel
11
from senaite.core.interfaces import ISenaiteFormLayer
12
from senaite.core.schema.interfaces import IUIDReferenceField
13
from senaite.core.z3cform.interfaces import IUIDReferenceWidget
14
from plone.z3cform.fieldsets.interfaces import IDescriptiveGroup
15
from z3c.form import interfaces
16
from z3c.form.browser import widget
17
from z3c.form.browser.textlines import TextLinesWidget
18
from z3c.form.converter import TextLinesConverter
19
from z3c.form.interfaces import IDataConverter
20
from z3c.form.interfaces import IFieldWidget
21
from z3c.form.interfaces import ISubForm
22
from z3c.form.widget import FieldWidget
23
from zope.component import adapter
24
from zope.component import getUtility
25
from zope.component.interfaces import IFactory
26
from zope.interface import implementer
27
28
DISPLAY_TEMPLATE = "<a href='${url}' _target='blank'>${title}</a>"
29
30
_marker = object
31
32
33
@adapter(IUIDReferenceField, interfaces.IWidget)
34
class UIDReferenceDataConverter(TextLinesConverter):
35
    """Converts the raw field data for widget/field usage
36
    """
37
38
    def toWidgetValue(self, value):
39
        """Return the value w/o changes
40
41
        Note:
42
43
        All widget templates use the `get_value` method,
44
        which ensures a list of UIDs.
45
46
        However, `toWidgetValue` is called by `widget.update()` implicitly for
47
        `self.value`, which is then used by the `get_value` method again.
48
        """
49
        return value
50
51
    def toFieldValue(self, value):
52
        """Converts a unicode string to a list of UIDs
53
        """
54
        # remove any blank lines at the end
55
        value = value.rstrip("\r\n")
56
        return super(UIDReferenceDataConverter, self).toFieldValue(value)
57
58
59
@implementer(IUIDReferenceWidget)
60
class UIDReferenceWidget(TextLinesWidget):
61
    """Senaite UID reference widget
62
    """
63
    klass = u"senaite-uidreference-widget"
64
65
    def __init__(self, request, *args, **kw):
66
        super(UIDReferenceWidget, self).__init__(request)
67
        self.request = request
68
69
    def update(self):
70
        super(UIDReferenceWidget, self).update()
71
        widget.addFieldClass(self)
72
73
    def get_form(self):
74
        """Return the current form of the widget
75
        """
76
        form = self.form
77
        # form is a fieldset group
78
        if IDescriptiveGroup.providedBy(form):
79
            form = form.parentForm
80
        # form is a subform (e.g. DataGridFieldObjectSubForm)
81
        if ISubForm.providedBy(form):
82
            form = form.parentForm
83
        return form
84
85
    def get_context(self):
86
        """Get the current context
87
88
        NOTE: If we are in the ++add++ form, `self.context` is the container!
89
              Therefore, we create one here to have access to the methods.
90
        """
91
        schema_iface = self.field.interface
92
        if schema_iface and schema_iface.providedBy(self.context):
93
            return self.context
94
95
        # we might be in a subform, so try first to retrieve the object from
96
        # the base form itself first
97
        form = self.get_form()
98
        portal_type = getattr(form, "portal_type", None)
99
        context = getattr(form, "context", None)
100
        if api.is_object(context):
101
            if api.get_portal_type(context) == portal_type:
102
                return context
103
104
        # Hack alert!
105
        # we are in ++add++ form and have no context!
106
        # Create a temporary object to be able to access class methods
107
        if not portal_type:
108
            portal_type = api.get_portal_type(self.context)
109
        portal_types = api.get_tool("portal_types")
110
        fti = portal_types[portal_type]
111
        factory = getUtility(IFactory, fti.factory)
112
        context = factory("temporary")
113
        # hook into acquisition chain
114
        context = context.__of__(self.context)
115
        return context
116
117
    def attr(self, name, default=None):
118
        """Get the named attribute of the widget or the field
119
        """
120
        value = getattr(self, name, _marker)
121
        if value is _marker:
122
            return default
123
        if isinstance(value, six.string_types):
124
            context = self.get_context()
125
            if base_hasattr(context, value):
126
                attr = getattr(context, value)
127
                if callable(attr):
128
                    value = attr()
129
                else:
130
                    value = attr
131
        return value
132
133
    def get_value(self):
134
        """Extract the value from the request or get it from the field
135
        """
136
        # get the processed value from the `update` method
137
        value = self.value
138
        # the value might come from the request, e.g. on object creation
139
        if isinstance(value, six.string_types):
140
            value = IDataConverter(self).toFieldValue(value)
141
        # we handle always lists in the templates
142
        if value is None:
143
            return []
144
        if not isinstance(value, (list, tuple)):
145
            value = [value]
146
        # just to be sure (paranoid)
147
        return [uid for uid in value if api.is_uid(uid)]
148
149
    def get_api_url(self):
150
        portal = api.get_portal()
151
        portal_url = api.get_url(portal)
152
        api_url = "{}/@@API/senaite/v1".format(portal_url)
153
        return api_url
154
155
    def get_display_template(self):
156
        return self.attr("display_template", DISPLAY_TEMPLATE)
157
158
    def get_catalog(self):
159
        return self.attr("catalog", "portal_catalog")
160
161
    def get_query(self):
162
        return self.attr("query", {})
163
164
    def get_columns(self):
165
        return self.attr("columns", [])
166
167
    def get_limit(self):
168
        return self.attr("limit", 25)
169
170
    def is_multi_valued(self):
171
        return getattr(self.field, "multi_valued", False)
172
173
    def get_input_widget_attributes(self):
174
        """Return input widget attributes for the ReactJS component
175
        """
176
        uids = self.get_value()
177
        attributes = {
178
            "data-id": self.id,
179
            "data-name": self.name,
180
            "data-uids": uids,
181
            "data-api_url": self.get_api_url(),
182
            "data-records": dict(zip(uids, map(self.get_obj_info, uids))),
183
            "data-query": self.get_query(),
184
            "data-catalog": self.get_catalog(),
185
            "data-columns": self.get_columns(),
186
            "data-display_template": self.get_display_template(),
187
            "data-limit": self.get_limit(),
188
            "data-multi_valued": self.is_multi_valued(),
189
            "data-disabled": self.disabled or False,
190
            "data-readonly": self.readonly or False,
191
        }
192
193
        # convert all attributes to JSON
194
        for key, value in attributes.items():
195
            attributes[key] = json.dumps(value)
196
197
        return attributes
198
199
    def get_obj_info(self, uid):
200
        """Returns a dictionary with the object info
201
        """
202
        model = SuperModel(uid)
203
        obj_info = model.to_dict()
204
        obj_info["uid"] = uid
205
        obj_info["url"] = api.get_url(model)
206
        return obj_info
207
208
    def render_reference(self, uid):
209
        """Returns a rendered HTML element for the reference
210
        """
211
        template = string.Template(self.get_display_template())
212
        obj_info = self.get_obj_info(uid)
213
        return template.safe_substitute(obj_info)
214
215
216
@adapter(IUIDReferenceField, ISenaiteFormLayer)
217
@implementer(IFieldWidget)
218
def UIDReferenceWidgetFactory(field, request):
219
    """Widget factory for UIDReferenceField
220
    """
221
    return FieldWidget(field, UIDReferenceWidget(request))
222