[Classroom] Get course list for an user

This commit is contained in:
Araozu 2023-10-04 11:29:03 -05:00
parent b5c6f9caca
commit 60e461f3c0
15 changed files with 223 additions and 45 deletions

View File

@ -1,9 +1,14 @@
use crate::{
json_result::JsonResult,
model::classroom_user::ClassroomPersonCreate,
online_classroom::{create_user::create, get_courses::ClassroomCourse},
};
use rocket::{http::Status, serde::json::Json}; use rocket::{http::Status, serde::json::Json};
use crate::{json_result::JsonResult, model::classroom_user::ClassroomPersonCreate, online_classroom::create_user::create};
#[options("/classroom/user")] #[options("/classroom/user")]
pub fn create_user_options() -> Status { Status::Ok } pub fn create_user_options() -> Status {
Status::Ok
}
#[post("/classroom/user", format = "json", data = "<data>")] #[post("/classroom/user", format = "json", data = "<data>")]
pub async fn create_user(data: Json<ClassroomPersonCreate>) -> (Status, Json<JsonResult<()>>) { pub async fn create_user(data: Json<ClassroomPersonCreate>) -> (Status, Json<JsonResult<()>>) {
@ -12,3 +17,11 @@ pub async fn create_user(data: Json<ClassroomPersonCreate>) -> (Status, Json<Jso
Err(err) => return (Status::InternalServerError, JsonResult::err(err)), Err(err) => return (Status::InternalServerError, JsonResult::err(err)),
} }
} }
#[get("/classroom/course/<user_id>")]
pub async fn get_courses(user_id: i32) -> (Status, Json<JsonResult<Vec<ClassroomCourse>>>) {
match crate::online_classroom::get_courses::get_courses(user_id).await {
Ok(courses) => return (Status::Ok, JsonResult::ok(courses)),
Err(err) => return (Status::InternalServerError, JsonResult::err(err)),
}
}

View File

@ -1,5 +1,5 @@
pub mod classroom;
pub mod course; pub mod course;
pub mod custom_label; pub mod custom_label;
pub mod person; pub mod person;
pub mod register; pub mod register;
pub mod classroom;

View File

@ -129,21 +129,23 @@ pub async fn get_by_dni(dni: i32) -> (Status, Json<JsonResult<Person>>) {
) )
} }
#[options("/person/link")] #[options("/person/link")]
pub fn options_p() -> Status { Status::Ok } pub fn options_p() -> Status {
Status::Ok
}
#[put("/person/link", format = "json", data = "<data>")] #[put("/person/link", format = "json", data = "<data>")]
pub async fn link_person(data: Json<PersonLink>) -> (Status, Json<JsonResult<()>>) { pub async fn link_person(data: Json<PersonLink>) -> (Status, Json<JsonResult<()>>) {
match data.0.insert().await { match data.0.insert().await {
Ok(_) => (Status::Ok, JsonResult::ok(())), Ok(_) => (Status::Ok, JsonResult::ok(())),
Err(reason) => (Status::Ok, JsonResult::err(reason)) Err(reason) => (Status::Ok, JsonResult::err(reason)),
} }
} }
#[options("/person")] #[options("/person")]
pub fn options() -> Status { Status::Ok } pub fn options() -> Status {
Status::Ok
}
#[post("/person", format = "json", data = "<person>")] #[post("/person", format = "json", data = "<person>")]
pub async fn create_person(person: Json<PersonCreate>) -> Status { pub async fn create_person(person: Json<PersonCreate>) -> Status {

View File

@ -61,6 +61,7 @@ async fn rocket() -> _ {
online_classroom::user::get_users, online_classroom::user::get_users,
controller::classroom::create_user_options, controller::classroom::create_user_options,
controller::classroom::create_user, controller::classroom::create_user,
controller::classroom::get_courses,
], ],
) )
} }

View File

