From f4571721e5757474b419c2fb74d0eba4ef215f82 Mon Sep 17 00:00:00 2001 From: Manuel Gugger Date: Wed, 11 Jan 2023 20:30:33 +0100 Subject: [PATCH] serve static files via actix_files --- src/builder.rs | 7 +- src/routes/delete.rs | 68 ++++++++++------- src/routes/{static_content.rs => file.rs} | 2 +- src/routes/mod.rs | 4 +- src/routes/show.rs | 2 +- static/css/default.css | 12 +++ static/js/default.js | 68 +++++++++++++++++ templates/form_elements/input.html | 4 +- templates/head.html | 89 +---------------------- templates/index.html | 1 + templates/list.html | 2 +- templates/show.html | 2 +- tests/crud_get_resp_is_success.rs | 22 +----- tests/crud_post_resp_is_success.rs | 82 +++++++-------------- tests/test_setup/helper.rs | 21 ++++++ 15 files changed, 188 insertions(+), 198 deletions(-) rename src/routes/{static_content.rs => file.rs} (94%) create mode 100644 static/css/default.css create mode 100644 static/js/default.js diff --git a/src/builder.rs b/src/builder.rs index a500221..0f943d7 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,4 +1,4 @@ -use crate::{prelude::*, ActixAdminMenuElement, routes::delete_static_content}; +use crate::{prelude::*, ActixAdminMenuElement, routes::delete_file}; use actix_web::{web, Route}; use std::collections::HashMap; use std::fs; @@ -102,8 +102,8 @@ impl ActixAdminBuilderTrait for ActixAdminBuilder { .route("/delete", web::delete().to(delete_many::)) .route("/delete/{id}", web::delete().to(delete::)) .route("/show/{id}", web::get().to(show::)) - .route("/static_content/{id}/{column_name}", web::get().to(download::)) - .route("/static_content/{id}/{column_name}", web::delete().to(delete_static_content::)) + .route("/file/{id}/{column_name}", web::get().to(download::)) + .route("/file/{id}/{column_name}", web::delete().to(delete_file::)) .default_service(web::to(not_found)) ); @@ -232,6 +232,7 @@ impl ActixAdminBuilderTrait for ActixAdminBuilder { }; let mut admin_scope = web::scope("/admin") .route("/", index_handler) + .service(actix_files::Files::new("/static", "./static").show_files_listing()) .default_service(web::to(not_found)); for (_entity, scope) in self.scopes { diff --git a/src/routes/delete.rs b/src/routes/delete.rs index 6fd71de..47c2ba8 100644 --- a/src/routes/delete.rs +++ b/src/routes/delete.rs @@ -1,16 +1,16 @@ -use actix_web::{web, Error, HttpRequest, HttpResponse}; +use super::{render_unauthorized, user_can_access_page}; +use crate::prelude::*; +use actix_session::Session; use actix_web::http::header; -use actix_session::{Session}; -use crate::{prelude::*}; -use tera::{Context}; -use super::{ user_can_access_page, render_unauthorized}; +use actix_web::{web, Error, HttpRequest, HttpResponse}; +use tera::Context; pub async fn delete( session: Session, _req: HttpRequest, data: web::Data, _text: String, - id: web::Path + id: web::Path, ) -> Result { let actix_admin = data.get_actix_admin(); let entity_name = E::get_entity_name(); @@ -32,16 +32,25 @@ pub async fn delete( (Ok(model), Ok(_)) => { for field in view_model.fields { if field.field_type == ActixAdminViewModelFieldType::FileUpload { - let file_name = model.get_value::(&field.field_name, true, true).unwrap_or_default(); - let file_path = format!("{}/{}/{}", actix_admin.configuration.file_upload_directory, E::get_entity_name(), file_name.unwrap_or_default()); - std::fs::remove_file(file_path)?; + let file_name = model + .get_value::(&field.field_name, true, true) + .unwrap_or_default(); + if file_name.is_some() { + let file_path = format!( + "{}/{}/{}", + actix_admin.configuration.file_upload_directory, + E::get_entity_name(), + file_name.unwrap() + ); + std::fs::remove_file(file_path)?; + } } } Ok(HttpResponse::Ok().finish()) - }, - (_,_) => Ok(HttpResponse::InternalServerError().finish()) - } + } + (_, _) => Ok(HttpResponse::InternalServerError().finish()), + } } pub async fn delete_many( @@ -61,15 +70,15 @@ pub async fn delete_many ctx.insert("render_partial", &true); return render_unauthorized(&ctx); } - + let db = &data.get_db(); let entity_name = E::get_entity_name(); let entity_ids: Vec = text .split("&") .filter(|id| !id.is_empty()) - .map(|id_str| id_str.replace("ids=", "").parse::().unwrap() - ).collect(); - + .map(|id_str| id_str.replace("ids=", "").parse::().unwrap()) + .collect(); + // TODO: implement delete_many for id in entity_ids { let model_result = E::get_entity(db, id).await; @@ -79,27 +88,30 @@ pub async fn delete_many (Ok(_), Ok(model)) => { for field in view_model.fields { if field.field_type == ActixAdminViewModelFieldType::FileUpload { - let file_name = model.get_value::(&field.field_name, true, true).unwrap_or_default(); - let file_path = format!("{}/{}/{}", actix_admin.configuration.file_upload_directory, E::get_entity_name(), file_name.unwrap_or_default()); + let file_name = model + .get_value::(&field.field_name, true, true) + .unwrap_or_default(); + let file_path = format!( + "{}/{}/{}", + actix_admin.configuration.file_upload_directory, + E::get_entity_name(), + file_name.unwrap_or_default() + ); std::fs::remove_file(file_path)?; } } - }, - (Ok(_), Err(e)) => errors.push(e) + } + (Ok(_), Err(e)) => errors.push(e), } } match errors.is_empty() { - true => { - Ok(HttpResponse::SeeOther() + true => Ok(HttpResponse::SeeOther() .append_header(( header::LOCATION, format!("/admin/{}/list?render_partial=true", entity_name), )) - .finish()) - }, - false => { - Ok(HttpResponse::InternalServerError().finish()) - } + .finish()), + false => Ok(HttpResponse::InternalServerError().finish()), } -} \ No newline at end of file +} diff --git a/src/routes/static_content.rs b/src/routes/file.rs similarity index 94% rename from src/routes/static_content.rs rename to src/routes/file.rs index ff612ab..d7318df 100644 --- a/src/routes/static_content.rs +++ b/src/routes/file.rs @@ -41,7 +41,7 @@ pub async fn download(re } -pub async fn delete_static_content(session: Session, data: web::Data, params: web::Path<(i32, String)>) -> Result { +pub async fn delete_file(session: Session, data: web::Data, params: web::Path<(i32, String)>) -> Result { let actix_admin = data.get_actix_admin(); let db = &data.get_db(); diff --git a/src/routes/mod.rs b/src/routes/mod.rs index eef2b9c..d560b39 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -19,5 +19,5 @@ pub use delete::{ delete, delete_many }; mod helpers; pub use helpers::{ add_auth_context, user_can_access_page, render_unauthorized }; -mod static_content; -pub use static_content::{download, delete_static_content}; \ No newline at end of file +mod file; +pub use file::{download, delete_file}; \ No newline at end of file diff --git a/src/routes/show.rs b/src/routes/show.rs index 2f8ea87..71b910a 100644 --- a/src/routes/show.rs +++ b/src/routes/show.rs @@ -50,6 +50,6 @@ pub async fn show(sessio let body = TERA .render("show.html", &ctx) - .map_err(|_| error::ErrorInternalServerError("Template error"))?; + .map_err(|err| error::ErrorInternalServerError(format!("{:?}", err)))?; Ok(http_response_code.content_type("text/html").body(body)) } \ No newline at end of file diff --git a/static/css/default.css b/static/css/default.css new file mode 100644 index 0000000..e46f995 --- /dev/null +++ b/static/css/default.css @@ -0,0 +1,12 @@ +.loader-wrapper { + position: absolute; + height: 100%; + width: 100%; + display: flex; + background: rgba(255, 255, 255, 0.3); + justify-content: center; + border-radius: 6px; + align-items: center; + z-index: 6; + pointer-events: none +} \ No newline at end of file diff --git a/static/js/default.js b/static/js/default.js new file mode 100644 index 0000000..3019ba1 --- /dev/null +++ b/static/js/default.js @@ -0,0 +1,68 @@ +document.onkeydown = function (e) { + switch (e.which) { + case 37: // left + let left_el = document.getElementsByClassName('left-arrow-click').item(0); + if (left_el) { left_el.click(); }; + break; + + //case 38: // up + // break; + + case 39: // right + let right_el = document.getElementsByClassName('right-arrow-click').item(0); + if (right_el) { right_el.click(); }; + break; + + //case 40: // down + // break; + + default: return; // exit this handler for other keys + } + e.preventDefault(); // prevent the default action (scroll / move caret) +}; + +function checkAll(bx) { + var cbs = document.getElementsByTagName('input'); + for (var i = 0; i < cbs.length; i++) { + if (cbs[i].type == 'checkbox') { + cbs[i].checked = bx.checked; + } + } +} + +function sort_by(column) { + document.getElementById("sort_by").value = column; + current_sort_order = document.getElementById("sort_order").value; + if (current_sort_order == "{{ sort_order_asc }}") { + document.getElementById("sort_order").value = "{{ sort_order_desc }}"; + } else { + document.getElementById("sort_order").value = "{{ sort_order_asc }}"; + } + document.getElementById('search_form').submit(); +} + +document.addEventListener('DOMContentLoaded', () => { + // Get all "navbar-burger" elements + const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0); + + // Add a click event on each of them + $navbarBurgers.forEach(el => { + el.addEventListener('click', () => { + + // Get the target from the "data-target" attribute + const target = el.dataset.target; + const $target = document.getElementById(target); + + // Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu" + el.classList.toggle('is-active'); + $target.classList.toggle('is-active'); + + }); + }); +}); + +htmx.on("htmx:responseError", function () { + document.getElementById("notifications").insertAdjacentHTML( + "afterend", + "
An Error occurred
"); +}) \ No newline at end of file diff --git a/templates/form_elements/input.html b/templates/form_elements/input.html index 97c2801..3dbe643 100644 --- a/templates/form_elements/input.html +++ b/templates/form_elements/input.html @@ -12,9 +12,9 @@ aria-label="{{ model_field.field_name }}">{{ model.values | get(key=model_field.field_name, default="") }} {% elif model_field.field_type == "FileUpload" and model.values | get(key=model_field.field_name, default="") != "" %} {% else %} diff --git a/templates/head.html b/templates/head.html index 7e470df..f9dbc61 100644 --- a/templates/head.html +++ b/templates/head.html @@ -6,90 +6,5 @@ -{% raw %} - - - -{% endraw %} \ No newline at end of file + + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index e92ac1f..bb5efd0 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,6 +1,7 @@ {% extends "base.html" %} {% block content %} +{% include "loader.html" %} You may customize this site by using a custom index page! {% endblock content %} diff --git a/templates/list.html b/templates/list.html index 1150583..af412c9 100644 --- a/templates/list.html +++ b/templates/list.html @@ -117,7 +117,7 @@ {% if model_field.field_type == "Checkbox" %} {{ entity.values | get(key=model_field.field_name) | get_icon | safe }} {% elif model_field.field_type == "FileUpload" %} - {{ + {{ entity.values | get(key=model_field.field_name) }} {% else %} diff --git a/templates/show.html b/templates/show.html index 05c0e83..4cdfc51 100644 --- a/templates/show.html +++ b/templates/show.html @@ -9,7 +9,7 @@ {% if model_field.field_type == "Checkbox" %} {{ model.values | get(key=model_field.field_name) | get_icon | safe }} {% elif model_field.field_type == "FileUpload" %} - {{ entity.values | get(key=model_field.field_name) }} + {{ model.values | get(key=model_field.field_name) }} {% else %} {{ model.values | get(key=model_field.field_name) }} {% endif %} diff --git a/tests/crud_get_resp_is_success.rs b/tests/crud_get_resp_is_success.rs index b315dc1..c89302e 100644 --- a/tests/crud_get_resp_is_success.rs +++ b/tests/crud_get_resp_is_success.rs @@ -4,10 +4,10 @@ use test_setup::helper::{AppState, create_tables_and_get_connection, create_acti #[cfg(test)] mod tests { extern crate serde_derive; - + use actix_admin::prelude::*; use actix_web::test; use actix_web::{web, App}; - use actix_admin::prelude::*; + use super::create_app; #[actix_web::test] async fn admin_index_get() { @@ -36,27 +36,13 @@ mod tests { async fn test_get_is_success(url: &str) { let db = super::create_tables_and_get_connection().await; - - let actix_admin_builder = super::create_actix_admin_builder(); - let actix_admin = actix_admin_builder.get_actix_admin(); - - let app_state = super::AppState { - actix_admin, - db, - }; - - let app = test::init_service( - App::new() - .app_data(web::Data::new(app_state)) - .service(actix_admin_builder.get_scope::()) - ) - .await; + let app = create_app!(db); let req = test::TestRequest::get() .uri(url) .to_request(); let resp = test::call_service(&app, req).await; - assert_eq!(200, resp.status().as_u16()); + assert!(resp.status().is_success()); } } diff --git a/tests/crud_post_resp_is_success.rs b/tests/crud_post_resp_is_success.rs index eb1bfe6..abd3946 100644 --- a/tests/crud_post_resp_is_success.rs +++ b/tests/crud_post_resp_is_success.rs @@ -3,12 +3,10 @@ use test_setup::helper::{create_actix_admin_builder, create_tables_and_get_conne #[cfg(test)] mod tests { - extern crate serde_derive; - use actix_admin::prelude::*; use actix_web::http::header::ContentType; use actix_web::test; - use actix_web::{middleware, web, App}; + use actix_web::{web, App}; use chrono::NaiveDate; use chrono::NaiveDateTime; use serde::{Serialize}; @@ -16,23 +14,12 @@ mod tests { use sea_orm::PaginatorTrait; use sea_orm::prelude::Decimal; + use crate::create_app; + #[actix_web::test] async fn comment_create_and_edit() { - let conn = super::create_tables_and_get_connection().await; - let actix_admin_builder = super::create_actix_admin_builder(); - let actix_admin = actix_admin_builder.get_actix_admin(); - let app_state = super::AppState { - db: conn, - actix_admin, - }; - - let app = test::init_service( - App::new() - .app_data(web::Data::new(app_state.clone())) - .service(actix_admin_builder.get_scope::()) - .wrap(middleware::Logger::default()), - ) - .await; + let db = super::create_tables_and_get_connection().await; + let app = create_app!(db); #[derive(Serialize, Clone)] pub struct CommentModel { @@ -66,7 +53,7 @@ mod tests { assert!(resp.status().is_redirection()); let entities = super::test_setup::Comment::find() - .paginate(&app_state.db, 50) + .paginate(&db, 50) .fetch_page(0) .await .expect("could not retrieve entities"); @@ -98,39 +85,26 @@ mod tests { assert!(resp.status().is_redirection()); let entities = super::test_setup::Comment::find() - .paginate(&app_state.db, 50) + .paginate(&db, 50) .fetch_page(0) .await .expect("could not retrieve entities"); - assert!(entities.len() == 1, "After edit post, db does not contain 1 model"); + assert_eq!(entities.len(), 1, "After edit post, db does not contain 1 model"); let entity = entities.first().unwrap(); - assert!(entity.id == 1); - assert!(entity.comment == "updated"); - assert!(entity.user == "updated"); + assert_eq!(entity.id, 1); + assert_eq!(entity.comment, "updated"); + assert_eq!(entity.user, "updated"); assert!(!entity.is_visible); assert!(entity.post_id.is_none()); - assert!(entity.my_decimal == Decimal::new(213141, 3)); - assert!(entity.insert_date == NaiveDateTime::parse_from_str("1987-04-01T14:00", "%Y-%m-%dT%H:%M").unwrap()); + assert_eq!(entity.my_decimal, Decimal::new(213141, 3)); + assert_eq!(entity.insert_date, NaiveDateTime::parse_from_str("1987-04-01T14:00", "%Y-%m-%dT%H:%M").unwrap()); } #[actix_web::test] async fn post_create_and_edit() { - let conn = super::create_tables_and_get_connection().await; - let actix_admin_builder = super::create_actix_admin_builder(); - let actix_admin = actix_admin_builder.get_actix_admin(); - let app_state = super::AppState { - db: conn, - actix_admin, - }; - - let app = test::init_service( - App::new() - .app_data(web::Data::new(app_state.clone())) - .service(actix_admin_builder.get_scope::()) - .wrap(middleware::Logger::default()), - ) - .await; + let db = super::create_tables_and_get_connection().await; + let app = create_app!(db); #[derive(Serialize, Clone)] pub struct PostModel { @@ -159,18 +133,18 @@ mod tests { assert!(resp.status().is_redirection()); let entities = super::test_setup::Post::find() - .paginate(&app_state.db, 50) + .paginate(&db, 50) .fetch_page(0) .await .expect("could not retrieve entities"); - assert!(entities.len() == 1, "After post, db does not contain 1 model"); + assert_eq!(entities.len(), 1, "After post, db does not contain 1 model"); let entity = entities.first().unwrap(); - assert!(entity.id == 1); - assert!(entity.tea_mandatory == super::test_setup::post::Tea::EverydayTea); - assert!(entity.title == model.title); - assert!(entity.text == model.text); - assert!(entity.insert_date == NaiveDate::parse_from_str("1977-04-01", "%Y-%m-%d").unwrap()); + assert_eq!(entity.id, 1); + assert_eq!(entity.tea_mandatory, super::test_setup::post::Tea::EverydayTea); + assert_eq!(entity.title, model.title); + assert_eq!(entity.text, model.text); + assert_eq!(entity.insert_date, NaiveDate::parse_from_str("1977-04-01", "%Y-%m-%d").unwrap()); // update entity model.tea_mandatory = "BreakfastTea"; @@ -188,16 +162,16 @@ mod tests { assert!(resp.status().is_redirection()); let entities = super::test_setup::Post::find() - .paginate(&app_state.db, 50) + .paginate(&db, 50) .fetch_page(0) .await .expect("could not retrieve entities"); - assert!(entities.len() == 1, "After edit post, db does not contain 1 model"); + assert_eq!(entities.len(), 1, "After edit post, db does not contain 1 model"); let entity = entities.first().unwrap(); - assert!(entity.id == 1); - assert!(entity.text == "updated"); - assert!(entity.title == "updated"); - assert!(entity.insert_date == NaiveDate::parse_from_str("1987-04-01", "%Y-%m-%d").unwrap()); + assert_eq!(entity.id, 1); + assert_eq!(entity.text, "updated"); + assert_eq!(entity.title, "updated"); + assert_eq!(entity.insert_date, NaiveDate::parse_from_str("1987-04-01", "%Y-%m-%d").unwrap()); } } diff --git a/tests/test_setup/helper.rs b/tests/test_setup/helper.rs index aafa8b7..fc9a057 100644 --- a/tests/test_setup/helper.rs +++ b/tests/test_setup/helper.rs @@ -16,6 +16,27 @@ pub async fn create_tables_and_get_connection() -> DatabaseConnection { conn } +#[macro_export] +macro_rules! create_app ( + ($db: expr) => ({ + let conn = $db.clone(); + let actix_admin_builder = super::create_actix_admin_builder(); + let actix_admin = actix_admin_builder.get_actix_admin(); + let app_state = super::AppState { + db: conn, + actix_admin, + }; + + test::init_service( + App::new() + .app_data(web::Data::new(app_state.clone())) + .service(actix_admin_builder.get_scope::()) + ) + .await + }); +); + + #[derive(Clone)] pub struct AppState { pub db: DatabaseConnection,