Add listing decisions (like/dislike) backend with detail endpoint
- ListingDecision model with unique constraint on (user_id, listing_id, listing_type)
- Alembic migration for listingdecision table
- DecisionRepository with dialect-aware upsert (MySQL/SQLite)
- DecisionService with input validation
- Decision API routes: PUT/GET/DELETE on /api/decisions
- GET /api/listing/{id}/detail endpoint extracting full property info from additional_info
- Add listing ID to GeoJSON feature properties
- Decision filtering on GeoJSON stream endpoint (decision_filter param)
This commit is contained in:
parent
a2e7d59af2
commit
9e1beb7495
7 changed files with 447 additions and 138 deletions
|
|
@ -1,5 +1,4 @@
|
|||
"""Repository for listing decisions (like/dislike)."""
|
||||
from datetime import datetime, UTC
|
||||
from datetime import datetime
|
||||
|
||||
from models.decision import ListingDecision
|
||||
from sqlalchemy import Engine
|
||||
|
|
@ -13,32 +12,53 @@ class DecisionRepository:
|
|||
self.engine = engine
|
||||
|
||||
def upsert_decision(
|
||||
self, user_id: int, listing_id: int, listing_type: str, decision: str
|
||||
self,
|
||||
user_id: int,
|
||||
listing_id: int,
|
||||
listing_type: str,
|
||||
decision: str,
|
||||
) -> ListingDecision:
|
||||
"""Create or update a decision. Uses dialect-specific upsert."""
|
||||
with Session(self.engine) as session:
|
||||
statement = select(ListingDecision).where(
|
||||
ListingDecision.user_id == user_id,
|
||||
ListingDecision.listing_id == listing_id,
|
||||
ListingDecision.listing_type == listing_type,
|
||||
)
|
||||
existing = session.exec(statement).first()
|
||||
if existing:
|
||||
existing.decision = decision
|
||||
existing.updated_at = datetime.now(UTC)
|
||||
session.add(existing)
|
||||
session.commit()
|
||||
session.refresh(existing)
|
||||
return existing
|
||||
new_decision = ListingDecision(
|
||||
user_id=user_id,
|
||||
listing_id=listing_id,
|
||||
listing_type=listing_type,
|
||||
decision=decision,
|
||||
)
|
||||
session.add(new_decision)
|
||||
now = datetime.utcnow()
|
||||
values = {
|
||||
"user_id": user_id,
|
||||
"listing_id": listing_id,
|
||||
"listing_type": listing_type,
|
||||
"decision": decision,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
}
|
||||
dialect = self.engine.dialect.name
|
||||
if dialect == "mysql":
|
||||
from sqlalchemy.dialects.mysql import insert as mysql_insert
|
||||
stmt = mysql_insert(ListingDecision).values(**values)
|
||||
stmt = stmt.on_duplicate_key_update(
|
||||
decision=stmt.inserted.decision,
|
||||
updated_at=stmt.inserted.updated_at,
|
||||
)
|
||||
else:
|
||||
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
|
||||
stmt = sqlite_insert(ListingDecision).values(**values)
|
||||
stmt = stmt.on_conflict_do_update(
|
||||
index_elements=["user_id", "listing_id", "listing_type"],
|
||||
set_={
|
||||
"decision": stmt.excluded.decision,
|
||||
"updated_at": stmt.excluded.updated_at,
|
||||
},
|
||||
)
|
||||
session.execute(stmt)
|
||||
session.commit()
|
||||
session.refresh(new_decision)
|
||||
return new_decision
|
||||
# Fetch the result
|
||||
result = session.exec(
|
||||
select(ListingDecision).where(
|
||||
ListingDecision.user_id == user_id,
|
||||
ListingDecision.listing_id == listing_id,
|
||||
ListingDecision.listing_type == listing_type,
|
||||
)
|
||||
).first()
|
||||
assert result is not None
|
||||
return result
|
||||
|
||||
def get_decisions_for_user(self, user_id: int) -> list[ListingDecision]:
|
||||
with Session(self.engine) as session:
|
||||
|
|
@ -48,23 +68,29 @@ class DecisionRepository:
|
|||
return list(session.exec(statement).all())
|
||||
|
||||
def delete_decision(
|
||||
self, user_id: int, listing_id: int, listing_type: str
|
||||
self,
|
||||
user_id: int,
|
||||
listing_id: int,
|
||||
listing_type: str,
|
||||
) -> bool:
|
||||
with Session(self.engine) as session:
|
||||
statement = select(ListingDecision).where(
|
||||
ListingDecision.user_id == user_id,
|
||||
ListingDecision.listing_id == listing_id,
|
||||
ListingDecision.listing_type == listing_type,
|
||||
)
|
||||
existing = session.exec(statement).first()
|
||||
if existing is None:
|
||||
result = session.exec(
|
||||
select(ListingDecision).where(
|
||||
ListingDecision.user_id == user_id,
|
||||
ListingDecision.listing_id == listing_id,
|
||||
ListingDecision.listing_type == listing_type,
|
||||
)
|
||||
).first()
|
||||
if result is None:
|
||||
return False
|
||||
session.delete(existing)
|
||||
session.delete(result)
|
||||
session.commit()
|
||||
return True
|
||||
|
||||
def get_disliked_listing_ids(
|
||||
self, user_id: int, listing_type: str
|
||||
self,
|
||||
user_id: int,
|
||||
listing_type: str,
|
||||
) -> set[int]:
|
||||
with Session(self.engine) as session:
|
||||
statement = select(ListingDecision.listing_id).where(
|
||||
|
|
@ -73,3 +99,16 @@ class DecisionRepository:
|
|||
ListingDecision.decision == "disliked",
|
||||
)
|
||||
return {row for row in session.exec(statement).all()}
|
||||
|
||||
def get_liked_listing_ids(
|
||||
self,
|
||||
user_id: int,
|
||||
listing_type: str,
|
||||
) -> set[int]:
|
||||
with Session(self.engine) as session:
|
||||
statement = select(ListingDecision.listing_id).where(
|
||||
ListingDecision.user_id == user_id,
|
||||
ListingDecision.listing_type == listing_type,
|
||||
ListingDecision.decision == "liked",
|
||||
)
|
||||
return {row for row in session.exec(statement).all()}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue