add custom handlers & post integration test

This commit is contained in:
manuel 2022-08-28 17:06:23 +02:00
parent 920c5d00aa
commit a7352e5793
9 changed files with 223 additions and 34 deletions

View File

@ -1 +1,2 @@
# actix-web-sample-app # Actix-Admin

View File

@ -7,6 +7,7 @@ use azure_auth::{AppDataTrait as AzureAuthAppDataTrait, AzureAuth, UserInfo};
use oauth2::basic::BasicClient; use oauth2::basic::BasicClient;
use oauth2::RedirectUrl; use oauth2::RedirectUrl;
use sea_orm::{ConnectOptions, DatabaseConnection}; use sea_orm::{ConnectOptions, DatabaseConnection};
use actix_web::http::header::ContentType;
use std::env; use std::env;
use std::time::Duration; use std::time::Duration;
use tera::{Context, Tera}; use tera::{Context, Tera};
@ -37,6 +38,19 @@ impl AzureAuthAppDataTrait for AppState {
} }
} }
async fn custom_handler<
T: ActixAdminAppDataTrait,
E: ActixAdminViewModelTrait,
>(
_session: Session,
_data: web::Data<T>,
_text: String
) -> HttpResponse {
HttpResponse::Ok()
.content_type(ContentType::plaintext())
.body("data")
}
async fn index(session: Session, data: web::Data<AppState>) -> HttpResponse { async fn index(session: Session, data: web::Data<AppState>) -> HttpResponse {
let login = session.get::<UserInfo>("user_info").unwrap(); let login = session.get::<UserInfo>("user_info").unwrap();
let web_auth_link = if login.is_some() { let web_auth_link = if login.is_some() {
@ -62,12 +76,16 @@ fn create_actix_admin_builder() -> ActixAdminBuilder {
user_info.is_some() user_info.is_some()
}), }),
login_link: Some("/azure-auth/login".to_string()), login_link: Some("/azure-auth/login".to_string()),
logout_link: Some("/azure-auth/logout".to_string() logout_link: Some("/azure-auth/logout".to_string())
}; };
let mut admin_builder = ActixAdminBuilder::new(configuration); let mut admin_builder = ActixAdminBuilder::new(configuration);
admin_builder.add_entity::<AppState, Post>(&post_view_model); admin_builder.add_entity::<AppState, Post>(&post_view_model);
admin_builder.add_entity::<AppState, Comment>(&comment_view_model); admin_builder.add_entity::<AppState, Comment>(&comment_view_model);
admin_builder.add_custom_handler_for_entity::<AppState, Comment>(
"/custom_handler",
web::get().to(custom_handler::<AppState, Comment>)
);
admin_builder admin_builder
} }

View File

@ -1,4 +1,4 @@
use actix_web::web; use actix_web::{ web, Route };
use std::collections::HashMap; use std::collections::HashMap;
use crate::prelude::*; use crate::prelude::*;
@ -6,7 +6,7 @@ use crate::prelude::*;
use crate::routes::{create_get, create_post, delete, delete_many, edit_get, edit_post, index, list}; use crate::routes::{create_get, create_post, delete, delete_many, edit_get, edit_post, index, list};
pub struct ActixAdminBuilder { pub struct ActixAdminBuilder {
pub scopes: Vec<actix_web::Scope>, pub scopes: HashMap<String, actix_web::Scope>,
pub actix_admin: ActixAdmin, pub actix_admin: ActixAdmin,
} }
@ -16,6 +16,11 @@ pub trait ActixAdminBuilderTrait {
&mut self, &mut self,
view_model: &ActixAdminViewModel, view_model: &ActixAdminViewModel,
); );
fn add_custom_handler_for_entity<T: ActixAdminAppDataTrait + 'static, E: ActixAdminViewModelTrait + 'static>(
&mut self,
path: &str,
route: Route
);
fn get_scope<T: ActixAdminAppDataTrait + 'static>(self) -> actix_web::Scope; fn get_scope<T: ActixAdminAppDataTrait + 'static>(self) -> actix_web::Scope;
fn get_actix_admin(&self) -> ActixAdmin; fn get_actix_admin(&self) -> ActixAdmin;
} }
@ -28,7 +33,7 @@ impl ActixAdminBuilderTrait for ActixAdminBuilder {
view_models: HashMap::new(), view_models: HashMap::new(),
configuration: configuration configuration: configuration
}, },
scopes: Vec::new(), scopes: HashMap::new(),
} }
} }
@ -36,7 +41,8 @@ impl ActixAdminBuilderTrait for ActixAdminBuilder {
&mut self, &mut self,
view_model: &ActixAdminViewModel, view_model: &ActixAdminViewModel,
) { ) {
self.scopes.push( self.scopes.insert(
E::get_entity_name(),
web::scope(&format!("/{}", E::get_entity_name())) web::scope(&format!("/{}", E::get_entity_name()))
.route("/list", web::get().to(list::<T, E>)) .route("/list", web::get().to(list::<T, E>))
.route("/create", web::get().to(create_get::<T, E>)) .route("/create", web::get().to(create_get::<T, E>))
@ -52,13 +58,38 @@ impl ActixAdminBuilderTrait for ActixAdminBuilder {
self.actix_admin.view_models.insert(key, view_model.clone()); self.actix_admin.view_models.insert(key, view_model.clone());
} }
fn get_scope<T: ActixAdminAppDataTrait + 'static>(self) -> actix_web::Scope { fn add_custom_handler_for_entity<T: ActixAdminAppDataTrait + 'static, E: ActixAdminViewModelTrait + 'static>(
let mut scope = web::scope("/admin").route("/", web::get().to(index::<T>)); &mut self,
for entity_scope in self.scopes { path: &str,
scope = scope.service(entity_scope); route: Route
) {
let existing_scope = self.scopes.remove(&E::get_entity_name());
match existing_scope {
Some(scope) => {
let existing_scope = scope.route(path, route);
self.scopes.insert(E::get_entity_name(), existing_scope);
},
_ => {
let new_scope =
web::scope(&format!("/{}", E::get_entity_name()))
.route(path, route);
self.scopes.insert(E::get_entity_name(), new_scope);
}
} }
scope if !self.actix_admin.entity_names.contains(&E::get_entity_name()) {
self.actix_admin.entity_names.push(E::get_entity_name());
}
}
fn get_scope<T: ActixAdminAppDataTrait + 'static>(mut self) -> actix_web::Scope {
let mut admin_scope = web::scope("/admin").route("/", web::get().to(index::<T>));
for entity_name in self.actix_admin.entity_names {
let scope = self.scopes.remove(&entity_name).unwrap();
admin_scope = admin_scope.service(scope);
}
admin_scope
} }
fn get_actix_admin(&self) -> ActixAdmin { fn get_actix_admin(&self) -> ActixAdmin {

View File

@ -18,6 +18,7 @@ pub mod prelude {
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::{ ActixAdminAppDataTrait, ActixAdmin, ActixAdminConfiguration };
pub use crate::{ hashmap, ActixAdminSelectListTrait }; pub use crate::{ hashmap, ActixAdminSelectListTrait };
pub use crate::routes::{ create_or_edit_post };
} }
use crate::prelude::*; use crate::prelude::*;

View File

@ -1,30 +1,38 @@
use actix_web::http::header; use super::{render_unauthorized, user_can_access_page};
use actix_web::{web, error, Error, HttpResponse};
use tera::{Context};
use actix_session::{Session};
use crate::TERA;
use actix_multipart::Multipart;
use super::{ user_can_access_page, render_unauthorized};
use crate::prelude::*; use crate::prelude::*;
use crate::TERA;
use actix_session::Session;
use actix_web::http::header;
use actix_web::{error, web, Error, HttpResponse};
use tera::Context;
use actix_multipart::Multipart;
use std::collections::HashMap;
pub async fn create_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>( pub async fn create_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
session: Session, session: Session,
data: web::Data<T>, data: web::Data<T>,
payload: Multipart, payload: Multipart,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
create_or_edit_post::<T, E>(&session, &data, payload, None).await let model = ActixAdminModel::create_from_payload(payload).await.unwrap();
create_or_edit_post::<T, E>(&session, &data, model, None).await
} }
pub async fn edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>( pub async fn edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
session: Session, session: Session,
data: web::Data<T>, data: web::Data<T>,
payload: Multipart, payload: Multipart,
id: web::Path<i32> id: web::Path<i32>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
create_or_edit_post::<T, E>(&session, &data, payload, Some(id.into_inner())).await let model = ActixAdminModel::create_from_payload(payload).await.unwrap();
create_or_edit_post::<T, E>(&session, &data, model, Some(id.into_inner())).await
} }
async fn create_or_edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(session: &Session, data: &web::Data<T>, payload: Multipart, id: Option<i32>) -> Result<HttpResponse, Error> { pub async fn create_or_edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
session: &Session,
data: &web::Data<T>,
mut model: ActixAdminModel,
id: Option<i32>,
) -> 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();
@ -35,15 +43,16 @@ async fn create_or_edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTr
ctx.insert("render_partial", &true); ctx.insert("render_partial", &true);
return render_unauthorized(&ctx); return render_unauthorized(&ctx);
} }
let db = &data.get_db(); let db = &data.get_db();
let mut model = ActixAdminModel::create_from_payload(payload).await.unwrap();
E::validate_entity(&mut model); E::validate_entity(&mut model);
if model.has_errors() { if model.has_errors() {
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("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);
@ -52,11 +61,10 @@ async fn create_or_edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTr
.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(HttpResponse::Ok().content_type("text/html").body(body))
} } else {
else {
match id { match id {
Some(id) => E::edit_entity(db, id, model).await, Some(id) => E::edit_entity(db, id, model).await,
None => E::create_entity(db, model).await None => E::create_entity(db, model).await,
}; };
Ok(HttpResponse::SeeOther() Ok(HttpResponse::SeeOther()
@ -65,5 +73,29 @@ async fn create_or_edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTr
format!("/admin/{}/list", view_model.entity_name), format!("/admin/{}/list", view_model.entity_name),
)) ))
.finish()) .finish())
} }
}
#[doc(hidden)]
impl From<String> for ActixAdminModel {
fn from(string: String) -> Self {
let mut hashmap = HashMap::new();
let key_values: Vec<&str> = string.split('&').collect();
for key_value in key_values {
if !key_value.is_empty() {
let mut iter = key_value.splitn(2, '=');
hashmap.insert(
iter.next().unwrap().to_string(),
iter.next().unwrap().to_string(),
);
}
}
ActixAdminModel {
primary_key: None,
values: hashmap,
errors: HashMap::new(),
custom_errors: HashMap::new(),
}
}
} }

