diff --git a/api/controllers/EventController.ts b/api/controllers/EventController.ts index ca9e2d2d..5683dd52 100644 --- a/api/controllers/EventController.ts +++ b/api/controllers/EventController.ts @@ -19,6 +19,7 @@ import { GetFutureEventsResponse, GetAllEventsResponse, GetPastEventsResponse, + GetFeedbackForEventResponse, } from '../../types'; import { UuidParam } from '../validators/GenericRequests'; import { @@ -27,18 +28,23 @@ import { CreateEventRequest, SubmitEventFeedbackRequest, } from '../validators/EventControllerRequests'; +import FeedbackService from '../../services/FeedbackService'; @JsonController('/event') export class EventController { private eventService: EventService; + private feedbackService: FeedbackService; + private storageService: StorageService; private attendanceService: AttendanceService; - constructor(eventService: EventService, storageService: StorageService, attendanceService: AttendanceService) { + constructor(eventService: EventService, storageService: StorageService, + attendanceService: AttendanceService, feedbackService: FeedbackService) { this.eventService = eventService; this.storageService = storageService; + this.feedbackService = feedbackService; this.attendanceService = attendanceService; } @@ -60,6 +66,19 @@ export class EventController { return { error: null, events }; } + @UseBefore(OptionalUserAuthentication) + @Get('/:uuid/feedback') + async getFeedbackByEvent(@Params() params: UuidParam, @AuthenticatedUser() user: UserModel): + Promise { + const options = { + event: params.uuid, + }; + const canSeeAllFeedback = PermissionsService.canSeeAllFeedback(user); + + const feedback = await this.feedbackService.getFeedback(canSeeAllFeedback, user, options); + return { error: null, feedback }; + } + @UseBefore(UserAuthentication) @Post('/picture/:uuid') async updateEventCover(@UploadedFile('image', diff --git a/api/controllers/FeedbackController.ts b/api/controllers/FeedbackController.ts index 91db2921..14cf7771 100644 --- a/api/controllers/FeedbackController.ts +++ b/api/controllers/FeedbackController.ts @@ -1,5 +1,5 @@ import { Body, ForbiddenError, Get, JsonController, Params, - Patch, Post, UseBefore, QueryParams } from 'routing-controllers'; + Patch, Post, UseBefore } from 'routing-controllers'; import { AuthenticatedUser } from '../decorators/AuthenticatedUser'; import { UserModel } from '../../models/UserModel'; import PermissionsService from '../../services/PermissionsService'; @@ -10,7 +10,6 @@ import { UserAuthentication } from '../middleware/UserAuthentication'; import { SubmitFeedbackRequest, UpdateFeedbackStatusRequest, - FeedbackSearchOptions, } from '../validators/FeedbackControllerRequests'; @UseBefore(UserAuthentication) @@ -22,11 +21,14 @@ export class FeedbackController { this.feedbackService = feedbackService; } - @Get() - async getFeedback(@QueryParams() options: FeedbackSearchOptions, - @AuthenticatedUser() user: UserModel): Promise { - const canSeeAllFeedback = PermissionsService.canSeeAllFeedback(user); - const feedback = await this.feedbackService.getFeedback(canSeeAllFeedback, user, options); + @Get('/event/:uuid') + async getEventFeedback(@Params() params: UuidParam, @AuthenticatedUser() user: UserModel): + Promise { + const options = { + user: user.uuid, + event: params.uuid, + }; + const feedback = await this.feedbackService.getFeedback(true, user, options); return { error: null, feedback }; } diff --git a/services/FeedbackService.ts b/services/FeedbackService.ts index f8d5f059..aea413b0 100644 --- a/services/FeedbackService.ts +++ b/services/FeedbackService.ts @@ -20,12 +20,12 @@ export default class FeedbackService { options: FeedbackSearchOptions): Promise { return this.transactions.readOnly(async (txn) => { const feedbackRepository = Repositories.feedback(txn); - if (canSeeAllFeedback) { - return (await feedbackRepository.getAllFeedback(options)) - .map((fb) => fb.getPublicFeedback()); + + if (!canSeeAllFeedback) { + throw new UserError('Incorrect permissions to view event feedback'); } - const userFeedback = await feedbackRepository.getStandardUserFeedback(user, options); + const userFeedback = await feedbackRepository.getAllFeedback(options); return userFeedback.map((fb) => fb.getPublicFeedback()); }); } diff --git a/tests/controllers/ControllerFactory.ts b/tests/controllers/ControllerFactory.ts index 8d993284..d7b7a207 100644 --- a/tests/controllers/ControllerFactory.ts +++ b/tests/controllers/ControllerFactory.ts @@ -58,7 +58,8 @@ export class ControllerFactory { const eventService = new EventService(conn.manager); const storageService = new StorageService(); const attendanceService = new AttendanceService(conn.manager); - return new EventController(eventService, storageService, attendanceService); + const feedbackService = new FeedbackService(conn.manager); + return new EventController(eventService, storageService, attendanceService, feedbackService); } public static leaderboard(conn: Connection): LeaderboardController { diff --git a/tests/event.test.ts b/tests/event.test.ts index ef1d64dd..e4fcf64d 100644 --- a/tests/event.test.ts +++ b/tests/event.test.ts @@ -1,10 +1,23 @@ import * as moment from 'moment'; import { ForbiddenError } from 'routing-controllers'; -import { UserAccessType } from '../types'; +import { FeedbackStatus, UserAccessType } from '../types'; import { ControllerFactory } from './controllers'; import { DatabaseConnection, EventFactory, PortalState, UserFactory } from './data'; import { CreateEventRequest } from '../api/validators/EventControllerRequests'; import { EventModel } from '../models/EventModel'; +import { FeedbackFactory } from './data/FeedbackFactory'; + +function buildFeedbackRequest(feedback) { + return { + feedback: { + event: feedback.event.uuid, + source: feedback.source, + status: feedback.status, + type: feedback.type, + description: feedback.description, + }, + }; +} beforeAll(async () => { await DatabaseConnection.connect(); @@ -175,4 +188,86 @@ describe('event deletion', () => { expect(repositoryEvent).toEqual(event); }); + + test('admin can view all feedback for any event', async () => { + // setting up inputs + const conn = await DatabaseConnection.get(); + const admin = UserFactory.fake({ accessType: UserAccessType.ADMIN }); + const [member1, member2] = UserFactory.create(2); + const event1 = EventFactory.fake(); + const event2 = EventFactory.fake(); + const feedback1 = FeedbackFactory.fake({ event: event1, user: member1 }); + const feedback2 = FeedbackFactory.fake({ event: event1, user: member2 }); + const feedback3 = FeedbackFactory.fake({ event: event2, user: member1 }); + + await new PortalState() + .createUsers(admin, member1, member2) + .createEvents(event1, event2) + .write(); + + const eventController = ControllerFactory.event(conn); + const feedbackController = ControllerFactory.feedback(conn); + await feedbackController.submitFeedback(buildFeedbackRequest(feedback1), member1); + await feedbackController.submitFeedback(buildFeedbackRequest(feedback2), member2); + await feedbackController.submitFeedback(buildFeedbackRequest(feedback3), member1); + + const event1Feedback = await eventController.getFeedbackByEvent({ uuid: event1.uuid }, admin); + const event2Feedback = await eventController.getFeedbackByEvent({ uuid: event2.uuid }, admin); + const event1Sorted = event1Feedback.feedback.sort(); + + expect(event1Feedback.feedback).toHaveLength(2); + expect(event2Feedback.feedback).toHaveLength(1); + + expect(event2Feedback.feedback[0]).toMatchObject({ + ...event2Feedback.feedback[0], + user: member1.getPublicProfile(), + event: event2.getPublicEvent(), + source: feedback3.source, + description: feedback3.description, + status: FeedbackStatus.SUBMITTED, + type: feedback3.type, + }); + + expect(event1Sorted[1]).toMatchObject({ + ...event1Sorted[1], + user: member1.getPublicProfile(), + event: event1.getPublicEvent(), + source: feedback1.source, + description: feedback1.description, + status: FeedbackStatus.SUBMITTED, + type: feedback1.type, + }); + + expect(event1Sorted[0]).toMatchObject({ + ...event1Sorted[0], + user: member2.getPublicProfile(), + event: event1.getPublicEvent(), + source: feedback2.source, + description: feedback2.description, + status: FeedbackStatus.SUBMITTED, + type: feedback2.type, + }); + }); + + test('members cannot view all feedback for event', async () => { + // setting up inputs + const conn = await DatabaseConnection.get(); + const [member1, member2] = UserFactory.create(2); + const event1 = EventFactory.fake(); + const feedback1 = FeedbackFactory.fake({ event: event1, user: member1 }); + + await new PortalState() + .createUsers(member1, member2) + .createEvents(event1) + .write(); + + const eventController = ControllerFactory.event(conn); + const feedbackController = ControllerFactory.feedback(conn); + await feedbackController.submitFeedback(buildFeedbackRequest(feedback1), member1); + + const errorMessage = 'Incorrect permissions to view event feedback'; + + await expect(eventController.getFeedbackByEvent({ uuid: event1.uuid }, member1)) + .rejects.toThrow(errorMessage); + }); }); diff --git a/tests/feedback.test.ts b/tests/feedback.test.ts index f2516307..99266bcb 100644 --- a/tests/feedback.test.ts +++ b/tests/feedback.test.ts @@ -2,7 +2,7 @@ import { validate } from 'class-validator'; import { plainToClass } from 'class-transformer'; import { DatabaseConnection, EventFactory, PortalState, UserFactory } from './data'; import { FeedbackFactory } from './data/FeedbackFactory'; -import { ActivityScope, ActivityType, FeedbackStatus, FeedbackType, UserAccessType } from '../types'; +import { ActivityScope, ActivityType, FeedbackStatus, UserAccessType } from '../types'; import { Feedback } from '../api/validators/FeedbackControllerRequests'; import { ControllerFactory } from './controllers'; @@ -54,7 +54,7 @@ describe('feedback submission', () => { expect(submittedFeedbackResponse.feedback.event.uuid).toEqual(feedback.event.uuid); // check if it persists - const queriedFeedback = await feedbackController.getFeedback({}, member); + const queriedFeedback = await feedbackController.getEventFeedback({ uuid: event.uuid }, member); expect(queriedFeedback.feedback).toHaveLength(1); expect(queriedFeedback.feedback[0]).toEqual({ @@ -82,19 +82,7 @@ describe('feedback submission', () => { }); test('has proper activity scope and type', async () => { - const event = EventFactory.fake({ - title: 'AI: Intro to Neural Nets', - description: `Artificial neural networks (ANNs), usually simply called - neural networks (NNs), are computing systems vaguely inspired by the - biological neural networks that constitute animal brains. An ANN is based - on a collection of connected units or nodes called artificial neurons, - which loosely model the neurons in a biological brain.`, - committee: 'AI', - location: 'Qualcomm Room', - ...EventFactory.daysBefore(6), - attendanceCode: 'galaxybrain', - requiresStaff: true, - }); + const event = EventFactory.fake(); const conn = await DatabaseConnection.get(); const member = UserFactory.fake(); @@ -113,42 +101,7 @@ describe('feedback submission', () => { expect(feedbackSubmissionActivity.type).toEqual(ActivityType.SUBMIT_FEEDBACK); }); - test('admins can view feedback from any member', async () => { - const event = EventFactory.fake(); - - const conn = await DatabaseConnection.get(); - const [member1, member2] = UserFactory.create(2); - const admin = UserFactory.fake({ accessType: UserAccessType.ADMIN }); - const feedback1 = FeedbackFactory.fake({ event }); - const feedback2 = FeedbackFactory.fake({ event }); - - await new PortalState() - .createUsers(member1, member2, admin) - .createEvents(event) - .write(); - - const feedbackController = ControllerFactory.feedback(conn); - - const submittedFeedback1Response = await feedbackController.submitFeedback( - buildFeedbackRequest(feedback1), member1, - ); - const submittedFeedback2Response = await feedbackController.submitFeedback( - buildFeedbackRequest(feedback2), member2, - ); - - const allSubmittedFeedback = await feedbackController.getFeedback({}, admin); - - expect(allSubmittedFeedback.feedback).toHaveLength(2); - - expect(allSubmittedFeedback.feedback).toEqual( - expect.arrayContaining([ - submittedFeedback1Response.feedback, - submittedFeedback2Response.feedback, - ]), - ); - }); - - test('members can view only their own feedback', async () => { + test('members can view only their own feedback for any event', async () => { const event = EventFactory.fake(); const conn = await DatabaseConnection.get(); @@ -164,7 +117,7 @@ describe('feedback submission', () => { const feedbackController = ControllerFactory.feedback(conn); await feedbackController.submitFeedback(buildFeedbackRequest(feedback1), member1); await feedbackController.submitFeedback(buildFeedbackRequest(feedback2), member2); - const user1Feedback = await feedbackController.getFeedback({ user: member1.uuid }, member1); + const user1Feedback = await feedbackController.getEventFeedback({ uuid: event.uuid }, member1); expect(user1Feedback.feedback).toHaveLength(1); @@ -218,249 +171,4 @@ describe('feedback submission', () => { await expect(feedbackController.updateFeedbackStatus(feedbackToIgnoreParams, ignored, admin)) .rejects.toThrow(errorMessage); }); - - test('get all feedback for an event', async () => { - const event1 = EventFactory.fake({ - title: 'AI: Intro to Neural Nets', - description: `Artificial neural networks (ANNs), usually simply called - neural networks (NNs), are computing systems vaguely inspired by the - biological neural networks that constitute animal brains. An ANN is based - on a collection of connected units or nodes called artificial neurons, - which loosely model the neurons in a biological brain.`, - committee: 'AI', - location: 'Qualcomm Room', - ...EventFactory.daysBefore(6), - attendanceCode: 'galaxybrain', - requiresStaff: true, - cover: null, - thumbnail: null, - eventLink: null, - }); - - const event2 = EventFactory.fake({ - title: 'Not the right event!', - description: `Artificial neural networks (ANNs), usually simply called - neural networks (NNs), are computing systems vaguely inspired by the - biological neural networks that constitute animal brains. An ANN is based - on a collection of connected units or nodes called artificial neurons, - which loosely model the neurons in a biological brain.`, - committee: 'AI', - location: 'Qualcomm Room', - ...EventFactory.daysBefore(6), - attendanceCode: 'galxybrain', - requiresStaff: true, - cover: null, - thumbnail: null, - eventLink: null, - }); - - const conn = await DatabaseConnection.get(); - const [member1, member2] = UserFactory.create(2); - const admin = UserFactory.fake({ accessType: UserAccessType.ADMIN }); - const feedback1 = FeedbackFactory.fake({ event: event1, user: member1 }); - const feedback2 = FeedbackFactory.fake({ event: event1, user: member2 }); - const feedback3 = FeedbackFactory.fake({ event: event2, user: member1 }); - - await new PortalState() - .createUsers(member1, member2, admin) - .createEvents(event1, event2) - .write(); - - const feedbackController = ControllerFactory.feedback(conn); - const fb1Response = await feedbackController.submitFeedback(buildFeedbackRequest(feedback1), member1); - const fb2Response = await feedbackController.submitFeedback(buildFeedbackRequest(feedback2), member2); - const fb3Response = await feedbackController.submitFeedback(buildFeedbackRequest(feedback3), member1); - const event1Feedback = await feedbackController.getFeedback({ event: event1.uuid }, admin); - const event2Feedback = await feedbackController.getFeedback({ event: event2.uuid }, admin); - - expect(event1Feedback.feedback).toHaveLength(2); - expect(event2Feedback.feedback).toHaveLength(1); - - expect(event1Feedback.feedback).toEqual( - expect.arrayContaining([ - fb1Response.feedback, - fb2Response.feedback, - ]), - ); - - expect(event2Feedback.feedback).toEqual( - expect.arrayContaining([ - fb3Response.feedback, - ]), - ); - }); - - test('get all feedback by status', async () => { - const event1 = EventFactory.fake(); - - const conn = await DatabaseConnection.get(); - const [member1, member2, member3] = UserFactory.create(3); - const admin = UserFactory.fake({ accessType: UserAccessType.ADMIN }); - const feedback1 = FeedbackFactory.fake({ event: event1, status: FeedbackStatus.ACKNOWLEDGED }); - const feedback2 = FeedbackFactory.fake({ event: event1, status: FeedbackStatus.ACKNOWLEDGED }); - const feedback3 = FeedbackFactory.fake({ event: event1, status: FeedbackStatus.SUBMITTED }); - - await new PortalState() - .createUsers(member1, member2, member3, admin) - .createEvents(event1) - .write(); - - const feedbackController = ControllerFactory.feedback(conn); - const fb1Response = await feedbackController.submitFeedback(buildFeedbackRequest(feedback1), member1); - const fb2Response = await feedbackController.submitFeedback(buildFeedbackRequest(feedback2), member2); - const fb3Response = await feedbackController.submitFeedback(buildFeedbackRequest(feedback3), member3); - const status1Feedback = await feedbackController.getFeedback({ status: FeedbackStatus.ACKNOWLEDGED }, admin); - const status2Feedback = await feedbackController.getFeedback({ status: FeedbackStatus.SUBMITTED }, admin); - - expect(status1Feedback.feedback).toHaveLength(2); - expect(status2Feedback.feedback).toHaveLength(1); - - expect(status1Feedback.feedback).toEqual( - expect.arrayContaining([ - fb1Response.feedback, - fb2Response.feedback, - ]), - ); - - expect(status2Feedback.feedback).toEqual( - expect.arrayContaining([ - fb3Response.feedback, - ]), - ); - }); - - test('get all feedback by type', async () => { - const event1 = EventFactory.fake(); - - const conn = await DatabaseConnection.get(); - const [member1, member2, member3] = UserFactory.create(3); - const admin = UserFactory.fake({ accessType: UserAccessType.ADMIN }); - const feedback1 = FeedbackFactory.fake({ event: event1, type: FeedbackType.GENERAL, user: member1 }); - const feedback2 = FeedbackFactory.fake({ event: event1, type: FeedbackType.GENERAL, user: member2 }); - const feedback3 = FeedbackFactory.fake({ event: event1, type: FeedbackType.AI, user: member3 }); - - await new PortalState() - .createUsers(member1, member2, member3, admin) - .createEvents(event1) - .write(); - - const feedbackController = ControllerFactory.feedback(conn); - const fb1Response = await feedbackController.submitFeedback(buildFeedbackRequest(feedback1), member1); - const fb2Response = await feedbackController.submitFeedback(buildFeedbackRequest(feedback2), member2); - const fb3Response = await feedbackController.submitFeedback(buildFeedbackRequest(feedback3), member3); - const type1Feedback = await feedbackController.getFeedback({ type: FeedbackType.GENERAL }, admin); - const type2Feedback = await feedbackController.getFeedback({ type: FeedbackType.AI }, admin); - - expect(type1Feedback.feedback).toHaveLength(2); - expect(type2Feedback.feedback).toHaveLength(1); - - expect(type1Feedback.feedback).toEqual( - expect.arrayContaining([ - fb1Response.feedback, - fb2Response.feedback, - ]), - ); - - expect(type2Feedback.feedback).toEqual( - expect.arrayContaining([ - fb3Response.feedback, - ]), - ); - }); - - test('get all feedback by member', async () => { - const event1 = EventFactory.fake(); - - const conn = await DatabaseConnection.get(); - const member = UserFactory.fake(); - const member2 = UserFactory.fake(); - const admin = UserFactory.fake({ accessType: UserAccessType.ADMIN }); - const feedback1 = FeedbackFactory.fake({ event: event1, user: member }); - const feedback2 = FeedbackFactory.fake({ event: event1, user: member2 }); - - await new PortalState() - .createUsers(member, member2, admin) - .createEvents(event1) - .write(); - - const feedbackController = ControllerFactory.feedback(conn); - const fb1Response = await feedbackController.submitFeedback(buildFeedbackRequest(feedback1), member); - const fb2Response = await feedbackController.submitFeedback(buildFeedbackRequest(feedback2), member2); - - const type1Feedback = await feedbackController.getFeedback({ user: member.uuid }, admin); - const type2Feedback = await feedbackController.getFeedback({ user: member2.uuid }, admin); - - expect(type1Feedback.feedback).toHaveLength(1); - expect(type2Feedback.feedback).toHaveLength(1); - - expect(type1Feedback.feedback).toEqual( - expect.arrayContaining([ - fb1Response.feedback, - ]), - ); - - expect(type2Feedback.feedback).toEqual( - expect.arrayContaining([ - fb2Response.feedback, - ]), - ); - }); - - test('get all feedback with multiple parameters', async () => { - const event1 = EventFactory.fake(); - - const event2 = EventFactory.fake(); - - const conn = await DatabaseConnection.get(); - const [member1, member2, member3] = UserFactory.create(3); - const admin = UserFactory.fake({ accessType: UserAccessType.ADMIN }); - const feedback1 = FeedbackFactory.fake({ event: event1, - status: FeedbackStatus.ACKNOWLEDGED, - type: FeedbackType.GENERAL, - user: member1 }); - const feedback2 = FeedbackFactory.fake({ event: event1, - status: FeedbackStatus.IGNORED, - type: FeedbackType.GENERAL, - user: member2 }); - const feedback3 = FeedbackFactory.fake({ event: event1, - status: FeedbackStatus.SUBMITTED, - type: FeedbackType.INNOVATE, - user: member3 }); - const feedback4 = FeedbackFactory.fake({ event: event2, - status: FeedbackStatus.ACKNOWLEDGED, - type: FeedbackType.GENERAL, - user: member1 }); - - await new PortalState() - .createUsers(member1, member2, member3, admin) - .createEvents(event1, event2) - .write(); - - const feedbackController = ControllerFactory.feedback(conn); - const fb1Response = await feedbackController.submitFeedback(buildFeedbackRequest(feedback1), member1); - const fb2Response = await feedbackController.submitFeedback(buildFeedbackRequest(feedback2), member2); - await feedbackController.submitFeedback(buildFeedbackRequest(feedback3), member3); - const fb4Response = await feedbackController.submitFeedback(buildFeedbackRequest(feedback4), member1); - const query1Feedback = await feedbackController.getFeedback({ event: event1.uuid, - type: FeedbackType.GENERAL }, admin); - const query2Feedback = await feedbackController.getFeedback({ status: FeedbackStatus.ACKNOWLEDGED, - type: FeedbackType.GENERAL }, admin); - - expect(query1Feedback.feedback).toHaveLength(2); - expect(query2Feedback.feedback).toHaveLength(2); - - expect(query1Feedback.feedback).toEqual( - expect.arrayContaining([ - fb1Response.feedback, - fb2Response.feedback, - ]), - ); - - expect(query2Feedback.feedback).toEqual( - expect.arrayContaining([ - fb1Response.feedback, - fb4Response.feedback, - ]), - ); - }); }); diff --git a/types/ApiResponses.ts b/types/ApiResponses.ts index f0479224..3155a0d8 100644 --- a/types/ApiResponses.ts +++ b/types/ApiResponses.ts @@ -129,6 +129,10 @@ export interface GetFutureEventsResponse extends ApiResponse { events: PublicEvent[]; } +export interface GetFeedbackForEventResponse extends ApiResponse { + feedback: PublicFeedback[]; +} + export interface UpdateEventCoverResponse extends ApiResponse { event: PublicEvent; }