Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#3074-Get Unread Announcements #3076

Merged
merged 19 commits into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/backend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import workPackageTemplatesRouter from './src/routes/work-package-templates.rout
import carsRouter from './src/routes/cars.routes';
import organizationRouter from './src/routes/organizations.routes';
import recruitmentRouter from './src/routes/recruitment.routes';
import announcementsRouter from './src/routes/announcements.routes';
import onboardingRouter from './src/routes/onboarding.routes';

const app = express();
Expand Down Expand Up @@ -69,6 +70,7 @@ app.use('/templates', workPackageTemplatesRouter);
app.use('/cars', carsRouter);
app.use('/organizations', organizationRouter);
app.use('/recruitment', recruitmentRouter);
app.use('/announcements', announcementsRouter);
app.use('/onboarding', onboardingRouter);
app.use('/', (_req, res) => {
res.status(200).json('Welcome to FinishLine');
Expand Down
18 changes: 18 additions & 0 deletions src/backend/src/controllers/announcements.controllers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { NextFunction, Request, Response } from 'express';
import AnnouncementService from '../services/announcement.service';

export default class AnnouncementController {
static async getUserUnreadAnnouncements(req: Request, res: Response, next: NextFunction) {
try {
const { organization, currentUser } = req;

const unreadAnnouncements = await AnnouncementService.getUserUnreadAnnouncements(
currentUser.userId,
organization.organizationId
);
res.status(200).json(unreadAnnouncements);
} catch (error: unknown) {
next(error);
}
}
}
30 changes: 30 additions & 0 deletions src/backend/src/controllers/notifications.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,34 @@ export default class NotificationsController {
next(error);
}
}

static async getUserUnreadNotifications(req: Request, res: Response, next: NextFunction) {
try {
const { organization, currentUser } = req;

const unreadNotifications = await NotificationsService.getUserUnreadNotifications(
currentUser.userId,
organization.organizationId
);
res.status(200).json(unreadNotifications);
} catch (error: unknown) {
next(error);
}
}

static async removeUserNotification(req: Request, res: Response, next: NextFunction) {
try {
const { notificationId } = req.body;
const { organization, currentUser } = req;

const unreadNotifications = await NotificationsService.removeUserNotification(
currentUser.userId,
notificationId,
organization.organizationId
);
res.status(200).json(unreadNotifications);
} catch (error: unknown) {
next(error);
}
}
}
25 changes: 0 additions & 25 deletions src/backend/src/controllers/users.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,29 +191,4 @@ export default class UsersController {
next(error);
}
}

static async getUserUnreadNotifications(req: Request, res: Response, next: NextFunction) {
try {
const { userId } = req.params;
const { organization } = req;

const unreadNotifications = await UsersService.getUserUnreadNotifications(userId, organization);
res.status(200).json(unreadNotifications);
} catch (error: unknown) {
next(error);
}
}

static async removeUserNotification(req: Request, res: Response, next: NextFunction) {
try {
const { userId } = req.params;
const { notificationId } = req.body;
const { organization } = req;

const unreadNotifications = await UsersService.removeUserNotification(userId, notificationId, organization);
res.status(200).json(unreadNotifications);
} catch (error: unknown) {
next(error);
}
}
}
11 changes: 11 additions & 0 deletions src/backend/src/prisma-query-args/announcements.query.args.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Prisma } from '@prisma/client';
import { getUserQueryArgs } from './user.query-args';

export type AnnouncementQueryArgs = ReturnType<typeof getAnnouncementQueryArgs>;

