Work on online classroom user creation

master
Araozu 2023-10-03 17:55:58 -05:00
parent 816e26b790
commit adae0bd028
14 changed files with 556 additions and 180 deletions

View File

@ -0,0 +1,14 @@
use rocket::{http::Status, serde::json::Json};
use crate::{json_result::JsonResult, model::classroom_user::ClassroomPersonCreate};
#[options("/classroom/user")]
pub fn create_user_options() -> Status { Status::Ok }
#[post("/classroom/user", format = "json", data = "<data>")]
pub fn create_user(data: Json<ClassroomPersonCreate>) -> (Status, Json<JsonResult<()>>) {
(Status::InternalServerError, JsonResult::err("Error creando usuario".into()))
}

View File

@ -2,3 +2,4 @@ pub mod course;
pub mod custom_label;
pub mod person;
pub mod register;
pub mod classroom;

View File

@ -58,7 +58,9 @@ async fn rocket() -> _ {
// Online classroom routes
//
online_classroom::connection,
online_classroom::users::get_users,
online_classroom::user::get_users,
controller::classroom::create_user_options,
controller::classroom::create_user,
],
)
}

View File

@ -0,0 +1,21 @@
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct ClassroomPerson {
pub name: String,
pub surname: String,
pub username: String,
pub user_id: String,
}
#[derive(Debug, Deserialize)]
pub struct ClassroomPersonCreate {
pub person_email: String,
pub person_expiration_date: String,
pub person_id: i32,
pub person_names: String,
pub person_surnames: String,
pub person_username: String,
pub person_password: String,
}

View File

@ -3,3 +3,4 @@ pub mod custom_label;
pub mod person;
pub mod register;
pub mod reniec_person;
pub mod classroom_user;

View File

@ -0,0 +1,225 @@
use scraper::{Selector, Html};
use crate::model::classroom_user::ClassroomPersonCreate;
use super::session::request;
/// Creates an online classroom user
pub async fn create(data: &ClassroomPersonCreate) -> Result<(), String> {
let sec_token = get_form_sec_token().await?;
let body = get_request_body(
&data.person_surnames,
&data.person_names,
&data.person_email,
&data.person_username,
&data.person_password,
&data.person_expiration_date,
&sec_token,
);
Err("Not implemented".into())
}
async fn get_form_sec_token() -> Result<String, String> {
let creation_form = request("/main/admin/user_add.php".into()).await?;
let sec_token_selector = Selector::parse("#user_add_sec_token")
.or_else(|err| Err(format!("Error creating sec_token selector: {:?}", err)))?;
let fragment = Html::parse_document(&creation_form);
let input_element = match fragment.select(&sec_token_selector).next() {
Some(el) => el,
None => return Err(format!("Error selecting sec_token element. Not found")),
};
let sec_token_value = match input_element.value().attr("value") {
Some(val) => val,
None => return Err(format!("Error getting sec_token value from input. Not found")),
};
Ok(sec_token_value.into())
}
fn get_request_body(
surnames: &String,
names: &String,
email: &String,
username: &String,
password: &String,
expiration_date: &String,
sec_token: &String,
) -> String {
format!(
r#"
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="lastname"
{surnames}
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="firstname"
{names}
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="official_code"
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="email"
{email}
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="phone"
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="picture"; filename=""
Content-Type: application/octet-stream
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="username"
{username}
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="password[password_auto]"
0
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="password[password]"
{password}
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="status"
5
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="admin[platform_admin]"
0
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="language"
spanish
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="mail[send_mail]"
1
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="radio_expiration_date"
1
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="expiration_date"
{expiration_date} 23:59
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="active"
1
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="extra_legal_accept"
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="extra_already_logged_in"
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="extra_update_type"
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="extra_rssfeeds"
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="extra_dashboard"
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="extra_timezone"
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="extra_mail_notify_invitation"
1
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="extra_mail_notify_message"
1
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="extra_mail_notify_group_message"
1
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="extra_user_chat_status"
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="extra_google_calendar_url"
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="extra_captcha_blocked_until_date"
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="extra_skype"
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="extra_linkedin_url"
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="extra_request_for_legal_agreement_consent_removal_justification"
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="extra_request_for_delete_account_justification"
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="extra_request_for_legal_agreement_consent_removal"
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="extra_request_for_delete_account"
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="submit"
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="_qf__user_add"
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="MAX_FILE_SIZE"
536870912
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="picture_crop_result"
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="picture_crop_image_base_64"
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="item_id"
0
-----------------------------83919643214156711801978607619
Content-Disposition: form-data; name="sec_token"
{sec_token}
-----------------------------83919643214156711801978607619--
"#
)
}

