155 lines
5.2 KiB
Python
155 lines
5.2 KiB
Python
import unicodedata
|
|
import re
|
|
|
|
from flask import Blueprint, jsonify, request
|
|
from sqlalchemy import func
|
|
|
|
from app.extensions import db
|
|
from app.models.location import City, Neighborhood
|
|
from app.models.property import Property
|
|
from app.schemas.catalog import CityOut, NeighborhoodOut
|
|
|
|
locations_bp = Blueprint("locations", __name__, url_prefix="/api/v1")
|
|
|
|
|
|
def _slugify(text: str) -> str:
|
|
text = unicodedata.normalize("NFD", text)
|
|
text = "".join(c for c in text if unicodedata.category(c) != "Mn")
|
|
text = text.lower()
|
|
text = re.sub(r"[^a-z0-9\s-]", "", text)
|
|
text = re.sub(r"[\s/]+", "-", text.strip())
|
|
return text
|
|
|
|
|
|
# ── Public endpoints ──────────────────────────────────────────────────────────
|
|
|
|
|
|
@locations_bp.get("/cities")
|
|
def list_cities():
|
|
"""List all cities ordered by state + name, with property_count."""
|
|
rows = (
|
|
db.session.query(City, func.count(Property.id).label("cnt"))
|
|
.outerjoin(
|
|
Property,
|
|
(Property.city_id == City.id) & (Property.is_active.is_(True)),
|
|
)
|
|
.group_by(City.id)
|
|
.order_by(City.state, City.name)
|
|
.all()
|
|
)
|
|
return jsonify(
|
|
[
|
|
{**CityOut.model_validate(city).model_dump(), "property_count": cnt}
|
|
for city, cnt in rows
|
|
]
|
|
)
|
|
|
|
|
|
@locations_bp.get("/neighborhoods")
|
|
def list_neighborhoods():
|
|
"""List neighborhoods, optionally filtered by city_id, with property_count."""
|
|
city_id = request.args.get("city_id")
|
|
q = (
|
|
db.session.query(Neighborhood, func.count(Property.id).label("cnt"))
|
|
.outerjoin(
|
|
Property,
|
|
(Property.neighborhood_id == Neighborhood.id)
|
|
& (Property.is_active.is_(True)),
|
|
)
|
|
.group_by(Neighborhood.id)
|
|
)
|
|
if city_id:
|
|
try:
|
|
q = q.filter(Neighborhood.city_id == int(city_id))
|
|
except ValueError:
|
|
pass
|
|
rows = q.order_by(Neighborhood.name).all()
|
|
return jsonify(
|
|
[
|
|
{
|
|
**NeighborhoodOut.model_validate(nbh).model_dump(),
|
|
"property_count": cnt,
|
|
}
|
|
for nbh, cnt in rows
|
|
]
|
|
)
|
|
|
|
|
|
# ── Admin endpoints ───────────────────────────────────────────────────────────
|
|
|
|
|
|
@locations_bp.post("/admin/cities")
|
|
def create_city():
|
|
data = request.get_json(force=True) or {}
|
|
name = (data.get("name") or "").strip()
|
|
state = (data.get("state") or "").strip().upper()[:2]
|
|
if not name or not state:
|
|
return jsonify({"error": "name e state são obrigatórios"}), 400
|
|
slug = data.get("slug") or _slugify(name)
|
|
if City.query.filter_by(slug=slug).first():
|
|
return jsonify({"error": "Já existe uma cidade com esse slug"}), 409
|
|
city = City(name=name, slug=slug, state=state)
|
|
db.session.add(city)
|
|
db.session.commit()
|
|
return jsonify(CityOut.model_validate(city).model_dump()), 201
|
|
|
|
|
|
@locations_bp.put("/admin/cities/<int:city_id>")
|
|
def update_city(city_id: int):
|
|
city = City.query.get_or_404(city_id)
|
|
data = request.get_json(force=True) or {}
|
|
if "name" in data:
|
|
city.name = data["name"].strip()
|
|
if "state" in data:
|
|
city.state = data["state"].strip().upper()[:2]
|
|
if "slug" in data:
|
|
city.slug = data["slug"].strip()
|
|
db.session.commit()
|
|
return jsonify(CityOut.model_validate(city).model_dump())
|
|
|
|
|
|
@locations_bp.delete("/admin/cities/<int:city_id>")
|
|
def delete_city(city_id: int):
|
|
city = City.query.get_or_404(city_id)
|
|
db.session.delete(city)
|
|
db.session.commit()
|
|
return "", 204
|
|
|
|
|
|
@locations_bp.post("/admin/neighborhoods")
|
|
def create_neighborhood():
|
|
data = request.get_json(force=True) or {}
|
|
name = (data.get("name") or "").strip()
|
|
city_id = data.get("city_id")
|
|
if not name or not city_id:
|
|
return jsonify({"error": "name e city_id são obrigatórios"}), 400
|
|
city = City.query.get_or_404(int(city_id))
|
|
slug = data.get("slug") or _slugify(name)
|
|
if Neighborhood.query.filter_by(slug=slug, city_id=city.id).first():
|
|
return jsonify({"error": "Já existe um bairro com esse slug nessa cidade"}), 409
|
|
neighborhood = Neighborhood(name=name, slug=slug, city_id=city.id)
|
|
db.session.add(neighborhood)
|
|
db.session.commit()
|
|
return jsonify(NeighborhoodOut.model_validate(neighborhood).model_dump()), 201
|
|
|
|
|
|
@locations_bp.put("/admin/neighborhoods/<int:neighborhood_id>")
|
|
def update_neighborhood(neighborhood_id: int):
|
|
n = Neighborhood.query.get_or_404(neighborhood_id)
|
|
data = request.get_json(force=True) or {}
|
|
if "name" in data:
|
|
n.name = data["name"].strip()
|
|
if "slug" in data:
|
|
n.slug = data["slug"].strip()
|
|
if "city_id" in data:
|
|
n.city_id = int(data["city_id"])
|
|
db.session.commit()
|
|
return jsonify(NeighborhoodOut.model_validate(n).model_dump())
|
|
|
|
|
|
@locations_bp.delete("/admin/neighborhoods/<int:neighborhood_id>")
|
|
def delete_neighborhood(neighborhood_id: int):
|
|
n = Neighborhood.query.get_or_404(neighborhood_id)
|
|
db.session.delete(n)
|
|
db.session.commit()
|
|
return "", 204
|