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 8 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
27 changes: 20 additions & 7 deletions src/backend/src/controllers/users.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,9 @@ export default class UsersController {

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

const unreadNotifications = await UsersService.getUserUnreadNotifications(userId, organization);
const unreadNotifications = await UsersService.getUserUnreadNotifications(currentUser.userId, organization);
res.status(200).json(unreadNotifications);
} catch (error: unknown) {
next(error);
Expand All @@ -206,14 +205,28 @@ export default class UsersController {

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

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

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

const unreadAnnouncements = await UsersService.getUserUnreadAnnouncements(currentUser.userId, organization);
res.status(200).json(unreadAnnouncements);
} 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
9 changes: 3 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,8 @@ userRouter.post(
validateInputs,
UsersController.getManyUserTasks
);
userRouter.get('/:userId/notifications', UsersController.getUserUnreadNotifications);
userRouter.post(
'/:userId/notifications/remove',
nonEmptyString(body('notificationId')),
UsersController.removeUserNotification
);
userRouter.get('/notifications/current-user', UsersController.getUserUnreadNotifications);
caiodasilva2005 marked this conversation as resolved.
Show resolved Hide resolved
userRouter.get('/announcements/current-user', UsersController.getUserUnreadAnnouncements);
userRouter.post('/notifications/:notificationId/remove', UsersController.removeUserNotification);

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

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);
}
}
48 changes: 36 additions & 12 deletions src/backend/src/services/users.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import { getTaskQueryArgs } from '../prisma-query-args/tasks.query-args';
import taskTransformer from '../transformers/tasks.transformer';
import { getNotificationQueryArgs } from '../prisma-query-args/notifications.query-args';
import notificationTransformer from '../transformers/notifications.transformer';
import { getAnnouncementQueryArgs } from '../prisma-query-args/announcements.query.args';
import announcementTransformer from '../transformers/announcements.transformer';

