Passed
Pull Request — 2.x (#1960)
by Ramon
15:25
created

senaite.core.z3cform.widgets.address   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 311
Duplicated Lines 4.18 %

Importance

Changes 0
Metric Value
wmc 44
eloc 200
dl 13
loc 311
rs 8.8798
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A AddressDataConverter.toFieldValue() 0 5 1
A AddressDataConverter.to_dict() 0 4 2
A AddressDataConverter.to_safe_unicode() 0 13 4
A AddressDataConverter.toWidgetValue() 0 5 1
A AddressDataConverter.to_utf8() 13 13 4
A AddressDataConverter.to_list_of_dicts() 0 5 2
A AjaxSubdivisions.__call__() 0 22 2
A AjaxSubdivisions.get_parent() 0 8 2
A AddressWidget.get_address_format() 0 19 3
A AddressWidget.extract() 0 24 5
A AddressWidget.get_formatted_address() 0 30 4
D AddressWidget.get_input_widget_attributes() 0 65 13

1 Function

Rating   Name   Duplication   Size   Complexity  
A AddressWidgetFactory() 0 6 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like senaite.core.z3cform.widgets.address often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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