| 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 |