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

senaite.core.z3cform.widgets.address   A

Complexity

Total Complexity 39

Size/Duplication

Total Lines 277
Duplicated Lines 4.69 %

Importance

Changes 0
Metric Value
wmc 39
eloc 155
dl 13
loc 277
rs 9.28
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A AddressWidget.get_address_format() 0 19 3
A AddressWidget.get_geographical_hierarchy() 0 43 5
A AddressWidget.extract() 0 24 5
A AddressWidget.get_formatted_address() 0 11 4
A AddressDataConverter.toFieldValue() 0 5 1
A AjaxSubdivisions.__call__() 0 15 3
A AddressWidget.get_input_widget_attributes() 0 29 2
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 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   

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:

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