@ -1,4 +1,4 @@
use serde::{Serialize, Deserialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct ClassroomPerson { pub struct ClassroomPerson {
@ -8,7 +8,6 @@ pub struct ClassroomPerson {
pub user_id: String, pub user_id: String,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct ClassroomPersonCreate { pub struct ClassroomPersonCreate {
pub person_email: String, pub person_email: String,

View File

@ -1,6 +1,6 @@
pub mod classroom_user;
pub mod course; pub mod course;
pub mod custom_label; pub mod custom_label;
pub mod person; pub mod person;
pub mod register; pub mod register;
pub mod reniec_person; pub mod reniec_person;
pub mod classroom_user;

View File

@ -64,7 +64,6 @@ impl PersonCreate {
} }
} }
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct PersonLink { pub struct PersonLink {
pub person_id: i32, pub person_id: i32,
@ -80,8 +79,8 @@ impl PersonLink {
self.person_classroom_id, self.person_classroom_id,
self.person_id, self.person_id,
) )
.execute(db) .execute(db)
.await; .await;
match res { match res {
Ok(_) => Ok(()), Ok(_) => Ok(()),

View File

@ -1,6 +1,6 @@
use scraper::{Selector, Html}; use super::session::{create_user_request, request};
use crate::model::{classroom_user::ClassroomPersonCreate, person::PersonLink}; use crate::model::{classroom_user::ClassroomPersonCreate, person::PersonLink};
use super::session::{request, create_user_request}; use scraper::{Html, Selector};
const CREATION_ERR: &str = "Creation successful, but linking failed"; const CREATION_ERR: &str = "Creation successful, but linking failed";
@ -28,30 +28,33 @@ pub async fn create(data: &ClassroomPersonCreate) -> Result<(), String> {
match users { match users {
Ok(user) if user.len() == 1 => { Ok(user) if user.len() == 1 => {
let user_id: i32 = user[0].user_id.parse() let user_id: i32 = user[0].user_id.parse().or_else(|err| {
.or_else(|err| Err(format!("{}: Error parsing user_id: {:?}", CREATION_ERR, err)))?; Err(format!(
"{}: Error parsing user_id: {:?}",
CREATION_ERR, err
))
})?;
let result = PersonLink { let result = PersonLink {
person_id: data.person_id, person_id: data.person_id,
person_classroom_id: user_id, person_classroom_id: user_id,
}.insert().await; }
.insert()
.await;
match result { match result {
Ok(_) => return Ok(()), Ok(_) => return Ok(()),
Err(reason) => { Err(reason) => return Err(format!("{}: {}", CREATION_ERR, reason)),
return Err(format!("{}: {}", CREATION_ERR, reason))
}
} }
} }
Ok(u) if u.is_empty() => { Ok(u) if u.is_empty() => {
return Err(format!("{}: No users with username {} found", CREATION_ERR, data.person_username)) return Err(format!(
} "{}: No users with username {} found",
Ok(_) => { CREATION_ERR, data.person_username
return Err(format!("{}: More than 1 user found", CREATION_ERR)) ))
}
Err(reason) => {
return Err(format!("{}: {}", CREATION_ERR, reason))
} }
Ok(_) => return Err(format!("{}: More than 1 user found", CREATION_ERR)),
Err(reason) => return Err(format!("{}: {}", CREATION_ERR, reason)),
} }
} }
@ -73,7 +76,11 @@ async fn get_form_sec_token() -> Result<String, String> {
let sec_token_value = match input_element.value().attr("value") { let sec_token_value = match input_element.value().attr("value") {
Some(val) => val, Some(val) => val,
None => return Err(format!("Error getting sec_token value from input. Not found")), None => {
return Err(format!(
"Error getting sec_token value from input. Not found"
))
}
}; };
Ok(sec_token_value.into()) Ok(sec_token_value.into())

View File

@ -0,0 +1,99 @@
use super::session::request;
use scraper::{Html, Selector};
use serde::Serialize;
#[derive(Debug, Serialize)]
pub struct ClassroomCourse {
name: String,
}
pub async fn get_courses(user_id: i32) -> Result<Vec<ClassroomCourse>, String> {
let html = request(format!(
"/main/admin/user_information.php?user_id={}",
user_id
))
.await?;
let fragment = Html::parse_document(&html);
// Courses selector
let courses_selector =
Selector::parse(".table.table-hover.table-striped.table-bordered.data_table")
.or_else(|err| Err(format!("Error creating table selector: {:?}", err)))?;
// tr
let tr_selector = Selector::parse("tr")
.or_else(|err| Err(format!("Error creating tr selector: {:?}", err)))?;
// td
let td_selector = Selector::parse("td")
.or_else(|err| Err(format!("Error creating td selector: {:?}", err)))?;
let table = match fragment.select(&courses_selector).next() {
Some(t) => t,
None => {
// The courses table was not found.
// If we are in the user page, it means that the user has no courses
if ensure_is_user_page(&fragment, user_id) {
return Ok(Vec::new());
} else {
// If we are not in the user page, it means that there was an error
return Err(format!("Error selecting courses table. Not found"));
}
}
};
let mut courses: Vec<ClassroomCourse> = Vec::new();
let mut tr_iter = table.select(&tr_selector);
// Consume table header
tr_iter.next();
for table_row in tr_iter {
let td_els = table_row.select(&td_selector).collect::<Vec<_>>();
if td_els.len() != 6 {
return Err(format!(
"Error parsing tr: td elements count is not 6, but {}",
td_els.len()
));
}
let course_name = td_els[1].inner_html();
courses.push(ClassroomCourse { name: course_name })
}
Ok(courses)
}
/// Searches in the document for the user toolbar, and checks if the user id matches
fn ensure_is_user_page(document: &Html, user_id: i32) -> bool {
// toolbar-user-information
let user_toolbar = Selector::parse("#toolbar-user-information").unwrap();
let a_selector = Selector::parse("a").unwrap();
let toolbar_el = match document.select(&user_toolbar).next() {
Some(t) => t,
None => return false,
};
let a_element = match toolbar_el.select(&a_selector).next() {
Some(a) => a,
None => return false,
};
// Get href attribute
let href_value = a_element.value().attr("href").unwrap_or("");
// href should be like: https://testing.aulavirtual.eegsac.com/main/mySpace/myStudents.php?student=2142
let user_id_start = match href_value.find("student=") {
Some(i) => i + 8,
None => return false,
};
let user_id_str = href_value[user_id_start..].to_string();
match user_id_str.parse::<i32>() {
Ok(id) => id == user_id,
Err(_) => false,
}
}

View File

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

View File

@ -44,13 +44,12 @@ pub async fn request(url: String) -> Result<String, String> {
} }
} }
/// Handles request for creating a user. /// Handles request for creating a user.
/// ///
/// Returns `Ok("")` if the request was redirected (i.e. the user was created successfully /// Returns `Ok("")` if the request was redirected (i.e. the user was created successfully
/// ///
/// Returns `Ok(html)` if the request was not redirected (i.e. the user was not created successfully) /// Returns `Ok(html)` if the request was not redirected (i.e. the user was not created successfully)
/// ///
/// Returns `Err(err)` if there was an error /// Returns `Err(err)` if there was an error
pub async fn create_user_request(url: String, body: String) -> Result<String, String> { pub async fn create_user_request(url: String, body: String) -> Result<String, String> {
let classroom_url = std::env::var("CLASSROOM_URL").expect("CLASSROOM_URL env var is not set!"); let classroom_url = std::env::var("CLASSROOM_URL").expect("CLASSROOM_URL env var is not set!");
@ -77,7 +76,7 @@ pub async fn create_user_request(url: String, body: String) -> Result<String, St
if response.status() == isahc::http::StatusCode::FOUND { if response.status() == isahc::http::StatusCode::FOUND {
println!("Redirected!"); println!("Redirected!");
return Ok("".into()) return Ok("".into());
} }
match response.text() { match response.text() {
@ -86,7 +85,6 @@ pub async fn create_user_request(url: String, body: String) -> Result<String, St
} }
} }
/// Makes sure that the session cookie is set, and that it is valid /// Makes sure that the session cookie is set, and that it is valid
pub async fn ensure_session() -> Result<(), String> { pub async fn ensure_session() -> Result<(), String> {
let last_usage_time = SESSION_TIME.read().unwrap().clone(); let last_usage_time = SESSION_TIME.read().unwrap().clone();

View File

@ -5,8 +5,6 @@ use rocket::{http::Status, serde::json::Json};
use scraper::{ElementRef, Html, Selector}; use scraper::{ElementRef, Html, Selector};
use urlencoding::encode; use urlencoding::encode;
// Instead of requesting pages and managing session & cookies manually, // Instead of requesting pages and managing session & cookies manually,
// create a wrapper that: // create a wrapper that:
// - Checks if the session cookie is valid // - Checks if the session cookie is valid
@ -26,7 +24,8 @@ pub async fn get_users_impl(search_param: String) -> Result<Vec<ClassroomPerson>
let html = request(format!( let html = request(format!(
"/main/admin/user_list.php?keyword={}&submit=&_qf__search_simple=", "/main/admin/user_list.php?keyword={}&submit=&_qf__search_simple=",
encode(search_param.as_str()) encode(search_param.as_str())
)).await; ))
.await;
match html { match html {
Ok(html) => match parse_users(&html) { Ok(html) => match parse_users(&html) {
@ -36,7 +35,7 @@ pub async fn get_users_impl(search_param: String) -> Result<Vec<ClassroomPerson>
Err(reason) Err(reason)
} }
}, },
Err(reason) => Err(reason) Err(reason) => Err(reason),
} }
} }

View File

@ -0,0 +1,58 @@
import { For, Show, createSignal, onMount } from "solid-js";
import { LoadingStatus, backend, useLoading, wait } from "../utils/functions";
import { JsonResult } from "../types/JsonResult";
import { ClassroomCourse } from "../types/ClassroomCourse";
import { LoadingIcon } from "../icons/LoadingIcon";
export function ClassroomUserCourses(props: {userid: number}) {
const [courses, setCourses] = createSignal<Array<ClassroomCourse>>([]);
const {setError, status, setStatus} = useLoading();
const loadCourses = async() => {
setStatus(LoadingStatus.Loading);
setError("");
if (import.meta.env.DEV) await wait(1500);
backend.get<JsonResult<Array<ClassroomCourse>>>(`/api/classroom/course/${props.userid}`)
.then((res) => {
setCourses(res.data.Ok);
setStatus(LoadingStatus.Ok);
})
.catch((err) => {
console.log(err);
setError(err);
setStatus(LoadingStatus.Error);
});
};
onMount(loadCourses);
return (
<div class="w-full">
<h2 class="text-2xl">
Cursos matriculados:
</h2>
<div class="py-2 text-center">
<Show when={status() === LoadingStatus.Loading}>
<span class="inline-block scale-[200%]">
<LoadingIcon
class="animate-spin mr-2"
fill="var(--c-primary)"
size={16}
/>
</span>
</Show>
</div>
<For each={courses()}>
{(c) => (<p>{c.name}</p>)}
</For>
<Show when={status() === LoadingStatus.Ok && courses().length === 0}>
<p>Esta persona no está matriculada en ningún curso</p>
</Show>
</div>
);
}

View File

@ -4,6 +4,7 @@ import { Person } from "../types/Person";
import { FilledCard } from "../components/FilledCard"; import { FilledCard } from "../components/FilledCard";
import { ClassroomUserCreation } from "./ClassroomUserCreation"; import { ClassroomUserCreation } from "./ClassroomUserCreation";
import { ClassroomVinculation } from "./ClassroomVinculation"; import { ClassroomVinculation } from "./ClassroomVinculation";
import { ClassroomUserCourses } from "./ClassroomUserCourses";
type TabType = "Vinculate" | "Create"; type TabType = "Vinculate" | "Create";
@ -22,9 +23,7 @@ export function OnlineClassroom() {
/> />
</Show> </Show>
<Show when={person() !== null && person()!.person_classroom_id !== null}> <Show when={person() !== null && person()!.person_classroom_id !== null}>
<p> <ClassroomUserCourses userid={person()!.person_classroom_id!} />
Esta persona tiene un usuario en el aula virtual.
</p>
</Show> </Show>
</div> </div>

View File

@ -0,0 +1,3 @@
export type ClassroomCourse = {
name: string
}