Skip to content

Commit 173fe0d

Browse files
jamesdoh0109claude
andcommitted
Rename favorites to saved across full stack
Replace all "favorites" terminology with "saved" — backend model field, serializers, views, API endpoints, frontend types, actions, and UI component. Swap Heart icon for Bookmark. Add migration using RenameField to preserve existing data. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a81b705 commit 173fe0d

9 files changed

Lines changed: 165 additions & 136 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from django.conf import settings
2+
from django.db import migrations, models
3+
4+
5+
class Migration(migrations.Migration):
6+
7+
dependencies = [
8+
("market", "0005_sublet_true_latitude_sublet_true_longitude"),
9+
]
10+
11+
operations = [
12+
migrations.RenameField(
13+
model_name="listing",
14+
old_name="favorites",
15+
new_name="saved",
16+
),
17+
migrations.AlterField(
18+
model_name="listing",
19+
name="saved",
20+
field=models.ManyToManyField(
21+
blank=True, related_name="listings_saved", to=settings.AUTH_USER_MODEL
22+
),
23+
),
24+
]

backend/market/models.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class Meta:
4747
def __str__(self):
4848
return f"Offer for {self.listing} made by {self.user}"
4949

50+
5051
class Category(models.Model):
5152
name = models.CharField(max_length=100, unique=True)
5253

@@ -81,9 +82,7 @@ class Meta:
8182
User, through=Offer, related_name="listings_offered", blank=True
8283
)
8384
tags = models.ManyToManyField(Tag, blank=True)
84-
favorites = models.ManyToManyField(
85-
User, related_name="listings_favorited", blank=True
86-
)
85+
saved = models.ManyToManyField(User, related_name="listings_saved", blank=True)
8786

8887
title = models.CharField(max_length=255)
8988
description = models.TextField(null=True, blank=True)
@@ -170,7 +169,8 @@ def _calculate_approximate_location(self, latitude, longitude):
170169
def approximate_location(self):
171170
if self.latitude is not None and self.longitude is not None:
172171
approximate_location = self._calculate_approximate_location(
173-
self.latitude, self.longitude)
172+
self.latitude, self.longitude
173+
)
174174
return approximate_location
175175
return None, None
176176

backend/market/serializers.py

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
from django.contrib.auth import get_user_model
32
from django.core.exceptions import ValidationError as ModelValidationError
43
from profanity_check import predict
@@ -126,6 +125,7 @@ def get_longitude(self, obj):
126125
return float(approx_lon)
127126
return None
128127

128+
129129
# Unified serializer for all listing types (Items and Sublets); used for CRUD operations
130130
class ListingSerializer(ListingTypeMixin, ModelSerializer):
131131
LISTING_TYPE_CONFIG = {
@@ -156,7 +156,7 @@ class ListingSerializer(ListingTypeMixin, ModelSerializer):
156156
seller = UserSerializer(read_only=True)
157157
listing_type = SerializerMethodField()
158158
additional_data = SerializerMethodField()
159-
is_favorited = SerializerMethodField()
159+
is_saved = SerializerMethodField()
160160
external_link = URLField(required=False, allow_blank=True, allow_null=True)
161161
negotiable = BooleanField(required=False, default=True)
162162
expires_at = DateTimeField(required=False, allow_null=True)
@@ -168,7 +168,7 @@ class Meta:
168168
"seller",
169169
"buyers",
170170
"tags",
171-
"favorites",
171+
"saved",
172172
"title",
173173
"description",
174174
"external_link",
@@ -179,15 +179,15 @@ class Meta:
179179
"images",
180180
"listing_type",
181181
"additional_data",
182-
"is_favorited",
182+
"is_saved",
183183
]
184184
read_only_fields = [
185185
"id",
186186
"created_at",
187187
"seller",
188188
"buyers",
189189
"images",
190-
"favorites",
190+
"saved",
191191
]
192192

193193
def validate(self, attrs):
@@ -221,11 +221,11 @@ def validate(self, attrs):
221221

