diff --git a/application/backend/secondchance_backend/item/api.py b/application/backend/secondchance_backend/item/api.py index 04a55b9..e7218a4 100644 --- a/application/backend/secondchance_backend/item/api.py +++ b/application/backend/secondchance_backend/item/api.py @@ -10,7 +10,7 @@ from .serializers import ItemListSerializer, ItemDetailSerializer, RentalListSerializer from django.shortcuts import get_object_or_404 from useraccount.models import User - +from datetime import date, datetime @api_view(["GET"]) @authentication_classes([]) @@ -161,11 +161,21 @@ def rent_item(request, pk): try: start_date = request.POST.get("start_date", "") end_date = request.POST.get("end_date", "") - number_of_days = request.POST.get("number_of_days", "") - total_price = request.POST.get("total_price", "") - - item = Item.objects.get(pk=pk) - + number_of_days = int(request.POST.get("number_of_days", 0)) + total_price = float(request.POST.get("total_price", 0)) + + try: + start_date = datetime.strptime(start_date, "%Y-%m-%d").date() + end_date = datetime.strptime(end_date, "%Y-%m-%d").date() + except ValueError as e: + return JsonResponse({"success": False, "error": "Error parsing dates."}, status=404) + + + try: + item = Item.objects.get(pk=pk) + except Item.DoesNotExist: + return JsonResponse({"success": False, "error": "Item not found."}, status=404) + Rental.objects.create( item=item, start_date=start_date, @@ -174,8 +184,10 @@ def rent_item(request, pk): total_price=total_price, created_by=request.user, ) - + + request.user.increment_items_rented() + return JsonResponse({"success": True}) except Exception as e: diff --git a/application/backend/secondchance_backend/item/utils/categories.py b/application/backend/secondchance_backend/item/utils/categories.py new file mode 100644 index 0000000..a503180 --- /dev/null +++ b/application/backend/secondchance_backend/item/utils/categories.py @@ -0,0 +1,62 @@ +def get_user_rented_categories(user): + """ + Get the categories of items that a user has rented from + + args: + user instance + used to get items rented by user + + + return: list of categories + rtype: list? + """ + from item.models import Rental + # convert QuerySet to list before returning + return list( + Rental.objects.filter(created_by=user) + .values_list('item__category', flat=True) # returns desired field + ) + + + +def get_user_listed_categories(user): + """ + Get the categories of items that a user has listed + + args: + user instance + used to get items listed by user + + + return: list of categories + rtype: list? + """ + from item.models import Item + + return list( + Item.objects.filter(seller=user) + .values_list('category', flat=True) + ) + +def count_items_rented_from_user(user): + """ + Get the number of items that are currently being rented from the given user + Find the rentals that have an item that belongs to the user instance passed in + + args: + user instance + + return: number of items being rented from the user + rtype: int + """ + from datetime import date + from item.models import Rental, Item + + today = date.today() + + # count the number of listings belonging to the user that are being rented out + return Rental.objects.filter( + item__seller = user, # listings put up by user + start_date__lte = today, # listed items rented today or sooner + end_date__gte = today # listed items rented today or after + ).count() \ No newline at end of file diff --git a/application/backend/secondchance_backend/useraccount/models.py b/application/backend/secondchance_backend/useraccount/models.py index c28250f..86d4536 100644 --- a/application/backend/secondchance_backend/useraccount/models.py +++ b/application/backend/secondchance_backend/useraccount/models.py @@ -3,7 +3,12 @@ from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, UserManager from django.db import models from django.utils import timezone +from enum import Enum +# from item.utils.categories import get_user_listed_categories, get_user_rented_categories +# this breaks the code +# from item.models import Item +# from item.models import Rental class CustomUserManager(UserManager): """ @@ -177,6 +182,8 @@ def increment_items_rented(self): self.items_rented += 1 self.calculate_sustainability_score() self.save(update_fields=["items_rented", "sustainability_score"]) + + def calculate_sustainability_score(self): """ @@ -190,19 +197,75 @@ def calculate_sustainability_score(self): :return: None :rtype: None """ + # 30% of total score = days since join date + # 25% items rented by user + # 30% items listed by user + # 15% popularity (items listed by the user currently being rented by other users) + + max_normalized_score = 100 + + # compute how many days out of 2 years the user has been on the platform + # then normalize it if self.date_joined: - days_on_platform = (timezone.now() - self.date_joined).days + days_on_platform = (timezone.now() - self.date_joined).days else: days_on_platform = 0 - score = ( - (self.items_rented_out * 2.5) - + (self.items_rented * 1.5) - + (days_on_platform * 0.1) + max_points_date_joined = 730 # 2 years in days since the user joined the platform + time_score = min((days_on_platform / max_points_date_joined) * 100, 100) + + # get the items rented by the user + + if self.items_rented or self.items_rented != 0: + from item.utils.categories import get_user_rented_categories + + rented_categories = get_user_rented_categories(self) + rented_items_weighted = 0 + + # sum weights + for category in rented_categories: + rented_items_weighted += CATEGORY_WEIGHTS[category.lower()] + + else: + rented_items_weighted = 0 + + max_rented_score = 40 + rented_items_score = min(max((rented_items_weighted / max_rented_score) * 100, 0), max_normalized_score) # compute here + + # compute score for items listed by user and how many of them are being rented + if self.items_rented_out: + from item.utils.categories import get_user_listed_categories + from item.utils.categories import count_items_rented_from_user + + raw_popularity_score = count_items_rented_from_user(self) + + listed_categories = get_user_listed_categories(self) + + listed_items_weighted = 0 + for category in listed_categories: + listed_items_weighted += CATEGORY_WEIGHTS[category.lower()] + + else: + raw_popularity_score = 0 + listed_items_weighted = 0 + + max_listed_score = 50 + max_raw_popularity_score = 80 + + listed_items_score = min(max((listed_items_weighted / max_listed_score) * 100, 0), max_normalized_score) # compute here + + popularity_score = min(max(raw_popularity_score / max_raw_popularity_score * 100, 0), max_normalized_score) + + total_sustainability_score = ( + time_score * 0.3 + + rented_items_score * 0.25 + + listed_items_score * 0.3 + + popularity_score * 0.15 ) - max_score = 100 - normalized_score = min(max(round(score), 1), max_score) + normalized_score = min(max(round(total_sustainability_score), 0), max_score) + + # update user's score in DB self.sustainability_score = normalized_score def save(self, *args, **kwargs): @@ -215,3 +278,21 @@ def save(self, *args, **kwargs): """ self.calculate_sustainability_score() super().save(*args, **kwargs) + +CATEGORY_WEIGHTS = { + "electronics": 2, + "furniture": 1.5, + "clothing": 3, + "books": 3, + "appliances": 2, + "sports": 2.5, + "toys": 2.5, + "tools": 2, + "vehicles": 2, + "party": 2, + "music": 1, + "photography": 1, + "gardening": 1.5, + "office": 1, + "other": 1, +} \ No newline at end of file diff --git a/application/secondchance/app/components/items/RentalSidebar.tsx b/application/secondchance/app/components/items/RentalSidebar.tsx index 6344945..0d020f4 100644 --- a/application/secondchance/app/components/items/RentalSidebar.tsx +++ b/application/secondchance/app/components/items/RentalSidebar.tsx @@ -39,14 +39,16 @@ const RentalSidebar: React.FC = ({ item, userId }) => { const [dateRange, setDateRange] = useState(initialDateRange); const [minDate, setMinDate] = useState(new Date()); const [reservedDates, setReservedDates] = useState([]); + const [sustainabilityScore, setSustainabilityScore] = useState(0); const processRental = async () => { if (userId) { if (dateRange.startDate && dateRange.endDate) { const formData = new FormData(); - formData.append('location', item.location); - formData.append('condition', item.condition); - formData.append('category', item.category); + // removed fields not present in Rental Model + // formData.append('location', item.location); + // formData.append('condition', item.condition); + // formData.append('category', item.category); formData.append('start_date', format(dateRange.startDate, 'yyyy-MM-dd')); formData.append('end_date', format(dateRange.endDate, 'yyyy-MM-dd')); formData.append('number_of_days', days.toString()); @@ -97,8 +99,25 @@ const RentalSidebar: React.FC = ({ item, userId }) => { setReservedDates(dates); }; + const fetchSustainabilityScore = async () => { + if (userId) { + try { + const userData = await apiService.get(`/api/auth/users/${userId}/`); + setSustainabilityScore(userData.sustainability_score || 0); + } catch (error) { + console.error('Error fetching sustainability score:', error); + } + } + }; + + // bug fix: made the second useEffect wait for the sustainabilityScore to change + // instead of executing before it useEffect(() => { getRentals(); + fetchSustainabilityScore(); + }, [userId]); + + useEffect(() => { if (dateRange.startDate && dateRange.endDate) { const dayCount = differenceInDays(dateRange.endDate, dateRange.startDate); @@ -106,8 +125,13 @@ const RentalSidebar: React.FC = ({ item, userId }) => { if (dayCount && item.price_per_day) { const _fee = ((dayCount * item.price_per_day) / 100) * 5; + const discountModifier = 0.03; + let sustainabilityDiscount = sustainabilityScore * discountModifier; + + console.log("SUSTAINABILITY DISCOUNT:", sustainabilityDiscount); + setFee(_fee); - setTotalPrice(dayCount * item.price_per_day + _fee); + setTotalPrice((dayCount * item.price_per_day + _fee) - sustainabilityDiscount); setDays(dayCount); } else { const _fee = (item.price_per_day / 100) * 5; @@ -117,7 +141,7 @@ const RentalSidebar: React.FC = ({ item, userId }) => { setDays(1); } } - }, [dateRange]); + }, [dateRange, sustainabilityScore], ); return ( );