add support for querying buying listings as well as by max price

This commit is contained in:
Viktor Barzin 2025-06-18 20:38:50 +00:00
parent ba4a95825b
commit 69d15e9a16
No known key found for this signature in database
GPG key ID: 4056458DBDBF8863
5 changed files with 95 additions and 31 deletions

View file

@ -1,8 +1,9 @@
from typing import Annotated from typing import Annotated
from api.auth import get_current_user from api.auth import get_current_user
from api.config import DEV_TIER_ORIGINS, PROD_TIER_ORIGINS from api.config import DEV_TIER_ORIGINS, PROD_TIER_ORIGINS
from fastapi import Depends, FastAPI from fastapi import Depends, FastAPI, Query
from api.auth import User from api.auth import User
from models.listing import QueryParameters
from repositories.listing_repository import ListingRepository from repositories.listing_repository import ListingRepository
from repositories.listing_repository import ListingRepository from repositories.listing_repository import ListingRepository
from database import engine from database import engine
@ -29,7 +30,12 @@ async def get_listing(user: Annotated[User, Depends(get_current_user)]):
@app.get("/api/listing_geojson") @app.get("/api/listing_geojson")
async def get_listing_geojson(user: Annotated[User, Depends(get_current_user)]): async def get_listing_geojson(
user: Annotated[User, Depends(get_current_user)],
query_parameters: Annotated[QueryParameters, Query()],
):
repository = ListingRepository(engine) repository = ListingRepository(engine)
geojson_data = await export_immoweb(repository, limit=None) geojson_data = await export_immoweb(
repository, query_parameters=query_parameters, limit=None
)
return geojson_data return geojson_data

View file

