diff --git a/backend/auction/urls.py b/backend/auction/urls.py index 4094b45..7ea380c 100644 --- a/backend/auction/urls.py +++ b/backend/auction/urls.py @@ -4,8 +4,8 @@ from .views import ( AuctionDayApiView, AuctionDetailApiView, + AuctionItemsApiView, AuctionListApiView, - AuctionVehiclesApiView, BidderVerificationApiView, CurrentAuctionApiView, GetSavedUnitApiView, @@ -29,7 +29,7 @@ ), path( "//days//vehicles", - AuctionVehiclesApiView.as_view(), + AuctionItemsApiView.as_view(), name="auction_vehicles", ), path( diff --git a/backend/auction/views.py b/backend/auction/views.py index d7cd469..a5ab3f5 100644 --- a/backend/auction/views.py +++ b/backend/auction/views.py @@ -1,10 +1,12 @@ from datetime import timedelta from django.contrib.contenttypes.models import ContentType +from django.core.paginator import Paginator from django.db.models import Prefetch, Q from django.utils import timezone from rest_framework import status from rest_framework.generics import get_object_or_404 +from rest_framework.pagination import PageNumberPagination from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView @@ -245,8 +247,6 @@ def post(self, request, **kwargs): vehicle_id = kwargs.get("vehicle_id") auction_id = kwargs.get("auction_id") - # Replace this later to fetch authentication details - # from headers instead of body vehicle = get_object_or_404(Vehicle, id=vehicle_id) auction_for_vehicle = Auction.objects.get(id=auction_id) bidder = self.cognitoService.get_user_details(bidder_id) @@ -316,48 +316,79 @@ def get(self, request, **kwargs): return Response({"vehicles": vehicle_data}, status=status.HTTP_200_OK) -class AuctionVehiclesApiView(APIView): - """ - An endpoint to retrieve an auction's associated vehicles - """ +class AuctionItemsApiView(APIView): + permission_classes = [IsAuthenticated] - cognitoService = AWSCognitoService() + def get_filtered_queryset(self, model, auction_day_id, filters): + """ + Filters the queryset of a given model based on the provided filters + and the relation to AuctionItem through ContentType. + """ + content_type = ContentType.objects.get_for_model(model) + related_item_ids = AuctionItem.objects.filter( + auction_day_id=auction_day_id, content_type=content_type + ).values_list("object_id", flat=True) - def get_permissions(self): - if self.request.method == "GET": - self.permission_classes = [IsAuthenticated] - else: - self.permission_classes = [IsAdminUser] - return super().get_permissions() + return model.objects.filter(id__in=related_item_ids, **filters) def get(self, request, auction_id, auction_day_id): - auction_day = get_object_or_404(AuctionDay, id=auction_day_id) - - auction_items = AuctionItem.objects.filter(auction_day=auction_day) + item_type = request.query_params.get("item_type", None) + type = request.query_params.get("type", None) + brand_id = request.query_params.get("brand", None) + min_price = request.query_params.get("min_price", None) + max_price = request.query_params.get("max_price", None) + search_term = request.query_params.get("search", None) + + filters = {} + if type: + filters["type"] = type + if brand_id: + filters["brand"] = brand_id + if min_price: + filters["current_price__gte"] = min_price + if max_price: + filters["current_price__lte"] = max_price + if search_term: + filters["description__icontains"] = search_term + + # Apply filters to each model's queryset + if item_type == "trucks": + combined_qs = self.get_filtered_queryset(Vehicle, auction_day_id, filters) + elif item_type == "equipment": + combined_qs = self.get_filtered_queryset(Equipment, auction_day_id, filters) + elif item_type == "trailers": + combined_qs = self.get_filtered_queryset(Trailer, auction_day_id, filters) + else: + vehicle_qs = self.get_filtered_queryset(Vehicle, auction_day_id, filters) + equipment_qs = self.get_filtered_queryset( + Equipment, auction_day_id, filters + ) + trailer_qs = self.get_filtered_queryset(Trailer, auction_day_id, filters) - vehicle_list = [] - equipment_list = [] - trailer_list = [] + combined_qs = list(vehicle_qs) + list(equipment_qs) + list(trailer_qs) - for auction_item in auction_items: - if isinstance(auction_item.content_object, Vehicle): - vehicle_list.append(auction_item.content_object) - elif isinstance(auction_item.content_object, Equipment): - equipment_list.append(auction_item.content_object) - elif isinstance(auction_item.content_object, Trailer): - trailer_list.append(auction_item.content_object) + # Implement custom pagination + page_number = request.query_params.get("page", 1) + paginator = Paginator(combined_qs, 5) + page_obj = paginator.get_page(page_number) - vehicle_data = [{"id": vehicle.id} for vehicle in vehicle_list] - equipment_data = [{"id": equipment.id} for equipment in equipment_list] - trailer_data = [{"id": trailer.id} for trailer in trailer_list] + serialized_data = [] + for obj in page_obj: + if isinstance(obj, Vehicle): + serializer = VehicleSerializer(obj) + elif isinstance(obj, Equipment): + serializer = EquipmentSerializer(obj) + elif isinstance(obj, Trailer): + serializer = TrailerSerializer(obj) + serialized_data.append(serializer.data) return Response( { - "vehicles": vehicle_data, - "equipment": equipment_data, - "trailers": trailer_data, - }, - status=status.HTTP_200_OK, + "count": paginator.count, + "next": page_obj.has_next(), + "previous": page_obj.has_previous(), + "results": serialized_data, + } ) def post(self, request, auction_id, auction_day_id): diff --git a/backend/bid/admin.py b/backend/bid/admin.py index b74914c..b688e4f 100644 --- a/backend/bid/admin.py +++ b/backend/bid/admin.py @@ -10,12 +10,13 @@ class BidAdmin(admin.ModelAdmin): "amount", "bidder", "auction", + "auction_day", "content_type", "object_id", "created_at", ) - search_fields = ("bidder__username", "auction__name", "amount") - list_filter = ("auction", "bidder", "content_type") + search_fields = ("bidder", "amount") + list_filter = ("auction", "auction_day", "content_type") def formfield_for_foreignkey(self, db_field, request, **kwargs): if db_field.name == "content_type": diff --git a/backend/bid/migrations/0003_bid_auction_day.py b/backend/bid/migrations/0003_bid_auction_day.py new file mode 100644 index 0000000..73e595c --- /dev/null +++ b/backend/bid/migrations/0003_bid_auction_day.py @@ -0,0 +1,24 @@ +# Generated by Django 5.0.4 on 2024-04-10 19:30 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("auction", "0003_alter_auctionverifieduser_cognito_user_id"), + ("bid", "0002_alter_bid_bidder"), + ] + + operations = [ + migrations.AddField( + model_name="bid", + name="auction_day", + field=models.ForeignKey( + default=0, + on_delete=django.db.models.deletion.PROTECT, + to="auction.auctionday", + ), + preserve_default=False, + ), + ] diff --git a/backend/bid/models.py b/backend/bid/models.py index 363e0a2..325df28 100644 --- a/backend/bid/models.py +++ b/backend/bid/models.py @@ -2,7 +2,7 @@ from django.contrib.contenttypes.models import ContentType from django.db import models -from auction.models import Auction +from auction.models import Auction, AuctionDay from core.models import MainModel @@ -14,6 +14,7 @@ class Bid(MainModel): amount = models.IntegerField(null=False) bidder = models.CharField(null=False, max_length=500) auction = models.ForeignKey(Auction, on_delete=models.PROTECT) + auction_day = models.ForeignKey(AuctionDay, on_delete=models.PROTECT) content_type = models.ForeignKey(ContentType, on_delete=models.PROTECT) object_id = models.UUIDField() content_object = GenericForeignKey("content_type", "object_id") diff --git a/backend/bid/signals.py b/backend/bid/signals.py index a825361..d4b6382 100644 --- a/backend/bid/signals.py +++ b/backend/bid/signals.py @@ -1,8 +1,7 @@ # signals.py -import json - from asgiref.sync import async_to_sync from channels.layers import get_channel_layer +from django.core.exceptions import ObjectDoesNotExist from django.db.models.signals import post_delete, post_save from django.dispatch import receiver @@ -13,24 +12,42 @@ channel_layer = get_channel_layer() +def update_current_price(bid_instance): + content_type = bid_instance.content_type + object_id = bid_instance.object_id + bid_amount = bid_instance.amount + + model_class = content_type.model_class() + + try: + if hasattr(model_class, "current_price"): + item = model_class.objects.get(pk=object_id) + item.current_price = bid_amount + item.save() + except ObjectDoesNotExist: + print(f"Item of type {content_type.model} with ID {object_id} not found.") + + @receiver(post_save, sender=Bid) -def bid_updated(sender, instance, **kwargs): - current_date = instance.auction.start_date.date() - auction_day = AuctionDay.objects.filter( - auction=instance.auction, date=current_date - ).first() - - bid_data = { - "id": str(instance.id), - "amount": instance.amount, - "auction_id": str(instance.auction.id), - "auction_day_id": str(auction_day.id) if auction_day else None, - "vehicle_id": str(instance.object_id), - "bidder": str(instance.bidder), - } - async_to_sync(channel_layer.group_send)( - "bid_updates", {"type": "bid.update", "bid_data": bid_data} - ) +def bid_updated(sender, instance, created, **kwargs): + if created: + bid_data = { + "id": str(instance.id), + "amount": instance.amount, + "auction_id": str(instance.auction.id), + "auction_day_id": str(instance.auction_day.id) + if instance.auction_day + else None, + "object_id": str(instance.object_id), + "bidder_id": str(instance.bidder), + } + + async_to_sync(channel_layer.group_send)( + "auction_{}".format(instance.auction_id), + {"type": "bid.update", "bid_data": bid_data}, + ) + + update_current_price(instance) @receiver(post_delete, sender=Bid) diff --git a/backend/bid/views.py b/backend/bid/views.py index 4ad0985..a673319 100644 --- a/backend/bid/views.py +++ b/backend/bid/views.py @@ -1,11 +1,12 @@ +from django.apps import apps from django.contrib.contenttypes.models import ContentType from rest_framework import status from rest_framework.generics import get_object_or_404 -from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView -from auction.models import Auction +from auction.models import Auction, AuctionDay from services.AWSCognitoService import AWSCognitoService from .models import Bid @@ -14,91 +15,57 @@ class BidListApiView(APIView): permission_classes = [IsAuthenticated] - serializer_class = BidSerializer cognitoService = AWSCognitoService() - def get(self, request, *args, **kwargs): - """ - Get all bids - """ - bids = Bid.objects.all() - serializer = BidSerializer(bids, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - def post(self, request, *args, **kwargs): - """ - Create a bid - """ data = request.data - amount = data.get("amount") - bidder_id = data.get("bidder") - auction_id = data.get("auction") - content_type_name = data.get("content_type") - object_id = data.get("object_id") - - # Check for missing fields - if not all([amount, bidder_id, auction_id, content_type_name, object_id]): - return Response( - {"error": "Missing fields in request."}, - status=status.HTTP_400_BAD_REQUEST, - ) + item_type = data.get("content_type", "truck") + + model_map = { + "truck": "vehicle", + "equipment": "equipment", + "trailer": "trailer", + } + model_name = model_map.get(item_type, "vehicle") - # Validate ContentType try: - content_type = ContentType.objects.get(model=content_type_name) - if content_type_name not in ["vehicle", "equipment", "trailer"]: - raise ValueError("Invalid content type") - except (ContentType.DoesNotExist, ValueError): + model = apps.get_model(app_label="vehicle", model_name=model_name) + content_type = ContentType.objects.get_for_model(model) + except (LookupError, ContentType.DoesNotExist): return Response( {"error": "Invalid content type."}, status=status.HTTP_400_BAD_REQUEST ) - # Validate bidder and auction - try: - auction = Auction.objects.get(id=auction_id) - except Auction.DoesNotExist: + required_fields = ["amount", "bidder_id", "auction_id", "object_id"] + if not all(field in data for field in required_fields): return Response( - {"error": "Invalid auction."}, + {"error": "Missing fields in request."}, status=status.HTTP_400_BAD_REQUEST, ) - bidder = self.cognitoService.get_user_details(bidder_id) + bidder = self.cognitoService.get_user_details(data.get("bidder_id")) if not bidder: return Response( - {"error": "Invalid bidder."}, - status=status.HTTP_400_BAD_REQUEST, + {"error": "Invalid bidder."}, status=status.HTTP_400_BAD_REQUEST ) - # Validate object_id - model_class = content_type.model_class() - try: - model_class.objects.get(id=object_id) - except model_class.DoesNotExist: - return Response( - {"error": "Invalid object ID for the given content type."}, - status=status.HTTP_400_BAD_REQUEST, - ) + item = get_object_or_404(model, id=data.get("object_id")) - highest_bid = ( - Bid.objects.filter( - content_type=content_type, object_id=data.get("object_id") - ) - .order_by("-amount") - .first() - ) - if highest_bid and int(amount) <= highest_bid.amount: + highest_bid = Bid.objects.filter(item=item).order_by("-amount").first() + if highest_bid and int(data["amount"]) <= highest_bid.amount: return Response( {"error": "Your bid must be higher than the current highest bid."}, status=status.HTTP_400_BAD_REQUEST, ) bid = Bid.objects.create( - amount=amount, - bidder=bidder, - auction=auction, + amount=data["amount"], + bidder=data["bidder_id"], + auction_id=data["auction_id"], + auction_day_id=data.get("auction_day_id"), content_type=content_type, - object_id=object_id, + object_id=data["object_id"], ) serialized_data = self.serializer_class(bid) @@ -106,10 +73,6 @@ def post(self, request, *args, **kwargs): class BidDetailApiView(APIView): - """ - Retrieve, update or delete an bid instance. - """ - permission_classes = [IsAuthenticated] def get(self, request, bid_id, format=None): @@ -119,7 +82,7 @@ def get(self, request, bid_id, format=None): def put(self, request, bid_id, format=None): bid = get_object_or_404(Bid, id=bid_id) - serializer = BidSerializer(bid, data=request.data) + serializer = BidSerializer(bid, data=request.data, partial=True) if serializer.is_valid(): serializer.save() return Response(serializer.data) diff --git a/backend/vehicle/admin.py b/backend/vehicle/admin.py index e75e8cf..c5017d6 100644 --- a/backend/vehicle/admin.py +++ b/backend/vehicle/admin.py @@ -1,4 +1,5 @@ from django.contrib import admin +from django.contrib.contenttypes.admin import GenericTabularInline from django.contrib.contenttypes.models import ContentType from .models import ( @@ -13,14 +14,21 @@ ) +class UnitImageInline(GenericTabularInline): + model = UnitImage + extra = 1 + + class BrandAdmin(admin.ModelAdmin): list_display = ("id", "name", "created_at") search_fields = ("name",) + readonly_fields = ("created_at",) class TypeAdmin(admin.ModelAdmin): list_display = ("id", "name", "created_at") search_fields = ("name",) + readonly_fields = ("created_at",) class VehicleAdmin(admin.ModelAdmin): @@ -31,7 +39,7 @@ class VehicleAdmin(admin.ModelAdmin): "model_number", "chassis_number", "brand", - "vehicle_type", + "type", "is_sold", "created_at", ) @@ -39,60 +47,69 @@ class VehicleAdmin(admin.ModelAdmin): "model_number", "chassis_number", "brand__name", - "vehicle_type__name", + "type__name", ) - list_filter = ("brand", "vehicle_type", "is_sold") + list_filter = ("brand", "type", "is_sold") + readonly_fields = ("created_at",) + inlines = [UnitImageInline] class EquipmentAdmin(admin.ModelAdmin): list_display = ( "id", "unicode_id", - "prefix_id", + "model_number", "chassis_number", "engine_number", "brand", - "equipment_type", + "type", "created_at", ) search_fields = ( - "prefix_id", + "model_number", "chassis_number", "engine_number", "brand__name", - "equipment_type__name", + "type__name", ) - list_filter = ("brand", "equipment_type") + list_filter = ("brand", "type") + readonly_fields = ("created_at",) + inlines = [UnitImageInline] class SupplierAdmin(admin.ModelAdmin): list_display = ("id", "name", "created_at") search_fields = ("name",) + readonly_fields = ("created_at",) class TrailerAdmin(admin.ModelAdmin): list_display = ( "id", "unicode_id", - "chassis_number", + "model_number", "supplier", - "trailer_type", + "type", "number_of_axles", "created_at", ) - search_fields = ("chassis_number", "supplier__name", "trailer_type__name") - list_filter = ("supplier", "trailer_type") + search_fields = ("model_number", "supplier__name", "type__name") + list_filter = ("supplier", "type") + readonly_fields = ("created_at",) + inlines = [UnitImageInline] class UnitImageAdmin(admin.ModelAdmin): list_display = ("id", "image_url", "content_type", "object_id", "created_at") search_fields = ("content_type__model",) list_filter = ("content_type",) + readonly_fields = ("created_at",) def formfield_for_foreignkey(self, db_field, request, **kwargs): if db_field.name == "content_type": - valid_models = ["vehicle", "equipment", "trailer"] - kwargs["queryset"] = ContentType.objects.filter(model__in=valid_models) + kwargs["queryset"] = ContentType.objects.filter( + model__in=["vehicle", "equipment", "trailer"] + ) return super().formfield_for_foreignkey(db_field, request, **kwargs) @@ -107,15 +124,17 @@ class SavedUnitsAdmin(admin.ModelAdmin): ) search_fields = ("auction_id__name", "content_type__model") list_filter = ("auction_id", "content_type") + readonly_fields = ("created_at",) def formfield_for_foreignkey(self, db_field, request, **kwargs): if db_field.name == "content_type": - valid_models = ["vehicle", "equipment", "trailer"] - kwargs["queryset"] = ContentType.objects.filter(model__in=valid_models) + kwargs["queryset"] = ContentType.objects.filter( + model__in=["vehicle", "equipment", "trailer"] + ) return super().formfield_for_foreignkey(db_field, request, **kwargs) -# Register your models here +# Register your models and their admins admin.site.register(Brand, BrandAdmin) admin.site.register(Type, TypeAdmin) admin.site.register(Vehicle, VehicleAdmin) diff --git a/backend/vehicle/helpers.py b/backend/vehicle/helpers.py index 2d1f1ed..37107b4 100644 --- a/backend/vehicle/helpers.py +++ b/backend/vehicle/helpers.py @@ -6,46 +6,14 @@ from .serializers import VehicleSerializer -def infinite_filter(request): - url_parameter = request.GET.get("search") - if url_parameter: - limit = request.GET.get("l") - offset = request.GET.get("o") - if limit and offset: - return Vehicle.objects.filter(unicode_id__icontains=url_parameter)[ - : int(offset) + int(limit) - ] - elif limit: - return Vehicle.objects.filter(unicode_id__icontains=url_parameter)[ - : int(limit) - ] - else: - return Vehicle.objects.filter(unicode_id__icontains=url_parameter)[:15] - return Vehicle.objects.all()[:40] - - -def has_more_data(request): - offset = request.GET.get("o") - limit = request.GET.get("l") - if offset: - return Vehicle.objects.all().count() > (int(offset) + int(limit)) - - elif limit: - return Vehicle.objects.all().count() > int(limit) - else: - return False - - def parse_excel_to_vehicle(excel_file): df0 = pd.read_excel(excel_file, sheet_name=0, engine="openpyxl") - # empty list to store Vehicle object vehicles = [] - # iterate through rows in dataframe for index, row in df0.iterrows(): if pd.isna(row.iloc[2]) or pd.isna(row.iloc[19]) or pd.isna(row.iloc[1]): - continue # exit the loop if an empty cell is encountered + continue vehicle_data = { "unicode_id": row["UNICODE"], @@ -54,7 +22,7 @@ def parse_excel_to_vehicle(excel_file): "engine_number": row["ENGINE NUMBER"], "description": row["DESCRIPTION"], "brand": row["BRAND"], - "vehicle_type": row["TYPE"], + "type": row["TYPE"], "minimum_price": row["SALE PRICE (PhP)"], # "is_sold": row[""], "remarks": row["REMARKS"], @@ -71,7 +39,7 @@ def parse_excel_to_vehicle(excel_file): "body_condition": row["Body"], } brand_id = vehicle_data.pop("brand", None) - type_id = vehicle_data.pop("vehicle_type", None) + type_id = vehicle_data.pop("type", None) brand = Brand.objects.filter(name=brand_id).first() if brand is None: brand = Brand.objects.create(name=brand_id) diff --git a/backend/vehicle/migrations/0002_rename_minimum_price_vehicle_current_price_and_more.py b/backend/vehicle/migrations/0002_rename_minimum_price_vehicle_current_price_and_more.py new file mode 100644 index 0000000..c472958 --- /dev/null +++ b/backend/vehicle/migrations/0002_rename_minimum_price_vehicle_current_price_and_more.py @@ -0,0 +1,75 @@ +# Generated by Django 5.0.4 on 2024-04-10 19:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("vehicle", "0001_initial"), + ] + + operations = [ + migrations.RenameField( + model_name="vehicle", + old_name="minimum_price", + new_name="current_price", + ), + migrations.RemoveField( + model_name="equipment", + name="prefix_id", + ), + migrations.RemoveField( + model_name="trailer", + name="chassis_number", + ), + migrations.AddField( + model_name="equipment", + name="current_price", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="equipment", + name="model_number", + field=models.CharField(blank=True, max_length=150, null=True), + ), + migrations.AddField( + model_name="equipment", + name="selling_price", + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name="equipment", + name="starting_price", + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name="trailer", + name="current_price", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="trailer", + name="model_number", + field=models.CharField(blank=True, max_length=150, null=True), + ), + migrations.AddField( + model_name="trailer", + name="selling_price", + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name="trailer", + name="starting_price", + field=models.IntegerField(default=0), + ), + migrations.AlterField( + model_name="equipment", + name="chassis_number", + field=models.CharField(blank=True, max_length=150, null=True), + ), + migrations.AlterField( + model_name="equipment", + name="engine_number", + field=models.CharField(blank=True, max_length=150, null=True), + ), + ] diff --git a/backend/vehicle/migrations/0003_rename_equipment_type_equipment_type_and_more.py b/backend/vehicle/migrations/0003_rename_equipment_type_equipment_type_and_more.py new file mode 100644 index 0000000..8e80f95 --- /dev/null +++ b/backend/vehicle/migrations/0003_rename_equipment_type_equipment_type_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 5.0.4 on 2024-04-11 00:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("vehicle", "0002_rename_minimum_price_vehicle_current_price_and_more"), + ] + + operations = [ + migrations.RenameField( + model_name="equipment", + old_name="equipment_type", + new_name="type", + ), + migrations.RenameField( + model_name="trailer", + old_name="trailer_type", + new_name="type", + ), + migrations.RenameField( + model_name="vehicle", + old_name="vehicle_type", + new_name="type", + ), + ] diff --git a/backend/vehicle/models.py b/backend/vehicle/models.py index 7a8acbf..026386c 100644 --- a/backend/vehicle/models.py +++ b/backend/vehicle/models.py @@ -21,11 +21,11 @@ class Vehicle(MainModel): engine_number = models.CharField(max_length=150, blank=True, null=True) selling_price = models.IntegerField(default=0) starting_price = models.IntegerField(default=0) + current_price = models.IntegerField(blank=True, null=True) chassis_number = models.CharField(max_length=50, blank=True, null=True) description = models.CharField(max_length=2000) brand = models.ForeignKey(Brand, on_delete=models.PROTECT) - vehicle_type = models.ForeignKey(Type, on_delete=models.PROTECT) - minimum_price = models.IntegerField(blank=True, null=True) + type = models.ForeignKey(Type, on_delete=models.PROTECT) is_sold = models.BooleanField(default=False) remarks = models.CharField(max_length=2000, blank=True, null=True) classification_type = models.CharField(max_length=50, blank=True, null=True) @@ -41,12 +41,15 @@ class Vehicle(MainModel): class Equipment(MainModel): unicode_id = models.IntegerField(unique=True) - prefix_id = models.CharField(max_length=10) - chassis_number = models.CharField(max_length=50, blank=True, null=True) - engine_number = models.CharField(max_length=50, blank=True, null=True) + model_number = models.CharField(max_length=150, blank=True, null=True) + chassis_number = models.CharField(max_length=150, blank=True, null=True) + engine_number = models.CharField(max_length=150, blank=True, null=True) description = models.CharField(max_length=2000) brand = models.ForeignKey(Brand, on_delete=models.PROTECT) - equipment_type = models.ForeignKey(Type, on_delete=models.PROTECT) + type = models.ForeignKey(Type, on_delete=models.PROTECT) + selling_price = models.IntegerField(default=0) + starting_price = models.IntegerField(default=0) + current_price = models.IntegerField(blank=True, null=True) location = models.CharField(max_length=50, blank=True, null=True) classification_type = models.CharField(max_length=50, blank=True, null=True) engine_condition = models.CharField(max_length=100, blank=True, null=True) @@ -70,11 +73,14 @@ class Supplier(MainModel): class Trailer(MainModel): unicode_id = models.IntegerField(unique=True) - chassis_number = models.CharField(max_length=50, blank=True, null=True) + model_number = models.CharField(max_length=150, blank=True, null=True) description = models.CharField(max_length=2000) supplier = models.ForeignKey(Supplier, on_delete=models.PROTECT) - trailer_type = models.ForeignKey(Type, on_delete=models.PROTECT) + type = models.ForeignKey(Type, on_delete=models.PROTECT) number_of_axles = models.IntegerField() + selling_price = models.IntegerField(default=0) + starting_price = models.IntegerField(default=0) + current_price = models.IntegerField(blank=True, null=True) class UnitImage(MainModel): diff --git a/backend/vehicle/serializers.py b/backend/vehicle/serializers.py index 0d69c34..fff509b 100644 --- a/backend/vehicle/serializers.py +++ b/backend/vehicle/serializers.py @@ -1,67 +1,69 @@ +from django.contrib.contenttypes.models import ContentType from rest_framework import serializers from .models import Brand, Equipment, Supplier, Trailer, Type, UnitImage, Vehicle -class BrandSerializer(serializers.ModelSerializer): - class Meta: - model = Brand - fields = ["id", "name"] - - -class TypeSerializer(serializers.ModelSerializer): - class Meta: - model = Type - fields = ["id", "name"] - - class EquipmentSerializer(serializers.ModelSerializer): + brand_name = serializers.SerializerMethodField() + type_name = serializers.SerializerMethodField() + content_type = serializers.SerializerMethodField() + class Meta: model = Equipment fields = [ "id", "unicode_id", - "prefix_id", + "model_number", "chassis_number", "engine_number", "description", - "brand", - "equipment_type", - "location", - "classification_type", - "engine_condition", - "transmission_condition", - "differentials_condition", - "brake_condition", - "electrical_condition", - "hydraulic_cylindar_condition", - "hydraulic_hoses_and_chrome_condition", - "chassis_condition", - "body_condition", + "brand_name", + "type_name", + "starting_price", + "current_price", + "content_type", ] + def get_brand_name(self, obj): + return obj.brand.name + + def get_type_name(self, obj): + return obj.type.name + + def get_content_type(self, obj): + return obj._meta.model_name + class VehicleSerializer(serializers.ModelSerializer): brand_name = serializers.SerializerMethodField() - vehicle_type_name = serializers.SerializerMethodField() + type_name = serializers.SerializerMethodField() + content_type = serializers.SerializerMethodField() class Meta: model = Vehicle fields = [ "id", + "unicode_id", "model_number", "engine_number", "chassis_number", "description", "brand_name", - "vehicle_type_name", + "type_name", + "starting_price", + "current_price", + "content_type", ] def get_brand_name(self, obj): return obj.brand.name - def get_vehicle_type_name(self, obj): - return obj.vehicle_type.name + def get_type_name(self, obj): + return obj.type.name + + def get_content_type(self, obj): + return obj._meta.model_name class SupplierSerializer(serializers.ModelSerializer): @@ -71,17 +73,32 @@ class Meta: class TrailerSerializer(serializers.ModelSerializer): + brand_name = serializers.SerializerMethodField() + type_name = serializers.SerializerMethodField() + content_type = serializers.SerializerMethodField() + class Meta: model = Trailer fields = [ "id," "unicode_id", - "chassis_number", + "model_number", "description", - "supplier", - "trailer_type", - "number_of_axles", + "supplier_name", + "type_name", + "starting_price", + "current_price", + "content_type", ] + def get_supplier_name(self, obj): + return obj.supplier.name + + def get_type_name(self, obj): + return obj.type.name + + def get_content_type(self, obj): + return obj._meta.model_name + class UnitImageSerializer(serializers.ModelSerializer): unit_type = serializers.SerializerMethodField() @@ -94,3 +111,15 @@ def get_unit_type(self, obj): if obj.content_type: return obj.content_type.model return None + + +class BrandSerializer(serializers.ModelSerializer): + class Meta: + model = Brand + fields = ["id", "name"] + + +class TypeSerializer(serializers.ModelSerializer): + class Meta: + model = Type + fields = ["id", "name"] diff --git a/backend/vehicle/urls.py b/backend/vehicle/urls.py index 6b9289c..0a6eded 100644 --- a/backend/vehicle/urls.py +++ b/backend/vehicle/urls.py @@ -2,21 +2,17 @@ from django.urls import include, path from .views import ( + BrandListApiView, + DetailApiView, + ItemListApiView, + TypeListApiView, UploadFileView, - VehicleDetailApiView, - VehicleFilterList, - VehicleListApiView, - VehiclePriceApiView, ) urlpatterns = [ - path("", VehicleListApiView.as_view(), name="vehicle"), - path("/filter", VehicleFilterList.as_view(), name="vehicle-list"), - path("/", VehicleDetailApiView.as_view(), name="vehicle_detail"), - path( - "//minimum_price", - VehiclePriceApiView.as_view(), - name="vehicle_price", - ), + path("", ItemListApiView.as_view(), name="vehicle"), + path("/", DetailApiView.as_view(), name="vehicle_detail"), path("/upload-file", UploadFileView.as_view(), name="upload_file"), + path("/brands", BrandListApiView.as_view(), name="brands"), + path("/types", TypeListApiView.as_view(), name="types"), ] diff --git a/backend/vehicle/views.py b/backend/vehicle/views.py index f8a39d3..296e209 100644 --- a/backend/vehicle/views.py +++ b/backend/vehicle/views.py @@ -1,6 +1,8 @@ import pandas as pd +from django.db.models import Q from rest_framework import permissions, status from rest_framework.generics import get_object_or_404 +from rest_framework.pagination import PageNumberPagination from rest_framework.parsers import FileUploadParser from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response @@ -8,15 +10,25 @@ from core.permissions import IsAdminUser -from .helpers import has_more_data, infinite_filter, parse_excel_to_vehicle -from .models import Brand, Type, Vehicle -from .serializers import VehicleSerializer +from .helpers import parse_excel_to_vehicle +from .models import Brand, Equipment, Trailer, Type, Vehicle +from .serializers import ( + BrandSerializer, + EquipmentSerializer, + TrailerSerializer, + TypeSerializer, + VehicleSerializer, +) -# Create your views here. -class VehicleListApiView(APIView): - serializer_class = VehicleSerializer +class StandardResultsPagination(PageNumberPagination): + page_size = 5 + page_size_query_param = "page_size" + max_page_size = 100 + +# Create your views here. +class ItemListApiView(APIView): def get_permissions(self): if self.request.method == "POST": self.permission_classes = [IsAdminUser] @@ -26,11 +38,58 @@ def get_permissions(self): def get(self, request, *args, **kwargs): """ - Get all vehicles + Get a list of items filtered by type, brand, current price range, + and search term. Supports pagination. """ - vehicles = Vehicle.objects.all() - serializer = VehicleSerializer(vehicles, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) + item_type = request.query_params.get("type", None) + brand_id = request.query_params.get("brand", None) + min_price = request.query_params.get("min_price", None) + max_price = request.query_params.get("max_price", None) + search_term = request.query_params.get("search", None) + paginator = StandardResultsPagination() + + filters = Q() + if brand_id: + filters &= Q(brand_id=brand_id) + if min_price: + filters &= Q(current_price__gte=min_price) + if max_price: + filters &= Q(current_price__lte=max_price) + if search_term: + filters &= Q(description__icontains=search_term) + + if item_type is None: + items_query = ( + Vehicle.objects.filter(filters) + | Trailer.objects.filter(filters) + | Equipment.objects.filter(filters) + ).distinct() + else: + type_map = { + "truck": Vehicle, + "equipment": Equipment, + "trailer": Trailer, + } + model_cls = type_map.get(item_type, Vehicle) + items_query = model_cls.objects.filter(filters) + + page = paginator.paginate_queryset(items_query, request) + if page is not None: + serializer_cls = ( + VehicleSerializer + if item_type == "truck" + else TrailerSerializer + if item_type == "trailer" + else EquipmentSerializer + ) + serialized_page = serializer_cls( + page, many=True, context={"request": request} + ) + return paginator.get_paginated_response(serialized_page.data) + + return Response( + {"detail": "Invalid request"}, status=status.HTTP_400_BAD_REQUEST + ) def post(self, request, *args, **kwargs): """ @@ -39,118 +98,65 @@ def post(self, request, *args, **kwargs): data = request.data.copy() brand_id = data.pop("brand", None) type_id = data.pop("type", None) - # Handling the possibility that brand or type IDs might not exist + brand = get_object_or_404(Brand, id=brand_id) if brand_id else None vehicle_type = get_object_or_404(Type, id=type_id) if type_id else None - vehicle = Vehicle.objects.create(brand=brand, vehicle_type=vehicle_type, **data) - # Use the serializer class's data directly + vehicle = Vehicle.objects.create(brand=brand, type=vehicle_type, **data) + serialized_data = self.serializer_class(vehicle) return Response(serialized_data.data, status=status.HTTP_201_CREATED) -class VehicleDetailApiView(APIView): - """ - Retrieve, update or delete a vehicle instance. - """ - - serializer_class = VehicleSerializer - +class DetailApiView(APIView): def get_permissions(self): - if self.request.method == "PUT" or self.request.method == "DELETE": - self.permission_classes = [IsAdminUser] + if self.request.method in ["DELETE"]: + self.permission_classes = [permissions.IsAdminUser] else: - self.permission_classes = [IsAuthenticated] + self.permission_classes = [permissions.IsAuthenticated] return super().get_permissions() - def get(self, request, vehicle_id, *args, **kwargs): - """ - Get specific vehicle - """ - try: - vehicle = get_object_or_404(Vehicle, id=vehicle_id) - serializer = VehicleSerializer(vehicle) - return Response(serializer.data, status=status.HTTP_200_OK) - except vehicle.DoesNotExist: - return Response(status=status.HTTP_404_NOT_FOUND) + def get_serializer_class(self, item_type): + serializer_map = { + "truck": VehicleSerializer, + "equipment": EquipmentSerializer, + "trailer": TrailerSerializer, + } + return serializer_map.get(item_type, VehicleSerializer) + + def get_object(self, item_id, item_type): + model_map = { + "truck": Vehicle, + "equipment": Equipment, + "trailer": Trailer, + } + model = model_map.get(item_type, Vehicle) + return get_object_or_404(model, id=item_id) + + def get(self, request, item_id, *args, **kwargs): + item_type = request.query_params.get("type", "truck") + item = self.get_object(item_id, item_type) + serializer_class = self.get_serializer_class(item_type) + serializer = serializer_class(item) + return Response(serializer.data, status=status.HTTP_200_OK) - def put(self, request, vehicle_id, format=None): - """ - Update specific vehicle - """ - vehicle = get_object_or_404(Vehicle, id=vehicle_id) - serializer = VehicleSerializer(vehicle, data=request.data) + def put(self, request, item_id, *args, **kwargs): + item_type = request.query_params.get("type", "truck") + item = self.get_object(item_id, item_type) + serializer_class = self.get_serializer_class(item_type) + serializer = serializer_class(item, data=request.data, partial=True) if serializer.is_valid(): serializer.save() return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def delete(self, request, vehicle_id, format=None): - """ - Delete specific vehicle - """ - vehicle = get_object_or_404(Vehicle, id=vehicle_id) - vehicle.delete() + def delete(self, request, item_id, *args, **kwargs): + item_type = request.query_params.get("type", "truck") + item = self.get_object(item_id, item_type) + item.delete() return Response(status=status.HTTP_204_NO_CONTENT) -class VehicleFilterList(APIView): - """ - Get list of vehicles based off of filter - Takes limit + offset from url - """ - - permission_classes = [IsAuthenticated] - - def get_queryset(self): - queryset = infinite_filter(self.request) - return queryset - - def get(self, request): - url_parameter = request.GET.get("search") - if url_parameter: - vehicles = self.get_queryset() - - serialized_data = VehicleSerializer(vehicles, many=True) - - return Response( - {"vehicles": serialized_data.data, "more_data": has_more_data(request)} - ) - - return Response(VehicleSerializer(Vehicle.objects.all()[:10], many=True).data) - - -class VehiclePriceApiView(APIView): - """ - Update a vehicle's minimum price - """ - - permission_classes = [IsAdminUser] - - serializer_class = VehicleSerializer - - def put(self, request, vehicle_id, format=None): - """ - Update specific vehicle price - """ - vehicle = get_object_or_404(Vehicle, id=vehicle_id) - - new_price = request.data.get("minimum_price") - if new_price is None: - return Response( - {"error": "Must provide minimum price"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - serializer = VehicleSerializer( - vehicle, data={"minimum_price": new_price}, partial=True - ) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - class UploadFileView(APIView): permission_classes = [permissions.AllowAny] authentication_classes = [] @@ -172,3 +178,21 @@ def post(self, request): except Exception as e: error_message = str(e) return Response({"status": "error", "message": error_message}) + + +class BrandListApiView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request, *args, **kwargs): + brands = Brand.objects.all() + serialized_data = BrandSerializer(brands, many=True).data + return Response(serialized_data, status=status.HTTP_200_OK) + + +class TypeListApiView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request, *args, **kwargs): + types = Type.objects.all() + serialized_data = TypeSerializer(types, many=True).data + return Response(serialized_data, status=status.HTTP_200_OK) diff --git a/frontend/src/components/cards/NotificationPopUpCard.jsx b/frontend/src/components/cards/NotificationPopUpCard.jsx index 6c4f382..6f8de87 100644 --- a/frontend/src/components/cards/NotificationPopUpCard.jsx +++ b/frontend/src/components/cards/NotificationPopUpCard.jsx @@ -76,9 +76,9 @@ export default function NotificationPopUpCard() { 1 hour left until auction ends - +

Put in your bids by 21:59! - +

@@ -109,9 +109,9 @@ export default function NotificationPopUpCard() { You have been verified - +

You are now registered to make bids - +

diff --git a/frontend/src/components/dropdowns/Dropdown.jsx b/frontend/src/components/dropdowns/Dropdown.jsx index 6f41871..850fda5 100644 --- a/frontend/src/components/dropdowns/Dropdown.jsx +++ b/frontend/src/components/dropdowns/Dropdown.jsx @@ -51,7 +51,7 @@ export default function Dropdown({ title, items, onValueChange }) { {isOpen && (
item.name !== 'All'), + ]; + const handleValueChange = (newValue) => { setSelectedValue(newValue); setIsOpen(false); @@ -29,10 +34,7 @@ export default function FilterDropdown({ title, items, onValueChange }) { }, [isOpen]); return ( -
+
))}
diff --git a/frontend/src/components/dropdowns/SortByDropdown.jsx b/frontend/src/components/dropdowns/SortByDropdown.jsx index f7c4eab..069438e 100644 --- a/frontend/src/components/dropdowns/SortByDropdown.jsx +++ b/frontend/src/components/dropdowns/SortByDropdown.jsx @@ -32,7 +32,7 @@ export default function SortByDropdown({ title, items, onValueChange }) {