[BE] Detect QR, parse params & get data from DB

master
Araozu 2023-11-16 10:35:30 -05:00
parent 373ef5c74e
commit 605ee27b4f
7 changed files with 221 additions and 11 deletions

View File

@ -1,19 +1,20 @@
use crate::json_result::JsonResult; use crate::{json_result::JsonResult, model::register::Register};
use printpdf::{Mm, PdfDocument}; use printpdf::{Mm, PdfDocument};
use rocket::{http::Status, serde::json::Json}; use rocket::{http::Status, serde::json::Json};
use serde::Serialize; use serde::{Deserialize, Serialize};
use std::{ use std::{
f32::consts::E, f32::consts::E,
fs::{self, File}, fs::{self, File},
io::BufWriter, io::BufWriter,
path::PathBuf, path::PathBuf,
result,
time::{SystemTime, UNIX_EPOCH}, time::{SystemTime, UNIX_EPOCH},
}; };
const SCAN_PATH: &str = "/srv/srv/shares/eegsac/ESCANEOS/"; const SCAN_PATH: &str = "/srv/srv/shares/eegsac/ESCANEOS/";
/// Represents the result of parsing an eegsac URL /// Represents the result of parsing an eegsac URL
#[derive(Serialize)] #[derive(Serialize, Deserialize)]
enum ScanInfo { enum ScanInfo {
/// The url has both DNI & id /// The url has both DNI & id
Full(String, i32, String), Full(String, i32, String),
@ -38,6 +39,32 @@ pub async fn detect_scans() -> (Status, Json<JsonResult<Vec<(PathBuf, ScanInfo)>
(Status::Ok, JsonResult::ok(file_details)) (Status::Ok, JsonResult::ok(file_details))
} }
#[options("/scans/convert")]
pub fn convert_scans_options() -> Status {
Status::Ok
}
#[post("/scans/convert", format = "json", data = "<data>")]
pub async fn convert_scans(
data: Json<JsonResult<Vec<(PathBuf, ScanInfo)>>>,
) -> (Status, Json<JsonResult<()>>) {
let data = data.into_inner();
let data = match data {
JsonResult::Ok(v) => v,
JsonResult::Error(err) => {
return (
Status::BadRequest,
JsonResult::err("Se recibió un error.".into()),
)
}
};
match convert_scans_from_data(&data).await {
Ok(_) => (Status::Ok, JsonResult::ok(())),
Err(reason) => (Status::InternalServerError, JsonResult::err(reason)),
}
}
/// Reads the SCAN_PATH directory and returns a list of file paths /// Reads the SCAN_PATH directory and returns a list of file paths
/// that are valid for processing. /// that are valid for processing.
fn get_valid_files() -> Result<Vec<PathBuf>, String> { fn get_valid_files() -> Result<Vec<PathBuf>, String> {
@ -131,7 +158,7 @@ fn get_image_info(path: PathBuf) -> ScanInfo {
let width = img.width(); let width = img.width();
let height = img.height(); let height = img.height();
let third_point_height = (height / 3) * 2; let third_point_height = (height / 4) * 3;
let mid_point_width = width / 2; let mid_point_width = width / 2;
let remaining_height = height - third_point_height; let remaining_height = height - third_point_height;
@ -141,6 +168,11 @@ fn get_image_info(path: PathBuf) -> ScanInfo {
// get qr from cropped image // get qr from cropped image
let results = bardecoder::default_decoder().decode(&cropped_img); let results = bardecoder::default_decoder().decode(&cropped_img);
if results.is_empty() {
eprintln!("QR not found");
return ScanInfo::Error("QR no encontrado.".into());
}
let url = match &results[0] { let url = match &results[0] {
Ok(url) => url, Ok(url) => url,
Err(reason) => { Err(reason) => {
@ -201,3 +233,41 @@ fn get_image_info(path: PathBuf) -> ScanInfo {
} }
} }
} }
// TODO: this should return a list of files that succeeded & failed
async fn convert_scans_from_data(data: &Vec<(PathBuf, ScanInfo)>) -> Result<(), String> {
let (ids, dnis) = data.iter().fold(
(vec!["-1".to_string()], vec!["''".to_string()]),
|mut acc, (_, next)| {
match next {
ScanInfo::Full(_, id, _) => acc.0.push(id.to_string()),
ScanInfo::Partial(dni, _) => acc.1.push(format!("'{}'", dni)),
_ => (),
};
acc
},
);
let register_id_list = ids.join(",");
let person_dni_list = dnis.join("-");
log::info!("register_id_list: {}", register_id_list);
log::info!("person_dni_list: {}", person_dni_list);
let persons_info = Register::get_course_and_person(register_id_list, person_dni_list).await;
let persons_info = match persons_info {
Ok(v) => v,
Err(err) => {
eprintln!("Error getting persons info: {:?}", err);
return Err("Error obteniendo información de personas.".into());
}
};
for e in persons_info {
println!("\n{:?}", e);
}
Ok(())
}

View File

@ -72,6 +72,8 @@ async fn rocket() -> _ {
// Scans routes // Scans routes
// //
controller::scans::detect_scans, controller::scans::detect_scans,
controller::scans::convert_scans_options,
controller::scans::convert_scans,
], ],
) )
} }

