From 6fe1c48cb7038ffab0a60eee570ef7107168d392 Mon Sep 17 00:00:00 2001 From: cindy-x-liang <67083541+cindy-x-liang@users.noreply.github.com> Date: Sun, 26 Nov 2023 17:11:24 -0500 Subject: [PATCH] Enabling notifications for adding, editing, and deleting a flyer (#118) * Implemented getAllFlyerCategories and tests * update jest * update jest * implemented suggestions from zach, vin, and archit * got rid of ESLint comment * fixed names for tests * Added loops to test cases * Fixed test case to create flyers with the same category * Changed 'push' to 'concat' * Enabled notifications for adding, editing, and deleting a flyer - Created a function in NotificationRepo.ts called notifyFlyersForOrganization that takes in a flyerID and a title of the notification - the title of the notification should reflect the action performed (add, edit, delete) - This function is called in FlyerResolver.ts * Added more specific body text for updating flyers, removed notifyNewFlyers * Addressed Vin's Comments - used enums for action commparison instead of strings - used template strings instead of concat - added documentation for the various branches of the if statement in the notification for editing a flyer * Addressing Vin's Comments - changed nested if statements to a switch block - used proper capitlization for Enums * add num flyers for organizations * Changed edit and delete flyer notifications to only be sent to users who have flyers bookmarked - in UserRepo added getUsersBookmarkedFlyers to get a list of all the users that have the desired flyer bookmarked - in NotificationRepo, factored out a function titled notifyFlyersForBookmarks - in FlyerResolver, for edit/delete flyers notifyFlyersForBookmarks will be called instead of notifyFlyersForOrganizations * fixed specs for added functions * addressing vin's comments --------- Co-authored-by: Cindy Co-authored-by: Vin Bui --- src/repos/FlyerRepo.ts | 8 ++- src/repos/NotificationRepo.ts | 88 ++++++++++++++++++++++++++- src/repos/UserRepo.ts | 10 +++ src/resolvers/FlyerResolver.ts | 39 +++++++++++- src/resolvers/OrganizationResolver.ts | 7 +++ 5 files changed, 147 insertions(+), 5 deletions(-) diff --git a/src/repos/FlyerRepo.ts b/src/repos/FlyerRepo.ts index 162f359..2bbd17a 100644 --- a/src/repos/FlyerRepo.ts +++ b/src/repos/FlyerRepo.ts @@ -7,7 +7,11 @@ import { OrganizationModel } from '../entities/Organization'; import utils from '../utils'; const { IS_FILTER_ACTIVE } = process.env; - +export enum Actions { + Add, + Edit, + Delete, +} function isFlyerFiltered(flyer: Flyer) { if (IS_FILTER_ACTIVE === 'true') { const filter = new Filter({ list: FILTERED_WORDS }); @@ -251,6 +255,7 @@ const createFlyer = async ( startDate, title, }); + return FlyerModel.create(newFlyer); }; @@ -303,6 +308,7 @@ const editFlyer = async ( // Update flyer fields (if not nul) if (categorySlug) flyer.categorySlug = categorySlug; if (endDate) flyer.endDate = new Date(endDate); + if (flyerURL) flyer.flyerURL = flyerURL; if (imageURL) flyer.imageURL = imageURL; if (location) flyer.location = location; diff --git a/src/repos/NotificationRepo.ts b/src/repos/NotificationRepo.ts index aae4a01..3a91c21 100644 --- a/src/repos/NotificationRepo.ts +++ b/src/repos/NotificationRepo.ts @@ -1,7 +1,7 @@ import * as admin from 'firebase-admin'; import { User } from '../entities/User'; import ArticleRepo from './ArticleRepo'; -import FlyerRepo from './FlyerRepo'; +import FlyerRepo, { Actions } from './FlyerRepo'; import MagazineRepo from './MagazineRepo'; import OrganizationRepo from './OrganizationRepo'; import PublicationRepo from './PublicationRepo'; @@ -137,6 +137,90 @@ const notifyNewFlyers = async (flyerIDs: string[]): Promise => { }); }; +/** + * Send notifications for edited flyers or deleted flyers for those bookmarked by the user. + * + * @param flyerID - ID of the flyer being added/changed/deleted + * @param bodyText - a string containing the body of the notification + * @param action - a string indicating the action being peformed, a for add, e for edit, d for delete + */ +const notifyFlyersForBookmarks = async ( + flyerID: string, + bodyText: string, + action: Actions, +): Promise => { + const flyer = await FlyerRepo.getFlyerByID(flyerID); + const organization = await OrganizationRepo.getOrganizationBySlug(flyer.organizationSlug); + const followers = await UserRepo.getUsersBookmarkedFlyer(flyerID); + + let notifTitle = ''; + let notifBody = ''; + + followers.forEach(async (follower) => { + switch (action) { + case Actions.Edit: + notifTitle = `New Update from ${organization.name}!`; + notifBody = `${flyer.title} has ${bodyText}`; + break; + case Actions.Delete: + notifTitle = `${organization.name} Event Deleted`; + // the format for toDateString is Day of the Week Month Date Year ex: Tue Sep 05 2023 + notifBody = `${flyer.title} on ${flyer.startDate.toDateString()} has been removed`; + break; + default: + notifTitle = ''; + notifBody = ''; + } + + const uniqueData = { + flyerID: flyer.id, + flyerURL: flyer.flyerURL, + }; + + await sendNotif(follower, notifTitle, notifBody, uniqueData, 'flyer_notif'); + }); +}; + +/** + * Send notifications for new flyers by organizations followed by the user. + * + * @param flyerID - ID of the flyer being added/changed/deleted + * @param bodyText - a string containing the body of the notification + * @param action - a string indicating the action being peformed, a for add, e for edit, d for delete + */ +const notifyFlyersForOrganizations = async ( + flyerID: string, + bodyText: string, + action: Actions, +): Promise => { + const flyer = await FlyerRepo.getFlyerByID(flyerID); // eslint-disable-line + const organization = await OrganizationRepo.getOrganizationBySlug(flyer.organizationSlug); + const followers = await UserRepo.getUsersFollowingOrganization(flyer.organizationSlug); + + let notifTitle = ''; + let notifBody = ''; + + followers.forEach(async (follower) => { + switch (action) { + case Actions.Add: + notifTitle = `New ${organization.name} Event!`; + // the format for toDateString is Day of the Week Month Date Year ex: Tue Sep 05 2023 + notifBody = `${flyer.title} on ${flyer.startDate.toDateString()} at ${flyer.location}`; + break; + default: + notifTitle = ''; + notifBody = ''; + } + + const uniqueData = { + flyerID: flyer.id, + flyerURL: flyer.flyerURL, + }; + + await sendNotif(follower, notifTitle, notifBody, uniqueData, 'flyer_notif'); + }); +}; + /** * Send notifications for weekly debrief release. */ @@ -153,5 +237,7 @@ export default { notifyNewArticles, notifyNewFlyers, notifyNewMagazines, + notifyFlyersForBookmarks, + notifyFlyersForOrganizations, notifyWeeklyDebrief, }; diff --git a/src/repos/UserRepo.ts b/src/repos/UserRepo.ts index a12fe4e..1f52f3e 100644 --- a/src/repos/UserRepo.ts +++ b/src/repos/UserRepo.ts @@ -121,6 +121,15 @@ const getUsersFollowingOrganization = async (orgSlug: string): Promise = return matchedUsers; }; +/** + * Return all users who have a flyer bookmarked + */ +const getUsersBookmarkedFlyer = async (flyerID: string): Promise => { + const matchedUsers = await UserModel.find({ + bookmarkedFlyers: { $elemMatch: { _id: flyerID } }, + }); + return matchedUsers; +}; /** * Add article to a user's readArticles */ @@ -314,6 +323,7 @@ export default { getUserByUUID, getUsersFollowingPublication, getUsersFollowingOrganization, + getUsersBookmarkedFlyer, bookmarkArticle, bookmarkMagazine, bookmarkFlyer, diff --git a/src/resolvers/FlyerResolver.ts b/src/resolvers/FlyerResolver.ts index ba0c45d..ee848f2 100644 --- a/src/resolvers/FlyerResolver.ts +++ b/src/resolvers/FlyerResolver.ts @@ -11,8 +11,9 @@ import { import { Context } from 'vm'; import { Flyer } from '../entities/Flyer'; import { DEFAULT_LIMIT, DEFAULT_OFFSET } from '../common/constants'; -import FlyerRepo from '../repos/FlyerRepo'; +import FlyerRepo, { Actions } from '../repos/FlyerRepo'; import FlyerMiddleware from '../middlewares/FlyerMiddleware'; +import NotificationRepo from '../repos/NotificationRepo'; @Resolver((_of) => Flyer) class FlyerResolver { @@ -192,7 +193,7 @@ class FlyerResolver { @Arg('title') title: string, @Ctx() ctx: Context, ) { - return FlyerRepo.createFlyer( + const flyer = await FlyerRepo.createFlyer( categorySlug, endDate, flyerURL, @@ -202,12 +203,20 @@ class FlyerResolver { startDate, title, ); + NotificationRepo.notifyFlyersForOrganizations( + flyer.id, + ' just added a new flyer!', + Actions.Add, + ); + return flyer; } @Mutation((_returns) => Flyer, { description: `Delete a flyer with the id .`, }) async deleteFlyer(@Arg('id') id: string) { + const flyer = await FlyerRepo.getFlyerByID(id); + NotificationRepo.notifyFlyersForBookmarks(flyer.id, ' just deleted a flyer', Actions.Delete); return FlyerRepo.deleteFlyer(id); } @@ -228,7 +237,7 @@ class FlyerResolver { @Arg('title', { nullable: true }) title: string, @Ctx() ctx: Context, ) { - return FlyerRepo.editFlyer( + const flyer = await FlyerRepo.editFlyer( id, categorySlug, endDate, @@ -238,6 +247,30 @@ class FlyerResolver { startDate, title, ); + let editedResponse = ''; + if ((endDate && location) || (location && startDate)) { + // if endDate and location or startDate and location are nonempty, notification body would return date and location changed + editedResponse = 'changed its date and location'; + } else if (endDate && startDate) { + // if both endDate and startDate values changed, the notifcation body would just return that the event changed its date + editedResponse = 'changed its date'; + } else if (endDate) { + // if only the endDate changed for the event, then the notification body would print out specifically what the date was changed to + const date = new Date(endDate); + // the format for toDateString is Day of the Week Month Date Year ex: Tue Sep 05 2023 + editedResponse = `changed its end date to ${date.toDateString()}`; + } else if (location) { + // if only the location changed for the event, then the notification body would print out specifically what the location was changed to + editedResponse = `changed its location to ${location}`; + } else if (startDate) { + // if only the startDate changed for the event, then the notification body would print out specifically what the date was changed to + const date = new Date(startDate); + // the format for toDateString is Day of the Week Month Date Year ex: Tue Sep 05 2023 + editedResponse = `changed its start date to ${date.toDateString()}`; + } + + NotificationRepo.notifyFlyersForBookmarks(flyer.id, editedResponse, Actions.Edit); + return flyer; } } diff --git a/src/resolvers/OrganizationResolver.ts b/src/resolvers/OrganizationResolver.ts index 17a11b1..4bae6be 100644 --- a/src/resolvers/OrganizationResolver.ts +++ b/src/resolvers/OrganizationResolver.ts @@ -57,6 +57,13 @@ class OrganizationResolver { async clicks(@Root() organization: Organization): Promise { return OrganizationRepo.getClicks(organization); } + + @FieldResolver((_returns) => Number, { + description: "Returns the total times clicked of an ", + }) + async numFlyers(@Root() organization: Organization): Promise { + return OrganizationRepo.getNumFlyers(organization); + } } export default OrganizationResolver;