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

1import logging 

2 

3from django.core.exceptions import ValidationError 

4from django.db import models 

5 

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 ) 

12 

13logger = logging.getLogger(__name__) 

14 

15__all__ = ["Country", "State", "Locality", "Address", "AddressField"] 

16 

17 

18class InconsistentDictError(Exception): 

19 pass 

20 

21 

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) 

37 

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 

41 

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 

45 

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 

50 

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 

54 

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 

67 

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 

80 

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 

89 

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 ) 

106 

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) 

110 

111 # Need to save. 

112 address_obj.save() 

113 

114 # Done. 

115 return address_obj 

116 

117 

118## 

119# Convert a dictionary to an address. 

120## 

121 

122 

123def to_python(value): 

124 

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 

128 

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 

132 

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 

136 

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 

142 

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

145 

146 # Attempt a conversion. 

147 try: 

148 return _to_python(value) 

149 except InconsistentDictError: 

150 return Address.objects.create(raw=value["raw"]) 

151 

152 # Not in any of the formats I recognise. 

153 raise ValidationError("Invalid address value.") 

154 

155 

156## 

157# A country. 

158## 

159 

160 

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) 

164 

165 class Meta: 

166 verbose_name_plural = "Countries" 

167 ordering = ("name",) 

168 

169 def __str__(self): 

170 return "%s" % (self.name or self.code) 

171 

172 

173## 

174# A state. Google refers to this as `administration_level_1`. 

175## 

176 

177 

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

182 

183 class Meta: 

184 unique_together = ("name", "country") 

185 ordering = ("country", "name") 

186 

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 

194 

195 def to_str(self): 

196 return "%s" % (self.name or self.code) 

197 

198 

199## 

200# A locality (suburb). 

201## 

202 

203 

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

208 

209 class Meta: 

210 verbose_name_plural = "Localities" 

211 unique_together = ("name", "postal_code", "state") 

212 ordering = ("state", "name") 

213 

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 

226 

227 

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

232 

233 

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) 

248 

249 class Meta: 

250 verbose_name_plural = "Addresses" 

251 ordering = ("locality", "route", "street_number") 

252 

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 

270 

271 def clean(self): 

272 if not self.raw: 

273 raise ValidationError("Addresses may not have a blank `raw` field.") 

274 

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 

294 

295 

296class AddressDescriptor(ForwardManyToOneDescriptor): 

297 def __set__(self, inst, value): 

298 super(AddressDescriptor, self).__set__(inst, to_python(value)) 

299 

300 

301## 

302# A field for addresses in other models. 

303## 

304 

305 

306class AddressField(models.ForeignKey): 

307 description = "An address" 

308 

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) 

315 

316 def contribute_to_class(self, cls, name, virtual_only=False): 

317 from address.compat import compat_contribute_to_class 

318 

319 compat_contribute_to_class(self, cls, name, virtual_only) 

320 

321 setattr(cls, self.name, AddressDescriptor(self)) 

322 

323 def formfield(self, **kwargs): 

324 from .forms import AddressField as AddressFormField 

325 

326 defaults = dict(form_class=AddressFormField) 

327 defaults.update(kwargs) 

328 return super(AddressField, self).formfield(**defaults)