View File

@ -1,14 +1,24 @@
use rocket::form::validate::Contains; use rocket::form::validate::Contains;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{Executor, Row};
use crate::db; use crate::db;
use super::{course::Course, custom_label::CustomLabel}; use super::{course::Course, custom_label::CustomLabel};
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(crate = "rocket::serde")] pub struct ScanData {
pub course_name: String,
pub person_names: String,
pub person_paternal_surname: String,
pub person_maternal_surname: String,
pub person_dni: String,
pub register_id: i32,
}
/// Represents a single register send by the frontend /// Represents a single register send by the frontend
/// to create a new register in the database /// to create a new register in the database
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct RegisterCreate { pub struct RegisterCreate {
person_id: i32, person_id: i32,
course_id: i32, course_id: i32,
@ -161,4 +171,58 @@ impl Register {
Ok(()) Ok(())
} }
pub async fn get_course_and_person(
register_id_list: String,
person_dni_list: String,
) -> Result<Vec<ScanData>, sqlx::Error> {
let db = db();
let sql = format!(
"
select
course.course_name,
register.register_id,
person.person_dni,
person.person_names,
person.person_paternal_surname,
person.person_maternal_surname
from register
inner join course on course.course_id = register.register_course_id
inner join person on person.person_id = register.register_person_id
where register_id IN ({register_id_list})
union
select
'' as 'course_name',
0 as register_id,
'' as person_dni,
person.person_names,
person.person_paternal_surname,
person.person_maternal_surname
from person
where person_dni IN ({person_dni_list})
"
);
log::info!("sql: {}", sql);
let result = db.fetch_all(sql.as_str()).await?;
log::info!("rows: {}", result.len());
let data = result
.iter()
.map(|row| ScanData {
course_name: row.try_get("course_name").unwrap(),
person_names: row.try_get("person_names").unwrap(),
register_id: row.try_get("register_id").unwrap(),
person_dni: row.try_get("person_dni").unwrap(),
person_paternal_surname: row.try_get("person_paternal_surname").unwrap(),
person_maternal_surname: row.try_get("person_maternal_surname").unwrap(),
})
.collect();
Ok(data)
} }
}

View File

