From a81b036baf21b6c8de35367085f0564b20f26cc8 Mon Sep 17 00:00:00 2001 From: Brieuc Dubois <git@bhasher.com> Date: Wed, 9 Oct 2024 17:18:25 +0300 Subject: [PATCH] First implementation of admin dashboard #88 --- backend/app/crud.py | 21 +++++-- backend/app/main.py | 36 ++++++++++- backend/app/models.py | 10 +++ backend/app/schemas.py | 1 + frontend/src/lang/fr.json | 13 +++- frontend/src/lib/components/header.svelte | 10 ++- frontend/src/lib/types/session.ts | 12 +++- frontend/src/routes/admin/+page.svelte | 1 - .../src/routes/admin/sessions/+page.svelte | 63 +++++++++++++++++++ 9 files changed, 154 insertions(+), 13 deletions(-) create mode 100644 frontend/src/routes/admin/sessions/+page.svelte diff --git a/backend/app/crud.py b/backend/app/crud.py index a7bd7601..57d3c044 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 677d8b0d..8b578828 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 04fd61c3..c2db8afb 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 634a2f78..721a36b5 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 b1569c9d..80642565 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 2fb79036..3f320e1f 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 19c2d1ec..2fbdbb65 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 841c103e..e3a114a9 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 00000000..36fe1416 --- /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> -- GitLab