From 0bd8bf1e864cc47532f859fc84a25b26cdf97587 Mon Sep 17 00:00:00 2001
From: Matteo Carbone
Date: Sat, 23 Mar 2024 13:45:04 +0000
Subject: [PATCH 1/9] refactor: registrations and sessions RCs #128
---
back-end/src/handlers/registrations.ts | 158 +++++++++++--------------
back-end/src/handlers/sessions.ts | 37 ++----
2 files changed, 83 insertions(+), 112 deletions(-)
diff --git a/back-end/src/handlers/registrations.ts b/back-end/src/handlers/registrations.ts
index 83520f9..bbae0d6 100644
--- a/back-end/src/handlers/registrations.ts
+++ b/back-end/src/handlers/registrations.ts
@@ -23,13 +23,13 @@ const DDB_TABLES = {
const ddb = new DynamoDB();
-export const handler = (ev: any, _: any, cb: any) => new SessionRegistrations(ev, cb).handleRequest();
+export const handler = (ev: any, _: any, cb: any): Promise => new SessionRegistrationsRC(ev, cb).handleRequest();
///
/// RESOURCE CONTROLLER
///
-class SessionRegistrations extends ResourceController {
+class SessionRegistrationsRC extends ResourceController {
user: User;
configurations: Configurations;
registration: SessionRegistration;
@@ -39,10 +39,11 @@ class SessionRegistrations extends ResourceController {
}
protected async checkAuthBeforeRequest(): Promise {
-
+ const sessionId = this.resourceId;
+ const userId = this.principalId;
try {
- this.user = new User(await ddb.get({ TableName: DDB_TABLES.users, Key: { userId: this.principalId } }));
+ this.user = new User(await ddb.get({ TableName: DDB_TABLES.users, Key: { userId } }));
} catch (err) {
throw new HandledError('User not found');
}
@@ -55,14 +56,11 @@ class SessionRegistrations extends ResourceController {
throw new HandledError('Configuration not found');
}
- if (!this.resourceId || this.httpMethod === 'POST') return;
+ if (!sessionId || this.httpMethod === 'POST') return;
try {
this.registration = new SessionRegistration(
- await ddb.get({
- TableName: DDB_TABLES.registrations,
- Key: { sessionId: this.resourceId, userId: this.principalId }
- })
+ await ddb.get({ TableName: DDB_TABLES.registrations, Key: { sessionId, userId } })
);
} catch (err) {
throw new HandledError('Registration not found');
@@ -70,24 +68,12 @@ class SessionRegistrations extends ResourceController {
}
protected async getResources(): Promise {
- if (this.queryParams.sessionId) {
- try {
- const registrationsOfSession = await ddb.query({
- TableName: DDB_TABLES.registrations,
- KeyConditionExpression: 'sessionId = :sessionId',
- ExpressionAttributeValues: { ':sessionId': this.queryParams.sessionId }
- });
- return registrationsOfSession.map(s => new SessionRegistration(s));
- } catch (error) {
- throw new HandledError('Could not load registrations for this session');
- }
- } else {
- return await this.getUsersRegistrations(this.principalId);
- }
+ if (this.queryParams.sessionId) return this.getSessionRegistrations(this.queryParams.sessionId);
+ else return await this.getUsersRegistrations(this.principalId);
}
protected async postResource(): Promise {
- if (!this.configurations.areSessionRegistrationsOpen) throw new HandledError('Registrations are closed!')
+ if (!this.configurations.areSessionRegistrationsOpen) throw new HandledError('Registrations are closed!');
this.registration = new SessionRegistration({
sessionId: this.resourceId,
@@ -105,84 +91,70 @@ class SessionRegistrations extends ResourceController {
}
protected async deleteResource(): Promise {
- if (!this.configurations.areSessionRegistrationsOpen) throw new HandledError('Registrations are closed!')
+ if (!this.configurations.areSessionRegistrationsOpen) throw new HandledError('Registrations are closed!');
- try {
- const { sessionId, userId } = this.registration;
-
- const deleteSessionRegistration = { TableName: DDB_TABLES.registrations, Key: { sessionId, userId } };
-
- const updateSessionCount = {
- TableName: DDB_TABLES.sessions,
- Key: { sessionId },
- UpdateExpression: 'ADD numberOfParticipants :minusOne',
- ExpressionAttributeValues: {
- ':minusOne': -1
- }
- };
-
- 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');
- }
+ const { sessionId, userId } = this.registration;
+
+ const deleteSessionRegistration = { TableName: DDB_TABLES.registrations, Key: { sessionId, userId } };
+
+ const updateSessionCount = {
+ TableName: DDB_TABLES.sessions,
+ Key: { sessionId },
+ UpdateExpression: 'ADD numberOfParticipants :minusOne',
+ ExpressionAttributeValues: { ':minusOne': -1 }
+ };
+
+ const removeFromFavorites = {
+ TableName: DDB_TABLES.usersFavoriteSessions,
+ Key: { userId: this.principalId, sessionId }
+ };
+
+ await ddb.transactWrites([
+ { Delete: deleteSessionRegistration },
+ { Delete: removeFromFavorites },
+ { Update: updateSessionCount }
+ ]);
}
private async putSafeResource(): Promise {
const { sessionId, userId } = this.registration;
- const session: Session = new Session(await ddb.get({ TableName: DDB_TABLES.sessions, Key: { sessionId } }));
+ const 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!");
- try {
- const putSessionRegistration = { TableName: DDB_TABLES.registrations, Item: this.registration };
-
- const updateSessionCount = {
- TableName: DDB_TABLES.sessions,
- Key: { sessionId },
- UpdateExpression: 'ADD numberOfParticipants :one',
- ConditionExpression: 'numberOfParticipants < :limit',
- ExpressionAttributeValues: {
- ':one': 1,
- ":limit": session.limitOfParticipants
- }
- };
-
- 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');
- }
+ const putSessionRegistration = { TableName: DDB_TABLES.registrations, Item: this.registration };
+
+ const updateSessionCount = {
+ TableName: DDB_TABLES.sessions,
+ Key: { sessionId },
+ UpdateExpression: 'ADD numberOfParticipants :one',
+ ConditionExpression: 'numberOfParticipants < :limit',
+ ExpressionAttributeValues: { ':one': 1, ':limit': session.limitOfParticipants }
+ };
+
+ 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;
}
- private async validateRegistration(session: Session, userId: string) {
+ private async validateRegistration(session: Session, userId: string): Promise {
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.');
- const userRegistrations: SessionRegistration[] = await this.getUsersRegistrations(userId);
-
+ const userRegistrations = await this.getUsersRegistrations(userId);
if (!userRegistrations.length) return true;
- const sessions: Session[] = (
+ const sessions = (
await ddb.batchGet(
DDB_TABLES.sessions,
userRegistrations.map(ur => ({ sessionId: ur.sessionId }))
@@ -215,7 +187,7 @@ class SessionRegistrations extends ResourceController {
private async getUsersRegistrations(userId: string): Promise {
try {
- const registrationsOfUser = await ddb.query({
+ const registrationsOfUser: SessionRegistration[] = await ddb.query({
TableName: DDB_TABLES.registrations,
IndexName: 'userId-sessionId-index',
KeyConditionExpression: 'userId = :userId',
@@ -226,4 +198,16 @@ class SessionRegistrations extends ResourceController {
throw new HandledError('Could not load registrations for this user');
}
}
+ private async getSessionRegistrations(sessionId: string): Promise {
+ try {
+ const registrationsOfSession: SessionRegistration[] = await ddb.query({
+ TableName: DDB_TABLES.registrations,
+ KeyConditionExpression: 'sessionId = :sessionId',
+ ExpressionAttributeValues: { ':sessionId': sessionId }
+ });
+ return registrationsOfSession.map(s => new SessionRegistration(s));
+ } catch (error) {
+ throw new HandledError('Could not load registrations for this session');
+ }
+ }
}
diff --git a/back-end/src/handlers/sessions.ts b/back-end/src/handlers/sessions.ts
index bd84097..1002aa7 100644
--- a/back-end/src/handlers/sessions.ts
+++ b/back-end/src/handlers/sessions.ts
@@ -14,23 +14,21 @@ import { User } from '../models/user.model';
///
const PROJECT = process.env.PROJECT;
-
const DDB_TABLES = {
users: process.env.DDB_TABLE_users,
sessions: process.env.DDB_TABLE_sessions,
rooms: process.env.DDB_TABLE_rooms,
speakers: process.env.DDB_TABLE_speakers
};
-
const ddb = new DynamoDB();
-export const handler = (ev: any, _: any, cb: any) => new Sessions(ev, cb).handleRequest();
+export const handler = (ev: any, _: any, cb: any): Promise => new SessionsRC(ev, cb).handleRequest();
///
/// RESOURCE CONTROLLER
///
-class Sessions extends ResourceController {
+class SessionsRC extends ResourceController {
user: User;
session: Session;
@@ -73,12 +71,11 @@ class Sessions extends ResourceController {
await ddb.get({ TableName: DDB_TABLES.rooms, Key: { roomId: this.session.room.roomId } })
);
- const getSpeakers = await ddb.batchGet(
+ const getSpeakers: SpeakerLinked[] = 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();
@@ -98,11 +95,7 @@ class Sessions extends ResourceController {
protected async deleteResource(): Promise {
if (!this.user.permissions.canManageContents) throw new HandledError('Unauthorized');
- try {
- await ddb.delete({ TableName: DDB_TABLES.sessions, Key: { sessionId: this.resourceId } });
- } catch (err) {
- throw new HandledError('Delete failed');
- }
+ await ddb.delete({ TableName: DDB_TABLES.sessions, Key: { sessionId: this.resourceId } });
}
protected async postResources(): Promise {
@@ -115,20 +108,14 @@ class Sessions extends ResourceController {
}
protected async getResources(): Promise {
- try {
- const sessions = (await ddb.scan({ TableName: DDB_TABLES.sessions })).map((x: Session) => new Session(x));
-
- const filtertedSessions = sessions.filter(
- x =>
- (!this.queryParams.speaker || x.speakers.some(speaker => speaker.speakerId === this.queryParams.speaker)) &&
- (!this.queryParams.room || x.room.roomId === this.queryParams.room)
- );
+ const sessions = (await ddb.scan({ TableName: DDB_TABLES.sessions })).map(x => new Session(x));
- const sortedSessions = filtertedSessions.sort((a, b) => a.startsAt.localeCompare(b.startsAt));
+ const filtertedSessions = sessions.filter(
+ x =>
+ (!this.queryParams.speaker || x.speakers.some(speaker => speaker.speakerId === this.queryParams.speaker)) &&
+ (!this.queryParams.room || x.room.roomId === this.queryParams.room)
+ );
- return sortedSessions;
- } catch (err) {
- throw new HandledError('Operation failed');
- }
+ return filtertedSessions.sort((a, b): number => a.startsAt.localeCompare(b.startsAt));
}
}
From 803dcaebf72549da9356aa5ae8603b657f199115 Mon Sep 17 00:00:00 2001
From: Matteo Carbone
Date: Sat, 23 Mar 2024 13:48:13 +0000
Subject: [PATCH 2/9] refactor: prettier on save #128
---
.../src/app/tabs/manage/manage.page.html | 30 ++----------
.../src/app/tabs/sessions/session.page.html | 40 ++++++++--------
.../src/app/tabs/sessions/session.page.ts | 24 +++++-----
.../src/app/tabs/sessions/sessions.page.html | 46 ++++++++-----------
.../src/app/tabs/sessions/sessions.page.ts | 40 ++++++++--------
5 files changed, 73 insertions(+), 107 deletions(-)
diff --git a/front-end/src/app/tabs/manage/manage.page.html b/front-end/src/app/tabs/manage/manage.page.html
index 4074d03..0796d5a 100644
--- a/front-end/src/app/tabs/manage/manage.page.html
+++ b/front-end/src/app/tabs/manage/manage.page.html
@@ -40,43 +40,23 @@ {{ 'MANAGE.CONTENTS' | translate }}
{{ 'MANAGE.CONTENTS_I' | translate }}
-
+
{{ 'MANAGE.VENUES' | translate }}
-
+
{{ 'MANAGE.ROOMS' | translate }}
-
+
{{ 'MANAGE.ORGANIZATIONS' | translate }}
-
+
{{ 'MANAGE.SPEAKERS' | translate }}
-
+
{{ 'MANAGE.SESSIONS' | translate }}
diff --git a/front-end/src/app/tabs/sessions/session.page.html b/front-end/src/app/tabs/sessions/session.page.html
index a53275e..dd1dbab 100644
--- a/front-end/src/app/tabs/sessions/session.page.html
+++ b/front-end/src/app/tabs/sessions/session.page.html
@@ -1,25 +1,25 @@
-
-
-
-
-
-
-
-
-
+ *ngIf="session"
+ [session]="session"
+ [isSessionInFavorites]="isSessionInFavorites(session)"
+ [isUserRegisteredInSession]="isUserRegisteredInSession(session)"
+ (favorite)="toggleFavorite($event, session)"
+ (register)="toggleRegister($event, session)"
+ >
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/front-end/src/app/tabs/sessions/session.page.ts b/front-end/src/app/tabs/sessions/session.page.ts
index a99398c..b7fc961 100644
--- a/front-end/src/app/tabs/sessions/session.page.ts
+++ b/front-end/src/app/tabs/sessions/session.page.ts
@@ -17,7 +17,6 @@ import { ActivatedRoute } from '@angular/router';
styleUrls: ['./session.page.scss']
})
export class SessionPage implements OnInit {
-
session: Session;
favoriteSessionsIds: string[] = [];
registeredSessionsIds: string[] = [];
@@ -45,7 +44,7 @@ export class SessionPage implements OnInit {
// WARNING: do not pass any segment in order to get the favorites on the next api call.
// @todo improvable. Just amke a call to see if a session is or isn't favorited/registerd using a getById
const favoriteSessions = await this._sessions.getList({ force: true });
- this.favoriteSessionsIds = favoriteSessions.map( s => s.sessionId);
+ this.favoriteSessionsIds = favoriteSessions.map(s => s.sessionId);
this.registeredSessionsIds = (await this._sessions.loadUserRegisteredSessions()).map(ur => ur.sessionId);
} catch (error) {
this.message.error('COMMON.OPERATION_FAILED');
@@ -59,7 +58,7 @@ export class SessionPage implements OnInit {
}
async toggleFavorite(ev: any, session: Session): Promise {
- ev?.stopPropagation()
+ ev?.stopPropagation();
try {
await this.loading.show();
if (this.isSessionInFavorites(session)) {
@@ -68,7 +67,7 @@ export class SessionPage implements OnInit {
} else {
await this._sessions.addToFavorites(session.sessionId);
this.favoriteSessionsIds.push(session.sessionId);
- };
+ }
} catch (error) {
this.message.error('COMMON.OPERATION_FAILED');
} finally {
@@ -81,7 +80,7 @@ export class SessionPage implements OnInit {
}
async toggleRegister(ev: any, session: Session): Promise {
- ev?.stopPropagation()
+ ev?.stopPropagation();
try {
await this.loading.show();
if (this.isUserRegisteredInSession(session)) {
@@ -92,16 +91,16 @@ export class SessionPage implements OnInit {
await this._sessions.registerInSession(session.sessionId);
this.favoriteSessionsIds.push(session.sessionId);
this.registeredSessionsIds.push(session.sessionId);
- };
+ }
this.session = await this._sessions.getById(session.sessionId);
} catch (error) {
- if (error.message === "User can't sign up for this session!"){
+ if (error.message === "User can't sign up for this session!") {
this.message.error('SESSIONS.CANT_SIGN_UP');
- } else if (error.message === 'Registrations are closed!'){
+ } else if (error.message === 'Registrations are closed!') {
this.message.error('SESSIONS.REGISTRATION_CLOSED');
- } else if (error.message === 'Session is full! Refresh your page.'){
+ } else if (error.message === 'Session is full! Refresh your page.') {
this.message.error('SESSIONS.SESSION_FULL');
- } else if (error.message === 'You have 1 or more sessions during this time period.'){
+ } else if (error.message === 'You have 1 or more sessions during this time period.') {
this.message.error('SESSIONS.OVERLAP');
} else this.message.error('COMMON.OPERATION_FAILED');
} finally {
@@ -109,11 +108,10 @@ export class SessionPage implements OnInit {
}
}
-
async manageSession(): Promise {
if (!this.session) return;
- if (!this.app.user.permissions.canManageContents) return
+ if (!this.app.user.permissions.canManageContents) return;
const modal = await this.modalCtrl.create({
component: ManageSessionComponent,
@@ -130,4 +128,4 @@ export class SessionPage implements OnInit {
});
await modal.present();
}
-}
\ No newline at end of file
+}
diff --git a/front-end/src/app/tabs/sessions/sessions.page.html b/front-end/src/app/tabs/sessions/sessions.page.html
index 4578ca6..3f08778 100644
--- a/front-end/src/app/tabs/sessions/sessions.page.html
+++ b/front-end/src/app/tabs/sessions/sessions.page.html
@@ -1,8 +1,6 @@
-
- {{ 'SESSIONS.LIST' | translate }}
-
+ {{ 'SESSIONS.LIST' | translate }}
@@ -14,9 +12,7 @@
-
- {{ app.formatDateShort(day) }}
-
+ {{ app.formatDateShort(day) }}
@@ -48,24 +44,18 @@
-
-
- {{ session.room.name }} ({{ session.room.venue.name }})
-
+
+ {{ session.room.name }} ({{ session.room.venue.name }})
-
-
- {{ session.getSpeakers() }}
-
+
+ {{ session.getSpeakers() }}
- @if(session.requiresRegistration) {
- {{ session.isFull() ? this.t._('COMMON.FULL') : session.numberOfParticipants + '/' + session.limitOfParticipants }}
- } @else {
- {{ 'COMMON.OPEN' | translate }}
- }
+ @if(session.requiresRegistration) { {{ session.isFull() ? this.t._('COMMON.FULL') :
+ session.numberOfParticipants + '/' + session.limitOfParticipants }} } @else { {{ 'COMMON.OPEN' |
+ translate }} }
-
+
@@ -112,4 +102,4 @@
-
\ No newline at end of file
+
diff --git a/front-end/src/app/tabs/sessions/sessions.page.ts b/front-end/src/app/tabs/sessions/sessions.page.ts
index c091764..7307b53 100644
--- a/front-end/src/app/tabs/sessions/sessions.page.ts
+++ b/front-end/src/app/tabs/sessions/sessions.page.ts
@@ -19,14 +19,13 @@ export class SessionsPage {
@ViewChild(IonContent) content: IonContent;
@ViewChild(IonContent) searchbar: IonSearchbar;
-
- days: string[]
+ days: string[];
sessions: Session[];
favoriteSessionsIds: string[] = [];
registeredSessionsIds: string[] = [];
selectedSession: Session;
- segment = ''
+ segment = '';
constructor(
private modalCtrl: ModalController,
@@ -45,22 +44,22 @@ export class SessionsPage {
try {
await this.loading.show();
// WARNING: do not pass any segment in order to get the favorites on the next api call.
- this.segment = ''
+ this.segment = '';
this.sessions = await this._sessions.getList({ force: true });
- this.favoriteSessionsIds = this.sessions.map( s => s.sessionId);
+ this.favoriteSessionsIds = this.sessions.map(s => s.sessionId);
this.registeredSessionsIds = (await this._sessions.loadUserRegisteredSessions()).map(ur => ur.sessionId);
- this.days = await this._sessions.getSessionDays()
+ this.days = await this._sessions.getSessionDays();
} catch (error) {
this.message.error('COMMON.OPERATION_FAILED');
} finally {
this.loading.hide();
}
}
- changeSegment (segment: string, search = ''): void {
+ changeSegment(segment: string, search = ''): void {
this.selectedSession = null;
this.segment = segment;
this.filterSessions(search);
- };
+ }
async filterSessions(search = ''): Promise {
this.sessions = await this._sessions.getList({ search, segment: this.segment });
}
@@ -70,7 +69,7 @@ export class SessionsPage {
}
async toggleFavorite(ev: any, session: Session): Promise {
- ev?.stopPropagation()
+ ev?.stopPropagation();
try {
await this.loading.show();
if (this.isSessionInFavorites(session)) {
@@ -80,7 +79,7 @@ export class SessionsPage {
} else {
await this._sessions.addToFavorites(session.sessionId);
this.favoriteSessionsIds.push(session.sessionId);
- };
+ }
} catch (error) {
this.message.error('COMMON.OPERATION_FAILED');
} finally {
@@ -93,7 +92,7 @@ export class SessionsPage {
}
async toggleRegister(ev: any, session: Session): Promise {
- ev?.stopPropagation()
+ ev?.stopPropagation();
try {
await this.loading.show();
if (this.isUserRegisteredInSession(session)) {
@@ -105,17 +104,17 @@ export class SessionsPage {
await this._sessions.registerInSession(session.sessionId);
this.favoriteSessionsIds.push(session.sessionId);
this.registeredSessionsIds.push(session.sessionId);
- };
+ }
const updatedSession = await this._sessions.getById(session.sessionId);
session.numberOfParticipants = updatedSession.numberOfParticipants;
} catch (error) {
- if (error.message === "User can't sign up for this session!"){
+ if (error.message === "User can't sign up for this session!") {
this.message.error('SESSIONS.CANT_SIGN_UP');
- } else if (error.message === 'Registrations are closed!'){
+ } else if (error.message === 'Registrations are closed!') {
this.message.error('SESSIONS.REGISTRATION_CLOSED');
- } else if (error.message === 'Session is full! Refresh your page.'){
+ } else if (error.message === 'Session is full! Refresh your page.') {
this.message.error('SESSIONS.SESSION_FULL');
- } else if (error.message === 'You have 1 or more sessions during this time period.'){
+ } else if (error.message === 'You have 1 or more sessions during this time period.') {
this.message.error('SESSIONS.OVERLAP');
} else this.message.error('COMMON.OPERATION_FAILED');
} finally {
@@ -124,17 +123,16 @@ export class SessionsPage {
}
openDetail(ev: any, session: Session): void {
- ev?.stopPropagation()
+ ev?.stopPropagation();
if (this.app.isInMobileMode()) this.app.goToInTabs(['agenda', session.sessionId]);
else this.selectedSession = session;
}
-
async manageSession(): Promise {
if (!this.selectedSession) return;
- if (!this.app.user.permissions.canManageContents) return
+ if (!this.app.user.permissions.canManageContents) return;
const modal = await this.modalCtrl.create({
component: ManageSessionComponent,
@@ -147,9 +145,9 @@ export class SessionsPage {
} catch (error) {
// deleted
this.selectedSession = null;
- this.sessions = await this._sessions.getList({ force: true })
+ this.sessions = await this._sessions.getList({ force: true });
}
});
await modal.present();
}
-}
\ No newline at end of file
+}
From c93c1cb384a02bc7a16355cd91a24ed1ee86eb12 Mon Sep 17 00:00:00 2001
From: Matteo Carbone
Date: Sat, 23 Mar 2024 14:15:01 +0000
Subject: [PATCH 3/9] feat(sessionsRegistrations): download spreadsheet #128
---
back-end/src/handlers/registrations.ts | 53 ++++++++++++++++---
.../src/models/sessionRegistration.model.ts | 36 +++++++++++++
back-end/swagger.yaml | 5 ++
.../src/app/tabs/manage/manage.page.html | 8 +++
front-end/src/app/tabs/manage/manage.page.ts | 16 +++++-
.../sessionRegistrations.service.ts | 17 +++++-
.../src/app/tabs/sessions/session.page.html | 21 +++++---
.../src/app/tabs/sessions/session.page.ts | 13 +++++
.../src/app/tabs/sessions/sessions.page.html | 21 +++++---
.../src/app/tabs/sessions/sessions.page.ts | 16 ++++++
front-end/src/assets/i18n/en.json | 7 ++-
11 files changed, 189 insertions(+), 24 deletions(-)
diff --git a/back-end/src/handlers/registrations.ts b/back-end/src/handlers/registrations.ts
index bbae0d6..f9e197f 100644
--- a/back-end/src/handlers/registrations.ts
+++ b/back-end/src/handlers/registrations.ts
@@ -5,7 +5,7 @@
import { DynamoDB, HandledError, ResourceController } from 'idea-aws';
import { Session } from '../models/session.model';
-import { SessionRegistration } from '../models/sessionRegistration.model';
+import { SessionRegistration, SessionRegistrationExportable } from '../models/sessionRegistration.model';
import { User } from '../models/user.model';
import { Configurations } from '../models/configurations.model';
@@ -67,9 +67,16 @@ class SessionRegistrationsRC extends ResourceController {
}
}
- protected async getResources(): Promise {
- if (this.queryParams.sessionId) return this.getSessionRegistrations(this.queryParams.sessionId);
- else return await this.getUsersRegistrations(this.principalId);
+ protected async getResources(): Promise {
+ if (this.queryParams.sessionId) {
+ if (this.queryParams.export && this.user.permissions.canManageContents)
+ return await this.getExportableSessionRegistrations(this.queryParams.sessionId);
+ else return this.getRegistrationsOfSessionById(this.queryParams.sessionId);
+ } else {
+ if (this.queryParams.export && this.user.permissions.canManageContents)
+ return await this.getExportableSessionRegistrations();
+ else return await this.getRegistrationsOfUserById(this.principalId);
+ }
}
protected async postResource(): Promise {
@@ -118,7 +125,7 @@ class SessionRegistrationsRC extends ResourceController {
private async putSafeResource(): Promise {
const { sessionId, userId } = this.registration;
- const session = new Session(await ddb.get({ TableName: DDB_TABLES.sessions, Key: { sessionId } }));
+ const session = await this.getSessionById(sessionId);
const isValid = await this.validateRegistration(session, userId);
if (!isValid) throw new HandledError("User can't sign up for this session!");
@@ -151,7 +158,7 @@ class SessionRegistrationsRC extends ResourceController {
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.');
- const userRegistrations = await this.getUsersRegistrations(userId);
+ const userRegistrations = await this.getRegistrationsOfUserById(userId);
if (!userRegistrations.length) return true;
const sessions = (
@@ -185,7 +192,7 @@ class SessionRegistrationsRC extends ResourceController {
return true;
}
- private async getUsersRegistrations(userId: string): Promise {
+ private async getRegistrationsOfUserById(userId: string): Promise {
try {
const registrationsOfUser: SessionRegistration[] = await ddb.query({
TableName: DDB_TABLES.registrations,
@@ -198,7 +205,7 @@ class SessionRegistrationsRC extends ResourceController {
throw new HandledError('Could not load registrations for this user');
}
}
- private async getSessionRegistrations(sessionId: string): Promise {
+ private async getRegistrationsOfSessionById(sessionId: string): Promise {
try {
const registrationsOfSession: SessionRegistration[] = await ddb.query({
TableName: DDB_TABLES.registrations,
@@ -210,4 +217,34 @@ class SessionRegistrationsRC extends ResourceController {
throw new HandledError('Could not load registrations for this session');
}
}
+ private async getSessionById(sessionId: string): Promise {
+ try {
+ return new Session(await ddb.get({ TableName: DDB_TABLES.sessions, Key: { sessionId } }));
+ } catch (err) {
+ throw new HandledError('Session not found');
+ }
+ }
+ private async getExportableSessionRegistrations(sessionId?: string): Promise {
+ let sessions: Session[], registrations: SessionRegistration[];
+
+ if (sessionId) {
+ sessions = [await this.getSessionById(sessionId)];
+ registrations = await this.getRegistrationsOfSessionById(sessionId);
+ } else {
+ sessions = (await ddb.scan({ TableName: DDB_TABLES.sessions })).map(x => new Session(x));
+ registrations = (await ddb.scan({ TableName: DDB_TABLES.registrations })).map(x => new SessionRegistration(x));
+ }
+
+ const list: SessionRegistrationExportable[] = [];
+ sessions.map(session =>
+ list.push(
+ ...SessionRegistration.export(
+ session,
+ registrations.filter(r => r.sessionId === session.sessionId)
+ )
+ )
+ );
+
+ return list;
+ }
}
diff --git a/back-end/src/models/sessionRegistration.model.ts b/back-end/src/models/sessionRegistration.model.ts
index eb1a677..666a115 100644
--- a/back-end/src/models/sessionRegistration.model.ts
+++ b/back-end/src/models/sessionRegistration.model.ts
@@ -1,5 +1,7 @@
import { Resource } from 'idea-toolbox';
+import { Session } from './session.model';
+
export class SessionRegistration extends Resource {
/**
* The session ID.
@@ -30,4 +32,38 @@ export class SessionRegistration extends Resource {
this.name = this.clean(x.name, String);
if (x.sectionCountry) this.sectionCountry = this.clean(x.sectionCountry, String);
}
+
+ /**
+ * Get the exportable version of the list of registrations for a session.
+ */
+ static export(session: Session, registrations: SessionRegistration[]): SessionRegistrationExportable[] {
+ if (!registrations.length)
+ return [
+ {
+ Session: session.name,
+ Code: session.code ?? null,
+ Type: session.type,
+ Participant: null,
+ 'ESN Country': null
+ }
+ ];
+ return registrations.map(r => ({
+ Session: session.name,
+ Code: session.code ?? null,
+ Type: session.type,
+ Participant: r.name,
+ 'ESN Country': r.sectionCountry ?? null
+ }));
+ }
+}
+
+/**
+ * An exportable version of a session registration.
+ */
+export interface SessionRegistrationExportable {
+ Session: string;
+ Code: string | null;
+ Type: string;
+ Participant: string | null;
+ 'ESN Country': string | null;
}
diff --git a/back-end/swagger.yaml b/back-end/swagger.yaml
index 4aa3320..6b35cae 100644
--- a/back-end/swagger.yaml
+++ b/back-end/swagger.yaml
@@ -1060,6 +1060,11 @@ paths:
in: query
schema:
type: string
+ - name: export
+ in: query
+ description: If set, returns an exportable version of the registrations; it requires to be content manager
+ schema:
+ type: boolean
responses:
200:
$ref: '#/components/responses/Registrations'
diff --git a/front-end/src/app/tabs/manage/manage.page.html b/front-end/src/app/tabs/manage/manage.page.html
index 0796d5a..86ab1cf 100644
--- a/front-end/src/app/tabs/manage/manage.page.html
+++ b/front-end/src/app/tabs/manage/manage.page.html
@@ -59,6 +59,14 @@ {{ 'MANAGE.CONTENTS' | translate }}
{{ 'MANAGE.SESSIONS' | translate }}
+
+
+
diff --git a/front-end/src/app/tabs/manage/manage.page.ts b/front-end/src/app/tabs/manage/manage.page.ts
index aeeed4a..19b3943 100644
--- a/front-end/src/app/tabs/manage/manage.page.ts
+++ b/front-end/src/app/tabs/manage/manage.page.ts
@@ -1,6 +1,6 @@
import { Component } from '@angular/core';
import { ModalController } from '@ionic/angular';
-import { IDEALoadingService, IDEAMessageService } from '@idea-ionic/common';
+import { IDEALoadingService, IDEAMessageService, IDEATranslationsService } from '@idea-ionic/common';
import { EmailTemplateComponent } from './configurations/emailTemplate/emailTemplate.component';
import { ManageUsefulLinkStandaloneComponent } from '@app/common/usefulLinks/manageUsefulLink.component';
@@ -12,6 +12,7 @@ import { ManageSessionComponent } from '../sessions/manageSession.component';
import { AppService } from '@app/app.service';
import { UsefulLinksService } from '@app/common/usefulLinks/usefulLinks.service';
+import { SessionRegistrationsService } from '../sessionRegistrations/sessionRegistrations.service';
import { EmailTemplates, DocumentTemplates } from '@models/configurations.model';
import { UsefulLink } from '@models/usefulLink.model';
@@ -36,7 +37,9 @@ export class ManagePage {
private modalCtrl: ModalController,
private loading: IDEALoadingService,
private message: IDEAMessageService,
+ private t: IDEATranslationsService,
private _usefulLinks: UsefulLinksService,
+ private _sessionRegistrations: SessionRegistrationsService,
public app: AppService
) {}
async ionViewWillEnter(): Promise {
@@ -122,4 +125,15 @@ export class ManagePage {
});
await modal.present();
}
+ async downloadSessionsRegistrations(event?: Event): Promise {
+ if (event) event.stopPropagation();
+ try {
+ await this.loading.show();
+ await this._sessionRegistrations.downloadSpreadsheet(this.t._('SESSIONS.SESSION_REGISTRATIONS'));
+ } catch (error) {
+ this.message.error('COMMON.OPERATION_FAILED');
+ } finally {
+ this.loading.hide();
+ }
+ }
}
diff --git a/front-end/src/app/tabs/sessionRegistrations/sessionRegistrations.service.ts b/front-end/src/app/tabs/sessionRegistrations/sessionRegistrations.service.ts
index eb29a00..ab97994 100644
--- a/front-end/src/app/tabs/sessionRegistrations/sessionRegistrations.service.ts
+++ b/front-end/src/app/tabs/sessionRegistrations/sessionRegistrations.service.ts
@@ -1,7 +1,9 @@
import { Injectable } from '@angular/core';
+import { WorkBook, utils, writeFile } from 'xlsx';
import { IDEAApiService } from '@idea-ionic/common';
-import { SessionRegistration } from '@models/sessionRegistration.model';
+import { Session } from '@models/session.model';
+import { SessionRegistration, SessionRegistrationExportable } from '@models/sessionRegistration.model';
@Injectable({ providedIn: 'root' })
export class SessionRegistrationsService {
@@ -35,4 +37,17 @@ export class SessionRegistrationsService {
async delete(registration: SessionRegistration): Promise {
await this.api.deleteResource(['registrations', registration.sessionId]);
}
+
+ /**
+ * Download a spreadsheet containing the sessions registrations selected.
+ */
+ async downloadSpreadsheet(title: string, session?: Session): Promise {
+ const params: any = { export: true };
+ if (session) params.sessionId = session.sessionId;
+ const list: SessionRegistrationExportable[] = await this.api.getResource('registrations', { params });
+
+ const workbook: WorkBook = { SheetNames: [], Sheets: {}, Props: { Title: title } };
+ utils.book_append_sheet(workbook, utils.json_to_sheet(list), '1');
+ writeFile(workbook, title.concat('.xlsx'));
+ }
}
diff --git a/front-end/src/app/tabs/sessions/session.page.html b/front-end/src/app/tabs/sessions/session.page.html
index dd1dbab..48fcc9e 100644
--- a/front-end/src/app/tabs/sessions/session.page.html
+++ b/front-end/src/app/tabs/sessions/session.page.html
@@ -13,13 +13,22 @@
-
-
-
-
-
-
+ @if(session && app.user.permissions.canManageContents) {
+
+
+
+
+
+
+
+ }
diff --git a/front-end/src/app/tabs/sessions/session.page.ts b/front-end/src/app/tabs/sessions/session.page.ts
index b7fc961..36025ee 100644
--- a/front-end/src/app/tabs/sessions/session.page.ts
+++ b/front-end/src/app/tabs/sessions/session.page.ts
@@ -7,6 +7,7 @@ import { IDEALoadingService, IDEAMessageService, IDEATranslationsService } from
import { ManageSessionComponent } from './manageSession.component';
import { SessionsService } from './sessions.service';
+import { SessionRegistrationsService } from '../sessionRegistrations/sessionRegistrations.service';
import { Session } from '@models/session.model';
import { ActivatedRoute } from '@angular/router';
@@ -28,6 +29,7 @@ export class SessionPage implements OnInit {
private loading: IDEALoadingService,
private message: IDEAMessageService,
public _sessions: SessionsService,
+ private _sessionRegistrations: SessionRegistrationsService,
public t: IDEATranslationsService,
public app: AppService
) {}
@@ -128,4 +130,15 @@ export class SessionPage implements OnInit {
});
await modal.present();
}
+
+ async downloadSessionsRegistrations(): Promise {
+ try {
+ await this.loading.show();
+ await this._sessionRegistrations.downloadSpreadsheet(this.t._('SESSIONS.SESSION_REGISTRATIONS'), this.session);
+ } catch (error) {
+ this.message.error('COMMON.OPERATION_FAILED');
+ } finally {
+ this.loading.hide();
+ }
+ }
}
diff --git a/front-end/src/app/tabs/sessions/sessions.page.html b/front-end/src/app/tabs/sessions/sessions.page.html
index 3f08778..2b82b05 100644
--- a/front-end/src/app/tabs/sessions/sessions.page.html
+++ b/front-end/src/app/tabs/sessions/sessions.page.html
@@ -91,13 +91,22 @@
(favorite)="toggleFavorite($event, selectedSession)"
(register)="toggleRegister($event, selectedSession)"
>
-
-
-
-
-
-
+ @if(selectedSession && app.user.permissions.canManageContents) {
+
+
+
+
+
+
+
+ }
diff --git a/front-end/src/app/tabs/sessions/sessions.page.ts b/front-end/src/app/tabs/sessions/sessions.page.ts
index 7307b53..97a2acc 100644
--- a/front-end/src/app/tabs/sessions/sessions.page.ts
+++ b/front-end/src/app/tabs/sessions/sessions.page.ts
@@ -7,6 +7,7 @@ import { IDEALoadingService, IDEAMessageService, IDEATranslationsService } from
import { ManageSessionComponent } from './manageSession.component';
import { SessionsService } from './sessions.service';
+import { SessionRegistrationsService } from '../sessionRegistrations/sessionRegistrations.service';
import { Session } from '@models/session.model';
@@ -32,6 +33,7 @@ export class SessionsPage {
private loading: IDEALoadingService,
private message: IDEAMessageService,
public _sessions: SessionsService,
+ private _sessionRegistrations: SessionRegistrationsService,
public t: IDEATranslationsService,
public app: AppService
) {}
@@ -150,4 +152,18 @@ export class SessionsPage {
});
await modal.present();
}
+
+ async downloadSessionsRegistrations(): Promise {
+ try {
+ await this.loading.show();
+ await this._sessionRegistrations.downloadSpreadsheet(
+ this.t._('SESSIONS.SESSION_REGISTRATIONS'),
+ this.selectedSession
+ );
+ } catch (error) {
+ this.message.error('COMMON.OPERATION_FAILED');
+ } finally {
+ this.loading.hide();
+ }
+ }
}
diff --git a/front-end/src/assets/i18n/en.json b/front-end/src/assets/i18n/en.json
index e470221..ec6de7c 100644
--- a/front-end/src/assets/i18n/en.json
+++ b/front-end/src/assets/i18n/en.json
@@ -355,7 +355,8 @@
"OTHER_CONFIGURATIONS": "Other configurations",
"LIST_OF_ESN_COUNTRIES": "List of current ESN countries",
"USEFUL_LINKS": "Useful links",
- "USEFUL_LINKS_I": "Manage the links you want to make available to all users for quick access."
+ "USEFUL_LINKS_I": "Manage the links you want to make available to all users for quick access.",
+ "DOWNLOAD_SESSIONS_REGISTRATIONS": "Download a spreadsheet with all the sessions registrations"
},
"STRIPE": {
"BEFORE_YOU_PROCEED": "Before you proceed",
@@ -437,6 +438,8 @@
"SPEAKERS": "Speakers",
"NO_SPEAKERS": "No speakers yet...",
"ADD_SPEAKER": "Add speaker",
- "ROOM": "Room"
+ "ROOM": "Room",
+ "DOWNLOAD_REGISTRATIONS": "Download a spreadsheet with all the session registrations",
+ "SESSION_REGISTRATIONS": "Session registrations"
}
}
From ad42379e2bb22965649376d579bf265dab049172 Mon Sep 17 00:00:00 2001
From: Matteo Carbone
Date: Sun, 24 Mar 2024 10:51:12 +0000
Subject: [PATCH 4/9] refactor: prettier on save #81
---
back-end/src/models/user.model.ts | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/back-end/src/models/user.model.ts b/back-end/src/models/user.model.ts
index c4ea8b1..0a4a268 100644
--- a/back-end/src/models/user.model.ts
+++ b/back-end/src/models/user.model.ts
@@ -99,10 +99,10 @@ export class User extends Resource {
this.registrationForm = x.registrationForm ?? {};
if (x.registrationAt) this.registrationAt = this.clean(x.registrationAt, t => new Date(t).toISOString());
if (x.spot) this.spot = new EventSpotAttached(x.spot);
- this.socialMedia = {}
- if (x.socialMedia?.instagram) this.socialMedia.instagram = this.clean(x.socialMedia.instagram, String)
- if (x.socialMedia?.linkedIn) this.socialMedia.linkedIn = this.clean(x.socialMedia.linkedIn, String)
- if (x.socialMedia?.twitter) this.socialMedia.twitter = this.clean(x.socialMedia.twitter, String)
+ this.socialMedia = {};
+ if (x.socialMedia?.instagram) this.socialMedia.instagram = this.clean(x.socialMedia.instagram, String);
+ if (x.socialMedia?.linkedIn) this.socialMedia.linkedIn = this.clean(x.socialMedia.linkedIn, String);
+ if (x.socialMedia?.twitter) this.socialMedia.twitter = this.clean(x.socialMedia.twitter, String);
}
safeLoad(newData: any, safeData: any): void {
@@ -250,4 +250,4 @@ export interface SocialMedia {
instagram?: string;
linkedIn?: string;
twitter?: string;
-}
\ No newline at end of file
+}
From f087a6a7bac8d23b8abbb87a9af8320b84a045b1 Mon Sep 17 00:00:00 2001
From: Matteo Carbone
Date: Sun, 24 Mar 2024 14:20:57 +0000
Subject: [PATCH 5/9] feat(contests): init #81
---
back-end/deploy/main.ts | 6 +-
back-end/src/handlers/contests.ts | 164 +++++++++
back-end/src/models/contest.model.ts | 128 +++++++
back-end/src/models/user.model.ts | 9 +
back-end/swagger.yaml | 133 ++++++++
front-end/angular.json | 6 +-
front-end/package-lock.json | 29 +-
front-end/package.json | 1 +
.../src/app/common/datetimeWithTimezone.ts | 94 ++++++
.../src/app/tabs/contests/contest.page.ts | 225 ++++++++++++
.../src/app/tabs/contests/contests.module.ts | 10 +
.../src/app/tabs/contests/contests.page.ts | 94 ++++++
.../tabs/contests/contests.routing.module.ts | 16 +
.../src/app/tabs/contests/contests.service.ts | 102 ++++++
.../tabs/contests/manageContest.component.ts | 319 ++++++++++++++++++
.../src/app/tabs/manage/manage.page.html | 14 +-
front-end/src/app/tabs/manage/manage.page.ts | 11 +
front-end/src/app/tabs/menu/menu.page.html | 22 +-
front-end/src/app/tabs/tabs.routing.module.ts | 8 +-
front-end/src/assets/i18n/en.json | 35 +-
front-end/src/global.scss | 1 -
21 files changed, 1403 insertions(+), 24 deletions(-)
create mode 100644 back-end/src/handlers/contests.ts
create mode 100644 back-end/src/models/contest.model.ts
create mode 100644 front-end/src/app/common/datetimeWithTimezone.ts
create mode 100644 front-end/src/app/tabs/contests/contest.page.ts
create mode 100644 front-end/src/app/tabs/contests/contests.module.ts
create mode 100644 front-end/src/app/tabs/contests/contests.page.ts
create mode 100644 front-end/src/app/tabs/contests/contests.routing.module.ts
create mode 100644 front-end/src/app/tabs/contests/contests.service.ts
create mode 100644 front-end/src/app/tabs/contests/manageContest.component.ts
diff --git a/back-end/deploy/main.ts b/back-end/deploy/main.ts
index a42ba75..23546ed 100755
--- a/back-end/deploy/main.ts
+++ b/back-end/deploy/main.ts
@@ -34,7 +34,8 @@ const apiResources: ResourceController[] = [
{ name: 'speakers', paths: ['/speakers', '/speakers/{speakerId}'] },
{ name: 'sessions', paths: ['/sessions', '/sessions/{sessionId}'] },
{ name: 'registrations', paths: ['/registrations', '/registrations/{sessionId}'] },
- { name: 'connections', paths: ['/connections', '/connections/{connectionId}'] }
+ { name: 'connections', paths: ['/connections', '/connections/{connectionId}'] },
+ { name: 'contests', paths: ['/contests', '/contests/{contestId}'] }
];
const tables: { [tableName: string]: DDBTable } = {
@@ -115,6 +116,9 @@ const tables: { [tableName: string]: DDBTable } = {
projectionType: DDB.ProjectionType.ALL
}
]
+ },
+ contests: {
+ PK: { name: 'contestId', type: DDB.AttributeType.STRING }
}
};
diff --git a/back-end/src/handlers/contests.ts b/back-end/src/handlers/contests.ts
new file mode 100644
index 0000000..bd5f969
--- /dev/null
+++ b/back-end/src/handlers/contests.ts
@@ -0,0 +1,164 @@
+///
+/// IMPORTS
+///
+
+import { DynamoDB, HandledError, ResourceController } from 'idea-aws';
+
+import { Contest } from '../models/contest.model';
+import { User } from '../models/user.model';
+
+///
+/// CONSTANTS, ENVIRONMENT VARIABLES, HANDLER
+///
+
+const PROJECT = process.env.PROJECT;
+const DDB_TABLES = { users: process.env.DDB_TABLE_users, contests: process.env.DDB_TABLE_contests };
+const ddb = new DynamoDB();
+
+export const handler = (ev: any, _: any, cb: any): Promise => new ContestsRC(ev, cb).handleRequest();
+
+///
+/// RESOURCE CONTROLLER
+///
+
+class ContestsRC extends ResourceController {
+ user: User;
+ contest: Contest;
+
+ constructor(event: any, callback: any) {
+ super(event, callback, { resourceId: 'contestId' });
+ }
+
+ protected async checkAuthBeforeRequest(): Promise {
+ try {
+ this.user = new User(await ddb.get({ TableName: DDB_TABLES.users, Key: { userId: this.principalId } }));
+ } catch (err) {
+ throw new HandledError('User not found');
+ }
+
+ if (!this.resourceId) return;
+
+ try {
+ this.contest = new Contest(
+ await ddb.get({ TableName: DDB_TABLES.contests, Key: { contestId: this.resourceId } })
+ );
+ } catch (err) {
+ throw new HandledError('Contest not found');
+ }
+ }
+
+ protected async getResource(): Promise {
+ if (!this.user.permissions.canManageContents && !this.contest.publishedResults) delete this.contest.results;
+ return this.contest;
+ }
+
+ protected async putResource(): Promise {
+ if (!this.user.permissions.canManageContents) throw new HandledError('Unauthorized');
+
+ const oldResource = new Contest(this.contest);
+ this.contest.safeLoad(this.body, oldResource);
+
+ return await this.putSafeResource();
+ }
+ private async putSafeResource(opts: { noOverwrite?: boolean } = {}): Promise {
+ const errors = this.contest.validate();
+ if (errors.length) throw new HandledError(`Invalid fields: ${errors.join(', ')}`);
+
+ const putParams: any = { TableName: DDB_TABLES.contests, Item: this.contest };
+ if (opts.noOverwrite) putParams.ConditionExpression = 'attribute_not_exists(contestId)';
+ await ddb.put(putParams);
+
+ return this.contest;
+ }
+
+ protected async patchResource(): Promise {
+ switch (this.body.action) {
+ case 'VOTE':
+ return await this.userVote(this.body.candidate);
+ case 'PUBLISH_RESULTS':
+ return await this.publishResults();
+ default:
+ throw new HandledError('Unsupported action');
+ }
+ }
+ private async userVote(candidateName: string): Promise {
+ if (!this.contest.isVoteStarted() || this.contest.isVoteEnded()) throw new HandledError('Vote is not open');
+
+ if (this.user.isExternal()) throw new HandledError("Externals can't vote");
+ if (!this.user.spot?.paymentConfirmedAt) throw new HandledError("Can't vote without confirmed spot");
+
+ const candidateIndex = this.contest.candidates.findIndex(c => c.name === candidateName);
+ if (candidateIndex === -1) throw new HandledError('Candidate not found');
+
+ const candidateCountry = this.contest.candidates[candidateIndex].country;
+ if (candidateCountry && candidateCountry === this.user.sectionCountry)
+ throw new HandledError("Can't vote for your country");
+
+ const markUserContestVoted = {
+ TableName: DDB_TABLES.users,
+ Key: { userId: this.user.userId },
+ ConditionExpression: 'attribute_not_exists(votedInContests) OR NOT contains(votedInContests, :contestId)',
+ UpdateExpression: 'SET votedInContests = list_append(if_not_exists(votedInContests, :emptyArr), :contestList)',
+ ExpressionAttributeValues: {
+ ':contestId': this.contest.contestId,
+ ':contestList': [this.contest.contestId],
+ ':emptyArr': [] as string[]
+ }
+ };
+ const addUserVoteToContest = {
+ TableName: DDB_TABLES.contests,
+ Key: { contestId: this.contest.contestId },
+ UpdateExpression: `ADD results[${candidateIndex}] :one`,
+ ExpressionAttributeValues: { ':one': 1 }
+ };
+
+ await ddb.transactWrites([{ Update: markUserContestVoted }, { Update: addUserVoteToContest }]);
+ }
+ private async publishResults(): Promise {
+ if (this.contest.publishedResults) throw new HandledError('Already public');
+
+ if (!this.contest.voteEndsAt || new Date().toISOString() <= this.contest.voteEndsAt)
+ throw new HandledError('Vote is not done');
+
+ await ddb.update({
+ TableName: DDB_TABLES.contests,
+ Key: { contestId: this.contest.contestId },
+ UpdateExpression: 'SET publishedResults = :true',
+ ExpressionAttributeValues: { ':true': true }
+ });
+ }
+
+ protected async deleteResource(): Promise {
+ if (!this.user.permissions.canManageContents) throw new HandledError('Unauthorized');
+
+ await ddb.delete({ TableName: DDB_TABLES.contests, Key: { contestId: this.resourceId } });
+ }
+
+ protected async postResources(): Promise {
+ if (!this.user.permissions.canManageContents) throw new HandledError('Unauthorized');
+
+ this.contest = new Contest(this.body);
+ this.contest.contestId = await ddb.IUNID(PROJECT);
+ this.contest.createdAt = new Date().toISOString();
+ this.contest.enabled = false;
+ delete this.contest.voteEndsAt;
+ this.contest.results = [];
+ this.contest.candidates.forEach((): number => this.contest.results.push(0));
+ this.contest.publishedResults = false;
+
+ return await this.putSafeResource({ noOverwrite: true });
+ }
+
+ protected async getResources(): Promise {
+ let contests = (await ddb.scan({ TableName: DDB_TABLES.contests })).map(x => new Contest(x));
+
+ if (!this.user.permissions.canManageContents) {
+ contests = contests.filter(c => c.enabled);
+ contests.forEach(contest => {
+ if (contest.publishedResults) delete contest.results;
+ });
+ }
+
+ return contests.sort((a, b): number => b.createdAt.localeCompare(a.createdAt));
+ }
+}
diff --git a/back-end/src/models/contest.model.ts b/back-end/src/models/contest.model.ts
new file mode 100644
index 0000000..543db80
--- /dev/null
+++ b/back-end/src/models/contest.model.ts
@@ -0,0 +1,128 @@
+import { Resource, epochISOString } from 'idea-toolbox';
+
+/**
+ * A contest to which people can vote in.
+ */
+export class Contest extends Resource {
+ /**
+ * The ID of the contest.
+ */
+ contestId: string;
+ /**
+ * Timestamp of creation (for sorting).
+ */
+ createdAt: epochISOString;
+ /**
+ * Whether the contest is enabled and therefore shown in the menu.
+ */
+ enabled: boolean;
+ /**
+ * If set, the vote is active (users can vote) and ends at the configured timestamp.
+ */
+ voteEndsAt?: epochISOString;
+ /**
+ * Name of the contest.
+ */
+ name: string;
+ /**
+ * Description of the contest.
+ */
+ description: string;
+ /**
+ * The URI to the contest's main image.
+ */
+ imageURI: string;
+ /**
+ * The candidates of the contest (vote ballots).
+ */
+ candidates: ContestCandidate[];
+ /**
+ * The count of votes for each of the sorted candidates.
+ * Note: the order of the candidates list must not change after a vote is open.
+ * This attribute is not accessible to non-admin users until `publishedResults` is true.
+ */
+ results?: number[];
+ /**
+ * Whether the results are published and hence visible to any users.
+ */
+ publishedResults: boolean;
+
+ load(x: any): void {
+ super.load(x);
+ this.contestId = this.clean(x.contestId, String);
+ this.createdAt = this.clean(x.createdAt, t => new Date(t).toISOString(), new Date().toISOString());
+ this.enabled = this.clean(x.enabled, Boolean, false);
+ if (x.voteEndsAt) this.voteEndsAt = this.clean(x.voteEndsAt, t => new Date(t).toISOString());
+ else delete this.voteEndsAt;
+ this.name = this.clean(x.name, String);
+ this.description = this.clean(x.description, String);
+ this.imageURI = this.clean(x.imageURI, String);
+ this.candidates = this.cleanArray(x.candidates, c => new ContestCandidate(c));
+ this.results = [];
+ for (let i = 0; i < this.candidates.length; i++) this.results[i] = Number(x.results[i] ?? 0);
+ this.publishedResults = this.clean(x.publishedResults, Boolean, false);
+ }
+
+ safeLoad(newData: any, safeData: any): void {
+ super.safeLoad(newData, safeData);
+ this.contestId = safeData.contestId;
+ this.results = safeData.results;
+ if (safeData.isVoteStarted()) {
+ this.candidates = safeData.candidates;
+ }
+ }
+
+ validate(): string[] {
+ const e = super.validate();
+ if (this.iE(this.name)) e.push('name');
+ if (this.iE(this.candidates)) e.push('candidates');
+ this.candidates.forEach((c, index): void => c.validate().forEach(ea => e.push(`candidates[${index}].${ea}`)));
+ return e;
+ }
+
+ /**
+ * Whether the vote is started.
+ */
+ isVoteStarted(): boolean {
+ return !!this.voteEndsAt;
+ }
+ /**
+ * Whether the vote has started and ended.
+ */
+ isVoteEnded(): boolean {
+ return this.isVoteStarted() && new Date().toISOString() > this.voteEndsAt;
+ }
+}
+
+/**
+ * A candidate in a contest.
+ */
+export class ContestCandidate extends Resource {
+ /**
+ * The name of the candidate.
+ */
+ name: string;
+ /**
+ * An URL where to find more info about the candidate.
+ */
+ url: string;
+ /**
+ * The country of the candidate.
+ * This is particularly important beacuse, if set, users can't vote for candidates of their own countries.
+ */
+ country: string;
+
+ load(x: any): void {
+ super.load(x);
+ this.name = this.clean(x.name, String);
+ this.url = this.clean(x.url, String);
+ this.country = this.clean(x.country, String);
+ }
+
+ validate(): string[] {
+ const e = super.validate();
+ if (this.iE(this.name)) e.push('name');
+ if (this.url && this.iE(this.url, 'url')) e.push('url');
+ return e;
+ }
+}
diff --git a/back-end/src/models/user.model.ts b/back-end/src/models/user.model.ts
index 0a4a268..b5bcf7f 100644
--- a/back-end/src/models/user.model.ts
+++ b/back-end/src/models/user.model.ts
@@ -77,6 +77,11 @@ export class User extends Resource {
*/
socialMedia: SocialMedia;
+ /**
+ * The list of contests (IDs) the user voted in.
+ */
+ votedInContests: string[];
+
load(x: any): void {
super.load(x);
this.userId = this.clean(x.userId, String);
@@ -103,6 +108,8 @@ export class User extends Resource {
if (x.socialMedia?.instagram) this.socialMedia.instagram = this.clean(x.socialMedia.instagram, String);
if (x.socialMedia?.linkedIn) this.socialMedia.linkedIn = this.clean(x.socialMedia.linkedIn, String);
if (x.socialMedia?.twitter) this.socialMedia.twitter = this.clean(x.socialMedia.twitter, String);
+
+ this.votedInContests = this.cleanArray(x.votedInContests, String);
}
safeLoad(newData: any, safeData: any): void {
@@ -123,6 +130,8 @@ export class User extends Resource {
if (safeData.registrationForm) this.registrationForm = safeData.registrationForm;
if (safeData.registrationAt) this.registrationAt = safeData.registrationAt;
if (safeData.spot) this.spot = safeData.spot;
+
+ this.votedInContests = safeData.votedInContests;
}
validate(): string[] {
diff --git a/back-end/swagger.yaml b/back-end/swagger.yaml
index 6b35cae..788414c 100644
--- a/back-end/swagger.yaml
+++ b/back-end/swagger.yaml
@@ -46,6 +46,8 @@ tags:
description: The speakers of the event
- name: Sessions
description: The sessions of the event
+ - name: Contests
+ description: The contests of the event
paths:
/status:
@@ -1168,6 +1170,118 @@ paths:
$ref: '#/components/responses/OperationCompleted'
400:
$ref: '#/components/responses/BadParameters'
+ /contests:
+ get:
+ summary: Get the contests
+ tags: [Contests]
+ security:
+ - AuthFunction: []
+ responses:
+ 200:
+ $ref: '#/components/responses/Contests'
+ post:
+ summary: Insert a new contest
+ description: Requires to be content manager
+ tags: [Contests]
+ security:
+ - AuthFunction: []
+ requestBody:
+ required: true
+ description: Contest
+ content:
+ application/json:
+ schema:
+ type: object
+ responses:
+ 200:
+ $ref: '#/components/responses/Contest'
+ 400:
+ $ref: '#/components/responses/BadParameters'
+ /contests/{contestId}:
+ get:
+ summary: Get a contest
+ tags: [Contests]
+ security:
+ - AuthFunction: []
+ parameters:
+ - name: contestId
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ 200:
+ $ref: '#/components/responses/Contest'
+ put:
+ summary: Edit a contest
+ description: Requires to be content manager
+ tags: [Contests]
+ security:
+ - AuthFunction: []
+ parameters:
+ - name: contestId
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ required: true
+ description: Contest
+ content:
+ application/json:
+ schema:
+ type: object
+ responses:
+ 200:
+ $ref: '#/components/responses/Contest'
+ 400:
+ $ref: '#/components/responses/BadParameters'
+ patch:
+ summary: Actions on a contest
+ tags: [Contests]
+ security:
+ - AuthFunction: []
+ parameters:
+ - name: contestId
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ action:
+ type: string
+ enum: [VOTE, PUBLISH_RESULTS]
+ candidate:
+ type: string
+ description: (VOTE)
+ responses:
+ 200:
+ $ref: '#/components/responses/OperationCompleted'
+ 400:
+ $ref: '#/components/responses/BadParameters'
+ delete:
+ summary: Delete a contest
+ description: Requires to be content manager
+ tags: [Contests]
+ security:
+ - AuthFunction: []
+ parameters:
+ - name: contestId
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ 200:
+ $ref: '#/components/responses/Contest'
+ 400:
+ $ref: '#/components/responses/BadParameters'
components:
schemas:
@@ -1207,6 +1321,9 @@ components:
Registration:
type: object
additionalProperties: {}
+ Contest:
+ type: object
+ additionalProperties: {}
responses:
AppStatus:
@@ -1379,6 +1496,22 @@ components:
type: array
items:
$ref: '#/components/schemas/Registration'
+ Contest:
+ description: Contest
+ content:
+ application/json:
+ schema:
+ type: object
+ items:
+ $ref: '#/components/schemas/Contest'
+ Contests:
+ description: Contest[]
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Contest'
BadParameters:
description: Bad input parameters
content:
diff --git a/front-end/angular.json b/front-end/angular.json
index 07e3f55..7e2f31d 100644
--- a/front-end/angular.json
+++ b/front-end/angular.json
@@ -47,7 +47,11 @@
"js-cookie",
"qrcode",
"maplibre-gl",
- "docs-soap"
+ "docs-soap",
+ "date-fns/format/index.js",
+ "date-fns/_lib/getTimezoneOffsetInMilliseconds/index.js",
+ "date-fns/_lib/toInteger/index.js",
+ "date-fns/_lib/cloneObject/index.js"
]
},
"configurations": {
diff --git a/front-end/package-lock.json b/front-end/package-lock.json
index 66186b5..77bf0f4 100644
--- a/front-end/package-lock.json
+++ b/front-end/package-lock.json
@@ -28,6 +28,7 @@
"@ionic/storage-angular": "^4.0.0",
"@kolkov/angular-editor": "^3.0.0-beta.0",
"@swimlane/ngx-datatable": "^20.1.0",
+ "date-fns-tz": "^2.0.1",
"docs-soap": "^1.2.1",
"idea-toolbox": "^7.0.5",
"ionicons": "^7.2.2",
@@ -2323,7 +2324,6 @@
"version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz",
"integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==",
- "dev": true,
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@@ -6883,6 +6883,30 @@
"node": ">=4"
}
},
+ "node_modules/date-fns": {
+ "version": "2.30.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
+ "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
+ "peer": true,
+ "dependencies": {
+ "@babel/runtime": "^7.21.0"
+ },
+ "engines": {
+ "node": ">=0.11"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/date-fns"
+ }
+ },
+ "node_modules/date-fns-tz": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-2.0.1.tgz",
+ "integrity": "sha512-fJCG3Pwx8HUoLhkepdsP7Z5RsucUi+ZBOxyM5d0ZZ6c4SdYustq0VMmOu6Wf7bli+yS/Jwp91TOCqn9jMcVrUA==",
+ "peerDependencies": {
+ "date-fns": "2.x"
+ }
+ },
"node_modules/debounce": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
@@ -12543,8 +12567,7 @@
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
- "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
- "dev": true
+ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/regenerator-transform": {
"version": "0.15.2",
diff --git a/front-end/package.json b/front-end/package.json
index 08f1698..b9bc79e 100644
--- a/front-end/package.json
+++ b/front-end/package.json
@@ -31,6 +31,7 @@
"@ionic/storage-angular": "^4.0.0",
"@kolkov/angular-editor": "^3.0.0-beta.0",
"@swimlane/ngx-datatable": "^20.1.0",
+ "date-fns-tz": "^2.0.1",
"docs-soap": "^1.2.1",
"idea-toolbox": "^7.0.5",
"ionicons": "^7.2.2",
diff --git a/front-end/src/app/common/datetimeWithTimezone.ts b/front-end/src/app/common/datetimeWithTimezone.ts
new file mode 100644
index 0000000..5233182
--- /dev/null
+++ b/front-end/src/app/common/datetimeWithTimezone.ts
@@ -0,0 +1,94 @@
+import {
+ Component,
+ ElementRef,
+ EventEmitter,
+ Input,
+ OnChanges,
+ OnInit,
+ Output,
+ SimpleChanges,
+ ViewChild
+} from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { IonicModule } from '@ionic/angular';
+import { formatInTimeZone, zonedTimeToUtc } from 'date-fns-tz';
+import { epochISOString } from 'idea-toolbox';
+
+import { AppService } from '@app/app.service';
+
+@Component({
+ standalone: true,
+ imports: [CommonModule, FormsModule, IonicModule],
+ selector: 'app-datetime-timezone',
+ template: `
+
+ {{ label }} @if(obligatory) {}
+
+
+
+ `
+})
+export class DatetimeWithTimezoneStandaloneComponent implements OnInit, OnChanges {
+ /**
+ * The date to manage.
+ */
+ @Input() date: epochISOString;
+ @Output() dateChange = new EventEmitter();
+ /**
+ * The timezone to consider.
+ * Fallback to the default value set in the configurations.
+ */
+ @Input() timezone: string;
+ /**
+ * A label for the item.
+ */
+ @Input() label: string;
+ /**
+ * The color of the item.
+ */
+ @Input() color: string;
+ /**
+ * The lines attribute of the item.
+ */
+ @Input() lines: string;
+ /**
+ * Whether the component is disabled or editable.
+ */
+ @Input() disabled = false;
+ /**
+ * Whether the date is obligatory.
+ */
+ @Input() obligatory = false;
+
+ initialValue: epochISOString;
+
+ @ViewChild('dateTime') dateTime: ElementRef;
+
+ constructor(public app: AppService) {}
+ async ngOnInit(): Promise {
+ this.timezone = this.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
+ this.initialValue = this.utcToZonedTimeString(this.date);
+ }
+ ngOnChanges(changes: SimpleChanges): void {
+ // fix the date if the linked timezone changes
+ if (changes.timezone?.currentValue && this.dateTime) {
+ setTimeout((): void => {
+ this.dateChange.emit(this.zonedTimeStringToUTC(this.dateTime.nativeElement.value));
+ }, 100);
+ }
+ }
+
+ utcToZonedTimeString(isoString: epochISOString): string {
+ return formatInTimeZone(isoString, this.timezone, "yyyy-MM-dd'T'HH:mm");
+ }
+ zonedTimeStringToUTC(dateLocale: string): epochISOString {
+ return zonedTimeToUtc(new Date(dateLocale), this.timezone).toISOString();
+ }
+}
diff --git a/front-end/src/app/tabs/contests/contest.page.ts b/front-end/src/app/tabs/contests/contest.page.ts
new file mode 100644
index 0000000..1f37233
--- /dev/null
+++ b/front-end/src/app/tabs/contests/contest.page.ts
@@ -0,0 +1,225 @@
+import { Component, OnInit, inject } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { FormsModule } from '@angular/forms';
+import { CommonModule } from '@angular/common';
+import { AlertController, IonicModule, ModalController } from '@ionic/angular';
+import {
+ IDEALoadingService,
+ IDEAMessageService,
+ IDEATranslationsModule,
+ IDEATranslationsService
+} from '@idea-ionic/common';
+
+import { HTMLEditorComponent } from '@common/htmlEditor.component';
+import { ManageContestComponent } from './manageContest.component';
+
+import { AppService } from '@app/app.service';
+import { ContestsService } from './contests.service';
+
+import { Contest } from '@models/contest.model';
+
+@Component({
+ standalone: true,
+ imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule, HTMLEditorComponent],
+ selector: 'app-contest',
+ template: `
+
+
+
+
+
+
+
+ {{ 'CONTESTS.DETAILS' | translate }}
+ @if(_app.user.permissions.canManageContents) {
+
+
+
+
+
+ }
+
+
+
+ @if(contest) {
+
+
+
+
+ {{ contest.name }}
+
+ @if(contest.isVoteStarted()) { @if(contest.isVoteEnded()) { @if(contest.publishedResults) {
+ {{ 'CONTESTS.RESULTS' | translate }}
+ } @else {
+ {{ 'CONTESTS.VOTE_ENDED' | translate }}
+ } } @else {
+
+ {{ 'CONTESTS.VOTE_NOW_UNTIL' | translate : { deadline: (contest.voteEndsAt | dateLocale : 'short') } }}
+
+ } } @else {
+ {{ 'CONTESTS.VOTE_NOT_OPEN_YET' | translate }}
+ }
+
+
+
+ @if(contest.description) {
+
+ }
+
+
+
+ {{ 'CONTESTS.CANDIDATES' | translate }}
+
+
+
+ @for(candidate of contest.candidates; track candidate.name) {
+
+ @if(canUserVote()) {
+
+ }
+
+ {{ candidate.name }}
+ {{ candidate.country }}
+
+ @if(candidate.url) {
+
+
+
+ } @if(contest.publishedResults) { @if( isCandidateWinnerByIndex($index)) {
+
+ }
+
+ {{ contest.results[$index] ?? 0 }} {{ 'CONTESTS.VOTES' | translate | lowercase }}
+
+ }
+
+ }
+
+ @if(canUserVote()) {
+ {{ 'CONTESTS.VOTE_I' | translate }}
+
+ {{ 'CONTESTS.VOTE' | translate }}
+
+ } @if(userVoted()) {
+
+ {{ 'CONTESTS.YOU_ALREADY_VOTED' | translate }}
+
+ }
+
+
+
+
+ }
+
+ `,
+ styles: [
+ `
+ ion-card {
+ ion-img {
+ height: 300px;
+ object-fit: cover;
+ }
+ ion-card-header {
+ padding-bottom: 0;
+ }
+ }
+ ion-list-header ion-label b {
+ font-size: 1.2em;
+ font-weight: 500;
+ color: var(--ion-color-step-700);
+ }
+ `
+ ]
+})
+export class ContestPage implements OnInit {
+ contest: Contest;
+
+ private _route = inject(ActivatedRoute);
+ private _modal = inject(ModalController);
+ private _alert = inject(AlertController);
+ private _loading = inject(IDEALoadingService);
+ private _message = inject(IDEAMessageService);
+ private _t = inject(IDEATranslationsService);
+ private _contests = inject(ContestsService);
+ _app = inject(AppService);
+
+ voteForCandidate: string;
+
+ async ngOnInit(): Promise {
+ await this.loadData();
+ }
+
+ async loadData(): Promise {
+ try {
+ await this._loading.show();
+ const contestId = this._route.snapshot.paramMap.get('contestId');
+ this.contest = await this._contests.getById(contestId);
+ } catch (err) {
+ this._message.error('COMMON.NOT_FOUND');
+ } finally {
+ await this._loading.hide();
+ }
+ }
+
+ async manageContest(contest: Contest): Promise {
+ if (!this._app.user.permissions.canManageContents) return;
+
+ const modal = await this._modal.create({
+ component: ManageContestComponent,
+ componentProps: { contest },
+ backdropDismiss: false
+ });
+ modal.onDidDismiss().then(async (): Promise => {
+ this.contest = await this._contests.getById(contest.contestId);
+ });
+ await modal.present();
+ }
+
+ backToList(): void {
+ this._app.goToInTabs(['contests'], { back: true });
+ }
+
+ isCandidateWinnerByIndex(candidateIndex: number): boolean {
+ return this.contest.candidates.every(
+ (_, competitorIndex): boolean => this.contest.results[competitorIndex] <= this.contest.results[candidateIndex]
+ );
+ }
+ canUserVote(checkCountry?: string): boolean {
+ const voteOpen = this.contest.isVoteStarted() && !this.contest.isVoteEnded();
+ const canVoteCountry = !checkCountry || checkCountry === this._app.user.sectionCountry;
+ const hasConfirmedSpot = this._app.user.spot?.paymentConfirmedAt;
+ const isESNer = !this._app.user.isExternal();
+ return voteOpen && canVoteCountry && !this.userVoted() && hasConfirmedSpot && isESNer;
+ }
+ userVoted(): boolean {
+ return this._app.user.votedInContests.includes(this.contest.contestId);
+ }
+
+ async vote(): Promise {
+ const doVote = async (): Promise => {
+ try {
+ await this._loading.show();
+ await this._contests.vote(this.contest, this.voteForCandidate);
+ this._app.user.votedInContests.push(this.contest.contestId);
+ } catch (err) {
+ this._message.error('COMMON.OPERATION_FAILED');
+ } finally {
+ await this._loading.hide();
+ }
+ };
+
+ const header = this._t._('CONTESTS.YOU_ARE_VOTING');
+ const subHeader = this.voteForCandidate;
+ const message = this._t._('CONTESTS.VOTE_I');
+ const buttons = [
+ { text: this._t._('CONTESTS.NOT_NOW'), role: 'cancel' },
+ { text: this._t._('CONTESTS.VOTE'), handler: doVote }
+ ];
+ const alert = await this._alert.create({ header, subHeader, message, buttons });
+ alert.present();
+ }
+}
diff --git a/front-end/src/app/tabs/contests/contests.module.ts b/front-end/src/app/tabs/contests/contests.module.ts
new file mode 100644
index 0000000..6f48f08
--- /dev/null
+++ b/front-end/src/app/tabs/contests/contests.module.ts
@@ -0,0 +1,10 @@
+import { NgModule } from '@angular/core';
+
+import { ContestsRoutingModule } from './contests.routing.module';
+import { ContestsPage } from './contests.page';
+import { ContestPage } from './contest.page';
+
+@NgModule({
+ imports: [ContestsRoutingModule, ContestsPage, ContestPage]
+})
+export class ContestsModule {}
diff --git a/front-end/src/app/tabs/contests/contests.page.ts b/front-end/src/app/tabs/contests/contests.page.ts
new file mode 100644
index 0000000..8f00b1d
--- /dev/null
+++ b/front-end/src/app/tabs/contests/contests.page.ts
@@ -0,0 +1,94 @@
+import { Component, OnInit, inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { IonInfiniteScroll, IonicModule } from '@ionic/angular';
+import { IDEAMessageService, IDEATranslationsModule } from '@idea-ionic/common';
+
+import { AppService } from '@app/app.service';
+import { ContestsService } from './contests.service';
+
+import { Contest } from '@models/contest.model';
+
+@Component({
+ standalone: true,
+ imports: [CommonModule, IonicModule, IDEATranslationsModule],
+ selector: 'app-contests',
+ template: `
+
+ @if(_app.isInMobileMode()) {
+
+ {{ 'CONTESTS.LIST' | translate }}
+
+ }
+
+
+
+
+ @for(contest of contests; track contest.contestId) {
+
+ {{ contest.name }}
+ @if(contest.isVoteStarted()) { @if(contest.isVoteEnded()) { @if(contest.publishedResults) {
+ {{ 'CONTESTS.RESULTS' | translate }}
+ } @else {
+ {{ 'CONTESTS.VOTE_ENDED' | translate }}
+ } } @else {
+ {{ 'CONTESTS.VOTE_NOW' | translate }}
+ } }
+
+ } @empty { @if(contests) {
+
+ {{ 'COMMON.NO_ELEMENT_FOUND' | translate }}
+
+ } @else {
+
+
+
+ } }
+
+
+
+
+
+ `,
+ styles: [
+ `
+ ion-list {
+ padding: 0;
+ max-width: 500px;
+ margin: 0 auto;
+ }
+ `
+ ]
+})
+export class ContestsPage implements OnInit {
+ contests: Contest[];
+
+ private _message = inject(IDEAMessageService);
+ private _contests = inject(ContestsService);
+ _app = inject(AppService);
+
+ async ngOnInit(): Promise {
+ await this.loadData();
+ }
+
+ async loadData(): Promise {
+ try {
+ this.contests = await this._contests.getList({});
+ } catch (error) {
+ this._message.error('COMMON.OPERATION_FAILED');
+ }
+ }
+
+ async filterContests(search = '', scrollToNextPage?: IonInfiniteScroll): Promise {
+ let startPaginationAfterId = null;
+ if (scrollToNextPage && this.contests?.length)
+ startPaginationAfterId = this.contests[this.contests.length - 1].contestId;
+
+ this.contests = await this._contests.getList({ search, withPagination: true, startPaginationAfterId });
+
+ if (scrollToNextPage) setTimeout((): Promise => scrollToNextPage.complete(), 100);
+ }
+
+ selectContest(contest: Contest): void {
+ this._app.goToInTabs(['contests', contest.contestId]);
+ }
+}
diff --git a/front-end/src/app/tabs/contests/contests.routing.module.ts b/front-end/src/app/tabs/contests/contests.routing.module.ts
new file mode 100644
index 0000000..9bac7b2
--- /dev/null
+++ b/front-end/src/app/tabs/contests/contests.routing.module.ts
@@ -0,0 +1,16 @@
+import { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+
+import { ContestsPage } from './contests.page';
+import { ContestPage } from './contest.page';
+
+const routes: Routes = [
+ { path: '', component: ContestsPage },
+ { path: ':contestId', component: ContestPage }
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class ContestsRoutingModule {}
diff --git a/front-end/src/app/tabs/contests/contests.service.ts b/front-end/src/app/tabs/contests/contests.service.ts
new file mode 100644
index 0000000..71854ff
--- /dev/null
+++ b/front-end/src/app/tabs/contests/contests.service.ts
@@ -0,0 +1,102 @@
+import { Injectable } from '@angular/core';
+import { IDEAApiService } from '@idea-ionic/common';
+
+import { Contest } from '@models/contest.model';
+
+@Injectable({ providedIn: 'root' })
+export class ContestsService {
+ private contests: Contest[];
+
+ /**
+ * The number of contests to consider for the pagination, when active.
+ */
+ MAX_PAGE_SIZE = 24;
+
+ constructor(private api: IDEAApiService) {}
+
+ private async loadList(): Promise {
+ const contests: Contest[] = await this.api.getResource('contests');
+ this.contests = contests.map(c => new Contest(c));
+ }
+
+ /**
+ * Get (and optionally filter) the list of contests.
+ * Note: it can be paginated.
+ * Note: it's a slice of the array.
+ * Note: if venue id is passed, it will filter contests for that venue.
+ */
+ async getList(options: {
+ force?: boolean;
+ withPagination?: boolean;
+ startPaginationAfterId?: string;
+ search?: string;
+ }): Promise {
+ if (!this.contests || options.force) await this.loadList();
+ if (!this.contests) return null;
+
+ options.search = options.search ? String(options.search).toLowerCase() : '';
+
+ let filteredList = this.contests.slice();
+
+ if (options.search)
+ filteredList = filteredList.filter(x =>
+ options.search
+ .split(' ')
+ .every(searchTerm =>
+ [x.contestId, x.name, x.description, ...x.candidates.map(x => x.name)]
+ .filter(f => f)
+ .some(f => f.toLowerCase().includes(searchTerm))
+ )
+ );
+
+ if (options.withPagination && filteredList.length > this.MAX_PAGE_SIZE) {
+ let indexOfLastOfPreviousPage = 0;
+ if (options.startPaginationAfterId)
+ indexOfLastOfPreviousPage = filteredList.findIndex(x => x.contestId === options.startPaginationAfterId) || 0;
+ filteredList = filteredList.slice(0, indexOfLastOfPreviousPage + this.MAX_PAGE_SIZE);
+ }
+
+ return filteredList;
+ }
+
+ /**
+ * Get the full details of a contest by its id.
+ */
+ async getById(contestId: string): Promise {
+ return new Contest(await this.api.getResource(['contests', contestId]));
+ }
+
+ /**
+ * Insert a new contest.
+ */
+ async insert(contest: Contest): Promise {
+ return new Contest(await this.api.postResource(['contests'], { body: contest }));
+ }
+ /**
+ * Update an existing contest.
+ */
+ async update(contest: Contest): Promise {
+ return new Contest(await this.api.putResource(['contests', contest.contestId], { body: contest }));
+ }
+ /**
+ * Delete a contest.
+ */
+ async delete(contest: Contest): Promise {
+ await this.api.deleteResource(['contests', contest.contestId]);
+ }
+
+ /**
+ * Vote for a candidate in a contest.
+ */
+ async vote(contest: Contest, candidate: string): Promise {
+ const body = { action: 'VOTE', candidate };
+ await this.api.patchResource(['contests', contest.contestId], { body });
+ }
+ /**
+ * Publish the results of a contest.
+ */
+ async publishResults(contest: Contest): Promise {
+ const body = { action: 'PUBLISH_RESULTS' };
+ await this.api.patchResource(['contests', contest.contestId], { body });
+ }
+}
diff --git a/front-end/src/app/tabs/contests/manageContest.component.ts b/front-end/src/app/tabs/contests/manageContest.component.ts
new file mode 100644
index 0000000..9fbea0b
--- /dev/null
+++ b/front-end/src/app/tabs/contests/manageContest.component.ts
@@ -0,0 +1,319 @@
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { Component, Input, OnInit, inject } from '@angular/core';
+import { AlertController, IonicModule, ModalController } from '@ionic/angular';
+import {
+ IDEALoadingService,
+ IDEAMessageService,
+ IDEATranslationsModule,
+ IDEATranslationsService
+} from '@idea-ionic/common';
+
+import { HTMLEditorComponent } from '@common/htmlEditor.component';
+import { DatetimeWithTimezoneStandaloneComponent } from '@common/datetimeWithTimezone';
+
+import { AppService } from '@app/app.service';
+import { MediaService } from '@common/media.service';
+import { ContestsService } from './contests.service';
+
+import { Contest, ContestCandidate } from '@models/contest.model';
+
+@Component({
+ standalone: true,
+ imports: [
+ CommonModule,
+ FormsModule,
+ IonicModule,
+ IDEATranslationsModule,
+ HTMLEditorComponent,
+ DatetimeWithTimezoneStandaloneComponent
+ ],
+ selector: 'app-manage-contest',
+ template: `
+
+
+
+
+
+
+
+ {{ 'CONTESTS.MANAGE_CONTEST' | translate }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'CONTESTS.NAME' | translate }}
+
+
+
+
+ {{ 'CONTESTS.IMAGE_URI' | translate }}
+
+
+
+
+
+
+
+
+ {{ 'CONTESTS.OPTIONS' | translate }}
+
+
+
+
+ {{ 'CONTESTS.VISIBLE' | translate }}
+
+ @if(contest.enabled) { @if(!contest.voteEndsAt) {
+
+
+ {{ 'CONTESTS.OPEN_VOTE' | translate }}
+
+ } @else {
+
+
+
+
+
+ } }
+
+
+ {{ 'CONTESTS.CANDIDATES' | translate }}
+ {{ 'CONTESTS.CANDIDATES_I' | translate }}
+
+ @if(!contest.isVoteStarted()){
+
+
+
+ }
+
+ @for(candidate of contest.candidates; track $index) {
+
+
+
+ {{ 'CONTESTS.CANDIDATE_NAME' | translate }}
+
+
+
+
+ {{ 'CONTESTS.CANDIDATE_URL' | translate }}
+
+
+
+
+ @for(country of _app.configurations.sectionCountries; track $index) {
+ {{ country }}
+ }
+
+
+ @if(!contest.isVoteStarted()) {
+
+
+
+ }
+
+ } @empty {
+
+ {{ 'COMMON.NO_ELEMENTS' | translate }}
+
+ }
+
+
+ {{ 'CONTESTS.DESCRIPTION' | translate }}
+
+
+
+ @if(contest.isVoteEnded()) {
+
+
+ {{ 'CONTESTS.RESULTS' | translate }}
+
+
+ @for(candidate of contest.candidates; track candidate.name) {
+
+ {{ candidate.name }}
+ @if(isCandidateWinnerByIndex($index)) {
+
+ }
+
+ {{ contest.results[$index] ?? 0 }} {{ 'CONTESTS.VOTES' | translate | lowercase }}
+
+
+ } } @if(contest.contestId) {
+
+ @if(contest.isVoteEnded() && !contest.publishedResults) {
+
+
+ {{ 'CONTESTS.PUBLISH_RESULTS' | translate }}
+
+
+ }
+
+ {{ 'COMMON.DELETE' | translate }}
+
+
+ }
+
+
+ `
+})
+export class ManageContestComponent implements OnInit {
+ /**
+ * The contest to manage.
+ */
+ @Input() contest: Contest;
+
+ errors = new Set();
+
+ timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+
+ private _modal = inject(ModalController);
+ private _alert = inject(AlertController);
+ private _t = inject(IDEATranslationsService);
+ private _loading = inject(IDEALoadingService);
+ private _message = inject(IDEAMessageService);
+ private _media = inject(MediaService);
+ private _contests = inject(ContestsService);
+ _app = inject(AppService);
+
+ async ngOnInit(): Promise {
+ this.contest = new Contest(this.contest);
+ }
+
+ setVoteDeadline(remove = false): void {
+ if (remove) delete this.contest.voteEndsAt;
+ else {
+ const oneWeekAhead = new Date();
+ oneWeekAhead.setDate(oneWeekAhead.getDate() + 7);
+ this.contest.voteEndsAt = oneWeekAhead.toISOString();
+ }
+ }
+
+ hasFieldAnError(field: string): boolean {
+ return this.errors.has(field);
+ }
+
+ async uploadImage({ target }): Promise {
+ const file = target.files[0];
+ if (!file) return;
+
+ try {
+ await this._loading.show();
+ const imageURI = await this._media.uploadImage(file);
+ await sleepForNumSeconds(3);
+ this.contest.imageURI = imageURI;
+ } catch (error) {
+ this._message.error('COMMON.OPERATION_FAILED');
+ } finally {
+ if (target) target.value = '';
+ this._loading.hide();
+ }
+ }
+
+ async save(): Promise {
+ this.errors = new Set(this.contest.validate());
+ if (this.errors.size) return this._message.error('COMMON.FORM_HAS_ERROR_TO_CHECK');
+
+ try {
+ await this._loading.show();
+ let result: Contest;
+ if (!this.contest.contestId) result = await this._contests.insert(this.contest);
+ else result = await this._contests.update(this.contest);
+ this.contest.load(result);
+ this._message.success('COMMON.OPERATION_COMPLETED');
+ this.close();
+ } catch (err) {
+ this._message.error('COMMON.OPERATION_FAILED');
+ } finally {
+ this._loading.hide();
+ }
+ }
+ close(): void {
+ this._modal.dismiss();
+ }
+
+ async askAndDelete(): Promise {
+ const doDelete = async (): Promise => {
+ try {
+ await this._loading.show();
+ await this._contests.delete(this.contest);
+ this._message.success('COMMON.OPERATION_COMPLETED');
+ this.close();
+ this._app.goToInTabs(['contests']);
+ } catch (error) {
+ this._message.error('COMMON.OPERATION_FAILED');
+ } finally {
+ this._loading.hide();
+ }
+ };
+ const header = this._t._('COMMON.ARE_YOU_SURE');
+ const message = this._t._('COMMON.ACTION_IS_IRREVERSIBLE');
+ const buttons = [
+ { text: this._t._('COMMON.CANCEL'), role: 'cancel' },
+ { text: this._t._('COMMON.DELETE'), role: 'destructive', handler: doDelete }
+ ];
+ const alert = await this._alert.create({ header, message, buttons });
+ alert.present();
+ }
+
+ addCandidate(): void {
+ this.contest.candidates.push(new ContestCandidate());
+ }
+ removeCandidate(candidate: ContestCandidate): void {
+ this.contest.candidates.splice(this.contest.candidates.indexOf(candidate), 1);
+ }
+
+ isCandidateWinnerByIndex(candidateIndex: number): boolean {
+ return this.contest.candidates.every(
+ (_, competitorIndex): boolean => this.contest.results[competitorIndex] <= this.contest.results[candidateIndex]
+ );
+ }
+
+ async publishResults(): Promise {
+ const doPublish = async (): Promise => {
+ try {
+ await this._loading.show();
+ await this._contests.publishResults(this.contest);
+ this.contest.publishedResults = true;
+ this.close();
+ } catch (err) {
+ this._message.error('COMMON.OPERATION_FAILED');
+ } finally {
+ await this._loading.hide();
+ }
+ };
+
+ const header = this._t._('CONTESTS.PUBLISH_RESULTS');
+ const subHeader = this._t._('COMMON.ARE_YOU_SURE');
+ const buttons = [
+ { text: this._t._('COMMON.CANCEL'), role: 'cancel' },
+ { text: this._t._('COMMON.CONFIRM'), handler: doPublish }
+ ];
+ const alert = await this._alert.create({ header, subHeader, buttons });
+ alert.present();
+ }
+}
+
+const sleepForNumSeconds = (numSeconds = 1): Promise =>
+ new Promise(resolve => setTimeout((): void => resolve(null), 1000 * numSeconds));
diff --git a/front-end/src/app/tabs/manage/manage.page.html b/front-end/src/app/tabs/manage/manage.page.html
index 86ab1cf..217f3ad 100644
--- a/front-end/src/app/tabs/manage/manage.page.html
+++ b/front-end/src/app/tabs/manage/manage.page.html
@@ -41,23 +41,23 @@ {{ 'MANAGE.CONTENTS' | translate }}
-
+
{{ 'MANAGE.VENUES' | translate }}
-
+
{{ 'MANAGE.ROOMS' | translate }}
-
+
{{ 'MANAGE.ORGANIZATIONS' | translate }}
-
+
{{ 'MANAGE.SPEAKERS' | translate }}
-
+
{{ 'MANAGE.SESSIONS' | translate }}
{{ 'MANAGE.CONTENTS' | translate }}
+
+
+ {{ 'MANAGE.CONTESTS' | translate }}
+
diff --git a/front-end/src/app/tabs/manage/manage.page.ts b/front-end/src/app/tabs/manage/manage.page.ts
index 19b3943..d8812fb 100644
--- a/front-end/src/app/tabs/manage/manage.page.ts
+++ b/front-end/src/app/tabs/manage/manage.page.ts
@@ -9,6 +9,7 @@ import { ManageSpeakerComponent } from '../speakers/manageSpeaker.component';
import { ManageVenueComponent } from '../venues/manageVenue.component';
import { ManageRoomComponent } from '../rooms/manageRooms.component';
import { ManageSessionComponent } from '../sessions/manageSession.component';
+import { ManageContestComponent } from '../contests/manageContest.component';
import { AppService } from '@app/app.service';
import { UsefulLinksService } from '@app/common/usefulLinks/usefulLinks.service';
@@ -21,6 +22,7 @@ import { Venue } from '@models/venue.model';
import { Speaker } from '@models/speaker.model';
import { Room } from '@models/room.model';
import { Session } from '@models/session.model';
+import { Contest } from '@models/contest.model';
@Component({
selector: 'manage',
@@ -136,4 +138,13 @@ export class ManagePage {
this.loading.hide();
}
}
+
+ async addContest(): Promise {
+ const modal = await this.modalCtrl.create({
+ component: ManageContestComponent,
+ componentProps: { contest: new Contest() },
+ backdropDismiss: false
+ });
+ await modal.present();
+ }
}
diff --git a/front-end/src/app/tabs/menu/menu.page.html b/front-end/src/app/tabs/menu/menu.page.html
index 959511f..12d7745 100644
--- a/front-end/src/app/tabs/menu/menu.page.html
+++ b/front-end/src/app/tabs/menu/menu.page.html
@@ -1,34 +1,36 @@
-
- {{ 'TABS.MENU' | translate }}
-
+ {{ 'TABS.MENU' | translate }}
-
+
{{ 'MENU.PAGES' | translate }}
-
+
{{ 'MENU.HOME' | translate }}
-
+
{{ 'MENU.AGENDA' | translate }}
-
+
{{ 'MENU.VENUES' | translate }}
-
+
{{ 'MENU.ORGANIZATIONS' | translate }}
-
+
{{ 'MENU.SPEAKERS' | translate }}
+
+
+ {{ 'MENU.CONTESTS' | translate }}
+
-
\ No newline at end of file
+
diff --git a/front-end/src/app/tabs/tabs.routing.module.ts b/front-end/src/app/tabs/tabs.routing.module.ts
index 91e15d1..186cded 100644
--- a/front-end/src/app/tabs/tabs.routing.module.ts
+++ b/front-end/src/app/tabs/tabs.routing.module.ts
@@ -42,7 +42,8 @@ const routes: Routes = [
},
{
path: 'organizations',
- loadChildren: (): Promise => import('./organizations/organizations.module').then(m => m.OrganizationsModule),
+ loadChildren: (): Promise =>
+ import('./organizations/organizations.module').then(m => m.OrganizationsModule),
canActivate: [spotGuard]
},
{
@@ -54,6 +55,11 @@ const routes: Routes = [
path: 'agenda',
loadChildren: (): Promise => import('./sessions/sessions.module').then(m => m.SessionsModule),
canActivate: [spotGuard]
+ },
+ {
+ path: 'contests',
+ loadChildren: (): Promise => import('./contests/contests.module').then(m => m.ContestsModule),
+ canActivate: [spotGuard]
}
]
}
diff --git a/front-end/src/assets/i18n/en.json b/front-end/src/assets/i18n/en.json
index ec6de7c..bdfcd5d 100644
--- a/front-end/src/assets/i18n/en.json
+++ b/front-end/src/assets/i18n/en.json
@@ -159,7 +159,8 @@
"AGENDA": "Agenda",
"VENUES": "Venues",
"ORGANIZATIONS": "Organizations",
- "SPEAKERS": "Speakers"
+ "SPEAKERS": "Speakers",
+ "CONTESTS": "Contests"
},
"USER": {
"ESN_ACCOUNTS": "ESN Accounts",
@@ -356,7 +357,8 @@
"LIST_OF_ESN_COUNTRIES": "List of current ESN countries",
"USEFUL_LINKS": "Useful links",
"USEFUL_LINKS_I": "Manage the links you want to make available to all users for quick access.",
- "DOWNLOAD_SESSIONS_REGISTRATIONS": "Download a spreadsheet with all the sessions registrations"
+ "DOWNLOAD_SESSIONS_REGISTRATIONS": "Download a spreadsheet with all the sessions registrations",
+ "CONTESTS": "Contests"
},
"STRIPE": {
"BEFORE_YOU_PROCEED": "Before you proceed",
@@ -441,5 +443,34 @@
"ROOM": "Room",
"DOWNLOAD_REGISTRATIONS": "Download a spreadsheet with all the session registrations",
"SESSION_REGISTRATIONS": "Session registrations"
+ },
+ "CONTESTS": {
+ "MANAGE_CONTEST": "Manage contest",
+ "NAME": "Name",
+ "IMAGE_URI": "Image URI",
+ "DESCRIPTION": "Description",
+ "VISIBLE": "Visible to users",
+ "OPTIONS": "Options",
+ "OPEN_VOTE": "Vote is open",
+ "VOTE_ENDS_AT": "Vote ends at ({{timezone}})",
+ "CANDIDATES": "Candidates",
+ "CANDIDATES_I": "If you set a country for a candidate, users of that country won't be able to vote them.",
+ "RESULTS": "Results",
+ "CANDIDATE_NAME": "Name of the candidate",
+ "CANDIDATE_URL": "URL to a page where to discover the candidate",
+ "CANDIDATE_COUNTRY": "Country of the candidate",
+ "LIST": "Contests list",
+ "DETAILS": "Contest details",
+ "VOTE_ENDED": "Vote ended",
+ "VOTE_NOW": "Vote now",
+ "VOTES": "Votes",
+ "VOTE_NOT_OPEN_YET": "Vote is not open yet",
+ "VOTE_NOW_UNTIL": "Vote now, until: {{deadline}}",
+ "VOTE": "Vote",
+ "VOTE_I": "The vote is anonymous; please not that you can vote only once and you won't be able to change your vote.",
+ "YOU_ALREADY_VOTED": "You have voted.",
+ "YOU_ARE_VOTING": "You are voting",
+ "NOT_NOW": "Not now",
+ "PUBLISH_RESULTS": "Publish results"
}
}
diff --git a/front-end/src/global.scss b/front-end/src/global.scss
index 671fbd1..5659781 100644
--- a/front-end/src/global.scss
+++ b/front-end/src/global.scss
@@ -526,4 +526,3 @@ ion-img.inGallery::part(image) {
.forceMargins p {
margin: 15px 10px !important;
}
-
From 69f9a71e768ac2bf3e0c18925f332ff39345541c Mon Sep 17 00:00:00 2001
From: Matteo Carbone
Date: Sun, 24 Mar 2024 15:29:53 +0000
Subject: [PATCH 6/9] fix(contests): minor fixes and improvements #81
---
back-end/src/handlers/contests.ts | 9 ++++++---
back-end/src/models/contest.model.ts | 13 +++++++------
front-end/src/app/tabs/contests/contest.page.ts | 14 +++++++-------
front-end/src/app/tabs/contests/contests.page.ts | 4 ++--
.../app/tabs/contests/manageContest.component.ts | 10 ++++++----
5 files changed, 28 insertions(+), 22 deletions(-)
diff --git a/back-end/src/handlers/contests.ts b/back-end/src/handlers/contests.ts
index bd5f969..71d2bd4 100644
--- a/back-end/src/handlers/contests.ts
+++ b/back-end/src/handlers/contests.ts
@@ -12,6 +12,7 @@ import { User } from '../models/user.model';
///
const PROJECT = process.env.PROJECT;
+const STAGE = process.env.STAGE;
const DDB_TABLES = { users: process.env.DDB_TABLE_users, contests: process.env.DDB_TABLE_contests };
const ddb = new DynamoDB();
@@ -27,6 +28,7 @@ class ContestsRC extends ResourceController {
constructor(event: any, callback: any) {
super(event, callback, { resourceId: 'contestId' });
+ if (STAGE === 'prod') this.silentLambdaLogs(); // to make the vote anonymous
}
protected async checkAuthBeforeRequest(): Promise {
@@ -115,10 +117,11 @@ class ContestsRC extends ResourceController {
await ddb.transactWrites([{ Update: markUserContestVoted }, { Update: addUserVoteToContest }]);
}
private async publishResults(): Promise {
+ if (!this.user.permissions.canManageContents) throw new HandledError('Unauthorized');
+
if (this.contest.publishedResults) throw new HandledError('Already public');
- if (!this.contest.voteEndsAt || new Date().toISOString() <= this.contest.voteEndsAt)
- throw new HandledError('Vote is not done');
+ if (!this.contest.isVoteEnded()) throw new HandledError('Vote is not done');
await ddb.update({
TableName: DDB_TABLES.contests,
@@ -155,7 +158,7 @@ class ContestsRC extends ResourceController {
if (!this.user.permissions.canManageContents) {
contests = contests.filter(c => c.enabled);
contests.forEach(contest => {
- if (contest.publishedResults) delete contest.results;
+ if (!contest.publishedResults) delete contest.results;
});
}
diff --git a/back-end/src/models/contest.model.ts b/back-end/src/models/contest.model.ts
index 543db80..5921cb3 100644
--- a/back-end/src/models/contest.model.ts
+++ b/back-end/src/models/contest.model.ts
@@ -1,7 +1,7 @@
import { Resource, epochISOString } from 'idea-toolbox';
/**
- * A contest to which people can vote in.
+ * A contest to which users can vote in.
*/
export class Contest extends Resource {
/**
@@ -13,11 +13,11 @@ export class Contest extends Resource {
*/
createdAt: epochISOString;
/**
- * Whether the contest is enabled and therefore shown in the menu.
+ * Whether the contest is enabled and therefore shown in the menu to everyone.
*/
enabled: boolean;
/**
- * If set, the vote is active (users can vote) and ends at the configured timestamp.
+ * If set, the vote is active (users can vote), and it ends at the configured timestamp.
*/
voteEndsAt?: epochISOString;
/**
@@ -38,7 +38,7 @@ export class Contest extends Resource {
candidates: ContestCandidate[];
/**
* The count of votes for each of the sorted candidates.
- * Note: the order of the candidates list must not change after a vote is open.
+ * Note: the order of the candidates list must not change after the vote is open.
* This attribute is not accessible to non-admin users until `publishedResults` is true.
*/
results?: number[];
@@ -66,9 +66,10 @@ export class Contest extends Resource {
safeLoad(newData: any, safeData: any): void {
super.safeLoad(newData, safeData);
this.contestId = safeData.contestId;
- this.results = safeData.results;
+ this.createdAt = safeData.createdAt;
if (safeData.isVoteStarted()) {
this.candidates = safeData.candidates;
+ this.results = safeData.results;
}
}
@@ -110,7 +111,7 @@ export class ContestCandidate extends Resource {
* The country of the candidate.
* This is particularly important beacuse, if set, users can't vote for candidates of their own countries.
*/
- country: string;
+ country: string | null;
load(x: any): void {
super.load(x);
diff --git a/front-end/src/app/tabs/contests/contest.page.ts b/front-end/src/app/tabs/contests/contest.page.ts
index 1f37233..965cf20 100644
--- a/front-end/src/app/tabs/contests/contest.page.ts
+++ b/front-end/src/app/tabs/contests/contest.page.ts
@@ -49,11 +49,11 @@ import { Contest } from '@models/contest.model';
{{ contest.name }}
@if(contest.isVoteStarted()) { @if(contest.isVoteEnded()) { @if(contest.publishedResults) {
- {{ 'CONTESTS.RESULTS' | translate }}
+ {{ 'CONTESTS.RESULTS' | translate }}
} @else {
{{ 'CONTESTS.VOTE_ENDED' | translate }}
} } @else {
-
+
{{ 'CONTESTS.VOTE_NOW_UNTIL' | translate : { deadline: (contest.voteEndsAt | dateLocale : 'short') } }}
} } @else {
@@ -85,12 +85,12 @@ import { Contest } from '@models/contest.model';
- } @if(contest.publishedResults) { @if( isCandidateWinnerByIndex($index)) {
-
+ } @if(contest.publishedResults) { @if(isCandidateWinnerByIndex($index)) {
+
}
{{ contest.results[$index] ?? 0 }} {{ 'CONTESTS.VOTES' | translate | lowercase }}
@@ -161,7 +161,7 @@ export class ContestPage implements OnInit {
} catch (err) {
this._message.error('COMMON.NOT_FOUND');
} finally {
- await this._loading.hide();
+ this._loading.hide();
}
}
@@ -208,7 +208,7 @@ export class ContestPage implements OnInit {
} catch (err) {
this._message.error('COMMON.OPERATION_FAILED');
} finally {
- await this._loading.hide();
+ this._loading.hide();
}
};
diff --git a/front-end/src/app/tabs/contests/contests.page.ts b/front-end/src/app/tabs/contests/contests.page.ts
index 8f00b1d..a8ef07e 100644
--- a/front-end/src/app/tabs/contests/contests.page.ts
+++ b/front-end/src/app/tabs/contests/contests.page.ts
@@ -27,11 +27,11 @@ import { Contest } from '@models/contest.model';
{{ contest.name }}
@if(contest.isVoteStarted()) { @if(contest.isVoteEnded()) { @if(contest.publishedResults) {
- {{ 'CONTESTS.RESULTS' | translate }}
+ {{ 'CONTESTS.RESULTS' | translate }}
} @else {
{{ 'CONTESTS.VOTE_ENDED' | translate }}
} } @else {
- {{ 'CONTESTS.VOTE_NOW' | translate }}
+ {{ 'CONTESTS.VOTE_NOW' | translate }}
} }
} @empty { @if(contests) {
diff --git a/front-end/src/app/tabs/contests/manageContest.component.ts b/front-end/src/app/tabs/contests/manageContest.component.ts
index 9fbea0b..e4e6d0d 100644
--- a/front-end/src/app/tabs/contests/manageContest.component.ts
+++ b/front-end/src/app/tabs/contests/manageContest.component.ts
@@ -108,7 +108,7 @@ import { Contest, ContestCandidate } from '@models/contest.model';
{{ 'CONTESTS.CANDIDATE_URL' | translate }}
-
+
+
@for(country of _app.configurations.sectionCountries; track $index) {
{{ country }}
}
@@ -154,7 +155,7 @@ import { Contest, ContestCandidate } from '@models/contest.model';
}
{{ contest.results[$index] ?? 0 }} {{ 'CONTESTS.VOTES' | translate | lowercase }}
@@ -281,7 +282,8 @@ export class ManageContestComponent implements OnInit {
this.contest.candidates.push(new ContestCandidate());
}
removeCandidate(candidate: ContestCandidate): void {
- this.contest.candidates.splice(this.contest.candidates.indexOf(candidate), 1);
+ const candidateIndex = this.contest.candidates.indexOf(candidate);
+ if (candidateIndex !== -1) this.contest.candidates.splice(candidateIndex, 1);
}
isCandidateWinnerByIndex(candidateIndex: number): boolean {
@@ -300,7 +302,7 @@ export class ManageContestComponent implements OnInit {
} catch (err) {
this._message.error('COMMON.OPERATION_FAILED');
} finally {
- await this._loading.hide();
+ this._loading.hide();
}
};
From caa7922a11fbfa256936900faa0043117d0e1a6b Mon Sep 17 00:00:00 2001
From: Georg <135227061+georgfrodo@users.noreply.github.com>
Date: Fri, 29 Mar 2024 15:16:49 +0100
Subject: [PATCH 7/9] feat: session feedback #129
---
back-end/src/handlers/sessions.ts | 66 ++++++++++++++++++-
back-end/src/models/session.model.ts | 10 +++
.../src/models/sessionRegistration.model.ts | 4 ++
back-end/swagger.yaml | 32 +++++++++
.../src/app/tabs/sessions/session.page.html | 1 +
.../src/app/tabs/sessions/session.page.ts | 31 ++++++++-
.../sessions/sessionDetail.component.html | 47 +++++++++++++
.../tabs/sessions/sessionDetail.component.ts | 5 ++
.../src/app/tabs/sessions/sessions.page.html | 1 +
.../src/app/tabs/sessions/sessions.page.ts | 29 +++++++-
.../src/app/tabs/sessions/sessions.service.ts | 11 ++++
front-end/src/assets/i18n/en.json | 6 +-
12 files changed, 237 insertions(+), 6 deletions(-)
diff --git a/back-end/src/handlers/sessions.ts b/back-end/src/handlers/sessions.ts
index 1002aa7..666467c 100644
--- a/back-end/src/handlers/sessions.ts
+++ b/back-end/src/handlers/sessions.ts
@@ -18,7 +18,8 @@ const DDB_TABLES = {
users: process.env.DDB_TABLE_users,
sessions: process.env.DDB_TABLE_sessions,
rooms: process.env.DDB_TABLE_rooms,
- speakers: process.env.DDB_TABLE_speakers
+ speakers: process.env.DDB_TABLE_speakers,
+ registrations: process.env.DDB_TABLE_registrations,
};
const ddb = new DynamoDB();
@@ -55,6 +56,10 @@ class SessionsRC extends ResourceController {
}
protected async getResource(): Promise {
+ if (!this.user.permissions.canManageContents) {
+ delete this.session.feedbackResults;
+ delete this.session.feedbackComments;
+ }
return this.session;
}
@@ -92,6 +97,52 @@ class SessionsRC extends ResourceController {
}
}
+ protected async patchResource(): Promise {
+ switch (this.body.action) {
+ case 'GIVE_FEEDBACK':
+ return await this.userFeedback(this.body.rating, this.body.comment);
+ default:
+ throw new HandledError('Unsupported action');
+ }
+ }
+
+ private async userFeedback(rating: number, comment?: string): Promise {
+ const sessionRegistration = await ddb.get({
+ TableName: DDB_TABLES.registrations,
+ Key: { sessionId: this.session.sessionId, userId: this.user.userId }
+ });
+
+ if (!sessionRegistration) {
+ throw new HandledError("Can't rate a session without being registered");
+ }
+
+ if (sessionRegistration.hasUserRated) throw new HandledError("Already rated this session");
+
+ if (new Date().toISOString() < this.session.endsAt) throw new HandledError("Can't rate a session before it has ended");
+
+ if (rating < 1 || rating > 5 || !Number.isInteger(rating)) throw new HandledError('Invalid rating');
+
+ const addUserRatingToSession = {
+ TableName: DDB_TABLES.sessions,
+ Key: { sessionId: this.session.sessionId },
+ UpdateExpression: `ADD feedbackResults[${rating - 1}] :one`,
+ ExpressionAttributeValues: { ':one': 1 } as Record
+ };
+ if (comment) {
+ addUserRatingToSession.UpdateExpression += ', feedbackComments = list_append(feedbackComments, :comment)';
+ addUserRatingToSession.ExpressionAttributeValues[':comment'] = [comment];
+ }
+
+ const setHasUserRated = {
+ TableName: DDB_TABLES.registrations,
+ Key: { sessionId: this.session.sessionId, userId: this.user.userId },
+ UpdateExpression: 'SET hasUserRated = :true',
+ ExpressionAttributeValues: { ':true': true }
+ };
+
+ await ddb.transactWrites([{ Update: addUserRatingToSession }, { Update: setHasUserRated }]);
+ }
+
protected async deleteResource(): Promise {
if (!this.user.permissions.canManageContents) throw new HandledError('Unauthorized');
@@ -103,6 +154,8 @@ class SessionsRC extends ResourceController {
this.session = new Session(this.body);
this.session.sessionId = await ddb.IUNID(PROJECT);
+ this.session.feedbackResults = [0,0,0,0,0];
+ this.session.feedbackComments = [];
return await this.putSafeResource({ noOverwrite: true });
}
@@ -110,12 +163,19 @@ class SessionsRC extends ResourceController {
protected async getResources(): Promise {
const sessions = (await ddb.scan({ TableName: DDB_TABLES.sessions })).map(x => new Session(x));
- const filtertedSessions = sessions.filter(
+ const filteredSessions = sessions.filter(
x =>
(!this.queryParams.speaker || x.speakers.some(speaker => speaker.speakerId === this.queryParams.speaker)) &&
(!this.queryParams.room || x.room.roomId === this.queryParams.room)
);
- return filtertedSessions.sort((a, b): number => a.startsAt.localeCompare(b.startsAt));
+ if (!this.user.permissions.canManageContents) {
+ filteredSessions.forEach(session => {
+ delete session.feedbackResults;
+ delete session.feedbackComments;
+ });
+ }
+
+ return filteredSessions.sort((a, b): number => a.startsAt.localeCompare(b.startsAt));
}
}
diff --git a/back-end/src/models/session.model.ts b/back-end/src/models/session.model.ts
index 6d45f2e..ea81d64 100644
--- a/back-end/src/models/session.model.ts
+++ b/back-end/src/models/session.model.ts
@@ -61,6 +61,16 @@ export class Session extends Resource {
* Wether the sessions requires registration.
*/
requiresRegistration: boolean;
+ /**
+ * The counts of each star rating given to the session as feedback.
+ * Indices 0-4 correspond to 1-5 star ratings.
+ */
+ feedbackResults?: number[];
+ /**
+ * A list of feedback comments from the participants.
+ */
+ feedbackComments?: string[];
+
load(x: any): void {
super.load(x);
diff --git a/back-end/src/models/sessionRegistration.model.ts b/back-end/src/models/sessionRegistration.model.ts
index 666a115..39aa1b3 100644
--- a/back-end/src/models/sessionRegistration.model.ts
+++ b/back-end/src/models/sessionRegistration.model.ts
@@ -23,6 +23,10 @@ export class SessionRegistration extends Resource {
* The user's ESN Country if any.
*/
sectionCountry?: string;
+ /**
+ * Whether the user has rated the session.
+ */
+ hasUserRated: boolean;
load(x: any): void {
super.load(x);
diff --git a/back-end/swagger.yaml b/back-end/swagger.yaml
index 788414c..4327c5b 100644
--- a/back-end/swagger.yaml
+++ b/back-end/swagger.yaml
@@ -1034,6 +1034,38 @@ paths:
$ref: '#/components/responses/Session'
400:
$ref: '#/components/responses/BadParameters'
+ patch:
+ summary: Actions on a session
+ tags: [Sessions]
+ security:
+ - AuthFunction: []
+ parameters:
+ - name: sessionId
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ action:
+ type: string
+ enum: [GIVE_FEEDBACK]
+ rating:
+ type: number
+ description: (GIVE_FEEDBACK)
+ comment:
+ type: string
+ description: (GIVE_FEEDBACK)
+ responses:
+ 200:
+ $ref: '#/components/responses/OperationCompleted'
+ 400:
+ $ref: '#/components/responses/BadParameters'
delete:
summary: Delete a session
description: Requires to be content manager
diff --git a/front-end/src/app/tabs/sessions/session.page.html b/front-end/src/app/tabs/sessions/session.page.html
index 48fcc9e..93cb9e3 100644
--- a/front-end/src/app/tabs/sessions/session.page.html
+++ b/front-end/src/app/tabs/sessions/session.page.html
@@ -7,6 +7,7 @@
[isUserRegisteredInSession]="isUserRegisteredInSession(session)"
(favorite)="toggleFavorite($event, session)"
(register)="toggleRegister($event, session)"
+ (giveFeedback)="onGiveFeedback($event, session)"
>
diff --git a/front-end/src/app/tabs/sessions/session.page.ts b/front-end/src/app/tabs/sessions/session.page.ts
index 36025ee..e951d42 100644
--- a/front-end/src/app/tabs/sessions/session.page.ts
+++ b/front-end/src/app/tabs/sessions/session.page.ts
@@ -21,6 +21,7 @@ export class SessionPage implements OnInit {
session: Session;
favoriteSessionsIds: string[] = [];
registeredSessionsIds: string[] = [];
+ ratedSessionsIds: string[] = [];
selectedSession: Session;
constructor(
@@ -47,7 +48,9 @@ export class SessionPage implements OnInit {
// @todo improvable. Just amke a call to see if a session is or isn't favorited/registerd using a getById
const favoriteSessions = await this._sessions.getList({ force: true });
this.favoriteSessionsIds = favoriteSessions.map(s => s.sessionId);
- this.registeredSessionsIds = (await this._sessions.loadUserRegisteredSessions()).map(ur => ur.sessionId);
+ const userRegisteredSessions = await this._sessions.loadUserRegisteredSessions();
+ this.registeredSessionsIds = userRegisteredSessions.map(ur => ur.sessionId);
+ this.ratedSessionsIds = userRegisteredSessions.filter(ur => ur.hasUserRated).map(ur => ur.sessionId);
} catch (error) {
this.message.error('COMMON.OPERATION_FAILED');
} finally {
@@ -81,6 +84,14 @@ export class SessionPage implements OnInit {
return this.registeredSessionsIds.includes(session.sessionId);
}
+ hasUserRatedSession(session: Session): boolean {
+ return this.ratedSessionsIds.includes(session.sessionId);
+ }
+
+ hasSessionEnded(session: Session): boolean {
+ return new Date(session.endsAt) < new Date();
+ }
+
async toggleRegister(ev: any, session: Session): Promise {
ev?.stopPropagation();
try {
@@ -110,6 +121,24 @@ export class SessionPage implements OnInit {
}
}
+ async onGiveFeedback(ev: any, session: Session ): Promise {
+ try {
+ await this.loading.show();
+ let rating = ev.rating;
+ let comment = ev.comment;
+ if (rating === 0) {
+ this.message.error('SESSIONS.NO_RATING');
+ return;
+ }
+ await this._sessions.giveFeedback(session, rating, comment);
+ this.ratedSessionsIds.push(session.sessionId);
+ } catch (error) {
+ this.message.error('COMMON.OPERATION_FAILED');
+ } finally {
+ this.loading.hide();
+ }
+ }
+
async manageSession(): Promise {
if (!this.session) return;
diff --git a/front-end/src/app/tabs/sessions/sessionDetail.component.html b/front-end/src/app/tabs/sessions/sessionDetail.component.html
index 1116a88..ae18740 100644
--- a/front-end/src/app/tabs/sessions/sessionDetail.component.html
+++ b/front-end/src/app/tabs/sessions/sessionDetail.component.html
@@ -20,6 +20,53 @@
{{ 'SESSIONS.TYPES.' + session.type | translate }}
+
+
+
+ {{ 'SESSIONS.GIVE_FEEDBACK' | translate }}
+
+
+
+
+
+
+
+
+
+ {{ 'SESSIONS.FEEDBACK_HINT' | translate}}
+
+
+
+
+ {{ 'SESSIONS.SUBMIT_FEEDBACK' | translate}}
+
+
+
+
+
+
+ {{ 'SESSIONS.ALREADY_RATED' | translate }}
+
+
+
{{ app.formatDateShort(session.startsAt) }}
diff --git a/front-end/src/app/tabs/sessions/sessionDetail.component.ts b/front-end/src/app/tabs/sessions/sessionDetail.component.ts
index 2dbd5c3..1ff1a85 100644
--- a/front-end/src/app/tabs/sessions/sessionDetail.component.ts
+++ b/front-end/src/app/tabs/sessions/sessionDetail.component.ts
@@ -15,8 +15,13 @@ export class SessionDetailComponent {
@Input() session: Session;
@Input() isSessionInFavorites: boolean;
@Input() isUserRegisteredInSession: boolean;
+ @Input() hasUserRatedSession: boolean;
+ @Input() hasSessionEnded: boolean;
@Output() favorite = new EventEmitter();
@Output() register = new EventEmitter();
+ @Output() giveFeedback = new EventEmitter<{ rating: number; comment?: string }>();
+
+ selectedRating = 0;
constructor(public _sessions: SessionsService, public t: IDEATranslationsService, public app: AppService) {}
}
diff --git a/front-end/src/app/tabs/sessions/sessions.page.html b/front-end/src/app/tabs/sessions/sessions.page.html
index 2b82b05..7e77e28 100644
--- a/front-end/src/app/tabs/sessions/sessions.page.html
+++ b/front-end/src/app/tabs/sessions/sessions.page.html
@@ -90,6 +90,7 @@
[isUserRegisteredInSession]="isUserRegisteredInSession(selectedSession)"
(favorite)="toggleFavorite($event, selectedSession)"
(register)="toggleRegister($event, selectedSession)"
+ (giveFeedback)="onGiveFeedback($event, selectedSession)"
>
@if(selectedSession && app.user.permissions.canManageContents) {
diff --git a/front-end/src/app/tabs/sessions/sessions.page.ts b/front-end/src/app/tabs/sessions/sessions.page.ts
index 97a2acc..7c6b238 100644
--- a/front-end/src/app/tabs/sessions/sessions.page.ts
+++ b/front-end/src/app/tabs/sessions/sessions.page.ts
@@ -24,6 +24,7 @@ export class SessionsPage {
sessions: Session[];
favoriteSessionsIds: string[] = [];
registeredSessionsIds: string[] = [];
+ ratedSessionsIds: string[] = [];
selectedSession: Session;
segment = '';
@@ -49,7 +50,9 @@ export class SessionsPage {
this.segment = '';
this.sessions = await this._sessions.getList({ force: true });
this.favoriteSessionsIds = this.sessions.map(s => s.sessionId);
- this.registeredSessionsIds = (await this._sessions.loadUserRegisteredSessions()).map(ur => ur.sessionId);
+ const userRegisteredSessions = await this._sessions.loadUserRegisteredSessions();
+ this.registeredSessionsIds = userRegisteredSessions.map(ur => ur.sessionId);
+ this.ratedSessionsIds = userRegisteredSessions.filter(ur => ur.hasUserRated).map(ur => ur.sessionId);
this.days = await this._sessions.getSessionDays();
} catch (error) {
this.message.error('COMMON.OPERATION_FAILED');
@@ -93,6 +96,14 @@ export class SessionsPage {
return this.registeredSessionsIds.includes(session.sessionId);
}
+ hasUserRatedSession(session: Session): boolean {
+ return this.ratedSessionsIds.includes(session.sessionId);
+ }
+
+ hasSessionEnded(session: Session): boolean {
+ return new Date(session.endsAt) < new Date();
+ }
+
async toggleRegister(ev: any, session: Session): Promise {
ev?.stopPropagation();
try {
@@ -131,6 +142,22 @@ export class SessionsPage {
else this.selectedSession = session;
}
+ async onGiveFeedback(ev: any, session: Session ): Promise {
+ try {
+ await this.loading.show();
+ if (ev.rating === 0) {
+ this.message.error('SESSIONS.NO_RATING');
+ return;
+ }
+ await this._sessions.giveFeedback(session, ev.rating, ev.comment);
+ this.ratedSessionsIds.push(session.sessionId);
+ } catch (error) {
+ this.message.error('COMMON.OPERATION_FAILED');
+ } finally {
+ this.loading.hide();
+ }
+ }
+
async manageSession(): Promise {
if (!this.selectedSession) return;
diff --git a/front-end/src/app/tabs/sessions/sessions.service.ts b/front-end/src/app/tabs/sessions/sessions.service.ts
index c4b2a4f..04b5146 100644
--- a/front-end/src/app/tabs/sessions/sessions.service.ts
+++ b/front-end/src/app/tabs/sessions/sessions.service.ts
@@ -147,6 +147,17 @@ export class SessionsService {
this.userFavoriteSessions = await this.api.deleteResource(['registrations', sessionId]);
}
+ /**
+ * Rate a session with a number of stars (1-5) and optionally add a feedback comment.
+ */
+ async giveFeedback(session: Session, rating: number, comment?: string): Promise {
+ const body: any = { action: 'GIVE_FEEDBACK', rating };
+ if (comment) {
+ body.comment = comment;
+ }
+ await this.api.patchResource(['sessions', session.sessionId], { body });
+ }
+
getColourBySessionType(session: Session){
switch(session.type) {
case SessionType.DISCUSSION:
diff --git a/front-end/src/assets/i18n/en.json b/front-end/src/assets/i18n/en.json
index bdfcd5d..1316512 100644
--- a/front-end/src/assets/i18n/en.json
+++ b/front-end/src/assets/i18n/en.json
@@ -442,7 +442,11 @@
"ADD_SPEAKER": "Add speaker",
"ROOM": "Room",
"DOWNLOAD_REGISTRATIONS": "Download a spreadsheet with all the session registrations",
- "SESSION_REGISTRATIONS": "Session registrations"
+ "SESSION_REGISTRATIONS": "Session registrations",
+ "GIVE_FEEDBACK": "Give feedback for this session!",
+ "FEEDBACK_HINT": "Feedback is anonymous and cannot be changed once submitted.",
+ "SUBMIT_FEEDBACK": "Submit",
+ "NO_RATING": "Please provide a rating"
},
"CONTESTS": {
"MANAGE_CONTEST": "Manage contest",
From 0df069552b82bc9ce079f7448a4622d7b9473b0e Mon Sep 17 00:00:00 2001
From: rbento1096
Date: Tue, 2 Apr 2024 00:04:36 +0100
Subject: [PATCH 8/9] fix: review changes #129
---
.gitignore | 3 +-
back-end/src/handlers/sessions.ts | 44 +-
back-end/src/models/session.model.ts | 15 +-
.../src/models/sessionRegistration.model.ts | 1 +
.../src/app/tabs/sessions/session.page.html | 2 +
.../src/app/tabs/sessions/session.page.ts | 17 +-
.../sessions/sessionDetail.component.html | 247 +++++-----
.../src/app/tabs/sessions/sessions.page.html | 2 +
.../src/app/tabs/sessions/sessions.page.ts | 17 +-
front-end/src/assets/i18n/en.json | 7 +-
scripts/README.md | 6 +
scripts/package-lock.json | 454 ++++++++++++++++++
scripts/package.json | 18 +
scripts/src/sessionFeedback.ts | 66 +++
scripts/src/utils/ddb.utils.ts | 54 +++
scripts/tsconfig.json | 8 +
16 files changed, 804 insertions(+), 157 deletions(-)
create mode 100644 scripts/README.md
create mode 100644 scripts/package-lock.json
create mode 100644 scripts/package.json
create mode 100644 scripts/src/sessionFeedback.ts
create mode 100644 scripts/src/utils/ddb.utils.ts
create mode 100644 scripts/tsconfig.json
diff --git a/.gitignore b/.gitignore
index 36052cc..78548fd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,4 +41,5 @@ back-end/output-config.json
back-end/src/**/*.js
back-end/src/**/*.js.map
front-end/.angular/cache
-front-end/resources
\ No newline at end of file
+front-end/resources
+scripts/src/**/*.js
\ No newline at end of file
diff --git a/back-end/src/handlers/sessions.ts b/back-end/src/handlers/sessions.ts
index 666467c..7f5c0e4 100644
--- a/back-end/src/handlers/sessions.ts
+++ b/back-end/src/handlers/sessions.ts
@@ -8,6 +8,7 @@ import { Session } from '../models/session.model';
import { SpeakerLinked } from '../models/speaker.model';
import { RoomLinked } from '../models/room.model';
import { User } from '../models/user.model';
+import { SessionRegistration } from '../models/sessionRegistration.model';
///
/// CONSTANTS, ENVIRONMENT VARIABLES, HANDLER
@@ -19,7 +20,7 @@ const DDB_TABLES = {
sessions: process.env.DDB_TABLE_sessions,
rooms: process.env.DDB_TABLE_rooms,
speakers: process.env.DDB_TABLE_speakers,
- registrations: process.env.DDB_TABLE_registrations,
+ registrations: process.env.DDB_TABLE_registrations
};
const ddb = new DynamoDB();
@@ -56,7 +57,7 @@ class SessionsRC extends ResourceController {
}
protected async getResource(): Promise {
- if (!this.user.permissions.canManageContents) {
+ if (!this.user.permissions.canManageContents || !this.user.permissions.isAdmin) {
delete this.session.feedbackResults;
delete this.session.feedbackComments;
}
@@ -107,18 +108,24 @@ class SessionsRC extends ResourceController {
}
private async userFeedback(rating: number, comment?: string): Promise {
- const sessionRegistration = await ddb.get({
- TableName: DDB_TABLES.registrations,
- Key: { sessionId: this.session.sessionId, userId: this.user.userId }
- });
-
- if (!sessionRegistration) {
+ let sessionRegistration: SessionRegistration;
+ try {
+ sessionRegistration = new SessionRegistration(
+ await ddb.get({
+ TableName: DDB_TABLES.registrations,
+ Key: { sessionId: this.session.sessionId, userId: this.user.userId }
+ })
+ );
+ } catch (error) {
throw new HandledError("Can't rate a session without being registered");
}
- if (sessionRegistration.hasUserRated) throw new HandledError("Already rated this session");
+ if (!sessionRegistration) throw new HandledError("Can't rate a session without being registered");
+
+ if (sessionRegistration.hasUserRated) throw new HandledError('Already rated this session');
- if (new Date().toISOString() < this.session.endsAt) throw new HandledError("Can't rate a session before it has ended");
+ if (new Date().toISOString() < this.session.endsAt)
+ throw new HandledError("Can't rate a session before it has ended");
if (rating < 1 || rating > 5 || !Number.isInteger(rating)) throw new HandledError('Invalid rating');
@@ -126,12 +133,8 @@ class SessionsRC extends ResourceController {
TableName: DDB_TABLES.sessions,
Key: { sessionId: this.session.sessionId },
UpdateExpression: `ADD feedbackResults[${rating - 1}] :one`,
- ExpressionAttributeValues: { ':one': 1 } as Record
+ ExpressionAttributeValues: { ':one': 1 }
};
- if (comment) {
- addUserRatingToSession.UpdateExpression += ', feedbackComments = list_append(feedbackComments, :comment)';
- addUserRatingToSession.ExpressionAttributeValues[':comment'] = [comment];
- }
const setHasUserRated = {
TableName: DDB_TABLES.registrations,
@@ -141,6 +144,15 @@ class SessionsRC extends ResourceController {
};
await ddb.transactWrites([{ Update: addUserRatingToSession }, { Update: setHasUserRated }]);
+
+ if (comment) {
+ await ddb.update({
+ TableName: DDB_TABLES.sessions,
+ Key: { sessionId: this.session.sessionId },
+ UpdateExpression: 'SET feedbackComments = list_append(feedbackComments, :comment)',
+ ExpressionAttributeValues: { ':comment': [comment] }
+ });
+ }
}
protected async deleteResource(): Promise {
@@ -154,8 +166,6 @@ class SessionsRC extends ResourceController {
this.session = new Session(this.body);
this.session.sessionId = await ddb.IUNID(PROJECT);
- this.session.feedbackResults = [0,0,0,0,0];
- this.session.feedbackComments = [];
return await this.putSafeResource({ noOverwrite: true });
}
diff --git a/back-end/src/models/session.model.ts b/back-end/src/models/session.model.ts
index ea81d64..9b8793b 100644
--- a/back-end/src/models/session.model.ts
+++ b/back-end/src/models/session.model.ts
@@ -8,6 +8,11 @@ import { SpeakerLinked } from './speaker.model';
*/
type datetime = string;
+/**
+ * The max number of stars you can give to a session.
+ */
+const MAX_RATING = 5;
+
export class Session extends Resource {
/**
* The session ID.
@@ -71,7 +76,6 @@ export class Session extends Resource {
*/
feedbackComments?: string[];
-
load(x: any): void {
super.load(x);
this.sessionId = this.clean(x.sessionId, String);
@@ -91,6 +95,10 @@ export class Session extends Resource {
this.numberOfParticipants = this.clean(x.numberOfParticipants, Number, 0);
this.limitOfParticipants = this.clean(x.limitOfParticipants, Number);
}
+ this.feedbackResults = [];
+ for (let i = 0; i < MAX_RATING; i++)
+ this.feedbackResults[i] = x.feedbackResults ? Number(x.feedbackResults[i] ?? 0) : 0;
+ this.feedbackComments = this.cleanArray(x.feedbackComments, String);
}
safeLoad(newData: any, safeData: any): void {
super.safeLoad(newData, safeData);
@@ -126,15 +134,14 @@ export class Session extends Resource {
}
isFull(): boolean {
- return this.requiresRegistration ? this.numberOfParticipants >= this.limitOfParticipants : false
+ return this.requiresRegistration ? this.numberOfParticipants >= this.limitOfParticipants : false;
}
getSpeakers(): string {
- return this.speakers.map(s => s.name).join(', ')
+ return this.speakers.map(s => s.name).join(', ');
}
}
-
export enum SessionType {
DISCUSSION = 'DISCUSSION',
TALK = 'TALK',
diff --git a/back-end/src/models/sessionRegistration.model.ts b/back-end/src/models/sessionRegistration.model.ts
index 39aa1b3..144986f 100644
--- a/back-end/src/models/sessionRegistration.model.ts
+++ b/back-end/src/models/sessionRegistration.model.ts
@@ -35,6 +35,7 @@ export class SessionRegistration extends Resource {
this.registrationDateInMs = this.clean(x.registrationDateInMs, t => new Date(t).getTime());
this.name = this.clean(x.name, String);
if (x.sectionCountry) this.sectionCountry = this.clean(x.sectionCountry, String);
+ this.hasUserRated = this.clean(x.hasUserRated, Boolean);
}
/**
diff --git a/front-end/src/app/tabs/sessions/session.page.html b/front-end/src/app/tabs/sessions/session.page.html
index 93cb9e3..e7fef14 100644
--- a/front-end/src/app/tabs/sessions/session.page.html
+++ b/front-end/src/app/tabs/sessions/session.page.html
@@ -5,6 +5,8 @@
[session]="session"
[isSessionInFavorites]="isSessionInFavorites(session)"
[isUserRegisteredInSession]="isUserRegisteredInSession(session)"
+ [hasUserRatedSession]="hasUserRatedSession(session)"
+ [hasSessionEnded]="hasSessionEnded(session)"
(favorite)="toggleFavorite($event, session)"
(register)="toggleRegister($event, session)"
(giveFeedback)="onGiveFeedback($event, session)"
diff --git a/front-end/src/app/tabs/sessions/session.page.ts b/front-end/src/app/tabs/sessions/session.page.ts
index e951d42..a4aee1b 100644
--- a/front-end/src/app/tabs/sessions/session.page.ts
+++ b/front-end/src/app/tabs/sessions/session.page.ts
@@ -121,19 +121,24 @@ export class SessionPage implements OnInit {
}
}
- async onGiveFeedback(ev: any, session: Session ): Promise {
+ async onGiveFeedback(ev: any, session: Session): Promise {
try {
await this.loading.show();
let rating = ev.rating;
let comment = ev.comment;
- if (rating === 0) {
- this.message.error('SESSIONS.NO_RATING');
- return;
- }
+ if (rating === 0) return this.message.error('SESSIONS.NO_RATING');
await this._sessions.giveFeedback(session, rating, comment);
this.ratedSessionsIds.push(session.sessionId);
+
+ this.message.success('SESSIONS.FEEDBACK_SENT');
} catch (error) {
- this.message.error('COMMON.OPERATION_FAILED');
+ if (error.message === "Can't rate a session without being registered")
+ this.message.error('SESSIONS.NOT_REGISTERED');
+ else if (error.message === 'Already rated this session') this.message.error('SESSIONS.ALREADY_RATED');
+ else if (error.message === "Can't rate a session before it has ended")
+ this.message.error('SESSIONS.STILL_TAKING_PLACE');
+ else if (error.message === 'Invalid rating') this.message.error('SESSIONS.INVALID_RATING');
+ else this.message.error('COMMON.OPERATION_FAILED');
} finally {
this.loading.hide();
}
diff --git a/front-end/src/app/tabs/sessions/sessionDetail.component.html b/front-end/src/app/tabs/sessions/sessionDetail.component.html
index ae18740..9977a5e 100644
--- a/front-end/src/app/tabs/sessions/sessionDetail.component.html
+++ b/front-end/src/app/tabs/sessions/sessionDetail.component.html
@@ -1,132 +1,135 @@
-
-
-
-
- {{ session.code }}
-
-
-
-
- {{ session.name }}
-
-
-
-
-
-
-
- {{ 'SESSIONS.TYPES.' + session.type | translate }}
-
-
-
-
- {{ 'SESSIONS.GIVE_FEEDBACK' | translate }}
-
-
-
-
-
-
-
-
-
- {{ 'SESSIONS.FEEDBACK_HINT' | translate}}
-
-
-
-
- {{ 'SESSIONS.SUBMIT_FEEDBACK' | translate}}
-
-
-
-
-
-
- {{ 'SESSIONS.ALREADY_RATED' | translate }}
-
-
-
-
-
- {{ app.formatDateShort(session.startsAt) }}
-
-
-
- {{ app.formatTime(session.startsAt) }} - {{ app.formatTime(session.endsAt) }}
-
-
-
- {{ session.durationMinutes }} {{ 'COMMON.MINUTES' | translate }}
-
-
-
- {{ session.room.name }} ({{ session.room.venue.name }})
-
-
-
-
+
+
+
+
+ {{ session.code }}
+
+
+
+
+ {{ session.name }}
+
+
+
+
+
+
+
+ {{ 'SESSIONS.TYPES.' + session.type | translate }}
+
+
+
+
+ {{ 'SESSIONS.GIVE_FEEDBACK' | translate }}
+
+
+
+
+
+
+
+
+
+ {{ 'SESSIONS.FEEDBACK_HINT' | translate }}
+
+
+
+ {{ 'SESSIONS.SUBMIT_FEEDBACK' | translate }}
+
+
+
+
+
+
+ {{ 'SESSIONS.ALREADY_RATED' | translate }}
+
+
+
+
+
+ {{ app.formatDateShort(session.startsAt) }}
+
+
+
+ {{ app.formatTime(session.startsAt) }} - {{ app.formatTime(session.endsAt) }}
+
+
+
+ {{ session.durationMinutes }} {{ 'COMMON.MINUTES' | translate }}
+
+
+
+ {{ session.room.name }} ({{ session.room.venue.name }})
+
+
+
+
+
- {{ speaker.name }}
-
-
-
-
-
-
- {{ session.isFull() ? this.t._('COMMON.FULL') : session.numberOfParticipants + '/' + session.limitOfParticipants }}
-
-
-
-
-
-
-
-
-
-
-
-
+ {{ speaker.name }}
+
-
-
-
-
+
+
+
+
+ {{
+ session.isFull() ? this.t._('COMMON.FULL') : session.numberOfParticipants + '/' + session.limitOfParticipants
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/front-end/src/app/tabs/sessions/sessions.page.html b/front-end/src/app/tabs/sessions/sessions.page.html
index 7e77e28..4211f42 100644
--- a/front-end/src/app/tabs/sessions/sessions.page.html
+++ b/front-end/src/app/tabs/sessions/sessions.page.html
@@ -88,6 +88,8 @@
[session]="selectedSession"
[isSessionInFavorites]="isSessionInFavorites(selectedSession)"
[isUserRegisteredInSession]="isUserRegisteredInSession(selectedSession)"
+ [hasUserRatedSession]="hasUserRatedSession(selectedSession)"
+ [hasSessionEnded]="hasSessionEnded(selectedSession)"
(favorite)="toggleFavorite($event, selectedSession)"
(register)="toggleRegister($event, selectedSession)"
(giveFeedback)="onGiveFeedback($event, selectedSession)"
diff --git a/front-end/src/app/tabs/sessions/sessions.page.ts b/front-end/src/app/tabs/sessions/sessions.page.ts
index 7c6b238..a87be45 100644
--- a/front-end/src/app/tabs/sessions/sessions.page.ts
+++ b/front-end/src/app/tabs/sessions/sessions.page.ts
@@ -142,17 +142,22 @@ export class SessionsPage {
else this.selectedSession = session;
}
- async onGiveFeedback(ev: any, session: Session ): Promise {
+ async onGiveFeedback(ev: any, session: Session): Promise {
try {
await this.loading.show();
- if (ev.rating === 0) {
- this.message.error('SESSIONS.NO_RATING');
- return;
- }
+ if (ev.rating === 0) return this.message.error('SESSIONS.NO_RATING');
+
await this._sessions.giveFeedback(session, ev.rating, ev.comment);
this.ratedSessionsIds.push(session.sessionId);
+ this.message.success('SESSIONS.FEEDBACK_SENT');
} catch (error) {
- this.message.error('COMMON.OPERATION_FAILED');
+ if (error.message === "Can't rate a session without being registered")
+ this.message.error('SESSIONS.NOT_REGISTERED');
+ else if (error.message === 'Already rated this session') this.message.error('SESSIONS.ALREADY_RATED');
+ else if (error.message === "Can't rate a session before it has ended")
+ this.message.error('SESSIONS.STILL_TAKING_PLACE');
+ else if (error.message === 'Invalid rating') this.message.error('SESSIONS.INVALID_RATING');
+ else this.message.error('COMMON.OPERATION_FAILED');
} finally {
this.loading.hide();
}
diff --git a/front-end/src/assets/i18n/en.json b/front-end/src/assets/i18n/en.json
index 1316512..b3213ea 100644
--- a/front-end/src/assets/i18n/en.json
+++ b/front-end/src/assets/i18n/en.json
@@ -446,7 +446,12 @@
"GIVE_FEEDBACK": "Give feedback for this session!",
"FEEDBACK_HINT": "Feedback is anonymous and cannot be changed once submitted.",
"SUBMIT_FEEDBACK": "Submit",
- "NO_RATING": "Please provide a rating"
+ "NO_RATING": "Please provide a rating",
+ "ALREADY_RATED": "Already rated this session",
+ "FEEDBACK_SENT": "Feedback sent",
+ "STILL_TAKING_PLACE": "Session is still taking place",
+ "INVALID_RATING": "Invalid rating",
+ "NOT_REGISTERED": "Not registered"
},
"CONTESTS": {
"MANAGE_CONTEST": "Manage contest",
diff --git a/scripts/README.md b/scripts/README.md
new file mode 100644
index 0000000..8c23d4c
--- /dev/null
+++ b/scripts/README.md
@@ -0,0 +1,6 @@
+To run the scripts:
+
+- `npm run build` (from `scripts` folder).
+- `node src/trainings` or `node src/deliveries`
+
+_Note: in case of `ExpiredTokenException`, make sure you have signed-in with AWS SSO and ran both: `yawsso -p iter-idea` and `yawsso -p iter-idea-tfm`._
diff --git a/scripts/package-lock.json b/scripts/package-lock.json
new file mode 100644
index 0000000..f762052
--- /dev/null
+++ b/scripts/package-lock.json
@@ -0,0 +1,454 @@
+{
+ "name": "data-transfer",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "data-transfer",
+ "version": "1.0.0",
+ "dependencies": {
+ "aws-sdk": "^2.1094.0",
+ "commander": "^11.0.0",
+ "idea-toolbox": "^7.0.3"
+ },
+ "devDependencies": {
+ "@tsconfig/node14": "^1.0.0",
+ "@types/node": "^14.14.26",
+ "typescript": "^4.4.3"
+ }
+ },
+ "node_modules/@tsconfig/node14": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
+ "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
+ "dev": true
+ },
+ "node_modules/@types/node": {
+ "version": "14.18.63",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz",
+ "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==",
+ "dev": true
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
+ "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/aws-sdk": {
+ "version": "2.1544.0",
+ "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1544.0.tgz",
+ "integrity": "sha512-R0C9bonDL0IQ/j0tq6Xaq5weFiaiSOj6KGRseHy+78zdbP1tsG2LZSoN3J5RqjjLHA5/fTMwXO1IuW/4eCNLAg==",
+ "dependencies": {
+ "buffer": "4.9.2",
+ "events": "1.1.1",
+ "ieee754": "1.1.13",
+ "jmespath": "0.16.0",
+ "querystring": "0.2.0",
+ "sax": "1.2.1",
+ "url": "0.10.3",
+ "util": "^0.12.4",
+ "uuid": "8.0.0",
+ "xml2js": "0.6.2"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/buffer": {
+ "version": "4.9.2",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz",
+ "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==",
+ "dependencies": {
+ "base64-js": "^1.0.2",
+ "ieee754": "^1.1.4",
+ "isarray": "^1.0.0"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
+ "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
+ "dependencies": {
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.1",
+ "set-function-length": "^1.1.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/commander": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
+ "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
+ "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
+ "dependencies": {
+ "get-intrinsic": "^1.2.1",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/events": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
+ "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==",
+ "engines": {
+ "node": ">=0.4.x"
+ }
+ },
+ "node_modules/for-each": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
+ "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+ "dependencies": {
+ "is-callable": "^1.1.3"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
+ "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
+ "dependencies": {
+ "function-bind": "^1.1.2",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "hasown": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+ "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "dependencies": {
+ "get-intrinsic": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
+ "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
+ "dependencies": {
+ "get-intrinsic": "^1.2.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
+ "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
+ "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
+ "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/idea-toolbox": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/idea-toolbox/-/idea-toolbox-7.0.3.tgz",
+ "integrity": "sha512-yMEdhabfes68TXVE0V04oiWZMhYMspmKhOo4UkaQFG5K2GaXGXGhiMXx0iYp4zJ7Vb0RYBA04eYJeAvi1TSmrw==",
+ "dependencies": {
+ "marked": "^11.1.1",
+ "validator": "^13.11.0"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
+ "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "node_modules/is-arguments": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
+ "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-generator-function": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
+ "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz",
+ "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==",
+ "dependencies": {
+ "which-typed-array": "^1.1.11"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
+ },
+ "node_modules/jmespath": {
+ "version": "0.16.0",
+ "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz",
+ "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/marked": {
+ "version": "11.1.1",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-11.1.1.tgz",
+ "integrity": "sha512-EgxRjgK9axsQuUa/oKMx5DEY8oXpKJfk61rT5iY3aRlgU6QJtUcxU5OAymdhCvWvhYcd9FKmO5eQoX8m9VGJXg==",
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
+ "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw=="
+ },
+ "node_modules/querystring": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
+ "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==",
+ "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.",
+ "engines": {
+ "node": ">=0.4.x"
+ }
+ },
+ "node_modules/sax": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz",
+ "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz",
+ "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==",
+ "dependencies": {
+ "define-data-property": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.2",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "4.9.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
+ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=4.2.0"
+ }
+ },
+ "node_modules/url": {
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",
+ "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==",
+ "dependencies": {
+ "punycode": "1.3.2",
+ "querystring": "0.2.0"
+ }
+ },
+ "node_modules/util": {
+ "version": "0.12.5",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
+ "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "is-arguments": "^1.0.4",
+ "is-generator-function": "^1.0.7",
+ "is-typed-array": "^1.1.3",
+ "which-typed-array": "^1.1.2"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz",
+ "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/validator": {
+ "version": "13.11.0",
+ "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz",
+ "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz",
+ "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.4",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/xml2js": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
+ "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
+ "dependencies": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
+ "engines": {
+ "node": ">=4.0"
+ }
+ }
+ }
+}
diff --git a/scripts/package.json b/scripts/package.json
new file mode 100644
index 0000000..f659779
--- /dev/null
+++ b/scripts/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "data-transfer",
+ "version": "1.0.0",
+ "private": true,
+ "scripts": {
+ "build": "tsc --build"
+ },
+ "dependencies": {
+ "aws-sdk": "^2.1094.0",
+ "commander": "^11.0.0",
+ "idea-toolbox": "^7.0.3"
+ },
+ "devDependencies": {
+ "@tsconfig/node14": "^1.0.0",
+ "@types/node": "^14.14.26",
+ "typescript": "^4.4.3"
+ }
+}
diff --git a/scripts/src/sessionFeedback.ts b/scripts/src/sessionFeedback.ts
new file mode 100644
index 0000000..52f8697
--- /dev/null
+++ b/scripts/src/sessionFeedback.ts
@@ -0,0 +1,66 @@
+import { DynamoDB } from 'aws-sdk';
+import { InvalidArgumentError, program } from 'commander';
+
+import { initWithSSO, putItemsHelper, scanInfinite } from './utils/ddb.utils';
+
+//
+// PARAMS
+//
+
+program
+ .name('EGM: Adds the feedback results and feedback comments array to the session model to avoid errors.')
+ .option('-e, --env [environment]', 'The target environment', 'prod')
+ .option('-w, --write', 'Whether to write data or just to simulate the execution', false)
+ .showHelpAfterError('\tadd --help for additional information')
+ .parse();
+const options = program.opts();
+
+const AWS_PROFILE = 'egm';
+const DDB_TABLE_REGION = 'eu-central-1';
+const DDB_TABLE_BASE = `egm-${options.env}-api_`;
+
+//
+// MAIN
+//
+
+main();
+
+async function main(): Promise {
+ try {
+ if (!options.env) throw new InvalidArgumentError('Missing environment');
+ } catch (error) {
+ program.error((error as any).message);
+ }
+
+ try {
+ const { ddb } = await initWithSSO(AWS_PROFILE, DDB_TABLE_REGION);
+
+ const sessions: any[] = await getSessions(ddb);
+ sessions.forEach(s => {
+ s.feedbackComments = [];
+ s.feedbackResults = [0, 0, 0, 0, 0];
+ });
+ await putItemsHelper(ddb, DDB_TABLE_BASE.concat('sessions'), sessions, options.write);
+
+ const registrations: any[] = await getRegistrations(ddb);
+ registrations.forEach(r => {
+ r.hasUserRated = false;
+ });
+ await putItemsHelper(ddb, DDB_TABLE_BASE.concat('registrations'), registrations, options.write);
+
+ console.log('[DONE]');
+ } catch (err) {
+ console.error('[ERROR] Operation failed', err);
+ }
+
+ async function getSessions(ddb: DynamoDB.DocumentClient): Promise {
+ const sessions = await scanInfinite(ddb, { TableName: DDB_TABLE_BASE.concat('sessions') });
+ console.log(`Read ${sessions.length} sessions`);
+ return sessions;
+ }
+ async function getRegistrations(ddb: DynamoDB.DocumentClient): Promise {
+ const registrations = await scanInfinite(ddb, { TableName: DDB_TABLE_BASE.concat('registrations') });
+ console.log(`Read ${registrations.length} registrations`);
+ return registrations;
+ }
+}
diff --git a/scripts/src/utils/ddb.utils.ts b/scripts/src/utils/ddb.utils.ts
new file mode 100644
index 0000000..cde7251
--- /dev/null
+++ b/scripts/src/utils/ddb.utils.ts
@@ -0,0 +1,54 @@
+import { DynamoDB, SharedIniFileCredentials } from 'aws-sdk';
+
+export type DDB = DynamoDB.DocumentClient;
+
+export async function initWithSSO(profile: string, region?: string): Promise<{ ddb: DynamoDB.DocumentClient }> {
+ const credentials = new SharedIniFileCredentials({ profile });
+ return { ddb: new DynamoDB.DocumentClient({ region, credentials }) };
+}
+
+export async function scanInfinite(
+ ddb: AWS.DynamoDB.DocumentClient,
+ params: AWS.DynamoDB.DocumentClient.ScanInput,
+ items: AWS.DynamoDB.DocumentClient.AttributeMap[] = []
+): Promise {
+ const result = await ddb.scan(params).promise();
+
+ items = items.concat(result.Items);
+
+ if (result.LastEvaluatedKey) {
+ params.ExclusiveStartKey = result.LastEvaluatedKey;
+ return await scanInfinite(ddb, params, items);
+ } else return items;
+}
+
+export function chunkArray(array: any[], chunkSize: number = 100): any[][] {
+ return array.reduce((resultArray, item, index): any[][] => {
+ const chunkIndex = Math.floor(index / chunkSize);
+ if (!resultArray[chunkIndex]) resultArray[chunkIndex] = [];
+ resultArray[chunkIndex].push(item);
+ return resultArray;
+ }, []);
+}
+
+export async function putItemsHelper(ddb: DDB, tableName: string, items: any[], write = false): Promise {
+ const writeElement = async (ddb: DDB, element: any): Promise => {
+ try {
+ await ddb.put({ TableName: tableName, Item: element }).promise();
+ } catch (error) {
+ console.log(`Put failed (${tableName})`, element);
+ throw error;
+ }
+ };
+
+ if (!write) console.log(`${tableName} preview:`, items.slice(0, 5));
+ else {
+ console.log(`Writing ${tableName}`);
+ const chunkSize = 100;
+ const chunks = chunkArray(items, chunkSize);
+ for (let i = 0; i < chunks.length; i++) {
+ console.log('\tProgress:', i * chunkSize);
+ await Promise.allSettled(chunks[i].map(x => writeElement(ddb, x)));
+ }
+ }
+}
diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json
new file mode 100644
index 0000000..767c359
--- /dev/null
+++ b/scripts/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "./node_modules/@tsconfig/node14/tsconfig.json",
+ "compilerOptions": {
+ "strictPropertyInitialization": false,
+ "strictNullChecks": false
+ },
+ "include": ["src/**/*.ts"]
+}
From c3e46b796a86f0ac18b28176119646e37c8701ab Mon Sep 17 00:00:00 2001
From: Matteo Carbone
Date: Fri, 5 Apr 2024 09:19:07 +0200
Subject: [PATCH 9/9] v3.5.0
---
back-end/package-lock.json | 4 ++--
back-end/package.json | 2 +-
back-end/swagger.yaml | 2 +-
front-end/android/app/build.gradle | 2 +-
front-end/package-lock.json | 4 ++--
front-end/package.json | 2 +-
front-end/src/environments/environment.idea.ts | 2 +-
7 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/back-end/package-lock.json b/back-end/package-lock.json
index 3f05288..18bba1c 100644
--- a/back-end/package-lock.json
+++ b/back-end/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "back-end",
- "version": "3.4.0",
+ "version": "3.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "back-end",
- "version": "3.4.0",
+ "version": "3.5.0",
"dependencies": {
"axios": "^1.6.7",
"date-fns": "^3.3.1",
diff --git a/back-end/package.json b/back-end/package.json
index ebfdb88..7800198 100644
--- a/back-end/package.json
+++ b/back-end/package.json
@@ -1,5 +1,5 @@
{
- "version": "3.4.0",
+ "version": "3.5.0",
"name": "back-end",
"scripts": {
"lint": "eslint --ext .ts",
diff --git a/back-end/swagger.yaml b/back-end/swagger.yaml
index 4327c5b..9f9d37b 100644
--- a/back-end/swagger.yaml
+++ b/back-end/swagger.yaml
@@ -2,7 +2,7 @@ openapi: 3.0.3
info:
title: EGM API
- version: 3.4.0
+ version: 3.5.0
contact:
name: EGM Technical Lead
email: egm-technical@esn.org
diff --git a/front-end/android/app/build.gradle b/front-end/android/app/build.gradle
index fcab200..ddcf5b7 100644
--- a/front-end/android/app/build.gradle
+++ b/front-end/android/app/build.gradle
@@ -8,7 +8,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
- versionName "3.4.0"
+ versionName "3.5.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
diff --git a/front-end/package-lock.json b/front-end/package-lock.json
index 77bf0f4..c95e8dc 100644
--- a/front-end/package-lock.json
+++ b/front-end/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "egm-app",
- "version": "3.4.0",
+ "version": "3.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "egm-app",
- "version": "3.4.0",
+ "version": "3.5.0",
"dependencies": {
"@angular/animations": "^17.1.2",
"@angular/common": "^17.1.2",
diff --git a/front-end/package.json b/front-end/package.json
index b9bc79e..44c5d57 100644
--- a/front-end/package.json
+++ b/front-end/package.json
@@ -1,6 +1,6 @@
{
"name": "egm-app",
- "version": "3.4.0",
+ "version": "3.5.0",
"author": "ITER IDEA",
"homepage": "https://iter-idea.com/",
"scripts": {
diff --git a/front-end/src/environments/environment.idea.ts b/front-end/src/environments/environment.idea.ts
index b98d46c..d4ab1f2 100644
--- a/front-end/src/environments/environment.idea.ts
+++ b/front-end/src/environments/environment.idea.ts
@@ -5,7 +5,7 @@ export const environment = {
idea: {
project: 'egm-app',
app: {
- version: '3.4.0',
+ version: '3.5.0',
bundle: 'com.esn.egmapp',
url: 'https://app.erasmusgeneration.org',
mediaUrl: 'https://media.egm-app.click',