Skip to content

Commit

Permalink
Get another user's event attendance (#358)
Browse files Browse the repository at this point in the history
* attendences from user uuid

* lint and bugfix

* check same user

* controller factory changes

* lint fixes

* unit test for get attendance by uuid

* lint

* add permision

* add types

* add everything else

* rename migrtion

* test when permision is off

* lint

* forgor to add

* change permission name and fix logic a bit

* rename permission, change patch user

* lint fix

* lint fix

* oops

* check user exists

* lint

* rename tests

* public profile change

* change user model

* lint

* tests

* lint

* updated api version

---------

Co-authored-by: Nikhil Dange <[email protected]>
  • Loading branch information
maxwn04 and nik-dange authored Dec 31, 2023
1 parent f0a5d4e commit 03a4c3b
Show file tree
Hide file tree
Showing 11 changed files with 107 additions and 4 deletions.
12 changes: 11 additions & 1 deletion api/controllers/AttendanceController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,17 @@ export class AttendanceController {

@Get()
async getAttendancesForCurrentUser(@AuthenticatedUser() user: UserModel): Promise<GetAttendancesForUserResponse> {
const attendances = await this.attendanceService.getAttendancesForUser(user);
const attendances = await this.attendanceService.getAttendancesForCurrentUser(user);
return { error: null, attendances };
}

@Get('/user/:uuid')
async getAttendancesForUser(@Params() params: UuidParam,
@AuthenticatedUser() currentUser: UserModel): Promise<GetAttendancesForEventResponse> {
if (params.uuid === currentUser.uuid) {
return this.getAttendancesForCurrentUser(currentUser);
}
const attendances = await this.attendanceService.getAttendancesForUser(params.uuid);
return { error: null, attendances };
}

Expand Down
3 changes: 3 additions & 0 deletions api/validators/UserControllerRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export class UserPatches implements IUserPatches {
@Allow()
bio?: string;

@Allow()
isAttendancePublic?: boolean;

@Type(() => PasswordUpdate)
@ValidateNested()
@HasMatchingPasswords()
Expand Down
17 changes: 17 additions & 0 deletions migrations/0037-add-userAttendancePermission-to-userTable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';

const TABLE_NAME = 'Users';

export class AddUserAttendancePermissionToUserTable1691286073346 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(TABLE_NAME, new TableColumn({
name: 'isAttendancePublic',
type: 'boolean',
default: true,
}));
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn(TABLE_NAME, 'isAttendancePublic');
}
}
5 changes: 5 additions & 0 deletions models/UserModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ export class UserModel extends BaseEntity {
})
bio: string;

@Column('boolean', { default: true })
isAttendancePublic: boolean;

