split into models and viewmodels
This commit is contained in:
parent
10c50d7ddb
commit
8c106eb9f2
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,7 +1,7 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
**/target/
|
||||
# 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
|
||||
Cargo.lock
|
||||
|
25
Cargo.toml
25
Cargo.toml
@ -6,22 +6,23 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4.0.0-beta.12"
|
||||
actix-rt = "2.5.0"
|
||||
actix-session = "0.5.0-beta.4"
|
||||
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"
|
||||
|
||||
rand = "0.8.4"
|
||||
async-trait = "0.1.53"
|
||||
rand = "0.8.5"
|
||||
url = "2.2.2"
|
||||
http = "0.2.5"
|
||||
http = "0.2.6"
|
||||
dotenv = "0.15"
|
||||
futures = "0.3.18"
|
||||
serde = "1.0.130"
|
||||
serde_json = "1.0.71"
|
||||
serde_derive = "1.0.130"
|
||||
|
||||
sea-orm = { version = "0.5.0", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = false }
|
||||
futures = "0.3.21"
|
||||
serde = "1.0.136"
|
||||
serde_json = "1.0.79"
|
||||
serde_derive = "1.0.136"
|
||||
quote = "1.0"
|
||||
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
24
actix_admin/Cargo.toml
Normal 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 }
|
30
actix_admin/actix_admin_macros/Cargo.toml
Normal file
30
actix_admin/actix_admin_macros/Cargo.toml
Normal 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 }
|
40
actix_admin/actix_admin_macros/src/lib.rs
Normal file
40
actix_admin/actix_admin_macros/src/lib.rs
Normal 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)
|
||||
}
|
24
actix_admin/actix_admin_macros/tests/macros_sqlx.rs
Normal file
24
actix_admin/actix_admin_macros/tests/macros_sqlx.rs
Normal 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
153
actix_admin/src/lib.rs
Normal 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>"))
|
||||
}
|
14
actix_admin/templates/base.html
Normal file
14
actix_admin/templates/base.html
Normal 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>
|
5
actix_admin/templates/index.html
Normal file
5
actix_admin/templates/index.html
Normal file
@ -0,0 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<p>Index</p>
|
||||
{% endblock content %}
|
9
actix_admin/templates/list.html
Normal file
9
actix_admin/templates/list.html
Normal 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 %}
|
@ -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
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
// setup
|
||||
use sea_orm::sea_query::{ColumnDef, TableCreateStatement};
|
||||
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 use post::Entity as Post;
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
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")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
|
35
src/main.rs
35
src/main.rs
@ -11,21 +11,35 @@ use oauth2::{
|
||||
};
|
||||
use std::time::{Duration};
|
||||
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 entity;
|
||||
mod actix_admin;
|
||||
|
||||
use entity::{ Post };
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AppState {
|
||||
pub oauth: BasicClient,
|
||||
pub api_base_url: String,
|
||||
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 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 _ = 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 {
|
||||
oauth: client,
|
||||
api_base_url,
|
||||
tmpl: tera,
|
||||
db: conn
|
||||
db: conn,
|
||||
view_model_map: actix_admin.get_view_model_map()
|
||||
};
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(web::Data::new(app_state.clone()))
|
||||
.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("/login", web::get().to(web_auth::login))
|
||||
.route("/logout", web::get().to(web_auth::logout))
|
||||
|
@ -10,7 +10,7 @@ use oauth2::{
|
||||
use std::str;
|
||||
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.
|
||||
// let (_pkce_code_challenge, _pkce_code_verifier) = PkceCodeChallenge::new_random_sha256();
|
||||
// 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()
|
||||
}
|
||||
|
||||
pub fn logout(session: Session) -> HttpResponse {
|
||||
pub async fn logout(session: Session) -> HttpResponse {
|
||||
session.remove("user_info");
|
||||
HttpResponse::Found()
|
||||
.append_header((header::LOCATION, "/".to_string()))
|
||||
|
Loading…
Reference in New Issue
Block a user