@ -4,6 +4,7 @@ import { FilledCard } from "../components/FilledCard";
import { LoadingIcon } from "../icons/LoadingIcon"; import { LoadingIcon } from "../icons/LoadingIcon";
import { MagnifyingGlassIcon } from "../icons/MagnifyingGlassIcon"; import { MagnifyingGlassIcon } from "../icons/MagnifyingGlassIcon";
import { LoadingStatus, useLoading } from "../utils/functions"; import { LoadingStatus, useLoading } from "../utils/functions";
import { PDFIcon } from "../icons/PDFIcon";
/** /**
* Represents the data about the scans that were detected. * Represents the data about the scans that were detected.
@ -36,6 +37,7 @@ export function Scans() {
const detectScans = () => { const detectScans = () => {
setStatus(LoadingStatus.Loading); setStatus(LoadingStatus.Loading);
setScanData(null);
fetch(`${import.meta.env.VITE_BACKEND_URL}/api/scans/detect`) fetch(`${import.meta.env.VITE_BACKEND_URL}/api/scans/detect`)
.then((res) => res.json()) .then((res) => res.json())
@ -49,6 +51,37 @@ export function Scans() {
}); });
}; };
const convertScans = () => {
setStatus(LoadingStatus.Loading);
if (scanData() === null) {
setError("No se detectaron escaneos");
setStatus(LoadingStatus.Error);
return;
}
fetch(
`${import.meta.env.VITE_BACKEND_URL}/api/scans/convert`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(scanData()),
},
)
.then((res) => res.json())
.then((res) => {
setStatus(LoadingStatus.Ok);
console.log(res);
})
.catch((err) => {
setStatus(LoadingStatus.Error);
console.error(err);
});
};
return ( return (
<div class="grid grid-cols-[25rem_25rem_auto]"> <div class="grid grid-cols-[25rem_25rem_auto]">
<div> <div>
@ -126,6 +159,7 @@ export function Scans() {
<div class="relative"> <div class="relative">
<FilledButton <FilledButton
onClick={detectScans} onClick={detectScans}
disabled={loading()}
> >
<span <span
class="absolute top-[1.35rem] left-2" class="absolute top-[1.35rem] left-2"
@ -174,11 +208,43 @@ export function Scans() {
</div> </div>
<p class="p-4"> <p class="p-4">
Al convertir los escaneos a PDF, se eliminan los archivos JPG. - Al convertir los escaneos a PDF se eliminan los archivos JPG.
<br /> <br />
La lista de escaneos a convertir puede haber cambiado desde - Solo se convertiran los escaneos mostrados en esta lista.
la última vez que se realizó la detección. <br />
- Si ejecutas la detección de escaneos nuevamente los nombres de
los archivos cambiarán. Esto es normal. El contenido de los JPG
es el mismo.
</p> </p>
<div class="relative mx-4 mb-4">
<FilledButton
onClick={convertScans}
disabled={loading()}
>
<span
class="absolute top-[1.35rem] left-2"
style={{display: loading() ? "none" : "inline-block"}}
>
<PDFIcon
fill="var(--c-on-primary)"
/>
</span>
<span
class="absolute top-[1.35rem] left-2"
style={{display: loading() ? "inline-block" : "none"}}
>
<LoadingIcon
class="animate-spin"
fill="var(--c-primary-container)"
/>
</span>
<span class="ml-5">
Convertir escaneos
</span>
</FilledButton>
</div>
</FilledCard> </FilledCard>
</div> </div>
</div> </div>

View File

@ -36,7 +36,7 @@ export type CertData<T> = {
*/ */
certDay: string, certDay: string,
/** /**
* Id of the certificate * Id of the certificate in database (not the same as certCode)
*/ */
certIId: number, certIId: number,
/** /**

View File

@ -69,8 +69,7 @@ export async function getQR(data : {
verticalOffset: number, verticalOffset: number,
behindDocument?: boolean, behindDocument?: boolean,
}): Promise<typeof ImageRun> { }): Promise<typeof ImageRun> {
// Old URL: https://www.eegsac.com/alumnoscertificados.php?DNI=${dni()} // https://eegsac.com/certificado/${dni()}
// New URL: https://eegsac.com/certificado/${dni()}
const qr = await QR.toDataURL(`https://eegsac.com/certificado/${data.dni}?iid=${data.iid}`, {margin: 1}); const qr = await QR.toDataURL(`https://eegsac.com/certificado/${data.dni}?iid=${data.iid}`, {margin: 1});
return new ImageRun({ return new ImageRun({

View File

@ -0,0 +1,9 @@
export function PDFIcon(props: {fill: string}) {
return (
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill={props.fill} viewBox="0 0 256 256">
<path d="M224,152a8,8,0,0,1-8,8H192v16h16a8,8,0,0,1,0,16H192v16a8,8,0,0,1-16,0V152a8,8,0,0,1,8-8h32A8,8,0,0,1,224,152ZM92,172a28,28,0,0,1-28,28H56v8a8,8,0,0,1-16,0V152a8,8,0,0,1,8-8H64A28,28,0,0,1,92,172Zm-16,0a12,12,0,0,0-12-12H56v24h8A12,12,0,0,0,76,172Zm88,8a36,36,0,0,1-36,36H112a8,8,0,0,1-8-8V152a8,8,0,0,1,8-8h16A36,36,0,0,1,164,180Zm-16,0a20,20,0,0,0-20-20h-8v40h8A20,20,0,0,0,148,180ZM40,112V40A16,16,0,0,1,56,24h96a8,8,0,0,1,5.66,2.34l56,56A8,8,0,0,1,216,88v24a8,8,0,0,1-16,0V96H152a8,8,0,0,1-8-8V40H56v72a8,8,0,0,1-16,0ZM160,80h28.69L160,51.31Z" />
</svg>
);
}