-
Notifications
You must be signed in to change notification settings - Fork 1
Feat/devltz/reviews #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b20874a
86c3035
ecb4553
04bf4c6
5a16eea
a498dba
14a6847
c58a6e9
7d94b24
05c13c4
31aef89
419b305
e0a9537
9bb7a42
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # Generated by Django 5.2 on 2026-04-01 20:47 | ||
|
|
||
| import django.db.models.deletion | ||
| from django.conf import settings | ||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ('properties', '0002_rename_property_id_propertiesphotos_property'), | ||
| migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.CreateModel( | ||
| name='Reviews', | ||
| fields=[ | ||
| ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
| ('rating', models.IntegerField()), | ||
| ('comment', models.TextField(blank=True, null=True)), | ||
| ('created_at', models.DateTimeField(auto_now_add=True)), | ||
| ('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to='properties.properties')), | ||
| ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to=settings.AUTH_USER_MODEL)), | ||
| ], | ||
| options={ | ||
| 'db_table': 'reviews', | ||
| 'constraints': [models.UniqueConstraint(fields=('property', 'user'), name='unique_review_per_user_per_property')], | ||
| }, | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| from rest_framework import permissions | ||
|
|
||
| class IsAdvertiser(permissions.BasePermission): | ||
| message = "You do not have permission to do this action. Please, change your account type to advertise!" | ||
|
|
||
| def has_permission(self, request, view): | ||
| return ( | ||
| request.user.is_authenticated and | ||
| request.user.user_type == "A" | ||
| ) | ||
|
|
||
| class IsReviewOwner(permissions.BasePermission): | ||
| message = "You only can edit or delete your own reviews." | ||
|
|
||
| def has_object_permission(self, request, view, obj): | ||
| return obj.user == request.user | ||
|
|
||
| class IsPropertyOwner(permissions.BasePermission): | ||
| message = "You do not have permission to do this action." | ||
|
|
||
| def has_object_permission(self, request, view, obj): | ||
| if hasattr(obj, "owner"): | ||
| return obj.owner == request.user | ||
| if hasattr(obj, "property"): | ||
| return obj.property.owner == request.user | ||
| return False |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| from apps.properties.validators import validate_rating, validate_comment_length | ||
| from rest_framework import serializers | ||
| from apps.properties.models import Reviews | ||
|
|
||
| class ReviewsSerializer(serializers.ModelSerializer): | ||
| user_name = serializers.CharField(source="user.name", read_only=True) | ||
|
|
||
| class Meta: | ||
| model = Reviews | ||
| fields = ['id', 'user_name', 'rating', 'comment', 'created_at'] | ||
| read_only_fields = ['id', 'user_name', 'created_at'] | ||
|
|
||
| def validate_rating(self, value): | ||
| return validate_rating(value) | ||
|
|
||
| def validate_comment(self, value): | ||
| return validate_comment_length(value) | ||
|
|
||
| def validate(self, data): | ||
| request = self.context.get("request") | ||
| property_id = self.context.get("property_id") | ||
|
|
||
| if not property_id and self.instance: | ||
| property_id = self.instance.property_id | ||
|
|
||
| if not property_id: | ||
| return data | ||
| # Se estamos editando, self.instance não será Nones | ||
| queryset = Reviews.objects.filter(user=request.user, property_id=property_id) | ||
| if self.instance: | ||
| queryset = queryset.exclude(pk=self.instance.pk) | ||
| if queryset.exists(): | ||
| raise serializers.ValidationError("You have already reviewed this property.") | ||
| return data |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -6,4 +6,8 @@ | |||||
| path("<int:pk>/", property_views.RUDPropertyView.as_view()), | ||||||
| path("<int:pk>/photos/", photo_views.UploadPhotoPropertyView.as_view()), | ||||||
| path("photos/<int:pk>/", photo_views.RUDPhotoPropertyView.as_view()), | ||||||
| #path("search/filters/", property_views.FilterPropertyView.as_view()), | ||||||
| path("search/", property_views.SearchPropertyAIView.as_view()), | ||||||
| path("<int:pk>/reviews/", property_views.CreateListReviewPropertyView.as_view()), | ||||||
| path("<int:pk>/reviews/<int:review_pk>/", property_views.RUDReviewPropertyView.as_view()), | ||||||
|
||||||
| path("<int:pk>/reviews/<int:review_pk>/", property_views.RUDReviewPropertyView.as_view()), | |
| path("<int:property_pk>/reviews/<int:pk>/", property_views.RUDReviewPropertyView.as_view()), |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,42 +1,51 @@ | ||
| from django.shortcuts import get_object_or_404 | ||
| from rest_framework import generics, status | ||
| from rest_framework.views import APIView | ||
| from rest_framework import generics | ||
| from rest_framework.permissions import IsAuthenticated, AllowAny | ||
| from rest_framework.permissions import BasePermission | ||
| from rest_framework.exceptions import PermissionDenied | ||
| from apps.properties.serializers.property_serializers import PropertiesWriteSerializer, PropertiesReadSerializer | ||
| from apps.properties.models import Properties | ||
| from apps.properties.filters import PropertiesFilters | ||
| from rest_framework.response import Response | ||
|
|
||
| from rest_framework.permissions import IsAuthenticated, AllowAny, BasePermission | ||
| from apps.properties.models import Properties, Reviews | ||
| from apps.properties.filters import PropertiesFilters | ||
| from apps.properties.serializers.property_serializers import PropertiesWriteSerializer, PropertiesReadSerializer | ||
| from apps.properties.serializers.reviews_serializers import ReviewsSerializer | ||
| from apps.properties.permissions import IsAdvertiser, IsReviewOwner, IsPropertyOwner | ||
|
|
||
| # C -> Create | ||
| # R -> Read | ||
| # U -> Update | ||
| # D -> Delete | ||
|
|
||
| class IsAdvertiser(BasePermission): | ||
| message = "You do not have permission to do this action. Please, change your account type to advertise!" | ||
| def has_permission(self, request, view, obj): | ||
| return ( | ||
| request.user.is_authenticated and | ||
| request.user.user_type == "A" | ||
| ) | ||
| class CreateListReviewPropertyView(generics.ListCreateAPIView): | ||
| serializer_class = ReviewsSerializer | ||
|
|
||
| class IsPropertyOwner(BasePermission): | ||
| message = "You do not have permission to do this action." | ||
| def get_permissions(self): | ||
| if self.request.method == "GET": | ||
| return [AllowAny()] | ||
| return [IsAuthenticated()] | ||
|
|
||
| def get_queryset(self): | ||
| return Reviews.objects.filter( | ||
| property_id=self.kwargs["pk"] | ||
| ).order_by("-created_at") | ||
|
|
||
| def get_serializer_context(self): | ||
| context = super().get_serializer_context() | ||
| context["property_id"] = self.kwargs["pk"] | ||
| return context | ||
|
|
||
| def perform_create(self, serializer): | ||
| property_obj = get_object_or_404(Properties, pk=self.kwargs["pk"]) | ||
| serializer.save(user=self.request.user, property=property_obj) | ||
|
|
||
| class RUDReviewPropertyView(generics.RetrieveUpdateDestroyAPIView): | ||
| queryset = Reviews.objects.all() | ||
| serializer_class = ReviewsSerializer | ||
| lookup_url_kwarg = "review_pk" | ||
|
|
||
| def get_permissions(self): | ||
| if self.request.method in ["PUT", "PATCH", "DELETE"]: | ||
| return [IsAuthenticated(), IsReviewOwner()] | ||
| return [AllowAny()] | ||
|
Comment on lines
+39
to
+47
|
||
|
|
||
| def has_object_permission(self, request, view, obj): | ||
| if hasattr(obj, "owner"): | ||
| owner = obj.owner | ||
| elif hasattr(obj, "property"): | ||
| owner = obj.property.owner | ||
| else: | ||
| return False | ||
|
|
||
| if owner != request.user: | ||
| return False | ||
| return True | ||
|
|
||
| class CreateListPropertyView(generics.ListCreateAPIView): | ||
| queryset = Properties.objects.all().order_by("created_at") | ||
| filterset_class = PropertiesFilters | ||
|
|
@@ -48,8 +57,8 @@ def get_serializer_class(self): | |
|
|
||
| def get_permissions(self): | ||
| if self.request.method == "POST": | ||
| return [IsAuthenticated, IsAdvertiser] | ||
| return [AllowAny] | ||
| return [IsAuthenticated(), IsAdvertiser()] | ||
| return [AllowAny()] | ||
|
|
||
| def perform_create(self, serializer): | ||
| serializer.save(owner_id=self.request.user.id) | ||
|
|
@@ -65,16 +74,15 @@ def get_serializer_class(self): | |
|
|
||
| def get_permissions(self): | ||
| if self.request.method in ["PUT", "PATCH", "DELETE"]: | ||
| return [IsAuthenticated, IsPropertyOwner] | ||
| return [IsAuthenticated(), IsPropertyOwner()] | ||
| return [AllowAny()] | ||
|
|
||
| def destroy(self, request, *args, **kwargs): | ||
| self.perform_destroy(self.get_object()) | ||
|
|
||
| instance = self.get_object() | ||
| self.perform_destroy(instance) | ||
| return Response({ | ||
| "message": "Delete successfull!" | ||
| }, status=204) | ||
|
|
||
| "message": "Delete successful!" | ||
| }, status=status.HTTP_204_NO_CONTENT) | ||
|
|
||
| class SearchPropertyAIView(APIView): | ||
| pass | ||
| pass | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| # Generated by Django 5.2 on 2026-04-07 21:06 | ||
|
|
||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ('properties', '0003_reviews'), | ||
| ('users', '0003_alter_user_managers'), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name='user', | ||
| name='favorites', | ||
| field=models.ManyToManyField(blank=True, related_name='favorited_by', to='properties.properties'), | ||
| ), | ||
| ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
get_average_rating()performs an aggregate query per property instance. When serializing a list of properties, this will create an N+1 query pattern. Consider annotating the queryset with the average rating (or usingprefetch_related+ aggregation) in the view so the rating can be returned without per-row queries.