Passed
Push — 2.x ( 834c0d...271a22 )
by Ramon
07:32 queued 02:28
created

AddressWidget.get_value()   A

Complexity

Conditions 2

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 11
rs 10
c 0
b 0
f 0
cc 2
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-2022 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
import json
22
import six
23
from bika.lims import api
24
from bika.lims import senaiteMessageFactory as _
25
from bika.lims.decorators import returns_json
26
from Products.CMFPlone.utils import safe_unicode
27
from Products.Five.browser import BrowserView
28
from senaite.core.api import geo
29
from senaite.core.decorators import readonly_transaction
30
from senaite.core.interfaces import ISenaiteFormLayer
31
from senaite.core.schema.addressfield import BILLING_ADDRESS
32
from senaite.core.schema.addressfield import BUSINESS_ADDRESS
33
from senaite.core.schema.addressfield import NAIVE_ADDRESS
34
from senaite.core.schema.addressfield import OTHER_ADDRESS
35
from senaite.core.schema.addressfield import PHYSICAL_ADDRESS
36
from senaite.core.schema.addressfield import POSTAL_ADDRESS
37
from senaite.core.schema.interfaces import IAddressField
38
from senaite.core.z3cform.interfaces import IAddressWidget
39
from z3c.form.browser.widget import HTMLFormElement
40
from z3c.form.converter import BaseDataConverter
41
from z3c.form.interfaces import IFieldWidget
42
from z3c.form.interfaces import IWidget
43
from z3c.form.interfaces import NO_VALUE
44
from z3c.form.widget import FieldWidget
45
from z3c.form.widget import Widget
46
from zope.component import adapter
47
from zope.interface import implementer
48
49
50
@adapter(IAddressField, IWidget)
51
class AddressDataConverter(BaseDataConverter):
52
    """Value conversion between field and widget
53
    """
54
    def to_list_of_dicts(self, value):
55
        if not isinstance(value, list):
56
            value = [value]
57
        value = filter(None, value)
58
        return map(self.to_dict, value)
59
60
    def to_dict(self, value):
61
        if not isinstance(value, dict):
62
            return {}
63
        return value
64
65
    def toWidgetValue(self, value):
66
        """Returns the field value with encoded string
67
        """
68
        values = self.to_list_of_dicts(value)
69
        return self.to_utf8(values)
70
71
    def toFieldValue(self, value):
72
        """Converts from widget value to safe_unicode
73
        """
74
        values = self.to_list_of_dicts(value)
75
        return self.to_safe_unicode(values)
76
77
    def to_safe_unicode(self, data):
78
        """Converts the data to unicode
79
        """
80
        if isinstance(data, unicode):
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable unicode does not seem to be defined.
Loading history...
81
            return data
82
        if isinstance(data, list):
83
            return [self.to_safe_unicode(item) for item in data]
84
        if isinstance(data, dict):
85
            return {
86
                self.to_safe_unicode(key): self.to_safe_unicode(value)
87
                for key, value in six.iteritems(data)
88
            }
89
        return safe_unicode(data)
90
91 View Code Duplication
    def to_utf8(self, data):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
92
        """Encodes the data to utf-8
93
        """
94
        if isinstance(data, unicode):
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable unicode does not seem to be defined.
Loading history...
95
            return data.encode("utf-8")
96
        if isinstance(data, list):
97
            return [self.to_utf8(item) for item in data]
98
        if isinstance(data, dict):
99
            return {
100
                self.to_utf8(key): self.to_utf8(value)
101
                for key, value in six.iteritems(data)
102
            }
103
        return data
104
105
106
@implementer(IAddressWidget)
107
class AddressWidget(HTMLFormElement, Widget):
108
    """SENAITE Address Widget
109
    """
110
    klass = u"senaite-address-widget"
111
112
    # Address html format for display. Wildcards accepted: {type},
113
    # {address_type}, {zip}, {city}, {district}, {subdivision1}, {subdivision2},
114
    # {country}. Use '<br/>' for newlines
115
    address_format = ""
116
117
    # Mapping between address_types and their names for i18n
118
    address_types = {
119
        NAIVE_ADDRESS: _("Address"),
120
        PHYSICAL_ADDRESS: _("Physical address"),
121
        POSTAL_ADDRESS: _("Postal address"),
122
        BILLING_ADDRESS: _("Billing address"),
123
        BUSINESS_ADDRESS: _("Business address"),
124
        OTHER_ADDRESS: _("Other address"),
125
    }
126
127
    def get_address_format(self):
128
        """Returns the format for displaying the address
129
        """
130
        if self.address_format:
131
            return self.address_format
132
133
        lines = [
134
            "<strong>{address_type}</strong>",
135
            "{address}",
136
            "{zip} {city}",
137
            "{subdivision2}, {subdivision1}",
138
            "{country}"
139
        ]
140
141
        # Skip the type if multi-address
142
        if not self.field.is_multi_address():
143
            lines = lines[1:]
144
145
        return "<br/>".join(lines)
146
147
    def get_formatted_address(self, data):
148
        """Returns the address formatted in html
149
        """
150
        address = {
151
            "address_type": "",
152
            "address": "",
153
            "zip": "",
154
            "city": "",
155
            "country": "",
156
            "subdivision1": "",
157
            "subdivision2": "",
158
        }
159
        address.update(data)
160
161
        address_format = self.get_address_format()
162
163
        # Inject the name of the type of address translated
164
        address_type = address.get("type")
