split into models and viewmodels

This commit is contained in:
Manuel Gugger 2022-04-23 20:03:09 +02:00
parent 10c50d7ddb
commit 8c106eb9f2
15 changed files with 350 additions and 88 deletions

2
.gitignore vendored
View File

@ -1,7 +1,7 @@
# Generated by Cargo # Generated by Cargo
# will have compiled files and executables # will have compiled files and executables
/target/ /target/
**/target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock Cargo.lock

View File

@ -6,22 +6,23 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
actix-web = "4.0.0-beta.12" actix-web = "4.0.1"
actix-rt = "2.5.0" actix-rt = "2.7.0"
actix-session = "0.5.0-beta.4" actix-session = "0.5.0"
tera = "1.15.0" tera = "1.15.0"
oauth2 = "4.1" oauth2 = "4.1"
base64 = "0.13.0" base64 = "0.13.0"
async-trait = "0.1.53"
rand = "0.8.4" rand = "0.8.5"
url = "2.2.2" url = "2.2.2"
http = "0.2.5" http = "0.2.6"
dotenv = "0.15" dotenv = "0.15"
futures = "0.3.18" futures = "0.3.21"
serde = "1.0.130" serde = "1.0.136"
serde_json = "1.0.71" serde_json = "1.0.79"
serde_derive = "1.0.130" serde_derive = "1.0.136"
quote = "1.0"
sea-orm = { version = "0.5.0", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = false } sea-orm = { version = "0.6.0", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = false }
actix_admin = { path = "actix_admin" }

24
actix_admin/Cargo.toml Normal file
View File

@ -0,0 +1,24 @@
[package]
name = "actix_admin"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4.0.1"
actix-rt = "2.7.0"
actix-session = "0.5.0"
tera = "1.15.0"
actix_admin_macros = { path = "actix_admin_macros" }
oauth2 = "4.1"
base64 = "0.13.0"
async-trait = "0.1.53"
rand = "0.8.5"
url = "2.2.2"
http = "0.2.6"
dotenv = "0.15"
lazy_static = "1.4.0"
futures = "0.3.21"
serde = "1.0.136"
serde_json = "1.0.79"
serde_derive = "1.0.136"
sea-orm = { version = "0.6.0", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = false }

View File

@ -0,0 +1,30 @@
[package]
name = "actix_admin_macros"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
actix-web = "4.0.1"
actix-rt = "2.7.0"
actix-session = "0.5.0"
tera = "1.15.0"
oauth2 = "4.1"
base64 = "0.13.0"
quote = "1.0"
syn = { version = "1.0", features = ["full", "extra-traits"] }
proc-macro2 = { version = "1.0.36", default-features = false }
rand = "0.8.5"
url = "2.2.2"
http = "0.2.6"
dotenv = "0.15"
futures = "0.3.21"
serde = "1.0.136"
serde_json = "1.0.79"
serde_derive = "1.0.136"
sea-orm = { version = "0.6.0", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = false }

View File

@ -0,0 +1,40 @@
use proc_macro;
use quote::quote;
#[proc_macro_derive(DeriveActixAdminModel)]
pub fn derive_crud_fns(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let expanded = quote! {
use std::convert::From;
use async_trait::async_trait;
use actix_admin::{ ActixAdminModelTrait, ActixAdminModel };
impl From<Entity> for ActixAdminModel {
fn from(entity: Entity) -> Self {
ActixAdminModel {
fields: Vec::new()
}
}
}
#[async_trait]
impl ActixAdminModelTrait for Entity {
async fn list(db: &DatabaseConnection, page: usize, posts_per_page: usize) -> Vec<ActixAdminModel> {
use sea_orm::{ query::* };
let paginator = Entity::find()
.order_by_asc(Column::Id)
.paginate(db, posts_per_page);
let entities = paginator
.fetch_page(page - 1)
.await
.expect("could not retrieve entities");
//entities to ActixAdminModel
vec![
ActixAdminModel {
fields: Vec::new()
}
]
}
}
};
proc_macro::TokenStream::from(expanded)
}

View File

