Skip to content

Commit

Permalink
Feature/meal info (#209)
Browse files Browse the repository at this point in the history
* init commit of meal endpoints
* meal reminder endpoint
* email template typos
* add to serverless
* function outline
* dump meal group data
* meal summary lambda
* remove debug statement
* change get to post
* add attendees table name
* Adding a trigger Meal reminder button
* log meal reminders
* Remove debug
* add env variable
* import time
* Missed empty var

---------

Co-authored-by: Connor Monaghan <[email protected]>
  • Loading branch information
bardsley and connorkm2 authored Oct 29, 2024
1 parent db67810 commit 4a393bb
Show file tree
Hide file tree
Showing 12 changed files with 365 additions and 6 deletions.
27 changes: 27 additions & 0 deletions app/admin/meal/meal-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client'
import React, { useState } from "react";

export default function DiningPageClient() {
const [status,setStatus] = useState({} as any)
const handleSubmit = async (e) => {
e.preventDefault();

const response = await fetch('/api/admin/meal/trigger', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
})
console.log(response)
setStatus(response.ok ? {success: true, message: "Emails Triggered"} : { success: false, message: "Error" })
}

return (
<div>
{JSON.stringify(status) == "{}" ? null : <p className={`w-full p-3 rounded-lg ${status.success ? "bg-green-800": "bg-red-600"}`}>{status.message}</p> }
<button onClick={handleSubmit} className="py-3 px-6 mt-3 float-right bg-chillired-500 rounded-lg block">Trigger Meal Reminder</button>
</div>

)
}
29 changes: 29 additions & 0 deletions app/admin/meal/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from "react";
import Layout from "@components/layout/layout";
import { Container } from "@components/layout/container";
import Navigation from "@components/admin/navigation";
import DiningPageClient from "./meal-client"
// import { currentUser } from "@clerk/nextjs/server";

const pages = [
{ name: 'Admin', href: '/admin', current: true },
{ name: 'Dining', href: '/admin/dining', current: true },
]

export default async function AdminUserPage() {

// const loggedInUser = await currentUser();

return (<Layout>
<section className={`flex-1 relative transition duration-150 ease-out body-font overflow-hidden bg-none text-white`}>
{" "}
<Container width="large" padding="tight" className={`flex-1 pb-2`} size="top">
<Navigation pages={pages} />
</Container>
<Container width="large" padding="tight" className={`flex-1 pb-2`} size="none">
<DiningPageClient />
</Container>
</section>
</Layout>
);
}
29 changes: 29 additions & 0 deletions app/api/admin/meal/trigger/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { createClerkClient } from '@clerk/backend';
import { auth } from '@clerk/nextjs/server';
import { NextRequest } from 'next/server';

export async function POST(_req: NextRequest) {
const clerkClient = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY });
const {userId} = auth();

if(!userId){
return Response.json({error: "User is not signed in."}, { status: 401 });
}

const requestingUser = await clerkClient.users.getUser(userId);
if(!requestingUser.publicMetadata.admin){
return Response.json({error: "User is does not have permissions."}, { status: 401 });
}

const triggerUrl = process.env.LAMBDA_MEAL_TRIGGER_REMINDER
console.log(triggerUrl)
const triggerResponse = await fetch(triggerUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})

