Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 98.9%
Python 0.6%
1# -----------------------------------------------------------------------------2# Lou-Ka — Agrégateur de logements à louer (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# accounts.py : fonctionnalités de compte au-delà de l'authentification5# · Rôle : « locataire » (cherche un logement) ou « gestionnaire » (gère des6# logements affichés) — choisi à l'accueil après la création du compte.7# · Locataire : favoris (POST/DELETE /api/favorites/{uid}, GET /api/favorites).8# · Gestionnaire : réclame la page de SA source (première arrivée, une source9# par compte, un compte par source) puis la personnalise (accroche,10# description, coordonnées, logo) — affichée publiquement sur /g/{source}.11# -----------------------------------------------------------------------------12from __future__ import annotations1314import json15import time16from pathlib import Path1718from fastapi import APIRouter, HTTPException, Request19from pydantic import BaseModel2021from . import db, hubfav, kaid22from .auth import current_user2324SOURCES_PATH = Path(__file__).resolve().parent.parent / "data" / "sources.json"2526router = APIRouter(prefix="/api")2728ROLES = ("locataire", "gestionnaire")293031def _require(request: Request) -> dict:32 user = current_user(request)33 if user is None:34 raise HTTPException(401, "non connecté")35 return user363738def _row_listing(row) -> dict:39 d = dict(row)40 d["amenities"] = json.loads(d.get("amenities") or "[]")41 d["images"] = json.loads(d.get("images") or "[]")42 d["details"] = json.loads(d.get("details") or "{}")43 if d.get("furnished") is not None:44 d["furnished"] = bool(d["furnished"])45 return d464748# -- rôle -----------------------------------------------------------------------4950@router.post("/me/role")51def set_role(request: Request, role: str):52 user = _require(request)53 if role not in ROLES:54 raise HTTPException(400, f"rôle invalide (choix : {', '.join(ROLES)})")55 con = db.connect()56 con.execute("UPDATE users SET role=? WHERE id=?", (role, user["uid"]))57 con.commit()58 con.close()59 return {"role": role}606162# -- favoris (locataire) ---------------------------------------------------------6364@router.get("/favorites")65def favorites(request: Request):66 user = _require(request)67 con = db.connect()68 rows = con.execute(69 """SELECT l.* FROM favorites f JOIN listings l ON l.uid = f.uid70 WHERE f.user_id=? ORDER BY f.ts DESC""", (user["uid"],)).fetchall()71 uids = [r["uid"] for r in con.execute(72 "SELECT uid FROM favorites WHERE user_id=?", (user["uid"],))]73 con.close()74 return {"uids": uids, "listings": [_row_listing(r) for r in rows]}757677def _ka_id_of(con, user_id: int) -> str | None:78 row = con.execute("SELECT ka_id FROM users WHERE id=?", (user_id,)).fetchone()79 return row["ka_id"] if row else None808182@router.post("/favorites/{uid}")83def add_favorite(request: Request, uid: str):84 user = _require(request)85 con = db.connect()86 listing = con.execute("SELECT * FROM listings WHERE uid=?", (uid,)).fetchone()87 if listing is None:88 con.close()89 raise HTTPException(404, "annonce introuvable")90 con.execute("INSERT OR IGNORE INTO favorites (user_id, uid, ts) VALUES (?,?,?)",91 (user["uid"], uid, time.time()))92 con.commit()93 ka_id = _ka_id_of(con, user["uid"])94 con.close()95 # miroir « Mon univers Ka » sur le hub (arrière-plan, jamais bloquant)96 hubfav.push_favorite(ka_id, "add", hubfav.listing_item(listing))97 d = dict(listing)98 kaid.track({"ka_id": ka_id}, "favorite", entity_type="listing",99 entity_id=uid, features={k: v for k, v in {100 "city": d.get("city"), "sector": d.get("sector"),101 "unit_type": d.get("unit_type"), "price": d.get("price"),102 "pets": d.get("pets")}.items() if v not in (None, "")})103 return {"ok": True}104105106@router.delete("/favorites/{uid}")107def remove_favorite(request: Request, uid: str):108 user = _require(request)109 con = db.connect()110 con.execute("DELETE FROM favorites WHERE user_id=? AND uid=?",111 (user["uid"], uid))112 con.commit()113 ka_id = _ka_id_of(con, user["uid"])114 con.close()115 hubfav.push_favorite(ka_id, "remove", {"item_id": uid})116 kaid.track({"ka_id": ka_id}, "unfavorite", entity_type="listing",117 entity_id=uid)118 return {"ok": True}119120121# -- page gestionnaire -----------------------------------------------------------122123def _source_name(source_id: str) -> str | None:124 try:125 registry = json.loads(SOURCES_PATH.read_text(encoding="utf-8"))["sources"]126 except Exception:127 return None128 for s in registry:129 if s["id"] == source_id:130 return s.get("name") or source_id131 return None132133134class OrgUpdate(BaseModel):135 tagline: str = ""136 description: str = ""137 website: str = ""138 phone: str = ""139 email: str = ""140 logo_url: str = ""141142143@router.post("/org/claim")144def claim_org(request: Request, source: str):145 """Réclame la page d'une source (gestionnaire) — 1 source/compte, 1 compte/source."""146 user = _require(request)147 name = _source_name(source)148 if name is None:149 raise HTTPException(404, "source inconnue")150 con = db.connect()151 me_row = con.execute("SELECT role, org_source FROM users WHERE id=?",152 (user["uid"],)).fetchone()153 if me_row["role"] != "gestionnaire":154 con.close()155 raise HTTPException(403, "réservé aux comptes gestionnaires")156 if me_row["org_source"] and me_row["org_source"] != source:157 con.close()158 raise HTTPException(409, "votre compte gère déjà une autre page")159 owner = con.execute(160 "SELECT owner_user_id FROM source_profiles WHERE source_id=?",161 (source,)).fetchone()162 if owner and owner["owner_user_id"] not in (None, user["uid"]):163 con.close()164 raise HTTPException(409, "cette page est déjà réclamée par un autre compte")165 con.execute(166 """INSERT INTO source_profiles (source_id, owner_user_id, updated_at)167 VALUES (?,?,?)168 ON CONFLICT(source_id) DO UPDATE SET169 owner_user_id=excluded.owner_user_id, updated_at=excluded.updated_at""",170 (source, user["uid"], time.time()))171 con.execute("UPDATE users SET org_source=? WHERE id=?", (source, user["uid"]))172 con.commit()173 con.close()174 return {"source": source, "name": name}175176177@router.get("/org/mine")178def my_org(request: Request):179 user = _require(request)180 con = db.connect()181 me_row = con.execute("SELECT org_source FROM users WHERE id=?",182 (user["uid"],)).fetchone()183 source = me_row["org_source"] if me_row else None184 if not source:185 con.close()186 return {"source": None}187 prof = con.execute("SELECT * FROM source_profiles WHERE source_id=?",188 (source,)).fetchone()189 n = con.execute(190 "SELECT COUNT(*) c FROM listings WHERE source=? AND active=1",191 (source,)).fetchone()["c"]192 con.close()193 return {194 "source": source,195 "name": _source_name(source) or source,196 "active_listings": n,197 "profile": dict(prof) if prof else {},198 }199200201@router.put("/org")202def update_org(request: Request, body: OrgUpdate):203 user = _require(request)204 con = db.connect()205 me_row = con.execute("SELECT org_source FROM users WHERE id=?",206 (user["uid"],)).fetchone()207 source = me_row["org_source"] if me_row else None208 if not source:209 con.close()210 raise HTTPException(409, "aucune page réclamée")211 owner = con.execute(212 "SELECT owner_user_id FROM source_profiles WHERE source_id=?",213 (source,)).fetchone()214 if not owner or owner["owner_user_id"] != user["uid"]:215 con.close()216 raise HTTPException(403, "vous n'êtes pas propriétaire de cette page")217 con.execute(218 """UPDATE source_profiles SET tagline=?, description=?, website=?,219 phone=?, email=?, logo_url=?, updated_at=? WHERE source_id=?""",220 (body.tagline.strip()[:140], body.description.strip()[:2000],221 body.website.strip()[:300], body.phone.strip()[:40],222 body.email.strip()[:120], body.logo_url.strip()[:500],223 time.time(), source))224 con.commit()225 con.close()226 return {"ok": True}227228229@router.get("/org/{source_id}")230def public_org(source_id: str):231 """Page publique d'un gestionnaire : nom + personnalisation + inventaire."""232 name = _source_name(source_id)233 if name is None:234 raise HTTPException(404, "source inconnue")235 con = db.connect()236 prof = con.execute("SELECT * FROM source_profiles WHERE source_id=?",237 (source_id,)).fetchone()238 n = con.execute(239 "SELECT COUNT(*) c FROM listings WHERE source=? AND active=1",240 (source_id,)).fetchone()["c"]241 con.close()242 out = {"source": source_id, "name": name, "active_listings": n,243 "claimed": bool(prof and prof["owner_user_id"])}244 if prof:245 for f in ("tagline", "description", "website", "phone", "email", "logo_url"):246 out[f] = prof[f] or ""247 return out248