Skip to content

Commit

Permalink
Merge pull request #125 from uatisdeproblem/109-agenda-session-regist…
Browse files Browse the repository at this point in the history
…rations

109 agenda session registrations
  • Loading branch information
rbento1096 authored Mar 20, 2024
2 parents 4d8bcd1 + 44283c0 commit d20ea1e
Show file tree
Hide file tree
Showing 55 changed files with 2,421 additions and 170 deletions.
71 changes: 56 additions & 15 deletions back-end/src/handlers/registrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { DynamoDB, HandledError, ResourceController } from 'idea-aws';
import { Session } from '../models/session.model';
import { SessionRegistration } from '../models/sessionRegistration.model';
import { User } from '../models/user.model';
import { Configurations } from '../models/configurations.model';

///
/// CONSTANTS, ENVIRONMENT VARIABLES, HANDLER
Expand All @@ -15,7 +16,9 @@ import { User } from '../models/user.model';
const DDB_TABLES = {
users: process.env.DDB_TABLE_users,
sessions: process.env.DDB_TABLE_sessions,
registrations: process.env.DDB_TABLE_registrations
configurations: process.env.DDB_TABLE_configurations,
registrations: process.env.DDB_TABLE_registrations,
usersFavoriteSessions: process.env.DDB_TABLE_usersFavoriteSessions
};

