add support for querying buying listings as well as by max price
This commit is contained in:
parent
ba4a95825b
commit
69d15e9a16
5 changed files with 95 additions and 31 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue