diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index 41679be..3e7fc06 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -42,4 +42,4 @@ jobs: docker stack rm thestack sleep 20s docker stack deploy -c docker-compose.yml thestack - docker system prune -a + yes | docker system prune -a diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 23e3e41..4ecf75f 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -42,4 +42,4 @@ jobs: docker stack rm thestack sleep 20s docker stack deploy -c docker-compose.yml thestack - docker system prune -a + yes | docker system prune -a diff --git a/.gitignore b/.gitignore index 749e7f7..52fc3ef 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ .envrc .envlocal .envremote +secrets/eatery-blue-f810e-firebase-adminsdk-ds3co-7902015462.json # Hardcoded stuff db_snapshots/ @@ -13,6 +14,7 @@ static_sources/data.log __pycache__/ *.py[cod] *$py.class +.ruff_cache # More Python stuff .DS_Store @@ -42,3 +44,4 @@ MANIFEST local_settings.py db.sqlite3 db.sqlite3-journal +src/static_sources/external_eateries.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..af043df --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +repos: + # Updates all subsequent hooks + - repo: https://gitlab.com/vojko.pribudic.foss/pre-commit-update + rev: v0.5.1 + hooks: + - id: pre-commit-update + + # Linter hook + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.6.7 + hooks: + - id: ruff + args: [--fix] + + # Formatter hook + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 24.8.0 + hooks: + - id: black + + # Static code analysis hook + - repo: https://github.com/PyCQA/bandit + rev: 1.7.10 + hooks: + - id: bandit diff --git a/README.md b/README.md index 0f65339..f3e5800 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # eatery-blue-backend -This is the backend for eatery-blue-backend. +This is the backend for Eatery Blue. # Postgres Setup --- -- Install PostgresSQL here at https://www.postgresql.org/download/ +- Install PostgreSQL here at https://www.postgresql.org/download/ - Login to postgres via command line by entering `psql postgres` - Create the eatery database via `create database "eatery-dev";` - Quit psql via `\q` @@ -17,6 +17,16 @@ This is the backend for eatery-blue-backend. - To set up the tables and data (or if reseting the database), make sure current working directory is the `src` folder and run `python3 manage.py makemigrations; python3 manage.py migrate; python3 manage.py populate_models` - To run the backend, run `python3 manage.py runserver 0.0.0.0:8000` (Ensuring the env variables are loaded and all dependencies are installed) +# Documentation + +- Full Swagger Docs API Specs can be found at /docs when running the server + +## FA24 Members + +- Thomas Vignos +- Skye Slattery +- Cassidy Xu + ## SP24 Members - Thomas Vignos diff --git a/requirements.txt b/requirements.txt index d86038e..f9c09d7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,17 @@ asgiref==3.4.1 -black==21.12b0 cachetools==4.2.4 certifi==2021.10.8 +cfgv==3.4.0 charset-normalizer==2.0.9 click==8.0.3 +coreapi==2.3.3 +coreschema==0.0.4 +distlib==0.3.8 Django==4.0 +django-rest-swagger==2.2.0 djangorestframework==3.13.1 +drf-yasg==1.21.7 +filelock==3.15.4 google-api-core==2.3.2 google-api-python-client==2.33.0 google-auth==2.3.3 @@ -13,23 +19,37 @@ google-auth-httplib2==0.1.0 google-auth-oauthlib==0.4.6 googleapis-common-protos==1.54.0 httplib2==0.20.2 +identify==2.6.0 idna==3.3 +inflection==0.5.1 +itypes==1.2.0 +Jinja2==3.1.4 +MarkupSafe==2.1.5 mypy-extensions==0.4.3 +nodeenv==1.9.1 oauthlib==3.1.1 +openapi-codec==1.3.2 +packaging==24.1 pathspec==0.9.0 -platformdirs==2.4.0 +platformdirs==4.2.2 +pre-commit==3.8.0 protobuf==3.19.1 psycopg2-binary==2.9.3 pyasn1==0.4.8 pyasn1-modules==0.2.8 pyparsing==3.0.6 pytz==2021.3 +PyYAML==6.0.2 requests==2.26.0 requests-oauthlib==1.3.0 rsa==4.8 +sentry-sdk==2.14.0 +simplejson==3.19.3 six==1.16.0 sqlparse==0.4.2 tomli==1.2.3 typing_extensions==4.0.1 uritemplate==4.1.1 -urllib3==1.26.7 +urllib3 +virtualenv==20.26.3 +google-auth \ No newline at end of file diff --git a/.envrctemplate b/secrets/.envrctemplate similarity index 67% rename from .envrctemplate rename to secrets/.envrctemplate index f5d34f9..a8c1e43 100644 --- a/.envrctemplate +++ b/secrets/.envrctemplate @@ -6,6 +6,10 @@ export POSTGRES_PASSWORD= export POSTGRES_HOST= export POSTGRES_PORT= export DJANGO_SECRET_KEY= +export GOOGLE_APPLICATION_CREDENTIALS= +export GOOGLE_SHEETS_API_KEY= +export FREEDGE_SHEET_ID= +export FREEDGE_APPROVED_EMAILS= # Not used export CORNELL_VENDOR_TOKEN= diff --git a/src/category/apps.py b/src/category/apps.py index 1f46433..e953ee6 100644 --- a/src/category/apps.py +++ b/src/category/apps.py @@ -2,5 +2,5 @@ class CategoryConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'category' + default_auto_field = "django.db.models.BigAutoField" + name = "category" diff --git a/src/category/controllers/populate_category.py b/src/category/controllers/populate_category.py index 43601a6..7d627ac 100644 --- a/src/category/controllers/populate_category.py +++ b/src/category/controllers/populate_category.py @@ -1,6 +1,4 @@ -from category.models import Category from category.serializers import CategorySerializer -from item.models import Item import json from util.constants import eatery_is_cafe @@ -21,10 +19,20 @@ def generate_dining_hall_categories(self, json_event, event): """ category_items = {} - category_order = ["Chef's Table", "Chef's Table - Sides", "Grill", "Wok", - "Wok/Asian Station", "Iron Grill", "Mexican Station", "Global", - "Halal", "Kosher Station", "Flat Top Grill"] - + category_order = [ + "Chef's Table", + "Chef's Table - Sides", + "Grill", + "Wok", + "Wok/Asian Station", + "Iron Grill", + "Mexican Station", + "Global", + "Halal", + "Kosher Station", + "Flat Top Grill", + ] + def sort_menu(menu): try: return category_order.index(menu["category"].strip()) @@ -79,7 +87,9 @@ def process(self, events_dict, json_eateries): categories_dict = {} - with open("./static_sources/external_eateries.json", "r") as external_eateries_file: + with open( + "./static_sources/external_eateries.json", "r" + ) as external_eateries_file: external_eateries_json = json.load(external_eateries_file) json_eateries.extend(external_eateries_json["eateries"]) @@ -94,7 +104,7 @@ def process(self, events_dict, json_eateries): continue is_cafe = eatery_is_cafe(json_eatery) - + """ For every event in an eatery --> for every menu in an eatery --> get categories """ diff --git a/src/category/migrations/0001_initial.py b/src/category/migrations/0001_initial.py index bdcbcb3..038c293 100644 --- a/src/category/migrations/0001_initial.py +++ b/src/category/migrations/0001_initial.py @@ -5,20 +5,26 @@ class Migration(migrations.Migration): - initial = True dependencies = [ - ('event', '0001_initial'), + ("event", "0001_initial"), ] operations = [ migrations.CreateModel( - name='Category', + name="Category", fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('category', models.CharField(default='General', max_length=40)), - ('event', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='menu', to='event.event')), + ("id", models.AutoField(primary_key=True, serialize=False)), + ("category", models.CharField(default="General", max_length=40)), + ( + "event", + models.ForeignKey( + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="menu", + to="event.event", + ), + ), ], ), ] diff --git a/src/category/migrations/0002_alter_category_id.py b/src/category/migrations/0002_alter_category_id.py index 410927c..c10fb6d 100644 --- a/src/category/migrations/0002_alter_category_id.py +++ b/src/category/migrations/0002_alter_category_id.py @@ -4,15 +4,16 @@ class Migration(migrations.Migration): - dependencies = [ - ('category', '0001_initial'), + ("category", "0001_initial"), ] operations = [ migrations.AlterField( - model_name='category', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + model_name="category", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), ] diff --git a/src/category/models.py b/src/category/models.py index 01b85aa..9538a42 100644 --- a/src/category/models.py +++ b/src/category/models.py @@ -5,6 +5,6 @@ class Category(models.Model): event = models.ForeignKey(Event, related_name="menu", on_delete=models.DO_NOTHING) category = models.CharField(max_length=40, default="General") - + def __str__(self): return self.category diff --git a/src/category/serializers.py b/src/category/serializers.py index 6af5027..74fc181 100644 --- a/src/category/serializers.py +++ b/src/category/serializers.py @@ -16,6 +16,7 @@ class Meta: model = Category fields = ["id", "category", "event", "items"] + class CategorySerializerOptimized(serializers.ModelSerializer): items = ItemSerializerOptimized(many=True, read_only=True) diff --git a/src/category/urls.py b/src/category/urls.py index d32462d..e245c26 100644 --- a/src/category/urls.py +++ b/src/category/urls.py @@ -7,4 +7,4 @@ urlpatterns = [ path("", include(router.urls)), -] \ No newline at end of file +] diff --git a/src/category/views.py b/src/category/views.py index a95917c..bda65d2 100644 --- a/src/category/views.py +++ b/src/category/views.py @@ -2,6 +2,7 @@ from category.models import Category from category.serializers import CategorySerializer + class CategoryViewSet(viewsets.ModelViewSet): queryset = Category.objects.all() - serializer_class = CategorySerializer \ No newline at end of file + serializer_class = CategorySerializer diff --git a/src/person/__init__.py b/src/device_token/__init__.py similarity index 100% rename from src/person/__init__.py rename to src/device_token/__init__.py diff --git a/src/device_token/admin.py b/src/device_token/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/src/device_token/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/src/person/apps.py b/src/device_token/apps.py similarity index 59% rename from src/person/apps.py rename to src/device_token/apps.py index b30fee4..1b9b57f 100644 --- a/src/person/apps.py +++ b/src/device_token/apps.py @@ -1,6 +1,5 @@ from django.apps import AppConfig - -class PersonConfig(AppConfig): +class DeviceTokenConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' - name = 'person' + name = 'device_token' diff --git a/src/device_token/migrations/0001_initial.py b/src/device_token/migrations/0001_initial.py new file mode 100644 index 0000000..63959a5 --- /dev/null +++ b/src/device_token/migrations/0001_initial.py @@ -0,0 +1,22 @@ +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('user', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='DeviceToken', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('device_token', models.CharField(max_length=40)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='device_tokens', to='user.user')), + ], + ), + ] \ No newline at end of file diff --git a/src/person/migrations/__init__.py b/src/device_token/migrations/__init__.py similarity index 100% rename from src/person/migrations/__init__.py rename to src/device_token/migrations/__init__.py diff --git a/src/device_token/models.py b/src/device_token/models.py new file mode 100644 index 0000000..c81c14d --- /dev/null +++ b/src/device_token/models.py @@ -0,0 +1,12 @@ +from django.db import models +from user.models import User + +class DeviceToken(models.Model): + id = models.AutoField(primary_key=True) + user = models.ForeignKey( + User, related_name="device_tokens", on_delete=models.CASCADE + ) + device_token = models.CharField(max_length=40) + + def __str__(self): + return f'{self.user.netid} - {self.device_token}' \ No newline at end of file diff --git a/src/device_token/serializers.py b/src/device_token/serializers.py new file mode 100644 index 0000000..dec8387 --- /dev/null +++ b/src/device_token/serializers.py @@ -0,0 +1,7 @@ +from rest_framework import serializers +from device_token.models import DeviceToken + +class DeviceTokenSerializer(serializers.ModelSerializer): + class Meta: + model = DeviceToken + fields = ['id', 'user', 'device_token'] \ No newline at end of file diff --git a/src/device_token/tests.py b/src/device_token/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/src/device_token/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/src/person/urls.py b/src/device_token/urls.py similarity index 55% rename from src/person/urls.py rename to src/device_token/urls.py index 75e30dc..3c810a7 100644 --- a/src/person/urls.py +++ b/src/device_token/urls.py @@ -1,10 +1,10 @@ from django.urls import path, include -from person.views import StudentViewSet, ChefViewSet +from device_token.views import DeviceTokenViewSet + from rest_framework.routers import DefaultRouter router = DefaultRouter() -router.register("student", StudentViewSet) -router.register("chef", ChefViewSet) +router.register("", DeviceTokenViewSet) urlpatterns = [ path("", include(router.urls)), diff --git a/src/device_token/views.py b/src/device_token/views.py new file mode 100644 index 0000000..2e6bf1d --- /dev/null +++ b/src/device_token/views.py @@ -0,0 +1,8 @@ +from rest_framework import viewsets +from device_token.models import DeviceToken +from device_token.serializers import DeviceTokenSerializer + + +class DeviceTokenViewSet(viewsets.ModelViewSet): + queryset = DeviceToken.objects.all() + serializer_class = DeviceTokenSerializer diff --git a/src/eatery/controllers/populate_eatery.py b/src/eatery/controllers/populate_eatery.py index f3f6634..13e365a 100644 --- a/src/eatery/controllers/populate_eatery.py +++ b/src/eatery/controllers/populate_eatery.py @@ -1,9 +1,10 @@ import json -from eatery.util.constants import dining_id_to_internal_id, SnapshotFileName +from eatery.util.constants import dining_id_to_internal_id, dining_id_to_image_url, SnapshotFileName from eatery.serializers import EaterySerializer from eatery.models import Eatery from django.core.exceptions import ObjectDoesNotExist + class PopulateEateryController: def __init__(self): self = self @@ -16,6 +17,7 @@ def generate_eatery(self, json_eatery): data = { "id": eatery_id, "name": json_eatery["name"], + "image_url": dining_id_to_image_url(json_eatery["id"]), "campus_area": json_eatery["campusArea"]["descrshort"], "latitude": json_eatery["latitude"], "longitude": json_eatery["longitude"], @@ -66,11 +68,11 @@ def add_eatery_store(self): for line in file: if len(line) > 2: json_objs.append(json.loads(line)) - + for json_obj in json_objs: try: object = Eatery.objects.get(id=int(json_obj["id"])) - except object.DoesNotExist: + except Eatery.DoesNotExist: """ Create a new Eatery object """ @@ -90,4 +92,4 @@ def process(self, json_eateries): for json_eatery in json_eateries: self.generate_eatery(json_eatery) - self.add_eatery_store() + self.add_eatery_store() \ No newline at end of file diff --git a/src/eatery/controllers/update_eatery.py b/src/eatery/controllers/update_eatery.py index be31278..04c33c3 100644 --- a/src/eatery/controllers/update_eatery.py +++ b/src/eatery/controllers/update_eatery.py @@ -60,11 +60,12 @@ def upload_image(self, image): "bucket": os.environ["IMAGE_BUCKET"], "image": f"data:image/{extension};base64,{b64_encoded_image}", }, + timeout=10, ) try: return response.json()["data"] - except: + except Exception: raise Exception("Image uploading unsuccessful") """ @@ -72,6 +73,7 @@ def upload_image(self, image): >> left merge Eatery and CornellDiningNow >> left merge Events and CornellDiningNow """ + def compare(self): pass diff --git a/src/eatery/datatype/Eatery.py b/src/eatery/datatype/Eatery.py index b0a80bd..f9d4679 100644 --- a/src/eatery/datatype/Eatery.py +++ b/src/eatery/datatype/Eatery.py @@ -1,11 +1,9 @@ -from datetime import date from enum import Enum -from typing import Optional -import pytz -#from event.datatype.Event import Event, filter_range -#from api.datatype.WaitTimesDay import WaitTimesDay -#from eatery.datatype.EateryAlert import EateryAlert + +# from event.datatype.Event import Event, filter_range +# from api.datatype.WaitTimesDay import WaitTimesDay +# from eatery.datatype.EateryAlert import EateryAlert class EateryID(Enum): @@ -49,4 +47,4 @@ class EateryID(Enum): MORRISON_DINING = 39 NOVICKS_CAFE = 40 VET_CAFE = 41 - + FREEDGE = 46 diff --git a/src/eatery/datatype/EateryAlert.py b/src/eatery/datatype/EateryAlert.py index 2bb511b..7dafc3c 100644 --- a/src/eatery/datatype/EateryAlert.py +++ b/src/eatery/datatype/EateryAlert.py @@ -1,11 +1,6 @@ class EateryAlert: - def __init__( - self, - id: int, - description: str, - start_timestamp: int, - end_timestamp: int + self, id: int, description: str, start_timestamp: int, end_timestamp: int ): self.id = id self.description = description @@ -17,14 +12,14 @@ def to_json(self): "id": 1, "description": self.description, "start_timestamp": self.start_timestamp, - "end_timestamp": self.end_timestamp + "end_timestamp": self.end_timestamp, } @staticmethod def from_json(alert_json): return EateryAlert( - id = alert_json["id"], + id=alert_json["id"], description=alert_json["description"], start_timestamp=alert_json["start_timestamp"], - end_timestamp=alert_json["end_timestamp"] + end_timestamp=alert_json["end_timestamp"], ) diff --git a/src/eatery/migrations/0001_initial.py b/src/eatery/migrations/0001_initial.py index ca330b0..30c531e 100644 --- a/src/eatery/migrations/0001_initial.py +++ b/src/eatery/migrations/0001_initial.py @@ -4,28 +4,43 @@ class Migration(migrations.Migration): - initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Eatery', + name="Eatery", fields=[ - ('id', models.IntegerField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=40)), - ('menu_summary', models.TextField(blank=True, default='', null=True)), - ('image_url', models.URLField(blank=True)), - ('location', models.TextField(blank=True)), - ('campus_area', models.CharField(blank=True, choices=[('West', 'West'), ('North', 'North'), ('Central', 'Central'), ('Collegetown', 'Collegetown'), ('', 'None')], default='', max_length=15)), - ('online_order_url', models.URLField(blank=True, null=True)), - ('latitude', models.FloatField(blank=True, null=True)), - ('longitude', models.FloatField(blank=True, null=True)), - ('payment_accepts_meal_swipes', models.BooleanField(blank=True, null=True)), - ('payment_accepts_brbs', models.BooleanField(blank=True, null=True)), - ('payment_accepts_cash', models.BooleanField(blank=True, null=True)), + ("id", models.IntegerField(primary_key=True, serialize=False)), + ("name", models.CharField(max_length=40)), + ("menu_summary", models.TextField(blank=True, default="", null=True)), + ("image_url", models.URLField(blank=True)), + ("location", models.TextField(blank=True)), + ( + "campus_area", + models.CharField( + blank=True, + choices=[ + ("West", "West"), + ("North", "North"), + ("Central", "Central"), + ("Collegetown", "Collegetown"), + ("", "None"), + ], + default="", + max_length=15, + ), + ), + ("online_order_url", models.URLField(blank=True, null=True)), + ("latitude", models.FloatField(blank=True, null=True)), + ("longitude", models.FloatField(blank=True, null=True)), + ( + "payment_accepts_meal_swipes", + models.BooleanField(blank=True, null=True), + ), + ("payment_accepts_brbs", models.BooleanField(blank=True, null=True)), + ("payment_accepts_cash", models.BooleanField(blank=True, null=True)), ], ), ] diff --git a/src/eatery/migrations/0002_alter_eatery_id.py b/src/eatery/migrations/0002_alter_eatery_id.py index 714af0b..0bcb3d6 100644 --- a/src/eatery/migrations/0002_alter_eatery_id.py +++ b/src/eatery/migrations/0002_alter_eatery_id.py @@ -4,15 +4,16 @@ class Migration(migrations.Migration): - dependencies = [ - ('eatery', '0001_initial'), + ("eatery", "0001_initial"), ] operations = [ migrations.AlterField( - model_name='eatery', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + model_name="eatery", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), ] diff --git a/src/eatery/migrations/0003_alter_eatery_image_url.py b/src/eatery/migrations/0003_alter_eatery_image_url.py index f5e1c81..f975e32 100644 --- a/src/eatery/migrations/0003_alter_eatery_image_url.py +++ b/src/eatery/migrations/0003_alter_eatery_image_url.py @@ -4,15 +4,17 @@ class Migration(migrations.Migration): - dependencies = [ - ('eatery', '0002_alter_eatery_id'), + ("eatery", "0002_alter_eatery_id"), ] operations = [ migrations.AlterField( - model_name='eatery', - name='image_url', - field=models.URLField(blank=True, default='https://images-prod.healthline.com/hlcmsresource/images/AN_images/health-benefits-of-apples-1296x728-feature.jpg'), + model_name="eatery", + name="image_url", + field=models.URLField( + blank=True, + default="https://images-prod.healthline.com/hlcmsresource/images/AN_images/health-benefits-of-apples-1296x728-feature.jpg", + ), ), ] diff --git a/src/eatery/migrations/0004_alter_eatery_campus_area.py b/src/eatery/migrations/0004_alter_eatery_campus_area.py index c83d70d..619c925 100644 --- a/src/eatery/migrations/0004_alter_eatery_campus_area.py +++ b/src/eatery/migrations/0004_alter_eatery_campus_area.py @@ -4,15 +4,26 @@ class Migration(migrations.Migration): - dependencies = [ - ('eatery', '0003_alter_eatery_image_url'), + ("eatery", "0003_alter_eatery_image_url"), ] operations = [ migrations.AlterField( - model_name='eatery', - name='campus_area', - field=models.CharField(blank=True, choices=[('West', 'West'), ('North', 'North'), ('Central', 'Central'), ('Collegetown', 'Collegetown'), ('East', 'East'), ('', 'None')], default='', max_length=15), + model_name="eatery", + name="campus_area", + field=models.CharField( + blank=True, + choices=[ + ("West", "West"), + ("North", "North"), + ("Central", "Central"), + ("Collegetown", "Collegetown"), + ("East", "East"), + ("", "None"), + ], + default="", + max_length=15, + ), ), ] diff --git a/src/eatery/migrations/0005_alter_eatery_campus_area.py b/src/eatery/migrations/0005_alter_eatery_campus_area.py index 55187b0..1b219b1 100644 --- a/src/eatery/migrations/0005_alter_eatery_campus_area.py +++ b/src/eatery/migrations/0005_alter_eatery_campus_area.py @@ -4,15 +4,25 @@ class Migration(migrations.Migration): - dependencies = [ - ('eatery', '0004_alter_eatery_campus_area'), + ("eatery", "0004_alter_eatery_campus_area"), ] operations = [ migrations.AlterField( - model_name='eatery', - name='campus_area', - field=models.CharField(blank=True, choices=[('West', 'West'), ('North', 'North'), ('Central', 'Central'), ('Collegetown', 'Collegetown'), ('', 'None')], default='', max_length=15), + model_name="eatery", + name="campus_area", + field=models.CharField( + blank=True, + choices=[ + ("West", "West"), + ("North", "North"), + ("Central", "Central"), + ("Collegetown", "Collegetown"), + ("", "None"), + ], + default="", + max_length=15, + ), ), ] diff --git a/src/eatery/models.py b/src/eatery/models.py index 731858a..a7fe69c 100644 --- a/src/eatery/models.py +++ b/src/eatery/models.py @@ -1,7 +1,7 @@ from django.db import models -from django.db import connection from eatery.util.constants import DEFAULT_IMAGE_URL + class Eatery(models.Model): class CampusArea(models.TextChoices): WEST = "West" @@ -23,6 +23,6 @@ class CampusArea(models.TextChoices): payment_accepts_meal_swipes = models.BooleanField(null=True, blank=True) payment_accepts_brbs = models.BooleanField(null=True, blank=True) payment_accepts_cash = models.BooleanField(null=True, blank=True) - + def __str__(self): return self.name diff --git a/src/eatery/permissions.py b/src/eatery/permissions.py index 42af072..1608cd2 100644 --- a/src/eatery/permissions.py +++ b/src/eatery/permissions.py @@ -1,13 +1,13 @@ from rest_framework import permissions -class EateryPermission(permissions.BasePermission): +class EateryPermission(permissions.BasePermission): def has_permission(self, request, view): - if view.action in ['list', 'retrieve']: + if view.action in ["list", "retrieve"]: return True return request.user.is_staff - + def has_object_permission(self, request, view, obj): - if view.action in ['retrieve']: + if view.action in ["retrieve"]: return True - return request.user.is_staff \ No newline at end of file + return request.user.is_staff diff --git a/src/eatery/serializers.py b/src/eatery/serializers.py index dbe4c0d..9f6dae0 100644 --- a/src/eatery/serializers.py +++ b/src/eatery/serializers.py @@ -1,15 +1,23 @@ from rest_framework import serializers from eatery.models import Eatery from event.models import Event -from event.serializers import EventSerializer, EventSerializerSimple, EventSerializerOptimized +from event.serializers import ( + EventSerializer, + EventSerializerSimple, + EventSerializerOptimized, +) from datetime import timedelta, datetime from zoneinfo import ZoneInfo + class EaterySerializer(serializers.ModelSerializer): id = serializers.IntegerField() name = serializers.CharField() - menu_summary = serializers.CharField(allow_null=True,default="Cornell Eatery") - image_url = serializers.URLField(allow_null=True,default="https://images-prod.healthline.com/hlcmsresource/images/AN_images/health-benefits-of-apples-1296x728-feature.jpg") + menu_summary = serializers.CharField(allow_null=True, default="Cornell Eatery") + image_url = serializers.URLField( + allow_null=True, + default="https://images-prod.healthline.com/hlcmsresource/images/AN_images/health-benefits-of-apples-1296x728-feature.jpg", + ) location = serializers.CharField(allow_null=True) campus_area = serializers.CharField(allow_null=True) online_order_url = serializers.URLField(allow_null=True) @@ -24,48 +32,115 @@ class EaterySerializer(serializers.ModelSerializer): def create(self, validated_data): eatery, _ = Eatery.objects.get_or_create(**validated_data) return eatery - + class Meta: model = Eatery - fields = ['id', 'name', 'menu_summary', 'image_url', 'location', 'campus_area', 'online_order_url', 'latitude', 'longitude', 'payment_accepts_meal_swipes', 'payment_accepts_brbs', 'payment_accepts_cash', 'events'] - + fields = [ + "id", + "name", + "menu_summary", + "image_url", + "location", + "campus_area", + "online_order_url", + "latitude", + "longitude", + "payment_accepts_meal_swipes", + "payment_accepts_brbs", + "payment_accepts_cash", + "events", + ] + + class EaterySerializerOptimized(serializers.ModelSerializer): events = EventSerializerOptimized(many=True, read_only=True) class Meta: model = Eatery - fields = ['id', 'name', 'menu_summary', 'image_url', 'location', 'campus_area', 'online_order_url', 'latitude', 'longitude', 'payment_accepts_meal_swipes', 'payment_accepts_brbs', 'payment_accepts_cash', 'events'] + fields = [ + "id", + "name", + "menu_summary", + "image_url", + "location", + "campus_area", + "online_order_url", + "latitude", + "longitude", + "payment_accepts_meal_swipes", + "payment_accepts_brbs", + "payment_accepts_cash", + "events", + ] class EaterySerializerSimple(serializers.ModelSerializer): - menu_summary = serializers.CharField(allow_null=True,default="Cornell Eatery") - image_url = serializers.URLField(allow_null=True,default="https://images-prod.healthline.com/hlcmsresource/images/AN_images/health-benefits-of-apples-1296x728-feature.jpg") + menu_summary = serializers.CharField(allow_null=True, default="Cornell Eatery") + image_url = serializers.URLField( + allow_null=True, + default="https://images-prod.healthline.com/hlcmsresource/images/AN_images/health-benefits-of-apples-1296x728-feature.jpg", + ) events = EventSerializerSimple(many=True, read_only=True) class Meta: model = Eatery - fields = ['id', 'name', 'menu_summary', 'image_url', 'location', 'campus_area', 'online_order_url', 'latitude', 'longitude', 'payment_accepts_meal_swipes', 'payment_accepts_brbs', 'payment_accepts_cash', 'events'] + fields = [ + "id", + "name", + "menu_summary", + "image_url", + "location", + "campus_area", + "online_order_url", + "latitude", + "longitude", + "payment_accepts_meal_swipes", + "payment_accepts_brbs", + "payment_accepts_cash", + "events", + ] + class EaterySerializerByDay(serializers.ModelSerializer): - menu_summary = serializers.CharField(allow_null=True,default="Cornell Eatery") - image_url = serializers.URLField(allow_null=True,default="https://images-prod.healthline.com/hlcmsresource/images/AN_images/health-benefits-of-apples-1296x728-feature.jpg") + menu_summary = serializers.CharField(allow_null=True, default="Cornell Eatery") + image_url = serializers.URLField( + allow_null=True, + default="https://images-prod.healthline.com/hlcmsresource/images/AN_images/health-benefits-of-apples-1296x728-feature.jpg", + ) events = serializers.SerializerMethodField() def get_events(self, obj): day_offset = self.context.get("day") now = datetime.now(ZoneInfo("America/New_York")) - day = now.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=day_offset) + day = now.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta( + days=day_offset + ) day_unix = int(day.timestamp()) day_end_unix = int((day + timedelta(days=1)).timestamp()) print(f"Now: {now}") print(f"Day: {day}") print(f"Day Unix: {day_unix}") print(f"Day End Unix: {day_end_unix}") - events = Event.objects.filter(eatery=obj.id, start__gte=day_unix, start__lt=day_end_unix) + events = Event.objects.filter( + eatery=obj.id, start__gte=day_unix, start__lt=day_end_unix + ) serializer = EventSerializerOptimized(instance=events, many=True) return serializer.data class Meta: model = Eatery - fields = ['id', 'name', 'menu_summary', 'image_url', 'location', 'campus_area', 'online_order_url', 'latitude', 'longitude', 'payment_accepts_meal_swipes', 'payment_accepts_brbs', 'payment_accepts_cash', 'events'] - + fields = [ + "id", + "name", + "menu_summary", + "image_url", + "location", + "campus_area", + "online_order_url", + "latitude", + "longitude", + "payment_accepts_meal_swipes", + "payment_accepts_brbs", + "payment_accepts_cash", + "events", + ] diff --git a/src/eatery/urls.py b/src/eatery/urls.py index e4519cf..5bed8fd 100644 --- a/src/eatery/urls.py +++ b/src/eatery/urls.py @@ -1,21 +1,15 @@ from django.urls import path from eatery.views import EateryViewSet, GetEateriesSimple, GetEateriesByDay -eateries_list = EateryViewSet.as_view({ - 'get':'list', - 'post': 'create' -}) +eateries_list = EateryViewSet.as_view({"get": "list", "post": "create"}) -eatery_list = EateryViewSet.as_view({ - 'get':'retrieve', - 'put':'update', - 'patch':'partial_update', - 'delete':'destroy' -}) +eatery_list = EateryViewSet.as_view( + {"get": "retrieve", "put": "update", "patch": "partial_update", "delete": "destroy"} +) urlpatterns = [ - path("", eateries_list, name='eateries-list'), - path("/", eatery_list, name='eatery-list'), - path("simple/", GetEateriesSimple.as_view(), name='eateries-simple'), + path("", eateries_list, name="eateries-list"), + path("/", eatery_list, name="eatery-list"), + path("simple/", GetEateriesSimple.as_view(), name="eateries-simple"), path("day//", GetEateriesByDay.as_view(), name="eateries-day"), -] \ No newline at end of file +] diff --git a/src/eatery/util/constants.py b/src/eatery/util/constants.py index 76e95ff..f9494a2 100644 --- a/src/eatery/util/constants.py +++ b/src/eatery/util/constants.py @@ -3,12 +3,23 @@ from eatery.datatype.Eatery import EateryID CORNELL_DINING_URL = "https://now.dining.cornell.edu/api/1.0/dining/eateries.json" -CORNELL_VENDOR_URL = "https://vendor-api-extra.scl.cornell.edu/api/external/location-count" +CORNELL_VENDOR_URL = ( + "https://vendor-api-extra.scl.cornell.edu/api/external/location-count" +) -DAY_OF_WEEK_LIST = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] +DAY_OF_WEEK_LIST = [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", +] DEFAULT_IMAGE_URL = "https://images-prod.healthline.com/hlcmsresource/images/AN_images/health-benefits-of-apples-1296x728-feature.jpg" + class SnapshotFileName(Enum): EATERY_STORE = "eatery_store.txt" ALERT_STORE = "alert_store.txt" @@ -25,155 +36,241 @@ class SnapshotFileName(Enum): SCHEDULE_EXCEPTION = "schedule_exception.txt" +EATERY_INFO = { + 31: { + "internal_id": EateryID.ONE_ZERO_FOUR_WEST, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/104-West.jpg" + }, + 7: { + "internal_id": EateryID.LIBE_CAFE, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Amit-Bhatia-Libe-Cafe.jpg" + }, + 8: { + "internal_id": EateryID.ATRIUM_CAFE, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Atrium-Cafe.jpg" + }, + 1: { + "internal_id": EateryID.BEAR_NECESSITIES, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Bear-Necessities.jpg" + }, + 25: { + "internal_id": EateryID.BECKER_HOUSE, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Becker-House-Dining.jpg" + }, + 10: { + "internal_id": EateryID.BIG_RED_BARN, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Big-Red-Barn.jpg" + }, + 11: { + "internal_id": EateryID.BUS_STOP_BAGELS, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Bug-Stop-Bagels.jpg" + }, + 12: { + "internal_id": EateryID.CAFE_JENNIE, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Cafe-Jennie.jpg" + }, + 26: { + "internal_id": EateryID.COOK_HOUSE, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Cook-House-Dining.jpg" + }, + 14: { + "internal_id": EateryID.DAIRY_BAR, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Cornell-Dairy-Bar.jpg" + }, + 41: { + "internal_id": EateryID.CROSSINGS_CAFE, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Crossings-Cafe.jpg" + }, + 32: { + "internal_id": EateryID.FRANNYS, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/frannys.jpg" + }, + 16: { + "internal_id": EateryID.GOLDIES_CAFE, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Goldies-Cafe.jpg" + }, + 15: { + "internal_id": EateryID.GREEN_DRAGON, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Green-Dragon.jpg" + }, + 24: { + "internal_id": EateryID.HOT_DOG_CART, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Hot-Dog-Cart.jpg" + }, + 34: { + "internal_id": EateryID.ICE_CREAM_BIKE, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/icecreamcart.jpg" + }, + 27: { + "internal_id": EateryID.BETHE_HOUSE, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Jansens-Dining.jpg" + }, + 28: { + "internal_id": EateryID.JANSENS_MARKET, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Jansens-Market.jpg" + }, + 29: { + "internal_id": EateryID.KEETON_HOUSE, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Keeton-House-Dining.jpg" + }, + 42: { + "internal_id": EateryID.MANN_CAFE, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Mann-Cafe.jpg" + }, + 18: { + "internal_id": EateryID.MARTHAS_CAFE, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Marthas-Cafe.jpg" + }, + 19: { + "internal_id": EateryID.MATTINS_CAFE, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Mattins-Cafe.jpg" + }, + 33: { + "internal_id": EateryID.MCCORMICKS, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/mccormicks.jpg" + }, + 3: { + "internal_id": EateryID.NORTH_STAR_DINING, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/North-Star.jpg" + }, + 20: { + "internal_id": EateryID.OKENSHIELDS, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Okenshields.jpg" + }, + 4: { + "internal_id": EateryID.RISLEY, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Risley-Dining.jpg" + }, + 5: { + "internal_id": EateryID.RPCC, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Risley-Dining.jpg" + }, + 30: { + "internal_id": EateryID.ROSE_HOUSE, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Rose-House-Dining.jpg" + }, + 21: { + "internal_id": EateryID.RUSTYS, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Rustys.jpg" + }, + 13: { + "internal_id": EateryID.STRAIGHT_FROM_THE_MARKET, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/StraightMarket.jpg" + }, + 23: { + "internal_id": EateryID.TRILLIUM, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Trillium.jpg" + }, + 43: { + "internal_id": EateryID.MORRISON_DINING, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Morrison-Dining.jpg" + }, + 44: { + "internal_id": EateryID.NOVICKS_CAFE, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/novicks-cafe.jpg" + }, + 45: { + "internal_id": EateryID.VET_CAFE, + "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/vets-cafe.jpg" + } +} + +VENDOR_NAME_TO_INTERNAL = { + "bearnecessities": EateryID.BEAR_NECESSITIES, + "northstarmarketplace": EateryID.NORTH_STAR_DINING, + "jansensmarket": EateryID.JANSENS_MARKET, + "stockinghallcafe": EateryID.DAIRY_BAR, + "stockinghall": EateryID.DAIRY_BAR, + "marthas": EateryID.MARTHAS_CAFE, + "cafejennie": EateryID.CAFE_JENNIE, + "goldiescafe": EateryID.GOLDIES_CAFE, + "alicecookhouse": EateryID.COOK_HOUSE, + "carlbeckerhouse": EateryID.BECKER_HOUSE, + "duffield": EateryID.MATTINS_CAFE, + "greendragon": EateryID.GREEN_DRAGON, + "trillium": EateryID.TRILLIUM, + "olinlibecafe": EateryID.LIBE_CAFE, + "statlerterrace": EateryID.TERRACE, + "busstopbagels": EateryID.BUS_STOP_BAGELS, + "kosher": EateryID.ONE_ZERO_FOUR_WEST, + "jansensatbethehouse": EateryID.BETHE_HOUSE, + "keetonhouse": EateryID.KEETON_HOUSE, + "rpme": EateryID.RPCC, + "rosehouse": EateryID.ROSE_HOUSE, + "risley": EateryID.RISLEY, + "frannysft": EateryID.FRANNYS, + "mccormicks": EateryID.MCCORMICKS, + "sage": EateryID.ATRIUM_CAFE, + "straightmarket": EateryID.STRAIGHT_FROM_THE_MARKET, + "crossingscafe": EateryID.CROSSINGS_CAFE, + "okenshields": EateryID.OKENSHIELDS, + "bigredbarn": EateryID.BIG_RED_BARN, + "rustys": EateryID.RUSTYS, + "manncafe": EateryID.MANN_CAFE, + "statlermacs": EateryID.MACS_CAFE, + "morrisondining": EateryID.MORRISON_DINING, + "morrison": EateryID.MORRISON_DINING, + "novickscafe": EateryID.NOVICKS_CAFE, + "Vet College Cafe": EateryID.VET_CAFE, +} + + +def get_eatery_info(id: int): + """Get both internal ID and image URL for a given dining ID""" + if id in EATERY_INFO: + return EATERY_INFO[id] + + print(f"Missing eatery_id {id}") + return { + "internal_id": None, + "image_url": DEFAULT_IMAGE_URL + } + + +def get_eatery_info_by_vendor_name(vendor_eatery_name): + """Get both internal ID and image URL for a given vendor name""" + vendor_eatery_name = "".join(c.lower() for c in vendor_eatery_name if c.isalpha()) + + internal_id = VENDOR_NAME_TO_INTERNAL.get(vendor_eatery_name) + + if internal_id is not None: + for dining_id, info in EATERY_INFO.items(): + if info["internal_id"] == internal_id: + return info + + return { + "internal_id": internal_id, + "image_url": DEFAULT_IMAGE_URL + } + + # TODO: Add a slack notif / flag that a wait time location was not recognized + return { + "internal_id": None, + "image_url": DEFAULT_IMAGE_URL + } + + def dining_id_to_internal_id(id: int): - if id == 31: - return EateryID.ONE_ZERO_FOUR_WEST - elif id == 7: - return EateryID.LIBE_CAFE - elif id == 8: - return EateryID.ATRIUM_CAFE - elif id == 1: - return EateryID.BEAR_NECESSITIES - elif id == 25: - return EateryID.BECKER_HOUSE - elif id == 10: - return EateryID.BIG_RED_BARN - elif id == 11: - return EateryID.BUS_STOP_BAGELS - elif id == 12: - return EateryID.CAFE_JENNIE - elif id == 26: - return EateryID.COOK_HOUSE - elif id == 14: - return EateryID.DAIRY_BAR - elif id == 41: - return EateryID.CROSSINGS_CAFE - elif id == 32: - return EateryID.FRANNYS - elif id == 16: - return EateryID.GOLDIES_CAFE - elif id == 15: - return EateryID.GREEN_DRAGON - elif id == 24: - return EateryID.HOT_DOG_CART - elif id == 34: - return EateryID.ICE_CREAM_BIKE - elif id == 27: - return EateryID.BETHE_HOUSE - elif id == 28: - return EateryID.JANSENS_MARKET - elif id == 29: - return EateryID.KEETON_HOUSE - elif id == 42: - return EateryID.MANN_CAFE - elif id == 18: - return EateryID.MARTHAS_CAFE - elif id == 19: - return EateryID.MATTINS_CAFE - elif id == 33: - return EateryID.MCCORMICKS - elif id == 3: - return EateryID.NORTH_STAR_DINING - elif id == 20: - return EateryID.OKENSHIELDS - elif id == 4: - return EateryID.RISLEY - elif id == 5: - return EateryID.RPCC - elif id == 30: - return EateryID.ROSE_HOUSE - elif id == 21: - return EateryID.RUSTYS - elif id == 13: - return EateryID.STRAIGHT_FROM_THE_MARKET - elif id == 23: - return EateryID.TRILLIUM - elif id == 43: - return EateryID.MORRISON_DINING - elif id == 44: - return EateryID.NOVICKS_CAFE - elif id == 45: - return EateryID.VET_CAFE - else: - print(f"Missing eatery_id {id}") - return None - - -# Our transactions vendor + """Convert dining ID to internal eatery ID""" + info = get_eatery_info(id) + return info["internal_id"] + + def vendor_name_to_internal_id(vendor_eatery_name): + """Convert vendor eatery name to internal eatery ID""" vendor_eatery_name = "".join(c.lower() for c in vendor_eatery_name if c.isalpha()) - if vendor_eatery_name == "bearnecessities": - return EateryID.BEAR_NECESSITIES - elif vendor_eatery_name == "northstarmarketplace": - return EateryID.NORTH_STAR_DINING - elif vendor_eatery_name == "jansensmarket": - return EateryID.JANSENS_MARKET - elif ( - vendor_eatery_name == "stockinghallcafe" or vendor_eatery_name == "stockinghall" - ): - return EateryID.DAIRY_BAR - elif vendor_eatery_name == "marthas": - return EateryID.MARTHAS_CAFE - elif vendor_eatery_name == "cafejennie": - return EateryID.CAFE_JENNIE - elif vendor_eatery_name == "goldiescafe": - return EateryID.GOLDIES_CAFE - elif vendor_eatery_name == "alicecookhouse": - return EateryID.COOK_HOUSE - elif vendor_eatery_name == "carlbeckerhouse": - return EateryID.BECKER_HOUSE - elif vendor_eatery_name == "duffield": - return EateryID.MATTINS_CAFE - elif vendor_eatery_name == "greendragon": - return EateryID.GREEN_DRAGON - elif vendor_eatery_name == "trillium": - return EateryID.TRILLIUM - elif vendor_eatery_name == "olinlibecafe": - return EateryID.LIBE_CAFE - elif vendor_eatery_name == "carolscafe": - return EateryID.CAROLS_CAFE - elif vendor_eatery_name == "statlerterrace": - return EateryID.TERRACE - elif vendor_eatery_name == "busstopbagels": - return EateryID.BUS_STOP_BAGELS - elif vendor_eatery_name == "kosher": - return EateryID.ONE_ZERO_FOUR_WEST - elif vendor_eatery_name == "jansensatbethehouse": - return EateryID.BETHE_HOUSE - elif vendor_eatery_name == "keetonhouse": - return EateryID.KEETON_HOUSE - elif vendor_eatery_name == "rpme": - return EateryID.RPCC - elif vendor_eatery_name == "rosehouse": - return EateryID.ROSE_HOUSE - elif vendor_eatery_name == "risley": - return EateryID.RISLEY - elif vendor_eatery_name == "frannysft": - return EateryID.FRANNYS - elif vendor_eatery_name == "mccormicks": - return EateryID.MCCORMICKS - elif vendor_eatery_name == "sage": - return EateryID.ATRIUM_CAFE - elif vendor_eatery_name == "straightmarket": - return EateryID.STRAIGHT_FROM_THE_MARKET - elif vendor_eatery_name == "crossingscafe": - return EateryID.CROSSINGS_CAFE - elif vendor_eatery_name == "okenshields": - return EateryID.OKENSHIELDS - elif vendor_eatery_name == "bigredbarn": - return EateryID.BIG_RED_BARN - elif vendor_eatery_name == "rustys": - return EateryID.RUSTYS - elif vendor_eatery_name == "manncafe": - return EateryID.MANN_CAFE - elif vendor_eatery_name == "statlermacs": - return EateryID.MACS_CAFE - elif vendor_eatery_name == "morrisondining" or vendor_eatery_name == "morrison": - return EateryID.MORRISON_DINING - elif vendor_eatery_name == "novickscafe": - return EateryID.NOVICKS_CAFE - elif vendor_eatery_name == "Vet College Cafe": - return EateryID.VET_CAFE - else: - # TODO: Add a slack notif / flag that a wait time location was not recognized - return None + + if vendor_eatery_name in VENDOR_NAME_TO_INTERNAL: + return VENDOR_NAME_TO_INTERNAL[vendor_eatery_name] + + # TODO: Add a slack notif / flag that a wait time location was not recognized + return None + + +def dining_id_to_image_url(id: int): + """Convert dining ID directly to image URL""" + if id in EATERY_INFO: + return EATERY_INFO[id]["image_url"] + + print(f"Missing image url for dining_id {id}") + return DEFAULT_IMAGE_URL \ No newline at end of file diff --git a/src/eatery/util/convert_from_json.py b/src/eatery/util/convert_from_json.py index 9134b41..7fca648 100644 --- a/src/eatery/util/convert_from_json.py +++ b/src/eatery/util/convert_from_json.py @@ -22,4 +22,4 @@ def from_json(obj: Union[list, dict], *args, **kwargs): return Event.from_json(obj) def description(self): - return "ConvertFromJson""" \ No newline at end of file + return "ConvertFromJson""" diff --git a/src/eatery/util/eatery_store.txt b/src/eatery/util/eatery_store.txt index 9488a26..977a9e7 100644 --- a/src/eatery/util/eatery_store.txt +++ b/src/eatery/util/eatery_store.txt @@ -3,4 +3,5 @@ {"id": 35, "name": "Temple of Zeus", "menu_summary": "Coffee, pastries, sandwiches", "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Zeus.jpg", "location": "Goldwin Smith Hall", "campus_area": "Central", "latitude": 42.449091, "longitude": -76.483414, "payment_accepts_meal_swipes": false, "payment_accepts_brbs": false, "payment_accepts_cash": true, "online_order_url": null} {"id": 36, "name": "Gimme Coffee", "menu_summary": "Coffee, pastries, tea", "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Gimme-Coffee.jpg", "location": "Gates Hall", "campus_area": "Central", "latitude": 42.444958, "longitude": -76.481169, "payment_accepts_meal_swipes": false, "payment_accepts_brbs": false, "payment_accepts_cash": true, "online_order_url": null} {"id": 37, "name": "Louie's Lunch", "menu_summary": "Burgers, fries, shakes", "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Louies-Lunch.jpg", "location": "Across from Risley", "campus_area": "Central", "latitude": 42.45336, "longitude": -76.481225, "payment_accepts_meal_swipes": false, "payment_accepts_brbs": false, "payment_accepts_cash": true, "online_order_url": null} -{"id": 38, "name": "Anabel's Grocery", "menu_summary": "Groceries, quick bites", "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Anabels-Grocery.jpg", "location": "Anabel Taylor Hall", "campus_area": "Central", "latitude": 42.445061, "longitude": -76.485826, "payment_accepts_meal_swipes": false, "payment_accepts_brbs": false, "payment_accepts_cash": true, "online_order_url": null} \ No newline at end of file +{"id": 38, "name": "Anabel's Grocery", "menu_summary": "Groceries, quick bites", "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Anabels-Grocery.jpg", "location": "Anabel Taylor Hall", "campus_area": "Central", "latitude": 42.445061, "longitude": -76.485826, "payment_accepts_meal_swipes": false, "payment_accepts_brbs": false, "payment_accepts_cash": true, "online_order_url": null} +{"id": 46, "name": "Free Food Fridge", "menu_summary": "Free food", "image_url": "https://raw.githubusercontent.com/cuappdev/assets/master/eatery/eatery-images/Freege.jpg", "location": "Anabel Taylor Hall Room 120", "campus_area": "Central", "latitude": 42.445061, "longitude": -76.485826, "payment_accepts_meal_swipes": false, "payment_accepts_brbs": false, "payment_accepts_cash": false, "online_order_url": null} \ No newline at end of file diff --git a/src/eatery/util/json.py b/src/eatery/util/json.py index a7d80af..17688a9 100644 --- a/src/eatery/util/json.py +++ b/src/eatery/util/json.py @@ -23,7 +23,7 @@ def verify_json_fields( if not isinstance(json[field], str): return False elif field_type_map[field] is FieldType.EATERYID: - if not isinstance(json[field], int) or EateryID(json[field]) == None: + if not isinstance(json[field], int) or EateryID(json[field]) is None: return False for field in json: diff --git a/src/eatery/util/time.py b/src/eatery/util/time.py index 7fce11d..b03a0dd 100644 --- a/src/eatery/util/time.py +++ b/src/eatery/util/time.py @@ -1,5 +1,6 @@ from datetime import date, time, datetime import pytz + def combined_timestamp(date: date, time: time, tzinfo: pytz.timezone) -> int: return int(tzinfo.localize(datetime.combine(date, time)).timestamp()) diff --git a/src/eatery/views.py b/src/eatery/views.py index db04c35..3c6f6db 100644 --- a/src/eatery/views.py +++ b/src/eatery/views.py @@ -1,4 +1,9 @@ -from eatery.serializers import EaterySerializer, EaterySerializerSimple, EaterySerializerByDay, EaterySerializerOptimized +from eatery.serializers import ( + EaterySerializer, + EaterySerializerSimple, + EaterySerializerByDay, + EaterySerializerOptimized, +) from eatery.util.json import FieldType, error_json, success_json, verify_json_fields from django.http import JsonResponse from django.shortcuts import get_object_or_404 @@ -12,10 +17,12 @@ from eatery.models import Eatery from .controllers.update_eatery import UpdateEateryController + class EateryViewSet(viewsets.ModelViewSet): """ View and edit eateries (all, or specific) """ + queryset = Eatery.objects.all() serializer_class = EaterySerializer permission_classes = [EateryPermission] @@ -25,7 +32,7 @@ def retrieve(self, request, *args, **kwargs): serializer = EaterySerializerOptimized(instance) return Response(serializer.data) - @method_decorator(cache_page(60*60*2)) # cache for 2 hours + @method_decorator(cache_page(60 * 60 * 2)) # cache for 2 hours def list(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) serializer = EaterySerializerOptimized(queryset, many=True) @@ -35,12 +42,12 @@ def get_object(self): queryset = self.filter_queryset(self.get_queryset()) lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field - assert lookup_url_kwarg in self.kwargs, ( - 'Expected view %s to be called with a URL keyword argument ' - 'named "%s". Fix your URL conf, or set the `.lookup_field` ' - 'attribute on the view correctly.' % - (self.__class__.__name__, lookup_url_kwarg) - ) + # assert lookup_url_kwarg in self.kwargs, ( + # "Expected view %s to be called with a URL keyword argument " + # 'named "%s". Fix your URL conf, or set the `.lookup_field` ' + # "attribute on the view correctly." + # % (self.__class__.__name__, lookup_url_kwarg) + # ) # Uses the lookup_field attribute, which defaults to `pk` filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]} obj = get_object_or_404(queryset, **filter_kwargs) @@ -75,7 +82,7 @@ def update(self, request, *args, **kwargs): id = int(text_params.get("id")) try: image_param = request.FILES.get("image") - except: + except Exception: image_param = None try: @@ -84,19 +91,27 @@ def update(self, request, *args, **kwargs): except Exception as e: return JsonResponse(error_json(str(e))) + class GetEateriesSimple(APIView): """ View all eateries with less information """ + def get(self, request): eateries = EaterySerializerSimple(Eatery.objects.all(), many=True) return Response(eateries.data) - + + class GetEateriesByDay(APIView): """ Get all eatery information by day """ - @method_decorator(cache_page(60*60*2)) # cache for 2 hours + + @method_decorator(cache_page(60 * 60 * 2)) # cache for 2 hours def get(self, request, day): - eateries = EaterySerializerByDay(Eatery.objects.exclude(events__event_description="Open"), many=True, context={"day": day}) - return Response(eateries.data) \ No newline at end of file + eateries = EaterySerializerByDay( + Eatery.objects.exclude(events__event_description="Open"), + many=True, + context={"day": day}, + ) + return Response(eateries.data) diff --git a/src/eatery_blue_backend/management/commands/populate_models.py b/src/eatery_blue_backend/management/commands/populate_models.py index 4191bd0..89e1d13 100644 --- a/src/eatery_blue_backend/management/commands/populate_models.py +++ b/src/eatery_blue_backend/management/commands/populate_models.py @@ -1,72 +1,155 @@ from django.core.management.base import BaseCommand from datetime import datetime -import requests +import requests +from eatery.datatype.Eatery import EateryID from eatery.util.constants import CORNELL_DINING_URL from event.models import Event from eatery.controllers.populate_eatery import PopulateEateryController from event.controllers.populate_event import PopulateEventController from item.controllers.populate_item import PopulateItemController from category.controllers.populate_category import PopulateCategoryController +import os +import json + class Command(BaseCommand): - help = 'Populates all models' - def handle(self, *args, **kwargs): - self.stdout.write(f"Populating models at {datetime.now()} UTC") - start = int(datetime.now().timestamp()) - self.process() - self.stdout.write(f"Finished populating models at {datetime.now()} UTC ({int(datetime.now().timestamp()) - start}s)") - - def get_json(self): - try: - response = requests.get(CORNELL_DINING_URL) - except Exception as e: - raise e - if response.status_code <= 400: - response = response.json() - json_eateries = response["data"]["eateries"] - return json_eateries - - def logger_wrapper(self, command_obj, log_title, args): - pre = int(datetime.now().timestamp()) - print(f"{datetime.now()} UTC: {log_title}") - output = command_obj.process(*args) - print(f"Done ({int(datetime.now().timestamp()) - pre}s) ") - return output - - def process(self): - """ - 1. Get JSON from API + help = "Populates all models" + + def handle(self, *args, **kwargs): + self.stdout.write(f"Populating models at {datetime.now()} UTC") + start = int(datetime.now().timestamp()) + self.process() + self.stdout.write( + f"Finished populating models at {datetime.now()} UTC ({int(datetime.now().timestamp()) - start}s)" + ) + + def get_json(self): + try: + response = requests.get(CORNELL_DINING_URL, timeout=10) + except Exception as e: + raise e + if response.status_code <= 400: + response = response.json() + json_eateries = response["data"]["eateries"] + return json_eateries - 2. create eateries (fron CDN json) + def update_freedge_external_eatery(self): + GOOGLE_SHEETS_API_KEY = os.environ.get("GOOGLE_SHEETS_API_KEY") + FREEDGE_SHEET_ID = os.environ.get("FREEDGE_SHEET_ID") + FREEDGE_APPROVED_EMAILS = os.environ.get("FREEDGE_APPROVED_EMAILS") + if not GOOGLE_SHEETS_API_KEY: + print("GOOGLE_SHEETS_API_KEY not set, cannot update freedge external eatery") + return + if not FREEDGE_SHEET_ID: + print("FREEDGE_SHEET_ID not set, cannot update freedge external eatery") + return + if not FREEDGE_APPROVED_EMAILS: + print("FREEDGE_APPROVED_EMAILS not set, cannot update freedge external eatery") + return + + approved_emails = FREEDGE_APPROVED_EMAILS.split(",") + + try: + response = requests.get(f"https://sheets.googleapis.com/v4/spreadsheets/{FREEDGE_SHEET_ID}/values/A2:M?key={GOOGLE_SHEETS_API_KEY}", timeout=10) + except Exception as e: + raise e + if response.status_code > 400: + return + + response = response.json() + if "values" not in response: + print("No values in response, cannot update freedge external eatery") + print(response) + return + freege_items = response["values"] + + freege_dining_items = [] + for item in freege_items: + # item[1] is the email + # item[4] is item name + # item[7] is allergens + # item[8] is dietary preferences + # item[11] is if it is there + if len(item) < 12: + continue + if item[1] not in approved_emails or item[11] != "Yes": + continue + + freege_dining_items.append({ + "item": item[4].strip(), + "healthy": False, + "category": "Free Food", + "dietaryPreferences": item[8].split(", ") if item[8] != "N/A" else [], + "allergens": item[7].split(", ") if item[7] != "N/A" else [], + }) + + with open("./static_sources/external_eateries_static.json", "r") as external_eateries_file: + external_eateries_json = json.load(external_eateries_file) + + for eatery in external_eateries_json["eateries"]: + if eatery["id"] == EateryID.FREEDGE.value: + print("Updating freege external eatery") + eatery["diningItems"] = freege_dining_items + break + + with open("./static_sources/external_eateries.json", "w") as external_eateries_file: + json.dump(external_eateries_json, external_eateries_file, indent=2) - 3. create events (from CDN json) - return events_dict = { eatery_id : [event, event, event...], eatery_id : ... } + def logger_wrapper(self, command_obj, log_title, args): + pre = int(datetime.now().timestamp()) + print(f"{datetime.now()} UTC: {log_title}") + output = command_obj.process(*args) + print(f"Done ({int(datetime.now().timestamp()) - pre}s) ") + return output - 4. create menus for every eatery's events - return menus_dict = { eatery_id : [menu, menu, menu...] } + def process(self): + """ + 1. Get JSON from API - 5. create categories in each menu - return categories_dict = - { eatery_id : - { menu[i] : {"category_name" : id, "category_name" : id...}, - menu[i] : {"category_name" : id...} - } - } + 2. create eateries (fron CDN json) - 6. create items for each category + 3. create events (from CDN json) + return events_dict = { eatery_id : [event, event, event...], eatery_id : ... } - """ + 4. create menus for every eatery's events + return menus_dict = { eatery_id : [menu, menu, menu...] } - json_eateries = self.get_json() - - Event.truncate() + 5. create categories in each menu + return categories_dict = + { eatery_id : + { menu[i] : {"category_name" : id, "category_name" : id...}, + menu[i] : {"category_name" : id...} + } + } + + 6. create items for each category + + """ + + json_eateries = self.get_json() + + self.update_freedge_external_eatery() + + Event.truncate() - self.logger_wrapper(PopulateEateryController(), "Populating eateries", [json_eateries]) + self.logger_wrapper( + PopulateEateryController(), "Populating eateries", [json_eateries] + ) - events_dict = self.logger_wrapper(PopulateEventController(), "Populating events", [json_eateries]) + events_dict = self.logger_wrapper( + PopulateEventController(), "Populating events", [json_eateries] + ) - categories_dict = self.logger_wrapper(PopulateCategoryController(), "Populating categories", [events_dict, json_eateries]) + categories_dict = self.logger_wrapper( + PopulateCategoryController(), + "Populating categories", + [events_dict, json_eateries], + ) - self.logger_wrapper(PopulateItemController(), "Populating items", [categories_dict, json_eateries]) + self.logger_wrapper( + PopulateItemController(), + "Populating items", + [categories_dict, json_eateries], + ) - print("Done populating") + print("Done populating") diff --git a/src/eatery_blue_backend/settings.py b/src/eatery_blue_backend/settings.py index 53dbae8..27a1841 100644 --- a/src/eatery_blue_backend/settings.py +++ b/src/eatery_blue_backend/settings.py @@ -12,6 +12,18 @@ import os from pathlib import Path +import sentry_sdk + +sentry_sdk.init( + dsn="https://0f2451bfd9bf84c1858cf7319bd51425@o4507365244010496.ingest.us.sentry.io/4508008440856576", + # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for tracing. + traces_sample_rate=1.0, + # Set profiles_sample_rate to 1.0 to profile 100% + # of sampled transactions. + # We recommend adjusting this value in production. + profiles_sample_rate=1.0, +) # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -40,7 +52,7 @@ "django.contrib.messages", "django.contrib.staticfiles", "rest_framework.authtoken", - + "drf_yasg", # Apps "eatery_blue_backend", "eatery", @@ -48,8 +60,8 @@ "report", "item", "category", - "person", - + "user", + "device_token", # Third party "rest_framework", ] diff --git a/src/eatery_blue_backend/urls.py b/src/eatery_blue_backend/urls.py index f289980..b1de75f 100644 --- a/src/eatery_blue_backend/urls.py +++ b/src/eatery_blue_backend/urls.py @@ -1,5 +1,18 @@ +from drf_yasg import openapi from django.contrib import admin from django.urls import include, path +from drf_yasg.views import get_schema_view +from rest_framework import permissions + +schema_view = get_schema_view( + openapi.Info( + title="Eatery Blue Backend API Docs", + default_version="v1", + ), + public=True, + permission_classes=(permissions.AllowAny,), +) + urlpatterns = [ path("admin/", admin.site.urls), @@ -8,5 +21,7 @@ path("item/", include("item.urls")), path("category/", include("category.urls")), path("report/", include("report.urls")), - path("person/", include("person.urls")), + path("user/", include("user.urls")), + path("device-token/", include("device_token.urls")), + path("docs/", schema_view.with_ui("swagger", cache_timeout=0)), ] diff --git a/src/event/apps.py b/src/event/apps.py index 766f251..52dbc82 100644 --- a/src/event/apps.py +++ b/src/event/apps.py @@ -1,6 +1,6 @@ from django.apps import AppConfig + class EventConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' + default_auto_field = "django.db.models.BigAutoField" name = "event" - diff --git a/src/event/controllers/populate_event.py b/src/event/controllers/populate_event.py index 8823920..b98a25e 100644 --- a/src/event/controllers/populate_event.py +++ b/src/event/controllers/populate_event.py @@ -4,7 +4,8 @@ import json import pytz -class PopulateEventController(): + +class PopulateEventController: def __init__(self): self = self @@ -13,33 +14,34 @@ def generate_events(self, json_eatery): From an eatery json from CDN, create events for that eatery and add to event model. """ - #events = [ event obj, event, event ... ] for an eatery. + # events = [ event obj, event, event ... ] for an eatery. events = [] json_dates = json_eatery["operatingHours"] for json_date in json_dates: json_events = json_date["events"] - + for json_event in json_events: # Create an event: eatery_id = dining_id_to_internal_id(json_eatery["id"]).value data = { - 'eatery': eatery_id, - 'event_description': json_event["descr"], - 'start' : int(json_event["startTimestamp"]), - 'end' : int(json_event["endTimestamp"])} + "eatery": eatery_id, + "event_description": json_event["descr"], + "start": int(json_event["startTimestamp"]), + "end": int(json_event["endTimestamp"]), + } event = EventSerializer(data=data) - + if event.is_valid(): event.save() else: print(event.errors) return event.errors - - events.append(event.data["id"]) + + events.append(event.data["id"]) return events - + def generate_external_events(self, json_eatery): json_dates = json_eatery["operatingHours"] events = [] @@ -48,47 +50,60 @@ def generate_external_events(self, json_eatery): date = datetime.now() while date.strftime("%A").lower() != json_date["weekday"].lower(): date += timedelta(days=1) - start_string = json_event['start'] - timezone = pytz.timezone('US/Eastern') - start_time = datetime(date.year, date.month, date.day, int(start_string[:2]), int(start_string[3:])) + start_string = json_event["start"] + timezone = pytz.timezone("US/Eastern") + start_time = datetime( + date.year, + date.month, + date.day, + int(start_string[:2]), + int(start_string[3:]), + ) start_timestamp = timezone.localize(start_time).timestamp() - end_string = json_event['end'] + end_string = json_event["end"] if int(end_string[:2]) < int(start_string[:2]): date += timedelta(days=1) - end_time = datetime(date.year, date.month, date.day, int(end_string[:2]), int(end_string[3:])) + end_time = datetime( + date.year, + date.month, + date.day, + int(end_string[:2]), + int(end_string[3:]), + ) end_timestamp = timezone.localize(end_time).timestamp() eatery_id = json_eatery["id"] data = { - 'eatery': eatery_id, - 'event_description': json_event["descr"], - 'start' : start_timestamp, - 'end' : end_timestamp} + "eatery": eatery_id, + "event_description": json_event["descr"], + "start": start_timestamp, + "end": end_timestamp, + } event = EventSerializer(data=data) - + if event.is_valid(): event.save() else: print(event.errors) return event.errors - + events.append(event.data["id"]) return events def process(self, json_eateries): - #events_dict { eatery_id : [event, event, event...], eatery_id : ... } + # events_dict { eatery_id : [event, event, event...], eatery_id : ... } events_dict = {} for json_eatery in json_eateries: eatery_id = int(json_eatery["id"]) events = self.generate_events(json_eatery) - events_dict[eatery_id] = events + events_dict[eatery_id] = events # create custom events for external eateries with open("./static_sources/external_eateries.json", "r") as file: json_obj = json.load(file) - for eatery in json_obj['eateries']: - events_dict[eatery['id']] = self.generate_external_events(eatery) + for eatery in json_obj["eateries"]: + events_dict[eatery["id"]] = self.generate_external_events(eatery) - return events_dict + return events_dict diff --git a/src/event/controllers/update_models/CornellDiningEvents.py b/src/event/controllers/update_models/CornellDiningEvents.py index 2ae6387..a3debc5 100644 --- a/src/event/controllers/update_models/CornellDiningEvents.py +++ b/src/event/controllers/update_models/CornellDiningEvents.py @@ -18,7 +18,7 @@ def __init__(self, eatery_id: EateryID, cache): def __call__(self, *args, **kwargs) -> list[Eatery]: if "eateries" not in self.cache: try: - response = requests.get(CORNELL_DINING_URL).json() + response = requests.get(CORNELL_DINING_URL, timeout=10).json() except Exception as e: raise e if response["status"] == "success": @@ -48,7 +48,8 @@ def parse_eatery(json_eatery: dict) -> Eatery: @staticmethod def eatery_events_from_json( - json_operating_hours: list, json_dining_items: list, is_cafe: bool) -> list[Event]: + json_operating_hours: list, json_dining_items: list, is_cafe: bool + ) -> list[Event]: json_operating_hours = sorted( json_operating_hours, key=lambda json_date_events: json_date_events["date"] ) @@ -114,10 +115,7 @@ def dining_hall_menu_from_json(json_menu: list) -> Menu: @staticmethod def from_cornell_dining_json(json_item: dict): - return MenuItem( - healthy=json_item["healthy"], - name=json_item["item"] - ) + return MenuItem(healthy=json_item["healthy"], name=json_item["item"]) def description(self): return "CornellDiningEvents" diff --git a/src/event/controllers/update_models/CornellDiningNow.py b/src/event/controllers/update_models/CornellDiningNow.py index 98693c7..d668a11 100644 --- a/src/event/controllers/update_models/CornellDiningNow.py +++ b/src/event/controllers/update_models/CornellDiningNow.py @@ -7,7 +7,7 @@ class CornellDiningNow(DfgNode): def __call__(self, *args, **kwargs) -> list[Eatery]: try: - response = requests.get(CORNELL_DINING_URL).json() + response = requests.get(CORNELL_DINING_URL, timeout=10).json() except Exception as e: raise e diff --git a/src/event/controllers/update_models/schedule/CornellDiningEvents.py b/src/event/controllers/update_models/schedule/CornellDiningEvents.py index 7101169..a3debc5 100644 --- a/src/event/controllers/update_models/schedule/CornellDiningEvents.py +++ b/src/event/controllers/update_models/schedule/CornellDiningEvents.py @@ -18,7 +18,7 @@ def __init__(self, eatery_id: EateryID, cache): def __call__(self, *args, **kwargs) -> list[Eatery]: if "eateries" not in self.cache: try: - response = requests.get(CORNELL_DINING_URL).json() + response = requests.get(CORNELL_DINING_URL, timeout=10).json() except Exception as e: raise e if response["status"] == "success": @@ -115,10 +115,7 @@ def dining_hall_menu_from_json(json_menu: list) -> Menu: @staticmethod def from_cornell_dining_json(json_item: dict): - return MenuItem( - healthy=json_item["healthy"], - name=json_item["item"] - ) + return MenuItem(healthy=json_item["healthy"], name=json_item["item"]) def description(self): return "CornellDiningEvents" diff --git a/src/event/controllers/update_models/schedule/RepeatingSchedule.py b/src/event/controllers/update_models/schedule/RepeatingSchedule.py index 6b27a8a..58c5d10 100644 --- a/src/event/controllers/update_models/schedule/RepeatingSchedule.py +++ b/src/event/controllers/update_models/schedule/RepeatingSchedule.py @@ -14,9 +14,9 @@ def __init__(self, eatery_id: EateryID, cache): def __call__(self, *args, **kwargs) -> list[Eatery]: if "day_of_week_schedules" not in self.cache: - self.cache[ - "day_of_week_schedules" - ] = RepeatingEventSchedule.objects.all().values() + self.cache["day_of_week_schedules"] = ( + RepeatingEventSchedule.objects.all().values() + ) repeating_schedules = [ sched for sched in self.cache["day_of_week_schedules"] diff --git a/src/event/datatype/Event.py b/src/event/datatype/Event.py index 02e8024..a6d2912 100644 --- a/src/event/datatype/Event.py +++ b/src/event/datatype/Event.py @@ -3,7 +3,8 @@ import pytz from datatype.Menu import Menu -#from api.util.time import combined_timestamp + +from api.util.time import combined_timestamp class Event: @@ -114,4 +115,4 @@ def filter_range( else: raise Exception( f"Improper arguments. tzinfo={tzinfo}, start={start}, end={end}" - ) \ No newline at end of file + ) diff --git a/src/event/datatype/Menu.py b/src/event/datatype/Menu.py index 118d378..b7375aa 100644 --- a/src/event/datatype/Menu.py +++ b/src/event/datatype/Menu.py @@ -1,7 +1,7 @@ from datatype.MenuCategory import MenuCategory -class Menu: +class Menu: def __init__(self, categories: list[MenuCategory]): self.categories = categories diff --git a/src/event/datatype/MenuCategory.py b/src/event/datatype/MenuCategory.py index f2cd1cc..55770f9 100644 --- a/src/event/datatype/MenuCategory.py +++ b/src/event/datatype/MenuCategory.py @@ -2,7 +2,6 @@ class MenuCategory: - def __init__(self, category: str, items: list[MenuItem]): self.category = category self.items = items @@ -10,12 +9,12 @@ def __init__(self, category: str, items: list[MenuItem]): def to_json(self): return { "category": self.category, - "items": [item.to_json() for item in self.items] + "items": [item.to_json() for item in self.items], } @staticmethod def from_json(category_json): return MenuCategory( category=category_json["category"], - items=[MenuItem.from_json(item) for item in category_json["items"]] + items=[MenuItem.from_json(item) for item in category_json["items"]], ) diff --git a/src/event/datatype/MenuItem.py b/src/event/datatype/MenuItem.py index 34124db..12a60ee 100644 --- a/src/event/datatype/MenuItem.py +++ b/src/event/datatype/MenuItem.py @@ -2,15 +2,15 @@ from api.datatype.MenuItemSection import MenuItemSection -class MenuItem: +class MenuItem: def __init__( - self, - name: str, - healthy: Optional[bool] = None, - base_price: Optional[float] = None, - description: Optional[str] = None, - sections: Optional[MenuItemSection] = None + self, + name: str, + healthy: Optional[bool] = None, + base_price: Optional[float] = None, + description: Optional[str] = None, + sections: Optional[MenuItemSection] = None, ): self.healthy = healthy self.name = name @@ -24,7 +24,11 @@ def to_json(self): "name": self.name, "base_price": self.base_price, "description": self.description, - "sections": None if self.sections is None else [section.to_json() for section in self.sections] + "sections": ( + None + if self.sections is None + else [section.to_json() for section in self.sections] + ), } @staticmethod @@ -34,6 +38,12 @@ def from_json(item_json): healthy=item_json["healthy"], base_price=item_json["base_price"], description=item_json["description"], - sections=None if "sections" not in item_json or item_json["sections"] is None - else [MenuItemSection.from_json(section) for section in item_json["sections"]] - ) + sections=( + None + if "sections" not in item_json or item_json["sections"] is None + else [ + MenuItemSection.from_json(section) + for section in item_json["sections"] + ] + ), + ) diff --git a/src/event/datatype/MenuItemSection.py b/src/event/datatype/MenuItemSection.py index 770c376..fae1105 100644 --- a/src/event/datatype/MenuItemSection.py +++ b/src/event/datatype/MenuItemSection.py @@ -1,7 +1,7 @@ from api.datatype.MenuSubItem import MenuSubItem -class MenuItemSection: +class MenuItemSection: def __init__(self, name: str, subitems: list[MenuSubItem]): self.name = name self.subitems = subitems @@ -9,12 +9,12 @@ def __init__(self, name: str, subitems: list[MenuSubItem]): def to_json(self): return { "name": self.name, - "subitems": [item.to_json() for item in self.subitems] + "subitems": [item.to_json() for item in self.subitems], } @staticmethod def from_json(section_json): return MenuItemSection( name=section_json["name"], - subitems=[MenuSubItem.from_json(item) for item in section_json["subitems"]] + subitems=[MenuSubItem.from_json(item) for item in section_json["subitems"]], ) diff --git a/src/event/datatype/MenuSubItem.py b/src/event/datatype/MenuSubItem.py index 854b0ed..e236317 100644 --- a/src/event/datatype/MenuSubItem.py +++ b/src/event/datatype/MenuSubItem.py @@ -1,12 +1,9 @@ from typing import Optional -class MenuSubItem: +class MenuSubItem: def __init__( - self, - name: str, - total_price: Optional[float], - additional_price: Optional[float] + self, name: str, total_price: Optional[float], additional_price: Optional[float] ): self.name = name self.total_price = total_price @@ -16,7 +13,7 @@ def to_json(self): return { "name": self.name, "total_price": self.total_price, - "additional_price": self.additional_price + "additional_price": self.additional_price, } @staticmethod @@ -24,5 +21,5 @@ def from_json(item_json): return MenuSubItem( name=item_json["name"], total_price=item_json.get("total_price"), - additional_price=item_json.get("additional_price") + additional_price=item_json.get("additional_price"), ) diff --git a/src/event/datatype/WaitTime.py b/src/event/datatype/WaitTime.py index 7e5cfca..70c8265 100644 --- a/src/event/datatype/WaitTime.py +++ b/src/event/datatype/WaitTime.py @@ -1,10 +1,10 @@ class WaitTime: def __init__( - self, - timestamp: int, - wait_time_low: float, - wait_time_expected: float, - wait_time_high: float + self, + timestamp: int, + wait_time_low: float, + wait_time_expected: float, + wait_time_high: float, ): self.timestamp = timestamp self.wait_time_low = wait_time_low @@ -16,7 +16,7 @@ def to_json(self): "timestamp": self.timestamp, "wait_time_low": self.wait_time_low, "wait_time_expected": self.wait_time_expected, - "wait_time_high": self.wait_time_high + "wait_time_high": self.wait_time_high, } @staticmethod @@ -25,5 +25,5 @@ def from_json(wait_time_json): timestamp=wait_time_json["timestamp"], wait_time_low=wait_time_json["wait_time_low"], wait_time_expected=wait_time_json["wait_time_expected"], - wait_time_high=wait_time_json["wait_time_high"] + wait_time_high=wait_time_json["wait_time_high"], ) diff --git a/src/event/datatype/WaitTimesDay.py b/src/event/datatype/WaitTimesDay.py index cb9ea86..e3c724c 100644 --- a/src/event/datatype/WaitTimesDay.py +++ b/src/event/datatype/WaitTimesDay.py @@ -3,23 +3,22 @@ class WaitTimesDay: - def __init__( - self, - canonical_date: date, - data: list[WaitTime] - ): + def __init__(self, canonical_date: date, data: list[WaitTime]): self.canonical_date = canonical_date self.data = data def to_json(self): return { "canonical_date": str(self.canonical_date), - "data": [wait_time.to_json() for wait_time in self.data] + "data": [wait_time.to_json() for wait_time in self.data], } @staticmethod def from_json(wait_times_day_json): return WaitTimesDay( canonical_date=date.fromisoformat(wait_times_day_json["canonical_date"]), - data=[WaitTime.from_json(wait_time) for wait_time in wait_times_day_json["data"]] + data=[ + WaitTime.from_json(wait_time) + for wait_time in wait_times_day_json["data"] + ], ) diff --git a/src/event/migrations/0001_initial.py b/src/event/migrations/0001_initial.py index fc114f2..a8788ed 100644 --- a/src/event/migrations/0001_initial.py +++ b/src/event/migrations/0001_initial.py @@ -5,22 +5,44 @@ class Migration(migrations.Migration): - initial = True dependencies = [ - ('eatery', '0001_initial'), + ("eatery", "0001_initial"), ] operations = [ migrations.CreateModel( - name='Event', + name="Event", fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('event_description', models.TextField(blank=True, choices=[('Breakfast', 'Breakfast'), ('Brunch', 'Brunch'), ('Lunch', 'Lunch'), ('Dinner', 'Dinner'), ('General', 'General'), ('Cafe', 'Cafe'), ('Pants', 'Pants')], default='General', null=True)), - ('start', models.IntegerField(default=0)), - ('end', models.IntegerField(default=0)), - ('eatery', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='events', to='eatery.eatery')), + ("id", models.AutoField(primary_key=True, serialize=False)), + ( + "event_description", + models.TextField( + blank=True, + choices=[ + ("Breakfast", "Breakfast"), + ("Brunch", "Brunch"), + ("Lunch", "Lunch"), + ("Dinner", "Dinner"), + ("General", "General"), + ("Cafe", "Cafe"), + ("Pants", "Pants"), + ], + default="General", + null=True, + ), + ), + ("start", models.IntegerField(default=0)), + ("end", models.IntegerField(default=0)), + ( + "eatery", + models.ForeignKey( + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="eatery.eatery", + ), + ), ], ), ] diff --git a/src/event/migrations/0002_alter_event_id.py b/src/event/migrations/0002_alter_event_id.py index 6570118..a18da95 100644 --- a/src/event/migrations/0002_alter_event_id.py +++ b/src/event/migrations/0002_alter_event_id.py @@ -4,15 +4,16 @@ class Migration(migrations.Migration): - dependencies = [ - ('event', '0001_initial'), + ("event", "0001_initial"), ] operations = [ migrations.AlterField( - model_name='event', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + model_name="event", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), ] diff --git a/src/event/migrations/0003_alter_event_event_description.py b/src/event/migrations/0003_alter_event_event_description.py index 86afd1b..744b654 100644 --- a/src/event/migrations/0003_alter_event_event_description.py +++ b/src/event/migrations/0003_alter_event_event_description.py @@ -4,15 +4,27 @@ class Migration(migrations.Migration): - dependencies = [ - ('event', '0002_alter_event_id'), + ("event", "0002_alter_event_id"), ] operations = [ migrations.AlterField( - model_name='event', - name='event_description', - field=models.TextField(blank=True, choices=[('Breakfast', 'Breakfast'), ('Brunch', 'Brunch'), ('Lunch', 'Lunch'), ('Dinner', 'Dinner'), ('General', 'General'), ('Open', 'Cafe'), ('Pants', 'Pants')], default='General', null=True), + model_name="event", + name="event_description", + field=models.TextField( + blank=True, + choices=[ + ("Breakfast", "Breakfast"), + ("Brunch", "Brunch"), + ("Lunch", "Lunch"), + ("Dinner", "Dinner"), + ("General", "General"), + ("Open", "Cafe"), + ("Pants", "Pants"), + ], + default="General", + null=True, + ), ), ] diff --git a/src/event/migrations/0004_alter_event_event_description.py b/src/event/migrations/0004_alter_event_event_description.py index b3f7d98..003ab07 100644 --- a/src/event/migrations/0004_alter_event_event_description.py +++ b/src/event/migrations/0004_alter_event_event_description.py @@ -4,15 +4,27 @@ class Migration(migrations.Migration): - dependencies = [ - ('event', '0003_alter_event_event_description'), + ("event", "0003_alter_event_event_description"), ] operations = [ migrations.AlterField( - model_name='event', - name='event_description', - field=models.TextField(blank=True, choices=[('Breakfast', 'Breakfast'), ('Brunch', 'Brunch'), ('Lunch', 'Lunch'), ('Dinner', 'Dinner'), ('General', 'General'), ('Cafe', 'Cafe'), ('Pants', 'Pants')], default='General', null=True), + model_name="event", + name="event_description", + field=models.TextField( + blank=True, + choices=[ + ("Breakfast", "Breakfast"), + ("Brunch", "Brunch"), + ("Lunch", "Lunch"), + ("Dinner", "Dinner"), + ("General", "General"), + ("Cafe", "Cafe"), + ("Pants", "Pants"), + ], + default="General", + null=True, + ), ), ] diff --git a/src/event/models.py b/src/event/models.py index d71a303..e0a4d86 100644 --- a/src/event/models.py +++ b/src/event/models.py @@ -1,8 +1,9 @@ -from django.db import models +from django.db import models from django.db import connection from eatery.models import Eatery + class EventDescription(models.TextChoices): BREAKFAST = "Breakfast" BRUNCH = "Brunch" @@ -12,17 +13,24 @@ class EventDescription(models.TextChoices): CAFE = "Cafe" PANTS = "Pants" -class Event(models.Model): - eatery = models.ForeignKey(Eatery, related_name = "events", on_delete=models.DO_NOTHING) + +class Event(models.Model): + eatery = models.ForeignKey( + Eatery, related_name="events", on_delete=models.DO_NOTHING + ) event_description = models.TextField( - choices=EventDescription.choices, default = EventDescription.GENERAL, blank=True, null = True) - start = models.IntegerField(default = 0) - end = models.IntegerField(default = 0) - + choices=EventDescription.choices, + default=EventDescription.GENERAL, + blank=True, + null=True, + ) + start = models.IntegerField(default=0) + end = models.IntegerField(default=0) + def __str__(self): return f"{self.eatery.name}: {self.event_description} from {self.start} to {self.end}" @classmethod def truncate(cls): with connection.cursor() as cursor: - cursor.execute('TRUNCATE TABLE {} CASCADE'.format(cls._meta.db_table)) \ No newline at end of file + cursor.execute("TRUNCATE TABLE {} CASCADE".format(cls._meta.db_table)) diff --git a/src/event/serializers.py b/src/event/serializers.py index 435a044..daa7202 100644 --- a/src/event/serializers.py +++ b/src/event/serializers.py @@ -1,11 +1,13 @@ from rest_framework import serializers from event.models import Event from category.serializers import CategorySerializer, CategorySerializerOptimized -from datetime import datetime + class EventSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False, read_only=True) - event_description = serializers.CharField(allow_null=True, allow_blank=True, default=None) + event_description = serializers.CharField( + allow_null=True, allow_blank=True, default=None + ) start = serializers.IntegerField() end = serializers.IntegerField() menu = CategorySerializer(many=True, read_only=True) @@ -18,6 +20,7 @@ class Meta: model = Event fields = ["id", "eatery", "event_description", "start", "end", "menu"] + class EventSerializerOptimized(serializers.ModelSerializer): menu = CategorySerializerOptimized(many=True, read_only=True) @@ -25,6 +28,7 @@ class Meta: model = Event fields = ["id", "event_description", "start", "end", "menu"] + class EventSerializerSimple(serializers.ModelSerializer): class Meta: model = Event diff --git a/src/event/urls.py b/src/event/urls.py index 5c0709c..072081d 100644 --- a/src/event/urls.py +++ b/src/event/urls.py @@ -7,4 +7,4 @@ urlpatterns = [ path("", include(router.urls)), -] \ No newline at end of file +] diff --git a/src/event/views.py b/src/event/views.py index a0bdf91..85b3022 100644 --- a/src/event/views.py +++ b/src/event/views.py @@ -2,6 +2,7 @@ from event.models import Event from event.serializers import EventSerializer + class EventViewSet(viewsets.ModelViewSet): queryset = Event.objects.all() - serializer_class = EventSerializer \ No newline at end of file + serializer_class = EventSerializer diff --git a/src/item/apps.py b/src/item/apps.py index f290658..77040d0 100644 --- a/src/item/apps.py +++ b/src/item/apps.py @@ -2,5 +2,5 @@ class ItemConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'item' + default_auto_field = "django.db.models.BigAutoField" + name = "item" diff --git a/src/item/controllers/populate_item.py b/src/item/controllers/populate_item.py index 132d775..34a26a9 100644 --- a/src/item/controllers/populate_item.py +++ b/src/item/controllers/populate_item.py @@ -1,55 +1,50 @@ -from item.models import Item from item.serializers import ItemSerializer -from eatery.models import Eatery -from eatery.serializers import EaterySerializer -import string import json from util.constants import eatery_is_cafe -class PopulateItemController(): + +class PopulateItemController: def __init__(self): - self = self + self = self def generate_cafe_items(self, menu, json_eatery): - - for json_item in json_eatery["diningItems"]: - category_name = json_item['category'].strip() + for json_item in json_eatery["diningItems"]: + category_name = json_item["category"].strip() try: category_id = menu[category_name] except KeyError: continue - data = { - "category" : category_id, - "name" : json_item["item"] - } + dietary_preferences = json_item.get("dietaryPreferences", []) + allergens = json_item.get("allergens", []) + + data = {"category": category_id, "name": json_item["item"], "dietary_preferences": dietary_preferences, "allergens": allergens} item = ItemSerializer(data=data) if item.is_valid(): item.save() else: print(item.errors) - def generate_dining_hall_items(self, menu, json_event, json_eatery): - json_menus = json_event['menu'] + json_menus = json_event["menu"] for json_menu in json_menus: - - category_name = json_menu['category'].strip() + category_name = json_menu["category"].strip() category_id = menu[category_name] - for json_item in json_menu['items']: - data = { - "category" : category_id, - "name" : json_item["item"] - } + for json_item in json_menu["items"]: + dietary_preferences = json_item.get("dietaryPreferences", []) + allergens = json_item.get("allergens", []) + data = {"category": category_id, "name": json_item["item"], "dietary_preferences": dietary_preferences, "allergens": allergens} item = ItemSerializer(data=data) if item.is_valid(): item.save() - else: - print(item.errors) + else: + print(item.errors) def process(self, categories_dict, json_eateries): - with open("./static_sources/external_eateries.json", "r") as external_eateries_file: + with open( + "./static_sources/external_eateries.json", "r" + ) as external_eateries_file: external_eateries_json = json.load(external_eateries_file) json_eateries.extend(external_eateries_json["eateries"]) @@ -65,14 +60,15 @@ def process(self, categories_dict, json_eateries): is_cafe = eatery_is_cafe(json_eatery) json_dates = json_eatery["operatingHours"] - for json_date in json_dates: + for json_date in json_dates: json_events = json_date["events"] for json_event in json_events: if i < len(iter): menu_id = iter[i] - menu = eatery_menus[menu_id]; i += 1 + menu = eatery_menus[menu_id] + i += 1 - if is_cafe: + if is_cafe: self.generate_cafe_items(menu, json_eatery) - else: + else: self.generate_dining_hall_items(menu, json_event, json_eatery) diff --git a/src/item/migrations/0001_initial.py b/src/item/migrations/0001_initial.py index 621174f..7341f90 100644 --- a/src/item/migrations/0001_initial.py +++ b/src/item/migrations/0001_initial.py @@ -5,21 +5,27 @@ class Migration(migrations.Migration): - initial = True dependencies = [ - ('category', '0001_initial'), + ("category", "0001_initial"), ] operations = [ migrations.CreateModel( - name='Item', + name="Item", fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('name', models.CharField(default='Item', max_length=40)), - ('base_price', models.FloatField(blank=True, default=0.0, null=True)), - ('category', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='items', to='category.category')), + ("id", models.AutoField(primary_key=True, serialize=False)), + ("name", models.CharField(default="Item", max_length=40)), + ("base_price", models.FloatField(blank=True, default=0.0, null=True)), + ( + "category", + models.ForeignKey( + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="items", + to="category.category", + ), + ), ], ), ] diff --git a/src/item/migrations/0002_alter_item_id.py b/src/item/migrations/0002_alter_item_id.py index 7d59a20..5c0ea2e 100644 --- a/src/item/migrations/0002_alter_item_id.py +++ b/src/item/migrations/0002_alter_item_id.py @@ -4,15 +4,16 @@ class Migration(migrations.Migration): - dependencies = [ - ('item', '0001_initial'), + ("item", "0001_initial"), ] operations = [ migrations.AlterField( - model_name='item', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + model_name="item", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), ] diff --git a/src/item/migrations/0003_allergen_dietarypreference_item_allergens_and_more.py b/src/item/migrations/0003_allergen_dietarypreference_item_allergens_and_more.py new file mode 100644 index 0000000..49481aa --- /dev/null +++ b/src/item/migrations/0003_allergen_dietarypreference_item_allergens_and_more.py @@ -0,0 +1,35 @@ +# Generated by Django 4.0 on 2025-04-08 17:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('item', '0002_alter_item_id'), + ] + + operations = [ + migrations.CreateModel( + name='Allergen', + fields=[ + ('name', models.CharField(max_length=30, primary_key=True, serialize=False)), + ], + ), + migrations.CreateModel( + name='DietaryPreference', + fields=[ + ('name', models.CharField(max_length=30, primary_key=True, serialize=False)), + ], + ), + migrations.AddField( + model_name='item', + name='allergens', + field=models.ManyToManyField(blank=True, related_name='items', to='item.Allergen'), + ), + migrations.AddField( + model_name='item', + name='dietary_preferences', + field=models.ManyToManyField(blank=True, related_name='items', to='item.DietaryPreference'), + ), + ] diff --git a/src/item/models.py b/src/item/models.py index 223539d..1ed640a 100644 --- a/src/item/models.py +++ b/src/item/models.py @@ -1,11 +1,31 @@ from django.db import models -from eatery.models import Eatery from category.models import Category +class DietaryPreference(models.Model): + name = models.CharField(max_length=30, primary_key=True) + + def __str__(self): + return self.name + +class Allergen(models.Model): + name = models.CharField(max_length=30, primary_key=True) + + def __str__(self): + return self.name + + class Item(models.Model): - category = models.ForeignKey(Category, related_name = "items", on_delete=models.DO_NOTHING) - name = models.CharField(max_length=40, default = "Item") + category = models.ForeignKey( + Category, related_name="items", on_delete=models.DO_NOTHING + ) + name = models.CharField(max_length=40, default="Item") base_price = models.FloatField(null=True, blank=True, default=0.0) - + dietary_preferences = models.ManyToManyField( + DietaryPreference, blank=True, related_name="items" + ) + allergens = models.ManyToManyField( + Allergen, blank=True, related_name="items" + ) + def __str__(self): - return f"{self.name} ({self.category.name})" + return f"{self.name} ({self.category.id})" \ No newline at end of file diff --git a/src/item/serializers.py b/src/item/serializers.py index 519109e..769ce15 100644 --- a/src/item/serializers.py +++ b/src/item/serializers.py @@ -1,19 +1,38 @@ from rest_framework import serializers -from item.models import Item +from item.models import Item, DietaryPreference, Allergen + class ItemSerializer(serializers.ModelSerializer): - id = serializers.IntegerField(read_only = True) - name = serializers.CharField(default = "Item") + id = serializers.IntegerField(read_only=True) + name = serializers.CharField(default="Item") + dietary_preferences = serializers.ListField( + child=serializers.CharField(), allow_empty=True, default=[] + ) + allergens = serializers.ListField( + child=serializers.CharField(), allow_empty=True, default=[] + ) def create(self, validated_data): - item, _ = Item.objects.get_or_create(**validated_data) + dietary_prefs = validated_data.pop('dietary_preferences', []) + allergens = validated_data.pop('allergens', []) + item, _ = Item.objects.get_or_create(**validated_data) + + for pref_name in dietary_prefs: + pref, _ = DietaryPreference.objects.get_or_create(name=pref_name) + item.dietary_preferences.add(pref) + + for allergen_name in allergens: + allergen, _ = Allergen.objects.get_or_create(name=allergen_name) + item.allergens.add(allergen) + return item class Meta: model = Item - fields = ['id', 'category', 'name'] + fields = ["id", "category", "name", "dietary_preferences", "allergens"] + class ItemSerializerOptimized(serializers.ModelSerializer): class Meta: model = Item - fields = ['id', 'name'] \ No newline at end of file + fields = ["id", "name", "dietary_preferences", "allergens"] diff --git a/src/item/urls.py b/src/item/urls.py index 6c5de78..3827d0a 100644 --- a/src/item/urls.py +++ b/src/item/urls.py @@ -7,4 +7,4 @@ urlpatterns = [ path("", include(router.urls)), -] \ No newline at end of file +] diff --git a/src/item/views.py b/src/item/views.py index 745b028..9ca7171 100644 --- a/src/item/views.py +++ b/src/item/views.py @@ -2,6 +2,7 @@ from item.models import Item from item.serializers import ItemSerializer + class ItemViewSet(viewsets.ModelViewSet): queryset = Item.objects.all() - serializer_class = ItemSerializer \ No newline at end of file + serializer_class = ItemSerializer diff --git a/src/manage.py b/src/manage.py index ad32ebf..f73fb68 100755 --- a/src/manage.py +++ b/src/manage.py @@ -1,13 +1,13 @@ #!/usr/bin/env python """Django's command-line utility for administrative tasks.""" + import os import sys def main(): """Run administrative tasks.""" - os.environ.setdefault("DJANGO_SETTINGS_MODULE", - "eatery_blue_backend.settings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "eatery_blue_backend.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: diff --git a/src/person/admin.py b/src/person/admin.py deleted file mode 100644 index 8b935f3..0000000 --- a/src/person/admin.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.contrib import admin -from person.models import Student, Chef - -admin.site.register(Student) -admin.site.register(Chef) \ No newline at end of file diff --git a/src/person/migrations/0001_initial.py b/src/person/migrations/0001_initial.py deleted file mode 100644 index 0bbad96..0000000 --- a/src/person/migrations/0001_initial.py +++ /dev/null @@ -1,36 +0,0 @@ -# Generated by Django 4.0 on 2023-04-13 00:10 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('item', '0001_initial'), - ('eatery', '0001_initial'), - ('auth', '0012_alter_user_first_name_max_length'), - ] - - operations = [ - migrations.CreateModel( - name='Student', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('net_id', models.TextField()), - ('favorite_eateries', models.ManyToManyField(related_name='student', to='eatery.Eatery')), - ('favorite_items', models.ManyToManyField(related_name='student', to='item.Item')), - ('user', models.OneToOneField(default=None, on_delete=django.db.models.deletion.CASCADE, to='auth.user')), - ], - ), - migrations.CreateModel( - name='Chef', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('eateries_managed', models.ManyToManyField(related_name='chef', to='eatery.Eatery')), - ('user', models.OneToOneField(default=None, on_delete=django.db.models.deletion.CASCADE, to='auth.user')), - ], - ), - ] diff --git a/src/person/models.py b/src/person/models.py deleted file mode 100644 index 82067aa..0000000 --- a/src/person/models.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.db import models -from django.contrib.auth.models import User - - -class Student(models.Model): - net_id = models.TextField() - favorite_eateries = models.ManyToManyField('eatery.Eatery', related_name='student') - favorite_items = models.ManyToManyField('item.Item', related_name='student') - user = models.OneToOneField(User, on_delete=models.CASCADE, unique=True, default=None) - - def __str__(self): - return f"Student {self.net_id}" - -class Chef(models.Model): - eateries_managed = models.ManyToManyField('eatery.Eatery', related_name='chef') - user = models.OneToOneField(User, on_delete=models.CASCADE, unique=True, default=None) - - def __str__(self): - return f"Chef {self.user.username}" \ No newline at end of file diff --git a/src/person/serializers.py b/src/person/serializers.py deleted file mode 100644 index a5e3942..0000000 --- a/src/person/serializers.py +++ /dev/null @@ -1,14 +0,0 @@ -from rest_framework import serializers - -from person.models import Student, Chef -from django.contrib.auth.models import User - -class StudentSerializer(serializers.ModelSerializer): - class Meta: - model = Student - fields = ['id', 'net_id', 'user', 'favorite_eateries', 'favorite_items'] - -class ChefSerializer(serializers.ModelSerializer): - class Meta: - model = Chef - fields = ['id', 'user', 'eateries_managed'] diff --git a/src/person/views.py b/src/person/views.py deleted file mode 100644 index 778d6e7..0000000 --- a/src/person/views.py +++ /dev/null @@ -1,11 +0,0 @@ -from rest_framework import viewsets -from person.models import Student, Chef -from person.serializers import StudentSerializer, ChefSerializer - -class StudentViewSet(viewsets.ModelViewSet): - queryset = Student.objects.all() - serializer_class = StudentSerializer - -class ChefViewSet(viewsets.ModelViewSet): - queryset = Chef.objects.all() - serializer_class = ChefSerializer \ No newline at end of file diff --git a/src/report/apps.py b/src/report/apps.py index a3e9973..f02dd49 100644 --- a/src/report/apps.py +++ b/src/report/apps.py @@ -2,5 +2,5 @@ class ReportConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = "report" \ No newline at end of file + default_auto_field = "django.db.models.BigAutoField" + name = "report" diff --git a/src/report/migrations/0001_initial.py b/src/report/migrations/0001_initial.py index 7fa1323..bc8a89c 100644 --- a/src/report/migrations/0001_initial.py +++ b/src/report/migrations/0001_initial.py @@ -5,21 +5,36 @@ class Migration(migrations.Migration): - initial = True dependencies = [ - ('eatery', '0001_initial'), + ("eatery", "0001_initial"), ] operations = [ migrations.CreateModel( - name='Report', + name="Report", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('content', models.TextField()), - ('created', models.DateTimeField(auto_now_add=True)), - ('eatery', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='eatery.eatery')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("content", models.TextField()), + ("created", models.DateTimeField(auto_now_add=True)), + ( + "eatery", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="eatery.eatery", + ), + ), ], ), ] diff --git a/src/report/migrations/0002_report_netid.py b/src/report/migrations/0002_report_netid.py index 22c3b63..533fc58 100644 --- a/src/report/migrations/0002_report_netid.py +++ b/src/report/migrations/0002_report_netid.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('report', '0001_initial'), + ("report", "0001_initial"), ] operations = [ migrations.AddField( - model_name='report', - name='netid', + model_name="report", + name="netid", field=models.CharField(blank=True, max_length=10, null=True), ), ] diff --git a/src/report/models.py b/src/report/models.py index 6b81978..f331648 100644 --- a/src/report/models.py +++ b/src/report/models.py @@ -7,8 +7,6 @@ class Report(models.Model): netid = models.CharField(max_length=10, null=True, blank=True) content = models.TextField() created = models.DateTimeField(auto_now_add=True) - + def __str__(self): - return f'{self.content} - {self.created}' - - + return f"{self.content} - {self.created}" diff --git a/src/report/serializers.py b/src/report/serializers.py index 1356886..6e7c4a9 100644 --- a/src/report/serializers.py +++ b/src/report/serializers.py @@ -5,4 +5,4 @@ class ReportSerializer(serializers.ModelSerializer): class Meta: model = Report - fields = ['id', 'eatery', 'netid', 'content', 'created'] + fields = ["id", "eatery", "netid", "content", "created"] diff --git a/src/report/urls.py b/src/report/urls.py index e4a4df6..2977993 100644 --- a/src/report/urls.py +++ b/src/report/urls.py @@ -8,4 +8,4 @@ urlpatterns = [ path("", include(router.urls)), -] \ No newline at end of file +] diff --git a/src/static_sources/external_eateries.json b/src/static_sources/external_eateries_static.json similarity index 83% rename from src/static_sources/external_eateries.json rename to src/static_sources/external_eateries_static.json index c927f6a..d2ba168 100644 --- a/src/static_sources/external_eateries.json +++ b/src/static_sources/external_eateries_static.json @@ -139,7 +139,7 @@ "id": 34, "slug": "Macs", "external": true, - "name": "Mac's Café", + "name": "Mac's Caf\u00e9", "nameshort": "Mac's", "about": "", "cornellDining": false, @@ -360,7 +360,9 @@ ] } ], - "datesClosed": ["09/06/21"], + "datesClosed": [ + "09/06/21" + ], "payMethods": [ { "descr": "Cash", @@ -823,6 +825,169 @@ "category": "General" } ] + }, + { + "id": 46, + "slug": "Freedge", + "external": true, + "name": "Free Food Fridge", + "nameshort": "Freedge", + "about": "", + "cornellDining": false, + "contactPhone": "", + "latitude": 42.445061, + "longitude": -76.485826, + "campusArea": { + "descr": "South Campus", + "descrshort": "South" + }, + "location": "Anabel Taylor Hall Room 120", + "eateryTypes": [ + { + "descr": "Cash", + "descrshort": "Cash" + } + ], + "onlineOrdering": false, + "onlineOrderUrl": null, + "operatingHours": [ + { + "weekday": "Monday", + "events": [ + { + "descr": "Free Food", + "start": "08:30", + "end": "23:00", + "menu": [] + } + ] + }, + { + "weekday": "Tuesday", + "events": [ + { + "descr": "Free Food", + "start": "08:30", + "end": "23:00", + "menu": [] + } + ] + }, + { + "weekday": "Wednesday", + "events": [ + { + "descr": "Free Food", + "start": "08:30", + "end": "23:00", + "menu": [] + } + ] + }, + { + "weekday": "Thursday", + "events": [ + { + "descr": "Free Food", + "start": "08:30", + "end": "23:00", + "menu": [] + } + ] + }, + { + "weekday": "Friday", + "events": [ + { + "descr": "Free Food", + "start": "08:30", + "end": "23:00", + "menu": [] + } + ] + }, + { + "weekday": "Saturday", + "events": [ + { + "descr": "Free Food", + "start": "08:30", + "end": "23:00", + "menu": [] + } + ] + }, + { + "weekday": "Sunday", + "events": [ + { + "descr": "Free Food", + "start": "08:30", + "end": "23:00", + "menu": [] + } + ] + } + ], + "datesClosed": [], + "payMethods": [ + { + "descr": "Free", + "descrshort": "Free" + } + ], + "diningItems": [ + { + "item": "Croissant", + "healthy": false, + "category": "General" + }, + { + "item": "Coffee Cake", + "healthy": false, + "category": "General" + }, + { + "item": "Filled Crossiant", + "healthy": false, + "category": "General" + }, + { + "item": "Cookies", + "healthy": false, + "category": "General" + }, + { + "item": "Asorted Scones", + "healthy": false, + "category": "General" + }, + { + "item": "Bufflao Chicken Wraps", + "healthy": false, + "category": "General" + }, + { + "item": "Turkey Sandwich", + "healthy": false, + "category": "General" + }, + { + "item": "Italian Combo", + "healthy": false, + "category": "General" + }, + { + "item": "Prosciutto Mozzarela", + "healthy": false, + "category": "General" + }, + { + "item": "Roast Beef & Asiago Cheese", + "healthy": false, + "category": "General" + } + ] } ] -} +} \ No newline at end of file diff --git a/src/user/__init__.py b/src/user/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/user/admin.py b/src/user/admin.py new file mode 100644 index 0000000..8518016 --- /dev/null +++ b/src/user/admin.py @@ -0,0 +1,4 @@ +from django.contrib import admin +from user.models import User + +admin.site.register(User) diff --git a/src/user/apps.py b/src/user/apps.py new file mode 100644 index 0000000..578292c --- /dev/null +++ b/src/user/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UserConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "user" diff --git a/src/user/migrations/0001_initial.py b/src/user/migrations/0001_initial.py new file mode 100644 index 0000000..2f8ef51 --- /dev/null +++ b/src/user/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# Generated by Django 4.0 on 2024-11-10 18:03 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('eatery', '0005_alter_eatery_campus_area'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('netid', models.CharField(blank=True, max_length=10, null=True)), + ('given_name', models.CharField(blank=True, max_length=30, null=True)), + ('family_name', models.CharField(blank=True, max_length=30, null=True)), + ('google_id', models.CharField(blank=True, max_length=50, null=True, unique=True)), + ('email', models.EmailField(blank=True, max_length=255, null=True)), + ('favorite_items', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), blank=True, default=list, size=None)), + ('favorite_eateries', models.ManyToManyField(blank=True, related_name='favorited_by', to='eatery.Eatery')), + ], + ), + ] diff --git a/src/user/migrations/0002_remove_user_email_remove_user_family_name_and_more.py b/src/user/migrations/0002_remove_user_email_remove_user_family_name_and_more.py new file mode 100644 index 0000000..40f7e97 --- /dev/null +++ b/src/user/migrations/0002_remove_user_email_remove_user_family_name_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 4.0 on 2025-03-04 03:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='email', + ), + migrations.RemoveField( + model_name='user', + name='family_name', + ), + migrations.RemoveField( + model_name='user', + name='given_name', + ), + migrations.RemoveField( + model_name='user', + name='google_id', + ), + migrations.RemoveField( + model_name='user', + name='netid', + ), + migrations.AddField( + model_name='user', + name='device_id', + field=models.CharField(blank=True, max_length=100), + ), + migrations.AddField( + model_name='user', + name='fcm_token', + field=models.CharField(blank=True, max_length=100), + ), + ] diff --git a/src/user/migrations/0003_user_brb_balance_user_city_bucks_balance.py b/src/user/migrations/0003_user_brb_balance_user_city_bucks_balance.py new file mode 100644 index 0000000..4d0afb9 --- /dev/null +++ b/src/user/migrations/0003_user_brb_balance_user_city_bucks_balance.py @@ -0,0 +1,23 @@ +# Generated by Django 4.0 on 2025-03-04 22:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0002_remove_user_email_remove_user_family_name_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='brb_balance', + field=models.FloatField(default=0), + ), + migrations.AddField( + model_name='user', + name='city_bucks_balance', + field=models.FloatField(default=0), + ), + ] diff --git a/src/user/migrations/0004_user_laundry_balance.py b/src/user/migrations/0004_user_laundry_balance.py new file mode 100644 index 0000000..7f06ae3 --- /dev/null +++ b/src/user/migrations/0004_user_laundry_balance.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0 on 2025-03-04 22:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0003_user_brb_balance_user_city_bucks_balance'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='laundry_balance', + field=models.FloatField(default=0), + ), + ] diff --git a/src/user/migrations/0005_user_brb_account_name_user_citybucks_account_name_and_more.py b/src/user/migrations/0005_user_brb_account_name_user_citybucks_account_name_and_more.py new file mode 100644 index 0000000..f2f53dd --- /dev/null +++ b/src/user/migrations/0005_user_brb_account_name_user_citybucks_account_name_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.0 on 2025-03-04 23:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0004_user_laundry_balance'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='brb_account_name', + field=models.CharField(blank=True, max_length=255), + ), + migrations.AddField( + model_name='user', + name='citybucks_account_name', + field=models.CharField(blank=True, max_length=255), + ), + migrations.AddField( + model_name='user', + name='laundry_account_name', + field=models.CharField(blank=True, max_length=255), + ), + ] diff --git a/src/user/migrations/0006_rename_citybucks_account_name_user_city_bucks_account_name.py b/src/user/migrations/0006_rename_citybucks_account_name_user_city_bucks_account_name.py new file mode 100644 index 0000000..47c56bb --- /dev/null +++ b/src/user/migrations/0006_rename_citybucks_account_name_user_city_bucks_account_name.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0 on 2025-03-04 23:02 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0005_user_brb_account_name_user_citybucks_account_name_and_more'), + ] + + operations = [ + migrations.RenameField( + model_name='user', + old_name='citybucks_account_name', + new_name='city_bucks_account_name', + ), + ] diff --git a/src/user/migrations/__init__.py b/src/user/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/user/models.py b/src/user/models.py new file mode 100644 index 0000000..40023cf --- /dev/null +++ b/src/user/models.py @@ -0,0 +1,23 @@ +from django.db import models +from django.contrib.postgres.fields import ArrayField + + +class User(models.Model): + device_id = models.CharField(max_length=100, blank=True) + fcm_token = models.CharField(max_length=100, blank=True) + favorite_items = ArrayField( + models.CharField(max_length=100), blank=True, default=list + ) + favorite_eateries = models.ManyToManyField( + "eatery.Eatery", related_name="favorited_by", blank=True + ) + brb_account_name = models.CharField(max_length=255, blank=True) + city_bucks_account_name = models.CharField(max_length=255, blank=True) + laundry_account_name = models.CharField(max_length=255, blank=True) + + brb_balance = models.FloatField(default=0) + city_bucks_balance = models.FloatField(default=0) + laundry_balance = models.FloatField(default=0) + + def __str__(self): + return f"{self.netid}" diff --git a/src/user/serializers.py b/src/user/serializers.py new file mode 100644 index 0000000..5770d72 --- /dev/null +++ b/src/user/serializers.py @@ -0,0 +1,25 @@ +from rest_framework import serializers +from user.models import User + + +class UserSerializer(serializers.ModelSerializer): + + favorite_items = serializers.ListField( + child=serializers.CharField(max_length=100), required=False + ) + + class Meta: + model = User + fields = [ + "id", + "device_id", + "fcm_token", + "favorite_eateries", + "favorite_items", + "brb_balance", + "city_bucks_balance", + "laundry_balance", + "brb_account_name", + "city_bucks_account_name", + "laundry_account_name", + ] diff --git a/src/user/tests.py b/src/user/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/src/user/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/src/user/urls.py b/src/user/urls.py new file mode 100644 index 0000000..dbf70f2 --- /dev/null +++ b/src/user/urls.py @@ -0,0 +1,10 @@ +from django.urls import path, include +from user.views import UserViewSet +from rest_framework.routers import DefaultRouter + +router = DefaultRouter() +router.register("", UserViewSet) + +urlpatterns = [ + path("", include(router.urls)), +] diff --git a/src/user/views.py b/src/user/views.py new file mode 100644 index 0000000..aebbfd3 --- /dev/null +++ b/src/user/views.py @@ -0,0 +1,332 @@ +from rest_framework import viewsets, status +from rest_framework.response import Response +from rest_framework.decorators import action +from django.shortcuts import get_object_or_404 +import requests as http_requests +import os + +from user.models import User +from user.serializers import UserSerializer +from eatery.models import Eatery + +class UserViewSet(viewsets.ModelViewSet): + queryset = User.objects.all() + serializer_class = UserSerializer + + CBORD_BASE_URL = os.getenv('CBORD_BASE_URL') + + @action(detail=True, methods=["post"], url_path="eatery/add") + def add_favorite_eatery(self, request, pk=None): + user = get_object_or_404(User, pk=pk) + eatery_id = request.data.get("eatery_id") + eatery = get_object_or_404(Eatery, id=eatery_id) + user.favorite_eateries.add(eatery) + user.save() + user_data = UserSerializer(user).data + return Response(user_data, status=status.HTTP_200_OK) + + @action(detail=True, methods=["post"], url_path="eatery/remove") + def remove_favorite_eatery(self, request, pk=None): + user = get_object_or_404(User, pk=pk) + eatery_id = request.data.get("eatery_id") + eatery = get_object_or_404(Eatery, id=eatery_id) + user.favorite_eateries.remove(eatery) + user.save() + user_data = UserSerializer(user).data + return Response(user_data, status=status.HTTP_200_OK) + + @action(detail=True, methods=["post"], url_path="item/add") + def add_favorite_item(self, request, pk=None): + user = get_object_or_404(User, pk=pk) + item_name = request.data.get("item_name") + if item_name and item_name not in user.favorite_items: + user.favorite_items.append(item_name) + user.save() + user_data = UserSerializer(user).data + return Response(user_data, status=status.HTTP_200_OK) + + @action(detail=True, methods=["post"], url_path="item/remove") + def remove_favorite_item(self, request, pk=None): + user = get_object_or_404(User, pk=pk) + item_name = request.data.get("item_name") + if item_name in user.favorite_items: + user.favorite_items.remove(item_name) + user.save() + user_data = UserSerializer(user).data + return Response(user_data, status=status.HTTP_200_OK) + + def check_auth_header(self, request): + """ + Validate auth header and return session_id if valid + header should be in the form "Bearer " + """ + auth_header = request.headers.get("Authorization") + # + if not auth_header: + return None, Response({"error": "Missing authorization header"}, + status=status.HTTP_400_BAD_REQUEST) + if not auth_header.startswith("Bearer "): + return None, Response({"error": "Invalid authorization header - must start with 'Bearer '"}, + status=status.HTTP_400_BAD_REQUEST) + + session_id = auth_header[7:] + return session_id, None + + def handle_cbord_exception(self, result): + """ + Check for exceptions in cbord response + Returns Response object if exception, None otherwise + + note: right now if session_id is invalid, response is "error": "4001|Session not found" + w/ 400 status code + """ + if result.get("exception"): + if "not validated" in result.get("exception"): + return Response({"error": result.get("exception")}, + status=status.HTTP_401_UNAUTHORIZED) + return Response({"error": result.get("exception")}, + status=status.HTTP_400_BAD_REQUEST) + return None + + @action(detail=False, methods=["post"], url_path="authorize") + def authorize(self, request): + session_id, error = self.check_auth_header(request) + if error: + return error + + device_id = request.data.get("deviceId") + pin = request.data.get("pin") + fcm_token = request.data.get("fcmToken") + + if not device_id or not pin: + return Response({"error": "deviceId, pin required"}, + status=status.HTTP_400_BAD_REQUEST) + + # prepare payload for GET API + payload = { + "method": "createPIN", + "params": { + "PIN": pin, + "deviceId": device_id, + "sessionId": session_id + } + } + + # call createPIN from GET API + try: + get_response = http_requests.post( + f"{self.CBORD_BASE_URL}/user", + json=payload, + headers={"Content-Type": "application/json"} + ) + result = get_response.json() + except Exception as e: + return Response({"error": "Error communicating with GET API", "details": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + exception_response = self.handle_cbord_exception(result) + if exception_response: + return exception_response + + favorites = request.data.get("favorite_items") + + user, _ = User.objects.get_or_create( + device_id=device_id, + defaults={} + ) + + # merge favorites if they exist + if favorites and isinstance(favorites, list): + merged_favorites = list(set(user.favorite_items + favorites)) + user.favorite_items = merged_favorites + + if fcm_token: + user.fcm_token = fcm_token + + user.save() + user_data = UserSerializer(user).data + return Response(user_data, status=status.HTTP_200_OK) + + @action(detail=False, methods=["post"], url_path="refresh") + def refresh(self, request): + device_id = request.data.get("deviceId") + pin = request.data.get("pin") + if not device_id or not pin: + return Response({"error": "deviceId and pin are required"}, + status=status.HTTP_400_BAD_REQUEST) + + payload = { + "method": "authenticatePIN", + "params": { + "systemCredentials": { + "domain": "", + "userName": "get_mobile", + "password": "NOTUSED" + }, + "deviceId": device_id, + "pin": pin + } + } + + # refresh sessionId with GET API's authenticatePIN method + try: + get_response = http_requests.post( + f"{self.CBORD_BASE_URL}/authentication", + json=payload, + headers={"Content-Type": "application/json"} + ) + result = get_response.json() + except Exception as e: + return Response({"error": "Error communicating with GET API", "details": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # if exception, return 401 and user should be fully logged out + if result.get("exception"): + return Response({"error": result.get("exception")}, + status=status.HTTP_401_UNAUTHORIZED) + + new_session_id = result.get("response") + if not new_session_id: + return Response({"error": "Failed to retrieve new sessionId"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + return Response({"sessionId": new_session_id}, status=status.HTTP_200_OK) + + @action(detail=False, methods=["post"], url_path="transactions") + def transactions(self, request): + session_id, error = self.check_auth_header(request) + if error: + return error + + # note: gets 100 most recent transcations including meal swipes, so we dont + # have an exact number of BRB transactions. can change to date range if it + # becomes a problem + payload = { + "method": "retrieveTransactionHistoryWithinDateRange", + "params": { + "paymentSystemType": 0, + "queryCriteria": { + "maxReturnMostRecent": 100, + "newestDate": None, + "oldestDate": None, + "accountId": None + }, + "sessionId": session_id + } + } + + try: + get_response = http_requests.post( + f"{self.CBORD_BASE_URL}/commerce", + json=payload, + headers={"Content-Type": "application/json"} + ) + result = get_response.json() + except Exception as e: + return Response({"error": "Error communicating with GET API", "details": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + exception_response = self.handle_cbord_exception(result) + if exception_response: + return exception_response + + # brb transactions (tenderId == "000000449") + transactions = result.get("response", {}).get("transactions", []) + brb_transactions = [] + for txn in transactions: + account_name = txn.get("accountName", "") + brb_transactions.append({ + "amount": txn.get("amount"), + "tenderId": txn.get("tenderId"), + "accountName": account_name, + "date": txn.get("postedDate"), + "location": txn.get("locationName") + }) + + return Response({"transactions": brb_transactions}, status=status.HTTP_200_OK) + + @action(detail=False, methods=["post"], url_path="accounts") + def accounts(self, request): + session_id, error = self.check_auth_header(request) + if error: + return error + + device_id = request.data.get("deviceId") + if not device_id: + return Response({"error": "deviceId is required"}, + status=status.HTTP_400_BAD_REQUEST) + + payload = { + "method": "retrieveAccounts", + "params": { + "sessionId": session_id + } + } + + try: + get_response = http_requests.post( + f"{self.CBORD_BASE_URL}/commerce", + json=payload, + headers={"Content-Type": "application/json"} + ) + result = get_response.json() + except Exception as e: + return Response({"error": "Error communicating with GET API", "details": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + if result.get("exception"): + if "not validated" in result.get("exception"): + return Response({"error": result.get("exception")}, + status=status.HTTP_401_UNAUTHORIZED) + return Response({"error": result.get("exception")}, + status=status.HTTP_400_BAD_REQUEST) + + + accounts = result.get("response", {}).get("accounts", []) + brb_account = None + city_bucks_account = None + laundry_account = None + + for account in accounts: + display_name = account.get("accountDisplayName", "") + if "Big Red Bucks" in display_name and brb_account is None: + brb_account = account + # two city bucks accounts, one for GET which is not the main one + if "City Bucks" in display_name and "GET" not in display_name and city_bucks_account is None: + city_bucks_account = account + if "Laundry" in display_name and laundry_account is None: + laundry_account = account + if brb_account and city_bucks_account and laundry_account: + break + + try: + user = User.objects.get(device_id=device_id) + except User.DoesNotExist: + return Response({"error": "User not found for the given deviceId"}, status=status.HTTP_404_NOT_FOUND) + + # update brb balance and account name + if brb_account: + user.brb_balance = brb_account.get("balance", 0) + user.brb_account_name = brb_account.get("accountDisplayName", "") + else: + user.brb_account_name = None + + if city_bucks_account: + user.city_bucks_balance = city_bucks_account.get("balance", 0) + user.city_bucks_account_name = city_bucks_account.get("accountDisplayName", "") + else: + user.city_bucks_account_name = None + + if laundry_account: + user.laundry_balance = laundry_account.get("balance", 0) + user.laundry_account_name = laundry_account.get("accountDisplayName", "") + else: + user.laundry_account_name = None + + user.save() + + return Response({ + "brb": {"name": user.brb_account_name, "balance": user.brb_balance}, + "city_bucks": {"name": user.city_bucks_account_name, "balance": user.city_bucks_balance}, + "laundry": {"name": user.laundry_account_name, "balance": user.laundry_balance} + }, status=status.HTTP_200_OK) \ No newline at end of file diff --git a/src/util/constants.py b/src/util/constants.py index c658cf0..80c7cb9 100644 --- a/src/util/constants.py +++ b/src/util/constants.py @@ -1,2 +1,4 @@ def eatery_is_cafe(json_eatery): - return not "Dining Room" in [eatery_type["descr"] for eatery_type in json_eatery["eateryTypes"]] \ No newline at end of file + return "Dining Room" not in [ + eatery_type["descr"] for eatery_type in json_eatery["eateryTypes"] + ]