Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 3 additions & 16 deletions backend/market/management/commands/generate_listings.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,19 +180,6 @@ def handle(self, *args, **options):
"House",
]

neighborhoods = [
"University City",
"Center City",
"Rittenhouse Square",
"Fairmount",
"Graduate Hospital",
"Old City",
"Northern Liberties",
"Fishtown",
"Queen Village",
"South Philadelphia",
]

street_names = [
"Walnut",
"Chestnut",
Expand Down Expand Up @@ -234,8 +221,8 @@ def handle(self, *args, **options):
# generate random address
street_number = random.randint(100, 4999)
street = random.choice(street_names)
neighborhood = random.choice(neighborhoods)
address = f"{street_number} {street} St, {neighborhood}, Philadelphia, PA"

street_address = f"{street_number} {street} St, Philadelphia, PA"

# random beds and baths
beds = random.randint(0, 4) # 0 for studio
Expand Down Expand Up @@ -271,7 +258,7 @@ def handle(self, *args, **options):
price=price,
negotiable=negotiable,
expires_at=expires_at,
address=address,
street_address=street_address,
beds=beds,
baths=baths,
start_date=start_date,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 5.0.2 on 2026-02-02 20:37

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("market", "0003_delete_phoneverification"),
]

operations = [
migrations.RenameField(
model_name="sublet",
old_name="address",
new_name="street_address",
),
migrations.AlterField(
model_name="listing",
name="expires_at",
field=models.DateTimeField(blank=True, null=True),
),
]
4 changes: 2 additions & 2 deletions backend/market/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ class Meta:
)
negotiable = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
expires_at = models.DateTimeField()
expires_at = models.DateTimeField(null=True, blank=True)

def __str__(self):
return f"{self.title} by {self.seller}"
Expand Down Expand Up @@ -125,7 +125,7 @@ class Condition(models.TextChoices):


class Sublet(Listing):
address = models.CharField(max_length=255)
street_address = models.CharField(max_length=255)
beds = models.PositiveIntegerField()
baths = models.PositiveIntegerField()
start_date = models.DateField()
Expand Down
28 changes: 23 additions & 5 deletions backend/market/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
from django.core.exceptions import ValidationError as ModelValidationError
from profanity_check import predict
from rest_framework.serializers import (
BooleanField,
DateTimeField,
ImageField,
ModelSerializer,
SerializerMethodField,
SlugRelatedField,
URLField,
ValidationError,
)

Expand Down Expand Up @@ -97,7 +100,7 @@ def get_condition(self, obj):
class SubletDataSerializer(ModelSerializer):
class Meta:
model = Sublet
fields = ["address", "beds", "baths", "start_date", "end_date"]
fields = ["street_address", "beds", "baths", "start_date", "end_date"]


# Unified serializer for all listing types (Items and Sublets); used for CRUD operations
Expand All @@ -108,16 +111,31 @@ class ListingSerializer(ListingTypeMixin, ModelSerializer):
"model": Item,
},
"sublet": {
"required_fields": ["address", "beds", "baths", "start_date", "end_date"],
"required_fields": [
"street_address",
"beds",
"baths",
"start_date",
"end_date",
],
"model": Sublet,
},
}

images = ListingImageSerializer(many=True, required=False, read_only=True)
tags = SlugRelatedField(many=True, slug_field="name", queryset=Tag.objects.all())
tags = SlugRelatedField(
many=True,
slug_field="name",
queryset=Tag.objects.all(),
required=False,
allow_empty=True,
)
seller = UserSerializer(read_only=True)
listing_type = SerializerMethodField()
additional_data = SerializerMethodField()
external_link = URLField(required=False, allow_blank=True, allow_null=True)
negotiable = BooleanField(required=False, default=True)
expires_at = DateTimeField(required=False, allow_null=True)

class Meta:
model = Listing
Expand Down Expand Up @@ -240,7 +258,7 @@ def _create_sublet(self, validated_data, additional_data):
tags = validated_data.pop("tags", None)