@ -0,0 +1,24 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use actix_admin_macros::ActixAdmin;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize, ActixAdmin)]
#[sea_orm(table_name = "test")]
pub struct Model {
#[sea_orm(primary_key)]
#[serde(skip_deserializing)]
pub id: i32,
pub title: String,
#[sea_orm(column_type = "Text")]
pub text: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
#[test]
fn test_macro() {
assert_eq!(4, 2);
}

153
actix_admin/src/lib.rs Normal file
View File

@ -0,0 +1,153 @@
use actix_web::{error, guard, web, Error, HttpRequest, HttpResponse};
use actix_web::{ dev, App, FromRequest};
use actix_web::error::ErrorBadRequest;
use serde_derive::Deserialize;
use std::collections::HashMap;
use tera::{Context, Tera};
use futures::future::{ok, err, Ready};
use lazy_static::lazy_static;
use sea_orm::DatabaseConnection;
use sea_orm::EntityTrait;
use sea_orm::ModelTrait;
use async_trait::async_trait;
pub use actix_admin_macros::DeriveActixAdminModel;
const DEFAULT_POSTS_PER_PAGE: usize = 5;
// templates
lazy_static! {
static ref TERA: Tera = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap();
}
// Paging
#[derive(Debug, Deserialize)]
pub struct Params {
page: Option<usize>,
posts_per_page: Option<usize>,
}
// Fields
#[derive(Clone, Debug)]
pub enum Field {
Text
}
// AppDataTrait
pub trait AppDataTrait {
fn get_db(&self) -> &DatabaseConnection;
fn get_view_model_map(&self) -> &HashMap<&'static str, ActixAdminViewModel>;
}
// ActixAdminModel
#[async_trait]
pub trait ActixAdminModelTrait {
async fn list(db: &DatabaseConnection, page: usize, posts_per_page: usize) -> Vec<ActixAdminModel>;
}
#[derive(Clone, Debug)]
pub struct ActixAdminModel {
pub fields: Vec<(&'static str, Field)>
}
// ActixAdminViewModel
pub trait ActixAdminViewModelTrait : Clone {
fn get_model_name(&self) -> &str;
//fn get_entities() -> Vec<ActixAdminModel>;
}
impl ActixAdminViewModelTrait for ActixAdminViewModel {
fn get_model_name(&self) -> &str {
&self.entity_name
}
}
#[derive(Clone, Debug)]
pub struct ActixAdminViewModel {
pub entity_name: &'static str,
pub admin_model: ActixAdminModel
}
// ActixAdminController
#[derive(Clone, Debug)]
pub struct ActixAdmin {
view_models: HashMap<&'static str, ActixAdminViewModel>,
}
impl ActixAdmin {
pub fn new() -> Self {
let actix_admin = ActixAdmin {
view_models: HashMap::new(),
};
actix_admin
}
pub fn create_scope<T: AppDataTrait + 'static>(self, _app_state: &T) -> actix_web::Scope {
let mut scope = web::scope("/admin").route("/", web::get().to(index::<T>));
for view_model in self.view_models {
scope = scope.service(
web::scope(&format!("/{}", view_model.0)).route("/list", web::get().to(list::<T>))
);
}
scope
}
pub fn add_entity(mut self, view_model: ActixAdminViewModel) -> Self {
self.view_models.insert(view_model.entity_name, view_model);
self
}
pub fn get_view_model_map(&self) -> HashMap<&'static str, ActixAdminViewModel> {
self.view_models.clone()
}
}
async fn index<T: AppDataTrait>(data: web::Data<T>) -> Result<HttpResponse, Error> {
let keys = Vec::from_iter(data.get_view_model_map().keys());
let mut ctx = Context::new();
ctx.insert("view_models", &keys);
let body = TERA
.render("index.html", &ctx)
.map_err(|_| error::ErrorInternalServerError("Template error"))?;
Ok(HttpResponse::Ok().content_type("text/html").body(body))
}
async fn list<T: AppDataTrait>(req: HttpRequest, data: web::Data<T>) -> Result<HttpResponse, Error> {
let db = &data.get_db();
let params = web::Query::<Params>::from_query(req.query_string()).unwrap();
let page = params.page.unwrap_or(1);
let posts_per_page = params.posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE);
let columns: Vec<String> = Vec::new();
// let paginator = post::Entity::find()
// .order_by_asc(post::Column::Id)
// .paginate(db, posts_per_page);
//let num_pages = paginator.num_pages().await.ok().unwrap();
let posts: Vec<&str> = Vec::new();
//let posts = paginator
// .fetch_page(page - 1)
// .await
// .expect("could not retrieve posts");
let mut ctx = Context::new();
ctx.insert("posts", &posts);
ctx.insert("page", &page);
ctx.insert("posts_per_page", &posts_per_page);
ctx.insert("num_pages", "5" /*&num_pages*/);
ctx.insert("columns", &columns);
// let body = data.tmpl
// .render("list.html", &ctx)
// .map_err(|_| error::ErrorInternalServerError("Template error"))?;
//Ok(HttpResponse::Ok().content_type("text/html").body(body))
Ok(HttpResponse::Ok()
.content_type("text/html")
.body("<html></html>"))
}

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Actix Admin</title>
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
</head>
<body>
{% block content %}
{% endblock content %}
</body>
</html>