export default class UsersService {
/**
Expand Down Expand Up @@ -576,29 +578,49 @@ export default class UsersService {
* @returns the unread notifications of the user
*/
static async getUserUnreadNotifications(userId: string, organization: Organization) {
const requestedUser = await prisma.user.findUnique({
where: { userId },
include: { unreadNotifications: getNotificationQueryArgs(organization.organizationId) }
const unreadNotifications = await prisma.notification.findMany({
where: {
users: {
some: { userId }
}
},
...getNotificationQueryArgs(organization.organizationId)
});
if (!requestedUser) throw new NotFoundException('User', userId);

return requestedUser.unreadNotifications.map(notificationTransformer);
if (!unreadNotifications) throw new HttpException(404, 'User Unread Notifications Not Found');

return unreadNotifications.map(notificationTransformer);
}

/**
* 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, organization: Organization) {
const unreadAnnouncements = await prisma.announcement.findMany({
where: {
usersReceived: {
some: { userId }
}
},
...getAnnouncementQueryArgs(organization.organizationId)
});

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

return unreadAnnouncements.map(announcementTransformer);
}

/**
* Removes a notification from the user's unread notifications
* @param userId id of the user to remove notification from
* @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, organization: Organization) {
const requestedUser = await prisma.user.findUnique({
where: { userId }
});

if (!requestedUser) throw new NotFoundException('User', userId);

const updatedUser = await prisma.user.update({
where: { userId },
data: {
Expand All @@ -611,6 +633,8 @@ export default class UsersService {
include: { unreadNotifications: getNotificationQueryArgs(organization.organizationId) }
});

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

return updatedUser.unreadNotifications.map(notificationTransformer);
}
}
19 changes: 19 additions & 0 deletions src/backend/src/transformers/announcements.transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Prisma } from '@prisma/client';
import { AnnouncementQueryArgs } from '../prisma-query-args/announcements.query.args';
import { Announcement } from 'shared';
import { userTransformer } from './user.transformer';

const announcementTransformer = (announcement: Prisma.AnnouncementGetPayload<AnnouncementQueryArgs>): Announcement => {
return {
caiodasilva2005 marked this conversation as resolved.
Show resolved Hide resolved
announcementId: announcement.announcementId,
text: announcement.text,
usersReceived: announcement.usersReceived.map(userTransformer),
dateCreated: announcement.dateCreated,
senderName: announcement.senderName,
slackEventId: announcement.slackEventId,
slackChannelName: announcement.slackChannelName,
dateDeleted: announcement.dateDeleted ?? undefined
};
};

export default announcementTransformer;
1 change: 1 addition & 0 deletions src/backend/tests/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export const resetUsers = async () => {
await prisma.frequentlyAskedQuestion.deleteMany();
await prisma.organization.deleteMany();
await prisma.user.deleteMany();
await prisma.announcement.deleteMany();
};

export const createFinanceTeamAndLead = async (organization?: Organization) => {
Expand Down
47 changes: 30 additions & 17 deletions src/backend/tests/unmocked/users.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { batmanAppAdmin } from '../test-data/users.test-data';
import UsersService from '../../src/services/users.services';
import { NotFoundException } from '../../src/utils/errors.utils';
import NotificationsService from '../../src/services/notifications.services';
import AnnouncementService from '../../src/services/announcement.service';

describe('User Tests', () => {
let orgId: string;
Expand Down Expand Up @@ -51,12 +52,6 @@ describe('User Tests', () => {
});

describe('Get Notifications', () => {
it('fails on invalid user id', async () => {
await expect(async () => await UsersService.getUserUnreadNotifications('1', organization)).rejects.toThrow(
new NotFoundException('User', '1')
);
});

it('Succeeds and gets user notifications', async () => {
const testBatman = await createTestUser(batmanAppAdmin, orgId);
await NotificationsService.sendNotifcationToUsers('test1', 'test1', [testBatman.userId], orgId);
Expand All @@ -71,17 +66,7 @@ describe('User Tests', () => {
});

describe('Remove Notifications', () => {
it('Fails with invalid user', async () => {
const testBatman = await createTestUser(batmanAppAdmin, orgId);
await NotificationsService.sendNotifcationToUsers('test1', 'test1', [testBatman.userId], orgId);
const notifications = await UsersService.getUserUnreadNotifications(testBatman.userId, organization);

await expect(
async () => await UsersService.removeUserNotification('1', notifications[0].notificationId, organization)
).rejects.toThrow(new NotFoundException('User', '1'));
});

it('Succeeds and gets user notifications', async () => {
it('Succeeds and removes user notification', async () => {
const testBatman = await createTestUser(batmanAppAdmin, orgId);
await NotificationsService.sendNotifcationToUsers('test1', 'test1', [testBatman.userId], orgId);
await NotificationsService.sendNotifcationToUsers('test2', 'test2', [testBatman.userId], orgId);
Expand All @@ -102,4 +87,32 @@ describe('User Tests', () => {
expect(updatedNotifications[0].text).toBe('test2');
});
});

describe('Get Announcements', () => {
it('Succeeds and gets user announcements', async () => {
const testBatman = await createTestUser(batmanAppAdmin, orgId);
await AnnouncementService.createAnnouncement(
'test1',
[testBatman.userId],
'Thomas Emrax',
'1',
'software',
organization.organizationId
);
await AnnouncementService.createAnnouncement(
'test2',
[testBatman.userId],
'Superman',
'50',
'mechanical',
organization.organizationId
);

const announcements = await UsersService.getUserUnreadAnnouncements(testBatman.userId, organization);

expect(announcements).toHaveLength(2);
expect(announcements[0].text).toBe('test1');
expect(announcements[1].text).toBe('test2');
});
});
});
1 change: 1 addition & 0 deletions src/shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from './src/types/task-types';
export * from './src/types/reimbursement-requests-types';
export * from './src/types/design-review-types';
export * from './src/types/notifications.types';
export * from './src/types/announcements.types';
export * from './src/validate-wbs';
export * from './src/date-utils';

Expand Down
Loading
Loading