View File

@ -2,7 +2,7 @@ mod create_or_edit_get;
pub use create_or_edit_get::{create_get, edit_get}; pub use create_or_edit_get::{create_get, edit_get};
mod create_or_edit_post; mod create_or_edit_post;
pub use create_or_edit_post::{ create_post, edit_post }; pub use create_or_edit_post::{ create_post, edit_post, create_or_edit_post };
mod index; mod index;
pub use index::index; pub use index::index;

View File

@ -11,27 +11,27 @@ mod tests {
use actix_admin::prelude::*; use actix_admin::prelude::*;
#[actix_web::test] #[actix_web::test]
async fn test_admin_index_get() { async fn admin_index_get() {
test_get_is_success("/admin/").await test_get_is_success("/admin/").await
} }
#[actix_web::test] #[actix_web::test]
async fn test_post_list() { async fn post_list_get() {
test_get_is_success("/admin/post/list").await test_get_is_success("/admin/post/list").await
} }
#[actix_web::test] #[actix_web::test]
async fn test_comment_list() { async fn comment_list_get() {
test_get_is_success("/admin/comment/list").await test_get_is_success("/admin/comment/list").await
} }
#[actix_web::test] #[actix_web::test]
async fn test_post_create() { async fn post_create_get() {
test_get_is_success("/admin/post/create").await test_get_is_success("/admin/post/create").await
} }
#[actix_web::test] #[actix_web::test]
async fn test_comment_create() { async fn comment_create_get() {
test_get_is_success("/admin/comment/create").await test_get_is_success("/admin/comment/create").await
} }

View File

@ -0,0 +1,60 @@
mod test_setup;
use test_setup::helper::{create_actix_admin_builder, create_tables_and_get_connection, AppState};
#[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};
#[actix_web::test]
async fn comment_create_post() {
test_post_is_success("/admin/comment/create_post_from_plaintext").await
}
#[actix_web::test]
async fn post_create_post() {
test_post_is_success("/admin/post/create_post_from_plaintext").await
}
#[actix_web::test]
async fn post_edit_post() {
test_post_is_success("/admin/post/edit_post_from_plaintext").await
}
#[actix_web::test]
async fn comment_edit_post() {
test_post_is_success("/admin/comment/edit_post_from_plaintext").await
}
async fn test_post_is_success(url: &str) {
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: 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 req = test::TestRequest::post()
.insert_header(ContentType::form_url_encoded())
.uri(url)
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
}
}

