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

senaite.core.z3cform.widgets.address   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 222
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 33
eloc 114
dl 0
loc 222
rs 9.76
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A AddressWidget.get_address_format() 0 13 2
A AddressWidget.get_geographical_hierarchy() 0 41 3
A AddressWidget.get_state_code() 0 10 4
A AddressWidget.get_countries_names() 0 6 2
A AddressWidget.get_districts() 0 12 4
A AddressWidget.get_country_iso() 0 9 4
A AddressWidget.get_formatted_address() 0 8 2
A AddressDataConverter.toFieldValue() 0 4 1
A AddressDataConverter.to_dict() 0 4 2
A AddressWidget.get_input_widget_attributes() 0 21 2
A AddressDataConverter.toWidgetValue() 0 4 1
A AddressWidget.get_states() 0 9 3
A AddressDataConverter.to_list_of_dicts() 0 5 2

1 Function

Rating   Name   Duplication   Size   Complexity  
A AddressWidgetFactory() 0 6 1
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
from bika.lims import logger
23
from senaite.core.locales import DISTRICTS
24
from senaite.core.locales import STATES
25
26
from bika.lims import api
27
from senaite.core.locales import COUNTRIES
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 FieldDataConverter
33
from z3c.form.interfaces import IFieldWidget
34
from z3c.form.interfaces import IWidget
35
from z3c.form.widget import FieldWidget
36
from z3c.form.widget import Widget
37
from zope.component import adapter
38
from zope.interface import implementer
39
40
41
@adapter(IAddressField, IWidget)
42
class AddressDataConverter(FieldDataConverter):
43
    """Value conversion between field and widget
44
    """
45
    def to_list_of_dicts(self, value):
46
        if not isinstance(value, list):
47
            value = [value]
48
        value = filter(None, value)
49
        return map(self.to_dict, value)
50
51
    def to_dict(self, value):
52
        if not isinstance(value, dict):
53
            return {}
54
        return value
55
56
    def toWidgetValue(self, value):
57
        """Returns the field value with encoded string
58
        """
59
        return self.to_list_of_dicts(value)
60
61
    def toFieldValue(self, value):
62
        """Converts from widget value to safe_unicode
63
        """
64
        return self.to_list_of_dicts(value)
65
66
67
@implementer(IAddressWidget)
68
class AddressWidget(HTMLFormElement, Widget):
69
    """SENAITE Address Widget
70
    """
71
    klass = u"senaite-address-widget"
72
73
    # Address html format for display. Wildcards accepted: {type}, {address},
74
    # {zip}, {district}, {city}, {state}, {country}. Use '<br/>' for newlines
75
    address_format = ""
76
77
    def get_address_format(self):
78
        """Returns the format for displaying the address
79
        """
80
        if self.address_format:
81
            return self.address_format
82
        lines = [
83
            "<strong>{type}</strong>",
84
            "{address}",
85
            "{zip} {district} {city}",
86
            "{state}",
87
            "{country}"
88
        ]
89
        return "<br/>".join(lines)
90
91
    def get_formatted_address(self, address):
92
        """Returns the address formatted in html
93
        """
94
        address_format = self.get_address_format()
95
        lines = address_format.split("<br/>")
96
        values = map(lambda line: line.format(**address), lines)
97
        values = filter(None, values)
98
        return "<br/>".join(values)
99
100
    def get_countries_names(self):
101
        """Returns the list of names of available countries
102
        """
103
        countries = map(lambda country: country.get("Country"), COUNTRIES)
104
        countries = sorted(filter(None, countries))
105
        return countries
106
107
    def get_country_iso(self, name_or_iso):
108
        """Returns the iso for the country with the given name or iso
109
        """
110
        for country in COUNTRIES:
111
            if country.get("Country") == name_or_iso:
112
                return country.get("ISO")
113
            elif country.get("ISO") == name_or_iso:
114
                return name_or_iso
115
        return None
116
117
    def get_state_code(self, country, state):
118
        """Returns the code of the state from the country with given name or iso
119
        """
120
        iso = self.get_country_iso(country)
121
        for state_info in STATES:
122
            if iso != state_info[0]:
123
                continue
124
            if state in state_info:
125
                return state_info[1]
126
        return None
127
128
    def get_geographical_hierarchy(self):
129
        """Returns a dict with geographical information as follows:
130
131
            {<country_name_1>: {
132
                <state_name_1>: [
133
                    <district_name_1>,
134
                    ...
135
                    <district_name_n>,
136
                ],
137
                ...
138
                <state_name_n>: [..]
139
                },
140
            <country_name_2>: {
141
                ...
142
            }
143
144
        Available states and districts for each country are only considered for
145
        those countries selected in current addresses
146
        """
147
148
        # Initialize a dict with countries names as keys and None as values
149
        countries = self.get_countries_names()
150
        countries = dict.fromkeys(countries)
151
152
        # Fill the dict with geo information for selected countries only
153
        for item in self.value:
154
            country = item.get("country")
155
156
            # Initialize a dict with states names as keys and None as values
157
            states = self.get_states(country)
158
            states = map(lambda st: st[1], states)
159
            states = dict.fromkeys(states)
160
161
            # Fill the dict with geo information for selected state only
162
            state = item.get("state")
163
            states[state] = self.get_districts(country, state)
164
165
            # Set the value to the geomap
166
            countries[country] = states
167
168
        return countries
169
170
    def get_states(self, country):
171
        """Returns the states for the country with the given name or iso
172
173
        :param country: name or iso of the country
174
        :return: a list of tuples as (<state_code>, <state_name>)
175
        """
176
        iso = self.get_country_iso(country)
177
        states = filter(lambda state: state[0] == iso, STATES)
178
        return map(lambda state: (state[1], state[2]), states)
179
180
    def get_districts(self, country, state):
181
        """Returns the districts for the country and state given
182
183
        :param country: name or iso of the country
184
        :param state: name or code of the state
185
        :return: a list of districts
186
        """
187
        iso = self.get_country_iso(country)
188
        state_code = self.get_state_code(iso, state)
189
        districts = filter(lambda dis: dis[0] == iso, DISTRICTS)
190
        districts = filter(lambda dis: dis[1] == state_code, districts)
191
        return map(lambda dis: dis[2], districts)
192
193
    def get_input_widget_attributes(self):
194
        """Return input widget attributes for the ReactJS component
195
        """
196
        attributes = {
197
            "data-id": self.id,
198
            "data-uid": api.get_uid(self.context),
199
            "data-geography": self.get_geographical_hierarchy(),
200
            "data-name": self.name,
201
            "data-portal_url": api.get_url(api.get_portal()),
202
            "data-items": self.value,
203
            "data-title": self.title,
204
            "data-class": self.klass,
205
            "data-style": self.style,
206
            "data-disabled": self.disabled or False,
207
        }
208
209
        # convert all attributes to JSON
210
        for key, value in attributes.items():
211
            attributes[key] = json.dumps(value)
212
213
        return attributes
214
215
216
@adapter(IAddressField, ISenaiteFormLayer)
217
@implementer(IFieldWidget)
218
def AddressWidgetFactory(field, request):
219
    """Widget factory for Address Widget
220
    """
221
    return FieldWidget(field, AddressWidget(request))
222