Passed
Pull Request — 2.x (#1960)
by Jordi
05:47
created

senaite.core.z3cform.widgets.address   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 317
Duplicated Lines 4.1 %

Importance

Changes 0
Metric Value
wmc 47
eloc 184
dl 13
loc 317
rs 8.64
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A AddressWidget.get_address_format() 0 19 3
A AddressWidget.get_geographical_hierarchy() 0 43 5
B AddressWidget.get_address_type_name() 0 16 7
A AddressWidget.extract() 0 24 5
A AddressWidget.get_formatted_address() 0 19 4
A AddressDataConverter.toFieldValue() 0 5 1
A AjaxSubdivisions.__call__() 0 15 3
A AddressDataConverter.to_dict() 0 4 2
A AddressWidget.get_input_widget_attributes() 0 35 3
A AddressDataConverter.to_safe_unicode() 0 13 4
A AddressDataConverter.toWidgetValue() 0 5 1
A AddressDataConverter.to_utf8() 13 13 4
A AjaxSubdivisions.get_parent() 0 8 2
A AddressDataConverter.to_list_of_dicts() 0 5 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 get_geographical_hierarchy(self):
176
        """Returns a dict with geographical information as follows:
177
178
            {<country_name_1>: {
179
                <subdivision1_name_1>: [
180
                    <subdivision2_name_1>,
181
                    ...
182
                    <subdivision2_name_n>,
183
                ],
184
                ...
185
                <subdivision1_name_n>: [..]
186
                },
187
            <country_name_2>: {
188
                ...
189
            }
190
191
        Available 2-level subdivisions for each country are only considered for
192
        those countries selected in current addresses
193
        """
194
195
        # Initialize a dict with countries names as keys and None as values
196
        countries = map(lambda c: c.name, geo.get_countries())
197
        countries = dict.fromkeys(countries)
198
199
        # Fill the dict with geo information for selected countries only
200
        for item in self.value:
201
            country = item.get("country")
202
            subdivision1 = item.get("subdivision1")
203
204
            # Init a dict with 1st level subdivision names as keys
205
            subdivisions1 = geo.get_subdivisions(country, default=[])
206
            subdivisions1 = map(lambda sub: sub.name, subdivisions1)
207
            subdivisions1 = dict.fromkeys(subdivisions1)
208
209
            # Fill with 2nd level subdivision names
210
            subdivisions2 = geo.get_subdivisions(subdivision1, default=[])
211
            subdivisions2 = map(lambda sub: sub.name, subdivisions2)
212
            subdivisions1[subdivision1] = subdivisions2
213
214
            # Set the value to the geomap
215
            countries[country] = subdivisions1
216
217
        return countries
218
219
    def extract(self, default=NO_VALUE):
220
        """Extracts the value based on the request and nothing else
221
        """
222
        mapping = {}
223
        prefix = "{}.".format(self.name)
224
        keys = filter(lambda key: key.startswith(prefix), self.request.keys())
225
        for key in keys:
226
            val = key.replace(prefix, "")
227
            idx, subfield_name = val.split(".")
228
            if not api.is_floatable(idx):
229
                continue
230
231
            empty_record = self.field.get_empty_address("")
232
            mapping.setdefault(api.to_int(idx), empty_record).update({
233
                subfield_name: self.request.get(key)
234
            })
235
236
        # Return the list of items sorted correctly
237
        records = []
238
        for index in sorted(mapping.keys()):
239
            record = mapping.get(index)
240
            records.append(record)
241
242
        return records or default
243
244
    def get_input_widget_attributes(self):
245
        """Return input widget attributes for the ReactJS component
246
        """
247
        attributes = {
248
            "data-id": self.id,
249
            "data-uid": api.get_uid(self.context),
250
            "data-geography": self.get_geographical_hierarchy(),
251
            "data-name": self.name,
252
            "data-portal_url": api.get_url(api.get_portal()),
253
            "data-items": self.value,
254
            "data-title": self.title,
255
            "data-class": self.klass,
256
            "data-style": self.style,
257
            "data-disabled": self.disabled or False,
258
            "data-labels": {
259
                "country": api.to_utf8(_("Country")),
260
                "subdivision1": api.to_utf8(_("State")),
261
                "subdivision2": api.to_utf8(_("District")),
262
                "city": api.to_utf8(_("City")),
263
                "zip": api.to_utf8(_("Postal code")),
264
                "address": api.to_utf8(_("Address"))
265
            },
266
        }
267
268
        # Generate the i18n labels for address types
269
        for a_type in self.address_types:
270
            attributes["data-labels"].update({
271
                a_type: api.to_utf8(self.get_address_type_name(a_type))
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
    def __call__(self):
294
        """Returns a json with the list of geographic subdivisions that are
295
        immediately below the country or subdivision passed in with `parent`
296
        parameter via POST/GET
297
        """
298
        items = []
299
        parent = self.get_parent()
300
        if not parent:
301
            return json.dumps(items)
302
303
        # Extract the subdivisions for this parent
304
        parent = safe_unicode(parent)
305
        items = geo.get_subdivisions(parent, default=[])
306
        items = map(lambda sub: [sub.country_code, sub.code, sub.name], items)
307
        return json.dumps(items)
308
309
    def get_parent(self):
310
        """Returns the parent passed through the request
311
        """
312
        parent = self.request.get("parent", None)
313
        if not parent:
314
            values = json.loads(self.request.get("BODY", "{}"))
315
            parent = values.get("parent", None)
316
        return parent
317