diff --git a/backend/app/crud.py b/backend/app/crud.py index a7bd760106c7ae78176e1628b317bf11c154e3f9..57d3c04439f71427113109accc186d84b9205319 100644 --- a/backend/app/crud.py +++ b/backend/app/crud.py @@ -86,9 +86,7 @@ def get_contact_sessions(db: Session, user_id: int, contact_id: int): def create_session(db: Session, user: schemas.User): - print("before") db_session = models.Session(is_active=True, users=[user]) - print(db_session.created_at) db.add(db_session) db.commit() db.refresh(db_session) @@ -112,11 +110,13 @@ def create_session_with_users( def get_session(db: Session, session_id: int): - return db.query(models.Session).filter(models.Session.id == session_id).first() + session = db.query(models.Session).filter(models.Session.id == session_id).first() + + return session def get_sessions(db: Session, user: schemas.User, skip: int = 0): - return ( + sessions = ( db.query(models.Session) .filter(models.Session.users.any(models.User.id == user.id)) .filter(models.Session.is_active or user.type < 2) @@ -126,9 +126,20 @@ def get_sessions(db: Session, user: schemas.User, skip: int = 0): .all() ) + return sessions + def get_all_sessions(db: Session, skip: int = 0): - return db.query(models.Session).offset(skip).all() + sessions = db.query(models.Session).offset(skip).all() + + for session in sessions: + session.length = ( + db.query(models.Message) + .filter(models.Message.session_id == session.id) + .count() + ) + + return sessions def delete_session(db: Session, session_id: int): diff --git a/backend/app/main.py b/backend/app/main.py index 677d8b0d96a1f49ff4be954cd0abd9aaebf89137..8b578828de724fb4154e16cc7539fae291a2129d 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -15,9 +15,12 @@ from fastapi import ( from sqlalchemy.orm import Session from fastapi.middleware.cors import CORSMiddleware from fastapi.websockets import WebSocket +from fastapi.responses import StreamingResponse from contextlib import asynccontextmanager import json from jose import jwt +from io import StringIO +import csv import schemas import crud @@ -409,7 +412,10 @@ def create_session( # status_code=401, detail="You do not have permission to create a session" # ) - return crud.create_session(db, current_user) + rep = crud.create_session(db, current_user) + rep.length = 0 + + return rep @sessionsRouter.get("/{session_id}", response_model=schemas.Session) @@ -465,6 +471,34 @@ def update_session( crud.update_session(db, session, session_id) +@sessionsRouter.get("/{session_id}/download/messages") +def download_session( + session_id: int, + db: Session = Depends(get_db), + current_user: schemas.User = Depends(get_jwt_user), +): + db_session = crud.get_session(db, session_id) + if db_session is None: + raise HTTPException(status_code=404, detail="Session not found") + + if not check_user_level(current_user, models.UserType.ADMIN): + raise HTTPException( + status_code=401, detail="You do not have permission to download this session" + ) + + data = crud.get_messages(db, session_id) + + output = StringIO() + writer = csv.writer(output) + + writer.writerow(models.Message.__table__.columns.keys()) + for row in data: + writer.writerow(row.raw()) + + output.seek(0) + + return StreamingResponse(output, media_type="text/csv", headers={"Content-Disposition": f"attachment; filename={session_id}-messages.csv"}) + @sessionsRouter.post( "/{session_id}/users/{user_id}", status_code=status.HTTP_201_CREATED diff --git a/backend/app/models.py b/backend/app/models.py index 04fd61c3cfe762fef5c73b0ebb030da8410f76a4..c2db8afb652089e4f40e4db186b69b497a98eb3c 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -130,6 +130,16 @@ class Message(Base): feedbacks = relationship("MessageFeedback", backref="message") + def raw(self): + return [ + self.id, + self.message_id, + self.content, + self.user_id, + self.session_id, + self.created_at, + ] + class MessageMetadata(Base): __tablename__ = "message_metadata" diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 634a2f7861ba86c358b7a4b2e448cc315959ef0b..721a36b533e115ad7ae87f7526279e689c6766ca 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -82,6 +82,7 @@ class Session(BaseModel): start_time: NaiveDatetime end_time: NaiveDatetime language: str + length: int | None = None class Config: from_attributes = True diff --git a/frontend/src/lang/fr.json b/frontend/src/lang/fr.json index b1569c9ddd63824e2fc1fe6042baf624ee72d2aa..80642565ae413a791ab8a6eb181d5488ec9c1028 100644 --- a/frontend/src/lang/fr.json +++ b/frontend/src/lang/fr.json @@ -9,7 +9,10 @@ "tutorSelection": "Tuteur", "language": "Langue", "availability": "Disponibilités", - "admin": "Administration" + "admin": { + "users": "Utilisateurs", + "sessions": "Sessions" + } }, "chatbox": { "placeholder": "Écrivez votre message ici...", @@ -190,6 +193,9 @@ "admin": "Admin", "tutor": "Tuteur", "student": "Étudiant", + "admins": "Admin(s)", + "tutors": "Tuteur(s)", + "students": "Étudiant(s)", "0": "Admin", "1": "Tuteur", "2": "Étudiant" @@ -296,6 +302,11 @@ "bool": { "true": "Oui", "false": "Non" + }, + "words": { + "date": "Date", + "messages": "Messages", + "actions": "Actions" } }, "inputs": { diff --git a/frontend/src/lib/components/header.svelte b/frontend/src/lib/components/header.svelte index 2fb790361613b733e6e4eef132a0bd9e2bb0dcd9..3f320e1f3a04e434c6237bd64a4ec145a2505603 100644 --- a/frontend/src/lib/components/header.svelte +++ b/frontend/src/lib/components/header.svelte @@ -95,13 +95,17 @@ <summary class="p-3"> <Icon src={Cog6Tooth} class="h-5 w-5" /> </summary> - <ul class="menu menu-sm dropdown-content absolute right-0"> + <ul class="menu menu-sm dropdown-content absolute right-0 z-10"> <li> <a data-sveltekit-reload href="/admin"> - {$t('header.admin')} + {$t('header.admin.users')} + </a> + </li> + <li> + <a data-sveltekit-reload href="/admin/sessions"> + {$t('header.admin.sessions')} </a> </li> - <li><a>Submenu 2</a></li> </ul> </details> </li> diff --git a/frontend/src/lib/types/session.ts b/frontend/src/lib/types/session.ts index 19c2d1ec9062a6425df82c9d608e528e34d5b291..2fbdbb65211ae4376f927c328b393e6a3272ccb6 100644 --- a/frontend/src/lib/types/session.ts +++ b/frontend/src/lib/types/session.ts @@ -38,6 +38,7 @@ export default class Session { private _lastTyping: Writable<Date | null> = writable(null); private _onlineUsers: Writable<Set<number>> = writable(new Set()); private _onlineTimers: Map<number, number> = new Map(); + private _length: number; private constructor( id: number, @@ -47,7 +48,8 @@ export default class Session { created_at: Date, start_time: Date, end_time: Date, - language: string + language: string, + length: number ) { this._id = id; this._token = token; @@ -58,6 +60,7 @@ export default class Session { this._start_time = start_time; this._end_time = end_time; this._language = language; + this._length = length; } get id(): number { @@ -108,6 +111,10 @@ export default class Session { return this._onlineUsers; } + get length(): number { + return this._length; + } + usersList(maxLength = 30): string { const users = this._users .filter((u) => u.id != get(user)?.id) @@ -375,7 +382,8 @@ export default class Session { parseToLocalDate(json.created_at), parseToLocalDate(json.start_time), parseToLocalDate(json.end_time), - json.language + json.language, + json.length ); session._users = User.parseAll(json.users); diff --git a/frontend/src/routes/admin/+page.svelte b/frontend/src/routes/admin/+page.svelte index 841c103ef900f578c8c501928c20e98a4f11ff20..e3a114a93ded997e4e4d428edd024247aab0cb2a 100644 --- a/frontend/src/routes/admin/+page.svelte +++ b/frontend/src/routes/admin/+page.svelte @@ -3,7 +3,6 @@ import User, { users } from '$lib/types/user'; import { getUsersAPI } from '$lib/api/users'; import { t } from '$lib/services/i18n'; - import { Icon, Trash } from 'svelte-hero-icons'; import UserItem from '$lib/components/users/userItem.svelte'; let ready = false; diff --git a/frontend/src/routes/admin/sessions/+page.svelte b/frontend/src/routes/admin/sessions/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..36fe14165f20a05b97f0cc4e665fe69bfbfdc2ac --- /dev/null +++ b/frontend/src/routes/admin/sessions/+page.svelte @@ -0,0 +1,63 @@ +<script lang="ts"> + import { getSessionsAPI } from '$lib/api/sessions'; + import Session from '$lib/types/session'; + import { onMount } from 'svelte'; + import { t } from '$lib/services/i18n'; + import { displayTime } from '$lib/utils/date'; + import { ArrowDownTray, ArrowRightStartOnRectangle, Icon } from 'svelte-hero-icons'; + import config from '$lib/config'; + + let sessions: Session[] = []; + + onMount(async () => { + sessions = Session.parseAll(await getSessionsAPI()); + }); +</script> + +<table class="table"> + <thead> + <tr> + <th>#</th> + <th>{$t('utils.words.date')}</th> + <th>{$t('users.type.tutors')}</th> + <th>{$t('users.type.students')}</th> + <th># {$t('utils.words.messages')}</th> + <th>{$t('utils.words.actions')}</th> + </tr> + </thead> + <tbody> + {#each sessions as session} + <tr> + <td>{session.id}</td> + <td>{displayTime(session.start_time)}</td> + <td> + {session.users + .filter((u) => u.is_tutor || u.is_admin) + .map((u) => u.nickname) + .join(', ')} + </td> + <td> + {session.users + .filter((u) => !u.is_tutor && !u.is_admin) + .map((u) => u.nickname) + .join(', ')} + </td> + <td> + {session.length} + </td> + <td> + <a class="button" title="Join" href={`/session?id=${session.id}`}> + <Icon src={ArrowRightStartOnRectangle} size="24" /> + </a> + <a + class="button" + title="Download" + href={`${config.API_URL}/sessions/${session.id}/download/messages`} + > + <Icon src={ArrowDownTray} size="24" /> + </a> + </td> + </tr> + {/each} + </tbody> +</table>