222222
return super().validate(attrs)
223223

224-
def get_is_favorited(self, obj):
224+
def get_is_saved(self, obj):
225225
request = self.context.get("request")
226226
if not request or not request.user or not request.user.is_authenticated:
227227
return False
228-
return request.user.listings_favorited.filter(id=obj.id).exists()
228+
return request.user.listings_saved.filter(id=obj.id).exists()
229229

230230
def validate_title(self, value):
231231
if self.contains_profanity(value):
@@ -291,7 +291,6 @@ def _create_sublet(self, validated_data, additional_data):
291291
latitude = additional_data.get("latitude")
292292
longitude = additional_data.get("longitude")
293293

294-
295294
if latitude is not None:
296295
latitude = float(latitude)
297296
if longitude is not None:
@@ -374,8 +373,8 @@ def _update_sublet(self, instance, additional_data):
374373
# Read-only serializer for use when reading a single listing
375374
class ListingSerializerPublic(ListingTypeMixin, ModelSerializer):
376375
buyer_count = SerializerMethodField()
377-
favorite_count = SerializerMethodField()
378-
is_favorited = SerializerMethodField()
376+
saved_count = SerializerMethodField()
377+
is_saved = SerializerMethodField()
379378
tags = SlugRelatedField(many=True, slug_field="name", queryset=Tag.objects.all())
380379
images = ListingImageURLSerializer(many=True)
381380
seller = UserSerializer(read_only=True)
@@ -396,29 +395,29 @@ class Meta:
396395
"negotiable",
397396
"expires_at",
398397
"images",
399-
"favorite_count",
398+
"saved_count",
400399
"listing_type",
401400
"additional_data",
402-
"is_favorited",
401+
"is_saved",
403402
]
404403
read_only_fields = fields
405404

406405
def get_buyer_count(self, obj):
407406
return obj.buyers.count()
408407

409-
def get_favorite_count(self, obj):
410-
return obj.favorites.count()
408+
def get_saved_count(self, obj):
409+
return obj.saved.count()
411410

412-
def get_is_favorited(self, obj):
411+
def get_is_saved(self, obj):
413412
request = self.context.get("request")
414413
if not request or not request.user or not request.user.is_authenticated:
415414
return False
416-
return request.user.listings_favorited.filter(id=obj.id).exists()
415+
return request.user.listings_saved.filter(id=obj.id).exists()
417416

418417

419418
# Read-only serializer for use when pulling all listings /etc
420419
class ListingSerializerList(ListingTypeMixin, ModelSerializer):
421-
favorite_count = SerializerMethodField()
420+
saved_count = SerializerMethodField()
422421
tags = SlugRelatedField(many=True, slug_field="name", queryset=Tag.objects.all())
423422
images = ListingImageURLSerializer(many=True)
424423
seller = UserSerializer(read_only=True)
@@ -435,11 +434,11 @@ class Meta:
435434
"price",
436435
"expires_at",
437436
"images",
438-
"favorite_count",
437+
"saved_count",
439438
"listing_type",
440439
"additional_data",
441440
]
442441
read_only_fields = fields
443442

444-
def get_favorite_count(self, obj):
445-
return obj.favorites.count()
443+
def get_saved_count(self, obj):
444+
return obj.saved.count()

