Skip to content

Commit c9ba984

Browse files
authored
Merge branch 'master' into favorites-functionality
2 parents 165bee5 + bcf14e6 commit c9ba984

20 files changed

Lines changed: 1170 additions & 107 deletions

backend/market/management/commands/generate_listings.py

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -180,19 +180,6 @@ def handle(self, *args, **options):
180180
"House",
181181
]
182182

183-
neighborhoods = [
184-
"University City",
185-
"Center City",
186-
"Rittenhouse Square",
187-
"Fairmount",
188-
"Graduate Hospital",
189-
"Old City",
190-
"Northern Liberties",
191-
"Fishtown",
192-
"Queen Village",
193-
"South Philadelphia",
194-
]
195-
196183
street_names = [
197184
"Walnut",
198185
"Chestnut",
@@ -234,8 +221,8 @@ def handle(self, *args, **options):
234221
# generate random address
235222
street_number = random.randint(100, 4999)
236223
street = random.choice(street_names)
237-
neighborhood = random.choice(neighborhoods)
238-
address = f"{street_number} {street} St, {neighborhood}, Philadelphia, PA"
224+
225+
street_address = f"{street_number} {street} St, Philadelphia, PA"
239226

240227
# random beds and baths
241228
beds = random.randint(0, 4) # 0 for studio
@@ -271,7 +258,7 @@ def handle(self, *args, **options):
271258
price=price,
272259
negotiable=negotiable,
273260
expires_at=expires_at,
274-
address=address,
261+
street_address=street_address,
275262
beds=beds,
276263
baths=baths,
277264
start_date=start_date,
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 5.0.2 on 2026-02-02 20:37
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("market", "0003_delete_phoneverification"),
10+
]
11+
12+
operations = [
13+
migrations.RenameField(
14+
model_name="sublet",
15+
old_name="address",
16+
new_name="street_address",
17+
),
18+
migrations.AlterField(
19+
model_name="listing",
20+
name="expires_at",
21+
field=models.DateTimeField(blank=True, null=True),
22+
),
23+
]

backend/market/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ class Meta:
8989
)
9090
negotiable = models.BooleanField(default=True)
9191
created_at = models.DateTimeField(auto_now_add=True)
92-
expires_at = models.DateTimeField()
92+
expires_at = models.DateTimeField(null=True, blank=True)
9393

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

126126

127127
class Sublet(Listing):
128-
address = models.CharField(max_length=255)
128+
street_address = models.CharField(max_length=255)
129129
beds = models.PositiveIntegerField()
130130
baths = models.PositiveIntegerField()
131131
start_date = models.DateField()

backend/market/serializers.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
from django.core.exceptions import ValidationError as ModelValidationError
33
from profanity_check import predict
44
from rest_framework.serializers import (
5+
BooleanField,
6+
DateTimeField,
57
ImageField,
68
ModelSerializer,
79
SerializerMethodField,
810
SlugRelatedField,
11+
URLField,
912
ValidationError,
1013
)
1114

@@ -97,7 +100,7 @@ def get_condition(self, obj):
97100
class SubletDataSerializer(ModelSerializer):
98101
class Meta:
99102
model = Sublet
100-
fields = ["address", "beds", "baths", "start_date", "end_date"]
103+
fields = ["street_address", "beds", "baths", "start_date", "end_date"]
101104

102105

103106
# Unified serializer for all listing types (Items and Sublets); used for CRUD operations
@@ -108,16 +111,31 @@ class ListingSerializer(ListingTypeMixin, ModelSerializer):
108111
"model": Item,
109112
},
110113
"sublet": {
111-
"required_fields": ["address", "beds", "baths", "start_date", "end_date"],
114+
"required_fields": [
115+
"street_address",
116+
"beds",
117+
"baths",
118+
"start_date",
119+
"end_date",
120+
],
112121
"model": Sublet,
113122
},
114123
}
115124

