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
30
backend/seeds/data/amenidades_pendentes.csv
Normal file
30
backend/seeds/data/amenidades_pendentes.csv
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
codigo_csv,traducao,opcao_1,opcao_2,opcao_3,escolha
|
||||
COLD_FLOOR,Piso frio,nova:caracteristica → piso-frio,ignorar,,
|
||||
DIVIDERS,Divisórias,nova:caracteristica → divisorias,ignorar,,
|
||||
FENCE,Cerca / Muro,nova:seguranca → cerca-muro,ignorar,,
|
||||
FRUIT_TREES,Árvores frutíferas,nova:caracteristica → arvores-frutiferas,mapear → jardim,ignorar,
|
||||
FULL_FLOOR,Andar corrido,nova:caracteristica → andar-corrido,ignorar,,
|
||||
GARAGE,Garagem (estrutural),ignorar (já capturado em vagas_garagem),nova:caracteristica → garagem,,
|
||||
INTEGRATED_ENVIRONMENTS,Ambientes integrados,nova:caracteristica → ambientes-integrados,ignorar,,
|
||||
KITCHEN,Cozinha,ignorar (estrutural),nova:caracteristica → cozinha,,
|
||||
LARGE_ROOM,Sala ampla,nova:caracteristica → sala-ampla,ignorar,,
|
||||
LUNCH_ROOM,Sala de almoço,mapear → copa,nova:caracteristica → sala-de-almoco,ignorar,
|
||||
MASSAGE_ROOM,Sala de massagem,nova:lazer → sala-de-massagem,mapear → spa,ignorar,
|
||||
NEAR_ACCESS_ROADS,Próximo a vias de acesso,nova:localizacao → proximo-a-vias-de-acesso,ignorar,,
|
||||
NEAR_HOSPITAL,Próximo a hospital,nova:localizacao → proximo-a-hospital,ignorar,,
|
||||
NEAR_PUBLIC_TRANSPORT,Próximo a transporte público,nova:localizacao → proximo-a-transporte-publico,ignorar,,
|
||||
NEAR_SCHOOL,Próximo a escola,nova:localizacao → proximo-a-escola,ignorar,,
|
||||
NEAR_SHOPPING_CENTER,Próximo a shopping,nova:localizacao → proximo-a-shopping,ignorar,,
|
||||
NUMBER_OF_FLOORS,Número de andares,ignorar (valor numérico),,,
|
||||
PANTRY,Despensa,nova:caracteristica → despensa,ignorar,,
|
||||
PARKING,Estacionamento,ignorar (já capturado em vagas_garagem),mapear → vaga-para-visitante,,
|
||||
PLAN,Planta (tipo de layout),ignorar (não é amenidade),,,
|
||||
RESTAURANT,Restaurante,nova:condominio → restaurante,ignorar,,
|
||||
SANCA,Sanca (acabamento de gesso),nova:caracteristica → sanca,ignorar,,
|
||||
SERVICE_ENTRANCE,Entrada de serviço,nova:caracteristica → entrada-de-servico,ignorar,,
|
||||
SIDE_ENTRANCE,Entrada lateral,nova:caracteristica → entrada-lateral,ignorar,,
|
||||
SLAB,Laje,nova:caracteristica → laje,ignorar,,
|
||||
SMALL_ROOM,Cômodo pequeno,ignorar,nova:caracteristica → comodo-pequeno,,
|
||||
SQUARE,Praça,nova:localizacao → praca,mapear → espaco-verde-parque,ignorar,
|
||||
STAIR,Escada,ignorar (estrutural),nova:caracteristica → escada,,
|
||||
TEEN_SPACE,Espaço teen,nova:lazer → espaco-teen,mapear → sala-de-jogos,ignorar,
|
||||
|
12064
backend/seeds/data/fotos.csv
Normal file
12064
backend/seeds/data/fotos.csv
Normal file
File diff suppressed because it is too large
Load diff
7122
backend/seeds/data/imoveis.csv
Normal file
7122
backend/seeds/data/imoveis.csv
Normal file
File diff suppressed because it is too large
Load diff
385
backend/seeds/import_from_csv.py
Normal file
385
backend/seeds/import_from_csv.py
Normal file
|
|
@ -0,0 +1,385 @@
|
|||
"""
|
||||
Importa imóveis de imoveis.csv + fotos.csv para o banco de dados.
|
||||
|
||||
Uso:
|
||||
cd backend
|
||||
uv run python seeds/import_from_csv.py [--csv-dir CAMINHO]
|
||||
|
||||
Padrão de CAMINHO: ../../aluguel_helper (relativo ao diretório backend/)
|
||||
Idempotente: pula imóveis cujo `code` já existe no banco.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import unicodedata
|
||||
from collections import defaultdict
|
||||
from decimal import Decimal
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from app import create_app
|
||||
from app.extensions import db
|
||||
from app.models.location import City, Neighborhood
|
||||
from app.models.property import Property, PropertyPhoto
|
||||
from app.models.catalog import Amenity, PropertyType
|
||||
from app.models.imobiliaria import Imobiliaria
|
||||
|
||||
# ── Configurações ─────────────────────────────────────────────────────────────
|
||||
|
||||
CITY_NAME = "Franca"
|
||||
CITY_STATE = "SP"
|
||||
|
||||
# Mapeamento tipo_imovel (CSV) → slug do subtipo (já criado pelo seed.py)
|
||||
SUBTYPE_MAP: dict[str, str] = {
|
||||
"casa": "casa",
|
||||
"apartamento": "apartamento",
|
||||
"flat/studio": "flat",
|
||||
"store": "loja-salao-ponto-comercial",
|
||||
"landform": "terreno-lote-condominio",
|
||||
}
|
||||
|
||||
# Mapeamento amenidade CSV (inglês) → slug português do banco
|
||||
# Valores sem correspondência no sistema são ignorados (None).
|
||||
AMENITY_MAP: dict[str, str | None] = {
|
||||
# ── Características ───────────────────────────────────────────────────────
|
||||
"PETS_ALLOWED": "aceita-animais",
|
||||
"AIR_CONDITIONING": "ar-condicionado",
|
||||
"SERVICE_AREA": "area-de-servico",
|
||||
"BUILTIN_WARDROBE": "armario-embutido",
|
||||
"BEDROOM_WARDROBE": "armario-embutido-no-quarto",
|
||||
"KITCHEN_CABINETS": "armario-na-cozinha",
|
||||
"BATHROOM_CABINETS": "armario-no-banheiro",
|
||||
"BLINDEX_BOX": "box-blindex",
|
||||
"CLOSET": "closet",
|
||||
"DRESS_ROOM2": "closet",
|
||||
"INTERNET_ACCESS": "conexao-a-internet",
|
||||
"AMERICAN_KITCHEN": "cozinha-americana",
|
||||
"LARGE_KITCHEN": "cozinha-americana",
|
||||
"GOURMET_KITCHEN": "cozinha-gourmet",
|
||||
"COOKER": "fogao",
|
||||
"INTERCOM": "interfone",
|
||||
"LARGE_WINDOW": "janela-grande",
|
||||
"FURNISHED": "mobiliado",
|
||||
"PLANNED_FURNITURE": "mobiliado",
|
||||
"BACKYARD": "quintal",
|
||||
"CABLE_TV": "tv-a-cabo",
|
||||
"BALCONY": "varanda",
|
||||
"WALL_BALCONY": "varanda",
|
||||
"GOURMET_BALCONY": "varanda-gourmet",
|
||||
"BARBECUE_BALCONY": "varanda-gourmet",
|
||||
"NATURAL_VENTILATION": "ventilacao-natural",
|
||||
"PANORAMIC_VIEW": "vista-panoramica",
|
||||
"EXTERIOR_VIEW": "vista-panoramica",
|
||||
"GAS_SHOWER": "aquecedor-a-gas",
|
||||
"HEATING": "aquecimento-central",
|
||||
"BATHTUB": "banheira",
|
||||
"SERVICE_BATHROOM": "banheiro-de-servico",
|
||||
"COPA": "copa",
|
||||
"LUNCH_ROOM": "copa",
|
||||
"EMPLOYEE_DEPENDENCY": "dependencia-de-empregada",
|
||||
"DEPOSIT": "deposito",
|
||||
"EDICULE": "edicula",
|
||||
"SOLAR_ENERGY": "energia-solar",
|
||||
"PET_SPACE": "espaco-pet",
|
||||
"ENTRANCE_HALL": "hall-de-entrada",
|
||||
"HOME_OFFICE": "home-office",
|
||||
"CORNER_PROPERTY": "imovel-de-esquina",
|
||||
"SOUNDPROOFING": "isolamento-acustico",
|
||||
"ALUMINUM_WINDOW": "janela-de-aluminio",
|
||||
"LAVABO": "lavabo",
|
||||
"MEZZANINE": "mezanino",
|
||||
"WOOD_FLOOR": "piso-de-madeira",
|
||||
"LAMINATED_FLOOR": "piso-laminado",
|
||||
"VINYL_FLOOR": "piso-vinilico",
|
||||
"PORCELAIN": "porcelanato",
|
||||
"DINNER_ROOM": "sala-de-jantar",
|
||||
"PANTRY": "despensa",
|
||||
"SERVICE_ENTRANCE": "entrada-de-servico",
|
||||
"INTEGRATED_ENVIRONMENTS": "ambientes-integrados",
|
||||
# ── Lazer ─────────────────────────────────────────────────────────────────
|
||||
"FITNESS_ROOM": "academia",
|
||||
"GYM": "academia",
|
||||
"BAR": "bar",
|
||||
"CINEMA": "cinema",
|
||||
"BARBECUE_GRILL": "churrasqueira",
|
||||
"PIZZA_OVEN": "churrasqueira",
|
||||
"GOURMET_SPACE": "espaco-gourmet",
|
||||
"GREEN_SPACE": "espaco-verde-parque",
|
||||
"RECREATION_AREA": "espaco-verde-parque",
|
||||
"WHIRLPOOL": "hidromassagem",
|
||||
"GARDEN": "jardim",
|
||||
"POOL": "piscina",
|
||||
"PRIVATE_POOL": "piscina",
|
||||
"ADULT_POOL": "piscina",
|
||||
"HEATED_POOL": "piscina",
|
||||
"CHILDRENS_POOL": "piscina",
|
||||
"PLAYGROUND": "playground",
|
||||
"TOYS_PLACE": "playground",
|
||||
"SPORTS_COURT": "quadra-poliesportiva",
|
||||
"FOOTBALL_FIELD": "quadra-poliesportiva",
|
||||
"GAMES_ROOM": "sala-de-jogos",
|
||||
"ADULT_GAME_ROOM": "sala-de-jogos",
|
||||
"YOUTH_GAME_ROOM": "sala-de-jogos",
|
||||
"TEEN_SPACE": "sala-de-jogos",
|
||||
"PARTY_HALL": "salao-de-festas",
|
||||
"SAUNA": "sauna",
|
||||
"SPA": "spa",
|
||||
"MASSAGE_ROOM": "spa",
|
||||
# ── Condomínio ────────────────────────────────────────────────────────────
|
||||
"DISABLED_ACCESS": "acesso-para-deficientes",
|
||||
"BICYCLES_PLACE": "bicicletario",
|
||||
"ELEVATOR": "elevador",
|
||||
"ELECTRIC_GENERATOR": "gerador-eletrico",
|
||||
"GRASS": "gramado",
|
||||
"LAUNDRY": "lavanderia",
|
||||
"RECEPTION": "recepcao",
|
||||
"MEETING_ROOM": "sala-de-reunioes",
|
||||
"COVENTION_HALL": "salao-de-convencoes",
|
||||
"GUEST_PARKING": "vaga-para-visitante",
|
||||
"RESTAURANT": "restaurante",
|
||||
# ── Segurança ─────────────────────────────────────────────────────────────
|
||||
"FENCE": "cerca-muro",
|
||||
"SAFETY_CIRCUIT": "circuito-de-seguranca",
|
||||
"SECURITY_CAMERA": "circuito-de-seguranca",
|
||||
"GATED_COMMUNITY": "condominio-fechado",
|
||||
"ELECTRONIC_GATE": "portao-eletronico",
|
||||
"CONCIERGE_24H": "portaria-24h",
|
||||
"ALARM_SYSTEM": "sistema-de-alarme",
|
||||
"WATCHMAN": "vigia",
|
||||
"PATROL": "vigia",
|
||||
"SECURITY_CABIN": "vigia",
|
||||
}
|
||||
|
||||
|
||||
# ── Helpers ────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def _decimal_or_none(value: str) -> Decimal | None:
|
||||
v = value.strip()
|
||||
if not v:
|
||||
return None
|
||||
try:
|
||||
return Decimal(v)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _int_or_zero(value: str) -> int:
|
||||
v = value.strip()
|
||||
if not v:
|
||||
return 0
|
||||
try:
|
||||
return int(float(v))
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
# ── Leitura dos CSVs ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def load_imoveis(csv_dir: str) -> list[dict]:
|
||||
path = os.path.join(csv_dir, "imoveis.csv")
|
||||
with open(path, encoding="utf-8", newline="") as f:
|
||||
return list(csv.DictReader(f))
|
||||
|
||||
|
||||
def load_fotos(csv_dir: str) -> dict[str, list[str]]:
|
||||
"""Retorna {id_imovel: [url_ordenada_por_indice]}"""
|
||||
path = os.path.join(csv_dir, "fotos.csv")
|
||||
grouped: dict[str, list[tuple[int, str]]] = defaultdict(list)
|
||||
with open(path, encoding="utf-8", newline="") as f:
|
||||
for row in csv.DictReader(f):
|
||||
grouped[row["id_imovel"]].append((int(row["indice"]), row["url"]))
|
||||
return {
|
||||
imovel_id: [url for _, url in sorted(fotos)]
|
||||
for imovel_id, fotos in grouped.items()
|
||||
}
|
||||
|
||||
|
||||
# ── Importação ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def importar(csv_dir: str) -> None:
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
imoveis = load_imoveis(csv_dir)
|
||||
fotos_map = load_fotos(csv_dir)
|
||||
|
||||
# ── Cidade Franca ─────────────────────────────────────────────────────
|
||||
city_slug = _slugify(CITY_NAME)
|
||||
city = City.query.filter_by(slug=city_slug).first()
|
||||
if not city:
|
||||
city = City(name=CITY_NAME, slug=city_slug, state=CITY_STATE)
|
||||
db.session.add(city)
|
||||
db.session.flush()
|
||||
print(f" Cidade criada: {CITY_NAME}")
|
||||
else:
|
||||
print(f" Cidade existente: {CITY_NAME}")
|
||||
|
||||
# ── Cache de bairros ──────────────────────────────────────────────────
|
||||
neighborhood_cache: dict[str, Neighborhood] = {
|
||||
nbh.slug: nbh for nbh in Neighborhood.query.filter_by(city_id=city.id).all()
|
||||
}
|
||||
|
||||
def get_or_create_neighborhood(name: str) -> Neighborhood:
|
||||
slug = _slugify(name)
|
||||
if slug not in neighborhood_cache:
|
||||
nbh = Neighborhood(name=name, slug=slug, city_id=city.id)
|
||||
db.session.add(nbh)
|
||||
db.session.flush()
|
||||
neighborhood_cache[slug] = nbh
|
||||
return neighborhood_cache[slug]
|
||||
|
||||
# ── Cache de subtipos ─────────────────────────────────────────────────
|
||||
subtype_cache: dict[str, PropertyType] = {
|
||||
pt.slug: pt for pt in PropertyType.query.all()
|
||||
}
|
||||
# ── Cache de amenidades ───────────────────────────────────────────────
|
||||
amenity_cache: dict[str, Amenity] = {a.slug: a for a in Amenity.query.all()}
|
||||
# ── Cache de imobiliárias (cria se não existir) ───────────────────────
|
||||
imob_cache: dict[str, Imobiliaria] = {
|
||||
i.name: i for i in Imobiliaria.query.all()
|
||||
}
|
||||
|
||||
def get_or_create_imobiliaria(name: str) -> Imobiliaria:
|
||||
if name not in imob_cache:
|
||||
imob = Imobiliaria(name=name, is_active=True, display_order=0)
|
||||
db.session.add(imob)
|
||||
db.session.flush()
|
||||
imob_cache[name] = imob
|
||||
print(f" Imobiliária criada: {name}")
|
||||
return imob_cache[name]
|
||||
|
||||
# ── Imóveis existentes (por code) ─────────────────────────────────────
|
||||
existing_codes: set[str] = {
|
||||
p.code for p in Property.query.with_entities(Property.code).all() if p.code
|
||||
}
|
||||
|
||||
created = 0
|
||||
skipped = 0
|
||||
|
||||
updated = 0
|
||||
|
||||
for row in imoveis:
|
||||
code = row["id"].strip()
|
||||
titulo = row["titulo"].strip()
|
||||
listing_type = "aluguel" if "alugar" in titulo.lower() else "venda"
|
||||
|
||||
if code in existing_codes:
|
||||
# Corrige o type de registros já importados
|
||||
rows_updated = (
|
||||
Property.query.filter_by(code=code)
|
||||
.update({"type": listing_type})
|
||||
)
|
||||
if rows_updated:
|
||||
updated += 1
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
# Bairro
|
||||
bairro_nome = row["bairro"].strip()
|
||||
nbh = get_or_create_neighborhood(bairro_nome) if bairro_nome else None
|
||||
|
||||
# Subtipo
|
||||
tipo_raw = row["tipo_imovel"].strip().lower()
|
||||
subtype_slug = SUBTYPE_MAP.get(tipo_raw, "casa")
|
||||
subtype = subtype_cache.get(subtype_slug)
|
||||
if subtype is None:
|
||||
# Fallback: pega qualquer subtipo residencial
|
||||
subtype = subtype_cache.get("casa")
|
||||
base_slug = _slugify(titulo)[:200]
|
||||
slug = base_slug
|
||||
suffix = 1
|
||||
while Property.query.filter_by(slug=slug).first():
|
||||
slug = f"{base_slug}-{suffix}"
|
||||
suffix += 1
|
||||
|
||||
# Amenidades
|
||||
amenidade_raw = row.get("amenidades", "").strip()
|
||||
prop_amenities: list[Amenity] = []
|
||||
if amenidade_raw:
|
||||
for code_en in amenidade_raw.split("|"):
|
||||
slug_pt = AMENITY_MAP.get(code_en.strip())
|
||||
if slug_pt and slug_pt in amenity_cache:
|
||||
amenity = amenity_cache[slug_pt]
|
||||
if amenity not in prop_amenities:
|
||||
prop_amenities.append(amenity)
|
||||
|
||||
# Imobiliária
|
||||
imob_name = row.get("imobiliaria", "").strip()
|
||||
imob = get_or_create_imobiliaria(imob_name) if imob_name else None
|
||||
|
||||
listing_type = "aluguel" if "alugar" in titulo.lower() else "venda"
|
||||
|
||||
prop = Property(
|
||||
title=titulo,
|
||||
slug=slug,
|
||||
code=code,
|
||||
description=row["descricao"].strip() or None,
|
||||
address=row["endereco"].strip() or None,
|
||||
price=Decimal(row["preco_aluguel"].strip()),
|
||||
condo_fee=_decimal_or_none(row["condominio"]),
|
||||
iptu_anual=_decimal_or_none(row["iptu"]),
|
||||
type=listing_type,
|
||||
subtype_id=subtype.id if subtype else None,
|
||||
city_id=city.id,
|
||||
neighborhood_id=nbh.id if nbh else None,
|
||||
bedrooms=_int_or_zero(row["quartos"]),
|
||||
bathrooms=_int_or_zero(row["banheiros"]),
|
||||
parking_spots=_int_or_zero(row["vagas_garagem"]),
|
||||
area_m2=_int_or_zero(row["area_m2"]) or 1,
|
||||
is_featured=False,
|
||||
is_active=True,
|
||||
imobiliaria_id=imob.id if imob else None,
|
||||
)
|
||||
prop.amenities = prop_amenities
|
||||
db.session.add(prop)
|
||||
db.session.flush()
|
||||
|
||||
# Fotos
|
||||
for order, url in enumerate(fotos_map.get(code, [])):
|
||||
db.session.add(
|
||||
PropertyPhoto(
|
||||
property_id=prop.id,
|
||||
url=url,
|
||||
alt_text=titulo,
|
||||
display_order=order,
|
||||
)
|
||||
)
|
||||
|
||||
existing_codes.add(code)
|
||||
created += 1
|
||||
|
||||
db.session.commit()
|
||||
print(
|
||||
f"\nImportação concluída: {created} imóveis criados, "
|
||||
f"{updated} tipo(s) atualizado(s), {skipped - updated} já existiam sem alteração."
|
||||
)
|
||||
|
||||
|
||||
# ── Entrypoint ─────────────────────────────────────────────────────────────────
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Importa imóveis do aluguel_helper.")
|
||||
parser.add_argument(
|
||||
"--csv-dir",
|
||||
default=os.path.join(os.path.dirname(os.path.abspath(__file__)), "data"),
|
||||
help="Diretório contendo imoveis.csv e fotos.csv",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
csv_dir = os.path.abspath(args.csv_dir)
|
||||
print(f"==> Lendo CSVs de: {csv_dir}")
|
||||
importar(csv_dir)
|
||||
798
backend/seeds/seed.py
Normal file
798
backend/seeds/seed.py
Normal file
|
|
@ -0,0 +1,798 @@
|
|||
"""
|
||||
Popula o banco com dados de exemplo para desenvolvimento.
|
||||
Uso: cd backend && uv run python seeds/seed.py
|
||||
Idempotente: apaga e recria tudo a cada execução.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import unicodedata
|
||||
import re
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import bcrypt
|
||||
|
||||
from app import create_app
|
||||
from app.extensions import db
|
||||
from app.models.homepage import HomepageConfig
|
||||
from app.models.property import Property, PropertyPhoto
|
||||
from app.models.catalog import Amenity, PropertyType
|
||||
from app.models.location import City, Neighborhood
|
||||
from app.models.user import ClientUser
|
||||
from app.models.agent import Agent
|
||||
|
||||
|
||||
# ── Property Types ────────────────────────────────────────────────────────────
|
||||
|
||||
PROPERTY_TYPES = [
|
||||
{
|
||||
"name": "Residencial",
|
||||
"slug": "residencial",
|
||||
"subtypes": [
|
||||
{"name": "Apartamento", "slug": "apartamento"},
|
||||
{"name": "Studio", "slug": "studio"},
|
||||
{"name": "Kitnet", "slug": "kitnet"},
|
||||
{"name": "Casa", "slug": "casa"},
|
||||
{"name": "Casa de Condomínio", "slug": "casa-de-condominio"},
|
||||
{"name": "Casa de Vila", "slug": "casa-de-vila"},
|
||||
{"name": "Cobertura", "slug": "cobertura"},
|
||||
{"name": "Flat", "slug": "flat"},
|
||||
{"name": "Loft", "slug": "loft"},
|
||||
{"name": "Terreno / Lote / Condomínio", "slug": "terreno-lote-condominio"},
|
||||
{"name": "Sobrado", "slug": "sobrado"},
|
||||
{"name": "Fazenda / Sítio / Chácara", "slug": "fazenda-sitio-chacara"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Comercial",
|
||||
"slug": "comercial",
|
||||
"subtypes": [
|
||||
{
|
||||
"name": "Loja / Salão / Ponto Comercial",
|
||||
"slug": "loja-salao-ponto-comercial",
|
||||
},
|
||||
{"name": "Conjunto Comercial / Sala", "slug": "conjunto-comercial-sala"},
|
||||
{"name": "Casa Comercial", "slug": "casa-comercial"},
|
||||
{"name": "Hotel / Motel / Pousada", "slug": "hotel-motel-pousada"},
|
||||
{"name": "Andar / Laje Corporativa", "slug": "andar-laje-corporativa"},
|
||||
{"name": "Prédio Inteiro", "slug": "predio-inteiro"},
|
||||
{"name": "Terrenos / Lotes Comerciais", "slug": "terreno-lote-comercial"},
|
||||
{"name": "Galpão / Depósito / Armazém", "slug": "galpao-deposito-armazem"},
|
||||
{"name": "Garagem", "slug": "garagem"},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
# ── Amenities ────────────────────────────────────────────────────────────────
|
||||
|
||||
AMENITIES: dict[str, list[str]] = {
|
||||
"caracteristica": [
|
||||
"Aceita animais",
|
||||
"Aquecedor a gás",
|
||||
"Aquecimento central",
|
||||
"Ar-condicionado",
|
||||
"Área de serviço",
|
||||
"Armário embutido",
|
||||
"Armário embutido no quarto",
|
||||
"Armário na cozinha",
|
||||
"Armário no banheiro",
|
||||
"Banheira",
|
||||
"Banheiro de serviço",
|
||||
"Box blindex",
|
||||
"Closet",
|
||||
"Conexão à internet",
|
||||
"Copa",
|
||||
"Cozinha americana",
|
||||
"Cozinha gourmet",
|
||||
"Dependência de empregada",
|
||||
"Depósito",
|
||||
"Despensa",
|
||||
"Edícula",
|
||||
"Energia solar",
|
||||
"Entrada de serviço",
|
||||
"Espaço pet",
|
||||
"Fogão",
|
||||
"Hall de entrada",
|
||||
"Home office",
|
||||
"Imóvel de esquina",
|
||||
"Interfone",
|
||||
"Ambientes integrados",
|
||||
"Isolamento acústico",
|
||||
"Janela de alumínio",
|
||||
"Janela grande",
|
||||
"Lavabo",
|
||||
"Mezanino",
|
||||
"Mobiliado",
|
||||
"Piso de madeira",
|
||||
"Piso laminado",
|
||||
"Piso vinílico",
|
||||
"Porcelanato",
|
||||
"Quintal",
|
||||
"Sala de jantar",
|
||||
"TV a cabo",
|
||||
"Varanda",
|
||||
"Varanda gourmet",
|
||||
"Ventilação natural",
|
||||
"Vista panorâmica",
|
||||
],
|
||||
"lazer": [
|
||||
"Academia",
|
||||
"Bar",
|
||||
"Cinema",
|
||||
"Churrasqueira",
|
||||
"Espaço gourmet",
|
||||
"Espaço verde / Parque",
|
||||
"Hidromassagem",
|
||||
"Jardim",
|
||||
"Piscina",
|
||||
"Playground",
|
||||
"Quadra poliesportiva",
|
||||
"Sala de jogos",
|
||||
"Salão de festas",
|
||||
"Sauna",
|
||||
"Spa",
|
||||
],
|
||||
"condominio": [
|
||||
"Acesso para deficientes",
|
||||
"Bicicletário",
|
||||
"Restaurante",
|
||||
"Elevador",
|
||||
"Gerador elétrico",
|
||||
"Gramado",
|
||||
"Lavanderia",
|
||||
"Recepção",
|
||||
"Sala de reuniões",
|
||||
"Salão de convenções",
|
||||
"Vaga para visitante",
|
||||
],
|
||||
"seguranca": [
|
||||
"Cerca / Muro",
|
||||
"Circuito de segurança",
|
||||
"Condomínio fechado",
|
||||
"Portão eletrônico",
|
||||
"Portaria 24h",
|
||||
"Sistema de alarme",
|
||||
"Vigia",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
# ── Agents ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
SAMPLE_AGENTS = [
|
||||
{
|
||||
"name": "Carlos Eduardo Mota",
|
||||
"photo_url": "https://randomuser.me/api/portraits/men/32.jpg",
|
||||
"creci": "123456-F",
|
||||
"email": "carlos.mota@imobiliaria.com",
|
||||
"phone": "(11) 98765-4321",
|
||||
"bio": "Especialista em imóveis residenciais de alto padrão na Grande São Paulo. Mais de 10 anos de experiência.",
|
||||
"is_active": True,
|
||||
"display_order": 1,
|
||||
},
|
||||
{
|
||||
"name": "Fernanda Lima Souza",
|
||||
"photo_url": "https://randomuser.me/api/portraits/women/44.jpg",
|
||||
"creci": "234567-F",
|
||||
"email": "fernanda.lima@imobiliaria.com",
|
||||
"phone": "(11) 97654-3210",
|
||||
"bio": "Formada em Administração com pós em Mercado Imobiliário. Foco em imóveis comerciais e corporativos.",
|
||||
"is_active": True,
|
||||
"display_order": 2,
|
||||
},
|
||||
{
|
||||
"name": "Ricardo Alves Pereira",
|
||||
"photo_url": "https://randomuser.me/api/portraits/men/51.jpg",
|
||||
"creci": "345678-F",
|
||||
"email": "ricardo.alves@imobiliaria.com",
|
||||
"phone": "(11) 96543-2109",
|
||||
"bio": "Especializado em lançamentos e imóveis na planta. Parceiro dos principais construtores da região.",
|
||||
"is_active": True,
|
||||
"display_order": 3,
|
||||
},
|
||||
{
|
||||
"name": "Juliana Nascimento",
|
||||
"photo_url": "https://randomuser.me/api/portraits/women/68.jpg",
|
||||
"creci": "456789-F",
|
||||
"email": "juliana.nascimento@imobiliaria.com",
|
||||
"phone": "(11) 95432-1098",
|
||||
"bio": "Apaixonada por conectar famílias ao imóvel perfeito. Especialista em Alphaville e Tamboré.",
|
||||
"is_active": True,
|
||||
"display_order": 4,
|
||||
},
|
||||
{
|
||||
"name": "Marcos Vinícius Costa",
|
||||
"photo_url": "https://randomuser.me/api/portraits/men/77.jpg",
|
||||
"creci": "567890-F",
|
||||
"email": "marcos.costa@imobiliaria.com",
|
||||
"phone": "(11) 94321-0987",
|
||||
"bio": "Corretor com expertise em locações comerciais e residenciais. Atendimento 100% personalizado.",
|
||||
"is_active": True,
|
||||
"display_order": 5,
|
||||
},
|
||||
{
|
||||
"name": "Patrícia Rodrigues",
|
||||
"photo_url": "https://randomuser.me/api/portraits/women/85.jpg",
|
||||
"creci": "678901-F",
|
||||
"email": "patricia.rodrigues@imobiliaria.com",
|
||||
"phone": "(11) 93210-9876",
|
||||
"bio": "Referência em imóveis de luxo. Fluente em inglês e espanhol para atendimento internacional.",
|
||||
"is_active": True,
|
||||
"display_order": 6,
|
||||
},
|
||||
{
|
||||
"name": "Bruno Henrique Ferreira",
|
||||
"photo_url": "https://randomuser.me/api/portraits/men/22.jpg",
|
||||
"creci": "789012-F",
|
||||
"email": "bruno.ferreira@imobiliaria.com",
|
||||
"phone": "(11) 92109-8765",
|
||||
"bio": "Especialista em avaliação de imóveis e consultoria de investimentos imobiliários.",
|
||||
"is_active": True,
|
||||
"display_order": 7,
|
||||
},
|
||||
{
|
||||
"name": "Aline Mendes Torres",
|
||||
"photo_url": "https://randomuser.me/api/portraits/women/12.jpg",
|
||||
"creci": "890123-F",
|
||||
"email": "aline.mendes@imobiliaria.com",
|
||||
"phone": "(11) 91098-7654",
|
||||
"bio": "Dedicada ao mercado de locação residencial. Processo ágil e desburocratizado do início ao fim.",
|
||||
"is_active": True,
|
||||
"display_order": 8,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# ── Cities & Neighborhoods ─────────────────────────────────────────────────────
|
||||
|
||||
LOCATIONS = [
|
||||
{
|
||||
"name": "São Paulo",
|
||||
"slug": "sao-paulo",
|
||||
"state": "SP",
|
||||
"neighborhoods": [
|
||||
{"name": "Centro", "slug": "centro"},
|
||||
{"name": "Jardim Primavera", "slug": "jardim-primavera"},
|
||||
{"name": "Vila Nova", "slug": "vila-nova"},
|
||||
{"name": "Pinheiros", "slug": "pinheiros"},
|
||||
{"name": "Itaim Bibi", "slug": "itaim-bibi"},
|
||||
{"name": "Vila Olímpia", "slug": "vila-olimpia"},
|
||||
{"name": "Centro Histórico", "slug": "centro-historico"},
|
||||
{"name": "Zona Norte", "slug": "zona-norte"},
|
||||
{"name": "Alto da Boa Vista", "slug": "alto-da-boa-vista"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Rio de Janeiro",
|
||||
"slug": "rio-de-janeiro",
|
||||
"state": "RJ",
|
||||
"neighborhoods": [
|
||||
{"name": "Praia Grande", "slug": "praia-grande"},
|
||||
{"name": "Copacabana", "slug": "copacabana"},
|
||||
{"name": "Ipanema", "slug": "ipanema"},
|
||||
{"name": "Barra da Tijuca", "slug": "barra-da-tijuca"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Campinas",
|
||||
"slug": "campinas",
|
||||
"state": "SP",
|
||||
"neighborhoods": [
|
||||
{"name": "Alphaville", "slug": "alphaville"},
|
||||
{"name": "Universitário", "slug": "universitario"},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# ── Sample Properties ─────────────────────────────────────────────────────────
|
||||
|
||||
SAMPLE_PROPERTIES = [
|
||||
{
|
||||
"title": "Apartamento 3 quartos — Centro",
|
||||
"slug": "apartamento-3-quartos-centro",
|
||||
"address": "Rua das Flores, 123 — Centro",
|
||||
"code": "SP-001",
|
||||
"description": "Excelente apartamento localizado no coração do Centro, com ótima iluminação natural e acabamento de qualidade. O imóvel conta com sala ampla, cozinha bem distribuída e quartos espaçosos com armários embutidos.\n\nPróximo a comércio, transporte público e todas as facilidades urbanas. Condomínio completo com elevador e portaria. Não perca esta oportunidade!",
|
||||
"price": "750000.00",
|
||||
"condo_fee": "800.00",
|
||||
"type": "venda",
|
||||
"subtype_slug": "apartamento",
|
||||
"city_slug": "sao-paulo",
|
||||
"neighborhood_slug": "centro",
|
||||
"bedrooms": 3,
|
||||
"bathrooms": 2,
|
||||
"parking_spots": 1,
|
||||
"area_m2": 98,
|
||||
"is_featured": True,
|
||||
"is_active": True,
|
||||
"amenity_slugs": [
|
||||
"ar-condicionado",
|
||||
"armario-embutido",
|
||||
"interfone",
|
||||
"elevador",
|
||||
],
|
||||
"photos": [
|
||||
{
|
||||
"url": "https://picsum.photos/seed/apt1/800/450",
|
||||
"alt_text": "Sala de estar ampla com varanda",
|
||||
"display_order": 0,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Casa 4 quartos — Jardim Primavera",
|
||||
"slug": "casa-4-quartos-jardim-primavera",
|
||||
"address": "Av. das Palmeiras, 456 — Jardim Primavera",
|
||||
"price": "1250000.00",
|
||||
"condo_fee": None,
|
||||
"type": "venda",
|
||||
"subtype_slug": "casa",
|
||||
"city_slug": "sao-paulo",
|
||||
"neighborhood_slug": "jardim-primavera",
|
||||
"bedrooms": 4,
|
||||
"bathrooms": 3,
|
||||
"parking_spots": 2,
|
||||
"area_m2": 220,
|
||||
"is_featured": True,
|
||||
"is_active": True,
|
||||
"amenity_slugs": ["piscina", "churrasqueira", "jardim", "quintal"],
|
||||
"photos": [
|
||||
{
|
||||
"url": "https://picsum.photos/seed/house2/800/450",
|
||||
"alt_text": "Fachada da casa com jardim",
|
||||
"display_order": 0,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Studio moderno — Vila Nova",
|
||||
"slug": "studio-moderno-vila-nova",
|
||||
"address": "Rua dos Ipês, 789 — Vila Nova",
|
||||
"price": "2800.00",
|
||||
"condo_fee": "350.00",
|
||||
"type": "aluguel",
|
||||
"subtype_slug": "studio",
|
||||
"city_slug": "sao-paulo",
|
||||
"neighborhood_slug": "vila-nova",
|
||||
"bedrooms": 1,
|
||||
"bathrooms": 1,
|
||||
"parking_spots": 0,
|
||||
"area_m2": 38,
|
||||
"is_featured": True,
|
||||
"is_active": True,
|
||||
"amenity_slugs": ["cozinha-americana", "mobiliado", "conexao-a-internet"],
|
||||
"photos": [
|
||||
{
|
||||
"url": "https://picsum.photos/seed/studio3/800/450",
|
||||
"alt_text": "Ambiente integrado do studio",
|
||||
"display_order": 0,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Cobertura duplex — Beira Mar",
|
||||
"slug": "cobertura-duplex-beira-mar",
|
||||
"address": "Av. Beira Mar, 1000 — Praia Grande",
|
||||
"price": "3200000.00",
|
||||
"condo_fee": "1500.00",
|
||||
"type": "venda",
|
||||
"subtype_slug": "cobertura",
|
||||
"city_slug": "rio-de-janeiro",
|
||||
"neighborhood_slug": "praia-grande",
|
||||
"bedrooms": 5,
|
||||
"bathrooms": 4,
|
||||
"parking_spots": 3,
|
||||
"area_m2": 380,
|
||||
"is_featured": True,
|
||||
"is_active": True,
|
||||
"amenity_slugs": [
|
||||
"piscina",
|
||||
"vista-panoramica",
|
||||
"varanda-gourmet",
|
||||
"cozinha-gourmet",
|
||||
"closet",
|
||||
],
|
||||
"photos": [
|
||||
{
|
||||
"url": "https://picsum.photos/seed/cobertura4/800/450",
|
||||
"alt_text": "Vista da cobertura para o mar",
|
||||
"display_order": 0,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Kitnet — Próximo à Universidade",
|
||||
"slug": "kitnet-proximo-universidade",
|
||||
"address": "Rua Estudante, 50 — Universitário",
|
||||
"price": "1500.00",
|
||||
"condo_fee": "200.00",
|
||||
"type": "aluguel",
|
||||
"subtype_slug": "kitnet",
|
||||
"city_slug": "campinas",
|
||||
"neighborhood_slug": "universitario",
|
||||
"bedrooms": 1,
|
||||
"bathrooms": 1,
|
||||
"parking_spots": 0,
|
||||
"area_m2": 25,
|
||||
"is_featured": True,
|
||||
"is_active": True,
|
||||
"amenity_slugs": ["mobiliado", "conexao-a-internet", "fogao"],
|
||||
"photos": [
|
||||
{
|
||||
"url": "https://picsum.photos/seed/kitnet5/800/450",
|
||||
"alt_text": "Interior da kitnet mobiliada",
|
||||
"display_order": 0,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Sobrado 3 quartos — Bairro Nobre",
|
||||
"slug": "sobrado-3-quartos-bairro-nobre",
|
||||
"address": "Rua Nobreza, 321 — Alto da Boa Vista",
|
||||
"price": "890000.00",
|
||||
"condo_fee": None,
|
||||
"type": "venda",
|
||||
"subtype_slug": "sobrado",
|
||||
"city_slug": "sao-paulo",
|
||||
"neighborhood_slug": "alto-da-boa-vista",
|
||||
"bedrooms": 3,
|
||||
"bathrooms": 2,
|
||||
"parking_spots": 2,
|
||||
"area_m2": 160,
|
||||
"is_featured": True,
|
||||
"is_active": True,
|
||||
"amenity_slugs": [
|
||||
"churrasqueira",
|
||||
"jardim",
|
||||
"portao-eletronico",
|
||||
"sistema-de-alarme",
|
||||
],
|
||||
"photos": [
|
||||
{
|
||||
"url": "https://picsum.photos/seed/sobrado6/800/450",
|
||||
"alt_text": "Fachada do sobrado com garagem",
|
||||
"display_order": 0,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Loft industrial — Centro Histórico",
|
||||
"slug": "loft-industrial-centro-historico",
|
||||
"address": "Rua da Consolação, 200 — Centro Histórico",
|
||||
"price": "4200.00",
|
||||
"condo_fee": "500.00",
|
||||
"type": "aluguel",
|
||||
"subtype_slug": "loft",
|
||||
"city_slug": "sao-paulo",
|
||||
"neighborhood_slug": "centro-historico",
|
||||
"bedrooms": 1,
|
||||
"bathrooms": 1,
|
||||
"parking_spots": 1,
|
||||
"area_m2": 65,
|
||||
"is_featured": False,
|
||||
"is_active": True,
|
||||
"amenity_slugs": [
|
||||
"ar-condicionado",
|
||||
"mobiliado",
|
||||
"conexao-a-internet",
|
||||
"elevador",
|
||||
],
|
||||
"photos": [
|
||||
{
|
||||
"url": "https://picsum.photos/seed/loft7/800/450",
|
||||
"alt_text": "Interior do loft",
|
||||
"display_order": 0,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Apartamento 2 quartos — Pinheiros",
|
||||
"slug": "apartamento-2-quartos-pinheiros",
|
||||
"address": "Rua Teodoro Sampaio, 450 — Pinheiros",
|
||||
"price": "580000.00",
|
||||
"condo_fee": "650.00",
|
||||
"type": "venda",
|
||||
"subtype_slug": "apartamento",
|
||||
"city_slug": "sao-paulo",
|
||||
"neighborhood_slug": "pinheiros",
|
||||
"bedrooms": 2,
|
||||
"bathrooms": 1,
|
||||
"parking_spots": 1,
|
||||
"area_m2": 72,
|
||||
"is_featured": False,
|
||||
"is_active": True,
|
||||
"amenity_slugs": [
|
||||
"ar-condicionado",
|
||||
"varanda",
|
||||
"interfone",
|
||||
"academia",
|
||||
"elevador",
|
||||
],
|
||||
"photos": [
|
||||
{
|
||||
"url": "https://picsum.photos/seed/apt8/800/450",
|
||||
"alt_text": "Sala com varanda",
|
||||
"display_order": 0,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Casa de Condomínio — Alphaville",
|
||||
"slug": "casa-condominio-alphaville",
|
||||
"address": "Alameda Itu, 10 — Alphaville",
|
||||
"price": "2100000.00",
|
||||
"condo_fee": "1200.00",
|
||||
"type": "venda",
|
||||
"subtype_slug": "casa-de-condominio",
|
||||
"city_slug": "campinas",
|
||||
"neighborhood_slug": "alphaville",
|
||||
"bedrooms": 4,
|
||||
"bathrooms": 4,
|
||||
"parking_spots": 4,
|
||||
"area_m2": 350,
|
||||
"is_featured": False,
|
||||
"is_active": True,
|
||||
"amenity_slugs": [
|
||||
"piscina",
|
||||
"churrasqueira",
|
||||
"salao-de-festas",
|
||||
"portaria-24h",
|
||||
"condominio-fechado",
|
||||
"playground",
|
||||
],
|
||||
"photos": [
|
||||
{
|
||||
"url": "https://picsum.photos/seed/cond9/800/450",
|
||||
"alt_text": "Área de lazer do condomínio",
|
||||
"display_order": 0,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Flat executivo — Itaim Bibi",
|
||||
"slug": "flat-executivo-itaim-bibi",
|
||||
"address": "Av. Faria Lima, 3000 — Itaim Bibi",
|
||||
"price": "6500.00",
|
||||
"condo_fee": "800.00",
|
||||
"type": "aluguel",
|
||||
"subtype_slug": "flat",
|
||||
"city_slug": "sao-paulo",
|
||||
"neighborhood_slug": "itaim-bibi",
|
||||
"bedrooms": 1,
|
||||
"bathrooms": 1,
|
||||
"parking_spots": 1,
|
||||
"area_m2": 45,
|
||||
"is_featured": False,
|
||||
"is_active": True,
|
||||
"amenity_slugs": [
|
||||
"ar-condicionado",
|
||||
"mobiliado",
|
||||
"recepcao",
|
||||
"academia",
|
||||
"elevador",
|
||||
"portaria-24h",
|
||||
],
|
||||
"photos": [
|
||||
{
|
||||
"url": "https://picsum.photos/seed/flat10/800/450",
|
||||
"alt_text": "Suite executiva",
|
||||
"display_order": 0,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Terreno 600m² — Zona Norte",
|
||||
"slug": "terreno-600m2-zona-norte",
|
||||
"address": "Estrada das Amendoeiras, s/n — Zona Norte",
|
||||
"price": "320000.00",
|
||||
"condo_fee": None,
|
||||
"type": "venda",
|
||||
"subtype_slug": "terreno-lote-condominio",
|
||||
"city_slug": "sao-paulo",
|
||||
"neighborhood_slug": "zona-norte",
|
||||
"bedrooms": 0,
|
||||
"bathrooms": 0,
|
||||
"parking_spots": 0,
|
||||
"area_m2": 600,
|
||||
"is_featured": False,
|
||||
"is_active": True,
|
||||
"amenity_slugs": [],
|
||||
"photos": [
|
||||
{
|
||||
"url": "https://picsum.photos/seed/terreno11/800/450",
|
||||
"alt_text": "Terreno plano",
|
||||
"display_order": 0,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Conjunto Comercial 80m² — Vila Olímpia",
|
||||
"slug": "conjunto-comercial-80m2-vila-olimpia",
|
||||
"address": "Rua Funchal, 500 — Vila Olímpia",
|
||||
"price": "8000.00",
|
||||
"condo_fee": "1800.00",
|
||||
"type": "aluguel",
|
||||
"subtype_slug": "conjunto-comercial-sala",
|
||||
"city_slug": "sao-paulo",
|
||||
"neighborhood_slug": "vila-olimpia",
|
||||
"bedrooms": 0,
|
||||
"bathrooms": 2,
|
||||
"parking_spots": 2,
|
||||
"area_m2": 80,
|
||||
"is_featured": False,
|
||||
"is_active": True,
|
||||
"amenity_slugs": ["ar-condicionado", "recepcao", "elevador", "portaria-24h"],
|
||||
"photos": [
|
||||
{
|
||||
"url": "https://picsum.photos/seed/comercial12/800/450",
|
||||
"alt_text": "Sala comercial",
|
||||
"display_order": 0,
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def seed() -> None:
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
# ── Wipe existing data (FK order) ──────────────────────────────────────
|
||||
db.session.execute(db.text("DELETE FROM property_amenity"))
|
||||
db.session.query(PropertyPhoto).delete()
|
||||
db.session.query(Property).delete()
|
||||
db.session.query(Neighborhood).delete()
|
||||
db.session.query(City).delete()
|
||||
db.session.query(Amenity).delete()
|
||||
# Delete subtypes before parents to satisfy self-referential FK
|
||||
db.session.query(PropertyType).filter(
|
||||
PropertyType.parent_id.isnot(None)
|
||||
).delete()
|
||||
db.session.query(PropertyType).delete()
|
||||
db.session.query(HomepageConfig).delete()
|
||||
db.session.commit()
|
||||
|
||||
# ── Property Types ──────────────────────────────────────────────────────
|
||||
subtype_map: dict[str, PropertyType] = {}
|
||||
for cat_data in PROPERTY_TYPES:
|
||||
cat = PropertyType(name=cat_data["name"], slug=cat_data["slug"])
|
||||
db.session.add(cat)
|
||||
db.session.flush()
|
||||
for sub_data in cat_data["subtypes"]:
|
||||
sub = PropertyType(
|
||||
name=sub_data["name"], slug=sub_data["slug"], parent_id=cat.id
|
||||
)
|
||||
db.session.add(sub)
|
||||
db.session.flush()
|
||||
subtype_map[sub.slug] = sub
|
||||
|
||||
# ── Amenities ───────────────────────────────────────────────────────────
|
||||
amenity_map: dict[str, Amenity] = {}
|
||||
for group, names in AMENITIES.items():
|
||||
for name in names:
|
||||
slug = _slugify(name)
|
||||
amenity = Amenity(name=name, slug=slug, group=group)
|
||||
db.session.add(amenity)
|
||||
db.session.flush()
|
||||
amenity_map[slug] = amenity
|
||||
|
||||
# ── Homepage Config ─────────────────────────────────────────────────────
|
||||
db.session.add(
|
||||
HomepageConfig(
|
||||
hero_headline="Encontre o imóvel dos seus sonhos",
|
||||
hero_subheadline="Mais de 200 imóveis disponíveis em toda a região",
|
||||
hero_cta_label="Ver Imóveis",
|
||||
hero_cta_url="/imoveis",
|
||||
featured_properties_limit=6,
|
||||
)
|
||||
)
|
||||
|
||||
# ── Cities & Neighborhoods ────────────────────────────────────────────
|
||||
city_map: dict[str, City] = {}
|
||||
neighborhood_map: dict[str, Neighborhood] = {}
|
||||
for loc_data in LOCATIONS:
|
||||
city = City(
|
||||
name=loc_data["name"],
|
||||
slug=loc_data["slug"],
|
||||
state=loc_data["state"],
|
||||
)
|
||||
db.session.add(city)
|
||||
db.session.flush()
|
||||
city_map[city.slug] = city
|
||||
for nbh_data in loc_data["neighborhoods"]:
|
||||
nbh = Neighborhood(
|
||||
name=nbh_data["name"],
|
||||
slug=nbh_data["slug"],
|
||||
city_id=city.id,
|
||||
)
|
||||
db.session.add(nbh)
|
||||
db.session.flush()
|
||||
neighborhood_map[f"{city.slug}/{nbh.slug}"] = nbh
|
||||
|
||||
# ── Properties ──────────────────────────────────────────────────────────
|
||||
for data in SAMPLE_PROPERTIES:
|
||||
amenity_slugs: list[str] = data.pop("amenity_slugs")
|
||||
photos_data: list[dict] = data.pop("photos")
|
||||
subtype_slug: str = data.pop("subtype_slug")
|
||||
city_slug: str | None = data.pop("city_slug", None)
|
||||
neighborhood_slug: str | None = data.pop("neighborhood_slug", None)
|
||||
|
||||
city_id = city_map[city_slug].id if city_slug else None
|
||||
nbh_key = (
|
||||
f"{city_slug}/{neighborhood_slug}"
|
||||
if city_slug and neighborhood_slug
|
||||
else None
|
||||
)
|
||||
neighborhood_id = neighborhood_map[nbh_key].id if nbh_key else None
|
||||
|
||||
prop = Property(
|
||||
**data,
|
||||
subtype_id=subtype_map[subtype_slug].id,
|
||||
city_id=city_id,
|
||||
neighborhood_id=neighborhood_id,
|
||||
)
|
||||
db.session.add(prop)
|
||||
db.session.flush()
|
||||
|
||||
for photo_data in photos_data:
|
||||
db.session.add(PropertyPhoto(property_id=prop.id, **photo_data))
|
||||
|
||||
for slug in amenity_slugs:
|
||||
if slug in amenity_map:
|
||||
prop.amenities.append(amenity_map[slug])
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# ── Admin user ──────────────────────────────────────────────────────────
|
||||
ADMIN_EMAIL = "admin@master.com"
|
||||
ADMIN_PASSWORD = "Hn84pFUgatYX"
|
||||
admin = ClientUser.query.filter_by(email=ADMIN_EMAIL).first()
|
||||
if not admin:
|
||||
admin = ClientUser(
|
||||
name="Admin",
|
||||
email=ADMIN_EMAIL,
|
||||
password_hash=bcrypt.hashpw(
|
||||
ADMIN_PASSWORD.encode(), bcrypt.gensalt()
|
||||
).decode(),
|
||||
role="admin",
|
||||
)
|
||||
db.session.add(admin)
|
||||
else:
|
||||
admin.password_hash = bcrypt.hashpw(
|
||||
ADMIN_PASSWORD.encode(), bcrypt.gensalt()
|
||||
).decode()
|
||||
admin.role = "admin"
|
||||
db.session.commit()
|
||||
print(f"Admin: {ADMIN_EMAIL}")
|
||||
|
||||
total_amenities = sum(len(v) for v in AMENITIES.values())
|
||||
total_types = sum(1 + len(c["subtypes"]) for c in PROPERTY_TYPES)
|
||||
total_cities = len(LOCATIONS)
|
||||
total_nbhs = sum(len(loc["neighborhoods"]) for loc in LOCATIONS)
|
||||
print(
|
||||
f"Seed concluído: {len(SAMPLE_PROPERTIES)} imóveis, "
|
||||
f"{total_types} tipos, {total_amenities} amenidades, "
|
||||
f"{total_cities} cidades, {total_nbhs} bairros."
|
||||
)
|
||||
|
||||
# ── Agents ─────────────────────────────────────────────────────────────
|
||||
db.session.query(Agent).delete()
|
||||
db.session.commit()
|
||||
for agent_data in SAMPLE_AGENTS:
|
||||
db.session.add(Agent(**agent_data))
|
||||
db.session.commit()
|
||||
print(f"Corretores seed: {len(SAMPLE_AGENTS)} cadastrados.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
seed()
|
||||
Loading…
Add table
Add a link
Reference in a new issue