Merge pull request #36 from fedyna-k/PMPR-35

Pmpr 35
This commit is contained in:
Kevin FEDYNA
2025-01-28 18:38:17 +01:00
committed by GitHub
23 changed files with 440 additions and 235 deletions
+8 -9
View File
@@ -1,15 +1,14 @@
create table promotions ( create table promotions (
id integer primary key autoincrement, id integer primary key autoincrement,
name text, endyear integer,
endyear integer, current integer
current integer
); );
create table students ( create table students (
userId text primary key, userId text primary key,
firstName text, firstName text,
lastName text, lastName text,
mail text, mail text,
promotionId integer, promotionId integer,
foreign key(promotionId) references promotions(id) foreign key(promotionId) references promotions(id)
); );
+2 -1
View File
@@ -1,9 +1,10 @@
import { type RegularTagNode, type TextNode } from "@melvdouc/xml-parser"; import { type RegularTagNode, type TextNode } from "@melvdouc/xml-parser";
import { AsyncRoute } from "$fresh/src/server/types.ts"; import { AsyncRoute } from "$fresh/src/server/types.ts";
interface AuthenticatedState { export interface AuthenticatedState {
isAuthenticated: true; isAuthenticated: true;
session: CasContent; session: CasContent;
availablePages: Record<string, string>;
} }
interface UnauthenticatedState { interface UnauthenticatedState {
+3 -1
View File
@@ -29,12 +29,14 @@
"@popov/jwt": "jsr:@popov/jwt@^1.0.1", "@popov/jwt": "jsr:@popov/jwt@^1.0.1",
"@psych/sheet": "jsr:@psych/sheet@^1.0.6", "@psych/sheet": "jsr:@psych/sheet@^1.0.6",
"@std/cli": "jsr:@std/cli@^1.0.10", "@std/cli": "jsr:@std/cli@^1.0.10",
"@std/dotenv": "jsr:@std/dotenv@^0.225.3",
"preact": "https://esm.sh/preact@10.22.0", "preact": "https://esm.sh/preact@10.22.0",
"preact/": "https://esm.sh/preact@10.22.0/", "preact/": "https://esm.sh/preact@10.22.0/",
"@preact/signals": "https://esm.sh/*@preact/signals@1.2.2", "@preact/signals": "https://esm.sh/*@preact/signals@1.2.2",
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.1", "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.1",
"$std/": "https://deno.land/std@0.216.0/", "$std/": "https://deno.land/std@0.216.0/",
"$root/": "./" "$root/": "./",
"$apps/": "./routes/(apps)/"
}, },
"compilerOptions": { "compilerOptions": {
"jsx": "react-jsx", "jsx": "react-jsx",
+2
View File
@@ -1,6 +1,8 @@
import { defineConfig } from "$fresh/server.ts"; import { defineConfig } from "$fresh/server.ts";
import ensureDatabases from "$root/databases/ensure.ts"; import ensureDatabases from "$root/databases/ensure.ts";
import { load } from "@std/dotenv";
await load({ envPath: "./.env.development.local", export: true });
await ensureDatabases(); await ensureDatabases();
export default defineConfig({ export default defineConfig({
server: { server: {
+6 -6
View File
@@ -3,6 +3,7 @@
// This file is automatically updated during development when running `dev.ts`. // This file is automatically updated during development when running `dev.ts`.
import * as $_apps_layout from "./routes/(apps)/_layout.tsx"; import * as $_apps_layout from "./routes/(apps)/_layout.tsx";
import * as $_apps_middleware from "./routes/(apps)/_middleware.ts";
import * as $_apps_mobility_api_insert_mobility from "./routes/(apps)/mobility/api/insert_mobility.ts"; import * as $_apps_mobility_api_insert_mobility from "./routes/(apps)/mobility/api/insert_mobility.ts";
import * as $_apps_mobility_index from "./routes/(apps)/mobility/index.tsx"; import * as $_apps_mobility_index from "./routes/(apps)/mobility/index.tsx";
import * as $_apps_mobility_partials_admin_edit_mobility from "./routes/(apps)/mobility/partials/(admin)/edit_mobility.tsx"; import * as $_apps_mobility_partials_admin_edit_mobility from "./routes/(apps)/mobility/partials/(admin)/edit_mobility.tsx";
@@ -12,12 +13,12 @@ import * as $_apps_notes_index from "./routes/(apps)/notes/index.tsx";
import * as $_apps_notes_partials_admin_courses from "./routes/(apps)/notes/partials/(admin)/courses.tsx"; import * as $_apps_notes_partials_admin_courses from "./routes/(apps)/notes/partials/(admin)/courses.tsx";
import * as $_apps_notes_partials_index from "./routes/(apps)/notes/partials/index.tsx"; import * as $_apps_notes_partials_index from "./routes/(apps)/notes/partials/index.tsx";
import * as $_apps_notes_partials_notes from "./routes/(apps)/notes/partials/notes.tsx"; import * as $_apps_notes_partials_notes from "./routes/(apps)/notes/partials/notes.tsx";
import * as $_apps_students_api_insert_students from "./routes/(apps)/students/api/insert_students.ts"; import * as $_apps_students_api_students from "./routes/(apps)/students/api/students.ts";
import * as $_apps_students_index from "./routes/(apps)/students/index.tsx"; import * as $_apps_students_index from "./routes/(apps)/students/index.tsx";
import * as $_apps_students_partials_admin_consult from "./routes/(apps)/students/partials/(admin)/consult.tsx"; import * as $_apps_students_partials_admin_consult from "./routes/(apps)/students/partials/(admin)/consult.tsx";
import * as $_apps_students_partials_admin_upload from "./routes/(apps)/students/partials/(admin)/upload.tsx"; import * as $_apps_students_partials_admin_upload from "./routes/(apps)/students/partials/(admin)/upload.tsx";
import * as $_apps_students_partials_index from "./routes/(apps)/students/partials/index.tsx"; import * as $_apps_students_partials_index from "./routes/(apps)/students/partials/index.tsx";
import * as $_apps_students_partials_overview from "./routes/(apps)/students/partials/overview.tsx"; import * as $_apps_students_types_d from "./routes/(apps)/students/types.d.ts";
import * as $_404 from "./routes/_404.tsx"; import * as $_404 from "./routes/_404.tsx";
import * as $_app from "./routes/_app.tsx"; import * as $_app from "./routes/_app.tsx";
import * as $_middleware from "./routes/_middleware.ts"; import * as $_middleware from "./routes/_middleware.ts";
@@ -39,6 +40,7 @@ import type { Manifest } from "$fresh/server.ts";
const manifest = { const manifest = {
routes: { routes: {
"./routes/(apps)/_layout.tsx": $_apps_layout, "./routes/(apps)/_layout.tsx": $_apps_layout,
"./routes/(apps)/_middleware.ts": $_apps_middleware,
"./routes/(apps)/mobility/api/insert_mobility.ts": "./routes/(apps)/mobility/api/insert_mobility.ts":
$_apps_mobility_api_insert_mobility, $_apps_mobility_api_insert_mobility,
"./routes/(apps)/mobility/index.tsx": $_apps_mobility_index, "./routes/(apps)/mobility/index.tsx": $_apps_mobility_index,
@@ -53,8 +55,7 @@ const manifest = {
$_apps_notes_partials_admin_courses, $_apps_notes_partials_admin_courses,
"./routes/(apps)/notes/partials/index.tsx": $_apps_notes_partials_index, "./routes/(apps)/notes/partials/index.tsx": $_apps_notes_partials_index,
"./routes/(apps)/notes/partials/notes.tsx": $_apps_notes_partials_notes, "./routes/(apps)/notes/partials/notes.tsx": $_apps_notes_partials_notes,
"./routes/(apps)/students/api/insert_students.ts": "./routes/(apps)/students/api/students.ts": $_apps_students_api_students,
$_apps_students_api_insert_students,
"./routes/(apps)/students/index.tsx": $_apps_students_index, "./routes/(apps)/students/index.tsx": $_apps_students_index,
"./routes/(apps)/students/partials/(admin)/consult.tsx": "./routes/(apps)/students/partials/(admin)/consult.tsx":
$_apps_students_partials_admin_consult, $_apps_students_partials_admin_consult,
@@ -62,8 +63,7 @@ const manifest = {
$_apps_students_partials_admin_upload, $_apps_students_partials_admin_upload,
"./routes/(apps)/students/partials/index.tsx": "./routes/(apps)/students/partials/index.tsx":
$_apps_students_partials_index, $_apps_students_partials_index,
"./routes/(apps)/students/partials/overview.tsx": "./routes/(apps)/students/types.d.ts": $_apps_students_types_d,
$_apps_students_partials_overview,
"./routes/_404.tsx": $_404, "./routes/_404.tsx": $_404,
"./routes/_app.tsx": $_app, "./routes/_app.tsx": $_app,
"./routes/_middleware.ts": $_middleware, "./routes/_middleware.ts": $_middleware,
+4 -7
View File
@@ -1,22 +1,19 @@
import { FreshContext } from "$fresh/server.ts"; import { FreshContext } from "$fresh/server.ts";
import { Partial } from "$fresh/runtime.ts"; import { Partial } from "$fresh/runtime.ts";
import { State } from "$root/defaults/interfaces.ts"; import { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { AppProperties } from "$root/defaults/interfaces.ts";
import Navbar from "$root/routes/(_islands)/Navbar.tsx"; import Navbar from "$root/routes/(_islands)/Navbar.tsx";
// deno-lint-ignore require-await
export default async function AppLayout( export default async function AppLayout(
request: Request, request: Request,
context: FreshContext<State>, context: FreshContext<AuthenticatedState>,
) { ) {
const pathname = new URL(request.url).pathname; const pathname = new URL(request.url).pathname;
const currentApp = pathname.split("/")[1]; const currentApp = pathname.split("/")[1];
const properties: AppProperties = (await import(
`./${currentApp}/(_props)/props.ts`
)).default;
return ( return (
<section id="app"> <section id="app">
<Navbar currentApp={currentApp} pages={properties.pages} /> <Navbar currentApp={currentApp} pages={context.state.availablePages} />
<section id="app-body"> <section id="app-body">
<Partial name="body"> <Partial name="body">
<context.Component /> <context.Component />
+36
View File
@@ -0,0 +1,36 @@
import { FreshContext, MiddlewareHandler } from "$fresh/server.ts";
import {
AppProperties,
AuthenticatedState,
} from "$root/defaults/interfaces.ts";
export const handler: MiddlewareHandler<AuthenticatedState>[] = [
/**
* Get all available pages for current user.
* @param request The HTTP incomming request.
* @param context The Fresh context object with custom `AuthenticatedState`.
* @returns The response from the next middleware.
*/
async function getAllAvailablePages(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const pathname = new URL(request.url).pathname;
const currentApp = pathname.split("/")[1];
const properties: AppProperties = (await import(
`./${currentApp}/(_props)/props.ts`
)).default;
context.state.availablePages = properties.pages;
if (
context.state.session.eduPersonPrimaryAffiliation == "student" &&
Deno.env.get("LOCAL") != "true"
) {
properties.adminOnly.forEach((page) =>
delete context.state.availablePages[page]
);
}
return await context.next();
},
];
@@ -0,0 +1,30 @@
import Student from "$root/routes/(apps)/students/(_components)/Student.tsx";
type PromotionProps = { students: Student[]; promo: Promotion };
export default function Promotion(props: PromotionProps) {
if (!props.promo) {
return <p>Unable to find user in database.</p>;
}
return (
<div key={props.promo.id}>
<h3>Promotion {props.promo.endyear}</h3>
<table>
<thead>
<tr>
<th>ID</th>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
{props.students
.filter((student) => student.promotionId === props.promo.id)
.map((student) => <Student student={student} />)}
</tbody>
</table>
</div>
);
}
@@ -0,0 +1,31 @@
import { CasContent } from "$root/defaults/interfaces.ts";
type SelfPortraitProps = { self: CasContent };
const regex =
/^(?<year>\d{4})(?<month>\d{2})(?<date>\d{2})(?<hours>\d{2})(?<minutes>\d{2})(?<seconds>\d{2})Z$/;
export default function SelfPortrait(props: SelfPortraitProps) {
const { year, month, date, hours, minutes, seconds } = props.self
.amuDateValidation.match(regex)!.groups!;
const validationIsoDate =
`${year}-${month}-${date}T${hours}:${minutes}:${seconds}Z`;
const validationDate = new Date(validationIsoDate);
return (
<div id="self-portrait">
<div>Identity</div>
<div>{props.self.supannCivilite} {props.self.displayName}</div>
<div>Student number</div>
<div>{props.self.uid}</div>
<div>amU mail</div>
<div>{props.self.mail}</div>
<div>First amU registration</div>
<div>{validationDate.toLocaleString()}</div>
<div>amU class code</div>
<div>{props.self.supannEtuEtape}</div>
</div>
);
}
@@ -0,0 +1,13 @@
type StudentProps = { student: Student; promo?: number };
export default function Student(props: StudentProps) {
return (
<tr key={props.student.userId}>
<td>{props.student.userId}</td>
<td>{props.student.firstName}</td>
<td>{props.student.lastName}</td>
<td>{props.student.mail}</td>
{props.promo && <td>{props.promo}</td>}
</tr>
);
}
@@ -1,75 +1,45 @@
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import Promotion from "$root/routes/(apps)/students/(_components)/Promotion.tsx";
interface Promotion { type SingleUserResponse = { promo: Promotion; student: Student };
id: number; type ManyUsersResponse = { promos: Promotion[]; students: Student[] };
name: string;
}
interface Student { type APIResponse = SingleUserResponse | ManyUsersResponse;
userId: string;
firstName: string;
lastName: string;
mail: string;
promotionId: number;
}
export default function ConsultStudents() { export default function ConsultStudents() {
const [data, setData] = useState< const [data, setData] = useState<APIResponse | null>(null);
{ promotions: Promotion[]; students: Student[] } | null
>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
try { const response = await fetch("/students/api/students");
const response = await fetch("/students/api/insert_students"); if (!response.ok) {
if (!response.ok) {
throw new Error(`Error fetching data: ${response.statusText}`);
}
const result = await response.json();
console.log("Fetched data:", result);
setData(result);
} catch (err) {
console.error("Error fetching data:", err);
setError("Failed to load data. Please try again later."); setError("Failed to load data. Please try again later.");
} }
const result: APIResponse = await response.json();
setData(result);
}; };
fetchData(); fetchData();
}, []); }, []);
return ( return (
<section> <>
<h2>Consult Students</h2>
{error && <p className="error">{error}</p>} {error && <p className="error">{error}</p>}
{data?.promotions.map((promo) => ( {data && ((Object.hasOwn(data, "student"))
<div key={promo.id}> ? (
<h3>Promotion: {promo.name}</h3> <Promotion
<table> students={[(data as SingleUserResponse).student]}
<thead> promo={(data as SingleUserResponse).promo}
<tr> />
<th>ID</th> )
<th>First Name</th> : (data as ManyUsersResponse).promos.map((promo) => (
<th>Last Name</th> <Promotion
<th>Email</th> students={(data as ManyUsersResponse).students}
</tr> promo={promo}
</thead> />
<tbody> )))}
{data.students </>
.filter((student) => student.promotionId === promo.id)
.map((student) => (
<tr key={student.userId}>
<td>{student.userId}</td>
<td>{student.firstName}</td>
<td>{student.lastName}</td>
<td>{student.mail}</td>
</tr>
))}
</tbody>
</table>
</div>
))}
</section>
); );
} }
@@ -1,75 +1,111 @@
// @deno-types="https://cdn.sheetjs.com/xlsx-0.20.3/package/types/index.d.ts" // @deno-types="https://cdn.sheetjs.com/xlsx-0.20.3/package/types/index.d.ts"
import * as XLSX from "https://cdn.sheetjs.com/xlsx-0.20.3/package/xlsx.mjs"; import * as XLSX from "https://cdn.sheetjs.com/xlsx-0.20.3/package/xlsx.mjs";
import { useSignal } from "@preact/signals"; import { Signal, useSignal } from "@preact/signals";
export default function UploadStudents() { /**
const statusMessage = useSignal<string>(""); * Create a new handler for file change that displays
const fileData = useSignal<File | null>(null); * messages in statusMessage and gets file data in fileData.
* @param statusMessage The status message signal.
const handleFileChange = (event: Event) => { * @param fileData The file data signal.
* @returns The file change handler.
*/
function getFileChangeHandler(
statusMessage: Signal<string>,
fileData: Signal<File | null>,
): (event: Event) => void {
/**
* Handle file change.
* @param event The file change event.
*/
return (event: Event) => {
const input = event.target as HTMLInputElement; const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) { if (input.files && input.files.length > 0) {
fileData.value = input.files[0]; fileData.value = input.files[0];
statusMessage.value = "File selected: " + input.files[0].name; statusMessage.value = `File selected: ${input.files[0].name}`;
} else { } else {
fileData.value = null; fileData.value = null;
statusMessage.value = "No file selected"; statusMessage.value = "No file selected";
} }
}; };
}
const confirmUpload = () => { /**
* Create a new handler that sends data file to server.
* @param statusMessage The status message signal.
* @param fileData The file data signal.
* @returns The file confirmation handler.
*/
function getUploadConfirmationFunction(
statusMessage: Signal<string>,
fileData: Signal<File | null>,
): () => void {
/**
* Add students to database.
* @returns Confirm upload of students.
*/
return () => {
if (!fileData.value) { if (!fileData.value) {
statusMessage.value = "Please select a file before confirming upload."; statusMessage.value = "Please select a file before confirming upload.";
return; return;
} }
try { const reader = new FileReader();
const reader = new FileReader();
reader.onload = async (e) => { /**
const arrayBuffer = e.target?.result as ArrayBuffer; * Send all data to the server.
const workbook = XLSX.read(arrayBuffer, { type: "array" }); * @param event The finished progress event.
*/
reader.onload = async (event: ProgressEvent<FileReader>) => {
const arrayBuffer = event.target!.result as ArrayBuffer;
const workbook = XLSX.read(arrayBuffer, { type: "array" });
let allOK = true;
for (const sheetName of workbook.SheetNames) { for (const sheetName of workbook.SheetNames) {
const sheet = workbook.Sheets[sheetName]; const sheet = workbook.Sheets[sheetName];
const data = XLSX.utils.sheet_to_json(sheet, { const data = XLSX.utils.sheet_to_json(sheet, {
header: ["Identifiant", "Nom", "Prénom", "Mail"], header: ["userId", "lastName", "firstName", "mail"],
range: 1, // Ignorer les en-têtes range: 1,
}); });
console.log(`Data from sheet ${sheetName}:`, data); const response = await fetch("/students/api/students", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ promoName: sheetName, data }),
});
const response = await fetch("/students/api/insert_students", { if (!response.ok) {
method: "POST", allOK = false;
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ promoName: sheetName, data }),
});
if (!response.ok) {
throw new Error(`Failed to insert data for promotion ${sheetName}`);
}
} }
}
statusMessage.value = "Data uploaded and inserted successfully!"; statusMessage.value = allOK
}; ? "Failed to insert all data."
: "Data uploaded and inserted successfully!";
};
reader.onerror = () => { /**
statusMessage.value = "Error reading the file."; * Display error message if any.
}; */
reader.onerror = () => {
statusMessage.value = "Error reading the file.";
};
reader.readAsArrayBuffer(fileData.value); reader.readAsArrayBuffer(fileData.value);
} catch (error) {
console.error("Error uploading file:", error);
statusMessage.value = "An unexpected error occurred during upload.";
}
}; };
}
export default function UploadStudents() {
const statusMessage = useSignal<string>("");
const fileData = useSignal<File | null>(null);
const handleFileChange = getFileChangeHandler(statusMessage, fileData);
const confirmUpload = getUploadConfirmationFunction(statusMessage, fileData);
return ( return (
<div> <>
<h2>Upload Students</h2>
<input type="file" accept=".xlsx, .xls" onChange={handleFileChange} /> <input type="file" accept=".xlsx, .xls" onChange={handleFileChange} />
<button onClick={confirmUpload}>Confirm Upload</button> <button onClick={confirmUpload}>Confirm Upload</button>
<p>{statusMessage.value}</p> <p>{statusMessage.value}</p>
</div> </>
); );
} }
-1
View File
@@ -5,7 +5,6 @@ const properties: AppProperties = {
icon: "badge", icon: "badge",
pages: { pages: {
index: "Homepage", index: "Homepage",
overview: "Students overview",
upload: "Upload students", upload: "Upload students",
consult: "Consult students", consult: "Consult students",
}, },
@@ -1,84 +0,0 @@
import { Handlers } from "$fresh/server.ts";
// import { Database } from "@db/sqlite";
import connect from "$root/databases/connect.ts";
export const handler: Handlers = {
// deno-lint-ignore require-await
async GET() {
try {
using connection = connect("students");
const promotions = connection.database.prepare(
"select id, name from promotions",
).all();
const students = connection.database
.prepare(
`select userId, firstName, lastName, mail, promotionId from students`,
)
.all();
return new Response(
JSON.stringify({ promotions, students }),
{
status: 200,
headers: { "Content-Type": "application/json" },
},
);
} catch (error) {
console.error("Error fetching data:", error);
return new Response("Failed to fetch data", { status: 500 });
}
},
async POST(request) {
console.log("API /students/api/insert_students called");
try {
const body = await request.json();
const { data, promoName } = body;
console.log("Received data:", { promoName, data });
if (!promoName || !Array.isArray(data)) {
throw new Error("Invalid request body");
}
using connection = connect("students");
connection.database.prepare(
"INSERT OR IGNORE INTO promotions (name) VALUES (?)",
).run(promoName);
const promoIdRow: { id: number } = connection.database
.prepare("SELECT id FROM promotions WHERE name = ?")
.get(promoName)!;
const promoId = promoIdRow.id;
console.log(`Promotion ID for "${promoName}":`, promoId);
const insertQuery = connection.database.prepare(
`INSERT INTO students
(userId, firstName, lastName, mail, promotionId)
VALUES (?, ?, ?, ?, ?)`,
);
for (const student of data) {
console.log("Inserting student:", student);
insertQuery.run(
student.Identifiant,
student.Nom,
student["Prénom"],
student.Mail,
promoId,
);
}
console.log("All data inserted successfully");
return new Response("Data inserted successfully", { status: 201 });
} catch (error) {
console.error("Error inserting data:", error);
return new Response("Failed to insert data", { status: 500 });
}
},
};
+151
View File
@@ -0,0 +1,151 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import connect from "$root/databases/connect.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { Database } from "@db/sqlite";
/**
* Gets itself from the database.
* @param database The database connection
* @param userId The user ID.
* @returns Itself from the database.
*/
function getItself(
database: Database,
userId: string,
): { student: Student | null; promo: Promotion | null } {
const studentQuery = "select * from students where userId = ?";
const student: Student | undefined = database.prepare(studentQuery).get(
userId,
);
if (!student) {
return { student: null, promo: null };
}
const promoQuery = "select * from promotions where id = ?";
const promo: Promotion | undefined = database.prepare(promoQuery).get(
student.promotionId,
);
return { student, promo: promo ?? null };
}
/**
* Gets itself from the database.
* @param database The database connexion
* @param userId The user ID.
* @returns Itself from the database.
*/
function getAll(
database: Database,
): { students: Student[]; promos: Promotion[] } {
const studentsQuery = `
select userId, firstName, lastName, mail, promotionId
from students inner join promotions
on students.promotionId = promotions.id
where promotions.current < 6`;
const students: Student[] = database.prepare(studentsQuery).all();
const promosQuery = "select * from promotions where promotions.current < 6";
const promos: Promotion[] | undefined = database.prepare(promosQuery).all();
return { students, promos };
}
/**
* Add users to the database.
* @param database The database connexion
* @param students The students to add
* @param promoId The promotion id.
*/
function addStudents(database: Database, students: Student[], promoId: string) {
const query = `
INSERT INTO students
(userId, firstName, lastName, mail, promotionId)
VALUES (?, ?, ?, ?, ?)`;
const statement = database.prepare(query);
for (const student of students) {
statement.run(
student.userId,
student.firstName,
student.lastName,
student.mail,
promoId,
);
}
}
export const handler: Handlers<null, AuthenticatedState> = {
/**
* The students the user can see.
* @param _request The HTTP request.
* @param _context The context with authenticated state.
* @returns All students our user can see.
*/
// deno-lint-ignore require-await
async GET(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
using connection = connect("students");
const database = connection.database;
if (context.state.session.eduPersonPrimaryAffiliation == "student") {
return new Response(
JSON.stringify(getItself(database, context.state.session.uid)),
{
headers: {
"content-type": "application/json",
},
},
);
}
return new Response(
JSON.stringify(getAll(database)),
{
headers: {
"content-type": "application/json",
},
},
);
},
/**
* Add students in the database.
* @param request The HTTP request.
* @param _context The Fresh context.
* @returns HTTP 201 on successful insert.
*/
async POST(
request: Request,
_context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const { students, promo }: { students: Student[]; promo: string } =
await request.json();
if (!promo || !promo.match(/^\d{4}-\dA$/) || !Array.isArray(students)) {
return new Response(null, { status: 400 });
}
using connection = connect("students");
const database = connection.database;
const { endyear, current } = promo.match(
/^(?<endyear>\d{4})-(?<current>\d)A$/,
)?.groups!;
database.prepare(
"insert or ignore into promotions (endyear, current) values (?, ?)",
).run(endyear, current);
const { id: promoId }: { id: string } = database
.prepare("select id from promotions where endyear = ? and current = ?")
.get(endyear, current)!;
addStudents(database, students, promoId);
return new Response(null, { status: 201 });
},
};
@@ -1,17 +1,16 @@
import ConsultStudents from "$root/routes/(apps)/students/(_islands)/ConsultStudents.tsx"; import ConsultStudents from "../../(_islands)/ConsultStudents.tsx";
import { import {
getPartialsConfig, getPartialsConfig,
makePartials, makePartials,
} from "$root/defaults/makePartials.tsx"; } from "$root/defaults/makePartials.tsx";
import { FreshContext } from "$fresh/server.ts"; import { FreshContext } from "$fresh/server.ts";
import { State } from "$root/routes/_middleware.ts"; import { State } from "$root/defaults/interfaces.ts";
//import EditStudents from "../(_islands)/EditStudents.tsx";
// deno-lint-ignore require-await // deno-lint-ignore require-await
async function Students(_request: Request, _context: FreshContext<State>) { async function Students(_request: Request, _context: FreshContext<State>) {
return ( return (
<> <>
<h1>Manage Promotions</h1> <h2>Consult students</h2>
<ConsultStudents /> <ConsultStudents />
</> </>
); );
@@ -1,17 +1,16 @@
import UploadStudents from "$root/routes/(apps)/students/(_islands)/UploadStudents.tsx"; import UploadStudents from "../../(_islands)/UploadStudents.tsx";
import { import {
getPartialsConfig, getPartialsConfig,
makePartials, makePartials,
} from "$root/defaults/makePartials.tsx"; } from "$root/defaults/makePartials.tsx";
import { FreshContext } from "$fresh/server.ts"; import { FreshContext } from "$fresh/server.ts";
import { State } from "$root/routes/_middleware.ts"; import { State } from "$root/defaults/interfaces.ts";
//import EditStudents from "../(_islands)/EditStudents.tsx";
// deno-lint-ignore require-await // deno-lint-ignore require-await
async function Students(_request: Request, _context: FreshContext<State>) { async function Students(_request: Request, _context: FreshContext<State>) {
return ( return (
<> <>
<h1>Manage Promotions</h1> <h2>Upload Students</h2>
<UploadStudents /> <UploadStudents />
</> </>
); );
+9 -2
View File
@@ -3,11 +3,18 @@ import {
makePartials, makePartials,
} from "$root/defaults/makePartials.tsx"; } from "$root/defaults/makePartials.tsx";
import { FreshContext } from "$fresh/server.ts"; import { FreshContext } from "$fresh/server.ts";
import { State } from "$root/routes/_middleware.ts"; import { State } from "$root/defaults/interfaces.ts";
import SelfPortrait from "$root/routes/(apps)/students/(_components)/SelfPortrait.tsx";
// deno-lint-ignore require-await // deno-lint-ignore require-await
export async function Index(_request: Request, context: FreshContext<State>) { export async function Index(_request: Request, context: FreshContext<State>) {
return <h2>Welcome to {context.state.session?.displayName}.</h2>; return (
<>
<h2>Welcome {context.state.session?.givenName}!</h2>
<h3>Your amU identity</h3>
<SelfPortrait self={context.state.session!} />
</>
);
} }
export const config = getPartialsConfig(); export const config = getPartialsConfig();
@@ -1,17 +0,0 @@
import { Partial } from "$fresh/runtime.ts";
import { RouteConfig } from "$fresh/server.ts";
type ModulesProps = Record<string | number | symbol, never>;
export const config: RouteConfig = {
skipAppWrapper: true,
skipInheritedLayouts: true,
};
export default function Modules(_props: ModulesProps) {
return (
<Partial name="body">
<a href="students" f-partial={"notes/partials"}>students</a>
</Partial>
);
}
+13
View File
@@ -0,0 +1,13 @@
interface Student {
userId: string;
firstName: string;
lastName: string;
mail: string;
promotionId: number;
}
interface Promotion {
id: number;
endyear: number;
current: number;
}
+1
View File
@@ -27,6 +27,7 @@ export default async function App(
<link rel="stylesheet" href="/styles/main.css" /> <link rel="stylesheet" href="/styles/main.css" />
<link rel="stylesheet" href="/styles/app.css" /> <link rel="stylesheet" href="/styles/app.css" />
<link rel="stylesheet" href="styles/app-cards.css" /> <link rel="stylesheet" href="styles/app-cards.css" />
<link rel="stylesheet" href="styles/students.css" />
</head> </head>
<body f-client-nav> <body f-client-nav>
<Header link={link} /> <Header link={link} />
+7 -2
View File
@@ -3,7 +3,7 @@
padding: 1em 0; padding: 1em 0;
display: grid; display: grid;
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
gap: 1em; gap: 4em;
} }
#app > #app-body { #app > #app-body {
@@ -16,7 +16,7 @@
} }
#app > nav > a { #app > nav > a {
padding: 0.25em 0.5em; padding: 0.5em 4em 0.5em 1em;
color: light-dark(var(--light-foreground), var(--dark-foreground)); color: light-dark(var(--light-foreground), var(--dark-foreground));
} }
@@ -57,5 +57,10 @@
#app { #app {
grid-template-rows: auto 1fr; grid-template-rows: auto 1fr;
grid-template-columns: none; grid-template-columns: none;
gap: 1em;
}
#app > nav > a {
padding: 0.5em 1em;
} }
} }
+15
View File
@@ -0,0 +1,15 @@
#self-portrait {
display: grid;
gap: 1em;
grid-template-columns: auto 1fr;
}
#self-portrait > div:nth-child(2n+1) {
font-weight: var(--font-weight-bold);
}
@media screen and (max-width: 1024px) {
#self-portrait {
grid-template-columns: 1fr;
}
}