[BE][Certs] Batch create registers in a single SQL query
This commit is contained in:
parent
ffb7518ef6
commit
ecdf900299
@ -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.
|
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 `<vacio>`
|
||||||
|
- 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
|
## Funcionamiento
|
||||||
|
|
||||||
Básicamente, al iniciar el backend realiza lo siguiente:
|
Básicamente, al iniciar el backend realiza lo siguiente:
|
||||||
|
@ -17,6 +17,8 @@ pub fn options_delete(_r: i32) -> Status {
|
|||||||
|
|
||||||
#[post("/register/batch", format = "json", data = "<data>")]
|
#[post("/register/batch", format = "json", data = "<data>")]
|
||||||
pub async fn insert_all(data: Json<Vec<RegisterCreate>>) -> (Status, Json<JsonResult<()>>) {
|
pub async fn insert_all(data: Json<Vec<RegisterCreate>>) -> (Status, Json<JsonResult<()>>) {
|
||||||
|
// Old way, inserts one by one
|
||||||
|
/*
|
||||||
for register_create in data.iter() {
|
for register_create in data.iter() {
|
||||||
let res = register_create.create().await;
|
let res = register_create.create().await;
|
||||||
|
|
||||||
@ -30,6 +32,18 @@ pub async fn insert_all(data: Json<Vec<RegisterCreate>>) -> (Status, Json<JsonRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
(Status::Ok, JsonResult::ok(()))
|
(Status::Ok, JsonResult::ok(()))
|
||||||
|
*/
|
||||||
|
|
||||||
|
match RegisterCreate::batch_create(data.into_inner()).await {
|
||||||
|
Ok(_) => (Status::Ok, JsonResult::ok(())),
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Error creating register: {:?}", err);
|
||||||
|
(
|
||||||
|
Status::InternalServerError,
|
||||||
|
JsonResult::err(format!("Error creando registros: {}", err)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/register/<dni>")]
|
#[get("/register/<dni>")]
|
||||||
|
@ -237,14 +237,14 @@ fn get_image_info(path: PathBuf) -> ScanInfo {
|
|||||||
// Here, an iid is found. Extract it & return
|
// Here, an iid is found. Extract it & return
|
||||||
|
|
||||||
let dni_length = p - 31;
|
let dni_length = p - 31;
|
||||||
let equals_pos = match url.find('=') {
|
let equals_pos =
|
||||||
Some(p) => p,
|
match url.find('=') {
|
||||||
None => {
|
Some(p) => p,
|
||||||
return ScanInfo::Error(
|
None => return ScanInfo::Error(
|
||||||
"QR invalido: La URL tiene signo de interrogacion (?) pero no igual (=).".into(),
|
"QR invalido: La URL tiene signo de interrogacion (?) pero no igual (=)."
|
||||||
)
|
.into(),
|
||||||
}
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
let dni = url.chars().skip(31).take(dni_length).collect::<String>();
|
let dni = url.chars().skip(31).take(dni_length).collect::<String>();
|
||||||
let iid = url.chars().skip(equals_pos + 1).collect::<String>();
|
let iid = url.chars().skip(equals_pos + 1).collect::<String>();
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
use cors::Cors;
|
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::Connection;
|
||||||
|
use sqlx::MySqlConnection;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate rocket;
|
extern crate rocket;
|
||||||
|
@ -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 {
|
let results = match results {
|
||||||
Ok(res) => res,
|
Ok(res) => res,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use rocket::form::validate::Contains;
|
use rocket::form::validate::Contains;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{Executor, Row};
|
use sqlx::{Executor, Row};
|
||||||
@ -138,6 +140,165 @@ impl RegisterCreate {
|
|||||||
|
|
||||||
Ok(new_max)
|
Ok(new_max)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn batch_create(registers: Vec<RegisterCreate>) -> Result<(), String> {
|
||||||
|
// Collect all the courses ids in a set
|
||||||
|
let mut course_ids = std::collections::HashSet::<i32>::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::<String>::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<i32>,
|
||||||
|
) -> Result<HashMap<i32, i32>, 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::<Vec<String>>()
|
||||||
|
.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)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
@ -18,11 +18,14 @@ function wait(ms: number) {
|
|||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Refactor
|
||||||
export function RegisterPreview(props: {selections: Array<RegistrationPreview>, personId: number | null, onDelete: (v: number) => void, onRegister: () => void}) {
|
export function RegisterPreview(props: {selections: Array<RegistrationPreview>, personId: number | null, onDelete: (v: number) => void, onRegister: () => void}) {
|
||||||
|
const [error, setError] = createSignal("");
|
||||||
const [loading, setLoading] = createSignal(false);
|
const [loading, setLoading] = createSignal(false);
|
||||||
|
|
||||||
const submit = async() => {
|
const submit = async() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
|
||||||
await wait(2000);
|
await wait(2000);
|
||||||
|
|
||||||
@ -41,11 +44,12 @@ export function RegisterPreview(props: {selections: Array<RegistrationPreview>,
|
|||||||
console.log("Create register: success");
|
console.log("Create register: success");
|
||||||
// Custom labels may have changed, reload them
|
// Custom labels may have changed, reload them
|
||||||
loadCustomLabels();
|
loadCustomLabels();
|
||||||
|
props.onRegister();
|
||||||
} else {
|
} else {
|
||||||
console.log(`error. ${result}`);
|
console.log(`error. ${result}`);
|
||||||
|
setError(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
props.onRegister();
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -85,6 +89,12 @@ export function RegisterPreview(props: {selections: Array<RegistrationPreview>,
|
|||||||
Registrar {props.selections.length} cursos
|
Registrar {props.selections.length} cursos
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<Show when={error() !== ""}>
|
||||||
|
<p class="bg-c-on-error text-c-error p-2 rounded-md">
|
||||||
|
{error()}
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</FilledCard>
|
</FilledCard>
|
||||||
);
|
);
|
||||||
@ -140,6 +150,6 @@ async function createRegisters(data: RegisterBatchCreate): Promise<null | string
|
|||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return JSON.stringify(data);
|
return data.Error.reason;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user