View File

@ -5,7 +5,8 @@ use crate::json_result::JsonResult;
use self::session::ensure_session;
mod session;
pub mod users;
pub mod user;
pub mod create_user;
/// Tries to connect to the online classroom, and get a session cookie
#[get("/classroom/connect")]

View File

@ -1,18 +1,11 @@
use crate::json_result::JsonResult;
use crate::{json_result::JsonResult, model::classroom_user::ClassroomPerson};
use super::session::request;
use rocket::{http::Status, serde::json::Json};
use scraper::{ElementRef, Html, Selector};
use serde::{Deserialize, Serialize};
use urlencoding::encode;
#[derive(Debug, Serialize, Deserialize)]
pub struct ClassroomPerson {
name: String,
surname: String,
username: String,
user_id: String,
}
// Instead of requesting pages and managing session & cookies manually,
// create a wrapper that:
@ -127,7 +120,7 @@ fn get_person_data(td_vec: &Vec<ElementRef>) -> Result<ClassroomPerson, String>
// Get the username
let username = username_ref.inner_html();
// Parse userid from href
// Parse user_id from href
// format: https://testing.aulavirtual.eegsac.com/main/admin/user_information.php?user_id=1087
// Get the position of 'user_id='
let user_id_start = href_value.find("user_id=").ok_or("Error parsing user_id")? + 8;

View File

