Pour tout problème contactez-nous par mail : support@froggit.fr | La FAQ :grey_question: | Rejoignez-nous sur le Chat :speech_balloon:

Skip to content
Snippets Groups Projects
Commit f1da2b41 authored by Pierre Jarriges's avatar Pierre Jarriges
Browse files

all good

parent f3ba7f03
No related branches found
No related tags found
1 merge request!1Dev
...@@ -6,4 +6,6 @@ node_modules ...@@ -6,4 +6,6 @@ node_modules
target target
.env .env
public/**/*.js public/**/*.js
public/**/view/* public/**/view/*
\ No newline at end of file public/standard/test_sitemap.xml
public/standard/sitemap.xml
\ No newline at end of file
...@@ -583,6 +583,15 @@ dependencies = [ ...@@ -583,6 +583,15 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "chrono_utils"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f69ed74e2117892a1a4e05d31f6612178e8e827bfbd83bbf8ca8c1bcfbda710"
dependencies = [
"chrono",
]
[[package]] [[package]]
name = "cipher" name = "cipher"
version = "0.3.0" version = "0.3.0"
...@@ -1284,6 +1293,7 @@ dependencies = [ ...@@ -1284,6 +1293,7 @@ dependencies = [
"rustls 0.18.1", "rustls 0.18.1",
"serde", "serde",
"serde_json", "serde_json",
"sitemap",
"time 0.2.27", "time 0.2.27",
"tokio", "tokio",
"wither", "wither",
...@@ -2178,6 +2188,18 @@ dependencies = [ ...@@ -2178,6 +2188,18 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "sitemap"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c697e1d88854f66d7d805e8b532e341661a4b8f49fab419be8e82b0cafdeb4d"
dependencies = [
"chrono",
"chrono_utils",
"url",
"xml-rs",
]
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.4" version = "0.4.4"
...@@ -2956,3 +2978,9 @@ dependencies = [ ...@@ -2956,3 +2978,9 @@ dependencies = [
"winapi 0.2.8", "winapi 0.2.8",
"winapi-build", "winapi-build",
] ]
[[package]]
name = "xml-rs"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
...@@ -24,3 +24,4 @@ dotenv = "0.15" ...@@ -24,3 +24,4 @@ dotenv = "0.15"
time = "0.2.7" time = "0.2.7"
regex = "1.5" regex = "1.5"
tokio = { version = "0.2", features = ["full"] } tokio = { version = "0.2", features = ["full"] }
sitemap = "0.4.1"
...@@ -14,7 +14,7 @@ reload-api: ...@@ -14,7 +14,7 @@ reload-api:
docker-compose restart kuadrado_server docker-compose restart kuadrado_server
test: test:
RESOURCES_DIR="./" cargo test -- --test-threads=1 RESOURCES_DIR="./" CONTEXT=testing cargo test -- --test-threads=1
doc: doc:
cargo doc --no-deps cargo doc --no-deps
......
...@@ -81,6 +81,9 @@ _Env vars may be defined in a .env file at the root of the project_ ...@@ -81,6 +81,9 @@ _Env vars may be defined in a .env file at the root of the project_
- `CRYPT_KEY`: Any random ascii string that will be used to encrypt data like emails and passwords. - `CRYPT_KEY`: Any random ascii string that will be used to encrypt data like emails and passwords.
## Tests
Create a public/standard/test_sitemap.xml file with a sitemap structure in order to pass the tests.
## Admin panel - create a Play Button ## Admin panel - create a Play Button
**Syntax** **Syntax**
......
...@@ -13,6 +13,7 @@ class Article { ...@@ -13,6 +13,7 @@ class Article {
this.body = ""; this.body = "";
this.locale = ""; this.locale = "";
this.display_priority_index = 1; this.display_priority_index = 1;
this.with_static_view = true;
this.metadata = { this.metadata = {
description: "", description: "",
}; };
......
...@@ -26,6 +26,10 @@ class CreateArticleForm { ...@@ -26,6 +26,10 @@ class CreateArticleForm {
this.state.output.metadata = metadata; this.state.output.metadata = metadata;
} }
handle_change_bool_checkbox(field, e) {
this.state.output[field] = e.target.checked;
}
handle_text_input(field, e) { handle_text_input(field, e) {
this.state.output[field] = e.target.value; this.state.output[field] = e.target.value;
} }
...@@ -335,6 +339,15 @@ class CreateArticleForm { ...@@ -335,6 +339,15 @@ class CreateArticleForm {
value: this.state.output.title, value: this.state.output.title,
oninput: this.handle_text_input.bind(this, "title") oninput: this.handle_text_input.bind(this, "title")
}, },
{
tag: "div", contents: [
{
tag: "input", type: "checkbox", checked: this.state.output.with_static_view,
onchange: this.handle_change_bool_checkbox.bind(this, "with_static_view")
},
{ tag: "label", contents: "Create static view" },
]
},
{ {
tag: "input", type: "text", tag: "input", type: "text",
style_rules: { style_rules: {
......
...@@ -8,6 +8,11 @@ db.auth(adminname, adminpwd); ...@@ -8,6 +8,11 @@ db.auth(adminname, adminpwd);
articles = db.getCollection("articles"); articles = db.getCollection("articles");
articles.update({}, articles.update({},
{ $set: { "metadata": { "description": "" } } }, {
$set: {
"metadata": { "description": "" },
"with_static_view": false
}
},
{ upsert: false, multi: true } { upsert: false, multi: true }
); );
\ No newline at end of file
...@@ -35,6 +35,7 @@ pub struct Article { ...@@ -35,6 +35,7 @@ pub struct Article {
pub category: String, pub category: String,
pub locale: String, pub locale: String,
pub display_priority_index: i8, pub display_priority_index: i8,
pub with_static_view: bool,
pub metadata: ArticleMetadata, pub metadata: ArticleMetadata,
} }
...@@ -61,6 +62,7 @@ impl Article { ...@@ -61,6 +62,7 @@ impl Article {
category: "testing".to_string(), category: "testing".to_string(),
locale: "fr".to_string(), locale: "fr".to_string(),
display_priority_index: 1, display_priority_index: 1,
with_static_view: true,
metadata: ArticleMetadata { metadata: ArticleMetadata {
description: "A test article".to_string(), description: "A test article".to_string(),
view_uri: None, view_uri: None,
......
...@@ -43,20 +43,28 @@ fn slugify(s: &String) -> String { ...@@ -43,20 +43,28 @@ fn slugify(s: &String) -> String {
} }
fn create_article_static_view_metadata(app_state: &AppState, art: &mut Article) { fn create_article_static_view_metadata(app_state: &AppState, art: &mut Article) {
let article_slug = slugify(&art.title); if art.with_static_view {
art.metadata.slug = Some(article_slug.to_owned()); let article_slug = slugify(&art.title);
art.metadata.view_uri = Some(format!( art.metadata.slug = Some(article_slug.to_owned());
"{}://{}/{}/{}/", art.metadata.view_uri = Some(format!(
&app_state.env.server_protocol, &app_state.env.server_host, &art.category, &article_slug, "{}://{}/{}/view/{}/{}/",
)); &app_state.env.server_protocol,
art.metadata.static_resource_path = Some( &app_state.env.server_host,
app_state &art.category,
.env &art.locale,
.public_dir &article_slug,
.join(&art.category) ));
.join("view")
.join(&article_slug), art.metadata.static_resource_path = Some(
); app_state
.env
.public_dir
.join(&art.category)
.join("view")
.join(&art.locale)
.join(&article_slug),
);
}
} }
async fn generate_article_view(app_state: &AppState, id: &ObjectId) -> Result<(), String> { async fn generate_article_view(app_state: &AppState, id: &ObjectId) -> Result<(), String> {
...@@ -67,44 +75,10 @@ async fn generate_article_view(app_state: &AppState, id: &ObjectId) -> Result<() ...@@ -67,44 +75,10 @@ async fn generate_article_view(app_state: &AppState, id: &ObjectId) -> Result<()
} }
let art = art.unwrap(); let art = art.unwrap();
if art.with_static_view {
if art.metadata.view_uri.is_none() { return create_static_view(&app_state, &art);
return Err(format!("Article {} doesn't have view uri", art.title));
}
let art_img_def = String::new();
let mut art_image_uri = art.images.iter().next().unwrap_or(&art_img_def).to_owned();
if !art_image_uri.is_empty() {
art_image_uri = format!("/assets/images/{}", art_image_uri);
} }
Ok(())
let slug = art.metadata.slug.unwrap();
let html = format!(
"
<html lang='{}'>
<head>
<meta charset='UTF-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<meta name='author' content='Kuadrado Software' />
<meta name='image' content='{}'/>
<meta name='description' content='{}'>
<link rel='icon' type='image/svg+xml' href='/favicon.svg' />
<title>{}</title>
<link href='/style/style.css' rel='stylesheet' />
</head>
<body>
<div>{}<div>
</body>
<script type='text/javascript' src='/games/view.js'></script>
</html>
",
art.locale, art_image_uri, art.metadata.description, art.title, art.body,
);
create_static_view(&app_state, art.category, slug, html)
} }
Err(e) => { Err(e) => {
return Err(format!( return Err(format!(
...@@ -178,10 +152,16 @@ pub async fn update_article( ...@@ -178,10 +152,16 @@ pub async fn update_article(
let mut article_data = article_data.into_inner(); let mut article_data = article_data.into_inner();
article_data.date = Some(DateTime(Utc::now())); article_data.date = Some(DateTime(Utc::now()));
if let Err(e) = delete_static_view(&article_data.metadata.static_resource_path) { if article_data.with_static_view {
return HttpResponse::InternalServerError() if let Err(e) = delete_static_view(
.body(format!("Error removing previous static view : {}", e)); &app_state,
}; &article_data.metadata.static_resource_path,
&article_data.metadata.view_uri,
) {
return HttpResponse::InternalServerError()
.body(format!("Error removing previous static view : {}", e));
};
}
create_article_static_view_metadata(&app_state, &mut article_data); create_article_static_view_metadata(&app_state, &mut article_data);
...@@ -223,13 +203,20 @@ pub async fn delete_article( ...@@ -223,13 +203,20 @@ pub async fn delete_article(
}; };
let articles = get_collection(&app_state); let articles = get_collection(&app_state);
match articles.find_one(doc! {"_id": &article_id}, None).await { match articles.find_one(doc! {"_id": &article_id}, None).await {
Ok(article) => match article { Ok(article) => match article {
Some(art) => { Some(art) => {
if let Err(e) = delete_static_view(&art.metadata.static_resource_path) { if art.with_static_view {
return HttpResponse::InternalServerError() if let Err(e) = delete_static_view(
.body(format!("Error removing article static view {}", e)); &app_state,
}; &art.metadata.static_resource_path,
&art.metadata.view_uri,
) {
return HttpResponse::InternalServerError()
.body(format!("Error removing article static view {}", e));
};
}
match articles.delete_one(doc! {"_id": &article_id}, None).await { match articles.delete_one(doc! {"_id": &article_id}, None).await {
Ok(_) => HttpResponse::Accepted().body("Article was deleted"), Ok(_) => HttpResponse::Accepted().body("Article was deleted"),
...@@ -339,12 +326,14 @@ mod test_articles { ...@@ -339,12 +326,14 @@ mod test_articles {
web::Bytes, web::Bytes,
App, App,
}; };
use std::fs::remove_dir_all;
async fn insert_test_article( async fn insert_test_article(
app_state: &AppState, app_state: &AppState,
test_article: Article, test_article: Article,
) -> Result<(ObjectId, String), String> { ) -> Result<(ObjectId, String), String> {
let title = test_article.title.to_owned(); let title = test_article.title.to_owned();
match get_collection(&app_state) match get_collection(&app_state)
.insert_one(test_article, None) .insert_one(test_article, None)
.await .await
...@@ -370,6 +359,17 @@ mod test_articles { ...@@ -370,6 +359,17 @@ mod test_articles {
} }
} }
fn clear_testing_static_view_dir(app_state: &AppState) -> Result<(), String> {
let path = app_state.env.public_dir.join("testing/view");
if path.exists() && path.is_dir() {
let res = remove_dir_all(path);
if let Err(e) = res {
return Err(format!("Error removing testing stativ views {}", e));
}
}
Ok(())
}
async fn get_authenticated_admin(app_state: &AppState) -> Administrator { async fn get_authenticated_admin(app_state: &AppState) -> Administrator {
Administrator::authenticated( Administrator::authenticated(
app_state, app_state,
...@@ -425,7 +425,17 @@ mod test_articles { ...@@ -425,7 +425,17 @@ mod test_articles {
.unwrap(); .unwrap();
assert!(find_inserted.is_some()); assert!(find_inserted.is_some());
assert_eq!(find_inserted.unwrap().title, article.title); let find_inserted = find_inserted.unwrap();
assert_eq!(find_inserted.title, article.title);
// Static view tests
let static_view_path = find_inserted.metadata.static_resource_path;
assert!(static_view_path.is_some());
let static_view_path = static_view_path.unwrap();
assert!(static_view_path.exists());
let cleared = clear_testing_static_view_dir(&app_state);
assert!(cleared.is_ok());
get_collection(&app_state) get_collection(&app_state)
.delete_one(doc! {"title": article.title}, None) .delete_one(doc! {"title": article.title}, None)
...@@ -518,7 +528,17 @@ mod test_articles { ...@@ -518,7 +528,17 @@ mod test_articles {
.unwrap(); .unwrap();
assert!(find_inserted.is_some()); assert!(find_inserted.is_some());
assert_eq!(find_inserted.unwrap().title, "changed title"); let find_inserted = find_inserted.unwrap();
assert_eq!(find_inserted.title, "changed title");
// Static view tests
let static_view_path = find_inserted.metadata.static_resource_path;
assert!(static_view_path.is_some());
let static_view_path = static_view_path.unwrap();
assert!(static_view_path.exists());
let cleared = clear_testing_static_view_dir(&app_state);
assert!(cleared.is_ok());
let del_count = delete_test_article(&app_state, &article_id).await.unwrap(); let del_count = delete_test_article(&app_state, &article_id).await.unwrap();
assert_eq!(del_count, 1); assert_eq!(del_count, 1);
......
use crate::app_state::AppState; use crate::{app_state::AppState, model::Article};
use std::fs::{create_dir, create_dir_all, remove_dir, remove_file, File}; use chrono::{Datelike, FixedOffset, TimeZone, Utc};
use std::io::Write; use sitemap::{
use std::path::PathBuf; reader::{SiteMapEntity, SiteMapReader},
structs::UrlEntry,
writer::SiteMapWriter,
};
use std::{
fs::{create_dir, create_dir_all, remove_dir_all, remove_file, rename, File},
io::Write,
path::PathBuf,
};
pub fn create_static_view( enum UpdateSitemapMode {
app_state: &AppState, CreateUrl,
category: String, DeleteUrl,
filename: String, }
html: String,
) -> Result<(), String> { pub fn create_static_view(app_state: &AppState, article: &Article) -> Result<(), String> {
let view_path = app_state.env.public_dir.join(&category).join("view"); let view_path = app_state
.env
.public_dir
.join(&article.category)
.join("view");
if !view_path.exists() { if !view_path.exists() {
if let Err(e) = create_dir(&view_path) { if let Err(e) = create_dir(&view_path) {
...@@ -17,46 +29,184 @@ pub fn create_static_view( ...@@ -17,46 +29,184 @@ pub fn create_static_view(
} }
} }
let d_path = app_state let d_path = article.metadata.static_resource_path.as_ref().unwrap();
.env
.public_dir
.join(&category)
.join("view")
.join(&filename);
if let Err(e) = create_dir_all(&d_path) { if let Err(e) = create_dir_all(&d_path) {
return Err(format!("Error creating directory {:?} : {}", d_path, e)); return Err(format!("Error creating directory {:?} : {}", d_path, e));
} }
let art_img_def = String::new();
let mut art_image_uri = article
.images
.iter()
.next()
.unwrap_or(&art_img_def)
.to_owned();
if !art_image_uri.is_empty() {
art_image_uri = format!("/assets/images/{}", art_image_uri);
}
let html = format!(
"
<html lang='{}'>
<head>
<meta charset='UTF-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<meta name='author' content='Kuadrado Software' />
<meta name='image' content='{}'/>
<meta name='description' content='{}'>
<link rel='icon' type='image/svg+xml' href='/favicon.svg' />
<title>{}</title>
<link href='/style/style.css' rel='stylesheet' />
</head>
<body>
<div>{}<div>
</body>
<script type='text/javascript' src='/games/view.js'></script>
</html>
",
article.locale, art_image_uri, article.metadata.description, article.title, article.body,
);
let f_path = d_path.join("index.html"); let f_path = d_path.join("index.html");
match File::create(&f_path) { match File::create(&f_path) {
Ok(mut f) => { Ok(mut f) => {
if let Err(e) = f.write_all(html.as_bytes()) { if let Err(e) = f.write_all(html.as_bytes()) {
return Err(format!("Error writing to {:?} : {}", f_path, e)); return Err(format!("Error writing to {:?} : {}", f_path, e));
} }
if let Err(e) = update_sitemap(
app_state,
&article.metadata.view_uri,
UpdateSitemapMode::CreateUrl,
) {
return Err(e);
};
Ok(()) Ok(())
} }
Err(e) => Err(format!("Error creating {:?} : {}", f_path, e)), Err(e) => Err(format!("Error creating {:?} : {}", f_path, e)),
} }
} }
pub fn delete_static_view(path: &Option<PathBuf>) -> Result<(), String> { pub fn delete_static_view(
app_state: &AppState,
path: &Option<PathBuf>,
uri: &Option<String>,
) -> Result<(), String> {
if let Some(path) = path { if let Some(path) = path {
if path.exists() { if path.exists() {
if let Err(e) = remove_file(path) {
return Err(format!("Error deleting static view at {:?} : {}", path, e));
};
let parent = path.parent().unwrap(); let parent = path.parent().unwrap();
if let Err(e) = remove_dir(parent) { if let Err(e) = remove_dir_all(parent) {
return Err(format!( return Err(format!(
"Error deleting static view directory at {:?} : {}", "Error deleting static view at {:?} : {}",
parent, e parent, e
)); ));
} }
} }
} }
if let Err(e) = update_sitemap(app_state, uri, UpdateSitemapMode::DeleteUrl) {
return Err(e);
};
Ok(())
}
fn update_sitemap(
app_state: &AppState,
uri: &Option<String>,
mode: UpdateSitemapMode,
) -> Result<(), String> {
if uri.is_none() {
return Ok(());
}
let sitemap_name = match std::env::var("CONTEXT") {
Ok(value) => {
if value.eq("testing") {
String::from("test_sitemap.xml")
} else {
String::from("sitemap.xml")
}
}
Err(_) => String::from("sitemap.xml"),
};
let standard_dir_pth = app_state.env.public_dir.join("standard");
let uri = uri.as_ref().unwrap().to_owned();
let sitemap =
File::open(standard_dir_pth.join(&sitemap_name)).expect("Couldn't open file sitemap.xml");
let mut urls = Vec::new();
for entity in SiteMapReader::new(sitemap) {
if let SiteMapEntity::Url(url_entry) = entity {
urls.push(url_entry.loc.get_url().unwrap().to_string());
}
}
let updated_sitemap = File::create(standard_dir_pth.join("tmp_sitemap.xml"))
.expect("Couldn't create temporary sitemap");
let writer = SiteMapWriter::new(updated_sitemap);
let mut url_writer = writer
.start_urlset()
.expect("Unable to write sitemap urlset");
match mode {
UpdateSitemapMode::CreateUrl => {
urls.push(uri);
}
UpdateSitemapMode::DeleteUrl => {
let mut updated_urls = Vec::new();
for u in urls {
if !u.eq(&uri) {
updated_urls.push(u);
}
}
urls = updated_urls;
}
}
let now = Utc::today().naive_utc();
for u in urls {
url_writer
.url(
UrlEntry::builder()
.loc(u)
.lastmod(
FixedOffset::west(0)
.ymd(now.year(), now.month(), now.day())
.and_hms(0, 0, 0),
)
.build()
.unwrap(),
)
.expect("Unable to write url");
}
url_writer
.end()
.expect("Unable to write sitemap closing tags");
if let Err(e) = remove_file(standard_dir_pth.join(&sitemap_name)) {
return Err(format!("Error updating sitemap.xml {}", e));
};
if let Err(e) = rename(
standard_dir_pth.join("tmp_sitemap.xml"),
standard_dir_pth.join(&sitemap_name),
) {
return Err(format!("Error updating sitemap.xml {}", e));
};
Ok(()) Ok(())
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment