Passed
Pull Request — 2.x (#1960)
by Jordi
06:31
created

senaite.core.z3cform.widgets.address   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 311
Duplicated Lines 4.18 %

Importance

Changes 0
Metric Value
wmc 51
eloc 201
dl 13
loc 311
rs 7.92
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A AddressWidget.get_address_format() 0 19 3
B AddressWidget.get_address_type_name() 0 16 7
A AddressWidget.get_formatted_address() 0 19 4
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 AddressWidget.extract() 0 24 5
A AjaxSubdivisions.__call__() 0 22 2
D AddressWidget.get_input_widget_attributes() 0 66 13
A AjaxSubdivisions.get_parent() 0 8 2

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
    address_types = [NAIVE_ADDRESS, PHYSICAL_ADDRESS, POSTAL_ADDRESS,
116
                     BILLING_ADDRESS, BUSINESS_ADDRESS, OTHER_ADDRESS]
117
118
    def get_address_type_name(self, address_type):
119
        """Returns the human-readable name of the address type passed in
120
        """
121
        if address_type == NAIVE_ADDRESS:
122
            return _("Address")
123
        elif address_type == PHYSICAL_ADDRESS:
124
            return _("Physical address")
125
        elif address_type == POSTAL_ADDRESS:
126
            return _("Postal address")
127
        elif address_type == BILLING_ADDRESS:
128
            return _("Billing address")
129
        elif address_type == BUSINESS_ADDRESS:
130
            return _("Business address")
131
        elif address_type == OTHER_ADDRESS:
132
            return _("Other address")
133
        return address_type
134
135
    def get_address_format(self):
136
        """Returns the format for displaying the address
137
        """
138
        if self.address_format:
139
            return self.address_format
140
141
        lines = [
142
            "<strong>{address_type}</strong>",
143
            "{address}",
144
            "{zip} {city}",
145
            "{subdivision2}, {subdivision1}",
146
            "{country}"
147
        ]
148
149
        # Skip the type if multi-address
150
        if not self.field.is_multi_address():
151
            lines = lines[1:]
152
153
        return "<br/>".join(lines)
154
155
    def get_formatted_address(self, address):
156
        """Returns the address formatted in html
157
        """
158
        address_format = self.get_address_format()
159
160
        # Inject the name of the type of address translated
161
        address_type = address.get("type")
162
        address_type_name = self.get_address_type_name(address_type)
163
        address.update({
164
            "address_type": address.get("address_type", address_type_name)
165
        })
166
167
        lines = address_format.split("<br/>")
168
        values = map(lambda line: line.format(**address), lines)
169
        # Some extra cleanup
170
        values = map(lambda line: line.strip(",- "), values)
171
        values = filter(lambda line: line != "()", values)
172
        values = filter(None, values)
173
        return "<br/>".join(values)
174
175
    def extract(self, default=NO_VALUE):
176
        """Extracts the value based on the request and nothing else
177
        """
178
        mapping = {}
179
        prefix = "{}.".format(self.name)
180
        keys = filter(lambda key: key.startswith(prefix), self.request.keys())
181
        for key in keys:
182
            val = key.replace(prefix, "")
183
            idx, subfield_name = val.split(".")
184
            if not api.is_floatable(idx):
185
                continue
186
187
            empty_record = self.field.get_empty_address("")
188
            mapping.setdefault(api.to_int(idx), empty_record).update({
189
                subfield_name: self.request.get(key)
190
            })
191
192
        # Return the list of items sorted correctly
193
        records = []
194
        for index in sorted(mapping.keys()):
195
            record = mapping.get(index)
196
            records.append(record)
197
198
        return records or default
199
200
    def get_input_widget_attributes(self):
201
        """Return input widget attributes for the ReactJS component
202
        """
203
        translate = self.context.translate
204
        countries = map(lambda c: c.name, geo.get_countries())
205
        labels = {country: {} for country in countries}
206
        labels.update({
207
            "country": translate(_("Country")),
208
            "subdivision1": translate(_("State")),
209
            "subdivision2": translate(_("District")),
210
            "city": translate(_("City")),
211
            "zip": translate(_("Postal code")),
212
            "address": translate(_("Address"))
213
        })
214
        sub1 = {}
215
        sub2 = {}
216
217
        for item in self.value:
218
            country = item.get("country")
219
            if country and country not in sub1:
220
                subdivisions = geo.get_subdivisions(country, [])
221
                sub1[country] = map(lambda sub: sub.name, subdivisions)
222
223
                label = _("State")
224
                if subdivisions:
225
                    label = _(subdivisions[0].type)
226
                labels[country]["subdivision1"] = translate(label)
227
228
            subdivision1 = item.get("subdivision1")
229
            if subdivision1 and subdivision1 not in sub2:
230
                subdivisions = geo.get_subdivisions(subdivision1, [])
231
                sub2[subdivision1] = map(lambda sub: sub.name, subdivisions)
232
233
                label = _("District")
234
                if subdivisions:
235
                    label = _(subdivisions[0].type)
236
                labels[country]["subdivision2"] = translate(label)
237
238
        attributes = {
239
            "data-id": self.id,
240
            "data-uid": api.get_uid(self.context),
241
            "data-countries": countries,
242
            "data-subdivisions1": sub1,
243
            "data-subdivisions2": sub2,
244
            "data-name": self.name,
245
            "data-portal_url": api.get_url(api.get_portal()),
246
            "data-items": self.value,
247
            "data-title": self.title,
248
            "data-class": self.klass,
249
            "data-style": self.style,
250
            "data-disabled": self.disabled or False,
251
            "data-labels": labels
252
        }
253
254
        # Generate the i18n labels for address types
255
        for a_type in self.address_types:
256
            name = self.get_address_type_name(a_type)
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