diff --git a/backend/myapi/db_functions/food.py b/backend/myapi/db_functions/food.py index 7e8f832..81ee999 100644 --- a/backend/myapi/db_functions/food.py +++ b/backend/myapi/db_functions/food.py @@ -1,43 +1,93 @@ from ..models import foods_collection -""" -food_name: str, -ratings: { - username: int -} -""" +def add_food(name: str) -> None: + foods_collection.insert_one({"name": name, "ratings": []}) -## Basic CRUD +def remove_food(food_name: str) -> None: + foods_collection.delete_one({"name": food_name}) -def set_food(name: str) -> None: - # check if food already exists - food = foods_collection.find_one({"food_name": name}) - if food: + +# NOTE: you might want to add more to seach for the type of food +def overwrite_food_rating(food_name: str, user_name: str, rating: int) -> None: + # make sure rating is between 0 and 5 + if rating < 0: + rating = 0 + elif rating > 5: + rating = 5 + + # get the food + food: dict[str, str | int | list[dict[str, str | int]]] | None = ( + foods_collection.find_one({"name": food_name}) + ) + + # if the food does not exist, add it + if food is None: + add_food(food_name) + food: dict[str, str | int | list[dict[str, str | int]]] | None = ( + foods_collection.find_one({"name": food_name}) + ) + + # check if the food is none + if food is None: return - # add food - foods_collection.insert_one({"food_name": name, "ratings": {}}) + # check if the user has already rated the food + ratings: list[dict[str, str | int]] = food["ratings"] + for rate in ratings: + if rate["user"] == user_name: + rate["rating"] = rating + return + + # add new rating to the food for the user + ratings.append({"user": user_name, "rating": rating}) + + +# NOTE: you might want to add more to seach for the type of food +def get_food_ratings(food_name: str) -> list[dict[str, str | int]]: + # get the food + food: dict[str, str | int | list[dict[str, str | int]]] | None = ( + foods_collection.find_one({"name": food_name}) + ) -def get_food(name: str) -> dict | None: - return foods_collection.find_one({"food_name": name}) + # if the food does not exist, return an empty list + if food is None: + return [] + return food["ratings"] -def update_food(food_name: str, username: str, rating: int | None = None) -> None: - # check if food exists - food = foods_collection.find_one({"food_name": food_name}) - if not food: + +def get_average_rating(food_name: str) -> float: + # get the food + ratings = get_food_ratings(food_name) + if len(ratings) == 0: + return 0 + + # calculate the average rating + total = 0 + for rate in ratings: + num: int = int(rate["rating"]) + total += num + + return total / len(ratings) + + +def remove_all_ratings(food_name: str) -> None: + # get the food + food: dict[str, str | int | list[dict[str, str | int]]] | None = ( + foods_collection.find_one({"name": food_name}) + ) + + # if the food does not exist, return + if food is None: return - if rating is not None: - # add/change rating - food["ratings"][username] = rating - # update food - foods_collection.update_one( - {"food_name": food_name}, {"$set": {"ratings": food["ratings"]}} - ) + # remove all ratings + food["ratings"] = [] + + # update the food + foods_collection.update_one({"name": food_name}, {"$set": {"ratings": []}}) -def delete_food(name: str) -> None: - foods_collection.delete_one({"food_name": name}) +# NOTE: there can be more functions to search for the type of food diff --git a/backend/myapi/db_functions/locations.py b/backend/myapi/db_functions/locations.py index d372752..97559e2 100644 --- a/backend/myapi/db_functions/locations.py +++ b/backend/myapi/db_functions/locations.py @@ -1,59 +1,31 @@ from ..models import locations_collection -""" -name: str -""" +def remove_locations_from_db(names: list[str]) -> None: + for name in names: + locations_collection.delete_many({"name": name}) -## Basic CRUD -def set_location(location: dict) -> None: - locations_collection.insert_one(location) +def add_locations_to_db(locations: list[dict]) -> None: + for dh in locations: + locations_collection.insert_one(dh) -def get_location(name: str) -> dict | None: - return locations_collection.find_one({"name": name}) +def get_names_of_locations(locations: list[dict]) -> list[str]: + names = [] + for dh in locations: + names.append(dh["name"]) + return names -def update_location(name: str, location: dict) -> None: - """ - check if the location exists then overwrite the location - """ - # check if the location exists - if get_location(name) is None: - set_location(location) # if not, create a new location - # overwrite the location - locations_collection.update_one({"name": name}, {"$set": location}) +def remove_add_locations_to_db(locations: list[dict]) -> None: + # get names of locations + names = get_names_of_locations(locations) + # remove locations with the names + remove_locations_from_db(names) + # add locations to db + add_locations_to_db(locations) -def delete_location(name: str) -> None: - locations_collection.delete_one({"name": name}) - - -## Bulk CRUD - - -def set_locations(locations: list[dict]) -> None: - locations_collection.insert_many(locations) - - -def get_locations(names: list[str] = []) -> list[dict]: - """ - if no names are given, return all locations - else, return the locations with the given names - """ - if len(names) == 0: - return list(locations_collection.find({})) # get all locations - - return list( - locations_collection.find({"name": {"$in": names}}) - ) # get specific locations - - -def update_locations(locations: list[dict]) -> None: - for location in locations: - update_location(location["name"], location) - - -def delete_locations(names: list[str]) -> None: - locations_collection.delete_many({"name": {"$in": names}}) +def get_all_locations_from_db() -> list[dict]: + return list(locations_collection.find({})) diff --git a/backend/myapi/db_functions/tasks.py b/backend/myapi/db_functions/tasks.py index cbcf3b1..3fe5bd4 100644 --- a/backend/myapi/db_functions/tasks.py +++ b/backend/myapi/db_functions/tasks.py @@ -1,4 +1,3 @@ -from requests import get from ..models import tasks_collection from datetime import datetime @@ -10,64 +9,39 @@ last_update: str """ -## Basic CRUD - -def set_task(task_name: str) -> dict[str, str]: +def set_task_last_update(task_name: str) -> None: """ - Create a new task with the given name + Set the time that the task were last update the time should be based upon the database time """ - # get the time from django and make it naive - time_now: datetime = timezone.now().replace(tzinfo=None) + # get the time from django + time_now: datetime = timezone.now() # convert the time to a string time_str = time_now.strftime("%Y-%m-%d %H:%M:%S") - # create a new task - task = {"name": task_name, "last_update": time_str} - - # insert the task into the tasks collection - tasks_collection.insert_one(task) - - return task - - -def get_task(task_name: str) -> dict | None: - """ - Get the task from the database - """ - return tasks_collection.find_one({"name": task_name}) - - -def update_task(task_name: str, last_update: datetime) -> None: - """ - Update the task in the database - """ - # convert the time to a string - time_str = last_update.strftime("%Y-%m-%d %H:%M:%S") - - # update the task - tasks_collection.update_one( - {"name": task_name}, {"$set": {"last_update": time_str}} - ) + # get the task from the tasks collection + task: dict | None = tasks_collection.find_one({"name": task_name}) - -def delete_task(task_name: str) -> None: - """ - Delete the task from the database - """ - tasks_collection.delete_one({"name": task_name}) - - -## Additional CRUD + # check if the task exists + if task is None: + # create a new task + task = {"name": task_name, "last_update": time_str} + # insert the task into the tasks collection + tasks_collection.insert_one(task) + else: + # update the task + tasks_collection.update_one( + {"name": task_name}, {"$set": {"last_update": time_str}} + ) -def get_last_update_time(task_name: str) -> datetime | None: +def get_task_last_update(task_name: str) -> datetime | None: """ Get the last update time of the task from the database """ # find the task in the tasks collection - task = get_task(task_name) + task: dict | None = tasks_collection.find_one({"name": task_name}) # check if the location exists if task is None: @@ -80,13 +54,3 @@ def get_last_update_time(task_name: str) -> datetime | None: last_update_time = datetime.strptime(last_update, "%Y-%m-%d %H:%M:%S") return last_update_time - - -## helper functions - - -def str_to_datetime(time_str: str) -> datetime: - """ - Convert a string to a datetime object - """ - return datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S") diff --git a/backend/myapi/db_functions/user.py b/backend/myapi/db_functions/user.py deleted file mode 100644 index f28f529..0000000 --- a/backend/myapi/db_functions/user.py +++ /dev/null @@ -1,42 +0,0 @@ -from requests import delete -from ..models import users_collection - -""" -username: str, -id: str, -ratings: { - food_name: int -} -""" - -## Basic CRUD - - -def set_user(username: str, id: str) -> None: - # check if user already exists - user = users_collection.find_one({"username": username}) - if user: - return - # add user - users_collection.insert_one({"username": username, "id": id, "ratings": {}}) - - -def get_user(id: str) -> dict | None: - return users_collection.find_one({"id": id}) - - -def update_user(id: str, food_name: str, rating: int | None = None) -> None: - # check if user exists - user = users_collection.find_one({"id": id}) - if not user: - return - - if rating is not None: - # add/change rating - user["ratings"][food_name] = rating - # update user - users_collection.update_one({"id": id}, {"$set": {"ratings": user["ratings"]}}) - - -def delete_user(id: str) -> None: - users_collection.delete_one({"id": id}) diff --git a/backend/myapi/db_functions/users.py b/backend/myapi/db_functions/users.py new file mode 100644 index 0000000..0c0e8b1 --- /dev/null +++ b/backend/myapi/db_functions/users.py @@ -0,0 +1,52 @@ +from ..models import users_collection +from pymongo.errors import PyMongoError + + +# Add user data to db, search by email/username +def add_user_data(user_data): + try: + # Insert new user's data or update existing user's data by overwritting fields in user_data + result = users_collection.update_one( + {"email": user_data["email"]}, {"$set": user_data}, upsert=True + ) + if result.upserted_id: + print("User data inserted with id:", result.upserted_id) + else: + print("User data updated") + except PyMongoError as e: + print("Error while adding user data:", e) + + +# Get all the user's data from db, search by email/username +def get_user_data(email): + try: + user_data = users_collection.find_one({"email": email}) + return user_data + except PyMongoError as e: + print("Error while getting user data:", e) + return None + + +# Get ratings data from user +def get_ratings_data(email): + try: + user_data = users_collection.find_one( + {"email": email}, {"_id": 0, "ratings": 1} + ) + # Return the "ratings" field, or an empty dictionary if not found + return user_data.get("ratings", {}) + except PyMongoError as e: + print("Error while getting ratings data:", e) + return {} + + +# Remove a user's data +def remove_user_data(email): + try: + result = users_collection.delete_one({"email": email}) + if result.deleted_count == 1: + print("User data removed successfully") + else: + print("User not found") + except PyMongoError as e: + print("Error while removing user data:", e) diff --git a/backend/myapi/models.py b/backend/myapi/models.py index 83a3f75..a04bc1c 100644 --- a/backend/myapi/models.py +++ b/backend/myapi/models.py @@ -3,11 +3,11 @@ # Create your models here. -# Locations Model +# locations Model locations_collection = db["locations"] -# Foods Model -foods_collection = db["foods"] +# Food Model +foods_collection = db["food"] # Users Model users_collection = db["users"] @@ -15,5 +15,15 @@ # Tasks Model tasks_collection = db["tasks"] -# User Model -users_collection = db["users"] + +# # NOTE: This is temporary and will be replaced with a background task +# from webscraper.food_locations import FoodLocations + +# # Fetch dining halls +# fo = FoodLocations() +# # Get dining halls as a list of json objects +# dining_halls: list[dict] = [dh.to_dict() for dh in fo.get_locations()] +# # Add dining halls to db +# from .db_functions.dining_halls import remove_add_dining_halls_to_db + +# remove_add_dining_halls_to_db(dining_halls) diff --git a/backend/myapi/views.py b/backend/myapi/views.py index 609f368..be8ef3c 100644 --- a/backend/myapi/views.py +++ b/backend/myapi/views.py @@ -1,5 +1,4 @@ -from dns import update -from requests import get +from django.conf.locale import fr from rest_framework.response import Response from rest_framework.decorators import api_view from django.conf import settings @@ -36,15 +35,11 @@ def hello_world(request): @api_view(["GET"]) def get_locations(request): # Get the last update time of the locations - last_update: datetime | None = get_last_update_time(task_name="locations") + last_update: datetime | None = get_task_last_update(task_name="locations") - # check if the last update time doesn't exist - if last_update is None: - task = set_task(task_name="locations") - time_now = str_to_datetime(task["last_update"]) - else: - # get the current time and make it naive - time_now: datetime = timezone.now().replace(tzinfo=None) + # get the current time and make it naive + time_now: datetime = timezone.now() + time_now = time_now.replace(tzinfo=None) print("Last time : ", last_update) print("Current time: ", time_now) @@ -52,26 +47,19 @@ def get_locations(request): # check if not updated in the last hour if last_update is None or (time_now - last_update).seconds > 3600: print("Locations need to be updated...") - # fetch the locations from the web scraper and add them to the db fo = FoodLocations() - - # Filter out the empty locations - filtered_locations = fo.get_non_empty_locations() - - # Convert the list of dining halls to a list of dictionaries - locations = [dh.to_dict() for dh in filtered_locations] - - # Update the locations in the db - update_locations(locations) + locations: list[dict] = [dh.to_dict() for dh in fo.get_locations()] + # add the locations to the db + remove_add_locations_to_db(locations) # update the last update time - update_task(task_name="locations", last_update=time_now) + set_task_last_update(task_name="locations") else: print("Locations are up to date. Getting from DB...") # Get all locations from the db - locations: list[dict] = get_locations_db() + locations: list[dict] = get_all_locations_from_db() # remove the _id field from each dining hall for dh in locations: diff --git a/backend/webscraper/category.py b/backend/webscraper/category.py index 8e5bff0..e4ae750 100644 --- a/backend/webscraper/category.py +++ b/backend/webscraper/category.py @@ -1,4 +1,3 @@ -from re import sub from bs4.element import Tag, ResultSet from webscraper.food import Food @@ -10,9 +9,6 @@ def __init__(self, name: str, html_list: list[Tag]) -> None: self.name = name self.foods: list[Food] = [Food(html) for html in html_list] - def is_empty(self) -> bool: - return len(self.foods) == 0 - def __str__(self) -> str: result = f"{self.name}\n" for food in self.foods: @@ -26,14 +22,9 @@ def to_dict(self) -> dict: class Category: def __init__(self, name: str, html: Tag) -> None: self.name = name - self.sub_categories: list[SubCategory] = self._process_data(html) - - def is_empty(self) -> bool: - return all(sub_cat.is_empty() for sub_cat in self.sub_categories) or ( - len(self.sub_categories) == 0 - ) + self.sub_categories: list[SubCategory] = self.__process_data(html) - def _process_data(self, html: Tag) -> list[SubCategory]: + def __process_data(self, html: Tag) -> list[SubCategory]: # find the categories in the meal time sub_cat_data: ResultSet = html.find_all("div", class_="shortmenucats") diff --git a/backend/webscraper/dining_hall.py b/backend/webscraper/dining_hall.py index d28e241..0fc13c0 100644 --- a/backend/webscraper/dining_hall.py +++ b/backend/webscraper/dining_hall.py @@ -12,16 +12,10 @@ class DiningHall: def __init__(self, url: str) -> None: self.name: str = "Error: Name not found" - self.categories: list[Category] = self._retrieve_data(url) + self.categories: list[Category] = self.__retrieve_data(url) print(self.name) - def is_empty(self) -> bool: - # empty if all categories are empty - return all(category.is_empty() for category in self.categories) or ( - len(self.categories) == 0 - ) - - def _retrieve_data(self, url: str) -> list[Category]: + def __retrieve_data(self, url: str) -> list[Category]: # Set the cookies to be empty to avoid loading nothing cookies = { "WebInaCartLocation": "", diff --git a/backend/webscraper/food.py b/backend/webscraper/food.py index 3e9daa2..c46c69a 100644 --- a/backend/webscraper/food.py +++ b/backend/webscraper/food.py @@ -5,9 +5,10 @@ class Food: def __init__(self, html: Tag) -> None: self.name = "Error: Food name not found" self.allergies = [] - self._process_data(html) + self.__process_data(html) + + def __process_data(self, html: Tag) -> None: - def _process_data(self, html: Tag) -> None: # find the food name name = html.find("div", class_="shortmenurecipes") if name: @@ -26,4 +27,4 @@ def __str__(self) -> str: def to_dict(self) -> dict: # foodObj = {self.name: self.allergies} - return {"name": self.name, "restrictions": self.allergies} + return {"name": self.name, self.name: self.allergies} diff --git a/backend/webscraper/food_locations.py b/backend/webscraper/food_locations.py index fd621de..53fa999 100644 --- a/backend/webscraper/food_locations.py +++ b/backend/webscraper/food_locations.py @@ -10,18 +10,12 @@ class FoodLocations: main_url = "https://nutrition.sa.ucsc.edu/" def __init__(self) -> None: - self.locations: list[DiningHall] = self._retrieve_data() + self.locations: list[DiningHall] = self.__retrieve_data() def get_locations(self) -> list[DiningHall]: return self.locations - def get_location_names(self) -> list[str]: - return [dh.name for dh in self.locations] - - def get_non_empty_locations(self) -> list[DiningHall]: - return [dh for dh in self.locations if not dh.is_empty()] - - def _retrieve_data(self) -> list[DiningHall]: + def __retrieve_data(self) -> list[DiningHall]: try: page = requests.get(self.main_url, verify=UCSC_SSL_CERT) except requests.exceptions.RequestException as e: # This is the correct syntax