Skip to content

Commit 00793a1

Browse files
author
Anthony Li
committed
add user ratings
1 parent 1ab0c7c commit 00793a1

4 files changed

Lines changed: 135 additions & 5 deletions

File tree

backend/market/models.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from django.conf import settings
66
from django.contrib.auth.models import AbstractUser
77
from django.core.exceptions import ValidationError
8-
from django.core.validators import MinValueValidator
8+
from django.core.validators import MinValueValidator, MaxValueValidator
99
from django.db import models
1010
from phonenumber_field.modelfields import PhoneNumberField
1111

@@ -63,7 +63,6 @@ class Tag(models.Model):
6363
def __str__(self):
6464
return self.name
6565

66-
6766
class Listing(models.Model):
6867
class Meta:
6968
indexes = [
@@ -177,3 +176,30 @@ def approximate_location(self):
177176
def save(self, *args, **kwargs):
178177
self.full_clean()
179178
super().save(*args, **kwargs)
179+
180+
class Rating(models.Model):
181+
class RatingType(models.TextChoices):
182+
BUYER = "BUYER", "Buyer Rating"
183+
SELLER = "SELLER", "Seller Rating"
184+
185+
class Meta:
186+
constraints = [
187+
models.UniqueConstraint(fields=["reviewer", "reviewed_user", "listing"], name="unique_rating_market")
188+
]
189+
indexes = [
190+
models.Index(fields=["reviewer"]),
191+
models.Index(fields=["reviewed_user"]),
192+
models.Index(fields=["listing"]),
193+
models.Index(fields=["created_at"]),
194+
]
195+
196+
reviewer = models.ForeignKey(User, on_delete=models.CASCADE, related_name="ratings_given")
197+
reviewed_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="ratings_received")
198+
listing = models.ForeignKey(Listing, on_delete=models.CASCADE, related_name="ratings")
199+
score = models.PositiveIntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)])
200+
rating_type = models.CharField(max_length=10, choices=RatingType.choices, default=RatingType.BUYER)
201+
comment = models.TextField(blank=True)
202+
created_at = models.DateTimeField(auto_now_add=True)
203+
204+
def __str__(self):
205+
return f"Rating of {self.score} for {self.reviewed_user} by {self.reviewer}"

backend/market/serializers.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
)
1515

1616
from market.mixins import ListingTypeMixin
17-
from market.models import Category, Item, Listing, ListingImage, Offer, Sublet, Tag
17+
from market.models import Category, Item, Listing, ListingImage, Offer, Rating, Sublet, Tag
1818

1919

2020
User = get_user_model()
@@ -443,3 +443,54 @@ class Meta:
443443

444444
def get_favorite_count(self, obj):
445445
return obj.favorites.count()
446+
447+
class RatingSerializer(ModelSerializer):
448+
reviewer = UserSerializer(read_only=True)
449+
450+
class Meta:
451+
model = Rating
452+
fields = [
453+
"id",
454+
"reviewer",
455+
"reviewed_user",
456+
"listing",
457+
"score",
458+
"rating_type",
459+
"comment",
460+
"created_at"
461+
]
462+
read_only_fields = ["id", "created_at", "reviewer", "rating_type"]
463+
464+
def validate(self, attr):
465+
reviewer = self.context["request"].user
466+
reviewed_user = attr["reviewed_user"]
467+
listing = attr["listing"]
468+
469+
if reviewer == reviewed_user:
470+
raise ValidationError("You cannot review yourself.")
471+
472+
is_seller = listing.seller == reviewer
473+
is_buyer = listing.offers_received.filter(user=reviewer).exists()
474+
if not is_seller and not is_buyer:
475+
raise ValidationError("You can only rate users on listings you have interacted with.")
476+
477+
target_is_seller = listing.seller == reviewed_user
478+
target_is_buyer = listing.offers_received.filter(user=reviewed_user).exists()
479+
if not target_is_seller and not target_is_buyer:
480+
raise ValidationError("You cannot rate a user who is not on either side of the transaction.")
481+
attr["rating_type"] = "SELLER" if is_seller else "BUYER"
482+
483+
return attr
484+
485+
486+
def validate_comment(self, value):
487+
if self.contains_profanity(value):
488+
raise ValidationError("The comment contains inappropriate language.")
489+
return value
490+
491+
def contains_profanity(self, text):
492+
return predict([text])[0]
493+
494+
def create(self, validated_data):
495+
validated_data["reviewer"] = self.context["request"].user
496+
return super().create(validated_data)

