Passed
Pull Request — 2.x (#1960)
by Jordi
07:28
created

senaite.core.z3cform.widgets.address   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 310
Duplicated Lines 4.19 %

Importance

Changes 0
Metric Value
wmc 51
eloc 199
dl 13
loc 310
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 64 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
201
    def get_input_widget_attributes(self):
202
        """Return input widget attributes for the ReactJS component
203
        """
204
        countries = map(lambda c: c.name, geo.get_countries())
205
        labels = {country: {} for country in countries}
206
        labels.update({
207
            "country": api.to_utf8(_("Country")),
208
            "subdivision1": api.to_utf8(_("State")),
209
            "subdivision2": api.to_utf8(_("District")),
210
            "city": api.to_utf8(_("City")),
211
            "zip": api.to_utf8(_("Postal code")),
212
            "address": api.to_utf8(_("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"] = api.to_utf8(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"] = api.to_utf8(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
            attributes["data-labels"].update({
257
                a_type: api.to_utf8(self.get_address_type_name(a_type))
258
            })
259
260
        # convert all attributes to JSON
261
        for key, value in attributes.items():
262
            attributes[key] = json.dumps(value)
263
264
        return attributes
265
266
267
@adapter(IAddressField, ISenaiteFormLayer)
268
@implementer(IFieldWidget)
269
def AddressWidgetFactory(field, request):
270
    """Widget factory for Address Widget
271
    """
272
    return FieldWidget(field, AddressWidget(request))
273
274
275
class AjaxSubdivisions(BrowserView):
276
    """Endpoint for the retrieval of geographic subdivisions
277
    """
278
279
    def __call__(self):
280
        """Returns a json with the list of geographic subdivisions that are
281
        immediately below the country or subdivision passed in with `parent`
282
        parameter via POST/GET
283
        """
284
        items = []
285
        parent = self.get_parent()
286
        if not parent:
287
            return json.dumps(items)
288
289
        # Extract the subdivisions for this parent
290
        parent = safe_unicode(parent)
291
        items = geo.get_subdivisions(parent, default=[])
292
293
        def to_dict(subdivision):
294
            return {
295
                "name": subdivision.name,
296
                "code": subdivision.code,
297
                "type": subdivision.type
298
            }
299
        items = map(to_dict, items)
300
        return json.dumps(items)
301
302
    def get_parent(self):
303
        """Returns the parent passed through the request
304
        """
305
        parent = self.request.get("parent", None)
306
        if not parent:
307
            values = json.loads(self.request.get("BODY", "{}"))
308
            parent = values.get("parent", None)
309
        return parent
310