From ecdf900299834591f5a62c0f8175f1a54eef0f31 Mon Sep 17 00:00:00 2001 From: Araozu Date: Wed, 13 Dec 2023 12:35:17 -0500 Subject: [PATCH] [BE][Certs] Batch create registers in a single SQL query --- backend/README.md | 15 ++ backend/src/controller/register/mod.rs | 14 ++ backend/src/controller/scans/mod.rs | 16 +- backend/src/main.rs | 6 +- backend/src/model/course.rs | 4 +- backend/src/model/register.rs | 163 +++++++++++++++++- .../src/certs/NewRegister/RegisterPreview.tsx | 14 +- 7 files changed, 216 insertions(+), 16 deletions(-) diff --git a/backend/README.md b/backend/README.md index 711edc3..1f1d306 100644 --- a/backend/README.md +++ b/backend/README.md @@ -28,6 +28,21 @@ Si se quiere lanzar mediante Jenkins ya hay un `Jenkinsfile` una carpeta arriba. Modificar lo que sea necesario para ejecutar en el entorno objetivo. +## Base de datos + +**IMPORTANTE**: Este sistema (backend y frontend) depende de que la base de datos tenga ciertos +registros con ciertos ids, estos valores estan escritos en el código (hard coded) + +- La tabla `custom_label` debe tener una fila con id: `0` y valor `` +- La tabla `course` debe tener filas para los cursos matpel: + - Matpel 1 con id `10` + - Matpel 2 con id `11` + - Matpel 3 con id `12` + +Estos valores deben estar en la base de datos. Si se cambiaron, es necesario +cambiarlos en el código (sería bueno sacarlos de .env). + + ## Funcionamiento Básicamente, al iniciar el backend realiza lo siguiente: diff --git a/backend/src/controller/register/mod.rs b/backend/src/controller/register/mod.rs index f390323..e8873cc 100644 --- a/backend/src/controller/register/mod.rs +++ b/backend/src/controller/register/mod.rs @@ -17,6 +17,8 @@ pub fn options_delete(_r: i32) -> Status { #[post("/register/batch", format = "json", data = "")] pub async fn insert_all(data: Json>) -> (Status, Json>) { + // Old way, inserts one by one + /* for register_create in data.iter() { let res = register_create.create().await; @@ -30,6 +32,18 @@ pub async fn insert_all(data: Json>) -> (Status, Json (Status::Ok, JsonResult::ok(())), + Err(err) => { + eprintln!("Error creating register: {:?}", err); + ( + Status::InternalServerError, + JsonResult::err(format!("Error creando registros: {}", err)), + ) + } + } } #[get("/register/")] diff --git a/backend/src/controller/scans/mod.rs b/backend/src/controller/scans/mod.rs index be5a03a..d65d869 100644 --- a/backend/src/controller/scans/mod.rs +++ b/backend/src/controller/scans/mod.rs @@ -237,14 +237,14 @@ fn get_image_info(path: PathBuf) -> ScanInfo { // Here, an iid is found. Extract it & return let dni_length = p - 31; - let equals_pos = match url.find('=') { - Some(p) => p, - None => { - return ScanInfo::Error( - "QR invalido: La URL tiene signo de interrogacion (?) pero no igual (=).".into(), - ) - } - }; + let equals_pos = + match url.find('=') { + Some(p) => p, + None => return ScanInfo::Error( + "QR invalido: La URL tiene signo de interrogacion (?) pero no igual (=)." + .into(), + ), + }; let dni = url.chars().skip(31).take(dni_length).collect::(); let iid = url.chars().skip(equals_pos + 1).collect::(); diff --git a/backend/src/main.rs b/backend/src/main.rs index cdffdf0..d283360 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,9 +1,7 @@ use cors::Cors; -use once_cell::sync::OnceCell; -use sqlx::mysql::MySqlPoolOptions; -use sqlx::{MySql, Pool, MySqlConnection}; -use std::env; use sqlx::Connection; +use sqlx::MySqlConnection; +use std::env; #[macro_use] extern crate rocket; diff --git a/backend/src/model/course.rs b/backend/src/model/course.rs index deffbfb..b61aeaa 100644 --- a/backend/src/model/course.rs +++ b/backend/src/model/course.rs @@ -35,7 +35,9 @@ impl Course { } }; - let results = sqlx::query!("SELECT * FROM course").fetch_all(&mut db).await; + let results = sqlx::query!("SELECT * FROM course") + .fetch_all(&mut db) + .await; let results = match results { Ok(res) => res, diff --git a/backend/src/model/register.rs b/backend/src/model/register.rs index 692ec1d..e5c53e9 100644 --- a/backend/src/model/register.rs +++ b/backend/src/model/register.rs @@ -1,3 +1,5 @@ +use std::collections::{HashMap, HashSet}; + use rocket::form::validate::Contains; use serde::{Deserialize, Serialize}; use sqlx::{Executor, Row}; @@ -138,6 +140,165 @@ impl RegisterCreate { Ok(new_max) } + + pub async fn batch_create(registers: Vec) -> Result<(), String> { + // Collect all the courses ids in a set + let mut course_ids = std::collections::HashSet::::new(); + for register in registers.iter() { + let course_id = register.course_id; + + // TODO: Move hardcoded values to .env + // If the course id is 10, 11 or 12 (matpel ids), add all of them to the set + if course_id == 10 || course_id == 11 || course_id == 12 { + course_ids.insert(10); + course_ids.insert(11); + course_ids.insert(12); + } else { + course_ids.insert(course_id); + } + } + + // Get next values from all courses ids + // This is a map of course_id -> next_register_code, + // and will be updated for every register to insert + let mut current_register_codes = Self::get_next_register_codes(course_ids).await?; + + // If there's matpel courses (ids 10, 11 & 12), use only id 10, and set it to the highest value + if current_register_codes.contains_key(&10) { + // Get the highest value + let max = current_register_codes + .iter() + .filter(|(id, _)| *id == &10 || *id == &11 || *id == &12) + .map(|(_, value)| value) + .max() + .unwrap_or(&50); + + current_register_codes.insert(10, *max); + } + + // Create values for each register + let mut values = Vec::::new(); + for register in registers.iter() { + let course_id = register.course_id; + + // This is the key in the hash map for the current course. + let course_code_key = if course_id == 10 || course_id == 11 || course_id == 12 { + // As per above, matpel courses share codes. Use the value at 10 + 10 + } else { + course_id + }; + + // Get the next register code for this course + let next_register_code = *current_register_codes.get_mut(&course_code_key).unwrap() + 1; + + // Get the custom label id + // This still requires several trips to the database, but: + // - Only if there's a custom label + // - Custom labels are used only in machineries certs + // - Usually there's between 1-2 machineries per request (I personally have never seen more than 3) + let custom_label_id = { + if register.custom_label.is_empty() { + 1 + } else { + // Get custom_label_id from db based of self.custom_label + let id = CustomLabel::get_id_by_value(®ister.custom_label).await?; + + if id > 0 { + id + } else { + CustomLabel::create(®ister.custom_label).await? + } + } + }; + + // Current date in YYYY-MM-DD format + let current_date = chrono::Local::now().format("%Y-%m-%d").to_string(); + + // Create the value string + let value = format!( + "({}, '{}', '{}', {}, {}, {}, {})", + next_register_code, + current_date, + register.date, + custom_label_id, + register.is_preview, + register.person_id, + register.course_id + ); + + // Add it to the list + values.push(value); + + // Update the current register code + current_register_codes.insert(course_code_key, next_register_code); + } + + // Insert all in a single statement + let sql = format!( + "INSERT INTO register ( + register_code, + register_creation_date, + register_display_date, + register_custom_label, + register_is_preview, + register_person_id, + register_course_id + ) VALUES {}", + values.join(", ") + ); + + let mut db = db().await?; + match db.fetch_all(sql.as_str()).await { + Ok(_) => Ok(()), + Err(err) => { + log::error!("Error fetching course & person: {:?}", err); + return Err("Error inserting multiple registers".into()); + } + } + } + + async fn get_next_register_codes( + course_ids: HashSet, + ) -> Result, String> { + let mut db = db().await?; + + let sql = format!( + "SELECT MAX(register_code) AS max, register_course_id + FROM register + WHERE register_course_id IN ({}) + GROUP BY register_course_id", + course_ids + .iter() + .map(|id| id.to_string()) + .collect::>() + .join(",") + ); + + log::info!("SQL: {}", sql); + + let result = match db.fetch_all(sql.as_str()).await { + Ok(res) => res, + Err(err) => { + log::error!("Error fetching max register codes: {:?}", err); + return Err("Error fetching max register codes".into()); + } + }; + + // Convert to a hashmap + let data = result + .iter() + .map(|row| { + ( + row.try_get("register_course_id").unwrap(), + // TODO: This 50 (the minimum value for new registers) should be defined in .env + row.try_get("max").unwrap_or(50), + ) + }) + .collect(); + + Ok(data) + } } #[derive(Serialize, Deserialize, Clone)] @@ -221,7 +382,7 @@ impl Register { person_dni_list: String, ) -> Result, String> { let mut db = db().await?; - + let sql = format!( " select diff --git a/frontend/src/certs/NewRegister/RegisterPreview.tsx b/frontend/src/certs/NewRegister/RegisterPreview.tsx index cb92775..bab5c1c 100644 --- a/frontend/src/certs/NewRegister/RegisterPreview.tsx +++ b/frontend/src/certs/NewRegister/RegisterPreview.tsx @@ -18,11 +18,14 @@ function wait(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } +// TODO: Refactor export function RegisterPreview(props: {selections: Array, personId: number | null, onDelete: (v: number) => void, onRegister: () => void}) { + const [error, setError] = createSignal(""); const [loading, setLoading] = createSignal(false); const submit = async() => { setLoading(true); + setError(""); await wait(2000); @@ -41,11 +44,12 @@ export function RegisterPreview(props: {selections: Array, console.log("Create register: success"); // Custom labels may have changed, reload them loadCustomLabels(); + props.onRegister(); } else { console.log(`error. ${result}`); + setError(result); } - props.onRegister(); setLoading(false); }; @@ -85,6 +89,12 @@ export function RegisterPreview(props: {selections: Array, Registrar {props.selections.length} cursos + + +

+ {error()} +

+
); @@ -140,6 +150,6 @@ async function createRegisters(data: RegisterBatchCreate): Promise