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/") 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/") 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/") 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/") def delete_neighborhood(neighborhood_id: int): n = Neighborhood.query.get_or_404(neighborhood_id) db.session.delete(n) db.session.commit() return "", 204