Passed
Pull Request — 2.x (#1960)
by Jordi
08:55 queued 03:23
created

senaite.core.z3cform.widgets.address   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 300
Duplicated Lines 4.33 %

Importance

Changes 0
Metric Value
wmc 44
eloc 191
dl 13
loc 300
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 AddressWidget.get_address_format() 0 19 3
A AddressWidget.extract() 0 24 5
A AddressWidget.get_formatted_address() 0 19 4
D AddressWidget.get_input_widget_attributes() 0 65 13
A AjaxSubdivisions.__call__() 0 22 2
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
    # 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, address):
146
        """Returns the address formatted in html
147
        """
148
        address_format = self.get_address_format()
149
150
        # Inject the name of the type of address translated
151
        address_type = address.get("type")
152
        address_type_name = self.address_types.get(address_type, address_type)
153
        address.update({
154
            "address_type": address.get("address_type", address_type_name)
155
        })
156
157
        lines = address_format.split("<br/>")
158
        values = map(lambda line: line.format(**address), lines)
159
        # Some extra cleanup
160
        values = map(lambda line: line.strip(",- "), values)
161
        values = filter(lambda line: line != "()", values)
162
        values = filter(None, values)
163
        return "<br/>".join(values)
164
165
    def extract(self, default=NO_VALUE):
166
        """Extracts the value based on the request and nothing else
167
        """
168
        mapping = {}
169
        prefix = "{}.".format(self.name)
170
        keys = filter(lambda key: key.startswith(prefix), self.request.keys())
171
        for key in keys:
172
            val = key.replace(prefix, "")
173
            idx, subfield_name = val.split(".")
174
            if not api.is_floatable(idx):
175
                continue
176
177
            empty_record = self.field.get_empty_address("")
178
            mapping.setdefault(api.to_int(idx), empty_record).update({
179
                subfield_name: self.request.get(key)
180
            })
181
182
        # Return the list of items sorted correctly
183
        records = []
184
        for index in sorted(mapping.keys()):
185
            record = mapping.get(index)
186
            records.append(record)
187
188
        return records or default
189
190
    def get_input_widget_attributes(self):
191
        """Return input widget attributes for the ReactJS component
192
        """
193
        translate = self.context.translate
194
        countries = map(lambda c: c.name, geo.get_countries())
195
        labels = {country: {} for country in countries}
196
        labels.update({
197
            "country": translate(_("Country")),
198
            "subdivision1": translate(_("State")),
199
            "subdivision2": translate(_("District")),
200
            "city": translate(_("City")),
201
            "zip": translate(_("Postal code")),
202
            "address": translate(_("Address"))
203
        })
204
        sub1 = {}
205
        sub2 = {}
206
207
        for item in self.value:
208
            country = item.get("country")
209
            if country and country not in sub1:
210
                subdivisions = geo.get_subdivisions(country, [])
211
                sub1[country] = map(lambda sub: sub.name, subdivisions)
212
213
                label = _("State")
214
                if subdivisions:
215
                    label = _(subdivisions[0].type)
216
                labels[country]["subdivision1"] = translate(label)
217
218
            subdivision1 = item.get("subdivision1")
219
            if subdivision1 and subdivision1 not in sub2:
220
                subdivisions = geo.get_subdivisions(subdivision1, [])
221
                sub2[subdivision1] = map(lambda sub: sub.name, subdivisions)
222
223
                label = _("District")
224
                if subdivisions:
225
                    label = _(subdivisions[0].type)
226
                labels[country]["subdivision2"] = translate(label)
227
228
        attributes = {
229
            "data-id": self.id,
230
            "data-uid": api.get_uid(self.context),
231
            "data-countries": countries,
232
            "data-subdivisions1": sub1,
233
            "data-subdivisions2": sub2,
234
            "data-name": self.name,
235
            "data-portal_url": api.get_url(api.get_portal()),
236
            "data-items": self.value,
237
            "data-title": self.title,
238
            "data-class": self.klass,
239
            "data-style": self.style,
240
            "data-disabled": self.disabled or False,
241
            "data-labels": labels
242
        }
243
244
        # Generate the i18n labels for address types
245
        for a_type, name in self.address_types.items():
246
            attributes["data-labels"].update({
247
                a_type: translate(name)
248
            })
249
250
        # convert all attributes to JSON
251
        for key, value in attributes.items():
252
            attributes[key] = json.dumps(value)
253
254
        return attributes
255
256
257
@adapter(IAddressField, ISenaiteFormLayer)
258
@implementer(IFieldWidget)
259
def AddressWidgetFactory(field, request):
260
    """Widget factory for Address Widget
261
    """
262
    return FieldWidget(field, AddressWidget(request))
263
264
265
class AjaxSubdivisions(BrowserView):
266
    """Endpoint for the retrieval of geographic subdivisions
267
    """
268
269
    def __call__(self):
270
        """Returns a json with the list of geographic subdivisions that are
271
        immediately below the country or subdivision passed in with `parent`
272
        parameter via POST/GET
273
        """
274
        items = []
275
        parent = self.get_parent()
276
        if not parent:
277
            return json.dumps(items)
278
279
        # Extract the subdivisions for this parent
280
        parent = safe_unicode(parent)
281
        items = geo.get_subdivisions(parent, default=[])
282
283
        def to_dict(subdivision):
284
            return {
285
                "name": subdivision.name,
286
                "code": subdivision.code,
287
                "type": self.context.translate(_(subdivision.type)),
288
            }
289
        items = map(to_dict, items)
290
        return json.dumps(items)
291
292
    def get_parent(self):
293
        """Returns the parent passed through the request
294
        """
295
        parent = self.request.get("parent", None)
296
        if not parent:
297
            values = json.loads(self.request.get("BODY", "{}"))
298
            parent = values.get("parent", None)
299
        return parent
300