165
        address_type_name = self.address_types.get(address_type, address_type)
166
        address.update({
167
            "address_type": address.get("address_type", address_type_name)
168
        })
169
170
        lines = address_format.split("<br/>")
171
        values = map(lambda line: line.format(**address), lines)
172
        # Some extra cleanup
173
        values = map(lambda line: line.strip(",- "), values)
174
        values = filter(lambda line: line != "()", values)
175
        values = filter(None, values)
176
        return "<br/>".join(values)
177
178
    def extract(self, default=NO_VALUE):
179
        """Extracts the value based on the request and nothing else
180
        """
181
        mapping = {}
182
        prefix = "{}.".format(self.name)
183
        keys = filter(lambda key: key.startswith(prefix), self.request.keys())
184
        for key in keys:
185
            val = key.replace(prefix, "")
186
            idx, subfield_name = val.split(".")
187
            if not api.is_floatable(idx):
188
                continue
189
190
            empty_record = self.field.get_empty_address("")
191
            mapping.setdefault(api.to_int(idx), empty_record).update({
192
                subfield_name: self.request.get(key)
193
            })
194
195
        # Return the list of items sorted correctly
196
        records = []
197
        for index in sorted(mapping.keys()):
198
            record = mapping.get(index)
199
            records.append(record)
200
201
        return records or default
202
203
    def get_value(self):
204
        values = self.value or []
205
        existing = dict([(a.get("type"), a) for a in values])
206
207
        # HACK - This is necessary for when adding a new object, cause current
208
        # context is the container instead of an object with the desired type
209
        output = []
210
        for address_type in self.field.get_address_types():
211
            default = self.field.get_empty_address(address_type)
212
            output.append(existing.get(address_type, default))
213
        return output
214
215
    def get_input_widget_attributes(self):
216
        """Return input widget attributes for the ReactJS component
217
        """
218
        translate = self.context.translate
219
        countries = map(lambda c: c.name, geo.get_countries())
220
        labels = {country: {} for country in countries}
221
        labels.update({
222
            "country": translate(_("Country")),
223
            "subdivision1": translate(_("State")),
224
            "subdivision2": translate(_("District")),
225
            "city": translate(_("City")),
226
            "zip": translate(_("Postal code")),
227
            "address": translate(_("Address"))
228
        })
229
        sub1 = {}
230
        sub2 = {}
231
232
        for item in self.get_value():
233
            country = item.get("country")
234
            if country and country not in sub1:
235
                subdivisions = geo.get_subdivisions(country, [])
236
                sub1[country] = map(lambda sub: sub.name, subdivisions)
237
238
                label = _("State")
239
                if subdivisions:
240
                    label = _(subdivisions[0].type)
241
                labels[country]["subdivision1"] = translate(label)
242
243
            subdivision1 = item.get("subdivision1")
244
            if subdivision1 and subdivision1 not in sub2:
245
                subdivisions = geo.get_subdivisions(subdivision1, [])
246
                sub2[subdivision1] = map(lambda sub: sub.name, subdivisions)
247
248
                label = _("District")
249
                if subdivisions:
250
                    label = _(subdivisions[0].type)
251
                labels[country]["subdivision2"] = translate(label)
252
253
        attributes = {
254
            "data-id": self.id,
255
            "data-countries": countries,
256
            "data-subdivisions1": sub1,
257
            "data-subdivisions2": sub2,
258
            "data-name": self.name,
259
            "data-portal_url": api.get_url(api.get_portal()),
260
            "data-items": self.get_value(),
261
            "data-title": self.title,
262
            "data-class": self.klass,
263
            "data-style": self.style,
264
            "data-disabled": self.disabled or False,
265
            "data-labels": labels
266
        }
267
268
        # Generate the i18n labels for address types
269
        for a_type, name in self.address_types.items():
270
            attributes["data-labels"].update({
271
                a_type: translate(name)
272
            })
273
274
        # convert all attributes to JSON
275
        for key, value in attributes.items():
276
            attributes[key] = json.dumps(value)
277
278
        return attributes
279
280
281
@adapter(IAddressField, ISenaiteFormLayer)
282
@implementer(IFieldWidget)
283
def AddressWidgetFactory(field, request):
284
    """Widget factory for Address Widget
285
    """
286
    return FieldWidget(field, AddressWidget(request))
287
288
289
class AjaxSubdivisions(BrowserView):
290
    """Endpoint for the retrieval of geographic subdivisions
291
    """
292
293
    @readonly_transaction
294
    @returns_json
295
    def __call__(self):
296
        """Returns a json with the list of geographic subdivisions that are
297
        immediately below the country or subdivision passed in with `parent`
298
        parameter via POST/GET
299
        """
300
        items = []
301
        parent = self.get_parent()
302
        if not parent:
303
            return json.dumps(items)
304
305
        # Extract the subdivisions for this parent
306
        parent = safe_unicode(parent)
307
        items = geo.get_subdivisions(parent, default=[])
308
309
        def to_dict(subdivision):
310
            return {
311
                "name": subdivision.name,
312
                "code": subdivision.code,
313
                "type": self.context.translate(_(subdivision.type)),
314
            }
315
        return [to_dict(item) for item in items]
316
317
    def get_parent(self):
318
        """Returns the parent passed through the request
319
        """
320
        parent = self.request.get("parent", None)
321
        if not parent:
322
            values = json.loads(self.request.get("BODY", "{}"))
323
            parent = values.get("parent", None)
324
        return parent
325