Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/address/models.py: 67%
155 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1import logging
3from django.core.exceptions import ValidationError
4from django.db import models
6try:
7 from django.db.models.fields.related_descriptors import ForwardManyToOneDescriptor
8except ImportError:
9 from django.db.models.fields.related import (
10 ReverseSingleRelatedObjectDescriptor as ForwardManyToOneDescriptor,
11 )
13logger = logging.getLogger(__name__)
15__all__ = ["Country", "State", "Locality", "Address", "AddressField"]
18class InconsistentDictError(Exception):
19 pass
22def _to_python(value):
23 raw = value.get("raw", "")
24 country = value.get("country", "")
25 country_code = value.get("country_code", "")
26 state = value.get("state", "")
27 state_code = value.get("state_code", "")
28 locality = value.get("locality", "")
29 sublocality = value.get("sublocality", "")
30 postal_town = value.get("postal_town", "")
31 postal_code = value.get("postal_code", "")
32 street_number = value.get("street_number", "")
33 route = value.get("route", "")
34 formatted = value.get("formatted", "")
35 latitude = value.get("latitude", None)
36 longitude = value.get("longitude", None)
38 # If there is no value (empty raw) then return None.
39 if not raw: 39 ↛ 40line 39 didn't jump to line 40, because the condition on line 39 was never true
40 return None
42 # Fix issue with NYC boroughs (https://code.google.com/p/gmaps-api-issues/issues/detail?id=635)
43 if not locality and sublocality: 43 ↛ 44line 43 didn't jump to line 44, because the condition on line 43 was never true
44 locality = sublocality
46 # Fix issue with UK addresses with no locality
47 # (https://github.com/furious-luke/django-address/issues/114)
48 if not locality and postal_town: 48 ↛ 49line 48 didn't jump to line 49, because the condition on line 48 was never true
49 locality = postal_town
51 # If we have an inconsistent set of value bail out now.
52 if (country or state or locality) and not (country and state and locality): 52 ↛ 53line 52 didn't jump to line 53, because the condition on line 52 was never true
53 raise InconsistentDictError
55 # Handle the country.
56 try:
57 country_obj = Country.objects.get(name=country)
58 except Country.DoesNotExist:
59 if country: 59 ↛ 66line 59 didn't jump to line 66, because the condition on line 59 was never false
60 if len(country_code) > Country._meta.get_field("code").max_length: 60 ↛ 61line 60 didn't jump to line 61, because the condition on line 60 was never true
61 if country_code != country:
62 raise ValueError("Invalid country code (too long): %s" % country_code)
63 country_code = ""
64 country_obj = Country.objects.create(name=country, code=country_code)
65 else:
66 country_obj = None
68 # Handle the state.
69 try:
70 state_obj = State.objects.get(name=state, country=country_obj)
71 except State.DoesNotExist:
72 if state: 72 ↛ 79line 72 didn't jump to line 79, because the condition on line 72 was never false
73 if len(state_code) > State._meta.get_field("code").max_length: 73 ↛ 74line 73 didn't jump to line 74, because the condition on line 73 was never true
74 if state_code != state:
75 raise ValueError("Invalid state code (too long): %s" % state_code)
76 state_code = ""
77 state_obj = State.objects.create(name=state, code=state_code, country=country_obj)
78 else:
79 state_obj = None
81 # Handle the locality.
82 try:
83 locality_obj = Locality.objects.get(name=locality, postal_code=postal_code, state=state_obj)
84 except Locality.DoesNotExist:
85 if locality: 85 ↛ 88line 85 didn't jump to line 88, because the condition on line 85 was never false
86 locality_obj = Locality.objects.create(name=locality, postal_code=postal_code, state=state_obj)
87 else:
88 locality_obj = None
90 # Handle the address.
91 try:
92 if not (street_number or route or locality): 92 ↛ 93line 92 didn't jump to line 93, because the condition on line 92 was never true
93 address_obj = Address.objects.get(raw=raw)
94 else:
95 address_obj = Address.objects.get(street_number=street_number, route=route, locality=locality_obj)
96 except Address.DoesNotExist:
97 address_obj = Address(
98 street_number=street_number,
99 route=route,
100 raw=raw,
101 locality=locality_obj,
102 formatted=formatted,
103 latitude=latitude,
104 longitude=longitude,
105 )
107 # If "formatted" is empty try to construct it from other values.
108 if not address_obj.formatted: 108 ↛ 112line 108 didn't jump to line 112, because the condition on line 108 was never false
109 address_obj.formatted = str(address_obj)
111 # Need to save.
112 address_obj.save()
114 # Done.
115 return address_obj
118##
119# Convert a dictionary to an address.
120##
123def to_python(value):
125 # Keep `None`s.
126 if value is None: 126 ↛ 127line 126 didn't jump to line 127, because the condition on line 126 was never true
127 return None
129 # Is it already an address object?
130 if isinstance(value, Address): 130 ↛ 131line 130 didn't jump to line 131, because the condition on line 130 was never true
131 return value
133 # If we have an integer, assume it is a model primary key.
134 elif isinstance(value, int): 134 ↛ 135line 134 didn't jump to line 135, because the condition on line 134 was never true
135 return value
137 # A string is considered a raw value.
138 elif isinstance(value, str): 138 ↛ 139line 138 didn't jump to line 139, because the condition on line 138 was never true
139 obj = Address(raw=value)
140 obj.save()
141 return obj
143 # A dictionary of named address components.
144 elif isinstance(value, dict): 144 ↛ 153line 144 didn't jump to line 153, because the condition on line 144 was never false
146 # Attempt a conversion.
147 try:
148 return _to_python(value)
149 except InconsistentDictError:
150 return Address.objects.create(raw=value["raw"])
152 # Not in any of the formats I recognise.
153 raise ValidationError("Invalid address value.")
156##
157# A country.
158##
161class Country(models.Model):
162 name = models.CharField(max_length=40, unique=True, blank=True)
163 code = models.CharField(max_length=2, blank=True) # not unique as there are duplicates (IT)
165 class Meta:
166 verbose_name_plural = "Countries"
167 ordering = ("name",)
169 def __str__(self):
170 return "%s" % (self.name or self.code)
173##
174# A state. Google refers to this as `administration_level_1`.
175##
178class State(models.Model):
179 name = models.CharField(max_length=165, blank=True)
180 code = models.CharField(max_length=8, blank=True)
181 country = models.ForeignKey(Country, on_delete=models.CASCADE, related_name="states")
183 class Meta:
184 unique_together = ("name", "country")
185 ordering = ("country", "name")
187 def __str__(self):
188 txt = self.to_str()
189 country = "%s" % self.country
190 if country and txt:
191 txt += ", "
192 txt += country
193 return txt
195 def to_str(self):
196 return "%s" % (self.name or self.code)
199##
200# A locality (suburb).
201##
204class Locality(models.Model):
205 name = models.CharField(max_length=165, blank=True)
206 postal_code = models.CharField(max_length=10, blank=True)
207 state = models.ForeignKey(State, on_delete=models.CASCADE, related_name="localities")
209 class Meta:
210 verbose_name_plural = "Localities"
211 unique_together = ("name", "postal_code", "state")
212 ordering = ("state", "name")
214 def __str__(self):
215 txt = "%s" % self.name
216 state = self.state.to_str() if self.state else ""
217 if txt and state:
218 txt += ", "
219 txt += state
220 if self.postal_code:
221 txt += " %s" % self.postal_code
222 cntry = "%s" % (self.state.country if self.state and self.state.country else "")
223 if cntry:
224 txt += ", %s" % cntry
225 return txt
228##
229# An address. If for any reason we are unable to find a matching
230# decomposed address we will store the raw address string in `raw`.
231##
234class Address(models.Model):
235 street_number = models.CharField(max_length=20, blank=True)
236 route = models.CharField(max_length=100, blank=True)
237 locality = models.ForeignKey(
238 Locality,
239 on_delete=models.CASCADE,
240 related_name="addresses",
241 blank=True,
242 null=True,
243 )
244 raw = models.CharField(max_length=200)
245 formatted = models.CharField(max_length=200, blank=True)
246 latitude = models.FloatField(blank=True, null=True)
247 longitude = models.FloatField(blank=True, null=True)
249 class Meta:
250 verbose_name_plural = "Addresses"
251 ordering = ("locality", "route", "street_number")
253 def __str__(self):
254 if self.formatted != "":
255 txt = "%s" % self.formatted
256 elif self.locality:
257 txt = ""
258 if self.street_number:
259 txt = "%s" % self.street_number
260 if self.route:
261 if txt:
262 txt += " %s" % self.route
263 locality = "%s" % self.locality
264 if txt and locality:
265 txt += ", "
266 txt += locality
267 else:
268 txt = "%s" % self.raw
269 return txt
271 def clean(self):
272 if not self.raw:
273 raise ValidationError("Addresses may not have a blank `raw` field.")
275 def as_dict(self):
276 ad = dict(
277 street_number=self.street_number,
278 route=self.route,
279 raw=self.raw,
280 formatted=self.formatted,
281 latitude=self.latitude if self.latitude else "",
282 longitude=self.longitude if self.longitude else "",
283 )
284 if self.locality:
285 ad["locality"] = self.locality.name
286 ad["postal_code"] = self.locality.postal_code
287 if self.locality.state:
288 ad["state"] = self.locality.state.name
289 ad["state_code"] = self.locality.state.code
290 if self.locality.state.country:
291 ad["country"] = self.locality.state.country.name
292 ad["country_code"] = self.locality.state.country.code
293 return ad
296class AddressDescriptor(ForwardManyToOneDescriptor):
297 def __set__(self, inst, value):
298 super(AddressDescriptor, self).__set__(inst, to_python(value))
301##
302# A field for addresses in other models.
303##
306class AddressField(models.ForeignKey):
307 description = "An address"
309 def __init__(self, *args, **kwargs):
310 kwargs["to"] = "address.Address"
311 # The address should be set to null when deleted if the relationship could be null
312 default_on_delete = models.SET_NULL if kwargs.get("null", False) else models.CASCADE
313 kwargs["on_delete"] = kwargs.get("on_delete", default_on_delete)
314 super(AddressField, self).__init__(*args, **kwargs)
316 def contribute_to_class(self, cls, name, virtual_only=False):
317 from address.compat import compat_contribute_to_class
319 compat_contribute_to_class(self, cls, name, virtual_only)
321 setattr(cls, self.name, AddressDescriptor(self))
323 def formfield(self, **kwargs):
324 from .forms import AddressField as AddressFormField
326 defaults = dict(form_class=AddressFormField)
327 defaults.update(kwargs)
328 return super(AddressField, self).formfield(**defaults)