View File

@ -1,5 +1,9 @@
use sea_orm::{ConnectOptions, DatabaseConnection}; use sea_orm::{ConnectOptions, DatabaseConnection};
use actix_admin::prelude::*; use actix_admin::prelude::*;
use actix_web::Error;
use actix_session::Session;
use actix_web::HttpResponse;
use actix_web::{web};
use super::{Post, Comment, create_tables}; use super::{Post, Comment, create_tables};
@ -43,5 +47,47 @@ pub fn create_actix_admin_builder() -> ActixAdminBuilder {
admin_builder.add_entity::<AppState, Post>(&post_view_model); admin_builder.add_entity::<AppState, Post>(&post_view_model);
admin_builder.add_entity::<AppState, Comment>(&comment_view_model); admin_builder.add_entity::<AppState, Comment>(&comment_view_model);
admin_builder.add_custom_handler_for_entity::<AppState, Comment>(
"/create_comment_from_plaintext",
web::post().to(create_post_from_plaintext::<AppState, Comment>));
admin_builder.add_custom_handler_for_entity::<AppState, Post>(
"/create_post_from_plaintext",
web::post().to(create_post_from_plaintext::<AppState, Post>));
admin_builder.add_custom_handler_for_entity::<AppState, Post>(
"/edit_post_from_plaintext/{id}",
web::post().to(edit_post_from_plaintext::<AppState, Post>));
admin_builder.add_custom_handler_for_entity::<AppState, Comment>(
"/edit_comment_from_plaintext/{id}",
web::post().to(edit_post_from_plaintext::<AppState, Comment>));
admin_builder admin_builder
} }
async fn create_post_from_plaintext<
T: ActixAdminAppDataTrait,
E: ActixAdminViewModelTrait,
>(
session: Session,
data: web::Data<T>,
text: String,
) -> Result<HttpResponse, Error> {
let model = ActixAdminModel::from(text);
create_or_edit_post::<T, E>(&session, &data, model, None).await
}
async fn edit_post_from_plaintext<
T: ActixAdminAppDataTrait,
E: ActixAdminViewModelTrait,
>(
session: Session,
data: web::Data<T>,
text: String,
id: web::Path<i32>,
) -> Result<HttpResponse, Error> {
println!("ok");
let model = ActixAdminModel::from(text);
create_or_edit_post::<T, E>(&session, &data, model, Some(id.into_inner())).await
}