import os import uuid from typing import Annotated, Optional from urllib.parse import urlencode from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile, File, Form from fastapi.responses import HTMLResponse, RedirectResponse from sqlalchemy.orm import Session from starlette import status from auth import get_db, get_current_user from config import templates from models import Property, PropertyImage, Favorite, User router = APIRouter(tags=["properties"]) PAGE_SIZE = 9 def _primary_image(prop): return next((img for img in prop.images if img.is_primary), prop.images[0] if prop.images else None) def _apply_image(props): for p in props: p.primary_image = _primary_image(p) return props @router.get("/properties", response_class=HTMLResponse) async def property_list(request: Request, db: Session = Depends(get_db)): search = request.query_params.get("search", "").strip() prop_type = request.query_params.get("type", "").strip() min_price = request.query_params.get("min_price", "").strip() max_price = request.query_params.get("max_price", "").strip() bedrooms = request.query_params.get("bedrooms", "").strip() city = request.query_params.get("city", "").strip() state = request.query_params.get("state", "").strip() page = int(request.query_params.get("page", 1)) query = db.query(Property).filter(Property.status == "active") if search: like = f"%{search}%" query = query.filter( (Property.title.ilike(like)) | (Property.city.ilike(like)) | (Property.state.ilike(like)) | (Property.zip_code.ilike(like)) | (Property.address.ilike(like)) ) if prop_type: query = query.filter(Property.property_type == prop_type) if min_price: query = query.filter(Property.price >= int(min_price)) if max_price: query = query.filter(Property.price <= int(max_price)) if bedrooms: query = query.filter(Property.bedrooms >= int(bedrooms)) if city: query = query.filter(Property.city.ilike(f"%{city}%")) if state: query = query.filter(Property.state.ilike(f"%{state}%")) total = query.count() total_pages = max(1, (total + PAGE_SIZE - 1) // PAGE_SIZE) page = max(1, min(page, total_pages)) properties = query.order_by(Property.created_at.desc()).offset((page - 1) * PAGE_SIZE).limit(PAGE_SIZE).all() _apply_image(properties) params = {} for k in ("search", "type", "min_price", "max_price", "bedrooms", "city", "state"): v = request.query_params.get(k, "") if v: params[k] = v query_string = urlencode(params) user = await get_current_user(request, None) return templates.TemplateResponse(request, "properties/list.html", { "user": user, "properties": properties, "total_pages": total_pages, "page": page, "query_string": query_string, "search": search, "prop_type": prop_type, "min_price": min_price, "max_price": max_price, "bedrooms": bedrooms, "city_filter": city, "state_filter": state, }) @router.get("/properties/new", response_class=HTMLResponse) async def property_create_form(request: Request, db: Session = Depends(get_db)): user = await get_current_user(request, None) if not user: return RedirectResponse(url="/auth/login", status_code=303) return templates.TemplateResponse(request, "properties/create.html", { "user": user, "values": {}, }) async def _save_property_image(image_file: UploadFile) -> str | None: if not image_file or not image_file.filename: return None ext = os.path.splitext(image_file.filename)[1].lower() if ext not in (".jpg", ".jpeg", ".png", ".webp"): return None upload_dir = "static/properties" os.makedirs(upload_dir, exist_ok=True) filename = f"{uuid.uuid4().hex}{ext}" filepath = os.path.join(upload_dir, filename) content = await image_file.read() if len(content) > 5 * 1024 * 1024: return None with open(filepath, "wb") as f: f.write(content) return f"properties/{filename}" @router.post("/properties/new") async def property_create_submit( request: Request, db: Session = Depends(get_db), title: str = Form(...), description: str = Form(...), price: int = Form(...), property_type: str = Form(...), bedrooms: int = Form(0), bathrooms: float = Form(0), area_sqft: int = Form(...), address: str = Form(...), city: str = Form(...), state: str = Form(...), zip_code: str = Form(...), year_built: int | None = Form(None), contact_email: str = Form(""), contact_phone: str = Form(""), image: UploadFile | None = File(None), ): user = await get_current_user(request, None) if not user: return RedirectResponse(url="/auth/login", status_code=303) contact_email = contact_email.strip() contact_phone = contact_phone.strip() if not contact_email and not contact_phone: return templates.TemplateResponse(request, "properties/create.html", { "user": user, "values": {k: v for k, v in request._form.items()} if hasattr(request, '_form') else {}, "error": "Please provide at least one contact method (email or phone).", }) prop = Property( title=title, description=description, price=price, property_type=property_type, bedrooms=bedrooms, bathrooms=bathrooms, area_sqft=area_sqft, address=address, city=city, state=state.upper(), zip_code=zip_code, year_built=year_built or None, contact_email=contact_email or None, contact_phone=contact_phone or None, owner_id=user["id"], ) db.add(prop) db.flush() image_path = await _save_property_image(image) if image_path: db.add(PropertyImage(image_path=image_path, is_primary=True, sort_order=0, property_id=prop.id)) db.commit() return RedirectResponse(url=f"/properties/{prop.id}", status_code=303) # --------------------------------------------------------------------------- # Parameterized routes — after all static paths # --------------------------------------------------------------------------- @router.get("/properties/{prop_id}", response_class=HTMLResponse) async def property_detail(prop_id: int, request: Request, db: Session = Depends(get_db)): prop = db.query(Property).filter(Property.id == prop_id).first() if not prop: raise HTTPException(status_code=404, detail="Property not found") primary_image = _primary_image(prop) images = sorted(prop.images, key=lambda img: img.sort_order) user = await get_current_user(request, None) is_favorited = False if user: is_favorited = db.query(Favorite).filter( Favorite.user_id == user["id"], Favorite.property_id == prop.id, ).first() is not None return templates.TemplateResponse(request, "properties/detail.html", { "user": user, "prop": prop, "primary_image": primary_image, "images": images, "is_favorited": is_favorited, }) @router.get("/properties/{prop_id}/edit", response_class=HTMLResponse) async def property_edit_form(prop_id: int, request: Request, db: Session = Depends(get_db)): user = await get_current_user(request, None) if not user: return RedirectResponse(url="/auth/login", status_code=303) prop = db.query(Property).filter(Property.id == prop_id).first() if not prop: raise HTTPException(status_code=404, detail="Property not found") if prop.owner_id != user["id"]: raise HTTPException(status_code=403, detail="Not authorized") primary_image = _primary_image(prop) return templates.TemplateResponse(request, "properties/edit.html", { "user": user, "prop": prop, "primary_image": primary_image, }) @router.post("/properties/{prop_id}/edit") async def property_edit_submit( prop_id: int, request: Request, db: Session = Depends(get_db), title: str = Form(...), description: str = Form(...), price: int = Form(...), property_type: str = Form(...), bedrooms: int = Form(0), bathrooms: float = Form(0), area_sqft: int = Form(...), address: str = Form(...), city: str = Form(...), state: str = Form(...), zip_code: str = Form(...), year_built: int | None = Form(None), status: str = Form("active"), contact_email: str = Form(""), contact_phone: str = Form(""), image: UploadFile | None = File(None), ): user = await get_current_user(request, None) if not user: return RedirectResponse(url="/auth/login", status_code=303) prop = db.query(Property).filter(Property.id == prop_id).first() if not prop: raise HTTPException(status_code=404, detail="Property not found") if prop.owner_id != user["id"]: raise HTTPException(status_code=403, detail="Not authorized") prop.title = title prop.description = description prop.price = price prop.property_type = property_type prop.bedrooms = bedrooms prop.bathrooms = bathrooms prop.area_sqft = area_sqft prop.address = address prop.city = city prop.state = state.upper() prop.zip_code = zip_code prop.year_built = year_built or None prop.contact_email = contact_email.strip() or None prop.contact_phone = contact_phone.strip() or None prop.status = status image_path = await _save_property_image(image) if image_path: for img in prop.images: old_path = os.path.join("static", img.image_path) if os.path.exists(old_path): os.remove(old_path) db.delete(img) db.flush() db.add(PropertyImage(image_path=image_path, is_primary=True, sort_order=0, property_id=prop.id)) db.commit() return RedirectResponse(url=f"/properties/{prop.id}", status_code=303) @router.post("/properties/{prop_id}/delete") async def property_delete(prop_id: int, request: Request, db: Session = Depends(get_db)): user = await get_current_user(request, None) if not user: return RedirectResponse(url="/auth/login", status_code=303) prop = db.query(Property).filter(Property.id == prop_id).first() if not prop: raise HTTPException(status_code=404, detail="Property not found") if prop.owner_id != user["id"]: raise HTTPException(status_code=403, detail="Not authorized") for img in prop.images: old_path = os.path.join("static", img.image_path) if os.path.exists(old_path): os.remove(old_path) db.delete(prop) db.commit() return RedirectResponse(url="/dashboard", status_code=303) @router.post("/properties/{prop_id}/favorite") async def toggle_favorite(prop_id: int, request: Request, db: Session = Depends(get_db)): user = await get_current_user(request, None) if not user: return RedirectResponse(url="/auth/login", status_code=303) existing = db.query(Favorite).filter( Favorite.user_id == user["id"], Favorite.property_id == prop_id, ).first() if existing: db.delete(existing) else: db.add(Favorite(user_id=user["id"], property_id=prop_id)) db.commit() return RedirectResponse(url=f"/properties/{prop_id}", status_code=303) @router.get("/dashboard", response_class=HTMLResponse) async def dashboard(request: Request, db: Session = Depends(get_db)): user = await get_current_user(request, None) if not user: return RedirectResponse(url="/auth/login", status_code=303) my_properties = ( db.query(Property) .filter(Property.owner_id == user["id"]) .order_by(Property.created_at.desc()) .all() ) _apply_image(my_properties) favorite_ids = [ f.property_id for f in db.query(Favorite).filter(Favorite.user_id == user["id"]).all() ] favorites = [] if favorite_ids: favorites = db.query(Property).filter(Property.id.in_(favorite_ids)).all() _apply_image(favorites) return templates.TemplateResponse(request, "dashboard/index.html", { "user": user, "my_properties": my_properties, "favorites": favorites, })