serve static files via actix_files

This commit is contained in:
Manuel Gugger 2023-01-11 20:30:33 +01:00
parent 1e916d1238
commit f4571721e5
15 changed files with 188 additions and 198 deletions

View File

@ -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::<T, E>))
.route("/delete/{id}", web::delete().to(delete::<T, E>))
.route("/show/{id}", web::get().to(show::<T, E>))
.route("/static_content/{id}/{column_name}", web::get().to(download::<T, E>))
.route("/static_content/{id}/{column_name}", web::delete().to(delete_static_content::<T, E>))
.route("/file/{id}/{column_name}", web::get().to(download::<T, E>))
.route("/file/{id}/{column_name}", web::delete().to(delete_file::<T, E>))
.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 {

View File

@ -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<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
session: Session,
_req: HttpRequest,
data: web::Data<T>,
_text: String,
id: web::Path<i32>
id: web::Path<i32>,
) -> Result<HttpResponse, Error> {
let actix_admin = data.get_actix_admin();
let entity_name = E::get_entity_name();
@ -32,16 +32,25 @@ pub async fn delete<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
(Ok(model), Ok(_)) => {
for field in view_model.fields {
if field.field_type == ActixAdminViewModelFieldType::FileUpload {
let file_name = model.get_value::<String>(&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::<String>(&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<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
@ -61,15 +70,15 @@ pub async fn delete_many<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>
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<i32> = text
.split("&")
.filter(|id| !id.is_empty())
.map(|id_str| id_str.replace("ids=", "").parse::<i32>().unwrap()
).collect();
.map(|id_str| id_str.replace("ids=", "").parse::<i32>().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<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>
(Ok(_), Ok(model)) => {
for field in view_model.fields {
if field.field_type == ActixAdminViewModelFieldType::FileUpload {
let file_name = model.get_value::<String>(&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::<String>(&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()),
}
}
}

View File

@ -41,7 +41,7 @@ pub async fn download<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(re
}
pub async fn delete_static_content<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(session: Session, data: web::Data<T>, params: web::Path<(i32, String)>) -> Result<HttpResponse, Error> {
pub async fn delete_file<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(session: Session, data: web::Data<T>, params: web::Path<(i32, String)>) -> Result<HttpResponse, Error> {
let actix_admin = data.get_actix_admin();
let db = &data.get_db();

View File

@ -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};
mod file;
pub use file::{download, delete_file};

View File

@ -50,6 +50,6 @@ pub async fn show<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(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))
}

12
static/css/default.css Normal file
View File

@ -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
}

68
static/js/default.js Normal file
View File

@ -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",
"<div class=\"notification mb-4 is-light is-danger\"><button class=\"delete\" onclick=\"this.parentElement.remove()\"></button>An Error occurred</div>");
})

View File

@ -12,9 +12,9 @@
aria-label="{{ model_field.field_name }}">{{ model.values | get(key=model_field.field_name, default="") }}</textarea>
{% elif model_field.field_type == "FileUpload" and model.values | get(key=model_field.field_name, default="") != "" %}
<div>
<a hx-disable href="{{ base_path }}/static_content/{{ model.primary_key }}/{{ model_field.field_name }}">{{ model.values |
<a hx-disable href="{{ base_path }}/file/{{ model.primary_key }}/{{ model_field.field_name }}">{{ model.values |
get(key=model_field.field_name, default="") }}</a>
<a class="is-pulled-right" hx-target="closest div" hx-push-url="false" hx-delete="{{ base_path }}/static_content/{{ model.primary_key }}/{{ model_field.field_name }}"
<a class="is-pulled-right" hx-target="closest div" hx-push-url="false" hx-delete="{{ base_path }}/file/{{ model.primary_key }}/{{ model_field.field_name }}"
hx-confirm="Are you sure?"><i class="fa-solid fa-trash"></i></a>
</div>
{% else %}

View File

@ -6,90 +6,5 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css">
<script src="https://unpkg.com/htmx.org@1.8.4"></script>
{% raw %}
<script>
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",
"<div class=\"notification mb-4 is-light is-danger\"><button class=\"delete\" onclick=\"this.parentElement.remove()\"></button>An Error occurred</div>");
})
</script>
<style>
.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
}
</style>
{% endraw %}
<link rel="stylesheet" href="/admin/static/css/default.css">
<script src="/admin/static/js/default.js"></script>

View File

@ -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 %}

View File

@ -117,7 +117,7 @@
{% if model_field.field_type == "Checkbox" %}
<td>{{ entity.values | get(key=model_field.field_name) | get_icon | safe }}</td>
{% elif model_field.field_type == "FileUpload" %}
<td><a href="static_content/{{ entity.primary_key }}/{{ model_field.field_name }}">{{
<td><a href="file/{{ entity.primary_key }}/{{ model_field.field_name }}">{{
entity.values
| get(key=model_field.field_name) }}</a></td>
{% else %}

View File

@ -9,7 +9,7 @@
{% if model_field.field_type == "Checkbox" %}
<td>{{ model.values | get(key=model_field.field_name) | get_icon | safe }}</td>
{% elif model_field.field_type == "FileUpload" %}
<td><a href="static_content/{{ entity.primary_key }}/{{ model_field.field_name }}">{{ entity.values | get(key=model_field.field_name) }}</a></td>
<td><a href="file/{{ view_model.primary_key }}/{{ model_field.field_name }}">{{ model.values | get(key=model_field.field_name) }}</a></td>
{% else %}
<td>{{ model.values | get(key=model_field.field_name) }}</td>
{% endif %}

View File

@ -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::<super::AppState>())
)
.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());
}
}

View File

@ -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::<super::AppState>())
.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::<super::AppState>())
.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());
}
}

View File

@ -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::<super::AppState>())
)
.await
});
);
#[derive(Clone)]
pub struct AppState {
pub db: DatabaseConnection,