View File

@ -0,0 +1,5 @@
{% extends "base.html" %}
{% block content %}
<p>Index</p>
{% endblock content %}

View File

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block content %}
Hello
<p>posts: {{ posts }}</p>
<p>page: {{ page }}</p>
<p>posts_per_page: {{ posts_per_page }}</p>
<p>num_pages: {{ num_pages }}</p>
{% endblock content %}

View File

@ -1,65 +0,0 @@
use actix_web::{web, guard, HttpRequest, HttpResponse, Error, error};
use tera::{ Tera, Context};
use crate::entity::Post;
use crate::entity::post;
use sea_orm::{ entity::*, query::*, SelectorTrait, ModelTrait, ConnectionTrait, ColumnTrait, PaginatorTrait, EntityTrait };
use sea_orm::{{ DatabaseConnection, ConnectOptions }};
const DEFAULT_POSTS_PER_PAGE: usize = 5;
#[derive(Debug, Deserialize)]
pub struct Params {
page: Option<usize>,
posts_per_page: Option<usize>,
}
async fn index(data: web::Data<super::AppState>) -> &'static str {
"Welcome!"
}
async fn list<T: EntityTrait>(
req: HttpRequest,
data: web::Data<super::AppState>) -> Result<HttpResponse, Error>
{
let db = &data.db;
let params = web::Query::<Params>::from_query(req.query_string()).unwrap();
let page = params.page.unwrap_or(1);
let posts_per_page = params.posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE);
let paginator = Post::find()
.order_by_asc(post::Column::Id)
.paginate(db, posts_per_page);
let num_pages = paginator.num_pages().await.ok().unwrap();
let posts = paginator
.fetch_page(page - 1)
.await
.expect("could not retrieve posts");
let mut ctx = Context::new();
ctx.insert("posts", &posts);
ctx.insert("page", &page);
ctx.insert("posts_per_page", &posts_per_page);
ctx.insert("num_pages", &num_pages);
let body = data.tmpl
.render("list.html", &ctx)
.map_err(|_| error::ErrorInternalServerError("Template error"))?;
Ok(HttpResponse::Ok().content_type("text/html").body(body))
}
fn entity_scope<T: EntityTrait>(entity: T) -> actix_web::Scope {
let entity_name = entity.table_name();
let scope = web::scope(&format!("/{}",entity_name))
.route("/list", web::get().to(list::<T>));
scope
}
pub fn admin_scope() -> actix_web::Scope {
let scope = web::scope("/admin")
.route("/", web::get().to(index))
.service(entity_scope(Post));
scope
}

View File

@ -1,7 +1,9 @@
// setup // setup
use sea_orm::sea_query::{ColumnDef, TableCreateStatement}; use sea_orm::sea_query::{ColumnDef, TableCreateStatement};
use sea_orm::{error::*, sea_query, ConnectionTrait, DbConn, ExecResult}; use sea_orm::{error::*, sea_query, ConnectionTrait, DbConn, ExecResult};
use sea_orm::{{ DatabaseConnection, ConnectOptions }};
use sea_orm::{ entity::*, query::*, SelectorTrait, ModelTrait, ColumnTrait, PaginatorTrait, EntityTrait };
use async_trait::async_trait;
pub mod post; pub mod post;
pub use post::Entity as Post; pub use post::Entity as Post;

View File

@ -1,7 +1,9 @@
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)] use actix_admin::{ DeriveActixAdminModel };
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize, DeriveActixAdminModel)]
#[sea_orm(table_name = "post")] #[sea_orm(table_name = "post")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]

