| Total Complexity | 46 |
| Total Lines | 325 |
| Duplicated Lines | 4 % |
| 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 bika.lims.decorators import returns_json |
||
| 26 | from Products.CMFPlone.utils import safe_unicode |
||
| 27 | from Products.Five.browser import BrowserView |
||
| 28 | from senaite.core.api import geo |
||
| 29 | from senaite.core.decorators import readonly_transaction |
||
| 30 | from senaite.core.interfaces import ISenaiteFormLayer |
||
| 31 | from senaite.core.schema.addressfield import BILLING_ADDRESS |
||
| 32 | from senaite.core.schema.addressfield import BUSINESS_ADDRESS |
||
| 33 | from senaite.core.schema.addressfield import NAIVE_ADDRESS |
||
| 34 | from senaite.core.schema.addressfield import OTHER_ADDRESS |
||
| 35 | from senaite.core.schema.addressfield import PHYSICAL_ADDRESS |
||
| 36 | from senaite.core.schema.addressfield import POSTAL_ADDRESS |
||
| 37 | from senaite.core.schema.interfaces import IAddressField |
||
| 38 | from senaite.core.z3cform.interfaces import IAddressWidget |
||
| 39 | from z3c.form.browser.widget import HTMLFormElement |
||
| 40 | from z3c.form.converter import BaseDataConverter |
||
| 41 | from z3c.form.interfaces import IFieldWidget |
||
| 42 | from z3c.form.interfaces import IWidget |
||
| 43 | from z3c.form.interfaces import NO_VALUE |
||
| 44 | from z3c.form.widget import FieldWidget |
||
| 45 | from z3c.form.widget import Widget |
||
| 46 | from zope.component import adapter |
||
| 47 | from zope.interface import implementer |
||
| 48 | |||
| 49 | |||
| 50 | @adapter(IAddressField, IWidget) |
||
| 51 | class AddressDataConverter(BaseDataConverter): |
||
| 52 | """Value conversion between field and widget |
||
| 53 | """ |
||
| 54 | def to_list_of_dicts(self, value): |
||
| 55 | if not isinstance(value, list): |
||
| 56 | value = [value] |
||
| 57 | value = filter(None, value) |
||
| 58 | return map(self.to_dict, value) |
||
| 59 | |||
| 60 | def to_dict(self, value): |
||
| 61 | if not isinstance(value, dict): |
||
| 62 | return {} |
||
| 63 | return value |
||
| 64 | |||
| 65 | def toWidgetValue(self, value): |
||
| 66 | """Returns the field value with encoded string |
||
| 67 | """ |
||
| 68 | values = self.to_list_of_dicts(value) |
||
| 69 | return self.to_utf8(values) |
||
| 70 | |||
| 71 | def toFieldValue(self, value): |
||
| 72 | """Converts from widget value to safe_unicode |
||
| 73 | """ |
||
| 74 | values = self.to_list_of_dicts(value) |
||
| 75 | return self.to_safe_unicode(values) |
||
| 76 | |||
| 77 | def to_safe_unicode(self, data): |
||
| 78 | """Converts the data to unicode |
||
| 79 | """ |
||
| 80 | if isinstance(data, unicode): |
||
|
|
|||
| 81 | return data |
||
| 82 | if isinstance(data, list): |
||
| 83 | return [self.to_safe_unicode(item) for item in data] |
||
| 84 | if isinstance(data, dict): |
||
| 85 | return { |
||
| 86 | self.to_safe_unicode(key): self.to_safe_unicode(value) |
||
| 87 | for key, value in six.iteritems(data) |
||
| 88 | } |
||
| 89 | return safe_unicode(data) |
||
| 90 | |||
| 91 | View Code Duplication | def to_utf8(self, data): |
|
| 92 | """Encodes the data to utf-8 |
||
| 93 | """ |
||
| 94 | if isinstance(data, unicode): |
||
| 95 | return data.encode("utf-8") |
||
| 96 | if isinstance(data, list): |
||
| 97 | return [self.to_utf8(item) for item in data] |
||
| 98 | if isinstance(data, dict): |
||
| 99 | return { |
||
| 100 | self.to_utf8(key): self.to_utf8(value) |
||
| 101 | for key, value in six.iteritems(data) |
||
| 102 | } |
||
| 103 | return data |
||
| 104 | |||
| 105 | |||
| 106 | @implementer(IAddressWidget) |
||
| 107 | class AddressWidget(HTMLFormElement, Widget): |
||
| 108 | """SENAITE Address Widget |
||
| 109 | """ |
||
| 110 | klass = u"senaite-address-widget" |
||
| 111 | |||
| 112 | # Address html format for display. Wildcards accepted: {type}, |
||
| 113 | # {address_type}, {zip}, {city}, {district}, {subdivision1}, {subdivision2}, |
||
| 114 | # {country}. Use '<br/>' for newlines |
||
| 115 | address_format = "" |
||
| 116 | |||
| 117 | # Mapping between address_types and their names for i18n |
||
| 118 | address_types = { |
||
| 119 | NAIVE_ADDRESS: _("Address"), |
||
| 120 | PHYSICAL_ADDRESS: _("Physical address"), |
||
| 121 | POSTAL_ADDRESS: _("Postal address"), |
||
| 122 | BILLING_ADDRESS: _("Billing address"), |
||
| 123 | BUSINESS_ADDRESS: _("Business address"), |
||
| 124 | OTHER_ADDRESS: _("Other address"), |
||
| 125 | } |
||
| 126 | |||
| 127 | def get_address_format(self): |
||
| 128 | """Returns the format for displaying the address |
||
| 129 | """ |
||
| 130 | if self.address_format: |
||
| 131 | return self.address_format |
||
| 132 | |||
| 133 | lines = [ |
||
| 134 | "<strong>{address_type}</strong>", |
||
| 135 | "{address}", |
||
| 136 | "{zip} {city}", |
||
| 137 | "{subdivision2}, {subdivision1}", |
||
| 138 | "{country}" |
||
| 139 | ] |
||
| 140 | |||
| 141 | # Skip the type if multi-address |
||
| 142 | if not self.field.is_multi_address(): |
||
| 143 | lines = lines[1:] |
||
| 144 | |||
| 145 | return "<br/>".join(lines) |
||
| 146 | |||
| 147 | def get_formatted_address(self, data): |
||
| 148 | """Returns the address formatted in html |
||
| 149 | """ |
||
| 150 | address = { |
||
| 151 | "address_type": "", |
||
| 152 | "address": "", |
||
| 153 | "zip": "", |
||
| 154 | "city": "", |
||
| 155 | "country": "", |
||
| 156 | "subdivision1": "", |
||
| 157 | "subdivision2": "", |
||
| 158 | } |
||
| 159 | address.update(data) |
||
| 160 | |||
| 161 | address_format = self.get_address_format() |
||
| 162 | |||
| 163 | # Inject the name of the type of address translated |
||
| 164 | address_type = address.get("type") |
||
| 165 | address_type_name = self.address_types.get(address_type, address_type) |
||
| 166 | address.update({ |
||
| 167 | "address_type": address.get("address_type", address_type_name) |
||
| 168 | }) |
||
| 169 | |||
| 170 | lines = address_format.split("<br/>") |
||
| 171 | values = map(lambda line: line.format(**address), lines) |
||
| 172 | # Some extra cleanup |
||
| 173 | values = map(lambda line: line.strip(",- "), values) |
||
| 174 | values = filter(lambda line: line != "()", values) |
||
| 175 | values = filter(None, values) |
||
| 176 | return "<br/>".join(values) |
||
| 177 | |||
| 178 | def extract(self, default=NO_VALUE): |
||
| 179 | """Extracts the value based on the request and nothing else |
||
| 180 | """ |
||
| 181 | mapping = {} |
||
| 182 | prefix = "{}.".format(self.name) |
||
| 183 | keys = filter(lambda key: key.startswith(prefix), self.request.keys()) |
||
| 184 | for key in keys: |
||
| 185 | val = key.replace(prefix, "") |
||
| 186 | idx, subfield_name = val.split(".") |
||
| 187 | if not api.is_floatable(idx): |
||
| 188 | continue |
||
| 189 | |||
| 190 | empty_record = self.field.get_empty_address("") |
||
| 191 | mapping.setdefault(api.to_int(idx), empty_record).update({ |
||
| 192 | subfield_name: self.request.get(key) |
||
| 193 | }) |
||
| 194 | |||
| 195 | # Return the list of items sorted correctly |
||
| 196 | records = [] |
||
| 197 | for index in sorted(mapping.keys()): |
||
| 198 | record = mapping.get(index) |
||
| 199 | records.append(record) |
||
| 200 | |||
| 201 | return records or default |
||
| 202 | |||
| 203 | def get_value(self): |
||
| 204 | values = self.value or [] |
||
| 205 | existing = dict([(a.get("type"), a) for a in values]) |
||
| 206 | |||
| 207 | # HACK - This is necessary for when adding a new object, cause current |
||
| 208 | # context is the container instead of an object with the desired type |
||
| 209 | output = [] |
||
| 210 | for address_type in self.field.get_address_types(): |
||
| 211 | default = self.field.get_empty_address(address_type) |
||
| 212 | output.append(existing.get(address_type, default)) |
||
| 213 | return output |
||
| 214 | |||
| 215 | def get_input_widget_attributes(self): |
||
| 216 | """Return input widget attributes for the ReactJS component |
||
| 217 | """ |
||
| 218 | translate = self.context.translate |
||
| 219 | countries = map(lambda c: c.name, geo.get_countries()) |
||
| 220 | labels = {country: {} for country in countries} |
||
| 221 | labels.update({ |
||
| 222 | "country": translate(_("Country")), |
||
| 223 | "subdivision1": translate(_("State")), |
||
| 224 | "subdivision2": translate(_("District")), |
||
| 225 | "city": translate(_("City")), |
||
| 226 | "zip": translate(_("Postal code")), |
||
| 227 | "address": translate(_("Address")) |
||
| 228 | }) |
||
| 229 | sub1 = {} |
||
| 230 | sub2 = {} |
||
| 231 | |||
| 232 | for item in self.get_value(): |
||
| 233 | country = item.get("country") |
||
| 234 | if country and country not in sub1: |
||
| 235 | subdivisions = geo.get_subdivisions(country, []) |
||
| 236 | sub1[country] = map(lambda sub: sub.name, subdivisions) |
||
| 237 | |||
| 238 | label = _("State") |
||
| 239 | if subdivisions: |
||
| 240 | label = _(subdivisions[0].type) |
||
| 241 | labels[country]["subdivision1"] = translate(label) |
||
| 242 | |||
| 243 | subdivision1 = item.get("subdivision1") |
||
| 244 | if subdivision1 and subdivision1 not in sub2: |
||
| 245 | subdivisions = geo.get_subdivisions(subdivision1, []) |
||
| 246 | sub2[subdivision1] = map(lambda sub: sub.name, subdivisions) |
||
| 247 | |||
| 248 | label = _("District") |
||
| 249 | if subdivisions: |
||
| 250 | label = _(subdivisions[0].type) |
||
| 251 | labels[country]["subdivision2"] = translate(label) |
||
| 252 | |||
| 253 | attributes = { |
||
| 254 | "data-id": self.id, |
||
| 255 | "data-countries": countries, |
||
| 256 | "data-subdivisions1": sub1, |
||
| 257 | "data-subdivisions2": sub2, |
||
| 258 | "data-name": self.name, |
||
| 259 | "data-portal_url": api.get_url(api.get_portal()), |
||
| 260 | "data-items": self.get_value(), |
||
| 261 | "data-title": self.title, |
||
| 262 | "data-class": self.klass, |
||
| 263 | "data-style": self.style, |
||
| 264 | "data-disabled": self.disabled or False, |
||
| 265 | "data-labels": labels |
||
| 266 | } |
||
| 267 | |||
| 268 | # Generate the i18n labels for address types |
||
| 269 | for a_type, name in self.address_types.items(): |
||
| 270 | attributes["data-labels"].update({ |
||
| 271 | a_type: translate(name) |
||
| 272 | }) |
||
| 273 | |||
| 274 | # convert all attributes to JSON |
||
| 275 | for key, value in attributes.items(): |
||
| 276 | attributes[key] = json.dumps(value) |
||
| 277 | |||
| 278 | return attributes |
||
| 279 | |||
| 280 | |||
| 281 | @adapter(IAddressField, ISenaiteFormLayer) |
||
| 282 | @implementer(IFieldWidget) |
||
| 283 | def AddressWidgetFactory(field, request): |
||
| 284 | """Widget factory for Address Widget |
||
| 285 | """ |
||
| 286 | return FieldWidget(field, AddressWidget(request)) |
||
| 287 | |||
| 288 | |||
| 289 | class AjaxSubdivisions(BrowserView): |
||
| 290 | """Endpoint for the retrieval of geographic subdivisions |
||
| 291 | """ |
||
| 292 | |||
| 293 | @readonly_transaction |
||
| 294 | @returns_json |
||
| 295 | def __call__(self): |
||
| 296 | """Returns a json with the list of geographic subdivisions that are |
||
| 297 | immediately below the country or subdivision passed in with `parent` |
||
| 298 | parameter via POST/GET |
||
| 299 | """ |
||
| 300 | items = [] |
||
| 301 | parent = self.get_parent() |
||
| 302 | if not parent: |
||
| 303 | return json.dumps(items) |
||
| 304 | |||
| 305 | # Extract the subdivisions for this parent |
||
| 306 | parent = safe_unicode(parent) |
||
| 307 | items = geo.get_subdivisions(parent, default=[]) |
||
| 308 | |||
| 309 | def to_dict(subdivision): |
||
| 310 | return { |
||
| 311 | "name": subdivision.name, |
||
| 312 | "code": subdivision.code, |
||
| 313 | "type": self.context.translate(_(subdivision.type)), |
||
| 314 | } |
||
| 315 | return [to_dict(item) for item in items] |
||
| 316 | |||
| 317 | def get_parent(self): |
||
| 318 | """Returns the parent passed through the request |
||
| 319 | """ |
||
| 320 | parent = self.request.get("parent", None) |
||
| 321 | if not parent: |
||
| 322 | values = json.loads(self.request.get("BODY", "{}")) |
||
| 323 | parent = values.get("parent", None) |
||
| 324 | return parent |
||
| 325 |