return triggerResponse.ok ? Response.json({message: "Email triggered"}, {status: 200}) : Response.json({message: "Email Failed"}, {status: 500})

}
4 changes: 2 additions & 2 deletions components/admin/hub.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ const actions = [
state: 'live',
},
{
title: 'Dining Information',
href: '#',
title: 'Meal Information',
href: '/admin/meal',
description: "Meal information and seating plans",
icon: CakeIcon,
iconForeground: 'text-green-800',
Expand Down
36 changes: 36 additions & 0 deletions functions/meal_groups/lambda_function.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import json
import os
import logging

import boto3
from boto3.dynamodb.conditions import Key, Attr
from shared import DecimalEncoder

logger = logging.getLogger()
logger.setLevel(logging.INFO)

## ENV
event_table_name = os.environ.get("EVENT_TABLE_NAME")
logger.info(event_table_name)

dynamodb_client = boto3.client('dynamodb')
db = boto3.resource('dynamodb')
table = db.Table(event_table_name)

def err(msg:str, code=400, logmsg=None, **kwargs):
logmsg = logmsg if logmsg else msg
logger.error(logmsg)
for k in kwargs:
logger.error(k+":"+kwargs[k])
return {
'statusCode': code,
'body': json.dumps({'error': msg})
}

def lambda_handler(event, context):
scan_response = table.scan(FilterExpression=Key('PK').begins_with('GROUP#'))

return {
'statusCode': 200,
'body': json.dumps(scan_response, cls=DecimalEncoder.DecimalEncoder)
}
72 changes: 72 additions & 0 deletions functions/meal_reminder/lambda_function.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import json
import logging

import boto3
from boto3.dynamodb.conditions import Key, Attr
from shared import DecimalEncoder
import os
import time

## ENV
attendees_table_name = os.environ.get("ATTENDEES_TABLE_NAME")
send_email_lambda = os.environ.get("SEND_EMAIL_LAMBDA")
event_table_name = os.environ.get("EVENT_TABLE_NAME")

logger = logging.getLogger()
logger.setLevel("INFO")

# profile_name='danceengine-admin'
# boto3.setup_default_session(profile_name=profile_name)
# logging.basicConfig()

db = boto3.resource('dynamodb')
table = db.Table(attendees_table_name)
event_table = db.Table(event_table_name)

lambda_client = boto3.client('lambda')

def err(msg:str, code=400, logmsg=None, **kwargs):
logmsg = logmsg if logmsg else msg
logger.error(logmsg)
for k in kwargs:
logger.error(k+":"+kwargs[k])
return {
'statusCode': code,
'body': json.dumps({'error': msg})
}

def lambda_handler(event, context):
logger.info(f"Event {event}")
if event['requestContext']['http']['method'] != "POST": return err("Method not allowed, make POST request", code=405)

response = table.scan(FilterExpression=Key('active').eq(True))

filtered_items = [item for item in response['Items'] if (item['access'][2] == 1) and ('meal_preferences' not in item or item['meal_preferences'] == None or any(choice == -1 for choice in item['meal_preferences']['choices']))]
# logger.info(filtered_items)

#! If no matches should problably return a 404
for item in filtered_items:
# send the email with these details
logger.info("Invoking send_email lambda")
response = lambda_client.invoke(
FunctionName=send_email_lambda,
InvocationType='Event',
Payload=json.dumps({
'email_type':"meal_reminder",
'email':item['email'],
'ticket_number':item['ticket_number'],
}, cls=DecimalEncoder.DecimalEncoder),
)
logger.info(response)

event_table.put_item(
Item={
'PK': "EMAIL#MEALREMINDER".format(),
'SK': "DETAIL#{}".format(time.time()),
'timestamp': "{}".format(time.time())
}
)

return {'statusCode': 200 }

# lambda_handler({'requestContext':{'http':{'method':"GET"}}}, None)
Empty file.
110 changes: 110 additions & 0 deletions functions/meal_summary/lambda_function.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import json
import logging

import boto3
from boto3.dynamodb.conditions import Key, Attr
from shared import DecimalEncoder
import os

## ENV
attendees_table_name = os.environ.get("ATTENDEES_TABLE_NAME")
event_table_name = os.environ.get("EVENT_TABLE_NAME")

logger = logging.getLogger()
logger.setLevel("INFO")

# profile_name='danceengine-admin'
# boto3.setup_default_session(profile_name=profile_name)
# logging.basicConfig()

db = boto3.resource('dynamodb')
table = db.Table(attendees_table_name)
event_table = db.Table(event_table_name)

lambda_client = boto3.client('lambda')

def err(msg:str, code=400, logmsg=None, **kwargs):
logmsg = logmsg if logmsg else msg
logger.error(logmsg)
for k in kwargs:
logger.error(k+":"+kwargs[k])
return {
'statusCode': code,
'body': json.dumps({'error': msg})
}

def get_last_email():
response = table.scan(FilterExpression=Key('PK').eq("EMAIL#MEALREMINDER"))
sorted_list = sorted(response['Items'], key=lambda item: item['timestamp']) if len(response['Items'] > 0) else None

return sorted_list

def lambda_handler(event, context):
logger.info(f"Event {event}")
if event['requestContext']['http']['method'] != "GET": return err("Method not allowed, make GET request", code=405)

response = table.scan(FilterExpression=Key('active').eq(True))

course_mappings = [
{0: "Vegetable Terrine", 1: "Chicken Liver Pate"},
{0: "Roasted Onion", 1: "Fish and Prawn Risotto", 2: "Chicken Supreme"},
{0: "Fruit Platter", 1: "Bread and Butter Pudding"}
]
num_courses = 3
course_frequencies = [{} for _ in range(num_courses)]

not_selected_count = 0 # All choices are -1 (not selected)
incomplete_count = 0 # Some choices are -1 (incomplete selection)
not_wanted_count = 0 # Any choice is -99 (not wanted)
selected_count = 0 # All choices are >= 0 (options selected)

filtered_items = [item for item in response['Items'] if (item['access'][2] == 1)]

#! If no matches should problably return a 404
for item in filtered_items:
if ('meal_preferences' not in item):
not_selected_count += 1
continue
elif (item['meal_preferences'] is None):
not_selected_count += 1
continue

meal_prefs = item['meal_preferences']
if meal_prefs and 'choices' in meal_prefs:
choices = meal_prefs['choices']

if all(choice == -1 for choice in choices):
not_selected_count += 1
elif any(choice == -1 for choice in choices):
incomplete_count += 1
elif any(choice == -99 for choice in choices):
not_wanted_count += 1
elif all(choice >= 0 for choice in choices):
selected_count += 1

for i, choice in enumerate(choices):
if choice >= 0 and choice in course_mappings[i]:
dish_name = course_mappings[i][choice]
if dish_name in course_frequencies[i]:
course_frequencies[i][dish_name] += 1
else:
course_frequencies[i][dish_name] = 1

logger.info(f"Statistics: Not selected: {not_selected_count}, Incomplete: {incomplete_count}, Not wanted: {not_wanted_count}, Selected: {selected_count}")
logger.info(f"Course Frequencies: {course_frequencies}")

return {
'statusCode': 200,
'body': json.dumps({
'statistics': {
'not_selected_count': not_selected_count,
'incomplete_count': incomplete_count,
'not_wanted_count': not_wanted_count,
'selected_count': selected_count,
'course_frequencies': course_frequencies,
'reminders_sent': get_last_email()
}}, cls=DecimalEncoder.DecimalEncoder)
}

# from pprint import pprint
# pprint(lambda_handler({'requestContext':{'http':{'method':"GET"}}}, None))
2 changes: 1 addition & 1 deletion functions/send_email/lambda_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def lambda_handler(event, context):
body_tmpl = Template(body_file.read())
subdomain = "www" if os.environ.get("STAGE_NAME") == "prod" else os.environ.get("STAGE_NAME")
body = body_tmpl.substitute({
'deanline_date':deadline_date,
'deadline_date':deadline_date,
'ticket_link':"http://{}.merseysidelatinfestival.co.uk/preferences?email={}&ticket_number={}".format(subdomain, event['email'], event['ticket_number']),
})
attachment = None
Expand Down
2 changes: 0 additions & 2 deletions functions/send_email/meal_body.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,10 @@ <h1>Set your meal preferences!</h1>
It will be an unforgettable evening with great food and even better dance moves.
Take a quick moment to select your meal options now.
</p>
<br />
<p>
You can modify your options up until the $deadline_date, after this date you can no longer select options and it will be set to the default option.
If you have purchased multiple tickets with dinner included you will need to select the options for each ticket.
</p>
<br />
<p>
To manage your ticket click the link below and you will be able to select your meal options.
If you have a table preference you can <b>join a group</b> and we will try out best to sit you with the people in your group.
Expand Down
Loading

0 comments on commit 4a393bb

Please sign in to comment.