View File

@ -11,21 +11,35 @@ use oauth2::{
}; };
use std::time::{Duration}; use std::time::{Duration};
use std::env; use std::env;
use sea_orm::{{ DatabaseConnection, ConnectOptions }}; use sea_orm::{{ DatabaseConnection, ConnectOptions, EntityName }};
use actix_admin::{ActixAdminViewModelTrait, AppDataTrait, ActixAdminViewModel, ActixAdminModel};
use std::collections::HashMap;
mod web_auth; mod web_auth;
mod entity; mod entity;
mod actix_admin;
use entity::{ Post };
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct AppState { pub struct AppState {
pub oauth: BasicClient, pub oauth: BasicClient,
pub api_base_url: String, pub api_base_url: String,
pub tmpl: Tera, pub tmpl: Tera,
pub db: DatabaseConnection pub db: DatabaseConnection,
pub view_model_map: HashMap<&'static str, ActixAdminViewModel>
} }
fn index(session: Session, data: web::Data<AppState>) -> HttpResponse { impl AppDataTrait for AppState {
fn get_db(&self) -> &DatabaseConnection {
&self.db
}
fn get_view_model_map(&self) -> &HashMap<&'static str, ActixAdminViewModel> {
&self.view_model_map
}
}
async fn index(session: Session, data: web::Data<AppState>) -> HttpResponse {
let login = session.get::<web_auth::UserInfo>("user_info").unwrap(); let login = session.get::<web_auth::UserInfo>("user_info").unwrap();
let web_auth_link = if login.is_some() { "logout" } else { "login" }; let web_auth_link = if login.is_some() { "logout" } else { "login" };
@ -85,18 +99,27 @@ async fn main() {
let conn = sea_orm::Database::connect(opt).await.unwrap(); let conn = sea_orm::Database::connect(opt).await.unwrap();
let _ = entity::create_post_table(&conn).await; let _ = entity::create_post_table(&conn).await;
let viewmodel_entity = ActixAdminViewModel {
entity_name: "posts",
admin_model: ActixAdminModel::from(Post)
};
let actix_admin = actix_admin::ActixAdmin::new()
.add_entity(viewmodel_entity.clone());
let app_state = AppState { let app_state = AppState {
oauth: client, oauth: client,
api_base_url, api_base_url,
tmpl: tera, tmpl: tera,
db: conn db: conn,
view_model_map: actix_admin.get_view_model_map()
}; };
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
.app_data(web::Data::new(app_state.clone())) .app_data(web::Data::new(app_state.clone()))
.wrap(CookieSession::signed(&[0; 32]).secure(false)) .wrap(CookieSession::signed(&[0; 32]).secure(false))
.service(actix_admin::admin_scope()) .service(actix_admin.clone().create_scope(&app_state))
.route("/", web::get().to(index)) .route("/", web::get().to(index))
.route("/login", web::get().to(web_auth::login)) .route("/login", web::get().to(web_auth::login))
.route("/logout", web::get().to(web_auth::logout)) .route("/logout", web::get().to(web_auth::logout))

View File

@ -10,7 +10,7 @@ use oauth2::{
use std::str; use std::str;
use url::Url; use url::Url;
pub fn login(data: web::Data<super::AppState>) -> HttpResponse { pub async fn login(data: web::Data<super::AppState>) -> HttpResponse {
// Create a PKCE code verifier and SHA-256 encode it as a code challenge. // Create a PKCE code verifier and SHA-256 encode it as a code challenge.
// let (_pkce_code_challenge, _pkce_code_verifier) = PkceCodeChallenge::new_random_sha256(); // let (_pkce_code_challenge, _pkce_code_verifier) = PkceCodeChallenge::new_random_sha256();
// Generate the authorization URL to which we'll redirect the user. // Generate the authorization URL to which we'll redirect the user.
@ -28,7 +28,7 @@ pub fn login(data: web::Data<super::AppState>) -> HttpResponse {
.finish() .finish()
} }
pub fn logout(session: Session) -> HttpResponse { pub async fn logout(session: Session) -> HttpResponse {
session.remove("user_info"); session.remove("user_info");
HttpResponse::Found() HttpResponse::Found()
.append_header((header::LOCATION, "/".to_string())) .append_header((header::LOCATION, "/".to_string()))