diff --git a/backend/market/migrations/0004_rename_image_itemimage_images.py b/backend/market/migrations/0004_rename_image_itemimage_images.py new file mode 100644 index 00000000..623d33f7 --- /dev/null +++ b/backend/market/migrations/0004_rename_image_itemimage_images.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.2 on 2024-11-08 23:07 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("market", "0003_alter_item_category_alter_item_seller_and_more"), + ] + + operations = [ + migrations.RenameField( + model_name="itemimage", + old_name="image", + new_name="images", + ), + ] diff --git a/backend/market/migrations/0005_rename_images_itemimage_image.py b/backend/market/migrations/0005_rename_images_itemimage_image.py new file mode 100644 index 00000000..04db11ff --- /dev/null +++ b/backend/market/migrations/0005_rename_images_itemimage_image.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.2 on 2024-11-08 23:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("market", "0004_rename_image_itemimage_images"), + ] + + operations = [ + migrations.RenameField( + model_name="itemimage", + old_name="images", + new_name="image", + ), + ] diff --git a/backend/market/migrations/0006_alter_itemimage_item.py b/backend/market/migrations/0006_alter_itemimage_item.py new file mode 100644 index 00000000..2364b16f --- /dev/null +++ b/backend/market/migrations/0006_alter_itemimage_item.py @@ -0,0 +1,21 @@ +# Generated by Django 5.0.2 on 2024-11-08 23:11 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("market", "0005_rename_images_itemimage_image"), + ] + + operations = [ + migrations.AlterField( + model_name="itemimage", + name="item", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, related_name="images", to="market.item" + ), + ), + ] diff --git a/backend/market/models.py b/backend/market/models.py index 57a33d54..6b5c338a 100644 --- a/backend/market/models.py +++ b/backend/market/models.py @@ -65,8 +65,13 @@ class Sublet(models.Model): start_date = models.DateTimeField() end_date = models.DateTimeField() + def delete(self, *args, **kwargs): + if self.item: + self.item.delete() + super().delete(*args, **kwargs) + # TODO: Verify that this S2 bucket exists. Check if we need to make it manually or will Django make it for us? class ItemImage(models.Model): - item = models.ForeignKey(Item, on_delete=models.CASCADE) + item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name="images") image = models.ImageField(upload_to="marketplace/images") \ No newline at end of file diff --git a/backend/market/serializers.py b/backend/market/serializers.py index 16c98a73..62023681 100644 --- a/backend/market/serializers.py +++ b/backend/market/serializers.py @@ -66,7 +66,7 @@ class Meta: # complex item serializer for use in C/U/D + getting info about a singular tag class ItemSerializer(serializers.ModelSerializer): # amenities = ItemSerializer(many=True, required=False) - # images = ItemImageURLSerializer(many=True, required=False) + images = ItemImageURLSerializer(many=True, required=False) tags = serializers.PrimaryKeyRelatedField( many=True, queryset=Tag.objects.all(), required=False @@ -78,6 +78,7 @@ class ItemSerializer(serializers.ModelSerializer): favorites = serializers.PrimaryKeyRelatedField( many=True, queryset=User.objects.all(), required=False ) + class Meta: model = Item @@ -86,7 +87,7 @@ class Meta: "created_at", "seller", "buyer", - # "images" + "images" ] fields = [ "id", @@ -100,7 +101,7 @@ class Meta: "price", "negotiable", "expires_at", - # "images", + "images", # images are now created/deleted through a separate endpoint (see urls.py) # this serializer isn't used for getting, # but gets on tags will include ids/urls for images @@ -152,7 +153,8 @@ def destroy(self, instance): raise serializers.ValidationError("You do not have permission to delete this item.") -class ItemSerializerRead(serializers.ModelSerializer): +# simple tag serializer for use when pulling all serializers/etc +class ItemSerializerSimple(serializers.ModelSerializer): tags = serializers.PrimaryKeyRelatedField( many=True, queryset=Tag.objects.all(), required=False ) @@ -164,7 +166,6 @@ class ItemSerializerRead(serializers.ModelSerializer): class Meta: model = Item - read_only_fields = ["id", "created_at", "seller", "buyer"] fields = [ "id", "seller", @@ -172,29 +173,11 @@ class Meta: "category", "favorites", "title", - "description", - "external_link", "price", "negotiable", - "expires_at", "images", ] - - -# simple tag serializer for use when pulling all serializers/etc -class ItemSerializerSimple(serializers.ModelSerializer): - tags = serializers.PrimaryKeyRelatedField( - many=True, queryset=Tag.objects.all(), required=False - ) - images = ItemImageURLSerializer(many=True, required=False) - category = serializers.PrimaryKeyRelatedField( - queryset=Category.objects.all(), required=True - ) - seller = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), required=False) - - class Meta: - model = Item - fields = [ + read_only_fields = [ "id", "seller", "tags", @@ -205,7 +188,6 @@ class Meta: "negotiable", "images", ] - read_only_fields = ["id", "seller"] class SubletSerializer(serializers.ModelSerializer): diff --git a/backend/market/urls.py b/backend/market/urls.py index b75b589e..d2e3738b 100644 --- a/backend/market/urls.py +++ b/backend/market/urls.py @@ -48,7 +48,7 @@ # Image Creation path("items//images/", CreateImages.as_view()), # Image Deletion - path("items/images//", DeleteImage.as_view()), + path("items/images//", DeleteImage.as_view()), ] urlpatterns = router.urls + additional_urls diff --git a/backend/market/views.py b/backend/market/views.py index 5d824f4a..61878d17 100644 --- a/backend/market/views.py +++ b/backend/market/views.py @@ -22,8 +22,6 @@ ItemImageSerializer, ItemImageURLSerializer, ItemSerializer, - ItemSerializerRead, - ItemSerializerRead, ItemSerializerSimple, SubletSerializer, ) @@ -71,6 +69,42 @@ def get_queryset(self): return Offer.objects.filter(user=user) +def apply_filters(queryset, params, filter_mappings, user=None, is_sublet=False, tag_field="tags__name"): + # Build dynamic filters based on filter mappings + filters = {} + for param, field in filter_mappings.items(): + if param_value := params.get(param): + filters[field] = param_value + + # Handle seller/owner filtering based on user ownership + + + # Apply basic filters to the queryset + + # Exclude specific categories if provided + if not is_sublet: + queryset = queryset.exclude(category__name__in=["Sublet"]) + if params.get("seller", "false").lower() == "true" and user: + filters["seller"] = user + else: + filters["expires_at__gte"] = timezone.now() + else : + queryset = queryset.filter(item__category__name__in=["Sublet"]) + if params.get("seller", "false").lower() == "true" and user: + filters["item__seller"] = user + else: + filters["item__expires_at__gte"] = timezone.now() + + queryset = queryset.filter(**filters) + + # Apply tag filtering iteratively if "tags" parameter is provided + if tags := params.getlist("tags"): + for tag in tags: + queryset = queryset.filter(**{tag_field: tag}) + + return queryset + + class Items(viewsets.ModelViewSet): """ list: @@ -87,187 +121,73 @@ class Items(viewsets.ModelViewSet): """ permission_classes = [ItemOwnerPermission | IsSuperUser] - - def get_serializer_class(self): - return ItemSerializerRead if self.action == "retrieve" else ItemSerializer - - def get_queryset(self): - return Item.objects.prefetch_related('sublet').all() - - def create(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) # Check if the data is valid - instance = serializer.save() # Create the Item - instance_serializer = ItemSerializerRead(instance=instance, context={"request": request}) - - #record_analytics(Metric.SUBLET_CREATED, request.user.username) - - return Response(instance_serializer.data, status=status.HTTP_201_CREATED) - - def update(self, request, *args, **kwargs): - partial = kwargs.pop("partial", False) - instance = self.get_object() - serializer = self.get_serializer(instance, data=request.data, partial=partial) - serializer.is_valid(raise_exception=True) - self.perform_update(serializer) - - queryset = self.filter_queryset(self.get_queryset()) - # no clue what this does but I copied it from the DRF source code - if queryset._prefetch_related_lookups: - # If 'prefetch_related' has been applied to a queryset, we need to - # forcibly invalidate the prefetch cache on the instance, - # and then re-prefetch related objects - instance._prefetched_objects_cache = {} - prefetch_related_objects([instance], *queryset._prefetch_related_lookups) - return Response(ItemSerializerRead(instance=instance).data) - - # This is currently redundant but will leave for use when implementing image creation - # def create(self, request, *args, **kwargs): - # # amenities = request.data.pop("amenities", []) - # new_data = request.data - # amenities = new_data.pop("amenities", []) - - # # check if valid amenities - # try: - # amenities = [Amenity.objects.get(name=amenity) for amenity in amenities] - # except Amenity.DoesNotExist: - # return Response({"amenities": "Invalid amenity"}, status=status.HTTP_400_BAD_REQUEST) - - # serializer = self.get_serializer(data=new_data) - # serializer.is_valid(raise_exception=True) - # item = serializer.save() - # item.amenities.set(amenities) - # item.save() - # return Response(serializer.data, status=status.HTTP_201_CREATED) - - def destroy(self, request, *args, **kwargs): - instance = self.get_object() - if self.request.user == instance.seller or self.request.user.is_superuser: - if hasattr(instance, 'sublet'): - instance.sublet.delete() - - instance.delete() - - return Response(status=status.HTTP_204_NO_CONTENT) - else: - raise serializers.ValidationError("You do not have permission to delete this item.") - + serializer_class = ItemSerializer + queryset = Item.objects.all() def list(self, request, *args, **kwargs): """Returns a list of Items that match query parameters and user ownership.""" params = request.query_params - category = params.get("category") - tags = params.getlist("tags") - title = params.get("title") - seller = params.get("seller", "false") # Defaults to False if not specified - min_price = params.get("min_price", None) - max_price = params.get("max_price", None) - negotiable = params.get("negotiable", None) - queryset = self.get_queryset() - if seller.lower() == "true": - queryset = queryset.filter(seller=request.user) - else: - queryset = queryset.filter(expires_at__gte=timezone.now()) - queryset = queryset.exclude(category__name="Sublet") - if category: - queryset = queryset.filter(category__name=category) - if title: - queryset = queryset.filter(title__icontains=title) - if tags: - for tag in tags: - queryset = queryset.filter(tags__name=tag) - if min_price: - queryset = queryset.filter(price__gte=min_price) - if max_price: - queryset = queryset.filter(price__lte=max_price) - if negotiable: - queryset = queryset.filter(negotiable=negotiable) - - - #record_analytics(Metric.SUBLET_BROWSE, request.user.username) + # Define a dictionary mapping query parameters to filter fields + filter_mappings = { + "category": "category__name", + "title": "title__icontains", + "min_price": "price__gte", + "max_price": "price__lte", + "negotiable": "negotiable", + } + + # Apply filters using the helper function + queryset = apply_filters( + queryset=queryset, + params=params, + filter_mappings=filter_mappings, + user=request.user, + is_sublet=True + ) + # Serialize and return the queryset - serializer = ItemSerializerSimple(queryset, many=True) + serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) class Sublets(viewsets.ModelViewSet): permission_classes = [SubletOwnerPermission | IsSuperUser] serializer_class = SubletSerializer - - def get_queryset(self): - return Sublet.objects.filter(item__isnull=False).prefetch_related('item') - - def create(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data, context={"request": request}) - serializer.is_valid(raise_exception=True) - sublet_instance = serializer.save() - response_serializer = self.get_serializer(sublet_instance) - - # record_analytics(Metric.SUBLET_CREATED, request.user.username) - - return Response(response_serializer.data, status=status.HTTP_201_CREATED) - - def update(self, request, *args, **kwargs): - partial = kwargs.pop("partial", False) - instance = self.get_object() - serializer = self.get_serializer(instance, data=request.data, partial=partial, context={"request": request}) - serializer.is_valid(raise_exception=True) - serializer.save() - - queryset = self.filter_queryset(self.get_queryset()) - if queryset._prefetch_related_lookups: - instance._prefetched_objects_cache = {} - return Response(self.get_serializer(instance).data, status=status.HTTP_200_OK) - - def destroy(self, request, *args, **kwargs): - instance = self.get_object() - if request.user == instance.item.seller or request.user.is_superuser: - if hasattr(instance, 'item'): - instance.item.delete() - instance.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - else: - raise serializers.ValidationError("You do not have permission to delete this item.") + queryset = Sublet.objects.all() def list(self, request, *args, **kwargs): """Returns a filtered list of Sublets based on query parameters.""" params = request.query_params - queryset = self.get_queryset().filter( - item__category__name="Sublet", - item__expires_at__gte=timezone.now() if params.get("seller", "false").lower() != "true" else timezone.now() + queryset = self.get_queryset() + + # Define filter mappings + filter_mappings = { + "title": "item__title__icontains", + "min_price": "item__price__gte", + "max_price": "item__price__lte", + "negotiable": "item__negotiable", + "address": "address__icontains", + "beds": "beds", + "baths": "baths", + "start_date_min": "start_date__gte", + "start_date_max": "start_date__lte", + "end_date_min": "end_date__gte", + "end_date_max": "end_date__lte", + } + + # Apply filters using the helper function + queryset = apply_filters( + queryset=queryset, + params=params, + filter_mappings=filter_mappings, + user=request.user, + is_sublet=True, + tag_field="item__tags__name", ) - if params.get("seller", "false").lower() == "true": - queryset = queryset.filter(seller=request.user) - if title := params.get("title"): - queryset = queryset.filter(item__title__icontains=title) - if tags := params.getlist("tags"): - for tag in tags: - queryset = queryset.filter(item__tags__name=tag) - if min_price := params.get("min_price"): - queryset = queryset.filter(item__price__gte=min_price) - if max_price := params.get("max_price"): - queryset = queryset.filter(item__price__lte=max_price) - if negotiable := params.get("negotiable"): - queryset = queryset.filter(item__negotiable=negotiable) - if address := params.get("address"): - queryset = queryset.filter(address__icontains=address) - if beds := params.get("beds"): - queryset = queryset.filter(beds=beds) - if baths := params.get("baths"): - queryset = queryset.filter(baths=baths) - if start_date_min := params.get("start_date_min"): - queryset = queryset.filter(start_date__gte=start_date_min) - if start_date_max := params.get("start_date_max"): - queryset = queryset.filter(start_date__lte=start_date_max) - if end_date_min := params.get("end_date_min"): - queryset = queryset.filter(end_date__gte=end_date_min) - if end_date_max := params.get("end_date_max"): - queryset = queryset.filter(end_date__lte=end_date_max) - - # record_analytics(Metric.SUBLET_BROWSE, request.user.username) + # Serialize and return the queryset serializer = self.get_serializer(queryset, many=True) return Response(serializer.data)