sublet = Sublet.objects.create(
address=additional_data.get("address"),
street_address=additional_data.get("street_address"),
beds=additional_data.get("beds"),
baths=additional_data.get("baths"),
start_date=additional_data.get("start_date"),
Expand Down Expand Up @@ -297,7 +315,7 @@ def _update_item(self, instance, additional_data):

def _update_sublet(self, instance, additional_data):
sublet = instance.sublet
sublet_fields = ["address", "beds", "baths", "start_date", "end_date"]
sublet_fields = ["street_address", "beds", "baths", "start_date", "end_date"]
for field in sublet_fields:
if field in additional_data:
setattr(sublet, field, additional_data[field])
Expand Down
9 changes: 7 additions & 2 deletions backend/market/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.cache import cache
from django.db.models import Q
from django.utils import timezone
from rest_framework import exceptions, mixins, status, viewsets
from rest_framework.decorators import api_view, permission_classes
Expand Down Expand Up @@ -128,7 +129,7 @@ def get_filter_dict(listing_type):
sublet_filters = {
"beds": "sublet__beds",
"baths": "sublet__baths",
"address": "sublet__address__icontains",
"address": "sublet__street_address__icontains",
}

if listing_type == "item":
Expand Down Expand Up @@ -168,7 +169,11 @@ def list(self, request, *args, **kwargs):
if request.query_params.get("seller", "false").lower() == "true":
queryset = queryset.filter(seller=request.user)
else:
queryset = queryset.filter(expires_at__gte=timezone.now())
# Show listings that are not expired, or have no expiration
now = timezone.now()
queryset = queryset.filter(
Q(expires_at__gte=now) | Q(expires_at__isnull=True)
)

page = self.paginate_queryset(queryset)
if page is not None:
Expand Down
8 changes: 4 additions & 4 deletions frontend/app/create/item/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import Link from "next/link";
import { BackButton } from "@/components/listings/detail/BackButton";
import { ListingForm } from "@/components/listings/ListingForm";
import { ItemForm } from "@/components/listings/form/ItemForm";

export default function CreateItemPage() {
return (
<div className="w-full mx-auto container max-w-[96rem] px-12 pt-4 pb-12">
<div className="container mx-auto w-full max-w-[96rem] px-12 pt-4 pb-12">
<Link href="/items">
<BackButton />
</Link>

<h1 className="text-3xl font-bold pt-2 mb-8">New Item</h1>
<h1 className="mb-8 pt-2 text-3xl font-bold">New Item</h1>

<ListingForm listingType="item" />
<ItemForm />
</div>
);
}
8 changes: 4 additions & 4 deletions frontend/app/create/sublet/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import Link from "next/link";
import { BackButton } from "@/components/listings/detail/BackButton";
import { ListingForm } from "@/components/listings/ListingForm";
import { SubletForm } from "@/components/listings/form/SubletForm";

export default function CreateSubletPage() {
return (
<div className="w-full mx-auto container max-w-[96rem] px-12 pt-6 pb-12">
<div className="container mx-auto w-full max-w-[96rem] px-12 pt-6 pb-12">
<Link href="/sublets">
<BackButton />
</Link>

<h1 className="text-3xl font-bold pt-2 mb-8">New Sublet</h1>
<h1 className="mb-8 pt-2 text-3xl font-bold">New Sublet</h1>

<ListingForm listingType="sublet" />
<SubletForm />
</div>
);
}
18 changes: 12 additions & 6 deletions frontend/components/common/FormField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,32 @@ interface FormFieldProps {
touched?: boolean;
optional?: boolean;
helperText?: string;
/** Rendered next to the label (e.g. info icon with hover message) */
labelSupplement?: ReactNode;
children: ReactNode;
}

export function FormField({
export const FormField = ({
label,
error,
touched = false,
optional = false,
helperText,
labelSupplement,
children,
}: FormFieldProps) {
}: FormFieldProps) => {
return (
<div>
<label className="mb-2 block text-sm font-medium">
{label}
{optional && <span className="font-normal text-gray-400"> (optional)</span>}
<label className="mb-2 flex items-center gap-1.5 text-sm font-medium">
<span>
{label}
{optional && <span className="font-normal text-gray-400"> (optional)</span>}
</span>
{labelSupplement}
</label>
{children}
{helperText && !error && <p className="mt-1 text-xs text-gray-500">{helperText}</p>}
{error && touched && <p className="text-destructive mt-1 text-sm">{error}</p>}
</div>
);
}
};
47 changes: 22 additions & 25 deletions frontend/components/common/FormSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,60 @@
import { FormField } from "@/components/common/FormField";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { FormField } from "@/components/common/FormField";

interface Option {
value: string;
label: string;
}

type FormSelectPropsBase = {
interface FormSelectPropsBase {
label: string;
options: Option[];
placeholder?: string;
error?: string;
touched?: boolean;
required?: boolean;
optional?: boolean;
helperText?: string;
};
}

type StringFormSelectProps = FormSelectPropsBase & {
valueType?: "string";
value: string | undefined;
onChange: (value: string) => void;
asNumber?: false;
};

type NumberFormSelectProps = FormSelectPropsBase & {
valueType: "number";
value: number | undefined;
onChange: (value: number) => void;
asNumber: true;
};

type FormSelectProps = StringFormSelectProps | NumberFormSelectProps;

export function FormSelect(props: FormSelectProps) {
const {
label,
options,
placeholder = "Select an option",
error,
touched,
required,
optional,
helperText,
} = props;

const stringValue = props.asNumber
? props.value !== undefined ? String(props.value) : ""
: props.value ?? "";
export const FormSelect = ({
label,
options,
placeholder = "Select an option",
error,
touched,
optional,
helperText,
valueType,
value,
onChange,
}: FormSelectProps) => {
const stringValue = value !== undefined ? String(value) : "";

const handleChange = (val: string) => {
if (props.asNumber) {
props.onChange(Number(val));
if (valueType === "number") {
onChange(Number(val));
} else {
props.onChange(val);
onChange(val);
}
};

Expand All @@ -83,4 +80,4 @@ export function FormSelect(props: FormSelectProps) {
</Select>
</FormField>
);
}
};
Loading