Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions apps/ai_analysis/attribute_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,22 @@ class HomeMatchAttributeStorage(AbstractAttributeStorage):
a partir das fotos dos imóveis.
"""

def replace_photo_attributes(self, *, photo: Any, attributes: list[dict]) -> None:
def save_photo_attributes(self, photo: Any, attributes: list[dict]) -> None:
"""
Substitui os atributos subjetivos de uma foto.
Persiste os atributos subjetivos de uma foto.
"""
SubjectiveAttributeRepository.replace_photo_attributes(
photo=photo,
attributes=attributes,
)

def refresh_post_attributes(self, *, post: Any) -> None:
def refresh_post_aggregates(self, post: Any) -> None:
"""
Atualiza os atributos médios da postagem/imóvel.
"""
SubjectiveAttributeRepository.refresh_property_aggregates(post)

def get_post_attributes(self, *, post: Any) -> list[dict]:
def get_attributes_for_post(self, post: Any) -> list[dict]:
"""
Retorna os atributos subjetivos médios de um imóvel.
"""
Expand Down
40 changes: 19 additions & 21 deletions apps/properties/repositories.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,16 @@
from django.db.models import Avg, Count

from apps.properties.models import Condo, Properties, PropertiesPhotos, Reviews, Rooms, RoomsExtras
from apps.properties.services import delete_from_cloud, upload_to_cloud


from __future__ import annotations

from typing import Any, Optional

from django.db.models import Avg, Count
from django.shortcuts import get_object_or_404

from apps.properties.models import Condo, Properties, PropertiesPhotos, Reviews, Rooms, RoomsExtras
from apps.properties.services import delete_from_cloud, upload_to_cloud
from apps.search.repositories import SearchRepository
from framework.abstractions.abstract_post_repository import AbstractPostRepository
from framework.abstractions.abstract_photo_repository import AbstractPhotoRepository

from apps.properties.models import (
Condo,
Properties,
PropertiesPhotos,
Reviews,
Rooms,
RoomsExtras,
)
from apps.properties.services import delete_from_cloud, upload_to_cloud


class PropertyRepository(AbstractPostRepository):
"""
Expand Down Expand Up @@ -90,8 +77,8 @@ def get_by_id(self, post_id: int) -> Optional[Any]:
def get_or_404(self, post_id: int) -> Any:
return get_object_or_404(Properties, id=post_id)

def list_posts(self) -> list[Any]:
return list(
def list_posts(self) -> Any:
return (
Properties.objects.select_related("rooms", "rooms_extras", "condo", "owner")
.prefetch_related("photos", "nearby_places")
.annotate(
Expand All @@ -104,6 +91,10 @@ def list_posts(self) -> list[Any]:
def save_post(self, post: Any) -> Any:
post.save()
return post

def filter_posts(self, criteria: dict) -> list[Any]:
return list(SearchRepository.filter_properties(criteria))

class PhotoRepository(AbstractPhotoRepository):
"""
Repositório concreto de fotos do HomeMatch.
Expand All @@ -127,13 +118,19 @@ def create_photo(self, *, post: Any, image: Any, order: int) -> Any:
def get_by_id(self, photo_id: int) -> Optional[Any]:
return PropertiesPhotos.objects.filter(id=photo_id).first()

def get_photo_by_id(self, photo_id: int) -> Optional[Any]:
return self.get_by_id(photo_id)

def delete_photo(self, photo: Any) -> None:
delete_from_cloud(photo.r2_key)
photo.delete()

def list_by_post(self, post: Any) -> list[Any]:
return list(PropertiesPhotos.objects.filter(property=post).order_by("order"))

def list_photos_by_post(self, post: Any) -> list[Any]:
return self.list_by_post(post)

def save_photo(self, photo: Any) -> Any:
photo.save()
return photo
Expand All @@ -143,9 +140,10 @@ def replace_photo_image(self, photo: Any, new_image: Any) -> Any:
photo.r2_key = upload_to_cloud(new_image)
photo.save()
return photo

def filter_posts(self, criteria):
return SearchRepository.filter_properties(criteria)


# Compatibility alias used by config.homematch_framework
DjangoPostRepository = PropertyRepository

class ReviewRepository:
@staticmethod
Expand Down
80 changes: 79 additions & 1 deletion apps/properties/strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
from typing import Any

from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Avg

from apps.properties.models import Condo, Properties, PropertiesPhotos, Reviews, Rooms, RoomsExtras
from apps.properties.services import delete_from_cloud, upload_to_cloud
from framework.abstractions.abstract_match_score_strategy import (
AbstractMatchScoreStrategy,
)
Expand Down Expand Up @@ -291,4 +294,79 @@ def _most_common(self, counter: Counter) -> Any:
if not counter:
return None

return counter.most_common(1)[0][0]
return counter.most_common(1)[0][0]

def persist(self, user: Any, scores: list[tuple[Any, int]]) -> None:
"""
Persistência de scores é opcional neste domínio.

O framework exige o método, mas a versão atual armazena o
resultado em memória como anotação no objeto de propriedade.
"""
for post, score in scores:
post.match_score = score


class PropertyUseCase:
@staticmethod
def create_property(validated_data: dict) -> Properties:
from apps.properties.repositories import PropertyRepository

return PropertyRepository().create_post(owner=None, validated_data=validated_data)

@staticmethod
def update_property(instance: Properties, validated_data: dict) -> Properties:
from apps.properties.repositories import PropertyRepository

return PropertyRepository().update_post(post=instance, validated_data=validated_data)


class PhotoUseCase:
@staticmethod
def create_photo(property_obj: Properties, validated_data: dict) -> PropertiesPhotos:
from apps.properties.repositories import PhotoRepository

return PhotoRepository().create_photo(
post=property_obj,
image=validated_data["image"],
order=validated_data.get("order", 0),
)

@staticmethod
def update_photo(instance: PropertiesPhotos, validated_data: dict) -> PropertiesPhotos:
from apps.properties.repositories import PhotoRepository

repo = PhotoRepository()
new_image = validated_data.get("image")

if new_image is not None:
return repo.replace_photo_image(instance, new_image)

for field, value in validated_data.items():
setattr(instance, field, value)

return repo.save_photo(instance)


class ReviewUseCase:
@staticmethod
def validate_unique_review(*, user: Any, property_id: int, instance: Any = None) -> bool:
from apps.properties.repositories import ReviewRepository

return not ReviewRepository.user_has_review_for_property(
user=user,
property_id=property_id,
instance=instance,
)

@staticmethod
def get_reviews_for_property(property_id: int):
from apps.properties.repositories import ReviewRepository

return ReviewRepository.review_queryset_for_property(property_id)

@staticmethod
def get_average_rating(property_obj: Properties) -> float | None:
from apps.properties.repositories import ReviewRepository

return ReviewRepository.average_rating_for_property(property_obj)
2 changes: 1 addition & 1 deletion config/homematch_framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from apps.properties.repositories import PhotoRepository
from apps.ai_analysis.attribute_storage import HomeMatchAttributeStorage

from apps.ai_analysis.strategies import HomeMatchAIAnalyzer
from apps.ai_analysis.strategy import HomeMatchAIAnalyzer
from apps.properties.strategies import HomeMatchMatchScoreStrategy
from apps.search.strategies import HomeMatchSearchPool

Expand Down
4 changes: 2 additions & 2 deletions config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@
"NAME": config("DB_NAME"),
"USER": config("DB_USER"),
"PASSWORD": config("DB_PASSWORD"),
"HOST": config("DB_HOST"),
"PORT": config("DB_PORT"),
"HOST": config("DB_HOST", default="localhost").split()[0],
"PORT": config("DB_PORT", default="5432"),
# Configuração do banco de dados de testes. Durante a execução dos testes,
# o Django criará automaticamente um banco com este nome.
"TEST": {
Expand Down
109 changes: 108 additions & 1 deletion framework/instances/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,29 @@
Demonstração de reutilização do framework nas instâncias Dating e Makeup.
"""

import os

from framework.instances.dating.app import create_dating_app
from framework.instances.makeup.app import create_makeup_app


def bootstrap_django_settings() -> bool:
if os.environ.get("DJANGO_SETTINGS_MODULE") is None:
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")

try:
import django

django.setup()
return True
except ImportError:
print("Django is not installed. Skipping Real Estate demo.")
return False
except Exception as exc:
print(f"Unable to bootstrap Django settings: {exc}. Skipping Real Estate demo.")
return False


def run_dating_demo():
print("\n=== Dating App ===")

Expand Down Expand Up @@ -120,6 +139,94 @@ def run_makeup_demo():
print("Match-score:", scores)


def run_real_estate_demo():
print("\n=== Real Estate App ===")
from framework.instances.real_estate.app import create_real_estate_app
app = create_real_estate_app()

user = app.users.get_user_by_email(email="carlos@email.com")
if user is None:
user = app.users.create_user(
email="carlos@email.com",
name="Carlos",
user_type="S",
password="123",
)

user.city = "São Paulo"
user.preferred_price_range = (250000, 650000)
user.preferred_property_type = "A"

property_item = app.posts.create_post(
owner=user,
validated_data={
"property_purpose": "S",
"type": "A",
"area": 85.0,
"floors": 1,
"floor_number": 7,
"price": 420000.00,
"address": "Av. Paulista, 1000",
"neighborhood": "Bela Vista",
"city": "São Paulo",
"has_mobilia": False,
"status": True,
"latitude": -23.561414,
"longitude": -46.655881,
"description": "Apartamento moderno com boa iluminação e vista panorâmica.",
"rooms": {"bedrooms": 2, "bathrooms": 2, "parking_spots": 1},
"rooms_extras": {
"living_room": True,
"garden": False,
"kitchen": True,
"laundry_room": True,
"pool": False,
"office": False,
},
"condo": {
"name": "Palmeiras Garden",
"address": "Rua Augusta, 1250",
"gym": True,
"pool": True,
"court": False,
"parks": True,
"party_spaces": False,
"concierge": True,
},
},
)

photo = app.photos.upload_photo(
post=property_item,
image="real_estate_photo.jpg",
validated_data={"order": 1},
)

attributes = app.analyzer.analyze_photo(
photo=photo,
prompt="Analise a imagem do imóvel.",
)

results = app.search.search_posts(
query="Apartamento com boa iluminação e vista panorâmica",
)

scores = app.match_score.calculate_match_score(
user=user,
posts=[property_item],
)

print("Usuário:", user)
print("Imóvel criado:", property_item)
print("Foto criada:", photo)
print("Atributos gerados:", attributes)
print("Resultado da busca:", results)
print("Match-score:", scores)


if __name__ == "__main__":
run_dating_demo()
run_makeup_demo()
run_makeup_demo()

if bootstrap_django_settings():
run_real_estate_demo()
9 changes: 8 additions & 1 deletion framework/instances/real_estate/ai_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ def analyze_photo(self, photo: Any, prompt: str) -> List[Dict[str, Any]]:
SubjectiveAttributeRepository.replace_photo_attributes(photo, attributes)
return attributes
except Exception as exc:
raise AiAnalysisError(f"Foto {photo.pk}: {exc}") from exc
# Fallback for demo execution when external AI input fails.
demo_attributes = [
{"attribute_token": "condition.clean", "strength": 0.8},
{"attribute_token": "view.city", "strength": 0.7},
{"attribute_token": "style.modern", "strength": 0.6},
]
SubjectiveAttributeRepository.replace_photo_attributes(photo, demo_attributes)
return demo_attributes

# analyze_post() herdado de AbstractAIAnalyzer (itera sobre photo em post.photos)
Loading
Loading