const ddb = new DynamoDB();
Expand All @@ -28,19 +31,30 @@ export const handler = (ev: any, _: any, cb: any) => new SessionRegistrations(ev

class SessionRegistrations extends ResourceController {
user: User;
configurations: Configurations;
registration: SessionRegistration;

constructor(event: any, callback: any) {
super(event, callback, { resourceId: 'sessionId' });
}

protected async checkAuthBeforeRequest(): Promise<void> {


try {
this.user = new User(await ddb.get({ TableName: DDB_TABLES.users, Key: { userId: this.principalId } }));
} catch (err) {
throw new HandledError('User not found');
}

try {
this.configurations = new Configurations(
await ddb.get({ TableName: DDB_TABLES.configurations, Key: { PK: Configurations.PK } })
);
} catch (err) {
throw new HandledError('Configuration not found');
}

if (!this.resourceId || this.httpMethod === 'POST') return;

try {
Expand Down Expand Up @@ -72,13 +86,15 @@ class SessionRegistrations extends ResourceController {
}
}

protected async postResources(): Promise<any> {
// @todo configurations.canSignUpForSessions()
protected async postResource(): Promise<any> {
if (!this.configurations.areSessionRegistrationsOpen) throw new HandledError('Registrations are closed!')

this.registration = new SessionRegistration({
sessionId: this.resourceId,
userId: this.principalId,
registrationDateInMs: new Date().getTime()
userId: this.user.userId,
registrationDateInMs: new Date().getTime(),
name: this.user.getName(),
sectionCountry: this.user.sectionCountry
});

return await this.putSafeResource();
Expand All @@ -89,7 +105,7 @@ class SessionRegistrations extends ResourceController {
}

protected async deleteResource(): Promise<void> {
// @todo configurations.canSignUpForSessions()
if (!this.configurations.areSessionRegistrationsOpen) throw new HandledError('Registrations are closed!')

try {
const { sessionId, userId } = this.registration;
Expand All @@ -105,15 +121,25 @@ class SessionRegistrations extends ResourceController {
}
};

await ddb.transactWrites([{ Delete: deleteSessionRegistration }, { Update: updateSessionCount }]);
const removeFromFavorites = {
TableName: DDB_TABLES.usersFavoriteSessions,
Key: { userId: this.principalId, sessionId }
};

await ddb.transactWrites([
{ Delete: deleteSessionRegistration },
{ Delete: removeFromFavorites },
{ Update: updateSessionCount }
]);
} catch (err) {
throw new HandledError('Delete failed');
}
}

private async putSafeResource(): Promise<SessionRegistration> {
const { sessionId, userId } = this.registration;
const isValid = await this.validateRegistration(sessionId, userId);
const session: Session = new Session(await ddb.get({ TableName: DDB_TABLES.sessions, Key: { sessionId } }));
const isValid = await this.validateRegistration(session, userId);

if (!isValid) throw new HandledError("User can't sign up for this session!");

Expand All @@ -124,22 +150,31 @@ class SessionRegistrations extends ResourceController {
TableName: DDB_TABLES.sessions,
Key: { sessionId },
UpdateExpression: 'ADD numberOfParticipants :one',
ConditionExpression: 'numberOfParticipants < :limit',
ExpressionAttributeValues: {
':one': 1
':one': 1,
":limit": session.limitOfParticipants
}
};

await ddb.transactWrites([{ Put: putSessionRegistration }, { Update: updateSessionCount }]);
const addToFavorites = {
TableName: DDB_TABLES.usersFavoriteSessions,
Item: { userId: this.principalId, sessionId: this.resourceId }
}

await ddb.transactWrites([
{ Put: putSessionRegistration },
{ Put: addToFavorites },
{ Update: updateSessionCount }
]);

return this.registration;
} catch (err) {
throw new HandledError('Operation failed');
}
}

private async validateRegistration(sessionId: string, userId: string) {
const session: Session = new Session(await ddb.get({ TableName: DDB_TABLES.sessions, Key: { sessionId } }));

private async validateRegistration(session: Session, userId: string) {
if (!session.requiresRegistration) throw new HandledError("User can't sign up for this session!");
if (session.isFull()) throw new HandledError('Session is full! Refresh your page.');

Expand All @@ -159,8 +194,14 @@ class SessionRegistrations extends ResourceController {
const sessionStartDate = s.calcDatetimeWithoutTimezone(s.startsAt);
const sessionEndDate = s.calcDatetimeWithoutTimezone(s.endsAt);

const targetSessionStart = session.calcDatetimeWithoutTimezone(session.startsAt);
const targetSessionEnd = session.calcDatetimeWithoutTimezone(session.endsAt);
const targetSessionStart = session.calcDatetimeWithoutTimezone(
session.startsAt,
-1 * this.configurations.sessionRegistrationBuffer || 0
);
const targetSessionEnd = session.calcDatetimeWithoutTimezone(
session.endsAt,
this.configurations.sessionRegistrationBuffer || 0
);

// it's easier to prove a session is valid than it is to prove it's invalid. (1 vs 5 conditional checks)
return sessionStartDate >= targetSessionEnd || sessionEndDate <= targetSessionStart;
Expand Down
20 changes: 10 additions & 10 deletions back-end/src/handlers/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,20 +69,20 @@ class Sessions extends ResourceController {
return await this.putSafeResource();
}
private async putSafeResource(opts: { noOverwrite?: boolean } = {}): Promise<Session> {
const errors = this.session.validate();
if (errors.length) throw new HandledError(`Invalid fields: ${errors.join(', ')}`);

this.session.room = new RoomLinked(
await ddb.get({ TableName: DDB_TABLES.rooms, Key: { roomId: this.session.room.roomId } })
);

this.session.speakers = (
await ddb.batchGet(
DDB_TABLES.speakers,
this.session.speakers?.map(speakerId => ({ speakerId })),
true
)
).map(s => new SpeakerLinked(s));
const getSpeakers = await ddb.batchGet(
DDB_TABLES.speakers,
this.session.speakers?.map(s => ({ speakerId: s.speakerId })),
true
)

this.session.speakers = getSpeakers.map(s => new SpeakerLinked(s));

const errors = this.session.validate();
if (errors.length) throw new HandledError(`Invalid fields: ${errors.join(', ')}`);

try {
const putParams: any = { TableName: DDB_TABLES.sessions, Item: this.session };
Expand Down
16 changes: 16 additions & 0 deletions back-end/src/models/configurations.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { User } from '../models/user.model';
import { ServiceLanguages } from './serviceLanguages.enum';

export const LANGUAGES = new Languages({ default: ServiceLanguages.English, available: [ServiceLanguages.English] });
const DEFAULT_SESSION_REGISTRATION_BUFFER_MINUTES = 10;

export class Configurations extends Resource {
static PK = 'EGM';
Expand All @@ -21,6 +22,14 @@ export class Configurations extends Resource {
* Whether externals and guests can register.
*/
isRegistrationOpenForExternals: boolean;
/**
* Whether participants can register for sessions.
*/
areSessionRegistrationsOpen: boolean;
/**
* The minimum amount of time (in minutes) a user must leave open between sessions.
*/
sessionRegistrationBuffer: number;
/**
* Whether the delegation leaders can assign spots.
*/
Expand Down Expand Up @@ -55,6 +64,12 @@ export class Configurations extends Resource {
this.PK = Configurations.PK;
this.isRegistrationOpenForESNers = this.clean(x.isRegistrationOpenForESNers, Boolean);
this.isRegistrationOpenForExternals = this.clean(x.isRegistrationOpenForExternals, Boolean);
this.areSessionRegistrationsOpen = this.clean(x.areSessionRegistrationsOpen, Boolean);
this.sessionRegistrationBuffer = this.clean(
x.sessionRegistrationBuffer,
Number,
DEFAULT_SESSION_REGISTRATION_BUFFER_MINUTES
);
this.canCountryLeadersAssignSpots = this.clean(x.canCountryLeadersAssignSpots, Boolean);
this.registrationFormDef = new CustomBlockMeta(x.registrationFormDef, LANGUAGES);
this.currency = this.clean(x.currency, String);
Expand All @@ -76,6 +91,7 @@ export class Configurations extends Resource {
validate(): string[] {
const e = super.validate();
this.registrationFormDef.validate(LANGUAGES).forEach(ea => e.push(`registrationFormDef.${ea}`));
if (this.sessionRegistrationBuffer < 0) e.push('sessionRegistrationBuffer')
return e;
}

Expand Down
8 changes: 0 additions & 8 deletions back-end/src/models/organization.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,6 @@ export class Organization extends Resource {
* The organization's contact email.
*/
contactEmail: string;
/**
* A link to perform a contact action.
*/
contactAction: string; // @todo check this

load(x: any): void {
super.load(x);
Expand All @@ -38,7 +34,6 @@ export class Organization extends Resource {
this.description = this.clean(x.description, String);
this.website = this.clean(x.website, String);
this.contactEmail = this.clean(x.contactEmail, String);
this.contactAction = this.clean(x.contactAction, String);
}
safeLoad(newData: any, safeData: any): void {
super.safeLoad(newData, safeData);
Expand All @@ -47,13 +42,10 @@ export class Organization extends Resource {
validate(): string[] {
const e = super.validate();
if (isEmpty(this.name)) e.push('name');
if (this.website && isEmpty(this.website, 'url')) e.push('website');
if (this.contactEmail && isEmpty(this.contactEmail, 'email')) e.push('contactEmail');
return e;
}
}

// @todo check this
export class OrganizationLinked extends Resource {
organizationId: string;
name: string;
Expand Down
5 changes: 0 additions & 5 deletions back-end/src/models/room.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,6 @@ export class Room extends Resource {
* An URI for an image of the room.
*/
imageURI: string;
/**
* An URI for a plan of the room.
*/
planImageURI: string;

load(x: any): void {
super.load(x);
Expand All @@ -40,7 +36,6 @@ export class Room extends Resource {
this.internalLocation = this.clean(x.internalLocation, String);
this.description = this.clean(x.description, String);
this.imageURI = this.clean(x.imageURI, String);
this.planImageURI = this.clean(x.planImageURI, String);
}
safeLoad(newData: any, safeData: any): void {
super.safeLoad(newData, safeData);
Expand Down
63 changes: 25 additions & 38 deletions back-end/src/models/session.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { RoomLinked } from './room.model';
import { SpeakerLinked } from './speaker.model';

/**
* YYYY-MM-DDTHH:MM, without timezone. // @todo do we need this?
* YYYY-MM-DDTHH:MM, without timezone.
*/
type datetime = string;

Expand Down Expand Up @@ -76,9 +76,11 @@ export class Session extends Resource {
this.endsAt = this.calcDatetimeWithoutTimezone(endsAt);
this.room = typeof x.room === 'string' ? new RoomLinked({ roomId: x.room }) : new RoomLinked(x.room);
this.speakers = this.cleanArray(x.speakers, x => new SpeakerLinked(x));
this.numberOfParticipants = this.clean(x.numberOfParticipants, Number, 0);
this.limitOfParticipants = this.clean(x.limitOfParticipants, Number);
this.requiresRegistration = Object.keys(IndividualSessionType).includes(this.type);
this.requiresRegistration = this.type !== SessionType.COMMON;
if (this.requiresRegistration) {
this.numberOfParticipants = this.clean(x.numberOfParticipants, Number, 0);
this.limitOfParticipants = this.clean(x.limitOfParticipants, Number);
}
}
safeLoad(newData: any, safeData: any): void {
super.safeLoad(newData, safeData);
Expand All @@ -92,58 +94,43 @@ export class Session extends Resource {
if (isEmpty(this.durationMinutes)) e.push('durationMinutes');
if (!this.room.roomId) e.push('room');
if (!this.speakers?.length) e.push('speakers');
if (this.requiresRegistration && !this.limitOfParticipants) e.push('limitOfParticipants');
return e;
}

// @todo add a method to check if a user/speaker is in the session or not

calcDatetimeWithoutTimezone(dateToFormat: Date | string | number): datetime {
calcDatetimeWithoutTimezone(dateToFormat: Date | string | number, bufferInMinutes = 0): datetime {
const date = new Date(dateToFormat);
return new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().slice(0, 16);
return new Date(
date.getTime() -
this.convertMinutesToMilliseconds(date.getTimezoneOffset()) +
this.convertMinutesToMilliseconds(bufferInMinutes)
)
.toISOString()
.slice(0, 16);
}

convertMinutesToMilliseconds(minutes: number) {
return minutes * 60 * 1000;
}

isFull(): boolean {
return this.numberOfParticipants >= this.limitOfParticipants;
return this.requiresRegistration ? this.numberOfParticipants >= this.limitOfParticipants : false
}
}

// @todo don't have three enums...
// @todo check if any is missing or we need to add.
export enum CommonSessionType {
OPENING = 'OPENING',
KEYNOTE = 'KEYNOTE',
MORNING = 'MORNING',
POSTER = 'POSTER',
EXPO = 'EXPO',
CANDIDATES = 'CANDIDATES',
HARVESTING = 'HARVESTING',
CLOSING = 'CLOSING',
OTHER = 'OTHER'
getSpeakers(): string {
return this.speakers.map(s => s.name).join(', ')
}
}

export enum IndividualSessionType {
DISCUSSION = 'DISCUSSION',
TALK = 'TALK',
IGNITE = 'IGNITE',
CAMPFIRE = 'CAMPFIRE',
IDEAS = 'IDEAS',
INCUBATOR = 'INCUBATOR'
}

export enum SessionType {
OPENING = 'OPENING',
KEYNOTE = 'KEYNOTE',
MORNING = 'MORNING',
POSTER = 'POSTER',
EXPO = 'EXPO',
CANDIDATES = 'CANDIDATES',
HARVESTING = 'HARVESTING',
CLOSING = 'CLOSING',
DISCUSSION = 'DISCUSSION',
TALK = 'TALK',
IGNITE = 'IGNITE',
CAMPFIRE = 'CAMPFIRE',
IDEAS = 'IDEAS',
INCUBATOR = 'INCUBATOR',
OTHER = 'OTHER'
HUB = 'HUB',
COMMON = 'COMMON'
}
Loading

0 comments on commit d20ea1e

Please sign in to comment.