export const getAnnouncementQueryArgs = (organizationId: string) =>
Prisma.validator<Prisma.AnnouncementDefaultArgs>()({
include: {
usersReceived: getUserQueryArgs(organizationId)
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ ALTER TABLE "Project" ADD COLUMN "organizationId" TEXT;
CREATE TABLE "Announcement" (
"announcementId" TEXT NOT NULL,
"text" TEXT NOT NULL,
"dateCrated" TIMESTAMP(3) NOT NULL,
"userCreatedId" TEXT NOT NULL,
"dateCreated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"dateDeleted" TIMESTAMP(3),
"senderName" TEXT NOT NULL,
"slackEventId" TEXT NOT NULL,
"slackChannelName" TEXT NOT NULL,

CONSTRAINT "Announcement_pkey" PRIMARY KEY ("announcementId")
);
Expand All @@ -36,6 +39,9 @@ CREATE TABLE "_userNotifications" (
"B" TEXT NOT NULL
);

-- CreateIndex
CREATE UNIQUE INDEX "Announcement_slackEventId_key" ON "Announcement"("slackEventId");

-- CreateIndex
CREATE UNIQUE INDEX "_receivedAnnouncements_AB_unique" ON "_receivedAnnouncements"("A", "B");

Expand All @@ -51,9 +57,6 @@ CREATE INDEX "_userNotifications_B_index" ON "_userNotifications"("B");
-- AddForeignKey
ALTER TABLE "Project" ADD CONSTRAINT "Project_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("organizationId") ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Announcement" ADD CONSTRAINT "Announcement_userCreatedId_fkey" FOREIGN KEY ("userCreatedId") REFERENCES "User"("userId") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "_receivedAnnouncements" ADD CONSTRAINT "_receivedAnnouncements_A_fkey" FOREIGN KEY ("A") REFERENCES "Announcement"("announcementId") ON DELETE CASCADE ON UPDATE CASCADE;

Expand Down
17 changes: 9 additions & 8 deletions src/backend/src/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,7 @@ model User {
deletedFrequentlyAskedQuestions FrequentlyAskedQuestion[] @relation(name: "frequentlyAskedQuestionDeleter")
createdMilestones Milestone[] @relation(name: "milestoneCreator")
deletedMilestones Milestone[] @relation(name: "milestoneDeleter")
receivedAnnouncements Announcement[] @relation(name: "receivedAnnouncements")
createdAnnouncements Announcement[] @relation(name: "createdAnnouncements")
unreadAnnouncements Announcement[] @relation(name: "receivedAnnouncements")
unreadNotifications Notification[] @relation(name: "userNotifications")
}

Expand Down Expand Up @@ -932,12 +931,14 @@ model Milestone {
}

model Announcement {
announcementId String @id @default(uuid())
text String
usersReceived User[] @relation("receivedAnnouncements")
dateCrated DateTime
userCreatedId String
userCreated User @relation("createdAnnouncements", fields: [userCreatedId], references: [userId])
announcementId String @id @default(uuid())
text String
usersReceived User[] @relation("receivedAnnouncements")
dateCreated DateTime @default(now())
dateDeleted DateTime?
senderName String
slackEventId String @unique
slackChannelName String
}

model Notification {
Expand Down
11 changes: 10 additions & 1 deletion src/backend/src/prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { writeFileSync } from 'fs';
import WorkPackageTemplatesService from '../services/work-package-template.services';
import RecruitmentServices from '../services/recruitment.services';
import OrganizationsService from '../services/organizations.services';
import NotificationsService from '../services/notifications.services';
import AnnouncementService from '../services/announcement.service';

const prisma = new PrismaClient();

Expand Down Expand Up @@ -1894,6 +1894,15 @@ const performSeed: () => Promise<void> = async () => {
await RecruitmentServices.createFaq(batman, 'When was FinishLine created?', 'FinishLine was created in 2019', ner);

await RecruitmentServices.createFaq(batman, 'How many developers are working on FinishLine?', '178 as of 2024', ner);

await AnnouncementService.createAnnouncement(
'Welcome to Finishline!',
[regina.userId],
'Thomas Emrax',
'1',
'software',
ner.organizationId
);
};

performSeed()
Expand Down
8 changes: 8 additions & 0 deletions src/backend/src/routes/announcements.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import express from 'express';
import AnnouncementController from '../controllers/announcements.controllers';

const announcementsRouter = express.Router();

announcementsRouter.get('/current-user', AnnouncementController.getUserUnreadAnnouncements);

export default announcementsRouter;
8 changes: 8 additions & 0 deletions src/backend/src/routes/notifications.routes.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import express from 'express';
import NotificationsController from '../controllers/notifications.controllers';
import { nonEmptyString } from '../utils/validation.utils';
import { body } from 'express-validator';

const notificationsRouter = express.Router();

notificationsRouter.post('/task-deadlines', NotificationsController.sendDailySlackNotifications);
notificationsRouter.get('/current-user', NotificationsController.getUserUnreadNotifications);
notificationsRouter.post(
'/current-user/remove',
nonEmptyString(body('notificationId')),
caiodasilva2005 marked this conversation as resolved.
Show resolved Hide resolved
NotificationsController.removeUserNotification
);

export default notificationsRouter;
6 changes: 0 additions & 6 deletions src/backend/src/routes/users.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,5 @@ userRouter.post(
validateInputs,
UsersController.getManyUserTasks
);
userRouter.get('/:userId/notifications', UsersController.getUserUnreadNotifications);
userRouter.post(
'/:userId/notifications/remove',
nonEmptyString(body('notificationId')),
UsersController.removeUserNotification
);

export default userRouter;
66 changes: 66 additions & 0 deletions src/backend/src/services/announcement.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Announcement } from 'shared';
import prisma from '../prisma/prisma';
import { getAnnouncementQueryArgs } from '../prisma-query-args/announcements.query.args';
import announcementTransformer from '../transformers/announcements.transformer';
import { HttpException } from '../utils/errors.utils';

export default class AnnouncementService {
/**
* Creates an announcement that is sent to users
* this data is populated from slack events
* @param text slack message text
* @param usersReceivedIds users to send announcements to
* @param dateCreated date created of slack message
* @param senderName name of user who sent slack message
* @param slackEventId id of slack event (provided by slack api)
* @param slackChannelName name of channel message was sent in
* @param organizationId id of organization of users
* @returns the created announcement
*/
static async createAnnouncement(
Peyton-McKee marked this conversation as resolved.
Show resolved Hide resolved
text: string,
usersReceivedIds: string[],
senderName: string,
slackEventId: string,
slackChannelName: string,
organizationId: string
): Promise<Announcement> {
const announcement = await prisma.announcement.create({
data: {
text,
usersReceived: {
connect: usersReceivedIds.map((id) => ({
userId: id
}))
},
senderName,
slackEventId,
slackChannelName
},
...getAnnouncementQueryArgs(organizationId)
});

return announcementTransformer(announcement);
}

/**
* Gets all of a user's unread announcements
* @param userId id of the current user
* @param organization the user's orgainzation
* @returns the unread announcements of the user
*/
static async getUserUnreadAnnouncements(userId: string, organizationId: string) {
const unreadAnnouncements = await prisma.announcement.findMany({
where: {
usersReceived: {
some: { userId }
}
},
...getAnnouncementQueryArgs(organizationId)
});

if (!unreadAnnouncements) throw new HttpException(404, 'User Unread Announcements Not Found');

return unreadAnnouncements.map(announcementTransformer);
}
}
46 changes: 46 additions & 0 deletions src/backend/src/services/notifications.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,52 @@ export default class NotificationsService {
await Promise.all(promises);
}

/**
* Gets all of a user's unread notifications
* @param userId id of user to get unread notifications from
* @param organization the user's orgainzation
* @returns the unread notifications of the user
*/
static async getUserUnreadNotifications(userId: string, organizationId: string) {
const unreadNotifications = await prisma.notification.findMany({
where: {
users: {
some: { userId }
}
},
...getNotificationQueryArgs(organizationId)
});

if (!unreadNotifications) throw new HttpException(404, 'User Unread Notifications Not Found');

return unreadNotifications.map(notificationTransformer);
}

/**
* Removes a notification from the user's unread notifications
* @param userId id of the current user
* @param notificationId id of the notification to remove
* @param organization the user's organization
* @returns the user's updated unread notifications
*/
static async removeUserNotification(userId: string, notificationId: string, organizationId: string) {
const updatedUser = await prisma.user.update({
where: { userId },
data: {
unreadNotifications: {
disconnect: {
notificationId
}
}
},
include: { unreadNotifications: getNotificationQueryArgs(organizationId) }
});

if (!updatedUser) throw new HttpException(404, `Failed to remove notication: ${notificationId}`);

return updatedUser.unreadNotifications.map(notificationTransformer);
}

/**
* Creates and sends a notification to all users with the given userIds
* @param text writing in the notification
Expand Down
Loading
Loading