@Column('integer', { default: 0 })
@Index('leaderboard_index')
points: number;
Expand Down Expand Up @@ -126,6 +129,7 @@ export class UserModel extends BaseEntity {
major: this.major,
bio: this.bio,
points: this.points,
isAttendancePublic: this.isAttendancePublic,
};
if (this.userSocialMedia) {
publicProfile.userSocialMedia = this.userSocialMedia.map((sm) => sm.getPublicSocialMedia());
Expand All @@ -148,6 +152,7 @@ export class UserModel extends BaseEntity {
bio: this.bio,
points: this.points,
credits: this.credits,
isAttendancePublic: this.isAttendancePublic,
};
if (this.userSocialMedia) {
fullUserProfile.userSocialMedia = this.userSocialMedia.map((sm) => sm.getPublicSocialMedia());
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@acmucsd/membership-portal",
"version": "2.11.1",
"version": "2.12.0",
"description": "REST API for ACM UCSD's membership portal.",
"main": "index.d.ts",
"files": [
Expand Down
14 changes: 12 additions & 2 deletions services/AttendanceService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Service } from 'typedi';
import { InjectManager } from 'typeorm-typedi-extensions';
import { BadRequestError, NotFoundError } from 'routing-controllers';
import { BadRequestError, ForbiddenError, NotFoundError } from 'routing-controllers';
import { EntityManager } from 'typeorm';
import * as moment from 'moment';
import { ActivityType, PublicAttendance, Uuid } from '../types';
Expand All @@ -27,13 +27,23 @@ export default class AttendanceService {
return attendances.map((attendance) => attendance.getPublicAttendance());
}

public async getAttendancesForUser(user: UserModel): Promise<PublicAttendance[]> {
public async getAttendancesForCurrentUser(user: UserModel): Promise<PublicAttendance[]> {
const attendances = await this.transactions.readOnly(async (txn) => Repositories
.attendance(txn)
.getAttendancesForUser(user));
return attendances.map((attendance) => attendance.getPublicAttendance());
}

public async getAttendancesForUser(uuid: Uuid): Promise<PublicAttendance[]> {
return this.transactions.readOnly(async (txn) => {
const user = await Repositories.user(txn).findByUuid(uuid);
if (!user) throw new NotFoundError('User does not exist');
if (!user.isAttendancePublic) throw new ForbiddenError();
const attendances = await Repositories.attendance(txn).getAttendancesForUser(user);
return attendances.map((attendance) => attendance.getPublicAttendance());
});
}

public async attendEvent(user: UserModel, attendanceCode: string, asStaff = false): Promise<PublicAttendance> {
return this.transactions.readWrite(async (txn) => {
const event = await Repositories.event(txn).findByAttendanceCode(attendanceCode);
Expand Down
54 changes: 54 additions & 0 deletions tests/attendance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,4 +272,58 @@ describe('attendance', () => {
expect(attendance.user.uuid).toEqual(staff.uuid);
expect(attendance.event.uuid).toEqual(event.uuid);
});

test('get another user attendance by uuid', async () => {
const conn = await DatabaseConnection.get();
const member1 = UserFactory.fake();
const member2 = UserFactory.fake();
const event1 = EventFactory.fake({ requiresStaff: true });
const event2 = EventFactory.fake({ requiresStaff: true });

await new PortalState()
.createUsers(member1, member2)
.createEvents(event1, event2)
.attendEvents([member1], [event1, event2])
.write();

const attendanceController = ControllerFactory.attendance(conn);
const params = { uuid: member1.uuid };

// returns all attendances for uuid
const getAttendancesForUserUuid = await attendanceController.getAttendancesForUser(params, member2);
const attendancesForEvent = getAttendancesForUserUuid.attendances.map((a) => ({
user: a.user.uuid,
event: a.event.uuid,
asStaff: a.asStaff,
}));
const expectedAttendances = [
{ event: event1.uuid, user: member1.uuid, asStaff: false },
{ event: event2.uuid, user: member1.uuid, asStaff: false },
];
expect(attendancesForEvent).toEqual(expect.arrayContaining(expectedAttendances));
});

test('throws error when isAttendancePublic is false', async () => {
const conn = await DatabaseConnection.get();
const member1 = UserFactory.fake();
const member2 = UserFactory.fake();
const event1 = EventFactory.fake({ requiresStaff: true });
const event2 = EventFactory.fake({ requiresStaff: true });

await new PortalState()
.createUsers(member1, member2)
.createEvents(event1, event2)
.attendEvents([member1, member2], [event1, event2])
.write();

const attendanceController = ControllerFactory.attendance(conn);
const userController = ControllerFactory.user(conn);
const params = { uuid: member1.uuid };

const changePublicAttendancePatch = { user: { isAttendancePublic: false } };
await userController.patchCurrentUser(changePublicAttendancePatch, member1);

await expect(attendanceController.getAttendancesForUser(params, member2))
.rejects.toThrow(ForbiddenError);
});
});
1 change: 1 addition & 0 deletions tests/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ describe('account registration', () => {
uuid: registerResponse.user.uuid,
profilePicture: null,
userSocialMedia: [],
isAttendancePublic: true,
});

// check that email verification is sent
Expand Down
1 change: 1 addition & 0 deletions tests/data/UserFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export class UserFactory {
points: 0,
credits: 0,
handle: UserAccountService.generateDefaultHandle(firstName, lastName),
isAttendancePublic: true,
});
return UserModel.merge(fake, substitute);
}
Expand Down
1 change: 1 addition & 0 deletions types/ApiRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export interface UserPatches {
major?: string;
graduationYear?: number;
bio?: string;
isAttendancePublic?: boolean;
passwordChange?: PasswordUpdate;
}

Expand Down
1 change: 1 addition & 0 deletions types/ApiResponses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ export interface PublicProfile {
bio: string,
points: number,
userSocialMedia?: PublicUserSocialMedia[];
isAttendancePublic: boolean,
}

export interface PrivateProfile extends PublicProfile {
Expand Down

0 comments on commit 03a4c3b

Please sign in to comment.