backend/market/urls.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@
99
Offers,
1010
OffersMade,
1111
OffersReceived,
12+
Ratings,
1213
Tags,
14+
UserBuyerRatings,
1315
UserFavorites,
16+
UserSellerRatings,
1417
get_current_user,
1518
get_phone_status,
1619
send_verification_code,
@@ -49,6 +52,23 @@
4952
"listings/<listing_id>/offers/",
5053
Offers.as_view({"get": "list", "post": "create", "delete": "destroy"}),
5154
),
55+
# Ratings for a listing (list + create)
56+
path(
57+
"listings/<listing_id>/ratings/",
58+
Ratings.as_view({"get": "list", "post": "create"}),
59+
name="listing-ratings",
60+
),
61+
# Current user's received ratings by type
62+
path(
63+
"user/ratings/buyer/",
64+
UserBuyerRatings.as_view(),
65+
name="user-ratings-buyer",
66+
),
67+
path(
68+
"user/ratings/seller/",
69+
UserSellerRatings.as_view(),
70+
name="user-ratings-seller",
71+
),
5272
# Image Creation
5373
path("listings/<listing_id>/images/", CreateImages.as_view()),
5474
# Image Deletion

backend/market/views.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from rest_framework.response import Response
1717

1818
from market.mixins import DefaultOrderMixin
19-
from market.models import Listing, ListingImage, Offer, Tag
19+
from market.models import Listing, ListingImage, Offer, Rating, Tag
2020
from market.pagination import PageSizeOffsetPagination
2121
from market.permissions import (
2222
IsSuperUser,
@@ -31,6 +31,7 @@
3131
ListingSerializerList,
3232
ListingSerializerPublic,
3333
OfferSerializer,
34+
RatingSerializer,
3435
TagSerializer,
3536
UserSerializer,
3637
)
@@ -189,7 +190,7 @@ def retrieve(self, request, *args, **kwargs):
189190
serializer_class = ListingSerializer
190191
else:
191192
serializer_class = ListingSerializerPublic
192-
serializer = serializer_class(instance)
193+
serializer = serializer_class(instance, context={"request": request})
193194
return Response(serializer.data)
194195

195196

@@ -342,6 +343,38 @@ def list(self, request, *args, **kwargs):
342343
return super().list(request, *args, **kwargs)
343344

344345

346+
class Ratings(mixins.CreateModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
347+
serializer_class = RatingSerializer
348+
permission_classes = [IsAuthenticated | IsSuperUser]
349+
350+
def get_queryset(self):
351+
return Rating.objects.filter(listing_id=self.kwargs["listing_id"])
352+
353+
def create(self, request, *args, **kwargs):
354+
data = request.data.copy()
355+
data["listing"] = int(self.kwargs["listing_id"])
356+
serializer = self.get_serializer(data=data)
357+
serializer.is_valid(raise_exception=True)
358+
serializer.save()
359+
return Response(serializer.data, status=status.HTTP_201_CREATED)
360+
361+
class UserBuyerRatings(ListAPIView, DefaultOrderMixin):
362+
serializer_class = RatingSerializer
363+
permission_classes = [IsAuthenticated | IsSuperUser]
364+
default_ordering = ["-created_at"]
365+
366+
def get_queryset(self):
367+
return Rating.objects.filter(reviewed_user=self.request.user, rating_type="BUYER")
368+
369+
class UserSellerRatings(ListAPIView, DefaultOrderMixin):
370+
serializer_class = RatingSerializer
371+
permission_classes = [IsAuthenticated | IsSuperUser]
372+
default_ordering = ["-created_at"]
373+
374+
def get_queryset(self):
375+
return Rating.objects.filter(reviewed_user=self.request.user, rating_type="SELLER")
376+
377+
345378
@api_view(["POST"])
346379
@permission_classes([IsAuthenticated])
347380
def send_verification_code(request):

0 commit comments

Comments
 (0)