116125
images = ListingImageSerializer(many=True, required=False, read_only=True)
117-
tags = SlugRelatedField(many=True, slug_field="name", queryset=Tag.objects.all())
126+
tags = SlugRelatedField(
127+
many=True,
128+
slug_field="name",
129+
queryset=Tag.objects.all(),
130+
required=False,
131+
allow_empty=True,
132+
)
118133
seller = UserSerializer(read_only=True)
119134
listing_type = SerializerMethodField()
120135
additional_data = SerializerMethodField()
136+
external_link = URLField(required=False, allow_blank=True, allow_null=True)
137+
negotiable = BooleanField(required=False, default=True)
138+
expires_at = DateTimeField(required=False, allow_null=True)
121139

122140
class Meta:
123141
model = Listing
@@ -240,7 +258,7 @@ def _create_sublet(self, validated_data, additional_data):
240258
tags = validated_data.pop("tags", None)
241259

242260
sublet = Sublet.objects.create(
243-
address=additional_data.get("address"),
261+
street_address=additional_data.get("street_address"),
244262
beds=additional_data.get("beds"),
245263
baths=additional_data.get("baths"),
246264
start_date=additional_data.get("start_date"),
@@ -297,7 +315,7 @@ def _update_item(self, instance, additional_data):
297315

298316
def _update_sublet(self, instance, additional_data):
299317
sublet = instance.sublet
300-
sublet_fields = ["address", "beds", "baths", "start_date", "end_date"]
318+
sublet_fields = ["street_address", "beds", "baths", "start_date", "end_date"]
301319
for field in sublet_fields:
302320
if field in additional_data:
303321
setattr(sublet, field, additional_data[field])

backend/market/views.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from django.conf import settings
22
from django.contrib.auth import get_user_model
33
from django.core.cache import cache
4+
from django.db.models import Q
45
from django.utils import timezone
56
from rest_framework import exceptions, mixins, status, viewsets
67
from rest_framework.decorators import api_view, permission_classes
@@ -128,7 +129,7 @@ def get_filter_dict(listing_type):
128129
sublet_filters = {
129130
"beds": "sublet__beds",
130131
"baths": "sublet__baths",
131-
"address": "sublet__address__icontains",
132+
"address": "sublet__street_address__icontains",
132133
}
133134

134135
if listing_type == "item":
@@ -168,7 +169,11 @@ def list(self, request, *args, **kwargs):
168169
if request.query_params.get("seller", "false").lower() == "true":
169170
queryset = queryset.filter(seller=request.user)
170171
else:
171-
queryset = queryset.filter(expires_at__gte=timezone.now())
172+
# Show listings that are not expired, or have no expiration
173+
now = timezone.now()
174+
queryset = queryset.filter(
175+
Q(expires_at__gte=now) | Q(expires_at__isnull=True)
176+
)
172177

173178
page = self.paginate_queryset(queryset)
174179
if page is not None:

frontend/app/create/item/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Link from "next/link";
22
import { BackButton } from "@/components/listings/detail/BackButton";
3+
import { ItemForm } from "@/components/listings/form/ItemForm";
34

45
export default function CreateItemPage() {
56
return (
@@ -9,6 +10,8 @@ export default function CreateItemPage() {
910
</Link>
1011

1112
<h1 className="mb-8 pt-2 text-3xl font-bold">New Item</h1>
13+
14+
<ItemForm />
1215
</div>
1316
);
1417
}

frontend/app/create/sublet/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Link from "next/link";
22
import { BackButton } from "@/components/listings/detail/BackButton";
3+
import { SubletForm } from "@/components/listings/form/SubletForm";
34

45
export default function CreateSubletPage() {
56
return (
@@ -9,6 +10,8 @@ export default function CreateSubletPage() {
910
</Link>
1011

1112
<h1 className="mb-8 pt-2 text-3xl font-bold">New Sublet</h1>
13+
14+
<SubletForm />
1215
</div>
1316
);
1417
}

frontend/components/common/FormField.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,32 @@ interface FormFieldProps {
66
touched?: boolean;
77
optional?: boolean;
88
helperText?: string;
9+
/** Rendered next to the label (e.g. info icon with hover message) */
10+
labelSupplement?: ReactNode;
911
children: ReactNode;
1012
}
1113

12-
export function FormField({
14+
export const FormField = ({
1315
label,
1416
error,
1517
touched = false,
1618
optional = false,
1719
helperText,
20+
labelSupplement,
1821
children,
19-
}: FormFieldProps) {
22+
}: FormFieldProps) => {
2023
return (
2124
<div>
22-
<label className="mb-2 block text-sm font-medium">
23-
{label}
24-
{optional && <span className="font-normal text-gray-400"> (optional)</span>}
25+
<label className="mb-2 flex items-center gap-1.5 text-sm font-medium">
26+
<span>
27+
{label}
28+
{optional && <span className="font-normal text-gray-400"> (optional)</span>}
29+
</span>
30+
{labelSupplement}
2531
</label>
2632
{children}
2733
{helperText && !error && <p className="mt-1 text-xs text-gray-500">{helperText}</p>}
2834
{error && touched && <p className="text-destructive mt-1 text-sm">{error}</p>}
2935
</div>
3036
);
31-
}
37+
};
Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,60 @@
1+
import { FormField } from "@/components/common/FormField";
12
import {
23
Select,
34
SelectContent,
45
SelectItem,
56
SelectTrigger,
67
SelectValue,
78
} from "@/components/ui/select";
8-
import { FormField } from "@/components/common/FormField";
99

1010
interface Option {
1111
value: string;
1212
label: string;
1313
}
1414

15-
type FormSelectPropsBase = {
15+
interface FormSelectPropsBase {
1616
label: string;
1717
options: Option[];
1818
placeholder?: string;
1919
error?: string;
2020
touched?: boolean;
21-
required?: boolean;
2221
optional?: boolean;
2322
helperText?: string;
24-
};
23+
}
2524

2625
type StringFormSelectProps = FormSelectPropsBase & {
26+
valueType?: "string";
2727
value: string | undefined;
2828
onChange: (value: string) => void;
29-
asNumber?: false;
3029
};
3130

3231
type NumberFormSelectProps = FormSelectPropsBase & {
32+
valueType: "number";
3333
value: number | undefined;
3434
onChange: (value: number) => void;
35-
asNumber: true;
3635
};
3736

3837
type FormSelectProps = StringFormSelectProps | NumberFormSelectProps;
3938

40-
export function FormSelect(props: FormSelectProps) {
41-
const {
42-
label,
43-
options,
44-
placeholder = "Select an option",
45-
error,
46-
touched,
47-
optional,
48-
helperText,
49-
} = props;
50-
51-
const stringValue = props.asNumber
52-
? props.value !== undefined
53-
? String(props.value)
54-
: ""
55-
: (props.value ?? "");
39+
export const FormSelect = ({
40+
label,
41+
options,
42+
placeholder = "Select an option",
43+
error,
44+
touched,
45+
optional,
46+
helperText,
47+
valueType,
48+
value,
49+
onChange,
50+
}: FormSelectProps) => {
51+
const stringValue = value !== undefined ? String(value) : "";
5652

5753
const handleChange = (val: string) => {
58-
if (props.asNumber) {
59-
props.onChange(Number(val));
54+
if (valueType === "number") {
55+
onChange(Number(val));
6056
} else {
61-
props.onChange(val);
57+
onChange(val);
6258
}
6359
};
6460

@@ -84,4 +80,4 @@ export function FormSelect(props: FormSelectProps) {
8480
</Select>
8581
</FormField>
8682
);
87-
}
83+
};

0 commit comments

Comments
 (0)