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

Update isUnusedAttendanceCode to allow any non-conflicting Event #426

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 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
14 changes: 14 additions & 0 deletions migrations/0044-remove-unique-attendance-code-constraint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

const TABLE_NAME = 'Events';
const CONSTRAINT_NAME = 'Events_attendanceCode_key';

export class RemoveUniqueAttendanceCodeConstraint1712188218208 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "${TABLE_NAME}" DROP CONSTRAINT "${CONSTRAINT_NAME}"`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "${TABLE_NAME}" ADD UNIQUE ("attendanceCode")`);
}
}
2 changes: 1 addition & 1 deletion models/EventModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export class EventModel extends BaseEntity {
end: Date;

@Column('varchar', { length: 255 })
@Index({ unique: true })
@Index({ unique: false })
attendanceCode: string;

@Column('integer')
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": "3.5.1",
"version": "3.5.2",
"description": "REST API for ACM UCSD's membership portal.",
"main": "index.d.ts",
"files": [
Expand Down
45 changes: 40 additions & 5 deletions repositories/EventRepository.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EntityRepository, SelectQueryBuilder } from 'typeorm';
import { EntityRepository, LessThanOrEqual, MoreThanOrEqual, SelectQueryBuilder } from 'typeorm';
import { EventSearchOptions, Uuid } from '../types';
import { EventModel } from '../models/EventModel';
import { BaseRepository } from './BaseRepository';
Expand Down Expand Up @@ -33,16 +33,51 @@ export class EventRepository extends BaseRepository<EventModel> {
}

public async findByAttendanceCode(attendanceCode: string): Promise<EventModel> {
return this.repository.findOne({ attendanceCode });
const allEvents = await this.repository.find({ attendanceCode });
nik-dange marked this conversation as resolved.
Show resolved Hide resolved

// Find all events with the given attendance code
const eligible = allEvents.filter((event) => !event.isTooEarlyToAttendEvent() && !event.isTooLateToAttendEvent());
nik-dange marked this conversation as resolved.
Show resolved Hide resolved

// If there are eligible events, return the first one
if (eligible.length > 0) {
return eligible[0];
}

// Otherwise, find the closest event to the current time
const currentTime = new Date();
let closestEvent = null;
let closestTimeDifference = Infinity;

allEvents.forEach((event) => {
const eventStartTime = new Date(event.start);
const timeDifference = Math.abs(eventStartTime.getTime() - currentTime.getTime());

// Update closest event if necessary
if (timeDifference < closestTimeDifference) {
closestEvent = event;
closestTimeDifference = timeDifference;
}
});

return closestEvent;
}

public async deleteEvent(event: EventModel): Promise<EventModel> {
return this.repository.remove(event);
}

public async isUnusedAttendanceCode(attendanceCode: string): Promise<boolean> {
const count = await this.repository.count({ attendanceCode });
return count === 0;
public async isAvailableAttendanceCode(attendanceCode: string, start: Date, end: Date): Promise<boolean> {
const hasOverlap = await this.repository.find({
where: [
{
attendanceCode,
start: LessThanOrEqual(end),
end: MoreThanOrEqual(start),
},
],
});

return hasOverlap.length === 0;
}

private getBaseEventSearchQuery(options: EventSearchOptions): SelectQueryBuilder<EventModel> {
Expand Down
1 change: 1 addition & 0 deletions services/AttendanceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export default class AttendanceService {
}

private validateEventToAttend(event: EventModel) {
console.log(event);
yimmyj marked this conversation as resolved.
Show resolved Hide resolved
if (!event) throw new NotFoundError('Oh no! That code didn\'t work.');
if (event.isTooEarlyToAttendEvent()) {
throw new UserError('This event hasn\'t started yet, please wait to check in.');
Expand Down
13 changes: 9 additions & 4 deletions services/EventService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ export default class EventService {
public async create(event: Event): Promise<PublicEvent> {
const eventCreated = await this.transactions.readWrite(async (txn) => {
const eventRepository = Repositories.event(txn);
const isUnusedAttendanceCode = await eventRepository.isUnusedAttendanceCode(event.attendanceCode);
if (!isUnusedAttendanceCode) throw new UserError('Attendance code has already been used');
const isAvailableAttendanceCode = await eventRepository.isAvailableAttendanceCode(event.attendanceCode,
event.start, event.end);
if (!isAvailableAttendanceCode) throw new UserError('There is a conflicting event with the same attendance code');
if (event.start > event.end) throw new UserError('Start date after end date');
return eventRepository.upsertEvent(EventModel.create(event));
});
Expand Down Expand Up @@ -68,8 +69,12 @@ export default class EventService {
const currentEvent = await eventRepository.findByUuid(uuid);
if (!currentEvent) throw new NotFoundError('Event not found');
if (changes.attendanceCode !== currentEvent.attendanceCode) {
const isUnusedAttendanceCode = await eventRepository.isUnusedAttendanceCode(changes.attendanceCode);
if (!isUnusedAttendanceCode) throw new UserError('Attendance code has already been used');
const isAvailableAttendanceCode = await eventRepository.isAvailableAttendanceCode(
changes.attendanceCode, changes.start, changes.end,
);
if (!isAvailableAttendanceCode) {
throw new UserError('There is a conflicting event with the same attendance code');
}
}
return eventRepository.upsertEvent(currentEvent, changes);
});
Expand Down
32 changes: 32 additions & 0 deletions tests/attendance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,38 @@ describe('attendance', () => {
expect(attendance.event.uuid).toEqual(event.uuid);
});

test('if there are two events with same attendance code, the user attends the right one', async () => {
const conn = await DatabaseConnection.get();
const member = UserFactory.fake();
const attendanceCode = 'samecode';
const event1 = EventFactory.fake({
attendanceCode,
start: moment().subtract(20, 'minutes').toDate(),
end: moment().add(2, 'hours').add(20, 'minutes').toDate(),
});
const event2 = EventFactory.fake({
attendanceCode,
start: moment().add(10, 'hours').toDate(),
end: moment().add(12, 'hours').add(20, 'minutes').toDate(),
});

await new PortalState()
.createUsers(member)
.createEvents(event1, event2)
.write();

// attend event
const attendanceController = ControllerFactory.attendance(conn);
const attendEventRequest = { attendanceCode };
await attendanceController.attendEvent(attendEventRequest, member);

// check attendances for user (event1 should be the attended one)
const getAttendancesForUserResponse = await attendanceController.getAttendancesForCurrentUser(member);
const attendance = getAttendancesForUserResponse.attendances[0];
expect(attendance.user.uuid).toEqual(member.uuid);
expect(attendance.event.uuid).toEqual(event1.uuid);
});

test('throws if invalid attendance code', async () => {
const conn = await DatabaseConnection.get();
const member = UserFactory.fake();
Expand Down
90 changes: 90 additions & 0 deletions tests/event.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,96 @@ describe('event creation', () => {
await expect(eventController.createEvent(createEventRequest, admin))
.rejects.toThrow('Start date after end date');
});

test('test non-conflicting event creation with re-used past attendance code', async () => {
const conn = await DatabaseConnection.get();
const admin = UserFactory.fake({ accessType: UserAccessType.ADMIN });
const user = UserFactory.fake();

await new PortalState()
.createUsers(admin, user)
.write();

const event = {
yimmyj marked this conversation as resolved.
Show resolved Hide resolved
cover: 'https://www.google.com',
title: 'ACM Party @ RIMAC',
description: 'Indoor Pool Party',
location: 'RIMAC',
committee: 'ACM',
start: new Date('2050-08-20T10:00:00.000Z'),
end: new Date('2050-08-20T12:00:00.000Z'),
attendanceCode: 'ferris',
pointValue: 10,
};

const createEventRequest: CreateEventRequest = {
event,
};

const eventController = ControllerFactory.event(conn);
const eventResponse = await eventController.createEvent(createEventRequest, admin);

expect(eventResponse.event.cover).toEqual(event.cover);
expect(eventResponse.event.title).toEqual(event.title);
expect(eventResponse.event.location).toEqual(event.location);
expect(eventResponse.event.committee).toEqual(event.committee);
expect(eventResponse.event.title).toEqual(event.title);
expect(eventResponse.event.start).toEqual(event.start);
expect(eventResponse.event.end).toEqual(event.end);
expect(eventResponse.event.pointValue).toEqual(event.pointValue);

const lookupEvent = await eventController.getOneEvent({ uuid: eventResponse.event.uuid }, user);
expect(lookupEvent.error).toEqual(null);
expect(JSON.stringify(lookupEvent.event)).toEqual(JSON.stringify(eventResponse.event));
});

test('test conflicting event creation with re-used attendance code', async () => {
const conn = await DatabaseConnection.get();
const admin = UserFactory.fake({ accessType: UserAccessType.ADMIN });
const user = UserFactory.fake();

await new PortalState()
.createUsers(admin, user)
.write();

let event = {
cover: 'https://www.google.com',
title: 'ACM Party @ RIMAC',
description: 'Indoor Pool Party',
location: 'RIMAC',
committee: 'ACM',
start: new Date('2050-08-20T10:00:00.000Z'),
end: new Date('2050-08-20T12:00:00.000Z'),
attendanceCode: 'repeated',
pointValue: 10,
};

const createEventRequest: CreateEventRequest = {
event,
};

const eventController = ControllerFactory.event(conn);
await eventController.createEvent(createEventRequest, admin);

event = {
yimmyj marked this conversation as resolved.
Show resolved Hide resolved
cover: 'https://www.google.com',
title: 'ACM Party @ RIMAC',
description: 'Indoor Pool Party',
location: 'RIMAC',
committee: 'ACM',
start: new Date('2050-08-20T09:00:00.000Z'),
end: new Date('2050-08-20T10:30:00.000Z'),
attendanceCode: 'repeated',
pointValue: 10,
};

const createEventRequest2: CreateEventRequest = {
event,
};

await expect(eventController.createEvent(createEventRequest2, admin))
.rejects.toThrow('There is a conflicting event with the same attendance code');
});
});

describe('event deletion', () => {
Expand Down