From 755a9582db0a275d93e9db467509c258b10c9d24 Mon Sep 17 00:00:00 2001 From: FabrizioCostaMedich Date: Wed, 4 Mar 2026 12:29:48 +0100 Subject: [PATCH 1/2] feat: add faculty exams models and endpoints --- src/common.tsp | 10 +++ src/examples/faculty/exams.tsp | 137 +++++++++++++++++++++++++++++ src/routes/faculty/exams.tsp | 153 +++++++++++++++++++++++++++++++++ 3 files changed, 300 insertions(+) create mode 100644 src/examples/faculty/exams.tsp create mode 100644 src/routes/faculty/exams.tsp diff --git a/src/common.tsp b/src/common.tsp index 0b7dec4..3aec90a 100644 --- a/src/common.tsp +++ b/src/common.tsp @@ -85,6 +85,11 @@ alias NoContentResponse = { @statusCode statusCode: 204; }; +alias AcceptedResponse = { + @statusCode statusCode: 202; + @body body: T; +}; + alias RedirectResponse = { @statusCode statusCode: 302; }; @@ -99,6 +104,11 @@ alias NotFound = { @body body: ErrorResponse; }; +alias NotAuthorized = { + @statusCode statusCode: 401; + @body body: ErrorResponse; +}; + alias ServerError = { @statusCode statusCode: 500; @body body: ErrorResponse; diff --git a/src/examples/faculty/exams.tsp b/src/examples/faculty/exams.tsp new file mode 100644 index 0000000..9f4f7ea --- /dev/null +++ b/src/examples/faculty/exams.tsp @@ -0,0 +1,137 @@ +import "@typespec/http"; + +using Http; + +namespace PolitoAPI; + +// List faculty exams example +const _ex_listFacultyExams_resp = #{ + returnType: #{ + statusCode: 200, + body: #{ + data: #[ + #{ + id: 888122, + courseId: 262147, + courseShortcode: "02JSKOV", + courseName: "Human Computer Interaction", + teacherId: 2154, + type: "Esami scritti a risposta aperta o chiusa tramite PC", + places: #[ + #{ + buildingId: "TO_CIT22", + floorId: "XPTE", + name: "Aula 1P", + roomId: "30", + siteId: "TO_CIT", + }, + ], + status: "available", + bookingStartsAt: utcDateTime.fromISO("2026-03-04T11:07:28.541Z"), + bookingEndsAt: utcDateTime.fromISO("2022-08-31T14:00:00Z"), + examStartsAt: utcDateTime.fromISO("2022-09-02T14:00:00Z"), + examEndsAt: utcDateTime.fromISO("2022-09-02T17:00:00Z"), + bookedCount: 42, + availableCount: 8, + question: null, + feedback: null, + isReschedulable: false, + notes: null, + moduleNumber: 1, + requestReason: null, + requestDetails: null, + enrolled: #[ + #{ + personId: "290683", + name: "Mario Rossi", + degree: "Ingegneria Informatica", + email: "mario.rossi@studenti.polito.it", + }, + ], + }, + ], + }, + }, +}; + +// Get faculty exam details example +const _ex_getFacultyExam_resp = #{ + returnType: #{ + statusCode: 200, + body: #{ + data: #{ + id: 888122, + courseId: 262147, + courseShortcode: "02JSKOV", + courseName: "Human Computer Interaction", + teacherId: 2154, + type: "Esami scritti a risposta aperta o chiusa tramite PC", + places: #[ + #{ + buildingId: "TO_CIT22", + floorId: "XPTE", + name: "Aula 1P", + roomId: "30", + siteId: "TO_CIT", + }, + ], + status: "available", + bookingStartsAt: utcDateTime.fromISO("2026-03-04T11:07:28.541Z"), + bookingEndsAt: utcDateTime.fromISO("2022-08-31T14:00:00Z"), + examStartsAt: utcDateTime.fromISO("2022-09-02T14:00:00Z"), + examEndsAt: utcDateTime.fromISO("2022-09-02T17:00:00Z"), + bookedCount: 42, + availableCount: 8, + question: null, + feedback: null, + isReschedulable: false, + notes: null, + moduleNumber: 1, + requestReason: null, + requestDetails: null, + enrolled: #[ + #{ + personId: "290683", + name: "Mario Rossi", + degree: "Ingegneria Informatica", + email: "mario.rossi@studenti.polito.it", + }, + #{ + personId: "291045", + name: "Laura Bianchi", + degree: "Ingegneria del Cinema", + email: "laura.bianchi@studenti.polito.it", + }, + ], + }, + }, + }, +}; + +// Notify exam subscribers example +const _ex_notifyExamSubscribers_resp = #{ + returnType: #{ + statusCode: 202, + body: #{ + data: #{ + status: "accepted", + trackingId: "track-abc-123", + }, + }, + }, +}; + +// Add student to exam example (201 - created) +const _ex_addStudentToExam_resp = #{ + returnType: #{ + statusCode: 201, + body: #{ + data: #{ + personId: "292145", + name: "Anna Ferrari", + degree: "Ingegneria Informatica", + email: "anna.ferrari@studenti.polito.it", + }, + }, + }, +}; diff --git a/src/routes/faculty/exams.tsp b/src/routes/faculty/exams.tsp new file mode 100644 index 0000000..4ca3a24 --- /dev/null +++ b/src/routes/faculty/exams.tsp @@ -0,0 +1,153 @@ +import "@typespec/http"; +import "@typespec/openapi3"; + +import "../../common.tsp"; +import "../../examples/faculty/exams.tsp"; + +using Http; + +namespace PolitoAPI; + +model ExamEnrollment { + @example("290683") + personId: string; + + @example("Mario Rossi") + name: string; + + @example("Ingegneria Informatica") + degree: string | null; + + @example("mario.rossi@studenti.polito.it") + email: string | null; +} + +model FacultyExam { + @example(888122) + id: integer; + + @example(262147) + courseId: integer; + + @example("02JSKOV") + courseShortcode: string; + + @example("Human Computer Interaction") + courseName: string; + + @example(2154) + teacherId: integer; + + @example("Esami scritti a risposta aperta o chiusa tramite PC") + type: string; + + places: PlaceRef[]; + + @example("available") + status: string; + + @example(utcDateTime.fromISO("2026-03-04T11:07:28.541Z")) + bookingStartsAt: utcDateTime; + + @example(utcDateTime.fromISO("2022-08-31T14:00:00Z")) + bookingEndsAt: utcDateTime; + + @example(utcDateTime.fromISO("2022-09-02T14:00:00Z")) + examStartsAt: utcDateTime; + + @example(utcDateTime.fromISO("2022-09-02T17:00:00Z")) + examEndsAt: utcDateTime; + + @example(42) + bookedCount: integer; + + @example(8) + availableCount: integer; + + question: string | null; + feedback: string | null; + + @example(false) + isReschedulable: boolean; + + notes: string | null; + + @example(1) + moduleNumber: integer; + + requestReason: string | null; + requestDetails: string | null; + + enrolled: ExamEnrollment[]; +} + +model ExamNotificationRequest { + @example("Importante: modifica orario esame") + subject: string; + + @example("Si comunica che l'orario dell'esame è stato spostato alle ore 14:00.") + message: string; + + @example(#["email", "sms"]) + channels: string[]; + + @example(#["290683", "291045"]) + recipientPersonIds?: string[]; +} + +model AcceptedData { + @example("accepted") + status: string; + + @example("track-abc-123") + trackingId: string; +} + +@tag("Faculty - Exams") +@route("/faculty/exams") +interface FacultyExams { + @get + @summary("List faculty exams | Elenco appelli del docente") + @doc("Elenca gli appelli d'esame del docente con filtri opzionali.") + @opExample(_ex_listFacultyExams_resp) + listFacultyExams( + @query courseId?: string, + @query dateFrom?: plainDate, + @query dateTo?: plainDate, + @query page?: integer = 1, + @query pageSize?: integer = 20, + ): OkDataResponse | NotAuthorized | ServerError; + + @route("/{examId}") + @get + @summary("Get faculty exam details | Dettaglio appello con iscritti") + @doc("Restituisce i dettagli dell'appello, inclusa la lista degli studenti iscritti.") + @opExample(_ex_getFacultyExam_resp) + getFacultyExam(@path examId: integer): + | OkDataResponse + | NotAuthorized + | NotFound + | ServerError; + + @route("/{examId}/notify") + @post + @summary("Notify exam subscribers | Invia comunicazione agli iscritti all'esame") + @doc("Invia un messaggio/avviso agli studenti iscritti a un appello specifico, permettendo l'invio sia all'intera lista sia a un sottoinsieme selezionato.") + @opExample(_ex_notifyExamSubscribers_resp) + notifyExamSubscribers( + @path examId: integer, + @body body: ExamNotificationRequest, + ): AcceptedResponse<{ data: AcceptedData }> | NotAuthorized | BaseErrors; + + @route("/{examId}/students/{studentId}") + @put + @summary("Add student to exam | Aggiungi studente all'esame") + @doc("Inserisce manualmente uno studente tra gli iscritti a un appello (casi eccezionali). Operazione idempotente.") + @opExample(_ex_addStudentToExam_resp) + addStudentToExam(@path examId: integer, @path studentId: integer): + | OkDataResponse + | OkDataCreatedResponse + | NotAuthorized + | NotFound + | ServerError; +} From cbab9eb6b0ac4c1f04ac1cb71e80b9b0bc02add9 Mon Sep 17 00:00:00 2001 From: FabrizioCostaMedich Date: Mon, 29 Jun 2026 00:09:19 +0200 Subject: [PATCH 2/2] fix(exam): change and add some faculty exams endpoints and models --- dist/clients/faculty/openapi.yaml | 507 ++++++++++++++++++++++++++++++ dist/clients/student/openapi.yaml | 100 +++--- src/clients/faculty/main.tsp | 1 + src/examples/faculty/exams.tsp | 134 ++++---- src/routes/common/exams.tsp | 22 ++ src/routes/faculty/exams.tsp | 148 ++++----- src/routes/student/exams.tsp | 32 +- 7 files changed, 728 insertions(+), 216 deletions(-) create mode 100644 src/routes/common/exams.tsp diff --git a/dist/clients/faculty/openapi.yaml b/dist/clients/faculty/openapi.yaml index a57bf43..836c6f0 100644 --- a/dist/clients/faculty/openapi.yaml +++ b/dist/clients/faculty/openapi.yaml @@ -15,6 +15,7 @@ tags: - name: News - name: People - name: Places + - name: Faculty - Exams paths: /announcements: get: @@ -1579,6 +1580,379 @@ paths: $ref: '#/components/schemas/ErrorResponse' tags: - Event Admissions + /exams: + get: + operationId: FacultyExams_getExams + summary: List exams | Elenca esami + parameters: [] + responses: + '200': + description: The request has succeeded. + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/ExamBase' + required: + - data + example: + data: + - id: 888122 + courseId: 262147 + courseShortcode: 02JSKOV + courseName: Human Computer Interaction + type: Esami scritti a risposta aperta o chiusa tramite PC + places: + - buildingId: TO_CIT22 + floorId: XPTE + name: Aula 1P + roomId: '30' + siteId: TO_CIT + bookingEndsAt: '2026-08-31T14:00:00Z' + examStartsAt: '2026-09-02T14:00:00Z' + examEndsAt: '2026-09-02T17:00:00Z' + bookedCount: 42 + availableCount: 8 + '400': + description: The server could not understand the request due to invalid syntax. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + tags: + - Faculty - Exams + /exams/{examId}/attendance: + post: + operationId: FacultyExams_checkAttendance + summary: Check exam attendance | Verifica presenze all'esame + description: Registra le presenze passando l'array delle matricole presenti. Restituisce lo stato aggiornato delle presenze. + parameters: + - name: examId + in: path + required: true + schema: + type: integer + responses: + '200': + description: The request has succeeded. + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/ExamAttendee' + required: + - data + example: + data: + - username: '290683' + name: Mario + surname: Rossi + needsExtraTime: true + extraTimeMinutes: 30 + isPresent: true + - username: '291045' + name: Laura + surname: Bianchi + needsExtraTime: false + extraTimeMinutes: null + isPresent: true + '401': + description: Access is unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: The server cannot find the requested resource. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + tags: + - Faculty - Exams + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CheckAttendanceRequest' + /exams/{examId}/notify: + post: + operationId: FacultyExams_notifyExamSubscribers + summary: Notify exam subscribers | Invia comunicazione agli iscritti all'esame + description: Invia un messaggio/avviso agli studenti iscritti a un appello specifico, permettendo l'invio sia all'intera lista sia a un sottoinsieme selezionato. + parameters: + - name: examId + in: path + required: true + schema: + type: integer + responses: + '202': + description: The request has been accepted for processing, but processing has not yet completed. + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/AcceptedData' + required: + - data + example: + data: + status: accepted + trackingId: track-abc-123 + '400': + description: The server could not understand the request due to invalid syntax. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Access is unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + tags: + - Faculty - Exams + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ExamNotificationRequest' + /exams/{examId}/students: + get: + operationId: FacultyExams_listStudentsInExam + summary: List students in exam | Elenca studenti nell'esame + description: Restituisce la lista degli studenti iscritti a un appello specifico. + parameters: + - name: examId + in: path + required: true + schema: + type: integer + responses: + '200': + description: The request has succeeded. + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/ExamAttendee' + required: + - data + example: + data: + - username: '290683' + name: Mario + surname: Rossi + needsExtraTime: true + extraTimeMinutes: 30 + isPresent: false + - username: '291045' + name: Laura + surname: Bianchi + needsExtraTime: false + extraTimeMinutes: null + isPresent: false + '401': + description: Access is unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: The server cannot find the requested resource. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + tags: + - Faculty - Exams + /exams/{examId}/students/{studentId}: + put: + operationId: FacultyExams_addStudentToExam + summary: Add student to exam | Aggiungi studente all'esame + description: Inserisce manualmente uno studente tra gli iscritti a un appello (casi eccezionali). Operazione idempotente. + parameters: + - name: examId + in: path + required: true + schema: + type: integer + - name: studentId + in: path + required: true + schema: + type: integer + responses: + '200': + description: The request has succeeded. + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/ExamEnrollment' + required: + - data + '201': + description: The request has succeeded and a new resource has been created as a result. + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/ExamEnrollment' + required: + - data + example: + data: + username: '292145' + name: Anna + surname: Ferrari + '401': + description: Access is unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: The server cannot find the requested resource. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + tags: + - Faculty - Exams + get: + operationId: FacultyExams_checkStudentEnrollment + summary: Check if student registration for the exam is possible | Verifica se possibile l'iscrizione studente all'esame + parameters: + - name: examId + in: path + required: true + schema: + type: integer + - name: studentId + in: path + required: true + schema: + type: integer + responses: + '204': + description: 'There is no content to send for this request, but the headers may be useful. ' + '401': + description: Access is unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: The server cannot find the requested resource. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + default: + description: An unexpected error response. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + tags: + - Faculty - Exams + post: + operationId: FacultyExams_enrollStudentToExam + summary: Enroll student to exam | Iscrivi studente all'esame + description: Iscrive uno studente a un appello specifico. Operazione + parameters: + - name: examId + in: path + required: true + schema: + type: integer + - name: studentId + in: path + required: true + schema: + type: integer + responses: + '204': + description: 'There is no content to send for this request, but the headers may be useful. ' + '401': + description: Access is unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: The server cannot find the requested resource. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + tags: + - Faculty - Exams /lectures: get: operationId: Lectures_getLectures @@ -2860,6 +3234,18 @@ security: - BearerAuth: [] components: schemas: + AcceptedData: + type: object + required: + - status + - trackingId + properties: + status: + type: string + example: accepted + trackingId: + type: string + example: track-abc-123 Announcement: type: object required: @@ -3366,6 +3752,19 @@ components: $ref: '#/components/schemas/PlaceCategoryOverview' geoJson: $ref: '#/components/schemas/GeoJsonPolygon' + CheckAttendanceRequest: + type: object + required: + - presentUsernames + properties: + presentUsernames: + type: array + items: + type: string + description: Matricole degli studenti risultati presenti + example: + - '290683' + - '291045' Client: type: object required: @@ -3935,6 +4334,114 @@ components: remainingAdmissions: type: integer example: 8 + ExamAttendee: + type: object + required: + - needsExtraTime + - extraTimeMinutes + - isPresent + properties: + needsExtraTime: + type: boolean + description: Se lo studente ha diritto a tempo aggiuntivo (DSA/BES) + example: true + extraTimeMinutes: + type: integer + nullable: true + description: Minuti aggiuntivi concessi, null se non applicabile + example: 30 + isPresent: + type: boolean + description: Se lo studente risulta presente all'esame + example: false + allOf: + - $ref: '#/components/schemas/ExamEnrollment' + ExamBase: + type: object + required: + - id + - courseId + - courseShortcode + - courseName + - type + - bookingEndsAt + - examStartsAt + - examEndsAt + - bookedCount + - availableCount + properties: + id: + type: integer + courseId: + type: number + courseShortcode: + type: string + courseName: + type: string + type: + type: string + places: + type: array + items: + $ref: '#/components/schemas/PlaceRef' + bookingEndsAt: + type: string + format: date-time + examStartsAt: + type: string + format: date-time + nullable: true + examEndsAt: + type: string + format: date-time + nullable: true + bookedCount: + type: integer + availableCount: + type: integer + ExamEnrollment: + type: object + required: + - username + - name + - surname + properties: + username: + type: string + example: '290683' + name: + type: string + example: Mario + surname: + type: string + example: Rossi + ExamNotificationRequest: + type: object + required: + - subject + - message + - channels + properties: + subject: + type: string + example: 'Importante: modifica orario esame' + message: + type: string + example: Si comunica che l'orario dell'esame è stato spostato alle ore 14:00. + channels: + type: array + items: + type: string + example: + - email + - sms + recipientPersonIds: + type: array + items: + type: string + example: + - '290683' + - '291045' FcmRegistrationRequest: type: object properties: diff --git a/dist/clients/student/openapi.yaml b/dist/clients/student/openapi.yaml index c63f2c0..c792b9f 100644 --- a/dist/clients/student/openapi.yaml +++ b/dist/clients/student/openapi.yaml @@ -5642,19 +5642,9 @@ components: Exam: type: object required: - - id - - courseId - - courseShortcode - - courseName - teacherId - - type - status - bookingStartsAt - - bookingEndsAt - - examStartsAt - - examEndsAt - - bookedCount - - availableCount - question - feedback - isReschedulable @@ -5663,51 +5653,18 @@ components: - requestReason - requestDetails properties: - id: - type: integer - courseId: - type: number - courseShortcode: - type: string - courseName: - type: string teacherId: type: integer - type: - type: string places: type: array items: $ref: '#/components/schemas/PlaceRef' status: - type: string - enum: - - available - - booked - - requestable - - requested - - requestAccepted - - requestRejected - - unavailable + $ref: '#/components/schemas/ExamStatus' bookingStartsAt: type: string format: date-time nullable: true - bookingEndsAt: - type: string - format: date-time - examStartsAt: - type: string - format: date-time - nullable: true - examEndsAt: - type: string - format: date-time - nullable: true - bookedCount: - type: integer - availableCount: - type: integer question: type: object properties: @@ -5742,6 +5699,51 @@ components: requestDetails: type: string nullable: true + allOf: + - $ref: '#/components/schemas/ExamBase' + ExamBase: + type: object + required: + - id + - courseId + - courseShortcode + - courseName + - type + - bookingEndsAt + - examStartsAt + - examEndsAt + - bookedCount + - availableCount + properties: + id: + type: integer + courseId: + type: number + courseShortcode: + type: string + courseName: + type: string + type: + type: string + places: + type: array + items: + $ref: '#/components/schemas/PlaceRef' + bookingEndsAt: + type: string + format: date-time + examStartsAt: + type: string + format: date-time + nullable: true + examEndsAt: + type: string + format: date-time + nullable: true + bookedCount: + type: integer + availableCount: + type: integer ExamGrade: type: object required: @@ -5787,6 +5789,16 @@ components: type: boolean description: Defines whether the credits from the exam count toward the total required for graduation example: true + ExamStatus: + type: string + enum: + - available + - booked + - requestable + - requested + - requestAccepted + - requestRejected + - unavailable FcmRegistrationRequest: type: object properties: diff --git a/src/clients/faculty/main.tsp b/src/clients/faculty/main.tsp index 2e2e803..3dbe431 100644 --- a/src/clients/faculty/main.tsp +++ b/src/clients/faculty/main.tsp @@ -14,6 +14,7 @@ import "../../routes/common/people.tsp"; import "../../routes/common/places.tsp"; // Faculty routes +import "../../routes/faculty/exams.tsp"; import "./version.tsp"; diff --git a/src/examples/faculty/exams.tsp b/src/examples/faculty/exams.tsp index 9f4f7ea..537d362 100644 --- a/src/examples/faculty/exams.tsp +++ b/src/examples/faculty/exams.tsp @@ -4,8 +4,8 @@ using Http; namespace PolitoAPI; -// List faculty exams example -const _ex_listFacultyExams_resp = #{ +// List exams example +const _ex_getExams_resp = #{ returnType: #{ statusCode: 200, body: #{ @@ -15,7 +15,6 @@ const _ex_listFacultyExams_resp = #{ courseId: 262147, courseShortcode: "02JSKOV", courseName: "Human Computer Interaction", - teacherId: 2154, type: "Esami scritti a risposta aperta o chiusa tramite PC", places: #[ #{ @@ -26,84 +25,67 @@ const _ex_listFacultyExams_resp = #{ siteId: "TO_CIT", }, ], - status: "available", - bookingStartsAt: utcDateTime.fromISO("2026-03-04T11:07:28.541Z"), - bookingEndsAt: utcDateTime.fromISO("2022-08-31T14:00:00Z"), - examStartsAt: utcDateTime.fromISO("2022-09-02T14:00:00Z"), - examEndsAt: utcDateTime.fromISO("2022-09-02T17:00:00Z"), + bookingEndsAt: utcDateTime.fromISO("2026-08-31T14:00:00Z"), + examStartsAt: utcDateTime.fromISO("2026-09-02T14:00:00Z"), + examEndsAt: utcDateTime.fromISO("2026-09-02T17:00:00Z"), bookedCount: 42, availableCount: 8, - question: null, - feedback: null, - isReschedulable: false, - notes: null, - moduleNumber: 1, - requestReason: null, - requestDetails: null, - enrolled: #[ - #{ - personId: "290683", - name: "Mario Rossi", - degree: "Ingegneria Informatica", - email: "mario.rossi@studenti.polito.it", - }, - ], }, ], }, }, }; -// Get faculty exam details example -const _ex_getFacultyExam_resp = #{ +// List students in exam example (with attendance + extra time info) +const _ex_listStudentsInExam_resp = #{ returnType: #{ statusCode: 200, body: #{ - data: #{ - id: 888122, - courseId: 262147, - courseShortcode: "02JSKOV", - courseName: "Human Computer Interaction", - teacherId: 2154, - type: "Esami scritti a risposta aperta o chiusa tramite PC", - places: #[ - #{ - buildingId: "TO_CIT22", - floorId: "XPTE", - name: "Aula 1P", - roomId: "30", - siteId: "TO_CIT", - }, - ], - status: "available", - bookingStartsAt: utcDateTime.fromISO("2026-03-04T11:07:28.541Z"), - bookingEndsAt: utcDateTime.fromISO("2022-08-31T14:00:00Z"), - examStartsAt: utcDateTime.fromISO("2022-09-02T14:00:00Z"), - examEndsAt: utcDateTime.fromISO("2022-09-02T17:00:00Z"), - bookedCount: 42, - availableCount: 8, - question: null, - feedback: null, - isReschedulable: false, - notes: null, - moduleNumber: 1, - requestReason: null, - requestDetails: null, - enrolled: #[ - #{ - personId: "290683", - name: "Mario Rossi", - degree: "Ingegneria Informatica", - email: "mario.rossi@studenti.polito.it", - }, - #{ - personId: "291045", - name: "Laura Bianchi", - degree: "Ingegneria del Cinema", - email: "laura.bianchi@studenti.polito.it", - }, - ], - }, + data: #[ + #{ + username: "290683", + name: "Mario", + surname: "Rossi", + needsExtraTime: true, + extraTimeMinutes: 30, + isPresent: false, + }, + #{ + username: "291045", + name: "Laura", + surname: "Bianchi", + needsExtraTime: false, + extraTimeMinutes: null, + isPresent: false, + }, + ], + }, + }, +}; + +// Check exam attendance example +const _ex_checkAttendance_resp = #{ + returnType: #{ + statusCode: 200, + body: #{ + data: #[ + #{ + username: "290683", + name: "Mario", + surname: "Rossi", + needsExtraTime: true, + extraTimeMinutes: 30, + isPresent: true, + }, + #{ + username: "291045", + name: "Laura", + surname: "Bianchi", + needsExtraTime: false, + extraTimeMinutes: null, + isPresent: true, + }, + ], }, }, }; @@ -127,11 +109,15 @@ const _ex_addStudentToExam_resp = #{ statusCode: 201, body: #{ data: #{ - personId: "292145", - name: "Anna Ferrari", - degree: "Ingegneria Informatica", - email: "anna.ferrari@studenti.polito.it", + username: "292145", + name: "Anna", + surname: "Ferrari", }, }, }, }; + +// Enroll / check student enrollment example (204 - no content) +const _ex_enrollStudentToExam_resp = #{ + returnType: #{ statusCode: 204 }, +}; diff --git a/src/routes/common/exams.tsp b/src/routes/common/exams.tsp new file mode 100644 index 0000000..bb10157 --- /dev/null +++ b/src/routes/common/exams.tsp @@ -0,0 +1,22 @@ +import "@typespec/http"; +import "@typespec/openapi3"; + +import "../../common.tsp"; + +using Http; + +namespace PolitoAPI; + +model ExamBase { + id: integer; + courseId: numeric; + courseShortcode: string; + courseName: string; + type: string; + places?: PlaceRef[]; + bookingEndsAt: utcDateTime; + examStartsAt: utcDateTime | null; + examEndsAt: utcDateTime | null; + bookedCount: integer; + availableCount: integer; +} diff --git a/src/routes/faculty/exams.tsp b/src/routes/faculty/exams.tsp index 4ca3a24..4fcae4f 100644 --- a/src/routes/faculty/exams.tsp +++ b/src/routes/faculty/exams.tsp @@ -2,6 +2,7 @@ import "@typespec/http"; import "@typespec/openapi3"; import "../../common.tsp"; +import "../../routes/common/exams.tsp"; import "../../examples/faculty/exams.tsp"; using Http; @@ -10,75 +11,33 @@ namespace PolitoAPI; model ExamEnrollment { @example("290683") - personId: string; + username: string; - @example("Mario Rossi") + @example("Mario") name: string; - @example("Ingegneria Informatica") - degree: string | null; - - @example("mario.rossi@studenti.polito.it") - email: string | null; + @example("Rossi") + surname: string; } -model FacultyExam { - @example(888122) - id: integer; - - @example(262147) - courseId: integer; - - @example("02JSKOV") - courseShortcode: string; - - @example("Human Computer Interaction") - courseName: string; - - @example(2154) - teacherId: integer; - - @example("Esami scritti a risposta aperta o chiusa tramite PC") - type: string; - - places: PlaceRef[]; - - @example("available") - status: string; - - @example(utcDateTime.fromISO("2026-03-04T11:07:28.541Z")) - bookingStartsAt: utcDateTime; +model ExamAttendee extends ExamEnrollment { + /** Se lo studente ha diritto a tempo aggiuntivo (DSA/BES) */ + @example(true) + needsExtraTime: boolean; - @example(utcDateTime.fromISO("2022-08-31T14:00:00Z")) - bookingEndsAt: utcDateTime; - - @example(utcDateTime.fromISO("2022-09-02T14:00:00Z")) - examStartsAt: utcDateTime; - - @example(utcDateTime.fromISO("2022-09-02T17:00:00Z")) - examEndsAt: utcDateTime; - - @example(42) - bookedCount: integer; - - @example(8) - availableCount: integer; - - question: string | null; - feedback: string | null; + /** Minuti aggiuntivi concessi, null se non applicabile */ + @example(30) + extraTimeMinutes: integer | null; + /** Se lo studente risulta presente all'esame */ @example(false) - isReschedulable: boolean; - - notes: string | null; - - @example(1) - moduleNumber: integer; - - requestReason: string | null; - requestDetails: string | null; + isPresent: boolean; +} - enrolled: ExamEnrollment[]; +model CheckAttendanceRequest { + /** Matricole degli studenti risultati presenti */ + @example(#["290683", "291045"]) + presentUsernames: string[]; } model ExamNotificationRequest { @@ -104,27 +63,34 @@ model AcceptedData { } @tag("Faculty - Exams") -@route("/faculty/exams") +@route("/exams") interface FacultyExams { @get - @summary("List faculty exams | Elenco appelli del docente") - @doc("Elenca gli appelli d'esame del docente con filtri opzionali.") - @opExample(_ex_listFacultyExams_resp) - listFacultyExams( - @query courseId?: string, - @query dateFrom?: plainDate, - @query dateTo?: plainDate, - @query page?: integer = 1, - @query pageSize?: integer = 20, - ): OkDataResponse | NotAuthorized | ServerError; - - @route("/{examId}") + @summary("List exams | Elenca esami") + @opExample(_ex_getExams_resp) + getExams(): OkDataResponse | BaseErrors; + + @route("/{examId}/students") @get - @summary("Get faculty exam details | Dettaglio appello con iscritti") - @doc("Restituisce i dettagli dell'appello, inclusa la lista degli studenti iscritti.") - @opExample(_ex_getFacultyExam_resp) - getFacultyExam(@path examId: integer): - | OkDataResponse + @summary("List students in exam | Elenca studenti nell'esame") + @doc("Restituisce la lista degli studenti iscritti a un appello specifico.") + @opExample(_ex_listStudentsInExam_resp) + listStudentsInExam(@path examId: integer): + | OkDataResponse + | NotAuthorized + | NotFound + | ServerError; + + @route("/{examId}/attendance") + @post + @summary("Check exam attendance | Verifica presenze all'esame") + @doc("Registra le presenze passando l'array delle matricole presenti. Restituisce lo stato aggiornato delle presenze.") + @opExample(_ex_checkAttendance_resp) + checkAttendance( + @path examId: integer, + @body body: CheckAttendanceRequest, + ): + | OkDataResponse | NotAuthorized | NotFound | ServerError; @@ -137,7 +103,9 @@ interface FacultyExams { notifyExamSubscribers( @path examId: integer, @body body: ExamNotificationRequest, - ): AcceptedResponse<{ data: AcceptedData }> | NotAuthorized | BaseErrors; + ): AcceptedResponse<{ + data: AcceptedData; + }> | NotAuthorized | BaseErrors; @route("/{examId}/students/{studentId}") @put @@ -150,4 +118,26 @@ interface FacultyExams { | NotAuthorized | NotFound | ServerError; + + @route("/{examId}/students/{studentId}") + @get + @summary("Check if student registration for the exam is possible | Verifica se possibile l'iscrizione studente all'esame") + @opExample(_ex_enrollStudentToExam_resp) + checkStudentEnrollment(@path examId: integer, @path studentId: integer): + | NoContentResponse + | ErrorResponse + | NotAuthorized + | NotFound + | ServerError; + + @route("/{examId}/students/{studentId}") + @post + @summary("Enroll student to exam | Iscrivi studente all'esame") + @doc("Iscrive uno studente a un appello specifico. Operazione") + @opExample(_ex_enrollStudentToExam_resp) + enrollStudentToExam(@path examId: integer, @path studentId: integer): + | NoContentResponse + | NotAuthorized + | NotFound + | ServerError; } diff --git a/src/routes/student/exams.tsp b/src/routes/student/exams.tsp index 28c1f78..02f3b57 100644 --- a/src/routes/student/exams.tsp +++ b/src/routes/student/exams.tsp @@ -2,34 +2,28 @@ import "@typespec/http"; import "@typespec/openapi3"; import "../../common.tsp"; +import "../../routes/common/exams.tsp"; import "../../examples/student/exams.tsp"; using Http; namespace PolitoAPI; -model Exam { - id: integer; - courseId: numeric; - courseShortcode: string; - courseName: string; +union ExamStatus { + available: "available", + booked: "booked", + requestable: "requestable", + requested: "requested", + requestAccepted: "requestAccepted", + requestRejected: "requestRejected", + unavailable: "unavailable", +} + +model Exam extends ExamBase { teacherId: integer; - type: string; places?: PlaceRef[]; - status: - | "available" - | "booked" - | "requestable" - | "requested" - | "requestAccepted" - | "requestRejected" - | "unavailable"; + status: ExamStatus; bookingStartsAt: utcDateTime | null; - bookingEndsAt: utcDateTime; - examStartsAt: utcDateTime | null; - examEndsAt: utcDateTime | null; - bookedCount: integer; - availableCount: integer; question: { id: integer; statement: string;