@ -1,11 +1,68 @@
import { createSignal } from "solid-js";
import { Show, createSignal, onMount } from "solid-js";
import { FilledButton } from "../components/FilledButton";
import { UserPlusIcon } from "../icons/UserPlusIcon";
import { MaterialInput } from "../components/MaterialInput";
import { Person } from "../types/Person";
import { LoadingStatus, backend, useLoading, wait } from "../utils/functions";
import { JsonResult } from "../types/JsonResult";
import { LoadingIcon } from "../icons/LoadingIcon";
export function ClassroomUserCreation() {
export function ClassroomUserCreation(props: {person: Person}) {
const [email, setEmail] = createSignal("yuli.palo.apaza@gmail.com");
const [loading, setLoading] = createSignal(false);
const [username, setUsername] = createSignal("USERNAME");
const {setError, status, setStatus} = useLoading();
const [names, setNames] = createSignal(props.person.person_names);
const [surnames, setSurnames] = createSignal(`${props.person.person_paternal_surname} ${props.person.person_maternal_surname}`);
const [password, setPassword] = createSignal(props.person.person_dni);
const [date, setDate] = createSignal("2023-10-03");
onMount(() => {
// Setup initial values
const [firstName, secondName] = props.person.person_names.split(" ");
const paternalSurname = props.person.person_paternal_surname;
const maternalSurname = props.person.person_maternal_surname;
const user = `${firstName[0]}${secondName[0] ?? ""}${paternalSurname}${maternalSurname[0]}`;
const next_date = new Date();
next_date.setDate(next_date.getDate() + 60);
setDate(next_date.toISOString().split("T")[0]);
setUsername(user.toUpperCase());
});
const createClassroomUser = async() => {
setStatus(LoadingStatus.Loading);
console.log("creating...");
if (import.meta.env.DEV) await wait(1500);
backend.post<JsonResult<null>>("/api/classroom/user", {
person_id: props.person.person_id,
person_names: names(),
person_surnames: surnames(),
person_email: email(),
person_username: username(),
person_password: password(),
person_expiration_date: date(),
})
.then((response) => {
if (response.status === 200) {
alert("Usuario creado con éxito");
setStatus(LoadingStatus.Ok);
} else {
console.error(response.data);
setError(response.data.Err.reason);
setStatus(LoadingStatus.Error);
}
})
.catch((err) => {
console.error(err);
setError(`Error fatal: ${err.message}`);
alert(`Error fatal: ${err.message}`);
setStatus(LoadingStatus.Error);
});
};
return (
<div class="px-4 pb-4">
@ -13,19 +70,69 @@ export function ClassroomUserCreation() {
O cree un nuevo usuario:
</p>
<form>
<form
onsubmit={(ev) => {
ev.preventDefault();
createClassroomUser();
}}
>
<MaterialInput
resourceName="Correo"
value={email()}
setValue={setEmail}
disabled={loading()}
resourceName="Apellidos"
value={surnames()}
setValue={setSurnames}
disabled={status() === LoadingStatus.Loading}
/>
<MaterialInput
resourceName="Nombres"
value={names()}
setValue={setNames}
disabled={status() === LoadingStatus.Loading}
/>
<MaterialInput
resourceName="Correo electrónico"
value={email()}
type="email"
setValue={setEmail}
disabled={status() === LoadingStatus.Loading}
/>
<div class="grid grid-cols-2 gap-1">
<MaterialInput
class="relative"
resourceName="Usuario"
value={username()}
setValue={(v) => setUsername(v.toUpperCase())}
disabled={status() === LoadingStatus.Loading}
/>
<MaterialInput
class="relative"
resourceName="Contraseña"
value={password().toString()}
setValue={setPassword}
disabled={status() === LoadingStatus.Loading}
/>
</div>
<MaterialInput
resourceName="Fecha de expiracion"
type="date"
value={date()}
setValue={setDate}
disabled={status() === LoadingStatus.Loading}
/>
</form>
<FilledButton class="ml-2">
<UserPlusIcon fill="var(--c-on-primary)" class="mr-2 relative scale-150" size={16} />
Crear usuario
</FilledButton>
<FilledButton type="submit" class="ml-2" disabled={status() === LoadingStatus.Loading}>
<Show when={status() === LoadingStatus.Loading}>
<LoadingIcon
class="animate-spin relative mr-2"
fill="var(--c-primary-container)"
size={16}
/>
</Show>
<Show when={status() !== LoadingStatus.Loading}>
<UserPlusIcon fill="var(--c-on-primary)" class="mr-2 relative scale-150" size={16} />
</Show>
Crear usuario
</FilledButton>
</form>
</div>
);
}

View File

@ -0,0 +1,148 @@
import { ClassroomRegistrationUser } from "../types/ClassroomRegistrationUser";
import { QuestionIcon } from "../icons/QuestionIcon";
import { JsonResult } from "../types/JsonResult";
import { XcircleIcon } from "../icons/XCircleIcon";
import { LoadingStatus, backend, useLoading, wait } from "../utils/functions";
import { LinkIcon } from "../icons/LinkIcon";
import { For, Show, createSignal, onMount } from "solid-js";
import { LoadingIcon } from "../icons/LoadingIcon";
type Status = "Init" | "Ok" | "Loading" | "Error";
export function ClassroomVinculation(props: {person_surname: string, personId: number, onLink: (classroom_id: number) => void,}) {
const [classroomUsers, setClassroomUsers] = createSignal<ClassroomRegistrationUser[]>([]);
const [error, setError] = createSignal("");
const [status, setStatus] = createSignal<Status>("Init");
const loadUsers = async() => {
setStatus("Loading");
const response = await fetch(`${import.meta.env.VITE_BACKEND_URL}/api/classroom/users/${encodeURIComponent(props.person_surname)}`);
const json: JsonResult<Array<ClassroomRegistrationUser>> = await response.json();
if (response.ok) {
setClassroomUsers(json.Ok);
setStatus("Ok");
} else {
console.error("Error loading users", json);
setError(json.Err.reason);
setStatus("Error");
}
};
onMount(loadUsers);
return (
<div>
<p class="py-2 px-4">
Vincule un usuario existente:
</p>
<Show when={status() === "Loading"}>
<div class="text-center h-12 scale-150">
<LoadingIcon class="animate-spin" fill="var(--c-primary)" />
</div>
</Show>
<Show when={status() === "Ok"}>
<Show when={classroomUsers().length === 0}>
<div class="px-4 pb-2">
<div class="text-center pt-6 pb-4">
<QuestionIcon class="scale-[200%]" fill="var(--c-outline)" />
</div>
<p>No se encontraron usuarios en el aula virtual.</p>
</div>
</Show>
<For each={classroomUsers()}>
{(u) => (
<ClassroomSingleUser
user={u}
personId={props.personId}
onLink={props.onLink}
/>
)}
</For>
</Show>
<Show when={status() === "Error"}>
<div class="px-4 pb-2 text-c-error">
<div class="text-center pt-6 pb-4">
<XcircleIcon class="scale-[200%]" fill="var(--c-error)" />
</div>
<p>Error buscando usuarios: {error()}.</p>
</div>
</Show>
</div>
);
}
function ClassroomSingleUser(props: {
user: ClassroomRegistrationUser,
onLink: (classroom_id: number) => void,
personId: number,
}) {
const {setError, status, setStatus} = useLoading();
const linkUser = async() => {
setStatus(LoadingStatus.Loading);
if (import.meta.env.DEV) await wait(1500);
backend.put<JsonResult<null>>(
"/api/person/link",
{
person_id: props.personId,
person_classroom_id: parseInt(props.user.user_id, 10),
},
)
.then((response) => {
if (response.status === 200) {
setStatus(LoadingStatus.Ok);
props.onLink(parseInt(props.user.user_id, 10));
} else {
console.error(response.data);
setError(response.data.Err.reason);
setStatus(LoadingStatus.Error);
}
})
.catch((err) => {
setError(`Error fatal: ${err.message}`);
console.error(err);
setStatus(LoadingStatus.Error);
});
};
return (
<div
class="hover:bg-c-surface-variant hover:text-c-on-surface-variant transition-colors
grid grid-cols-[auto_3rem] gap-4"
>
<div class="pl-4 py-2">
{props.user.name} {props.user.surname}
<br />
<div class="grid grid-cols-[auto_10rem]">
<span class="font-mono">
{props.user.username}
</span>
<div>
registrado:&nbsp;
<span class="font-mono">
??/??/????
</span>
</div>
</div>
</div>
<button
title="Vincular usuario"
class="border-2 border-c-transparent hover:border-c-primary transition-colors rounded"
onclick={linkUser}
disabled={status() === LoadingStatus.Loading}
>
<LinkIcon fill="var(--c-primary)" />
</button>
</div>
);
}

View File

@ -1,15 +1,9 @@
import { For, Show, createSignal, onMount } from "solid-js";
import { Show, createSignal } from "solid-js";
import { Search } from "../certs/Search";
import { Person } from "../types/Person";
import { FilledCard } from "../components/FilledCard";
import { LinkIcon } from "../icons/LinkIcon";
import { ClassroomUserCreation } from "./ClassroomUserCreation";
import { ClassroomRegistrationUser } from "../types/ClassroomRegistrationUser";
import { SpinnerGapIcon } from "../icons/SpinnerGapIcon";
import { QuestionIcon } from "../icons/QuestionIcon";
import { JsonResult } from "../types/JsonResult";
import { XcircleIcon } from "../icons/XCircleIcon";
import { LoadingStatus, backend, useLoading, wait } from "../utils/functions";
import { ClassroomVinculation } from "./ClassroomVinculation";
type TabType = "Vinculate" | "Create";
@ -21,13 +15,13 @@ export function OnlineClassroom() {
<Search setPerson={setPerson} />
<div>
<Show when={person() !== null && person()!.person_classroom_id === undefined}>
<Show when={person() !== null && person()!.person_classroom_id === null}>
<ClassroomUser
person={person()!}
onLink={(classroom_id) => setPerson((p) => ({...p!, person_classroom_id: classroom_id}))}
/>
</Show>
<Show when={person() !== null && person()!.person_classroom_id !== undefined}>
<Show when={person() !== null && person()!.person_classroom_id !== null}>
<p>Person has classroom_id</p>
</Show>
@ -58,7 +52,7 @@ function ClassroomUser(props: {person: Person, onLink: (classroom_id: number) =>
/>
</Show>
<Show when={active() === "Create"}>
<ClassroomUserCreation />
<ClassroomUserCreation person={props.person} />
</Show>
</div>
@ -67,144 +61,6 @@ function ClassroomUser(props: {person: Person, onLink: (classroom_id: number) =>
);
}
type Status = "Init" | "Ok" | "Loading" | "Error";
function ClassroomVinculation(props: {person_surname: string, personId: number, onLink: (classroom_id: number) => void,}) {
const [classroomUsers, setClassroomUsers] = createSignal<ClassroomRegistrationUser[]>([]);
const [error, setError] = createSignal("");
const [status, setStatus] = createSignal<Status>("Init");
const loadUsers = async() => {
setStatus("Loading");
const response = await fetch(`${import.meta.env.VITE_BACKEND_URL}/api/classroom/users/${encodeURIComponent(props.person_surname)}`);
const json: JsonResult<Array<ClassroomRegistrationUser>> = await response.json();
if (response.ok) {
setClassroomUsers(json.Ok);
setStatus("Ok");
} else {
console.error("Error loading users", json);
setError(json.Err.reason);
setStatus("Error");
}
};
onMount(loadUsers);
return (
<div>
<p class="py-2 px-4">
Vincule un usuario existente:
</p>
<Show when={status() === "Loading"}>
<div class="text-center h-12 scale-150">
<SpinnerGapIcon class="animate-spin" fill="var(--c-primary)" />
</div>
</Show>
<Show when={status() === "Ok"}>
<Show when={classroomUsers().length === 0}>
<div class="px-4 pb-2">
<div class="text-center pt-6 pb-4">
<QuestionIcon class="scale-[200%]" fill="var(--c-outline)" />
</div>
<p>No se encontraron usuarios en el aula virtual.</p>
</div>
</Show>
<For each={classroomUsers()}>
{(u) => (
<ClassroomSingleUser
user={u}
personId={props.personId}
onLink={props.onLink}
/>
)}
</For>
</Show>
<Show when={status() === "Error"}>
<div class="px-4 pb-2 text-c-error">
<div class="text-center pt-6 pb-4">
<XcircleIcon class="scale-[200%]" fill="var(--c-error)" />
</div>
<p>Error buscando usuarios: {error()}.</p>
</div>
</Show>
</div>
);
}
function ClassroomSingleUser(props: {
user: ClassroomRegistrationUser,
onLink: (classroom_id: number) => void,
personId: number,
}) {
const {setError, status, setStatus} = useLoading();
const linkUser = async() => {
setStatus(LoadingStatus.Loading);
if (import.meta.env.DEV) await wait(1500);
backend.put<JsonResult<null>>(
"/api/person/link",
{
person_id: props.personId,
person_classroom_id: parseInt(props.user.user_id, 10),
},
)
.then((response) => {
if (response.status === 200) {
setStatus(LoadingStatus.Ok);
props.onLink(parseInt(props.user.user_id, 10));
} else {
console.error(response.data);
setError(response.data.Err.reason);
setStatus(LoadingStatus.Error);
}
})
.catch((err) => {
setError(`Error fatal: ${err.message}`);
console.error(err);
setStatus(LoadingStatus.Error);
});
};
return (
<div
class="hover:bg-c-surface-variant hover:text-c-on-surface-variant transition-colors
grid grid-cols-[auto_3rem] gap-4"
>
<div class="pl-4 py-2">
{props.user.name} {props.user.surname}
<br />
<div class="grid grid-cols-[auto_10rem]">
<span class="font-mono">
{props.user.username}
</span>
<div>
registrado:&nbsp;
<span class="font-mono">
??/??/????
</span>
</div>
</div>
</div>
<button
title="Vincular usuario"
class="border-2 border-c-transparent hover:border-c-primary transition-colors rounded"
onclick={linkUser}
disabled={status() === LoadingStatus.Loading}
>
<LinkIcon fill="var(--c-primary)" />
</button>
</div>
);
}
function ClassroomTabs(props: {active: TabType, setActive: (v: TabType) => void}) {
const presetsClasses = () => ((props.active === "Vinculate") ? "font-bold border-c-primary" : "border-c-transparent");
const manualClasses = () => ((props.active === "Create") ? "font-bold border-c-primary" : "border-c-transparent");

View File

@ -3,12 +3,14 @@ import { JSX } from "solid-js";
export function FilledButton(props: {
children?: Array<JSX.Element> | JSX.Element,
class?: string,
type?: "button" | "input",
type?: "button" | "submit",
disabled?: boolean,
}) {
return (
<button
class={`bg-c-primary text-c-on-primary px-4 py-2 rounded-full cursor-pointer mt-4
disabled:opacity-50 disabled:cursor-not-allowed ${props.class}`}
disabled={props.disabled}
>
{props.children}
</button>

View File

@ -3,19 +3,21 @@ export function MaterialInput(props: {
disabled: boolean,
value: string,
setValue: (v: string) => void,
type?: string,
class?: string
}) {
let inputElement: HTMLInputElement | undefined;
return (
<div class="relative my-6">
<div class={props.class ?? "relative my-6"}>
<input
ref={inputElement}
id="search-dni"
id={props.resourceName.toLowerCase().replace(" ", "-")}
class="bg-c-background text-c-on-background border-c-outline border-2 rounded px-2 py-1 w-full
invalid:border-c-error invalid:text-c-error
focus:border-c-primary outline-none font-mono
disabled:opacity-50 disabled:cursor-not-allowed"
type="text"
type={props.type ?? "text"}
placeholder={props.resourceName}
value={props.value}
required
@ -24,7 +26,10 @@ export function MaterialInput(props: {
/>
<label for="search-dni" class="absolute -top-2 left-2 text-xs bg-c-surface px-1 select-none">
<label
for={props.resourceName.toLowerCase().replace(" ", "-")}
class="absolute -top-2 left-2 text-xs bg-c-surface px-1 select-none"
>
{props.resourceName}
</label>
</div>

View File

@ -5,6 +5,6 @@ export type Person = {
person_names: string
person_paternal_surname: string
person_maternal_surname: string
person_classroom_id: number | undefined
person_classroom_id: number | null
}