implement error handling through notifications

This commit is contained in:
Manuel Gugger 2022-10-07 21:32:59 +02:00
parent 5f515c96eb
commit 859c139f8b
18 changed files with 324 additions and 115 deletions

View File

@ -27,6 +27,7 @@ serde = "1.0.136"
serde_derive = "1.0.136" serde_derive = "1.0.136"
sea-orm = { version = "^0.9.1", features = [], default-features = false } sea-orm = { version = "^0.9.1", features = [], default-features = false }
actix-admin-macros = { version = "0.1.0", path = "actix_admin_macros" } actix-admin-macros = { version = "0.1.0", path = "actix_admin_macros" }
derive_more = "0.99.17"
[dev-dependencies] [dev-dependencies]
sea-orm = { version = "^0.9.1", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = true } sea-orm = { version = "^0.9.1", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = true }

View File

@ -67,7 +67,7 @@ pub fn derive_actix_admin_view_model(input: proc_macro::TokenStream) -> proc_mac
#[async_trait(?Send)] #[async_trait(?Send)]
impl ActixAdminViewModelTrait for Entity { impl ActixAdminViewModelTrait for Entity {
async fn list(db: &DatabaseConnection, page: usize, entities_per_page: usize, search: &String) -> (usize, Vec<ActixAdminModel>) { async fn list(db: &DatabaseConnection, page: usize, entities_per_page: usize, search: &String) -> Result<(usize, Vec<ActixAdminModel>), ActixAdminError> {
let entities = Entity::list_model(db, page, entities_per_page, search).await; let entities = Entity::list_model(db, page, entities_per_page, search).await;
entities entities
} }
@ -82,43 +82,51 @@ pub fn derive_actix_admin_view_model(input: proc_macro::TokenStream) -> proc_mac
} }
} }
async fn create_entity(db: &DatabaseConnection, mut model: ActixAdminModel) -> ActixAdminModel { async fn create_entity(db: &DatabaseConnection, mut model: ActixAdminModel) -> Result<ActixAdminModel, ActixAdminError> {
let new_model = ActiveModel::from(model.clone()); let new_model = ActiveModel::from(model.clone());
let insert_operation = Entity::insert(new_model).exec(db).await; let insert_operation = Entity::insert(new_model).exec(db).await?;
model model.primary_key = Some(insert_operation.last_insert_id.to_string());
Ok(model)
} }
async fn get_entity(db: &DatabaseConnection, id: i32) -> ActixAdminModel { async fn get_entity(db: &DatabaseConnection, id: i32) -> Result<ActixAdminModel, ActixAdminError> {
// TODO: separate primary key from other keys // TODO: separate primary key from other keys
let entity = Entity::find_by_id(id).one(db).await.unwrap().unwrap(); let entity = Entity::find_by_id(id).one(db).await?;
let model = ActixAdminModel::from(entity); match entity {
model Some(e) => Ok(ActixAdminModel::from(e)),
_ => Err(ActixAdminError::EntityDoesNotExistError)
}
} }
async fn edit_entity(db: &DatabaseConnection, id: i32, mut model: ActixAdminModel) -> ActixAdminModel { async fn edit_entity(db: &DatabaseConnection, id: i32, mut model: ActixAdminModel) -> Result<ActixAdminModel, ActixAdminError> {
let entity: Option<Model> = Entity::find_by_id(id).one(db).await.unwrap(); let entity: Option<Model> = Entity::find_by_id(id).one(db).await?;
let mut entity: ActiveModel = entity.unwrap().into();
match entity {
Some(e) => {
let mut entity: ActiveModel = e.into();
#(#fields_for_edit_model);*; #(#fields_for_edit_model);*;
let entity: Model = entity.update(db).await.unwrap(); let entity: Model = entity.update(db).await?;
Ok(model)
model },
_ => Err(ActixAdminError::EntityDoesNotExistError)
}
} }
async fn delete_entity(db: &DatabaseConnection, id: i32) -> bool { async fn delete_entity(db: &DatabaseConnection, id: i32) -> Result<bool, ActixAdminError> {
let result = Entity::delete_by_id(id).exec(db).await; let result = Entity::delete_by_id(id).exec(db).await;
match result { match result {
Ok(_) => true, Ok(_) => Ok(true),
Err(_) => false Err(_) => Err(ActixAdminError::DeleteError)
} }
} }
async fn get_select_lists(db: &DatabaseConnection) -> HashMap<String, Vec<(String, String)>> { async fn get_select_lists(db: &DatabaseConnection) -> Result<HashMap<String, Vec<(String, String)>>, ActixAdminError> {
hashmap![ Ok(hashmap![
#(#select_lists),* #(#select_lists),*
] ])
} }
fn get_entity_name() -> String { fn get_entity_name() -> String {
@ -173,7 +181,7 @@ pub fn derive_actix_admin_model(input: proc_macro::TokenStream) -> proc_macro::T
#[async_trait] #[async_trait]
impl ActixAdminModelTrait for Entity { impl ActixAdminModelTrait for Entity {
async fn list_model(db: &DatabaseConnection, page: usize, posts_per_page: usize, search: &String) -> (usize, Vec<ActixAdminModel>) { async fn list_model(db: &DatabaseConnection, page: usize, posts_per_page: usize, search: &String) -> Result<(usize, Vec<ActixAdminModel>), ActixAdminError> {
use sea_orm::{ query::* }; use sea_orm::{ query::* };
let paginator = Entity::find() let paginator = Entity::find()
.filter( .filter(
@ -182,11 +190,10 @@ pub fn derive_actix_admin_model(input: proc_macro::TokenStream) -> proc_macro::T
) )
.order_by_asc(Column::Id) .order_by_asc(Column::Id)
.paginate(db, posts_per_page); .paginate(db, posts_per_page);
let num_pages = paginator.num_pages().await.ok().unwrap(); let num_pages = paginator.num_pages().await?;
let entities = paginator let entities = paginator
.fetch_page(page - 1) .fetch_page(page - 1)
.await .await?;
.expect("could not retrieve entities");
let mut model_entities = Vec::new(); let mut model_entities = Vec::new();
for entity in entities { for entity in entities {
model_entities.push( model_entities.push(
@ -194,7 +201,7 @@ pub fn derive_actix_admin_model(input: proc_macro::TokenStream) -> proc_macro::T
); );
} }
(num_pages, model_entities) Ok((num_pages, model_entities))
} }
fn validate_model(model: &mut ActixAdminModel) { fn validate_model(model: &mut ActixAdminModel) {

View File

@ -12,15 +12,15 @@ pub fn get_select_list_from_model(_input: proc_macro::TokenStream) -> proc_macro
let expanded = quote! { let expanded = quote! {
#[async_trait] #[async_trait]
impl ActixAdminSelectListTrait for Entity { impl ActixAdminSelectListTrait for Entity {
async fn get_key_value(db: &DatabaseConnection) -> Vec<(String, String)> { async fn get_key_value(db: &DatabaseConnection) -> Result<Vec<(String, String)>, ActixAdminError> {
let entities = Entity::find().order_by_asc(Column::Id).all(db).await; let entities = Entity::find().order_by_asc(Column::Id).all(db).await?;
let mut key_value = Vec::new(); let mut key_value = Vec::new();
for entity in entities.unwrap() { for entity in entities {
key_value.push((entity.id.to_string(), entity.to_string())); key_value.push((entity.id.to_string(), entity.to_string()));
}; };
key_value.sort_by(|a, b| a.1.cmp(&b.1)); key_value.sort_by(|a, b| a.1.cmp(&b.1));
key_value Ok(key_value)
} }
} }
}; };
@ -35,12 +35,12 @@ pub fn get_select_list_from_enum(input: proc_macro::TokenStream) -> proc_macro::
let expanded = quote! { let expanded = quote! {
#[async_trait] #[async_trait]
impl ActixAdminSelectListTrait for #ty { impl ActixAdminSelectListTrait for #ty {
async fn get_key_value(db: &DatabaseConnection) -> Vec<(String, String)> { async fn get_key_value(db: &DatabaseConnection) -> Result<Vec<(String, String)>, ActixAdminError> {
let mut fields = Vec::new(); let mut fields = Vec::new();
for field in #ty::iter() { for field in #ty::iter() {
fields.push((field.to_string(), field.to_string())); fields.push((field.to_string(), field.to_string()));
} }
fields Ok(fields)
} }
} }
}; };
@ -56,7 +56,7 @@ pub fn get_select_lists(fields: &Vec<ModelField>) -> Vec<proc_macro2::TokenStrea
let ident_name = model_field.ident.to_string(); let ident_name = model_field.ident.to_string();
let select_list_ident = Ident::new(&(model_field.select_list), Span::call_site()); let select_list_ident = Ident::new(&(model_field.select_list), Span::call_site());
quote! { quote! {
#ident_name => #select_list_ident::get_key_value(db).await #ident_name => #select_list_ident::get_key_value(db).await?
} }
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()

View File

@ -122,6 +122,12 @@ use tera::{Tera, Result, to_value, try_get_value };
use std::{ hash::BuildHasher}; use std::{ hash::BuildHasher};
use actix_session::{Session}; use actix_session::{Session};
use async_trait::async_trait; use async_trait::async_trait;
use derive_more::{Display, Error};
use actix_web::{
error,
http::{header::ContentType, StatusCode},
HttpResponse,
};
pub mod view_model; pub mod view_model;
pub mod model; pub mod model;
@ -133,7 +139,7 @@ pub mod prelude {
pub use crate::model::{ ActixAdminModel, ActixAdminModelValidationTrait, ActixAdminModelTrait}; pub use crate::model::{ ActixAdminModel, ActixAdminModelValidationTrait, ActixAdminModelTrait};
pub use crate::view_model::{ ActixAdminViewModel, ActixAdminViewModelTrait, ActixAdminViewModelField, ActixAdminViewModelSerializable, ActixAdminViewModelFieldType }; pub use crate::view_model::{ ActixAdminViewModel, ActixAdminViewModelTrait, ActixAdminViewModelField, ActixAdminViewModelSerializable, ActixAdminViewModelFieldType };
pub use actix_admin_macros::{ DeriveActixAdmin, DeriveActixAdminModel, DeriveActixAdminViewModel, DeriveActixAdminEnumSelectList, DeriveActixAdminModelSelectList }; pub use actix_admin_macros::{ DeriveActixAdmin, DeriveActixAdminModel, DeriveActixAdminViewModel, DeriveActixAdminEnumSelectList, DeriveActixAdminModelSelectList };
pub use crate::{ ActixAdminAppDataTrait, ActixAdmin, ActixAdminConfiguration }; pub use crate::{ ActixAdminError, ActixAdminAppDataTrait, ActixAdmin, ActixAdminConfiguration };
pub use crate::{ hashmap, ActixAdminSelectListTrait }; pub use crate::{ hashmap, ActixAdminSelectListTrait };
pub use crate::routes::{ create_or_edit_post, get_admin_ctx }; pub use crate::routes::{ create_or_edit_post, get_admin_ctx };
pub use crate::{ TERA }; pub use crate::{ TERA };
@ -211,7 +217,7 @@ pub trait ActixAdminAppDataTrait {
// SelectListTrait // SelectListTrait
#[async_trait] #[async_trait]
pub trait ActixAdminSelectListTrait { pub trait ActixAdminSelectListTrait {
async fn get_key_value(db: &DatabaseConnection) -> Vec<(String, String)>; async fn get_key_value(db: &DatabaseConnection) -> core::result::Result<Vec<(String, String)>, ActixAdminError>;
} }
@ -236,3 +242,75 @@ pub struct ActixAdminMenuElement {
pub link: String, pub link: String,
pub is_custom_handler: bool pub is_custom_handler: bool
} }
// Errors
#[derive(Debug, Display, Error)]
pub enum ActixAdminError {
#[display(fmt = "Internal error")]
InternalError,
#[display(fmt = "Form has validation errors")]
ValidationErrors,
#[display(fmt = "Could not list entities")]
ListError,
#[display(fmt = "Could not create entity")]
CreateError,
#[display(fmt = "Could not delete entity")]
DeleteError,
#[display(fmt = "Could not edit entity")]
EditError,
#[display(fmt = "Database error")]
DatabaseError,
#[display(fmt = "Entity does not exist")]
EntityDoesNotExistError
}
impl error::ResponseError for ActixAdminError {
fn error_response(&self) -> HttpResponse {
HttpResponse::build(self.status_code())
.insert_header(ContentType::html())
.body(self.to_string())
}
fn status_code(&self) -> StatusCode {
match *self {
_ => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
impl std::convert::From<sea_orm::DbErr> for ActixAdminError {
fn from(_err: sea_orm::DbErr) -> ActixAdminError {
ActixAdminError::DatabaseError
}
}
// Notifications
#[derive(Debug, Display, Serialize)]
pub enum ActixAdminNotificationType {
#[display(fmt = "is-danger")]
Danger,
}
#[derive(Debug, Serialize)]
pub struct ActixAdminNotification {
css_class: String,
message: String
}
impl std::convert::From<ActixAdminError> for ActixAdminNotification {
fn from(e: ActixAdminError) -> ActixAdminNotification {
ActixAdminNotification {
css_class: ActixAdminNotificationType::Danger.to_string(),
message: e.to_string()
}
}
}

View File

@ -1,4 +1,4 @@
use crate::ActixAdminViewModelField; use crate::{ ActixAdminViewModelField, ActixAdminError};
use async_trait::async_trait; use async_trait::async_trait;
use sea_orm::DatabaseConnection; use sea_orm::DatabaseConnection;
use serde::Serialize; use serde::Serialize;
@ -14,7 +14,7 @@ pub trait ActixAdminModelTrait {
page: usize, page: usize,
posts_per_page: usize, posts_per_page: usize,
search: &String search: &String
) -> (usize, Vec<ActixAdminModel>); ) -> Result<(usize, Vec<ActixAdminModel>), ActixAdminError>;
fn get_fields() -> Vec<ActixAdminViewModelField>; fn get_fields() -> Vec<ActixAdminViewModelField>;
fn validate_model(model: &mut ActixAdminModel); fn validate_model(model: &mut ActixAdminModel);
} }

View File

@ -1,6 +1,8 @@
use actix_web::{error, web, Error, HttpRequest, HttpResponse}; use actix_web::{error, web, Error, HttpRequest, HttpResponse};
use tera::{Context}; use tera::{Context};
use actix_session::{Session}; use actix_session::{Session};
use crate::ActixAdminError;
use crate::ActixAdminNotification;
use crate::prelude::*; use crate::prelude::*;
use crate::TERA; use crate::TERA;
@ -16,7 +18,7 @@ pub async fn create_get<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
let db = &data.get_db(); let db = &data.get_db();
let model = ActixAdminModel::create_empty(); let model = ActixAdminModel::create_empty();
create_or_edit_get::<T, E>(&session, &data, db, model).await create_or_edit_get::<T, E>(&session, &data, db, Ok(model)).await
} }
pub async fn edit_get<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>( pub async fn edit_get<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
@ -32,13 +34,14 @@ pub async fn edit_get<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
create_or_edit_get::<T, E>(&session, &data, db, model).await create_or_edit_get::<T, E>(&session, &data, db, model).await
} }
async fn create_or_edit_get<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(session: &Session, data: &web::Data<T>, db: &sea_orm::DatabaseConnection, model: ActixAdminModel) -> Result<HttpResponse, Error>{ async fn create_or_edit_get<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(session: &Session, data: &web::Data<T>, db: &sea_orm::DatabaseConnection, model_result: Result<ActixAdminModel, ActixAdminError>) -> Result<HttpResponse, Error>{
let actix_admin = &data.get_actix_admin(); let actix_admin = &data.get_actix_admin();
let mut ctx = Context::new(); let mut ctx = Context::new();
add_auth_context(&session, actix_admin, &mut ctx); add_auth_context(&session, actix_admin, &mut ctx);
let entity_names = &actix_admin.entity_names; let entity_names = &actix_admin.entity_names;
ctx.insert("entity_names", entity_names); ctx.insert("entity_names", entity_names);
let entity_name = E::get_entity_name(); let entity_name = E::get_entity_name();
let mut errors: Vec<crate::ActixAdminError> = Vec::new();
let view_model = actix_admin.view_models.get(&entity_name).unwrap(); let view_model = actix_admin.view_models.get(&entity_name).unwrap();
@ -46,13 +49,33 @@ async fn create_or_edit_get<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTra
return render_unauthorized(&ctx); return render_unauthorized(&ctx);
} }
let model;
match model_result {
Ok(res) => {
model = res;
},
Err(e) => {
errors.push(e);
model = ActixAdminModel::create_empty();
}
}
let mut http_response_code = match errors.is_empty() {
true => HttpResponse::Ok(),
false => HttpResponse::InternalServerError(),
};
let notifications: Vec<ActixAdminNotification> = errors.into_iter()
.map(|err| ActixAdminNotification::from(err))
.collect();
ctx.insert("view_model", &ActixAdminViewModelSerializable::from(view_model.clone())); ctx.insert("view_model", &ActixAdminViewModelSerializable::from(view_model.clone()));
ctx.insert("select_lists", &E::get_select_lists(db).await); ctx.insert("select_lists", &E::get_select_lists(db).await?);
ctx.insert("list_link", &E::get_list_link(&entity_name)); ctx.insert("list_link", &E::get_list_link(&entity_name));
ctx.insert("model", &model); ctx.insert("model", &model);
ctx.insert("notifications", &notifications);
let body = TERA let body = TERA
.render("create_or_edit.html", &ctx) .render("create_or_edit.html", &ctx)
.map_err(|err| error::ErrorInternalServerError(err))?; .map_err(|err| error::ErrorInternalServerError(err))?;
Ok(HttpResponse::Ok().content_type("text/html").body(body)) Ok(http_response_code.content_type("text/html").body(body))
} }

View File

@ -1,6 +1,9 @@
use super::{render_unauthorized, user_can_access_page}; use super::{render_unauthorized, user_can_access_page};
use crate::ActixAdminError;
use crate::ActixAdminNotification;
use crate::prelude::*; use crate::prelude::*;
use crate::TERA; use crate::TERA;
use actix_multipart::MultipartError;
use actix_session::Session; use actix_session::Session;
use actix_web::http::header; use actix_web::http::header;
use actix_web::{error, web, Error, HttpResponse}; use actix_web::{error, web, Error, HttpResponse};
@ -13,7 +16,7 @@ pub async fn create_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>
data: web::Data<T>, data: web::Data<T>,
payload: Multipart, payload: Multipart,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let model = ActixAdminModel::create_from_payload(payload).await.unwrap(); let model = ActixAdminModel::create_from_payload(payload).await;
create_or_edit_post::<T, E>(&session, &data, model, None).await create_or_edit_post::<T, E>(&session, &data, model, None).await
} }
@ -23,20 +26,21 @@ pub async fn edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
payload: Multipart, payload: Multipart,
id: web::Path<i32>, id: web::Path<i32>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let model = ActixAdminModel::create_from_payload(payload).await.unwrap(); let model = ActixAdminModel::create_from_payload(payload).await;
create_or_edit_post::<T, E>(&session, &data, model, Some(id.into_inner())).await create_or_edit_post::<T, E>(&session, &data, model, Some(id.into_inner())).await
} }
pub async fn create_or_edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>( pub async fn create_or_edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
session: &Session, session: &Session,
data: &web::Data<T>, data: &web::Data<T>,
mut model: ActixAdminModel, model_res: Result<ActixAdminModel, MultipartError>,
id: Option<i32>, id: Option<i32>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let actix_admin = data.get_actix_admin(); let actix_admin = data.get_actix_admin();
let entity_name = E::get_entity_name(); let entity_name = E::get_entity_name();
let view_model = actix_admin.view_models.get(&entity_name).unwrap(); let view_model = actix_admin.view_models.get(&entity_name).unwrap();
let mut errors: Vec<ActixAdminError> = Vec::new();
if !user_can_access_page(&session, actix_admin, view_model) { if !user_can_access_page(&session, actix_admin, view_model) {
let mut ctx = Context::new(); let mut ctx = Context::new();
@ -44,36 +48,56 @@ pub async fn create_or_edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewMod
return render_unauthorized(&ctx); return render_unauthorized(&ctx);
} }
let db = &data.get_db(); let db = &data.get_db();
let mut model = model_res.unwrap();
E::validate_entity(&mut model); E::validate_entity(&mut model);
if model.has_errors() { if model.has_errors() {
let mut ctx = Context::new(); errors.push(ActixAdminError::ValidationErrors);
ctx.insert("entity_names", &actix_admin.entity_names); render_form::<E>(actix_admin, view_model, db, entity_name, &model, errors).await
ctx.insert(
"view_model",
&ActixAdminViewModelSerializable::from(view_model.clone()),
);
ctx.insert("select_lists", &E::get_select_lists(db).await);
ctx.insert("list_link", &E::get_list_link(&entity_name));
ctx.insert("model", &model);
let body = TERA
.render("create_or_edit.html", &ctx)
.map_err(|err| error::ErrorInternalServerError(err))?;
Ok(HttpResponse::Ok().content_type("text/html").body(body))
} else { } else {
match id { let res = match id {
Some(id) => E::edit_entity(db, id, model).await, Some(id) => E::edit_entity(db, id, model.clone()).await,
None => E::create_entity(db, model).await, None => E::create_entity(db, model.clone()).await,
}; };
match res {
Ok(_) => {
Ok(HttpResponse::SeeOther() Ok(HttpResponse::SeeOther()
.append_header(( .append_header((
header::LOCATION, header::LOCATION,
format!("/admin/{}/list", view_model.entity_name), format!("/admin/{}/list", view_model.entity_name),
)) ))
.finish()) .finish())
},
Err(e) => {
errors.push(e);
render_form::<E>(actix_admin, view_model, db, entity_name, &model, errors).await
} }
}
}
}
async fn render_form<E: ActixAdminViewModelTrait>(actix_admin: &ActixAdmin, view_model: &ActixAdminViewModel, db: &&sea_orm::DatabaseConnection, entity_name: String, model: &ActixAdminModel, errors: Vec<ActixAdminError>) -> Result<HttpResponse, Error> {
let mut ctx = Context::new();
ctx.insert("entity_names", &actix_admin.entity_names);
ctx.insert(
"view_model",
&ActixAdminViewModelSerializable::from(view_model.clone()),
);
ctx.insert("select_lists", &E::get_select_lists(db).await?);
ctx.insert("list_link", &E::get_list_link(&entity_name));
ctx.insert("model", model);
let notifications: Vec<ActixAdminNotification> = errors.into_iter()
.map(|err| ActixAdminNotification::from(err))
.collect();
ctx.insert("notifications", &notifications);
let body = TERA
.render("create_or_edit.html", &ctx)
.map_err(|err| error::ErrorInternalServerError(err))?;
Ok(HttpResponse::Ok().content_type("text/html").body(body))
} }
#[doc(hidden)] #[doc(hidden)]

View File

@ -24,10 +24,12 @@ pub async fn delete<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
} }
let db = &data.get_db(); let db = &data.get_db();
let _result = E::delete_entity(db, id.into_inner()).await; let result = E::delete_entity(db, id.into_inner()).await;
Ok(HttpResponse::Ok() match result {
.finish()) Ok(_) => Ok(HttpResponse::Ok().finish()),
Err(_) => Ok(HttpResponse::InternalServerError().finish())
}
} }
pub async fn delete_many<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>( pub async fn delete_many<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
@ -40,6 +42,7 @@ pub async fn delete_many<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>
let entity_name = E::get_entity_name(); let entity_name = E::get_entity_name();
let view_model = actix_admin.view_models.get(&entity_name).unwrap(); let view_model = actix_admin.view_models.get(&entity_name).unwrap();
let mut errors: Vec<crate::ActixAdminError> = Vec::new();
if !user_can_access_page(&session, actix_admin, view_model) { if !user_can_access_page(&session, actix_admin, view_model) {
let mut ctx = Context::new(); let mut ctx = Context::new();
@ -57,13 +60,24 @@ pub async fn delete_many<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>
// TODO: implement delete_many // TODO: implement delete_many
for id in entity_ids { for id in entity_ids {
let _result = E::delete_entity(db, id).await; let result = E::delete_entity(db, id).await;
match result {
Err(e) => errors.push(e),
_ => {}
}
} }
match errors.is_empty() {
true => {
Ok(HttpResponse::SeeOther() Ok(HttpResponse::SeeOther()
.append_header(( .append_header((
header::LOCATION, header::LOCATION,
format!("/admin/{}/list?render_partial=true", entity_name), format!("/admin/{}/list?render_partial=true", entity_name),
)) ))
.finish()) .finish())
},
false => {
Ok(HttpResponse::InternalServerError().finish())
}
}
} }

View File

@ -21,9 +21,11 @@ pub fn get_admin_ctx<T: ActixAdminAppDataTrait>(session: Session, data: &web::Da
pub async fn index<T: ActixAdminAppDataTrait>(session: Session, data: web::Data<T>) -> Result<HttpResponse, Error> { pub async fn index<T: ActixAdminAppDataTrait>(session: Session, data: web::Data<T>) -> Result<HttpResponse, Error> {
let actix_admin = data.get_actix_admin(); let actix_admin = data.get_actix_admin();
let notifications: Vec<crate::ActixAdminNotification> = Vec::new();
let mut ctx = Context::new(); let mut ctx = Context::new();
ctx.insert("entity_names", &actix_admin.entity_names); ctx.insert("entity_names", &actix_admin.entity_names);
ctx.insert("notifications", &notifications);
add_auth_context(&session, actix_admin, &mut ctx); add_auth_context(&session, actix_admin, &mut ctx);

View File

@ -6,6 +6,7 @@ use crate::prelude::*;
use crate::ActixAdminViewModelTrait; use crate::ActixAdminViewModelTrait;
use crate::ActixAdminViewModel; use crate::ActixAdminViewModel;
use crate::ActixAdminModel; use crate::ActixAdminModel;
use crate::ActixAdminNotification;
use crate::TERA; use crate::TERA;
use actix_session::{Session}; use actix_session::{Session};
use super::{ add_auth_context, user_can_access_page, render_unauthorized}; use super::{ add_auth_context, user_can_access_page, render_unauthorized};
@ -28,6 +29,7 @@ pub async fn list<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
let actix_admin = data.get_actix_admin(); let actix_admin = data.get_actix_admin();
let entity_name = E::get_entity_name(); let entity_name = E::get_entity_name();
let view_model: &ActixAdminViewModel = actix_admin.view_models.get(&entity_name).unwrap(); let view_model: &ActixAdminViewModel = actix_admin.view_models.get(&entity_name).unwrap();
let mut errors: Vec<ActixAdminError> = Vec::new();
let mut ctx = Context::new(); let mut ctx = Context::new();
add_auth_context(&session, actix_admin, &mut ctx); add_auth_context(&session, actix_admin, &mut ctx);
@ -48,22 +50,40 @@ pub async fn list<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
let search = params.search.clone().unwrap_or(String::new()); let search = params.search.clone().unwrap_or(String::new());
let db = data.get_db(); let db = data.get_db();
let result: (usize, Vec<ActixAdminModel>) = E::list(db, page, entities_per_page, &search).await; let result = E::list(db, page, entities_per_page, &search).await;
let entities = result.1; match result {
let num_pages = result.0; Ok(res) => {
let entities = res.1;
let num_pages = res.0;
ctx.insert("entities", &entities);
ctx.insert("num_pages", &num_pages);
},
Err(e) => {
ctx.insert("entities", &Vec::<ActixAdminModel>::new());
ctx.insert("num_pages", &0);
errors.push(e);
}
}
let mut http_response_code = match errors.is_empty() {
false => HttpResponse::InternalServerError(),
true => HttpResponse::Ok()
};
let notifications: Vec<ActixAdminNotification> = errors.into_iter()
.map(|err| ActixAdminNotification::from(err))
.collect();
ctx.insert("entity_name", &entity_name); ctx.insert("entity_name", &entity_name);
ctx.insert("entities", &entities); ctx.insert("notifications", &notifications);
ctx.insert("page", &page); ctx.insert("page", &page);
ctx.insert("params", &entities_per_page); ctx.insert("params", &entities_per_page);
ctx.insert("entities_per_page", &entities_per_page); ctx.insert("entities_per_page", &entities_per_page);
ctx.insert("render_partial", &render_partial); ctx.insert("render_partial", &render_partial);
ctx.insert("num_pages", &num_pages);
ctx.insert("view_model", &ActixAdminViewModelSerializable::from(view_model.clone())); ctx.insert("view_model", &ActixAdminViewModelSerializable::from(view_model.clone()));
ctx.insert("search", &search); ctx.insert("search", &search);
let body = TERA let body = TERA
.render("list.html", &ctx) .render("list.html", &ctx)
.map_err(|err| error::ErrorInternalServerError(err))?; .map_err(|err| error::ErrorInternalServerError(err))?;
Ok(HttpResponse::Ok().content_type("text/html").body(body)) Ok(http_response_code.content_type("text/html").body(body))
} }

View File

@ -2,6 +2,7 @@ use actix_web::{error, web, Error, HttpResponse};
use actix_session::{Session}; use actix_session::{Session};
use tera::{Context}; use tera::{Context};
use crate::ActixAdminNotification;
use crate::prelude::*; use crate::prelude::*;
use crate::TERA; use crate::TERA;
@ -11,20 +12,43 @@ use super::{ add_auth_context };
pub async fn show<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(session: Session, data: web::Data<T>, id: web::Path<i32>) -> Result<HttpResponse, Error> { pub async fn show<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(session: Session, data: web::Data<T>, id: web::Path<i32>) -> Result<HttpResponse, Error> {
let actix_admin = data.get_actix_admin(); let actix_admin = data.get_actix_admin();
let db = &data.get_db(); let db = &data.get_db();
let model = E::get_entity(db, id.into_inner()).await; let result = E::get_entity(db, id.into_inner()).await;
let mut errors: Vec<crate::ActixAdminError> = Vec::new();
let model;
match result {
Ok(res) => {
model = res;
},
Err(e) => {
errors.push(e);
model = ActixAdminModel::create_empty();
}
}
let entity_name = E::get_entity_name(); let entity_name = E::get_entity_name();
let view_model: &ActixAdminViewModel = actix_admin.view_models.get(&entity_name).unwrap(); let view_model: &ActixAdminViewModel = actix_admin.view_models.get(&entity_name).unwrap();
let mut http_response_code = match errors.is_empty() {
false => HttpResponse::InternalServerError(),
true => HttpResponse::Ok()
};
let notifications: Vec<ActixAdminNotification> = errors.into_iter()
.map(|err| ActixAdminNotification::from(err))
.collect();
let mut ctx = Context::new(); let mut ctx = Context::new();
ctx.insert("model", &model); ctx.insert("model", &model);
ctx.insert("view_model", &ActixAdminViewModelSerializable::from(view_model.clone())); ctx.insert("view_model", &ActixAdminViewModelSerializable::from(view_model.clone()));
ctx.insert("list_link", &E::get_list_link(&entity_name)); ctx.insert("list_link", &E::get_list_link(&entity_name));
ctx.insert("entity_names", &actix_admin.entity_names); ctx.insert("entity_names", &actix_admin.entity_names);
ctx.insert("notifications", &notifications);
add_auth_context(&session, actix_admin, &mut ctx); add_auth_context(&session, actix_admin, &mut ctx);
let body = TERA let body = TERA
.render("show.html", &ctx) .render("show.html", &ctx)
.map_err(|_| error::ErrorInternalServerError("Template error"))?; .map_err(|_| error::ErrorInternalServerError("Template error"))?;
Ok(HttpResponse::Ok().content_type("text/html").body(body)) Ok(http_response_code.content_type("text/html").body(body))
} }

View File

@ -5,6 +5,7 @@ use std::collections::HashMap;
use crate::ActixAdminModel; use crate::ActixAdminModel;
use actix_session::{Session}; use actix_session::{Session};
use std::convert::From; use std::convert::From;
use crate::ActixAdminError;
#[async_trait(?Send)] #[async_trait(?Send)]
pub trait ActixAdminViewModelTrait { pub trait ActixAdminViewModelTrait {
@ -13,14 +14,14 @@ pub trait ActixAdminViewModelTrait {
page: usize, page: usize,
entities_per_page: usize, entities_per_page: usize,
search: &String search: &String
) -> (usize, Vec<ActixAdminModel>); ) -> Result<(usize, Vec<ActixAdminModel>), ActixAdminError>;
// TODO: Replace return value with proper Result Type containing Ok or Err // TODO: Replace return value with proper Result Type containing Ok or Err
async fn create_entity(db: &DatabaseConnection, model: ActixAdminModel) -> ActixAdminModel; async fn create_entity(db: &DatabaseConnection, model: ActixAdminModel) -> Result<ActixAdminModel, ActixAdminError>;
async fn delete_entity(db: &DatabaseConnection, id: i32) -> bool; async fn delete_entity(db: &DatabaseConnection, id: i32) -> Result<bool, ActixAdminError>;
async fn get_entity(db: &DatabaseConnection, id: i32) -> ActixAdminModel; async fn get_entity(db: &DatabaseConnection, id: i32) -> Result<ActixAdminModel, ActixAdminError>;
async fn edit_entity(db: &DatabaseConnection, id: i32, model: ActixAdminModel) -> ActixAdminModel; async fn edit_entity(db: &DatabaseConnection, id: i32, model: ActixAdminModel) -> Result<ActixAdminModel, ActixAdminError>;
async fn get_select_lists(db: &DatabaseConnection) -> HashMap<String, Vec<(String, String)>>; async fn get_select_lists(db: &DatabaseConnection) -> Result<HashMap<String, Vec<(String, String)>>, ActixAdminError>;
fn validate_entity(model: &mut ActixAdminModel); fn validate_entity(model: &mut ActixAdminModel);
fn get_entity_name() -> String; fn get_entity_name() -> String;

View File

@ -17,6 +17,16 @@
</div> </div>
{% include "navbar.html" %} {% include "navbar.html" %}
<div class="container is-fluid"> <div class="container is-fluid">
<div id="notifications">
{% if notifications %}
{% for notification in notifications -%}
<div class="notification mb-4 is-light {{ notification.css_class }}">
<button class="delete" onclick="this.parentElement.remove()"></button>
{{ notification.message }}
</div>
{% endfor %}
{% endif %}
</div>
{% block content %} {% block content %}
{% endblock content %} {% endblock content %}
</div> </div>

View File

@ -1,7 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<form hx-boost="true" hx-indicator="#loading" hx-encoding="multipart/form-data" method="post" enctype="multipart/form-data"> <form hx-boost="true" hx-indicator="#loading" hx-push-url="true" hx-encoding="multipart/form-data" method="post" enctype="multipart/form-data">
{% for model_field in view_model.fields -%} {% for model_field in view_model.fields -%}
<div class="field"> <div class="field">
<label class="{{ model_field | get_html_input_type }}" for="{{ model_field.field_name }}"> <label class="{{ model_field | get_html_input_type }}" for="{{ model_field.field_name }}">

View File

@ -36,8 +36,13 @@
}); });
}); });
}); });
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> </script>
<style> <style>
@ -51,7 +56,7 @@
background: rgba(255, 255, 255, 0.3); background: rgba(255, 255, 255, 0.3);
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: 5; z-index: 6;
pointer-events: none pointer-events: none
} }
</style> </style>

View File

@ -6,7 +6,7 @@
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<div class="buttons"> <div class="buttons">
<a class="button is-primary" href="create">Create</a> <a class="button is-primary" href="create" hx-boost="true" hx-indicator="#loading">Create</a>
<div hx-include="#checked-rows" hx-target="#{{ entity_name }}table" class="dropdown is-hoverable"> <div hx-include="#checked-rows" hx-target="#{{ entity_name }}table" class="dropdown is-hoverable">
<div class="dropdown-trigger"> <div class="dropdown-trigger">
@ -20,7 +20,7 @@
<div class="dropdown-menu" id="dropdown-menu4"> <div class="dropdown-menu" id="dropdown-menu4">
<div class="dropdown-content"> <div class="dropdown-content">
<div class="dropdown-item"> <div class="dropdown-item">
<a href="#" hx-confirm="Are you sure?" hx-delete="delete">Delete</a> <a href="#" hx-indicator="#loading" hx-confirm="Are you sure?" hx-delete="delete">Delete</a>
</div> </div>
</div> </div>
</div> </div>
@ -45,7 +45,7 @@
</div> </div>
<div class="column is-narrow"> <div class="column is-narrow">
<div> <div>
<form> <form hx-boost="true" hx-indicator="#loading">
<input type="hidden" value="{{ search }}" name="search"> <input type="hidden" value="{{ search }}" name="search">
<div class="select"> <div class="select">
<div class="control has-icons-left has-icons-right"> <div class="control has-icons-left has-icons-right">
@ -82,7 +82,7 @@
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody hx-confirm="Are you sure?" hx-target="closest tr" hx-swap="outerHTML"> <tbody hx-confirm="Are you sure?" hx-target="closest tr" hx-indicator="#loading" hx-swap="outerHTML">
{% for entity in entities -%} {% for entity in entities -%}
<tr> <tr>
<td><input type="checkbox" name="ids" value="{{ entity.primary_key }}"></td> <td><input type="checkbox" name="ids" value="{{ entity.primary_key }}"></td>

View File

@ -17,9 +17,9 @@
{% if category == "" %} {% if category == "" %}
{% for menu_element in entities %} {% for menu_element in entities %}
{% if menu_element.is_custom_handler %} {% if menu_element.is_custom_handler %}
<a href="/admin/{{ menu_element.link }}" class="navbar-item {% if entity_name and entity_name == menu_element.name %}is-active{% endif %}">{{ menu_element.name }}</a> <a href="/admin/{{ menu_element.link }}" hx-boost="true" hx-indicator="#loading" class="navbar-item {% if entity_name and entity_name == menu_element.name %}is-active{% endif %}">{{ menu_element.name }}</a>
{% else %} {% else %}
<a href="/admin/{{ menu_element.link }}/list" class="navbar-item {% if entity_name and entity_name == menu_element.name %}is-active{% endif %}">{{ menu_element.name | title }}</a> <a href="/admin/{{ menu_element.link }}/list" hx-boost="true" hx-indicator="#loading" class="navbar-item {% if entity_name and entity_name == menu_element.name %}is-active{% endif %}">{{ menu_element.name | title }}</a>
{% endif %} {% endif %}
{%- endfor %} {%- endfor %}
{% else %} {% else %}
@ -30,9 +30,9 @@
<div class="navbar-dropdown"> <div class="navbar-dropdown">
{% for menu_element in entities %} {% for menu_element in entities %}
{% if menu_element.is_custom_handler %} {% if menu_element.is_custom_handler %}
<a href="/admin/{{ menu_element.link }}" class="navbar-item {% if entity_name and entity_name == menu_element.name %}is-active{% endif %}">{{ menu_element.name }}</a> <a href="/admin/{{ menu_element.link }}" hx-boost="true" hx-indicator="#loading" class="navbar-item {% if entity_name and entity_name == menu_element.name %}is-active{% endif %}">{{ menu_element.name }}</a>
{% else %} {% else %}
<a href="/admin/{{ menu_element.link }}/list" class="navbar-item {% if entity_name and entity_name == menu_element.name %}is-active{% endif %}">{{ menu_element.name | title }}</a> <a href="/admin/{{ menu_element.link }}/list" hx-boost="true" hx-indicator="#loading" class="navbar-item {% if entity_name and entity_name == menu_element.name %}is-active{% endif %}">{{ menu_element.name | title }}</a>
{% endif %} {% endif %}
{%- endfor %} {%- endfor %}
</div> </div>
@ -46,11 +46,11 @@
<div class="buttons"> <div class="buttons">
{% if enable_auth %} {% if enable_auth %}
{% if user_is_logged_in %} {% if user_is_logged_in %}
<a href="{{ logout_link }}" class="button is-light"> <a href="{{ logout_link }}" hx-boost="true" hx-indicator="#loading" class="button is-light">
Log out Log out
</a> </a>
{% else %} {% else %}
<a href="{{ login_link }}" class="button is-light"> <a href="{{ login_link }}" hx-boost="true" hx-indicator="#loading" class="button is-light">
Log in Log in
</a> </a>
{% endif %} {% endif %}

View File

@ -79,7 +79,7 @@ async fn create_post_from_plaintext<
text: String, text: String,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let model = ActixAdminModel::from(text); let model = ActixAdminModel::from(text);
create_or_edit_post::<T, E>(&session, &data, model, None).await create_or_edit_post::<T, E>(&session, &data, Ok(model), None).await
} }
async fn edit_post_from_plaintext< async fn edit_post_from_plaintext<
@ -92,5 +92,5 @@ async fn edit_post_from_plaintext<
id: web::Path<i32>, id: web::Path<i32>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let model = ActixAdminModel::from(text); let model = ActixAdminModel::from(text);
create_or_edit_post::<T, E>(&session, &data, model, Some(id.into_inner())).await create_or_edit_post::<T, E>(&session, &data, Ok(model), Some(id.into_inner())).await
} }