Skip to content

Commit 4657130

Browse files
authored
Merge pull request #52 from pennlabs/anthony/backend-geocoords
Add coords to sublet model
2 parents d24de67 + 1c1e0a6 commit 4657130

3 files changed

Lines changed: 90 additions & 4 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 5.0.2 on 2026-02-13 23:08
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("market", "0004_rename_address_sublet_street_address_and_more"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="sublet",
15+
name="latitude",
16+
field=models.FloatField(blank=True, null=True),
17+
),
18+
migrations.AddField(
19+
model_name="sublet",
20+
name="longitude",
21+
field=models.FloatField(blank=True, null=True),
22+
),
23+
]

backend/market/models.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import hashlib
2+
import math
3+
14
from django.contrib.auth.models import AbstractUser
25
from django.core.exceptions import ValidationError
36
from django.core.validators import MinValueValidator
@@ -42,7 +45,6 @@ class Meta:
4245
def __str__(self):
4346
return f"Offer for {self.listing} made by {self.user}"
4447

45-
4648
class Category(models.Model):
4749
name = models.CharField(max_length=100, unique=True)
4850

@@ -130,12 +132,42 @@ class Sublet(Listing):
130132
baths = models.PositiveIntegerField()
131133
start_date = models.DateField()
132134
end_date = models.DateField()
135+
latitude = models.FloatField(null=True, blank=True)
136+
longitude = models.FloatField(null=True, blank=True)
133137

134138
def clean(self):
135139
super().clean()
136140
if self.start_date and self.end_date and self.start_date >= self.end_date:
137141
raise ValidationError({"end_date": "End date must be after start date"})
138142

143+
def _calculate_approximate_location(self, latitude, longitude):
144+
if latitude is None or longitude is None:
145+
return None, None
146+
147+
lat_str = f"{float(latitude):.9f}"
148+
lon_str = f"{float(longitude):.9f}"
149+
seed = hashlib.md5(f"{lat_str}{lon_str}".encode()).hexdigest()
150+
151+
offset_factor = int(seed[:8], 16) / 0xFFFFFFFF
152+
153+
offset_distance = 0.0005 + (offset_factor * 0.0013)
154+
angle = offset_factor * 2 * math.pi
155+
156+
lat_offset = offset_distance * math.sin(angle)
157+
lon_offset = offset_distance * math.cos(angle)
158+
159+
approx_lat = float(latitude) + lat_offset
160+
approx_lon = float(longitude) + lon_offset
161+
return approx_lat, approx_lon
162+
163+
@property
164+
def approximate_location(self):
165+
if self.latitude is not None and self.longitude is not None:
166+
approximate_location = self._calculate_approximate_location(
167+
self.latitude, self.longitude)
168+
return approximate_location
169+
return None, None
170+
139171
def save(self, *args, **kwargs):
140172
self.full_clean()
141173
super().save(*args, **kwargs)

backend/market/serializers.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
from django.contrib.auth import get_user_model
23
from django.core.exceptions import ValidationError as ModelValidationError
34
from profanity_check import predict
@@ -98,10 +99,23 @@ def get_condition(self, obj):
9899

99100

100101
class SubletDataSerializer(ModelSerializer):
102+
latitude = SerializerMethodField()
103+
longitude = SerializerMethodField()
104+
101105
class Meta:
102106
model = Sublet
103-
fields = ["street_address", "beds", "baths", "start_date", "end_date"]
107+
fields = ["street_address", "beds", "baths", "start_date", "end_date",
108+
"latitude", "longitude"]
104109

110+
def get_latitude(self, obj):
111+
if obj.approximate_location is not None:
112+
return float(obj.approximate_location[0])
113+
return None
114+
115+
def get_longitude(self, obj):
116+
if obj.approximate_location is not None:
117+
return float(obj.approximate_location[1])
118+
return None
105119

106120
# Unified serializer for all listing types (Items and Sublets); used for CRUD operations
107121
class ListingSerializer(ListingTypeMixin, ModelSerializer):
@@ -265,12 +279,23 @@ def _create_item(self, validated_data, additional_data):
265279
def _create_sublet(self, validated_data, additional_data):
266280
tags = validated_data.pop("tags", None)
267281

282+
latitude = additional_data.get("latitude")
283+
longitude = additional_data.get("longitude")
284+
285+
286+
if latitude is not None:
287+
latitude = float(latitude)
288+
if longitude is not None:
289+
longitude = float(longitude)
290+
268291
sublet = Sublet.objects.create(
269292
street_address=additional_data.get("street_address"),
270293
beds=additional_data.get("beds"),
271294
baths=additional_data.get("baths"),
272295
start_date=additional_data.get("start_date"),
273296
end_date=additional_data.get("end_date"),
297+
latitude=latitude,
298+
longitude=longitude,
274299
**validated_data,
275300
)
276301

@@ -323,10 +348,16 @@ def _update_item(self, instance, additional_data):
323348

324349
def _update_sublet(self, instance, additional_data):
325350
sublet = instance.sublet
326-
sublet_fields = ["street_address", "beds", "baths", "start_date", "end_date"]
327-
for field in sublet_fields:
351+
str_fields = ["street_address", "beds", "baths", "start_date", "end_date"]
352+
float_fields = ["latitude", "longitude"]
353+
for field in str_fields:
328354
if field in additional_data:
329355
setattr(sublet, field, additional_data[field])
356+
357+
for field in float_fields:
358+
if field in additional_data:
359+
value = additional_data[field]
360+
setattr(sublet, field, float(value) if value is not None else None)
330361
sublet.full_clean()
331362
sublet.save()
332363

0 commit comments

Comments
 (0)