Total Complexity | 51 |
Total Lines | 311 |
Duplicated Lines | 4.18 % |
Changes | 0 |
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:
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): |
||
|
|||
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): |
|
90 | """Encodes the data to utf-8 |
||
91 | """ |
||
92 | if isinstance(data, unicode): |
||
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 |