Skip to content

Commit

Permalink
Merge pull request #4 from evanhsu/fix-entree-name-change
Browse files Browse the repository at this point in the history
- Recognize new category called "Entrees"
- Add unit tests and an updated mockApiResponse for testing
- Add new config parameter: debug: Boolean.
- Increase API request timeout from 8 seconds to 30 seconds.

The TitanSchools API appears to have changed their api responses to include a menu category called "Entrees" instead of using the name "Main Entree" that it used to use.
  • Loading branch information
evanhsu authored Jan 9, 2023
2 parents c00d771 + 995ac4b commit 2d52012
Show file tree
Hide file tree
Showing 8 changed files with 7,725 additions and 1,447 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# MMM-TitanSchoolMealMenu

A module for the MagicMirror framework that retrieves a school meal menu from the Titan Schools API (family.titank12.com)

---
Expand All @@ -20,14 +21,15 @@ Add this to your MagicMirror `config.js`:
updateIntervalMs: 3600000, // Optional: Milliseconds between updates; Default: 3600000 (1 hour)
numberOfDaysToDisplay: 3, // Optional: 0 - 5; Default: 3
recipeCategoriesToInclude: [
"Main Entree",
"Entrees",
"Grain"
// , "Fruit"
// , "Vegetable"
// , "Milk"
// , "Condiment"
// , "Extra"
]
],
debug: false // Optional: boolean; Default: false; Setting this to true will output verbose logs
},
},

Expand Down Expand Up @@ -69,4 +71,3 @@ You can also track multiple school menus by listing the module multiple times in
#### 3. Use your browser's developer tools to inspect a request to the `/FamilyMenu` endpoint. The `districtId` and `buildingId` will be present as query string parameters on these requests.

![Use developer tools to inspect a network request](./docs/step3.png)

123 changes: 100 additions & 23 deletions TitanSchoolsClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ class TitanSchoolsClient {
);
}

this.debug = config.debug === true ? true : false;

this.requestParams = {
buildingId: config.buildingId,
districtId: config.districtId
};

this.recipeCategoriesToInclude = config.recipeCategoriesToInclude ?? [
"Main Entree",
"Main Entree", // Maybe deprecated?
"Entrees",
"Grain"
// , "Fruit"
// , "Vegetable"
Expand All @@ -33,22 +36,22 @@ class TitanSchoolsClient {

this.client = axios.create({
baseURL: "https://family.titank12.com/api/",
timeout: 8000
timeout: 30000
});
}

async fetchMockMenu() {
const data = require("./test/mocks/mockApiResponse");
const data = require("./test/unit/mocks/mockApiResponse");
return this.processData(data);
}

/**
* Fetches menu data from the TitanSchools API and formats it as shown below
*
* @param string startDate
* @param Date startDate (Optional) A Date object that specifies which day the menu should start on
* @throws Error If the TitanSchools API responds with a 400- or 500-level HTTP status
*
* @returns An array of meals shaped like this:
* @returns An array of meals shaped like this (starting on {startDate} and including {config.numberOfDaysToDisplay} days):
* [
* { "date": "9-6-2021", "label": "Today" },
* {
Expand Down Expand Up @@ -80,19 +83,28 @@ class TitanSchoolsClient {
async fetchMenu(startDate = null) {
let params = {
...this.requestParams,
startDate
};

if (startDate === null) {
console.log("Using today as startDate");
// If no startDate was provided, use today's date
// API requires date to be formatted as: m-d-Y (i.e. 12-5-2021)
const now = new Date();
params.startDate = `${
now.getMonth() + 1 // javascript month is 0-indexed :facepalm:
}-${now.getDate()}-${now.getFullYear()}`;
} else {
console.log(`Using ${startDate} as startDate`);
startDate: this.formatDate(startDate ?? new Date())
};

if (this.debug) {
if (startDate === null) {
console.debug("Using today as startDate");
} else {
console.debug(`Using ${startDate} as startDate`);
}

// Log the outbound API request
this.client.interceptors.request.use((request) => {
console.debug(
`Sending API request: ${JSON.stringify({
url: request.url,
params: request.params
})}`
);
return request;
});
}

try {
Expand All @@ -118,23 +130,74 @@ class TitanSchoolsClient {
}
}

processData(data) {
const menus = data.FamilyMenuSessions.map((menuSession) => {
/**
*
* @param Date dateObject A Date object
* @returns string A date string formatted as m-d-Y (1-9-2023)
*/
formatDate(dateObject) {
return `${
dateObject.getMonth() + 1 // javascript month is 0-indexed :facepalm:
}-${dateObject.getDate()}-${dateObject.getFullYear()}`;
}

/**
* Takes in a raw response body from the TitanSchools API and outputs a normalized array of menus by date.
* Since the TitanSchools API has the potential to change without warning, this function will isolate breaking
* API changes and output normalized data that the rest of the functions can assume to be correct.
*
* @param Object apiResponse The response body from the TitanSchools API.
*/
extractMenusByDate(apiResponse) {
const menus = apiResponse.FamilyMenuSessions.map((menuSession) => {
// The titank12 API has several possible values for the ServingSession,
// including "Breakfast", "Lunch", "Seamless Summer Lunch", "Seamless Summer Breakfast".
const breakfastOrLunch = menuSession.ServingSession.match(/breakfast/i)
? "breakfast"
: "lunch";

const menusByDate = menuSession.MenuPlans[0].Days.map(
(menuForThisDate) => {
// Just for logging/troubleshooting, keep track of all the recipes in this menu and note which ones get
// intentionally filtered out.
const recipesToLog = {
all: [],
filteredOut: []
};

const recipeCategories = menuForThisDate.RecipeCategories.filter(
(recipeCategory) => {
recipesToLog.all.push(recipeCategory.CategoryName);

if (
this.recipeCategoriesToInclude.includes(
recipeCategory.CategoryName
)
) {
return true;
} else {
recipesToLog.filteredOut.push(recipeCategory.CategoryName);
return false;
}
}
);

if (this.debug) {
console.debug(
`The ${breakfastOrLunch} menu for ${
menuForThisDate.Date
} contains the following categories: ${recipesToLog.all.join(
", "
)}, but ${recipesToLog.filteredOut.join(
", "
)} were filtered out because they're not included in the config.recipeCategoriesToInclude array.`
);
}

return {
date: menuForThisDate.Date,
breakfastOrLunch,
menu: menuForThisDate.RecipeCategories.filter((recipeCategory) =>
this.recipeCategoriesToInclude.includes(
recipeCategory.CategoryName
)
)
menu: recipeCategories
.map((recipeCategory) => {
return recipeCategory.Recipes.map(
(recipe) => recipe.RecipeName
Expand All @@ -148,6 +211,20 @@ class TitanSchoolsClient {
return menusByDate;
});

if (this.debug) {
console.debug(
`Menus extracted from the TitanSchools API response: ${JSON.stringify(
menus
)}`
);
}

return menus;
}

processData(data) {
const menus = this.extractMenusByDate(data);

const upcomingMenuByDate = upcomingRelativeDates().map((day) => {
// day = { date: '9-6-2021', label: 'Today' }; // Possible labels: 'Today', 'Tomorrow', or a day of the week
const breakfastAndLunchForThisDay = menus.reduce(
Expand Down
Loading

0 comments on commit 2d52012

Please sign in to comment.