from django.contrib.gis.db import models as gis_models from django.db import models from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.core.validators import MinValueValidator, MaxValueValidator from simple_history.models import HistoricalRecords from django.contrib.gis.geos import Point class Location(models.Model): """ A generic location model that can be associated with any model using GenericForeignKey. Stores detailed location information including coordinates and address components. """ # Generic relation fields content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() content_object = GenericForeignKey('content_type', 'object_id') # Location name and type name = models.CharField(max_length=255, help_text="Name of the location (e.g. business name, landmark)") location_type = models.CharField(max_length=50, help_text="Type of location (e.g. business, landmark, address)") # Geographic coordinates latitude = models.DecimalField( max_digits=9, decimal_places=6, validators=[ MinValueValidator(-90), MaxValueValidator(90) ], help_text="Latitude coordinate (legacy field)", null=True, blank=True ) longitude = models.DecimalField( max_digits=9, decimal_places=6, validators=[ MinValueValidator(-180), MaxValueValidator(180) ], help_text="Longitude coordinate (legacy field)", null=True, blank=True ) # GeoDjango point field point = gis_models.PointField( srid=4326, # WGS84 coordinate system null=True, blank=True, help_text="Geographic coordinates as a Point" ) # Address components street_address = models.CharField(max_length=255, blank=True, null=True) city = models.CharField(max_length=100, blank=True, null=True) state = models.CharField(max_length=100, blank=True, null=True, help_text="State/Region/Province") country = models.CharField(max_length=100, blank=True, null=True) postal_code = models.CharField(max_length=20, blank=True, null=True) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) history = HistoricalRecords() class Meta: indexes = [ models.Index(fields=['content_type', 'object_id']), models.Index(fields=['city']), models.Index(fields=['country']), ] ordering = ['name'] def __str__(self): location_parts = [] if self.city: location_parts.append(self.city) if self.country: location_parts.append(self.country) location_str = ", ".join(location_parts) if location_parts else "Unknown location" return f"{self.name} ({location_str})" def save(self, *args, **kwargs): # Sync point field with lat/lon fields for backward compatibility if self.latitude is not None and self.longitude is not None and not self.point: self.point = Point(float(self.longitude), float(self.latitude)) elif self.point and (self.latitude is None or self.longitude is None): self.longitude = self.point.x self.latitude = self.point.y super().save(*args, **kwargs) def get_formatted_address(self): """Returns a formatted address string""" components = [] if self.street_address: components.append(self.street_address) if self.city: components.append(self.city) if self.state: components.append(self.state) if self.postal_code: components.append(self.postal_code) if self.country: components.append(self.country) return ", ".join(components) if components else "" @property def coordinates(self): """Returns coordinates as a tuple""" if self.point: return (self.point.y, self.point.x) # Returns (latitude, longitude) elif self.latitude is not None and self.longitude is not None: return (float(self.latitude), float(self.longitude)) return None def distance_to(self, other_location): """ Calculate the distance to another location in meters. Returns None if either location is missing coordinates. """ if not self.point or not other_location.point: return None return self.point.distance(other_location.point) * 100000 # Convert to meters def nearby_locations(self, distance_km=10): """ Find locations within specified distance in kilometers. Returns a queryset of nearby Location objects. """ if not self.point: return Location.objects.none() return Location.objects.filter( point__distance_lte=(self.point, distance_km * 1000) # Convert km to meters ).exclude(pk=self.pk)