backend/market/urls.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44
from market.views import (
55
CreateImages,
66
DeleteImage,
7-
Favorites,
87
Listings,
98
Offers,
109
OffersMade,
1110
OffersReceived,
11+
SavedListings,
1212
Tags,
13-
UserFavorites,
13+
UserSavedListings,
1414
get_current_user,
1515
get_phone_status,
1616
send_verification_code,
@@ -28,18 +28,18 @@
2828
path("user/me/", get_current_user, name="current-user"),
2929
# List of all amenities
3030
path("tags/", Tags.as_view(), name="tags"),
31-
# All favorites for user
32-
path("favorites/", UserFavorites.as_view(), name="user-favorites"),
31+
# All saved listings for user
32+
path("saved/", UserSavedListings.as_view(), name="user-saved-listings"),
3333
# All offers made by user
3434
path("offers/made/", OffersMade.as_view(), name="offers-made"),
3535
# All offers for an listing owned by user
3636
path("offers/received/", OffersReceived.as_view(), name="offers-received"),
37-
# Favorites
38-
# post: add a listing to the user's favorites
39-
# delete: remove a listing from the user's favorites
37+
# Saved listings
38+
# post: save a listing for the user
39+
# delete: unsave a listing for the user
4040
path(
41-
"listings/<listing_id>/favorites/",
42-
Favorites.as_view({"post": "create", "delete": "destroy"}),
41+
"listings/<listing_id>/saved/",
42+
SavedListings.as_view({"post": "create", "delete": "destroy"}),
4343
),
4444
# Offers
4545
# get: list all offers for an listing

backend/market/views.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,14 @@ def get_queryset(self):
4848
return Tag.objects.all()
4949

5050

51-
class UserFavorites(ListAPIView, DefaultOrderMixin):
51+
class UserSavedListings(ListAPIView, DefaultOrderMixin):
5252
serializer_class = ListingSerializerList
5353
permission_classes = [IsAuthenticated]
5454
pagination_class = PageSizeOffsetPagination
5555

5656
def get_queryset(self):
5757
user = self.request.user
58-
return user.listings_favorited.all()
58+
return user.listings_saved.all()
5959

6060

6161
# TODO: Can add feature to filter for active offers only
@@ -245,7 +245,7 @@ def destroy(self, request, *args, **kwargs):
245245

246246
# TODO: We don't use the CreateModelMixin or DestroyModelMixin's functionality here.
247247
# Think about if there's a better way
248-
class Favorites(
248+
class SavedListings(
249249
mixins.DestroyModelMixin, mixins.CreateModelMixin, viewsets.GenericViewSet
250250
):
251251
serializer_class = ListingSerializer
@@ -255,32 +255,32 @@ class Favorites(
255255

256256
def get_queryset(self):
257257
user = self.request.user
258-
return user.listings_favorited.all()
258+
return user.listings_saved.all()
259259

260260
def create(self, request, *args, **kwargs):
261261
listing_id = int(self.kwargs["listing_id"])
262-
favorites = request.user.listings_favorited
263-
if favorites.filter(id=listing_id).exists():
262+
saved = request.user.listings_saved
263+
if saved.filter(id=listing_id).exists():
264264
return Response(
265-
{"liked": True, "detail": "User has already liked the listing"},
265+
{"saved": True, "detail": "User has already saved the listing"},
266266
status=status.HTTP_409_CONFLICT,
267267
)
268268
listing = get_object_or_404(Listing, id=listing_id)
269-
favorites.add(listing)
270-
return Response({"liked": True}, status=status.HTTP_201_CREATED)
269+
saved.add(listing)
270+
return Response({"saved": True}, status=status.HTTP_201_CREATED)
271271

272272
def destroy(self, request, *args, **kwargs):
273273
listing_id = int(self.kwargs["listing_id"])
274274
listing = get_object_or_404(Listing, id=listing_id)
275275

276-
if listing not in request.user.listings_favorited.all():
276+
if listing not in request.user.listings_saved.all():
277277
return Response(
278-
{"liked": False, "detail": "User hasn't liked the listing yet"},
278+
{"saved": False, "detail": "User hasn't saved the listing yet"},
279279
status=status.HTTP_404_NOT_FOUND,
280280
)
281281

282-
request.user.listings_favorited.remove(listing)
283-
return Response({"liked": False}, status=status.HTTP_200_OK)
282+
request.user.listings_saved.remove(listing)
283+
return Response({"saved": False}, status=status.HTTP_200_OK)
284284

285285

286286
class Offers(viewsets.ModelViewSet):

0 commit comments

Comments
 (0)