Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: session feedback #129 #134

Merged
merged 2 commits into from
Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
front-end/resources
scripts/src/**/*.js
76 changes: 73 additions & 3 deletions back-end/src/handlers/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -18,7 +19,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();

Expand Down Expand Up @@ -55,6 +57,10 @@ class SessionsRC extends ResourceController {
}

protected async getResource(): Promise<Session> {
if (!this.user.permissions.canManageContents || !this.user.permissions.isAdmin) {
delete this.session.feedbackResults;
delete this.session.feedbackComments;
}
return this.session;
}

Expand Down Expand Up @@ -92,6 +98,63 @@ class SessionsRC extends ResourceController {
}
}

protected async patchResource(): Promise<void> {
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<void> {
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) 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 }
};

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 }]);

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<void> {
if (!this.user.permissions.canManageContents) throw new HandledError('Unauthorized');

Expand All @@ -110,12 +173,19 @@ class SessionsRC extends ResourceController {
protected async getResources(): Promise<Session[]> {
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));
}
}
23 changes: 20 additions & 3 deletions back-end/src/models/session.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -61,6 +66,15 @@ 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);
Expand All @@ -81,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);
Expand Down Expand Up @@ -116,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',
Expand Down
5 changes: 5 additions & 0 deletions back-end/src/models/sessionRegistration.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -31,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);
}

/**
Expand Down
32 changes: 32 additions & 0 deletions back-end/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions front-end/src/app/tabs/sessions/session.page.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
[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)"
>
<ion-buttons slot="start">
<ion-button color="primary" (click)="app.goToInTabs(['agenda'], { back: true })">
Expand Down
36 changes: 35 additions & 1 deletion front-end/src/app/tabs/sessions/session.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export class SessionPage implements OnInit {
session: Session;
favoriteSessionsIds: string[] = [];
registeredSessionsIds: string[] = [];
ratedSessionsIds: string[] = [];
selectedSession: Session;

constructor(
Expand All @@ -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 {
Expand Down Expand Up @@ -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<void> {
ev?.stopPropagation();
try {
Expand Down Expand Up @@ -110,6 +121,29 @@ export class SessionPage implements OnInit {
}
}

async onGiveFeedback(ev: any, session: Session): Promise<void> {
try {
await this.loading.show();
let rating = ev.rating;
let comment = ev.comment;
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) {
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();
}
}

async manageSession(): Promise<void> {
if (!this.session) return;

Expand Down
Loading