feat: add full project - backend, frontend, docker, specs and configs
This commit is contained in:
parent
b77c7d5a01
commit
e6cb06255b
24489 changed files with 61341 additions and 36 deletions
155
backend/app/routes/locations.py
Normal file
155
backend/app/routes/locations.py
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue