implement error handling through notifications
This commit is contained in:
parent
5f515c96eb
commit
859c139f8b
@ -27,6 +27,7 @@ serde = "1.0.136"
|
||||
serde_derive = "1.0.136"
|
||||
sea-orm = { version = "^0.9.1", features = [], default-features = false }
|
||||
actix-admin-macros = { version = "0.1.0", path = "actix_admin_macros" }
|
||||
derive_more = "0.99.17"
|
||||
|
||||
[dev-dependencies]
|
||||
sea-orm = { version = "^0.9.1", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = true }
|
@ -67,7 +67,7 @@ pub fn derive_actix_admin_view_model(input: proc_macro::TokenStream) -> proc_mac
|
||||
|
||||
#[async_trait(?Send)]
|
||||
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;
|
||||
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 insert_operation = Entity::insert(new_model).exec(db).await;
|
||||
let insert_operation = Entity::insert(new_model).exec(db).await?;
|
||||
|
||||
model.primary_key = Some(insert_operation.last_insert_id.to_string());
|
||||
|
||||
model
|
||||
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
|
||||
let entity = Entity::find_by_id(id).one(db).await.unwrap().unwrap();
|
||||
let model = ActixAdminModel::from(entity);
|
||||
model
|
||||
}
|
||||
|
||||
async fn edit_entity(db: &DatabaseConnection, id: i32, mut model: ActixAdminModel) -> ActixAdminModel {
|
||||
let entity: Option<Model> = Entity::find_by_id(id).one(db).await.unwrap();
|
||||
let mut entity: ActiveModel = entity.unwrap().into();
|
||||
|
||||
#(#fields_for_edit_model);*;
|
||||
let entity: Model = entity.update(db).await.unwrap();
|
||||
|
||||
model
|
||||
}
|
||||
|
||||
async fn delete_entity(db: &DatabaseConnection, id: i32) -> bool {
|
||||
let result = Entity::delete_by_id(id).exec(db).await;
|
||||
|
||||
match result {
|
||||
Ok(_) => true,
|
||||
Err(_) => false
|
||||
let entity = Entity::find_by_id(id).one(db).await?;
|
||||
match entity {
|
||||
Some(e) => Ok(ActixAdminModel::from(e)),
|
||||
_ => Err(ActixAdminError::EntityDoesNotExistError)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_select_lists(db: &DatabaseConnection) -> HashMap<String, Vec<(String, String)>> {
|
||||
hashmap![
|
||||
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?;
|
||||
|
||||
match entity {
|
||||
Some(e) => {
|
||||
let mut entity: ActiveModel = e.into();
|
||||
#(#fields_for_edit_model);*;
|
||||
let entity: Model = entity.update(db).await?;
|
||||
Ok(model)
|
||||
},
|
||||
_ => Err(ActixAdminError::EntityDoesNotExistError)
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_entity(db: &DatabaseConnection, id: i32) -> Result<bool, ActixAdminError> {
|
||||
let result = Entity::delete_by_id(id).exec(db).await;
|
||||
|
||||
match result {
|
||||
Ok(_) => Ok(true),
|
||||
Err(_) => Err(ActixAdminError::DeleteError)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_select_lists(db: &DatabaseConnection) -> Result<HashMap<String, Vec<(String, String)>>, ActixAdminError> {
|
||||
Ok(hashmap![
|
||||
#(#select_lists),*
|
||||
]
|
||||
])
|
||||
}
|
||||
|
||||
fn get_entity_name() -> String {
|
||||
@ -173,7 +181,7 @@ pub fn derive_actix_admin_model(input: proc_macro::TokenStream) -> proc_macro::T
|
||||
|
||||
#[async_trait]
|
||||
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::* };
|
||||
let paginator = Entity::find()
|
||||
.filter(
|
||||
@ -182,11 +190,10 @@ pub fn derive_actix_admin_model(input: proc_macro::TokenStream) -> proc_macro::T
|
||||
)
|
||||
.order_by_asc(Column::Id)
|
||||
.paginate(db, posts_per_page);
|
||||
let num_pages = paginator.num_pages().await.ok().unwrap();
|
||||
let num_pages = paginator.num_pages().await?;
|
||||
let entities = paginator
|
||||
.fetch_page(page - 1)
|
||||
.await
|
||||
.expect("could not retrieve entities");
|
||||
.await?;
|
||||
let mut model_entities = Vec::new();
|
||||
for entity in entities {
|
||||
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) {
|
||||
|
@ -12,15 +12,15 @@ pub fn get_select_list_from_model(_input: proc_macro::TokenStream) -> proc_macro
|
||||
let expanded = quote! {
|
||||
#[async_trait]
|
||||
impl ActixAdminSelectListTrait for Entity {
|
||||
async fn get_key_value(db: &DatabaseConnection) -> Vec<(String, String)> {
|
||||
let entities = Entity::find().order_by_asc(Column::Id).all(db).await;
|
||||
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 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.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! {
|
||||
#[async_trait]
|
||||
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();
|
||||
for field in #ty::iter() {
|
||||
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 select_list_ident = Ident::new(&(model_field.select_list), Span::call_site());
|
||||
quote! {
|
||||
#ident_name => #select_list_ident::get_key_value(db).await
|
||||
#ident_name => #select_list_ident::get_key_value(db).await?
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
|
82
src/lib.rs
82
src/lib.rs
@ -122,6 +122,12 @@ use tera::{Tera, Result, to_value, try_get_value };
|
||||
use std::{ hash::BuildHasher};
|
||||
use actix_session::{Session};
|
||||
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 model;
|
||||
@ -133,7 +139,7 @@ pub mod prelude {
|
||||
pub use crate::model::{ ActixAdminModel, ActixAdminModelValidationTrait, ActixAdminModelTrait};
|
||||
pub use crate::view_model::{ ActixAdminViewModel, ActixAdminViewModelTrait, ActixAdminViewModelField, ActixAdminViewModelSerializable, ActixAdminViewModelFieldType };
|
||||
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::routes::{ create_or_edit_post, get_admin_ctx };
|
||||
pub use crate::{ TERA };
|
||||
@ -211,7 +217,7 @@ pub trait ActixAdminAppDataTrait {
|
||||
// SelectListTrait
|
||||
#[async_trait]
|
||||
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 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()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
use crate::ActixAdminViewModelField;
|
||||
use crate::{ ActixAdminViewModelField, ActixAdminError};
|
||||
use async_trait::async_trait;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use serde::Serialize;
|
||||
@ -14,7 +14,7 @@ pub trait ActixAdminModelTrait {
|
||||
page: usize,
|
||||
posts_per_page: usize,
|
||||
search: &String
|
||||
) -> (usize, Vec<ActixAdminModel>);
|
||||
) -> Result<(usize, Vec<ActixAdminModel>), ActixAdminError>;
|
||||
fn get_fields() -> Vec<ActixAdminViewModelField>;
|
||||
fn validate_model(model: &mut ActixAdminModel);
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
use actix_web::{error, web, Error, HttpRequest, HttpResponse};
|
||||
use tera::{Context};
|
||||
use actix_session::{Session};
|
||||
use crate::ActixAdminError;
|
||||
use crate::ActixAdminNotification;
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::TERA;
|
||||
@ -16,7 +18,7 @@ pub async fn create_get<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
|
||||
let db = &data.get_db();
|
||||
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>(
|
||||
@ -32,13 +34,14 @@ pub async fn edit_get<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
|
||||
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 mut ctx = Context::new();
|
||||
add_auth_context(&session, actix_admin, &mut ctx);
|
||||
let entity_names = &actix_admin.entity_names;
|
||||
ctx.insert("entity_names", entity_names);
|
||||
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();
|
||||
|
||||
@ -46,13 +49,33 @@ async fn create_or_edit_get<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTra
|
||||
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("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("model", &model);
|
||||
ctx.insert("notifications", ¬ifications);
|
||||
|
||||
let body = TERA
|
||||
.render("create_or_edit.html", &ctx)
|
||||
.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))
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
use super::{render_unauthorized, user_can_access_page};
|
||||
use crate::ActixAdminError;
|
||||
use crate::ActixAdminNotification;
|
||||
use crate::prelude::*;
|
||||
use crate::TERA;
|
||||
use actix_multipart::MultipartError;
|
||||
use actix_session::Session;
|
||||
use actix_web::http::header;
|
||||
use actix_web::{error, web, Error, HttpResponse};
|
||||
@ -13,7 +16,7 @@ pub async fn create_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>
|
||||
data: web::Data<T>,
|
||||
payload: Multipart,
|
||||
) -> 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
|
||||
}
|
||||
|
||||
@ -23,20 +26,21 @@ pub async fn edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
|
||||
payload: Multipart,
|
||||
id: web::Path<i32>,
|
||||
) -> 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
|
||||
}
|
||||
|
||||
pub async fn create_or_edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
|
||||
session: &Session,
|
||||
data: &web::Data<T>,
|
||||
mut model: ActixAdminModel,
|
||||
model_res: Result<ActixAdminModel, MultipartError>,
|
||||
id: Option<i32>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let actix_admin = data.get_actix_admin();
|
||||
let entity_name = E::get_entity_name();
|
||||
|
||||
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) {
|
||||
let mut ctx = Context::new();
|
||||
@ -44,38 +48,58 @@ pub async fn create_or_edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewMod
|
||||
return render_unauthorized(&ctx);
|
||||
}
|
||||
let db = &data.get_db();
|
||||
|
||||
let mut model = model_res.unwrap();
|
||||
E::validate_entity(&mut model);
|
||||
|
||||
if model.has_errors() {
|
||||
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 body = TERA
|
||||
.render("create_or_edit.html", &ctx)
|
||||
.map_err(|err| error::ErrorInternalServerError(err))?;
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(body))
|
||||
errors.push(ActixAdminError::ValidationErrors);
|
||||
render_form::<E>(actix_admin, view_model, db, entity_name, &model, errors).await
|
||||
} else {
|
||||
match id {
|
||||
Some(id) => E::edit_entity(db, id, model).await,
|
||||
None => E::create_entity(db, model).await,
|
||||
let res = match id {
|
||||
Some(id) => E::edit_entity(db, id, model.clone()).await,
|
||||
None => E::create_entity(db, model.clone()).await,
|
||||
};
|
||||
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header((
|
||||
header::LOCATION,
|
||||
format!("/admin/{}/list", view_model.entity_name),
|
||||
))
|
||||
.finish())
|
||||
match res {
|
||||
Ok(_) => {
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header((
|
||||
header::LOCATION,
|
||||
format!("/admin/{}/list", view_model.entity_name),
|
||||
))
|
||||
.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", ¬ifications);
|
||||
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)]
|
||||
impl From<String> for ActixAdminModel {
|
||||
fn from(string: String) -> Self {
|
||||
|
@ -24,10 +24,12 @@ pub async fn delete<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
|
||||
}
|
||||
|
||||
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()
|
||||
.finish())
|
||||
match result {
|
||||
Ok(_) => Ok(HttpResponse::Ok().finish()),
|
||||
Err(_) => Ok(HttpResponse::InternalServerError().finish())
|
||||
}
|
||||
}
|
||||
|
||||
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 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) {
|
||||
let mut ctx = Context::new();
|
||||
@ -57,13 +60,24 @@ pub async fn delete_many<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>
|
||||
|
||||
// TODO: implement delete_many
|
||||
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()
|
||||
.append_header((
|
||||
header::LOCATION,
|
||||
format!("/admin/{}/list?render_partial=true", entity_name),
|
||||
))
|
||||
.finish())
|
||||
},
|
||||
false => {
|
||||
Ok(HttpResponse::InternalServerError().finish())
|
||||
}
|
||||
}
|
||||
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header((
|
||||
header::LOCATION,
|
||||
format!("/admin/{}/list?render_partial=true", entity_name),
|
||||
))
|
||||
.finish())
|
||||
}
|
@ -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> {
|
||||
let actix_admin = data.get_actix_admin();
|
||||
let notifications: Vec<crate::ActixAdminNotification> = Vec::new();
|
||||
|
||||
let mut ctx = Context::new();
|
||||
ctx.insert("entity_names", &actix_admin.entity_names);
|
||||
ctx.insert("notifications", ¬ifications);
|
||||
|
||||
add_auth_context(&session, actix_admin, &mut ctx);
|
||||
|
||||
|
@ -6,6 +6,7 @@ use crate::prelude::*;
|
||||
use crate::ActixAdminViewModelTrait;
|
||||
use crate::ActixAdminViewModel;
|
||||
use crate::ActixAdminModel;
|
||||
use crate::ActixAdminNotification;
|
||||
use crate::TERA;
|
||||
use actix_session::{Session};
|
||||
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 entity_name = E::get_entity_name();
|
||||
let view_model: &ActixAdminViewModel = actix_admin.view_models.get(&entity_name).unwrap();
|
||||
let mut errors: Vec<ActixAdminError> = Vec::new();
|
||||
|
||||
let mut ctx = Context::new();
|
||||
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 db = data.get_db();
|
||||
let result: (usize, Vec<ActixAdminModel>) = E::list(db, page, entities_per_page, &search).await;
|
||||
let entities = result.1;
|
||||
let num_pages = result.0;
|
||||
let result = E::list(db, page, entities_per_page, &search).await;
|
||||
match result {
|
||||
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("entities", &entities);
|
||||
ctx.insert("notifications", ¬ifications);
|
||||
ctx.insert("page", &page);
|
||||
ctx.insert("params", &entities_per_page);
|
||||
ctx.insert("entities_per_page", &entities_per_page);
|
||||
ctx.insert("render_partial", &render_partial);
|
||||
ctx.insert("num_pages", &num_pages);
|
||||
ctx.insert("view_model", &ActixAdminViewModelSerializable::from(view_model.clone()));
|
||||
ctx.insert("search", &search);
|
||||
|
||||
let body = TERA
|
||||
.render("list.html", &ctx)
|
||||
.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))
|
||||
}
|
@ -2,6 +2,7 @@ use actix_web::{error, web, Error, HttpResponse};
|
||||
use actix_session::{Session};
|
||||
use tera::{Context};
|
||||
|
||||
use crate::ActixAdminNotification;
|
||||
use crate::prelude::*;
|
||||
|
||||
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> {
|
||||
let actix_admin = data.get_actix_admin();
|
||||
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 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();
|
||||
ctx.insert("model", &model);
|
||||
ctx.insert("view_model", &ActixAdminViewModelSerializable::from(view_model.clone()));
|
||||
ctx.insert("list_link", &E::get_list_link(&entity_name));
|
||||
ctx.insert("entity_names", &actix_admin.entity_names);
|
||||
ctx.insert("notifications", ¬ifications);
|
||||
|
||||
add_auth_context(&session, actix_admin, &mut ctx);
|
||||
|
||||
let body = TERA
|
||||
.render("show.html", &ctx)
|
||||
.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))
|
||||
}
|
@ -5,6 +5,7 @@ use std::collections::HashMap;
|
||||
use crate::ActixAdminModel;
|
||||
use actix_session::{Session};
|
||||
use std::convert::From;
|
||||
use crate::ActixAdminError;
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait ActixAdminViewModelTrait {
|
||||
@ -13,14 +14,14 @@ pub trait ActixAdminViewModelTrait {
|
||||
page: usize,
|
||||
entities_per_page: usize,
|
||||
search: &String
|
||||
) -> (usize, Vec<ActixAdminModel>);
|
||||
) -> Result<(usize, Vec<ActixAdminModel>), ActixAdminError>;
|
||||
|
||||
// TODO: Replace return value with proper Result Type containing Ok or Err
|
||||
async fn create_entity(db: &DatabaseConnection, model: ActixAdminModel) -> ActixAdminModel;
|
||||
async fn delete_entity(db: &DatabaseConnection, id: i32) -> bool;
|
||||
async fn get_entity(db: &DatabaseConnection, id: i32) -> ActixAdminModel;
|
||||
async fn edit_entity(db: &DatabaseConnection, id: i32, model: ActixAdminModel) -> ActixAdminModel;
|
||||
async fn get_select_lists(db: &DatabaseConnection) -> HashMap<String, Vec<(String, String)>>;
|
||||
async fn create_entity(db: &DatabaseConnection, model: ActixAdminModel) -> Result<ActixAdminModel, ActixAdminError>;
|
||||
async fn delete_entity(db: &DatabaseConnection, id: i32) -> Result<bool, ActixAdminError>;
|
||||
async fn get_entity(db: &DatabaseConnection, id: i32) -> Result<ActixAdminModel, ActixAdminError>;
|
||||
async fn edit_entity(db: &DatabaseConnection, id: i32, model: ActixAdminModel) -> Result<ActixAdminModel, ActixAdminError>;
|
||||
async fn get_select_lists(db: &DatabaseConnection) -> Result<HashMap<String, Vec<(String, String)>>, ActixAdminError>;
|
||||
fn validate_entity(model: &mut ActixAdminModel);
|
||||
|
||||
fn get_entity_name() -> String;
|
||||
|
@ -17,6 +17,16 @@
|
||||
</div>
|
||||
{% include "navbar.html" %}
|
||||
<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 %}
|
||||
{% endblock content %}
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% 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 -%}
|
||||
<div class="field">
|
||||
<label class="{{ model_field | get_html_input_type }}" for="{{ model_field.field_name }}">
|
||||
|
@ -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>
|
||||
|
||||
<style>
|
||||
@ -51,7 +56,7 @@
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 5;
|
||||
z-index: 6;
|
||||
pointer-events: none
|
||||
}
|
||||
</style>
|
@ -6,7 +6,7 @@
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<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 class="dropdown-trigger">
|
||||
@ -20,7 +20,7 @@
|
||||
<div class="dropdown-menu" id="dropdown-menu4">
|
||||
<div class="dropdown-content">
|
||||
<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>
|
||||
@ -45,7 +45,7 @@
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<div>
|
||||
<form>
|
||||
<form hx-boost="true" hx-indicator="#loading">
|
||||
<input type="hidden" value="{{ search }}" name="search">
|
||||
<div class="select">
|
||||
<div class="control has-icons-left has-icons-right">
|
||||
@ -82,7 +82,7 @@
|
||||
</th>
|
||||
</tr>
|
||||
</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 -%}
|
||||
<tr>
|
||||
<td><input type="checkbox" name="ids" value="{{ entity.primary_key }}"></td>
|
||||
|
@ -17,9 +17,9 @@
|
||||
{% if category == "" %}
|
||||
{% for menu_element in entities %}
|
||||
{% 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 %}
|
||||
<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 %}
|
||||
{%- endfor %}
|
||||
{% else %}
|
||||
@ -30,9 +30,9 @@
|
||||
<div class="navbar-dropdown">
|
||||
{% for menu_element in entities %}
|
||||
{% 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 %}
|
||||
<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 %}
|
||||
{%- endfor %}
|
||||
</div>
|
||||
@ -46,11 +46,11 @@
|
||||
<div class="buttons">
|
||||
{% if enable_auth %}
|
||||
{% 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
|
||||
</a>
|
||||
{% 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
|
||||
</a>
|
||||
{% endif %}
|
||||
|
@ -79,7 +79,7 @@ async fn create_post_from_plaintext<
|
||||
text: String,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
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<
|
||||
@ -92,5 +92,5 @@ async fn edit_post_from_plaintext<
|
||||
id: web::Path<i32>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
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
|
||||
}
|
Loading…
Reference in New Issue
Block a user