@ -31,11 +31,15 @@ function App() {
const [isParametersModalOpen, setIsParametersModalOpen] = useState(true) const [isParametersModalOpen, setIsParametersModalOpen] = useState(true)
const [error, setError] = useState('') const [error, setError] = useState('')
const [queryParameters, setQueryParameters] = useState<ParameterValues | null>(null) const [queryParameters, setQueryParameters] = useState<ParameterValues | null>(null)
const fetchData = async () => { const fetchData = async (parameters: ParameterValues) => {
const accessToken = user?.access_token;
const queryString = new URLSearchParams();
queryString.append('listing_type', parameters.listing_type)
if (parameters.max_price) {
queryString.append("max_price", parameters.max_price.toString());
}
try { try {
const accessToken = user?.access_token; const response = await fetch("/api/listing_geojson?" + queryString,
const response = await fetch('/api/listing_geojson',
{ {
method: 'GET', method: 'GET',
headers: { headers: {
@ -49,14 +53,15 @@ function App() {
return data; return data;
} catch (err) { } catch (err) {
setError('Failed to fetch data: ' + err); setError('Failed to fetch data: ' + err);
alert(error) alert(JSON.stringify(err))
} finally { } finally {
} }
}; };
const onSubmit = async (parameters: ParameterValues) => { const onSubmit = async (parameters: ParameterValues) => {
// Fetch listing data // Fetch listing data
setQueryParameters(parameters) setQueryParameters(parameters)
const data = await fetchData(); const data = await fetchData(parameters);
console.log(data)
if (data) { if (data) {
setListingData(data); setListingData(data);
} }

View file

@ -4,7 +4,8 @@ import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { Dialog, DialogContent, DialogTrigger } from "./ui/dialog"; import { Dialog, DialogContent, DialogTrigger } from "./ui/dialog";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "./ui/form"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "./ui/form";
import { Input } from "./ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
@ -14,10 +15,16 @@ export enum Metric {
qm = 'qm', qm = 'qm',
price = 'total_price', price = 'total_price',
} }
export enum ListingType {
RENT = 'RENT',
BUY = 'BUY'
}
export interface ParameterValues { export interface ParameterValues {
metric: Metric metric: Metric
listing_type: ListingType
max_price?: number
} }
export function Parameters( export function Parameters(
@ -33,17 +40,21 @@ export function Parameters(
const formSchema = z.object({ const formSchema = z.object({
metric: z.nativeEnum(Metric, { required_error: "Metric is required" }), metric: z.nativeEnum(Metric, { required_error: "Metric is required" }),
listing_type: z.nativeEnum(ListingType, { required_error: "Listing Type is required" }),
max_price: z.number().optional(),
}) })
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
metric: Metric.qmprice, metric: Metric.qmprice,
max_price: 3000,
}, },
}) })
// 2. Define a submit handler. // 2. Define a submit handler.
function onSubmit(values: z.infer<typeof formSchema>) { function onSubmit(values: z.infer<typeof formSchema>) {
// Do something with the form values. // Do something with the form values.
// ✅ This will be type-safe and validated. // ✅ This will be type-safe and validated.
console.log(values)
props.onSubmit(values) props.onSubmit(values)
} }
@ -79,13 +90,49 @@ export function Parameters(
<SelectItem value={Metric.price}>Price</SelectItem> <SelectItem value={Metric.price}>Price</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="listing_type"
render={({ field }) => (
<FormItem>
<FormLabel>Listing Type</FormLabel>
<FormControl >
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Metric to Visualize" />
</SelectTrigger>
</FormControl>
<SelectContent >
<SelectItem value={ListingType.BUY}>To buy</SelectItem>
<SelectItem value={ListingType.RENT}>To rent</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="max_price"
render={({ field }) => (
<FormItem>
<FormLabel>Max price</FormLabel>
<FormControl >
<Input type="number" placeholder={"£"} {...field} onChange={(e) => field.onChange(Number(e.target.value))} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button> <Button type="submit">Submit</Button>
</form> </form>
</Form> </Form>

View file

@ -6,6 +6,7 @@ import enum
import json import json
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List from typing import Any, Dict, List
from pydantic import BaseModel
from rec import routing from rec import routing
from sqlmodel import JSON, SQLModel, Field, String from sqlmodel import JSON, SQLModel, Field, String
@ -230,7 +231,7 @@ class ListingType(enum.StrEnum):
@dataclass(frozen=True) @dataclass(frozen=True)
class QueryParameters: class QueryParameters(BaseModel):
listing_type: ListingType listing_type: ListingType
min_bedrooms: int = 1 min_bedrooms: int = 1
max_bedrooms: int = 999 max_bedrooms: int = 999

View file

@ -4,6 +4,7 @@ from models.listing import (
BuyListing, BuyListing,
FurnishType, FurnishType,
Listing as modelListing, Listing as modelListing,
ListingType,
QueryParameters, QueryParameters,
RentListing, RentListing,
) )
@ -32,12 +33,19 @@ class ListingRepository:
""" """
only_ids = only_ids or [] only_ids = only_ids or []
query = select( model = RentListing # if no query params, default to renting listings
RentListing if query_parameters:
) # TODO: one nice day I will think of a way to query both rent and buy model = (
RentListing
if query_parameters.listing_type == ListingType.RENT
else BuyListing
# else RentListing
)
query = select(model)
if only_ids: if only_ids:
query = query.where(RentListing.id.in_(only_ids)) # type: ignore query = query.where(model.id.in_(only_ids)) # type: ignore
query = self._add_where_from_query_parameters(query, query_parameters) query = self._add_where_from_query_parameters(query, model, query_parameters)
if limit: if limit:
query = query.limit(limit) query = query.limit(limit)
@ -47,34 +55,31 @@ class ListingRepository:
def _add_where_from_query_parameters( def _add_where_from_query_parameters(
self, self,
query: SelectOfScalar[RentListing], query: SelectOfScalar[Listing],
model: type[Listing],
query_parameters: QueryParameters | None = None, query_parameters: QueryParameters | None = None,
) -> SelectOfScalar[RentListing]: ) -> SelectOfScalar[Listing]:
if query_parameters is None: if query_parameters is None:
return query return query
query = query.where( query = query.where(
RentListing.number_of_bedrooms.between( model.number_of_bedrooms.between(
query_parameters.min_bedrooms, query_parameters.max_bedrooms query_parameters.min_bedrooms, query_parameters.max_bedrooms
), ),
RentListing.price.between( model.price.between(query_parameters.min_price, query_parameters.max_price),
query_parameters.min_price, query_parameters.max_price
),
) )
if query_parameters.min_sqm is not None: if query_parameters.min_sqm is not None:
query = query.where(RentListing.square_meters >= query_parameters.min_sqm) query = query.where(model.square_meters >= query_parameters.min_sqm)
if query_parameters.furnish_types: if query_parameters.furnish_types:
query = query.where( query = query.where(model.furnish_type.in_(query_parameters.furnish_types))
RentListing.furnish_type.in_(query_parameters.furnish_types)
)
if query_parameters.let_date_available_from is not None: if query_parameters.let_date_available_from is not None:
query = query.where( query = query.where(
RentListing.available_from >= query_parameters.let_date_available_from model.available_from >= query_parameters.let_date_available_from
) )
if query_parameters.last_seen_days is not None: if query_parameters.last_seen_days is not None:
last_seen_threshold = datetime.now() - timedelta( last_seen_threshold = datetime.now() - timedelta(
days=query_parameters.last_seen_days days=query_parameters.last_seen_days
) )
query = query.where(RentListing.last_seen >= last_seen_threshold) query = query.where(model.last_seen >= last_seen_threshold)
return query return query
async def upsert_listings( async def upsert_listings(