diff --git a/Cargo.lock b/Cargo.lock index f455033..863bd40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1404,9 +1404,9 @@ dependencies = [ [[package]] name = "nanobyte_opentelemetry" -version = "0.2.2" +version = "0.2.3" source = "sparse+https://git.nanobyte.cz/api/packages/nanobyte/cargo/" -checksum = "a3470844579913374d0a7026b7daf02a1be92e006f089aa908440026fa5b0abf" +checksum = "053fac4ff7f3cab0a088383bc68774e402c07c92d19478bcbb51d1d49a5d42bf" dependencies = [ "gethostname", "opentelemetry", @@ -1429,16 +1429,19 @@ dependencies = [ [[package]] name = "nanobyte_tera" -version = "0.1.0" -source = "git+https://glpat-Us_EdFTzQLv4shViQXi_:glpat-Us_EdFTzQLv4shViQXi_@gitlab.nanobyte.cz/tools/nanobyte_tera.git?branch=master#75c20a9806663ca04c6f8a7afff64d7b5906d113" +version = "0.2.0" +source = "sparse+https://git.nanobyte.cz/api/packages/nanobyte/cargo/" +checksum = "6fa686074d8273526885446e3c7f4a6f35affab0dfcbba55c9b1aca6efae7c67" dependencies = [ "chrono", "fluent-bundle", "fluent-resmgr", "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", + "serde", + "serde_json", "sha256", + "tracing", "unic-langid", ] @@ -1703,18 +1706,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "ovlach_data" -version = "0.1.0" -source = "git+https://glpat-Ju_qUN9Yh8qa5rEnd6T7:glpat-Ju_qUN9Yh8qa5rEnd6T7@gitlab.nanobyte.cz/ondrej/ov-site-api-data.git?branch=add_missing_fields#c13748b039d812d3bf1aaa93312699da7b921868" -dependencies = [ - "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" +version = "0.1.2" +source = "sparse+https://git.nanobyte.cz/api/packages/ovlach/cargo/" +checksum = "98843b3cefbdbf054f13312e82493248501cdd8abb39379ca2546559358c1437" dependencies = [ "chrono", "rocket", @@ -1732,12 +1726,13 @@ dependencies = [ "log", "nanobyte_opentelemetry", "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", "phf", "reqwest", "rocket", "serde", + "serde_json", "serde_yaml", "tempfile", "tera", @@ -1749,8 +1744,9 @@ dependencies = [ [[package]] name = "ovlach_tera" -version = "0.1.0" -source = "git+https://glpat-_yPuXbEzECyk3FaHudCN:glpat-_yPuXbEzECyk3FaHudCN@gitlab.nanobyte.cz/ondrej/ovlach_tera.git?branch=master#0ab1bbadd76c1336e00b2a38e572048003879ab6" +version = "0.2.0" +source = "sparse+https://git.nanobyte.cz/api/packages/ovlach/cargo/" +checksum = "fd6d3a0c415f223c68db7bd0290405b247f488180e6b96810b3be4acd0b37764" dependencies = [ "rocket_dyn_templates", "serde", diff --git a/Cargo.toml b/Cargo.toml index 8c9eb73..27932a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,6 @@ fern = "0.6.2" rocket = { version = "0.5.0", features = ["json"] } 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-mutex = "1.4.0" tokio = { version = "1.34.0", features = ["macros"] } @@ -22,9 +21,12 @@ yansi = "0.5.1" tera = "1.19.1" tempfile = "3.8.1" 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"] } 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" } -ovlach_tera = { git = "https://glpat-_yPuXbEzECyk3FaHudCN:glpat-_yPuXbEzECyk3FaHudCN@gitlab.nanobyte.cz/ondrej/ovlach_tera.git", branch = "master" } +[dev-dependencies] +serde_json = "1.0.108" diff --git a/Rocket.toml b/Rocket.toml index 6add778..1984b8f 100644 --- a/Rocket.toml +++ b/Rocket.toml @@ -5,4 +5,5 @@ cv_backend_path = "http://localhost:8002" [default] static_route = "http://localhost:8001" cv_backend_path = "http://localhost:8002" -port = 8003 \ No newline at end of file +port = 8003 +default_person_name = "ovlach" diff --git a/resources/cs-CZ/ovlach_frontend b/resources/cs-CZ/ovlach_pdf similarity index 100% rename from resources/cs-CZ/ovlach_frontend rename to resources/cs-CZ/ovlach_pdf diff --git a/resources/en-US/ovlach_frontend b/resources/en-US/ovlach_pdf similarity index 100% rename from resources/en-US/ovlach_frontend rename to resources/en-US/ovlach_pdf diff --git a/src/lib.rs b/src/lib.rs index 9202ed9..2f0f46e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,13 @@ pub struct CVBackendConfig { cv_backend_path: String, } + +#[derive(Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct DefaultPerson { + default_person_name: String, +} + pub fn rocket_builder() -> Rocket { rocket::build() .attach(TracingFairing::ignite()) @@ -22,8 +29,11 @@ pub fn rocket_builder() -> Rocket { .attach( AdHoc::config::() ) + .attach( + AdHoc::config::() + ) .mount("/", routes![ routes::pdf::render_pdf_cv, routes::pdf::render_html_cv ]) -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index 7a30c9f..4768c7c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,8 +11,9 @@ async fn main() { install_panic_handler(); let _opentelemetry = nanobyte_opentelemetry::init_telemetry( 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)) ); let _ = rocket_builder().launch().await; -} \ No newline at end of file +} diff --git a/src/routes/pdf.rs b/src/routes/pdf.rs index 7d0141f..9dc152f 100644 --- a/src/routes/pdf.rs +++ b/src/routes/pdf.rs @@ -1,7 +1,8 @@ use headless_chrome::Browser; use headless_chrome::types::PrintToPdfOptions; 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 rocket::fs::NamedFile; use ::rocket::{State, http::Status}; @@ -9,6 +10,7 @@ use ::rocket::get; use tempfile::NamedTempFile; use tera::Context; 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}; // TODO: request-id @@ -31,7 +33,7 @@ fn generate_pdf(browser: Browser, file: &NamedTempFile) -> Vec { ..PrintToPdfOptions::default() }; - //thread::sleep(Duration::from_secs(10)); + //thread::sleep(Duration::from_secs(10)); let bytes = info_span!("print_pdf").in_scope(|| { 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 let mut tera_context = Context::new(); 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) { Ok(_) => { 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///output.pdf")] pub async fn render_pdf_cv(username: &str, tera: NanoTera, tracing: TracingSpan, request_client: OtelReqwestClient, - cv_config: &State, language: RequestLanguage, browser: BrowserHolder) -> Result { + cv_config: &State, language: RequestLanguage, browser: BrowserHolder, + default_person: &State) -> Result { 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) => { - 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); let span = info_span!("render_pdf", username = username); 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 #[get("/cv///output.html")] pub async fn render_html_cv(username: &str, tera: NanoTera, tracing: TracingSpan, request_client: OtelReqwestClient, - cv_config: &State, language: RequestLanguage) -> Result { + cv_config: &State, language: RequestLanguage, + default_person: &State) -> Result { 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) => { - 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); 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) } } - -} \ No newline at end of file + +} diff --git a/src/services/cv.rs b/src/services/cv.rs index 90c62b0..3a74252 100644 --- a/src/services/cv.rs +++ b/src/services/cv.rs @@ -1,5 +1,6 @@ -use ovlach_data::cv::cv::CV; +use ovlach_data::cv::data::CV; use reqwest::Client; +use tracing::{debug, instrument}; #[derive(Debug)] @@ -13,11 +14,14 @@ impl From for FetchError { } } -pub async fn fetch_cv_data_from_backend(backend_host: String, client: Client) -> Result { +#[instrument] +pub async fn fetch_cv_data_from_backend(backend_host: &String, person_name: &String, client: &Client) -> Result { + let url = format!("{}/{}/{}", backend_host, "api/v1/cv", person_name); + debug!("Fetching CV data from backend: {}", url); let resp = client - .get(format!("{}/{}", backend_host, "/api/cv/ovlach")).send() + .get(url).send() .await? .json::() .await?; Ok(resp) -} \ No newline at end of file +} diff --git a/src/tools/tera.rs b/src/tools/tera.rs index a78861a..e69a508 100644 --- a/src/tools/tera.rs +++ b/src/tools/tera.rs @@ -25,6 +25,7 @@ impl<'r> FromRequest<'r> for NanoTera { tera.register_filter("format_date", get_year); // deprecated tera.register_filter("get_year", get_year); tera.register_filter("strip_proto", strip_proto); + tera.register_filter("advanced_filter", advanced_filter); 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) -> Result { + let mut arr = try_get_value!("filter", "value", Vec, 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::>(); + + 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, include_null: &str, value: Option<&str>) -> Result { + 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, + } + + impl TestClass { + pub fn new(against_value: Option) -> Self { + TestClass { against_value } + } + } + + fn generate_test_class(first_data: Option, second_data: Option) -> Vec { + 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 = vec![]; + assert_eq!(result, json!(vec)); + let result = call_advanced_filter(&data, "only", Some("zz")).is_err(); + assert!(result); + } +} diff --git a/templates/two_column.html.tera b/templates/two_column.html.tera index dc3a47d..73cfea5 100644 --- a/templates/two_column.html.tera +++ b/templates/two_column.html.tera @@ -3,10 +3,10 @@ - - + + @@ -219,30 +227,92 @@
- {% for skill in cv.skills %} - {% if skill.techtype == "LANGUAGE" %} -
- {{ skill.name }} ({{ skill.skill | lower}}) -
- {% endif %} + {% for skill in cv.skills | filter(attribute="techtype",value="Language") %} +
+ {{ skill.name }}{%if skill.skill %} ({{ skill.skill | lower}}){%endif %} +
{% endfor %}
- {% for skill in cv.skills %} - {% if skill.techtype == "TECHNOLOGY" %} -
- {{ skill.name }} ({{ skill.skill | lower}}) -
- {% endif %} + {% for skill in cv.skills | filter(attribute="techtype", value="Technology")%} +
+ {{ skill.name }}{%if skill.skill %} ({{ skill.skill | lower}}){%endif %} +
{% endfor %}
+
+
+
+
+

{{ "skills-frameworks" | translate(lang=lang) }}

+
+
+

{{ "skills-databases" | translate(lang=lang) }}

+
+
+
+
+
+ {% for skill in cv.skills | filter(attribute="techtype",value="Framework") %} +
+ {{ skill.name }}{%if skill.skill %} ({{ skill.skill | lower}}){%endif %} +
+ {% endfor %} +
+
+ {% for skill in cv.skills | filter(attribute="techtype", value="Database")%} +
+ {{ skill.name }}{%if skill.skill %} ({{ skill.skill | lower}}){%endif %} +
+ {% endfor %} +
+
+
+
+ {{"tools" | translate(lang=lang)}}: + {% 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 %}, + {% endfor %} + {{"operating-systems" | translate(lang=lang)}}: + {% for skill in cv.skills | filter(attribute="techtype",value="OperatingSystem") | advanced_filter(attribute="skill", include_null="all") %} + {{ skill.name }}{% if skill.skill %} - {{skill.skill}}{% endif %}{% if not loop.last %},{% endif %} + {% endfor %} +

{{ "work-experience" | translate(lang=lang) }}

- {% for job in cv.jobs %} + {% for job in cv.jobs | filter(attribute="jobtype", value="Contract") %} +
+
+
+
{{ job.title }}
+
{{ job.languages }}
+

+ {{ job.description | lang_entity(lang=lang) }} +

+
+
+
@{{ job.company }}
+
+ {% if job.from | format_date(type="job") != job.from | format_date(type="job") %} +
{{ job.from | format_date(type="job") }} - {{ job.to | format_date(type="job") }}
+ {% else %} +
{{ job.from | format_date(type="job") }}
+ {% endif %} +
+
+
+
+ {% endfor %} +
+
+
+

{{ "work-freelance" | translate(lang=lang) }}

+
+ {% for job in cv.jobs | filter(attribute="jobtype", value="Freelance") %}
@@ -281,12 +351,12 @@ {% endif %} {% if education.description %}

- + {{ education.description | lang_entity(lang=lang) }}

{% endif %}
- +
{% if education.degree %}
@{{ education.school }}
@@ -317,7 +387,7 @@ {% endif %} {% if cv.person.social.instagram %} {{ cv.person.social.instagram | strip_proto }} - {% endif %} + {% endif %} {% if cv.person.social.mastodon %} Mastodon @@ -333,8 +403,7 @@ +{{cv.person.phone | insert_space_every(times=3)}}
-
- \ No newline at end of file +