From 7e9f4319b9c01fa70d2b9e0c9e26a88fd17a437c Mon Sep 17 00:00:00 2001 From: Colin Toft <47548563+ColinToft@users.noreply.github.com> Date: Sat, 22 Jul 2023 08:33:40 -0400 Subject: [PATCH 1/3] Update food_request_group and food_request model (#82) * updated schema for food request models and corresponding GraphQL mutation --------- Co-authored-by: HeetShah Co-authored-by: Colin Toft Co-authored-by: Shahan Neda --- backend/app/graphql/food_request.py | 51 ++++- backend/app/models/food_request.py | 13 +- backend/app/models/food_request_group.py | 21 +- .../implementations/food_request_service.py | 44 ++-- backend/tests/graphql/test_food_request.py | 197 +++++++++++------- backend/tests/unit/test_food_request_model.py | 38 ---- 6 files changed, 207 insertions(+), 157 deletions(-) delete mode 100644 backend/tests/unit/test_food_request_model.py diff --git a/backend/app/graphql/food_request.py b/backend/app/graphql/food_request.py index a8a0c1a2..63c7d3b3 100644 --- a/backend/app/graphql/food_request.py +++ b/backend/app/graphql/food_request.py @@ -1,6 +1,7 @@ import graphene from .types import ( + ContactInput, Mutation, MutationList, ) @@ -14,21 +15,16 @@ class MealRequestTypeInput(graphene.InputObjectType): portions = graphene.Int(required=True) -class CreateFoodRequestDatesInput(graphene.InputObjectType): - date = graphene.DateTime(required=True) - meal_types = graphene.List(MealRequestTypeInput, required=True) - - # Response Types class MealRequestTypeResponse(graphene.ObjectType): - tags = graphene.List(graphene.String, required=True) portions = graphene.Int(required=True) + dietary_restrictions = graphene.String(required=True) + meal_suggestions = graphene.String(required=True) class CreateFoodRequestResponse(graphene.ObjectType): id = graphene.ID() - target_fulfillment_date = graphene.DateTime() - meal_types = graphene.List(MealRequestTypeResponse) + donation_date = graphene.Date() status = graphene.String() @@ -36,23 +32,56 @@ class CreateFoodRequestGroupResponse(graphene.ObjectType): id = graphene.ID() description = graphene.String() requests = graphene.List(CreateFoodRequestResponse) + meal_info = graphene.Field(MealRequestTypeResponse) status = graphene.String() +class MealTypeInput(graphene.InputObjectType): + portions = graphene.Int(required=True) + dietary_restrictions = graphene.String(required=True) + meal_suggestions = graphene.String(required=True) + + # Mutations class CreateFoodRequestGroup(Mutation): class Arguments: description = graphene.String(required=True) requestor = graphene.ID(required=True) - commitments = graphene.List(CreateFoodRequestDatesInput, required=True) + # request_dates is a list of dates + request_dates = graphene.List(graphene.Date, required=True) + + meal_info = MealTypeInput(required=True) + drop_off_time = graphene.Time(required=True) + drop_off_location = graphene.String(required=True) + delivery_instructions = graphene.String() + onsite_staff = graphene.List(ContactInput, required=True) # return values food_request_group = graphene.Field(CreateFoodRequestGroupResponse) - def mutate(self, info, description, requestor, commitments): + def mutate( + self, + info, + description, + requestor, + request_dates, + meal_info, + drop_off_time, + drop_off_location, + delivery_instructions, + onsite_staff, + ): result = services["food_request_service"].create_food_request_group( - description=description, requestor=requestor, commitments=commitments + description=description, + requestor=requestor, + request_dates=request_dates, + meal_info=meal_info, + drop_off_time=drop_off_time, + drop_off_location=drop_off_location, + delivery_instructions=delivery_instructions, + onsite_staff=onsite_staff, ) + return CreateFoodRequestGroup(food_request_group=result) diff --git a/backend/app/models/food_request.py b/backend/app/models/food_request.py index c92c9068..5a2718bb 100644 --- a/backend/app/models/food_request.py +++ b/backend/app/models/food_request.py @@ -3,18 +3,15 @@ class MealType(mg.EmbeddedDocument): - tags = mg.ListField(mg.StringField(required=True)) portions = mg.IntField(required=True) - portions_fulfilled = mg.IntField(required=True, default=0) + dietary_restrictions = mg.StringField(required=True, default="No restrictions") + meal_suggestions = mg.StringField(required=True) class FoodRequest(mg.EmbeddedDocument): _id = mg.ObjectIdField(required=True, default=ObjectId) - donor = mg.ObjectIdField() - target_fulfillment_date = mg.DateTimeField(required=True) - actual_fulfillment_date = mg.DateTimeField() - meal_types = mg.EmbeddedDocumentListField(MealType, default=list) - + # The date that the food is being + donation_date = mg.DateField(required=True) """ Open: Request has not been completely fulfilled Fulfilled: All meal types have been fulfilled @@ -23,6 +20,8 @@ class FoodRequest(mg.EmbeddedDocument): status = mg.StringField( choices=["Open", "Fulfilled", "Cancelled"], required=True, default="Open" ) + donor_id = mg.ObjectIdField(required=False) + commitment_date = mg.DateTimeField(required=False) def to_serializable_dict(self): """ diff --git a/backend/app/models/food_request_group.py b/backend/app/models/food_request_group.py index dc718810..a0f3f476 100644 --- a/backend/app/models/food_request_group.py +++ b/backend/app/models/food_request_group.py @@ -1,18 +1,18 @@ import mongoengine as mg -from .food_request import FoodRequest +from .food_request import FoodRequest, MealType from datetime import datetime +from .user_info import Contact +from bson.objectid import ObjectId class FoodRequestGroup(mg.Document): + _id = mg.ObjectIdField(required=True, default=ObjectId) description = mg.StringField(required=True) + requestor = mg.ObjectIdField() # The ASP making the request # TODO: make this required=True when we have users populated - requestor = mg.ObjectIdField() requests = mg.EmbeddedDocumentListField(FoodRequest, default=list) - date_created = mg.DateTimeField(required=True, default=datetime.utcnow) - date_updated = mg.DateTimeField(required=True, default=datetime.utcnow) - """ Open: At least one FoodRequest is open Fulfilled: All FoodRequests are fulfilled @@ -22,6 +22,17 @@ class FoodRequestGroup(mg.Document): choices=["Open", "Fulfilled", "Cancelled"], required=True, default="Open" ) + # Donation Details + meal_info = mg.EmbeddedDocumentField(MealType, required=True) + drop_off_time = mg.DateTimeField(required=True) + drop_off_location = mg.StringField(required=True) + delivery_instructions = mg.StringField(required=True) + onsite_staff = mg.EmbeddedDocumentListField(Contact, required=True) + + # Timestamps + date_created = mg.DateTimeField(required=True, default=datetime.utcnow) + date_updated = mg.DateTimeField(required=True, default=datetime.utcnow) + def to_serializable_dict(self): """ Returns a dict representation of the document that is JSON serializable diff --git a/backend/app/services/implementations/food_request_service.py b/backend/app/services/implementations/food_request_service.py index 01eefee7..e8f298b7 100644 --- a/backend/app/services/implementations/food_request_service.py +++ b/backend/app/services/implementations/food_request_service.py @@ -1,36 +1,42 @@ from ...models.food_request_group import FoodRequestGroup -from ...models.food_request import FoodRequest, MealType +from ...models.food_request import FoodRequest from ..interfaces.food_request_service import IFoodRequestService +from datetime import datetime class FoodRequestService(IFoodRequestService): def __init__(self, logger): self.logger = logger - def create_food_request_group(self, description, requestor, commitments): + def create_food_request_group( + self, + description, + requestor, + request_dates, + meal_info, + drop_off_time, + drop_off_location, + delivery_instructions, + onsite_staff, + ): try: # Create FoodRequestGroup new_food_request_group = FoodRequestGroup( description=description, - # TODO: uncomment when we have users populated - # requestor=requestor, - status="Open", + requestor=requestor, + requests=[ + FoodRequest(donation_date=request_date) + for request_date in request_dates + ], + meal_info=meal_info, + # Convert the time into a datetime object (date does not matter here) + drop_off_time=datetime.combine(datetime.today().date(), drop_off_time), + drop_off_location=drop_off_location, + delivery_instructions=delivery_instructions, + onsite_staff=onsite_staff, ) - - # Create FoodRequests - for commitment in commitments: - meal_types = [ - MealType(tags=meal_type.tags, portions=meal_type.portions) - for meal_type in commitment.meal_types - ] - new_food_request = FoodRequest( - target_fulfillment_date=commitment.date, - status="Open", - meal_types=meal_types, - ) - new_food_request_group.requests.append(new_food_request) - new_food_request_group.save() + print(new_food_request_group.to_serializable_dict()) except Exception as error: self.logger.error(str(error)) raise error diff --git a/backend/tests/graphql/test_food_request.py b/backend/tests/graphql/test_food_request.py index 4b640520..315a68e5 100644 --- a/backend/tests/graphql/test_food_request.py +++ b/backend/tests/graphql/test_food_request.py @@ -6,91 +6,134 @@ def test_create_food_request_group(graphql_schema): mutation = """ - mutation TestCreateFoodRequestGroup { - createFoodRequestGroup( - description: "sample food request", - requestor: "0", - commitments: [ - { - date: "2022-10-17T16:00:00", - mealTypes: [ - { - tags: ["Beef", "Chicken"], - portions: 50 - } - { - tags: ["Vegetarian"], - portions: 20 - } - ] - }, - { - date: "2022-10-17T16:00:00", - mealTypes: [ - { - tags: ["Halal"], - portions: 100 - } - { - tags: ["Vegetarian"], - portions: 30 - } - ] - } - ] - ) { + mutation testCreateFoodRequestGroup { + createFoodRequestGroup( + deliveryInstructions: "Leave at front door", + description: "Food request group for office employees", + dropOffLocation: "123 Main Street", + dropOffTime: "12:00:00Z", + mealInfo: {portions: 40, + dietaryRestrictions: "7 gluten free, 7 no beef", + mealSuggestions: "Burritos"}, + onsiteStaff: [ + {name: "John Doe", email: "john.doe@example.com", phone: "+1234567890"}, + {name: "Jane Smith", email: "jane.smith@example.com", phone: "+9876543210"}], + requestor: "507f1f77bcf86cd799439011", + requestDates: [ + "2023-06-01", + "2023-06-02", + ], + ) + { foodRequestGroup { - id - description - requests { + status + description + id + mealInfo { + portions + dietaryRestrictions + mealSuggestions + } + requests { id - targetFulfillmentDate + donationDate status - mealTypes { - tags - portions - } - } + } } - } + } } """ - expected_requests = [ - { - "targetFulfillmentDate": "2022-10-17T16:00:00", - "status": "Open", - "mealTypes": [ - {"tags": ["Beef", "Chicken"], "portions": 50}, - {"tags": ["Vegetarian"], "portions": 20}, - ], - }, - { - "targetFulfillmentDate": "2022-10-17T16:00:00", - "status": "Open", - "mealTypes": [ - {"tags": ["Halal"], "portions": 100}, - {"tags": ["Vegetarian"], "portions": 30}, - ], - }, - ] result = graphql_schema.execute(mutation) - assert result.errors is None - - food_request_group = result.data["createFoodRequestGroup"]["foodRequestGroup"] - # assert food_request_group values - assert food_request_group["id"] - assert food_request_group["description"] == "sample food request" + assert result.errors is None + assert result.data["createFoodRequestGroup"]["foodRequestGroup"]["status"] == "Open" + assert ( + result.data["createFoodRequestGroup"]["foodRequestGroup"]["description"] + == "Food request group for office employees" + ) + assert ( + result.data["createFoodRequestGroup"]["foodRequestGroup"]["mealInfo"][ + "portions" + ] + == 40 + ) + assert ( + result.data["createFoodRequestGroup"]["foodRequestGroup"]["mealInfo"][ + "dietaryRestrictions" + ] + == "7 gluten free, 7 no beef" + ) + assert ( + result.data["createFoodRequestGroup"]["foodRequestGroup"]["mealInfo"][ + "mealSuggestions" + ] + == "Burritos" + ) + assert ( + result.data["createFoodRequestGroup"]["foodRequestGroup"]["requests"][0][ + "donationDate" + ] + == "2023-06-01" + ) + assert ( + result.data["createFoodRequestGroup"]["foodRequestGroup"]["requests"][0][ + "status" + ] + == "Open" + ) + assert ( + result.data["createFoodRequestGroup"]["foodRequestGroup"]["requests"][1][ + "donationDate" + ] + == "2023-06-02" + ) + assert ( + result.data["createFoodRequestGroup"]["foodRequestGroup"]["requests"][1][ + "status" + ] + == "Open" + ) - # assert food_request values - food_requests = food_request_group["requests"] - assert len(food_requests) == len(expected_requests) - for i in range(len(food_requests)): - assert food_requests[i]["id"] - assert ( - food_requests[i]["targetFulfillmentDate"] - == expected_requests[i]["targetFulfillmentDate"] - ) - assert food_requests[i]["mealTypes"] == expected_requests[i]["mealTypes"] +def test_get_food_request_group_failure(graphql_schema): + mutation = """ + mutation testCreateFoodRequestGroup { + createFoodRequestGroup( + deliveryInstructions: "Leave at front door", + description: "Food request group for office employees", + dropOffLocation: "123 Main Street", + dropOffTime: "12:00:00Z", + mealInfo: {portions: 40, + dietaryRestrictions: "7 gluten free, 7 no beef", + mealSuggestions: "Burritos"}, + onsiteStaff: [ + {name: "John Doe", email: "john.doe@example.com", phone: "+1234567890"}, + {name: "Jane Smith", email: "jane.smith@example.com", phone: "+9876543210"}], + requestor: "507f1f77bcf86cd799439011", + requestDates: [ + "2023-06-01", + "2023-06-02", + ], + ) + { + foodRequestGroup { + status + description + id + mealInfo { + portions + dietaryRestrictions + mealSuggestions + } + requests { + id + donationDate + status + } + } + } + } + """ + result = graphql_schema.execute(mutation) + result.errors is not None diff --git a/backend/tests/unit/test_food_request_model.py b/backend/tests/unit/test_food_request_model.py deleted file mode 100644 index 01e724e7..00000000 --- a/backend/tests/unit/test_food_request_model.py +++ /dev/null @@ -1,38 +0,0 @@ -from app.models.food_request import MealType, FoodRequest -from app.models.food_request_group import FoodRequestGroup - -""" -Unit tests for FoodRequestGroup and FoodRequest mongo models -""" - - -def test_create_food_request_group(): - MealTypeExample = {"tags": ["Beef", "Chicken"], "portions": 50} - - test_date = "2022-10-17T16:00:00" - - FoodRequestExample = { - "target_fulfillment_date": test_date, - "meal_types": MealType(**MealTypeExample), - } - - FoodRequestGroupExample = { - "description": "sample_food_request", - "requests": [FoodRequest(**FoodRequestExample)], - } - - food_request_group = FoodRequestGroup(**FoodRequestGroupExample) - - assert food_request_group.description == "sample_food_request" - assert food_request_group.status == "Open" - - food_requests = food_request_group.requests - - assert food_requests[0].status == "Open" - assert food_requests[0].target_fulfillment_date == test_date - - meal_types = food_requests[0].meal_types - - assert meal_types.portions == 50 - assert meal_types.portions_fulfilled == 0 - assert meal_types.tags == ["Beef", "Chicken"] From 06505c31539cea39f8b19ab15eeb221e3568c560 Mon Sep 17 00:00:00 2001 From: Anson He <60114875+ansonjwhe@users.noreply.github.com> Date: Tue, 25 Jul 2023 18:42:39 -0400 Subject: [PATCH 2/3] User Settings Functionality (#85) * hide user settings behind auth wall * Implement User Info Additions - Frontend and Backend (#84) --- frontend/src/APIClients/AuthAPIClient.ts | 8 +- frontend/src/Routes.tsx | 6 +- frontend/src/components/auth/Join.tsx | 56 ++-- frontend/src/components/auth/Login.tsx | 4 +- frontend/src/components/pages/Settings.tsx | 309 ++++++++++++++++----- frontend/src/types/UserTypes.ts | 19 +- 6 files changed, 294 insertions(+), 108 deletions(-) diff --git a/frontend/src/APIClients/AuthAPIClient.ts b/frontend/src/APIClients/AuthAPIClient.ts index 0e04532b..be3edb55 100644 --- a/frontend/src/APIClients/AuthAPIClient.ts +++ b/frontend/src/APIClients/AuthAPIClient.ts @@ -6,16 +6,16 @@ import { import { googleLogout } from "@react-oauth/google"; import AUTHENTICATED_USER_KEY from "../constants/AuthConstants"; -import { AuthenticatedUser } from "../types/UserTypes"; +import { AuthenticatedUser, LoginData } from "../types/UserTypes"; import { setLocalStorageObjProperty } from "../utils/LocalStorageUtils"; type LoginFunction = ( options?: - | MutationFunctionOptions<{ login: AuthenticatedUser }, OperationVariables> + | MutationFunctionOptions<{ login: LoginData }, OperationVariables> | undefined, ) => Promise< FetchResult< - { login: AuthenticatedUser }, + { login: LoginData }, Record, Record > @@ -31,7 +31,7 @@ const login = async ( const result = await loginFunction({ variables: { email, password, idToken }, }); - user = result.data?.login ?? null; + user = result.data?.login?.registeredUser ?? null; if (user) { localStorage.setItem(AUTHENTICATED_USER_KEY, JSON.stringify(user)); } diff --git a/frontend/src/Routes.tsx b/frontend/src/Routes.tsx index 2a0adfc1..66ef5682 100644 --- a/frontend/src/Routes.tsx +++ b/frontend/src/Routes.tsx @@ -34,9 +34,8 @@ const Routes = (): React.ReactElement => ( } /> } /> } /> - } /> - }> - } /> + }> + } /> } /> } /> } /> @@ -54,6 +53,7 @@ const Routes = (): React.ReactElement => ( /> } /> } /> + } /> } /> diff --git a/frontend/src/components/auth/Join.tsx b/frontend/src/components/auth/Join.tsx index 71f05f3d..3c97ed6f 100644 --- a/frontend/src/components/auth/Join.tsx +++ b/frontend/src/components/auth/Join.tsx @@ -31,7 +31,11 @@ import { Role, UserInfo, } from "../../types/UserTypes"; -import { isValidEmail, trimWhiteSpace } from "../../utils/ValidationUtils"; +import { + isNonNegativeInt, + isValidEmail, + trimWhiteSpace, +} from "../../utils/ValidationUtils"; import useIsWebView from "../../utils/useIsWebView"; import OnsiteStaffSection from "../common/OnsiteStaffSection"; @@ -237,12 +241,13 @@ const Join = (): React.ReactElement => { - Number of Kids + Number of kids setNumKids(e.target.value)} @@ -269,22 +274,20 @@ const Join = (): React.ReactElement => { - - - - - Description of organization - -