tmp commit
This commit is contained in:
parent
7ea9bbbb5f
commit
8a035affe3
36
Cargo.lock
generated
36
Cargo.lock
generated
@ -1404,9 +1404,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nanobyte_opentelemetry"
|
name = "nanobyte_opentelemetry"
|
||||||
version = "0.2.2"
|
version = "0.2.3"
|
||||||
source = "sparse+https://git.nanobyte.cz/api/packages/nanobyte/cargo/"
|
source = "sparse+https://git.nanobyte.cz/api/packages/nanobyte/cargo/"
|
||||||
checksum = "a3470844579913374d0a7026b7daf02a1be92e006f089aa908440026fa5b0abf"
|
checksum = "053fac4ff7f3cab0a088383bc68774e402c07c92d19478bcbb51d1d49a5d42bf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gethostname",
|
"gethostname",
|
||||||
"opentelemetry",
|
"opentelemetry",
|
||||||
@ -1429,16 +1429,19 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nanobyte_tera"
|
name = "nanobyte_tera"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
source = "git+https://glpat-Us_EdFTzQLv4shViQXi_:glpat-Us_EdFTzQLv4shViQXi_@gitlab.nanobyte.cz/tools/nanobyte_tera.git?branch=master#75c20a9806663ca04c6f8a7afff64d7b5906d113"
|
source = "sparse+https://git.nanobyte.cz/api/packages/nanobyte/cargo/"
|
||||||
|
checksum = "6fa686074d8273526885446e3c7f4a6f35affab0dfcbba55c9b1aca6efae7c67"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"fluent-bundle",
|
"fluent-bundle",
|
||||||
"fluent-resmgr",
|
"fluent-resmgr",
|
||||||
"log",
|
"log",
|
||||||
"ovlach_data 0.1.0 (git+https://glpat-Ju_qUN9Yh8qa5rEnd6T7:glpat-Ju_qUN9Yh8qa5rEnd6T7@gitlab.nanobyte.cz/ondrej/ov-site-api-data.git?branch=add_missing_fields)",
|
|
||||||
"rocket_dyn_templates",
|
"rocket_dyn_templates",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"sha256",
|
"sha256",
|
||||||
|
"tracing",
|
||||||
"unic-langid",
|
"unic-langid",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1703,18 +1706,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ovlach_data"
|
name = "ovlach_data"
|
||||||
version = "0.1.0"
|
version = "0.1.2"
|
||||||
source = "git+https://glpat-Ju_qUN9Yh8qa5rEnd6T7:glpat-Ju_qUN9Yh8qa5rEnd6T7@gitlab.nanobyte.cz/ondrej/ov-site-api-data.git?branch=add_missing_fields#c13748b039d812d3bf1aaa93312699da7b921868"
|
source = "sparse+https://git.nanobyte.cz/api/packages/ovlach/cargo/"
|
||||||
dependencies = [
|
checksum = "98843b3cefbdbf054f13312e82493248501cdd8abb39379ca2546559358c1437"
|
||||||
"chrono",
|
|
||||||
"rocket",
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ovlach_data"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "git+ssh://git@gitlab.nanobyte.cz/ondrej/ov-site-api-data.git?branch=add_missing_fields#c13748b039d812d3bf1aaa93312699da7b921868"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"rocket",
|
"rocket",
|
||||||
@ -1732,12 +1726,13 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"nanobyte_opentelemetry",
|
"nanobyte_opentelemetry",
|
||||||
"nanobyte_tera",
|
"nanobyte_tera",
|
||||||
"ovlach_data 0.1.0 (git+ssh://git@gitlab.nanobyte.cz/ondrej/ov-site-api-data.git?branch=add_missing_fields)",
|
"ovlach_data",
|
||||||
"ovlach_tera",
|
"ovlach_tera",
|
||||||
"phf",
|
"phf",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rocket",
|
"rocket",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tera",
|
"tera",
|
||||||
@ -1749,8 +1744,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ovlach_tera"
|
name = "ovlach_tera"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
source = "git+https://glpat-_yPuXbEzECyk3FaHudCN:glpat-_yPuXbEzECyk3FaHudCN@gitlab.nanobyte.cz/ondrej/ovlach_tera.git?branch=master#0ab1bbadd76c1336e00b2a38e572048003879ab6"
|
source = "sparse+https://git.nanobyte.cz/api/packages/ovlach/cargo/"
|
||||||
|
checksum = "fd6d3a0c415f223c68db7bd0290405b247f488180e6b96810b3be4acd0b37764"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rocket_dyn_templates",
|
"rocket_dyn_templates",
|
||||||
"serde",
|
"serde",
|
||||||
|
10
Cargo.toml
10
Cargo.toml
@ -13,7 +13,6 @@ fern = "0.6.2"
|
|||||||
rocket = { version = "0.5.0", features = ["json"] }
|
rocket = { version = "0.5.0", features = ["json"] }
|
||||||
headless_chrome = "1.0.8"
|
headless_chrome = "1.0.8"
|
||||||
|
|
||||||
ovlach_data = { git = "ssh://git@gitlab.nanobyte.cz/ondrej/ov-site-api-data.git", branch = "add_missing_fields"}
|
|
||||||
async-trait = "0.1.74"
|
async-trait = "0.1.74"
|
||||||
async-mutex = "1.4.0"
|
async-mutex = "1.4.0"
|
||||||
tokio = { version = "1.34.0", features = ["macros"] }
|
tokio = { version = "1.34.0", features = ["macros"] }
|
||||||
@ -22,9 +21,12 @@ yansi = "0.5.1"
|
|||||||
tera = "1.19.1"
|
tera = "1.19.1"
|
||||||
tempfile = "3.8.1"
|
tempfile = "3.8.1"
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
nanobyte_opentelemetry = { version = "0.2.2", registry = "gitea", features = ["rocket-reqwest"]}
|
nanobyte_opentelemetry = { version = "0.2.3", registry = "gitea_nanobyte", features = ["rocket-reqwest"]}
|
||||||
reqwest = { version = "0.11", features = ["json"] }
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
phf = { version = "0.11.2", features = ["macros"] }
|
phf = { version = "0.11.2", features = ["macros"] }
|
||||||
|
nanobyte_tera = { version = "0.2.0", registry = "gitea_nanobyte" }
|
||||||
|
ovlach_tera = { version="0.2.0", registry="gitea_ovlach" }
|
||||||
|
ovlach_data = { version = "0.1.2", registry = "gitea_ovlach"}
|
||||||
|
|
||||||
nanobyte_tera = { git = "https://glpat-Us_EdFTzQLv4shViQXi_:glpat-Us_EdFTzQLv4shViQXi_@gitlab.nanobyte.cz/tools/nanobyte_tera.git", branch = "master" }
|
[dev-dependencies]
|
||||||
ovlach_tera = { git = "https://glpat-_yPuXbEzECyk3FaHudCN:glpat-_yPuXbEzECyk3FaHudCN@gitlab.nanobyte.cz/ondrej/ovlach_tera.git", branch = "master" }
|
serde_json = "1.0.108"
|
||||||
|
@ -5,4 +5,5 @@ cv_backend_path = "http://localhost:8002"
|
|||||||
[default]
|
[default]
|
||||||
static_route = "http://localhost:8001"
|
static_route = "http://localhost:8001"
|
||||||
cv_backend_path = "http://localhost:8002"
|
cv_backend_path = "http://localhost:8002"
|
||||||
port = 8003
|
port = 8003
|
||||||
|
default_person_name = "ovlach"
|
||||||
|
12
src/lib.rs
12
src/lib.rs
@ -15,6 +15,13 @@ pub struct CVBackendConfig {
|
|||||||
cv_backend_path: String,
|
cv_backend_path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(crate = "rocket::serde")]
|
||||||
|
pub struct DefaultPerson {
|
||||||
|
default_person_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn rocket_builder() -> Rocket<Build> {
|
pub fn rocket_builder() -> Rocket<Build> {
|
||||||
rocket::build()
|
rocket::build()
|
||||||
.attach(TracingFairing::ignite())
|
.attach(TracingFairing::ignite())
|
||||||
@ -22,8 +29,11 @@ pub fn rocket_builder() -> Rocket<Build> {
|
|||||||
.attach(
|
.attach(
|
||||||
AdHoc::config::<CVBackendConfig>()
|
AdHoc::config::<CVBackendConfig>()
|
||||||
)
|
)
|
||||||
|
.attach(
|
||||||
|
AdHoc::config::<DefaultPerson>()
|
||||||
|
)
|
||||||
.mount("/", routes![
|
.mount("/", routes![
|
||||||
routes::pdf::render_pdf_cv,
|
routes::pdf::render_pdf_cv,
|
||||||
routes::pdf::render_html_cv
|
routes::pdf::render_html_cv
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
@ -11,8 +11,9 @@ async fn main() {
|
|||||||
install_panic_handler();
|
install_panic_handler();
|
||||||
let _opentelemetry = nanobyte_opentelemetry::init_telemetry(
|
let _opentelemetry = nanobyte_opentelemetry::init_telemetry(
|
||||||
env!("CARGO_PKG_NAME"),
|
env!("CARGO_PKG_NAME"),
|
||||||
env!("CARGO_PKG_VERSION"),
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
&std::env::var("OTLP_ENDPOINT").unwrap_or("".to_string()),
|
||||||
Some(default_filter_layer(LogLevel::DebugWithoutRs))
|
Some(default_filter_layer(LogLevel::DebugWithoutRs))
|
||||||
);
|
);
|
||||||
let _ = rocket_builder().launch().await;
|
let _ = rocket_builder().launch().await;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
use headless_chrome::Browser;
|
use headless_chrome::Browser;
|
||||||
use headless_chrome::types::PrintToPdfOptions;
|
use headless_chrome::types::PrintToPdfOptions;
|
||||||
use nanobyte_opentelemetry::rocket::{TracingSpan, RequestId, OtelReqwestClient};
|
use nanobyte_opentelemetry::rocket::{TracingSpan, RequestId, OtelReqwestClient};
|
||||||
use ovlach_data::cv::cv::CV;
|
use nanobyte_tera::l18n::LanguageDescription;
|
||||||
|
use ovlach_data::cv::data::CV;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use rocket::fs::NamedFile;
|
use rocket::fs::NamedFile;
|
||||||
use ::rocket::{State, http::Status};
|
use ::rocket::{State, http::Status};
|
||||||
@ -9,6 +10,7 @@ use ::rocket::get;
|
|||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
use tera::Context;
|
use tera::Context;
|
||||||
use tracing::{info_span, error, debug};
|
use tracing::{info_span, error, debug};
|
||||||
|
use crate::DefaultPerson;
|
||||||
use crate::{chromium::rocket::BrowserHolder, tools::{tera::NanoTera, pdf::PdfStream, rocket::RequestLanguage}, services::cv::fetch_cv_data_from_backend, CVBackendConfig};
|
use crate::{chromium::rocket::BrowserHolder, tools::{tera::NanoTera, pdf::PdfStream, rocket::RequestLanguage}, services::cv::fetch_cv_data_from_backend, CVBackendConfig};
|
||||||
|
|
||||||
// TODO: request-id
|
// TODO: request-id
|
||||||
@ -31,7 +33,7 @@ fn generate_pdf(browser: Browser, file: &NamedTempFile) -> Vec<u8> {
|
|||||||
..PrintToPdfOptions::default()
|
..PrintToPdfOptions::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
//thread::sleep(Duration::from_secs(10));
|
//thread::sleep(Duration::from_secs(10));
|
||||||
|
|
||||||
let bytes = info_span!("print_pdf").in_scope(|| {
|
let bytes = info_span!("print_pdf").in_scope(|| {
|
||||||
tab.print_to_pdf(Some(options)).unwrap()
|
tab.print_to_pdf(Some(options)).unwrap()
|
||||||
@ -45,8 +47,8 @@ fn render_template(template_name: &str, file: &NamedTempFile, tera: NanoTera, cv
|
|||||||
// TODO: handle errors
|
// TODO: handle errors
|
||||||
let mut tera_context = Context::new();
|
let mut tera_context = Context::new();
|
||||||
tera_context.insert("cv", &cv);
|
tera_context.insert("cv", &cv);
|
||||||
tera_context.insert("lang", language.as_str());
|
tera_context.insert("lang", &LanguageDescription::new(&language, "ovlach_pdf"));
|
||||||
|
|
||||||
match tera.0.render_to("two_column.html.tera", &tera_context, file) {
|
match tera.0.render_to("two_column.html.tera", &tera_context, file) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
debug!("Rendered template to {}", file.path().to_str().unwrap());
|
debug!("Rendered template to {}", file.path().to_str().unwrap());
|
||||||
@ -59,11 +61,12 @@ fn render_template(template_name: &str, file: &NamedTempFile, tera: NanoTera, cv
|
|||||||
|
|
||||||
#[get("/cv/<username>/<language>/output.pdf")]
|
#[get("/cv/<username>/<language>/output.pdf")]
|
||||||
pub async fn render_pdf_cv(username: &str, tera: NanoTera, tracing: TracingSpan, request_client: OtelReqwestClient,
|
pub async fn render_pdf_cv(username: &str, tera: NanoTera, tracing: TracingSpan, request_client: OtelReqwestClient,
|
||||||
cv_config: &State<CVBackendConfig>, language: RequestLanguage, browser: BrowserHolder) -> Result<PdfStream, Status> {
|
cv_config: &State<CVBackendConfig>, language: RequestLanguage, browser: BrowserHolder,
|
||||||
|
default_person: &State<DefaultPerson>) -> Result<PdfStream, Status> {
|
||||||
let entered_span = tracing.0.enter();
|
let entered_span = tracing.0.enter();
|
||||||
match fetch_cv_data_from_backend(cv_config.cv_backend_path.clone(), request_client.0).await {
|
match fetch_cv_data_from_backend(&cv_config.cv_backend_path, &default_person.inner().default_person_name, &request_client.0).await {
|
||||||
Ok(cv_data) => {
|
Ok(cv_data) => {
|
||||||
let file = tempfile::Builder::new().suffix(".html").tempfile().unwrap();
|
let file = tempfile::Builder::new().suffix(".html").tempfile().unwrap();
|
||||||
render_template("two_column", &file, tera, cv_data, language.language);
|
render_template("two_column", &file, tera, cv_data, language.language);
|
||||||
let span = info_span!("render_pdf", username = username);
|
let span = info_span!("render_pdf", username = username);
|
||||||
let pdf = span.in_scope(||{
|
let pdf = span.in_scope(||{
|
||||||
@ -82,11 +85,12 @@ pub async fn render_pdf_cv(username: &str, tera: NanoTera, tracing: TracingSpan,
|
|||||||
/// Route only for debuging
|
/// Route only for debuging
|
||||||
#[get("/cv/<username>/<language>/output.html")]
|
#[get("/cv/<username>/<language>/output.html")]
|
||||||
pub async fn render_html_cv(username: &str, tera: NanoTera, tracing: TracingSpan, request_client: OtelReqwestClient,
|
pub async fn render_html_cv(username: &str, tera: NanoTera, tracing: TracingSpan, request_client: OtelReqwestClient,
|
||||||
cv_config: &State<CVBackendConfig>, language: RequestLanguage) -> Result<NamedFile, Status> {
|
cv_config: &State<CVBackendConfig>, language: RequestLanguage,
|
||||||
|
default_person: &State<DefaultPerson>) -> Result<NamedFile, Status> {
|
||||||
let _ = tracing.0.enter();
|
let _ = tracing.0.enter();
|
||||||
match fetch_cv_data_from_backend(cv_config.cv_backend_path.clone(), request_client.0).await {
|
match fetch_cv_data_from_backend(&cv_config.cv_backend_path, &default_person.inner().default_person_name, &request_client.0).await {
|
||||||
Ok(cv_data) => {
|
Ok(cv_data) => {
|
||||||
let file = tempfile::Builder::new().suffix(".html").tempfile().unwrap();
|
let file = tempfile::Builder::new().suffix(".html").tempfile().unwrap();
|
||||||
render_template("two_column", &file, tera, cv_data, language.language);
|
render_template("two_column", &file, tera, cv_data, language.language);
|
||||||
Ok(NamedFile::open(file.path()).await.unwrap())
|
Ok(NamedFile::open(file.path()).await.unwrap())
|
||||||
},
|
},
|
||||||
@ -95,5 +99,5 @@ pub async fn render_html_cv(username: &str, tera: NanoTera, tracing: TracingSpan
|
|||||||
Err(Status::InternalServerError)
|
Err(Status::InternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use ovlach_data::cv::cv::CV;
|
use ovlach_data::cv::data::CV;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
use tracing::{debug, instrument};
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -13,11 +14,14 @@ impl From<reqwest::Error> for FetchError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_cv_data_from_backend(backend_host: String, client: Client) -> Result<CV, FetchError> {
|
#[instrument]
|
||||||
|
pub async fn fetch_cv_data_from_backend(backend_host: &String, person_name: &String, client: &Client) -> Result<CV, FetchError> {
|
||||||
|
let url = format!("{}/{}/{}", backend_host, "api/v1/cv", person_name);
|
||||||
|
debug!("Fetching CV data from backend: {}", url);
|
||||||
let resp = client
|
let resp = client
|
||||||
.get(format!("{}/{}", backend_host, "/api/cv/ovlach")).send()
|
.get(url).send()
|
||||||
.await?
|
.await?
|
||||||
.json::<CV>()
|
.json::<CV>()
|
||||||
.await?;
|
.await?;
|
||||||
Ok(resp)
|
Ok(resp)
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ impl<'r> FromRequest<'r> for NanoTera {
|
|||||||
tera.register_filter("format_date", get_year); // deprecated
|
tera.register_filter("format_date", get_year); // deprecated
|
||||||
tera.register_filter("get_year", get_year);
|
tera.register_filter("get_year", get_year);
|
||||||
tera.register_filter("strip_proto", strip_proto);
|
tera.register_filter("strip_proto", strip_proto);
|
||||||
|
tera.register_filter("advanced_filter", advanced_filter);
|
||||||
rocket::outcome::Outcome::Success(NanoTera(tera))
|
rocket::outcome::Outcome::Success(NanoTera(tera))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -45,3 +46,113 @@ pub fn strip_proto(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use tera::{try_get_value, to_value};
|
||||||
|
// TODO: move to other library
|
||||||
|
/// If the `value` is not passed, optionally discard all elements where the attribute is null. (include_null -> all, none, only)
|
||||||
|
pub fn advanced_filter(value: &Value, args: &HashMap<String, Value>) -> Result<tera::Value, tera::Error> {
|
||||||
|
let mut arr = try_get_value!("filter", "value", Vec<Value>, value);
|
||||||
|
if arr.is_empty() {
|
||||||
|
return Ok(arr.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = match args.get("attribute") {
|
||||||
|
Some(val) => try_get_value!("filter", "attribute", String, val),
|
||||||
|
None => return Err(Error::msg("The `filter` filter has to have an `attribute` argument")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let null = match args.get("include_null") {
|
||||||
|
Some(val) => try_get_value!("filter", "attribute", String, val),
|
||||||
|
None => "none".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let against_value = match args.get("value") {
|
||||||
|
Some(val) => Some(try_get_value!("filter", "value", String, val)),
|
||||||
|
None => None
|
||||||
|
};
|
||||||
|
|
||||||
|
if null == "only" && against_value.is_some() {
|
||||||
|
return Err(Error::msg("The `filter` filter cannot have both `include_null=only` and `value`"))
|
||||||
|
}
|
||||||
|
|
||||||
|
arr = arr
|
||||||
|
.into_iter()
|
||||||
|
.filter(|v| {
|
||||||
|
let tested_value = v.get(key.clone()).unwrap_or(&Value::Null);
|
||||||
|
if tested_value.is_null() {
|
||||||
|
return match null.as_str() {
|
||||||
|
"all" => true,
|
||||||
|
"none" => false,
|
||||||
|
"only" => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
} else if null != "only" {
|
||||||
|
let val = tested_value.as_str();
|
||||||
|
match val {
|
||||||
|
Some(match_v) => {
|
||||||
|
match against_value.clone() {
|
||||||
|
Some(against_v) => match_v == against_v,
|
||||||
|
None => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => true,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(to_value(arr).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use serde_json::json;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
fn call_advanced_filter(data: &Vec<TestClass>, include_null: &str, value: Option<&str>) -> Result<Value, Error> {
|
||||||
|
let mut args = HashMap::new();
|
||||||
|
args.insert("attribute".to_string(), json!("against_value"));
|
||||||
|
args.insert("include_null".to_string(), json!(include_null));
|
||||||
|
if let Some(val) = value {
|
||||||
|
args.insert("value".to_string(), json!(val));
|
||||||
|
}
|
||||||
|
advanced_filter(&json!(data), &args)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||||
|
struct TestClass {
|
||||||
|
against_value: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestClass {
|
||||||
|
pub fn new(against_value: Option<String>) -> Self {
|
||||||
|
TestClass { against_value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_test_class(first_data: Option<String>, second_data: Option<String>) -> Vec<TestClass> {
|
||||||
|
vec![TestClass::new(first_data), TestClass::new(second_data)]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_advanced_filter() {
|
||||||
|
let data = vec![TestClass::new(Some("foo".to_string())), TestClass::new(None)];
|
||||||
|
let result = call_advanced_filter(&data, "all", None).unwrap();
|
||||||
|
assert_eq!(result, json!(generate_test_class(Some("foo".to_string()), None)));
|
||||||
|
let result = call_advanced_filter(&data, "none", None).unwrap();
|
||||||
|
assert_eq!(result, json!(vec![TestClass::new(Some("foo".to_string()))]));
|
||||||
|
let result = call_advanced_filter(&data, "only", None).unwrap();
|
||||||
|
assert_eq!(result, json!(vec![TestClass::new(None)]));
|
||||||
|
|
||||||
|
// Test filtering strings
|
||||||
|
let result = call_advanced_filter(&data, "all", Some("foo")).unwrap();
|
||||||
|
assert_eq!(result, json!(vec![TestClass::new(Some("foo".to_string())), TestClass::new(None)]));
|
||||||
|
let result = call_advanced_filter(&data, "none", Some("bar")).unwrap();
|
||||||
|
let vec: Vec<TestClass> = vec![];
|
||||||
|
assert_eq!(result, json!(vec));
|
||||||
|
let result = call_advanced_filter(&data, "only", Some("zz")).is_err();
|
||||||
|
assert!(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -3,10 +3,10 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;800&family=Roboto&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;800&family=Roboto&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
/*font-weight: 800;*/
|
/*font-weight: 800;*/
|
||||||
font-size: 3em;
|
font-size: 3em;
|
||||||
@ -46,7 +46,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.flex-container {
|
.flex-container {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@ -55,7 +55,7 @@
|
|||||||
align-content: normal;
|
align-content: normal;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-items:nth-child(1) {
|
.flex-items:nth-child(1) {
|
||||||
display: block;
|
display: block;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
@ -64,7 +64,7 @@
|
|||||||
align-self: auto;
|
align-self: auto;
|
||||||
order: 0;
|
order: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-items:nth-child(2) {
|
.flex-items:nth-child(2) {
|
||||||
display: block;
|
display: block;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
@ -90,7 +90,7 @@
|
|||||||
align-items: normal;
|
align-items: normal;
|
||||||
align-content: normal;
|
align-content: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-items-experience:nth-child(1) {
|
.flex-items-experience:nth-child(1) {
|
||||||
display: block;
|
display: block;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
@ -100,7 +100,7 @@
|
|||||||
width: 70%;
|
width: 70%;
|
||||||
order: 0;
|
order: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-items-experience:nth-child(2) {
|
.flex-items-experience:nth-child(2) {
|
||||||
display: block;
|
display: block;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
@ -130,7 +130,7 @@
|
|||||||
.header {
|
.header {
|
||||||
padding-top: 1em;
|
padding-top: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-items-skills:nth-child(1) {
|
.flex-items-skills:nth-child(1) {
|
||||||
display: block;
|
display: block;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
@ -140,7 +140,7 @@
|
|||||||
width: 50%;
|
width: 50%;
|
||||||
order: 0;
|
order: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-items-skills:nth-child(2) {
|
.flex-items-skills:nth-child(2) {
|
||||||
display: block;
|
display: block;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
@ -169,7 +169,7 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.contact-bar {
|
.contact-bar {
|
||||||
font-size: 11pt;
|
font-size: 11pt;
|
||||||
padding-left: 1em;
|
padding-left: 1em;
|
||||||
@ -190,13 +190,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.email{
|
.email{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
height: 1em;
|
height: 1em;
|
||||||
vertical-align:middle;
|
vertical-align:middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bolder {
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -219,30 +227,92 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-container-skills">
|
<div class="flex-container-skills">
|
||||||
<div class="flex-items-skills">
|
<div class="flex-items-skills">
|
||||||
{% for skill in cv.skills %}
|
{% for skill in cv.skills | filter(attribute="techtype",value="Language") %}
|
||||||
{% if skill.techtype == "LANGUAGE" %}
|
<div>
|
||||||
<div>
|
<span class="tech-name">{{ skill.name }}</span>{%if skill.skill %}<span class="tech-level"> ({{ skill.skill | lower}}){%endif %}</span>
|
||||||
<span class="tech-name">{{ skill.name }}</span><span class="tech-level"> ({{ skill.skill | lower}})</span>
|
</div>
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-items-skills">
|
<div class="flex-items-skills">
|
||||||
{% for skill in cv.skills %}
|
{% for skill in cv.skills | filter(attribute="techtype", value="Technology")%}
|
||||||
{% if skill.techtype == "TECHNOLOGY" %}
|
<div>
|
||||||
<div>
|
<span class="tech-name">{{ skill.name }}</span>{%if skill.skill %}<span class="tech-level"> ({{ skill.skill | lower}}){%endif %}</span>
|
||||||
<span class="tech-name">{{ skill.name }}</span><span class="tech-level"> ({{ skill.skill | lower}})</span>
|
</div>
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="part">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="flex-container-skills">
|
||||||
|
<div class="flex-items-skills">
|
||||||
|
<h2>{{ "skills-frameworks" | translate(lang=lang) }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="flex-items-skills">
|
||||||
|
<h2>{{ "skills-databases" | translate(lang=lang) }}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-container-skills">
|
||||||
|
<div class="flex-items-skills">
|
||||||
|
{% for skill in cv.skills | filter(attribute="techtype",value="Framework") %}
|
||||||
|
<div>
|
||||||
|
<span class="tech-name">{{ skill.name }}</span>{%if skill.skill %}<span class="tech-level"> ({{ skill.skill | lower}}){%endif %}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="flex-items-skills">
|
||||||
|
{% for skill in cv.skills | filter(attribute="techtype", value="Database")%}
|
||||||
|
<div>
|
||||||
|
<span class="tech-name">{{ skill.name }}</span>{%if skill.skill %}<span class="tech-level"> ({{ skill.skill | lower}}){%endif %}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="part">
|
||||||
|
<span class="bolder">{{"tools" | translate(lang=lang)}}:</span>
|
||||||
|
{% for skill in cv.skills | filter(attribute="techtype",value="Tool") | advanced_filter(attribute="skill", include_null="all") %}
|
||||||
|
{{ skill.name }}{% if skill.skill %} - {{skill.skill}}{% endif %},<!-- TODO: fix-me (empty "," if operating system empty) %-->
|
||||||
|
{% endfor %}
|
||||||
|
<span class="bolder">{{"operating-systems" | translate(lang=lang)}}:</span>
|
||||||
|
{% for skill in cv.skills | filter(attribute="techtype",value="OperatingSystem") | advanced_filter(attribute="skill", include_null="all") %}
|
||||||
|
{{ skill.name }}</span>{% if skill.skill %} - {{skill.skill}}{% endif %}{% if not loop.last %},{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
<div class="part">
|
<div class="part">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>{{ "work-experience" | translate(lang=lang) }}</h2>
|
<h2>{{ "work-experience" | translate(lang=lang) }}</h2>
|
||||||
</div>
|
</div>
|
||||||
{% for job in cv.jobs %}
|
{% for job in cv.jobs | filter(attribute="jobtype", value="Contract") %}
|
||||||
|
<div class="job part-content">
|
||||||
|
<div class="flex-container-experience">
|
||||||
|
<div class="flex-items-experience">
|
||||||
|
<div class="title">{{ job.title }}</div>
|
||||||
|
<div class="languages">{{ job.languages }}</div>
|
||||||
|
<p>
|
||||||
|
{{ job.description | lang_entity(lang=lang) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex-items-experience">
|
||||||
|
<div class="company">@{{ job.company }}</div>
|
||||||
|
<div class="dates">
|
||||||
|
{% if job.from | format_date(type="job") != job.from | format_date(type="job") %}
|
||||||
|
<div class="text-muted text-small mb-3">{{ job.from | format_date(type="job") }} - {{ job.to | format_date(type="job") }}</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-muted text-small mb-3">{{ job.from | format_date(type="job") }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="part">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>{{ "work-freelance" | translate(lang=lang) }}</h2>
|
||||||
|
</div>
|
||||||
|
{% for job in cv.jobs | filter(attribute="jobtype", value="Freelance") %}
|
||||||
<div class="job part-content">
|
<div class="job part-content">
|
||||||
<div class="flex-container-experience">
|
<div class="flex-container-experience">
|
||||||
<div class="flex-items-experience">
|
<div class="flex-items-experience">
|
||||||
@ -281,12 +351,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if education.description %}
|
{% if education.description %}
|
||||||
<p>
|
<p>
|
||||||
|
|
||||||
{{ education.description | lang_entity(lang=lang) }}
|
{{ education.description | lang_entity(lang=lang) }}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-items-experience">
|
<div class="flex-items-experience">
|
||||||
{% if education.degree %}
|
{% if education.degree %}
|
||||||
<div class="company">@{{ education.school }}</div>
|
<div class="company">@{{ education.school }}</div>
|
||||||
@ -317,7 +387,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if cv.person.social.instagram %}
|
{% if cv.person.social.instagram %}
|
||||||
<i class="fa-instagram"> {{ cv.person.social.instagram | strip_proto }}</i>
|
<i class="fa-instagram"> {{ cv.person.social.instagram | strip_proto }}</i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if cv.person.social.mastodon %}
|
{% if cv.person.social.mastodon %}
|
||||||
<i class="fa-mastodon">
|
<i class="fa-mastodon">
|
||||||
<img alt="Mastodon" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPgo8c3ZnIGZpbGw9IiMwMDAwMDAiIHdpZHRoPSI4MDBweCIgaGVpZ2h0PSI4MDBweCIgdmlld0JveD0iMCAwIDI0IDI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiPjxwYXRoIGQ9Ik0yMS4zMjcgOC41NjZjMC00LjMzOS0yLjg0My01LjYxLTIuODQzLTUuNjEtMS40MzMtLjY1OC0zLjg5NC0uOTM1LTYuNDUxLS45NTZoLS4wNjNjLTIuNTU3LjAyMS01LjAxNi4yOTgtNi40NS45NTYgMCAwLTIuODQzIDEuMjcyLTIuODQzIDUuNjEgMCAuOTkzLS4wMTkgMi4xODEuMDEyIDMuNDQxLjEwMyA0LjI0My43NzggOC40MjUgNC43MDEgOS40NjMgMS44MDkuNDc5IDMuMzYyLjU3OSA0LjYxMi41MSAyLjI2OC0uMTI2IDMuNTQxLS44MDkgMy41NDEtLjgwOWwtLjA3NS0xLjY0NnMtMS42MjEuNTExLTMuNDQxLjQ0OWMtMS44MDQtLjA2Mi0zLjcwNy0uMTk0LTMuOTk5LTIuNDA5YTQuNTIzIDQuNTIzIDAgMCAxLS4wNC0uNjIxczEuNzcuNDMzIDQuMDE0LjUzNmMxLjM3Mi4wNjMgMi42NTgtLjA4IDMuOTY1LS4yMzYgMi41MDYtLjI5OSA0LjY4OC0xLjg0MyA0Ljk2Mi0zLjI1NC40MzQtMi4yMjMuMzk4LTUuNDI0LjM5OC01LjQyNHptLTMuMzUzIDUuNTloLTIuMDgxVjkuMDU3YzAtMS4wNzUtLjQ1Mi0xLjYyLTEuMzU3LTEuNjItMSAwLTEuNTAxLjY0Ny0xLjUwMSAxLjkyN3YyLjc5MWgtMi4wNjlWOS4zNjRjMC0xLjI4LS41MDEtMS45MjctMS41MDItMS45MjctLjkwNSAwLTEuMzU3LjU0Ni0xLjM1NyAxLjYydjUuMDk5SDYuMDI2VjguOTAzYzAtMS4wNzQuMjczLTEuOTI3LjgyMy0yLjU1OC41NjYtLjYzMSAxLjMwNy0uOTU1IDIuMjI4LS45NTUgMS4wNjUgMCAxLjg3Mi40MDkgMi40MDUgMS4yMjhsLjUxOC44NjkuNTE5LS44NjljLjUzMy0uODE5IDEuMzQtMS4yMjggMi40MDUtMS4yMjguOTIgMCAxLjY2Mi4zMjQgMi4yMjguOTU1LjU0OS42MzEuODIyIDEuNDg0LjgyMiAyLjU1OHY1LjI1M3oiLz48L3N2Zz4=" class="icon" />
|
<img alt="Mastodon" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPgo8c3ZnIGZpbGw9IiMwMDAwMDAiIHdpZHRoPSI4MDBweCIgaGVpZ2h0PSI4MDBweCIgdmlld0JveD0iMCAwIDI0IDI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiPjxwYXRoIGQ9Ik0yMS4zMjcgOC41NjZjMC00LjMzOS0yLjg0My01LjYxLTIuODQzLTUuNjEtMS40MzMtLjY1OC0zLjg5NC0uOTM1LTYuNDUxLS45NTZoLS4wNjNjLTIuNTU3LjAyMS01LjAxNi4yOTgtNi40NS45NTYgMCAwLTIuODQzIDEuMjcyLTIuODQzIDUuNjEgMCAuOTkzLS4wMTkgMi4xODEuMDEyIDMuNDQxLjEwMyA0LjI0My43NzggOC40MjUgNC43MDEgOS40NjMgMS44MDkuNDc5IDMuMzYyLjU3OSA0LjYxMi41MSAyLjI2OC0uMTI2IDMuNTQxLS44MDkgMy41NDEtLjgwOWwtLjA3NS0xLjY0NnMtMS42MjEuNTExLTMuNDQxLjQ0OWMtMS44MDQtLjA2Mi0zLjcwNy0uMTk0LTMuOTk5LTIuNDA5YTQuNTIzIDQuNTIzIDAgMCAxLS4wNC0uNjIxczEuNzcuNDMzIDQuMDE0LjUzNmMxLjM3Mi4wNjMgMi42NTgtLjA4IDMuOTY1LS4yMzYgMi41MDYtLjI5OSA0LjY4OC0xLjg0MyA0Ljk2Mi0zLjI1NC40MzQtMi4yMjMuMzk4LTUuNDI0LjM5OC01LjQyNHptLTMuMzUzIDUuNTloLTIuMDgxVjkuMDU3YzAtMS4wNzUtLjQ1Mi0xLjYyLTEuMzU3LTEuNjItMSAwLTEuNTAxLjY0Ny0xLjUwMSAxLjkyN3YyLjc5MWgtMi4wNjlWOS4zNjRjMC0xLjI4LS41MDEtMS45MjctMS41MDItMS45MjctLjkwNSAwLTEuMzU3LjU0Ni0xLjM1NyAxLjYydjUuMDk5SDYuMDI2VjguOTAzYzAtMS4wNzQuMjczLTEuOTI3LjgyMy0yLjU1OC41NjYtLjYzMSAxLjMwNy0uOTU1IDIuMjI4LS45NTUgMS4wNjUgMCAxLjg3Mi40MDkgMi40MDUgMS4yMjhsLjUxOC44NjkuNTE5LS44NjljLjUzMy0uODE5IDEuMzQtMS4yMjggMi40MDUtMS4yMjguOTIgMCAxLjY2Mi4zMjQgMi4yMjguOTU1LjU0OS42MzEuODIyIDEuNDg0LjgyMiAyLjU1OHY1LjI1M3oiLz48L3N2Zz4=" class="icon" />
|
||||||
@ -333,8 +403,7 @@
|
|||||||
+{{cv.person.phone | insert_space_every(times=3)}}
|
+{{cv.person.phone | insert_space_every(times=3)}}
|
||||||
</i>
|
</i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
Loading…
Reference in New Issue
Block a user