Skip to content
Open
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
43 changes: 41 additions & 2 deletions backend/market/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from phonenumber_field.modelfields import PhoneNumberField

Expand Down Expand Up @@ -63,7 +63,6 @@ class Tag(models.Model):
def __str__(self):
return self.name


class Listing(models.Model):
class Meta:
indexes = [
Expand Down Expand Up @@ -177,3 +176,43 @@ def approximate_location(self):
def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)

class Rating(models.Model):
class RatingType(models.TextChoices):
BUYER = "BUYER", "Buyer Rating"
SELLER = "SELLER", "Seller Rating"

class Meta:
constraints = [
models.UniqueConstraint(
fields=["reviewer", "reviewed_user", "listing"],
name="unique_rating_market",
)
]
indexes = [
models.Index(fields=["reviewer"]),
models.Index(fields=["reviewed_user"]),
models.Index(fields=["listing"]),
models.Index(fields=["created_at"]),
]

reviewer = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="ratings_given"
)
reviewed_user = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="ratings_received"
)
listing = models.ForeignKey(
Listing, on_delete=models.CASCADE, related_name="ratings"
)
score = models.PositiveIntegerField(
validators=[MinValueValidator(1), MaxValueValidator(5)]
)
rating_type = models.CharField(
max_length=10, choices=RatingType.choices, default=RatingType.BUYER
)
comment = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)

def __str__(self):
return f"Rating of {self.score} for {self.reviewed_user} by {self.reviewer}"
66 changes: 65 additions & 1 deletion backend/market/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,16 @@
)

from market.mixins import ListingTypeMixin
from market.models import Category, Item, Listing, ListingImage, Offer, Sublet, Tag
from market.models import (
Category,
Item,
Listing,
ListingImage,
Offer,
Rating,
Sublet,
Tag,
)


User = get_user_model()
Expand Down Expand Up @@ -443,3 +452,58 @@ class Meta:

def get_favorite_count(self, obj):
return obj.favorites.count()

class RatingSerializer(ModelSerializer):
reviewer = UserSerializer(read_only=True)

class Meta:
model = Rating
fields = [
"id",
"reviewer",
"reviewed_user",
"listing",
"score",
"rating_type",
"comment",
"created_at"
]
read_only_fields = ["id", "created_at", "reviewer", "rating_type"]

def validate(self, attr):
reviewer = self.context["request"].user
reviewed_user = attr["reviewed_user"]
listing = attr["listing"]

if reviewer == reviewed_user:
raise ValidationError("You cannot review yourself.")

is_seller = listing.seller == reviewer
is_buyer = listing.offers_received.filter(user=reviewer).exists()
if not is_seller and not is_buyer:
raise ValidationError(
"You can only rate users on listings you have interacted with."
)

target_is_seller = listing.seller == reviewed_user
target_is_buyer = listing.offers_received.filter(user=reviewed_user).exists()
if not target_is_seller and not target_is_buyer:
raise ValidationError(
"You cannot rate a user who is not on either side of the transaction."
)
attr["rating_type"] = "SELLER" if is_seller else "BUYER"

return attr


def validate_comment(self, value):
if self.contains_profanity(value):
raise ValidationError("The comment contains inappropriate language.")
return value

def contains_profanity(self, text):
return predict([text])[0]

def create(self, validated_data):
validated_data["reviewer"] = self.context["request"].user
return super().create(validated_data)
20 changes: 20 additions & 0 deletions backend/market/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@
Offers,
OffersMade,
OffersReceived,
Ratings,
Tags,
UserBuyerRatings,
UserFavorites,
UserSellerRatings,
get_current_user,
get_phone_status,
send_verification_code,
Expand Down Expand Up @@ -49,6 +52,23 @@
"listings/<listing_id>/offers/",
Offers.as_view({"get": "list", "post": "create", "delete": "destroy"}),
),
# Ratings for a listing (list + create)
path(
"listings/<listing_id>/ratings/",
Ratings.as_view({"get": "list", "post": "create"}),
name="listing-ratings",
),
# Current user's received ratings by type
path(
"user/ratings/buyer/",
UserBuyerRatings.as_view(),
name="user-ratings-buyer",
),
path(
"user/ratings/seller/",
UserSellerRatings.as_view(),
name="user-ratings-seller",
),
# Image Creation
path("listings/<listing_id>/images/", CreateImages.as_view()),
# Image Deletion
Expand Down
41 changes: 39 additions & 2 deletions backend/market/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from rest_framework.response import Response

from market.mixins import DefaultOrderMixin
from market.models import Listing, ListingImage, Offer, Tag
from market.models import Listing, ListingImage, Offer, Rating, Tag
from market.pagination import PageSizeOffsetPagination
from market.permissions import (
IsSuperUser,
Expand All @@ -31,6 +31,7 @@
ListingSerializerList,
ListingSerializerPublic,
OfferSerializer,
RatingSerializer,
TagSerializer,
UserSerializer,
)
Expand Down Expand Up @@ -189,7 +190,7 @@ def retrieve(self, request, *args, **kwargs):
serializer_class = ListingSerializer
else:
serializer_class = ListingSerializerPublic
serializer = serializer_class(instance)
serializer = serializer_class(instance, context={"request": request})
return Response(serializer.data)


Expand Down Expand Up @@ -342,6 +343,42 @@ def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)


class Ratings(mixins.CreateModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
serializer_class = RatingSerializer
permission_classes = [IsAuthenticated | IsSuperUser]

def get_queryset(self):
return Rating.objects.filter(listing_id=self.kwargs["listing_id"])

def create(self, request, *args, **kwargs):
data = request.data.copy()
data["listing"] = int(self.kwargs["listing_id"])
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)

class UserBuyerRatings(ListAPIView, DefaultOrderMixin):
serializer_class = RatingSerializer
permission_classes = [IsAuthenticated | IsSuperUser]
default_ordering = ["-created_at"]

def get_queryset(self):
return Rating.objects.filter(
reviewed_user=self.request.user, rating_type="BUYER"
)

class UserSellerRatings(ListAPIView, DefaultOrderMixin):
serializer_class = RatingSerializer
permission_classes = [IsAuthenticated | IsSuperUser]
default_ordering = ["-created_at"]

def get_queryset(self):
return Rating.objects.filter(
reviewed_user=self.request.user, rating_type="SELLER"
)


@api_view(["POST"])
@permission_classes([IsAuthenticated])
def send_verification_code(request):
Expand Down
Loading