From 0bb8ae44e08475cc496ff9bade4f9b6bb7bc3638 Mon Sep 17 00:00:00 2001 From: Luisa Date: Fri, 24 Apr 2026 20:45:51 -0300 Subject: [PATCH 1/4] config: adding api key --- config/settings.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/settings.py b/config/settings.py index 355ae4f..0710c5b 100644 --- a/config/settings.py +++ b/config/settings.py @@ -128,4 +128,6 @@ # O redis vai enfileirar as tarefas e atribuir aos workers do celery. # O celery vai ter seus workers que vão ter suas funções já pré definidas nos arquivos tasks.py -# Quando uma tarefa é terminada, o redis vai armazenar seu resultado \ No newline at end of file +# Quando uma tarefa é terminada, o redis vai armazenar seu resultado + +GOOGLE_PLACES_API_KEY = config("GOOGLE_PLACES_API_KEY") \ No newline at end of file From a2a3b4c6b13603d9bc72f5724c0146bc888e8c72 Mon Sep 17 00:00:00 2001 From: Luisa Date: Fri, 24 Apr 2026 20:46:29 -0300 Subject: [PATCH 2/4] feat: implementing NearbyPlaces Model --- apps/properties/models.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/apps/properties/models.py b/apps/properties/models.py index a9dbd40..bd6bb6d 100644 --- a/apps/properties/models.py +++ b/apps/properties/models.py @@ -70,6 +70,8 @@ class Properties(models.Model): city = models.CharField(max_length=100, null=False) has_mobilia = models.BooleanField(default=False) status = models.BooleanField(default=True) + latitude = models.FloatField(null=True, blank=True) + longitude = models.FloatField(null=True, blank=True) description = models.TextField() embedding = models.TextField() created_at = models.DateTimeField(auto_now_add=True) @@ -107,3 +109,16 @@ class Meta: name="unique_review_per_user_per_property" ) ] + +class NearbyPlaces(models.Model): + CATEGORY_CHOICES = [("R", "Restaurant"), ("G", "Gym"), ("S", "School"), ("H", "Hospital"), ("SM", "Supermarket"), ("P", "Park")] + property = models.ForeignKey( + Properties, + on_delete=models.CASCADE, + related_name="nearby_places" + ) + name = models.TextField(null=False) + category = models.CharField(max_length=2, choices=CATEGORY_CHOICES) + distance_meters = models.FloatField() + rating = models.FloatField(null=True, blank=True) + fetched_at = models.DateTimeField(auto_now_add=True) From 0ddd211285d34a41e2c0a57469e88d316c931e58 Mon Sep 17 00:00:00 2001 From: Luisa Date: Fri, 24 Apr 2026 20:47:09 -0300 Subject: [PATCH 3/4] feat: implementing search nearby places method with api --- apps/properties/services.py | 142 +++++++++++++++++++++++++++++------- 1 file changed, 116 insertions(+), 26 deletions(-) diff --git a/apps/properties/services.py b/apps/properties/services.py index 84a0091..8ff6e30 100644 --- a/apps/properties/services.py +++ b/apps/properties/services.py @@ -1,37 +1,127 @@ import boto3 import uuid +import requests +import math from botocore.config import Config from django.conf import settings +from .models import NearbyPlaces -s3_client = boto3.client( - "s3", - endpoint_url=f"https://{settings.R2_ACCOUNT_ID}.r2.cloudflarestorage.com", - aws_access_key_id=settings.R2_ACCESS_KEY_ID, - aws_secret_access_key=settings.R2_SECRET_ACCESS_KEY, - config=Config(signature_version="s3v4") -) +class NomatimService: + BASE_URL = "https://nominatim.openstreetmap.org/search" -def upload_to_cloud(image): - r2_key = f"properties/{uuid.uuid4()}/{image.name}" - - s3_client.upload_fileobj( - image, - settings.R2_BUCKET_NAME, - r2_key, - ExtraArgs={"ContentType": image.content_type} + @staticmethod + def geocode(property_obj): + params = { + "q": f"{property_obj.address}, {property_obj.city}", + "format": "json", + "limit": 1 + } + headers = { + "User-Agent": "HomeMatch/1.0" + } + response = requests.get(NominatimService.BASE_URL, params=params, headers=headers) + data = response.json() + + if data: + property_obj.latitude = float(data[0]["lat"]) + property_obj.longitude = float(data[0]["lon"]) + property_obj.save(update_fields=["latitude", "longitude"]) + + +class CloudService: + s3_client = boto3.client( + "s3", + endpoint_url=f"https://{settings.R2_ACCOUNT_ID}.r2.cloudflarestorage.com", + aws_access_key_id=settings.R2_ACCESS_KEY_ID, + aws_secret_access_key=settings.R2_SECRET_ACCESS_KEY, + config=Config(signature_version="s3v4") ) + + @staticmethod + def upload_to_cloud(image): + r2_key = f"properties/{uuid.uuid4()}/{image.name}" + + CloudService.s3_client.upload_fileobj( + image, + settings.R2_BUCKET_NAME, + r2_key, + ExtraArgs={"ContentType": image.content_type} + ) + + return r2_key + + @staticmethod + def delete_from_cloud(r2_key): + CloudService.s3_client.delete_object( + Bucket=settings.R2_BUCKET_NAME, + Key=r2_key + ) + @staticmethod + def generate_url(r2_key): + return CloudService.s3_client.generate_presigned_url( + "get_object", + Params={"Bucket": settings.R2_BUCKET_NAME, "Key": r2_key}, + ExpiresIn=3600 + + + + + ) + +class NearbyPlacesService: + CATEGORIES = [("R", "Restaurant"), ("G", "Gym"), ("S", "School"), ("H", "Hospital"), ("SM", "Supermarket"), ("P", "Park")] + RADIUS = 3000 + + @staticmethod + def search_categories(lat, long, type): + url = "https://places.googleapis.com/v1/places:searchNearby" + headers = { + "Content-Type": "application/json", + "X-Goog-Api-Key": settings.GOOGLE_PLACES_API_KEY, + "X-Goog-FieldMask": "places.displayName,places.rating,places.location" + } + body = { + "includedTypes": [type], + "maxResultCount": 5, + "locationRestriction": { + "circle": { + "center": {"latitude": lat, "longitude": long}, + "radius": NearbyPlacesService.RADIUS + } + } + } + response = requests.post(url, json=body, headers=headers) + data = response.json() + return data.get("places", []) - return r2_key + @staticmethod + def calculate_distance(lat1, long1, lat2, long2): + R = 637100 + phi1 = math.radians(lat1) + phi2 = math.radians(lat2) + dphi = math.radians(lat2 - lat1) + dlambda = math.radians(long2 - long1) -def delete_from_cloud(r2_key): - s3_client.delete_object( - Bucket=settings.R2_BUCKET_NAME, - Key=r2_key - ) + a = math.sin(dphi/2)**2 + math.cos(phi1) * math.cos(phi2) * math.sin(dlambda/2)**2 + return round(2 * R * math.atan2(math.sqrt(a), math.sqrt(1 - a)), 2) -def generate_url(r2_key): - return s3_client.generate_presigned_url( - "get_object", - Params={"Bucket": settings.R2_BUCKET_NAME, "Key": r2_key}, - ExpiresIn=3600 + @staticmethod + def search(property): + lat = property.latitude + long = property.longitude + + for category_code, category_type in NearbyPlacesService.CATEGORIES: + places = NearbyPlacesService.search_categories(lat, long, category_type) + for place in places: + place_lat = place["location"]["latitude"] + place_long = place["location"]["longitude"] + NearbyPlaces.objects.update_or_create( + property=property, + name=place["displayName"]["text"], + category=category_code, + defaults={ + "distance_meters": NearbyPlacesService.calculate_distance(lat, long, place_lat, place_long), + "rating": place.get("rating") + } ) + From 42f1456fef181c262be8865d0f86094241adb769 Mon Sep 17 00:00:00 2001 From: Luisa Date: Fri, 24 Apr 2026 20:48:34 -0300 Subject: [PATCH 4/4] feat: creating celery tasks and adding to views and serializers --- apps/properties/serializers/photo_serializers.py | 10 +++++----- apps/properties/serializers/property_serializers.py | 7 ++++++- apps/properties/tasks.py | 13 +++++++++++++ apps/properties/views/property_views.py | 7 ++++++- 4 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 apps/properties/tasks.py diff --git a/apps/properties/serializers/photo_serializers.py b/apps/properties/serializers/photo_serializers.py index 330979a..6cebffb 100644 --- a/apps/properties/serializers/photo_serializers.py +++ b/apps/properties/serializers/photo_serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers from apps.properties.models import PropertiesPhotos -from apps.properties.services import upload_to_cloud, delete_from_cloud, generate_url +from apps.properties.services import CloudService class PropertiesUploadPhotosSerializer(serializers.ModelSerializer): image = serializers.ImageField(write_only=True) @@ -8,15 +8,15 @@ class PropertiesUploadPhotosSerializer(serializers.ModelSerializer): def create(self, validated_data): image = validated_data.pop('image') - r2_key = upload_to_cloud(image) + r2_key = CloudService.upload_to_cloud(image) return PropertiesPhotos.objects.create(r2_key=r2_key, **validated_data) def update(self, instance, validated_data): new_image = validated_data.pop('image', None) if new_image: - delete_from_cloud(instance.r2_key) - instance.r2_key = upload_to_cloud(new_image) + CloudService.delete_from_cloud(instance.r2_key) + instance.r2_key = CloudService.upload_to_cloud(new_image) instance.save() return instance @@ -30,7 +30,7 @@ class PropertiesPhotosSerializer(serializers.ModelSerializer): url = serializers.SerializerMethodField() def get_url(self, obj): - return generate_url(obj.r2_key) + return CloudService.generate_url(obj.r2_key) class Meta: diff --git a/apps/properties/serializers/property_serializers.py b/apps/properties/serializers/property_serializers.py index 6d4cc92..3147e86 100644 --- a/apps/properties/serializers/property_serializers.py +++ b/apps/properties/serializers/property_serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from apps.properties.models import Condo, Properties, Rooms, RoomsExtras +from apps.properties.models import Condo, Properties, Rooms, RoomsExtras, NearbyPlaces from apps.properties.validators import validate_positive_number, validate_required_field from apps.properties.serializers.photo_serializers import PropertiesPhotosSerializer from django.db.models import Avg @@ -38,12 +38,17 @@ class Meta: 'parking_spots': {'required': False}, } +class NearbyPlacesSerializer(serializers.ModelSerializer): + class Meta: + model = NearbyPlaces + fields = ["name", "category", "distance_meters", "rating"] class PropertiesReadSerializer(serializers.ModelSerializer): rooms = RoomsSerializer() condo = CondoSerializer() rooms_extras = RoomsExtrasSerializer() images = PropertiesPhotosSerializer(many=True, read_only=True, source="photos") + nearby_places = NearbyPlacesSerializer(many=True, read_only=True) average_rating = serializers.SerializerMethodField() owner_name = serializers.CharField(source="owner.name", read_only=True) diff --git a/apps/properties/tasks.py b/apps/properties/tasks.py new file mode 100644 index 0000000..28c6c63 --- /dev/null +++ b/apps/properties/tasks.py @@ -0,0 +1,13 @@ +import logging +from celery import shared_task + +logger = logging.getLogger(__name__) + +@shared_task +def search_nearby_places(property_id): + from .models import Properties + from .services import NearbyPlacesService + property_obj = Properties.objects.get(id=property_id) + + NearbyPlacesService.search(property_obj) + logger.info(f"Nearby places searched with success for property {property_id}") diff --git a/apps/properties/views/property_views.py b/apps/properties/views/property_views.py index 872c6aa..72d3a7f 100644 --- a/apps/properties/views/property_views.py +++ b/apps/properties/views/property_views.py @@ -8,6 +8,8 @@ from apps.properties.serializers.property_serializers import PropertiesWriteSerializer, PropertiesReadSerializer from apps.properties.serializers.reviews_serializers import ReviewsSerializer from apps.properties.permissions import IsAdvertiser, IsReviewOwner, IsPropertyOwner +from apps.properties.tasks import search_nearby_places +from apps.properties.services import NomatimService # C -> Create # R -> Read @@ -61,7 +63,10 @@ def get_permissions(self): return [AllowAny()] def perform_create(self, serializer): - serializer.save(owner_id=self.request.user.id) + property_obj = serializer.save(owner_id=self.request.user.id) + NomatimService.geocode(property_obj) + if property_obj.latitude and property_obj.longitude: + search_nearby_places.delay(property_obj.id) class RUDPropertyView(generics.RetrieveUpdateDestroyAPIView): queryset = Properties.objects.all()