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

109 agenda session registrations #125

Merged
merged 47 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
cac7d00
feat: registered sessions now are favorited. Added additional data to…
Mar 16, 2024
f336b78
refactor: changed session types and associated logic
Mar 16, 2024
af0d0bb
feat: added agenda and session registrations #109
Mar 16, 2024
09e306c
feat: added session detail (web view) #109
Mar 17, 2024
cf026cf
feat: added filter session by speakers #109
Mar 17, 2024
5df3e87
feat: added option to configurations to open/close session registrati…
Mar 17, 2024
2110de6
feat: added minimum time interval between sessions #109
Mar 17, 2024
926a08a
fix: removed block when session is full #109
Mar 17, 2024
829effb
fix: update session participant number after registration #109
Mar 17, 2024
b312d07
feat: added custom error messages to registrations #109
Mar 17, 2024
e41d8bf
feat: added guards for admins and users with spot
Mar 17, 2024
8db6bf5
fix: missing home tab for unregistered users
Mar 17, 2024
81deb83
feat: added social media links to users and speakers #78
Mar 17, 2024
7c6dc8d
fix: users with non admin permissions couldn't manage content #109
Mar 17, 2024
61490ba
feat: added organization management #111
Mar 17, 2024
703d73d
chore: fixed imports #111
Mar 17, 2024
7e099ad
feat: added venue management #111
Mar 17, 2024
f604ff8
fix: added missing translations #111
Mar 17, 2024
d0ea59a
fix: added missing html editor import #111
Mar 17, 2024
a163818
feat: added room management #111
Mar 17, 2024
9795599
feat: added speaker management #111
Mar 17, 2024
64bec70
feat: added content insertion to manage page #111
Mar 17, 2024
317d26e
fix: session error messages appear blank #109
Mar 17, 2024
cd07a01
fix: could not load full list of rooms #78
Mar 17, 2024
dfc0abd
fix: speakers not added to new and updated sessions #109
Mar 17, 2024
c05e985
feat: added session management #111
Mar 17, 2024
00e086e
chore: removed console.log #111
Mar 17, 2024
a4d012e
fix: missing attribute types on save session due to no load method #111
Mar 17, 2024
21d12e7
fix: refactored app guard and added admin override for admin guard
Mar 17, 2024
ba04ffa
feat: added date to session detail #109
Mar 17, 2024
24635eb
fix: missing obligatory dots and error highlighting #111
Mar 17, 2024
2e46b50
feat: added back-button to all detial pages #78
Mar 17, 2024
e6be87f
fix: speakers list is filtered by organization
Mar 17, 2024
e8c2441
fix: can't see ion-select data when editing entities #111
Mar 17, 2024
481e11e
fix: wrong link for sessions
Mar 18, 2024
48ed935
style: removed description from session card
Mar 18, 2024
2691ac6
fix: session description on detail not updating when changing #109
Mar 19, 2024
9fde6cb
feat: can't unfavorite registered sessions
Mar 19, 2024
fce7279
feat: added data migration script
Mar 19, 2024
687c072
style: added more info to session detail per request
Mar 19, 2024
8e0e887
feat: added links to speakers on session detail
Mar 19, 2024
1315094
style: when filtering sessions, detail becomes too small
Mar 19, 2024
1e85c5f
feat: added session page, link and open on mobile
Mar 19, 2024
8e85c18
refactor: improved registration for multiple concurrent requests
Mar 20, 2024
e978899
fix: favoriting/registering a session on mobile would open detail and…
Mar 20, 2024
870e186
fix: country leaders can't access manage page
Mar 20, 2024
44283c0
review changes #109 #111
Mar 20, 2024
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
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';
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: I'd rename this RC "sessionsRegistrations", not to be confused with users registrations

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() -
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the merge I can come back to this timezone logic and simplify it, similarly to how we do in the GA app

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