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 10 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
17 changes: 13 additions & 4 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 @@ -40,9 +40,18 @@ export class EventRepository extends BaseRepository<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
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
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
Loading