From 966291dbd95e143885472cfc87ae578046b4741d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabor=20K=C3=B6rber?= Date: Sat, 20 Jul 2024 09:34:51 +0200 Subject: [PATCH] refactor: using word depot in rear instead of admin --- rear/src/depot/context.rs | 78 ++++ rear/src/depot/mod.rs | 31 ++ rear/src/depot/prelude.rs | 7 + rear/src/depot/registry.rs | 164 ++++++++ rear/src/depot/repository.rs | 361 ++++++++++++++++++ rear/src/depot/state.rs | 11 + rear/src/depot/views.rs | 294 ++++++++++++++ rear/src/depot/widgets.rs | 79 ++++ rear/src/lib.rs | 2 +- rear/src/service/templates.rs | 21 +- rear_auth/src/user_admin_repository.rs | 13 +- rear_auth/src/views.rs | 10 +- src/admin_examples/empty_repository.rs | 4 +- src/admin_examples/file_repository.rs | 13 +- src/admin_examples/static_repository.rs | 15 +- src/main.rs | 6 +- src/state.rs | 8 +- static/{admin/admin.css => rear/depot.css} | 0 static/{admin => rear}/teddy_bear.png | Bin templates/admin/index.html | 10 - templates/{admin => depot}/base.jinja | 24 +- templates/{admin => depot}/base_hx.jinja | 0 templates/{admin => depot}/dashboard.jinja | 0 .../app_list.jinja => depot/depot.jinja} | 23 +- templates/{admin => depot}/django_base.html | 0 templates/depot/index.html | 10 + .../{admin => depot}/items/item_change.jinja | 0 .../items/item_change_form.jinja | 0 .../{admin => depot}/items/item_create.jinja | 0 .../{admin => depot}/items/item_detail.jinja | 0 .../{admin => depot}/items/item_list.jinja | 0 templates/{admin => depot}/items/items.jinja | 0 templates/{admin => depot}/login.html | 0 .../widgets/checkbox_toggle.jinja | 0 .../{admin => depot}/widgets/input_text.jinja | 0 .../widgets/input_textarea.jinja | 0 .../widgets/password_change.jinja | 0 .../widgets/select_labeled.jinja | 0 38 files changed, 1112 insertions(+), 72 deletions(-) create mode 100644 rear/src/depot/context.rs create mode 100644 rear/src/depot/mod.rs create mode 100644 rear/src/depot/prelude.rs create mode 100644 rear/src/depot/registry.rs create mode 100644 rear/src/depot/repository.rs create mode 100644 rear/src/depot/state.rs create mode 100644 rear/src/depot/views.rs create mode 100644 rear/src/depot/widgets.rs rename static/{admin/admin.css => rear/depot.css} (100%) rename static/{admin => rear}/teddy_bear.png (100%) delete mode 100644 templates/admin/index.html rename templates/{admin => depot}/base.jinja (82%) rename templates/{admin => depot}/base_hx.jinja (100%) rename templates/{admin => depot}/dashboard.jinja (100%) rename templates/{admin/app_list.jinja => depot/depot.jinja} (50%) rename templates/{admin => depot}/django_base.html (100%) create mode 100644 templates/depot/index.html rename templates/{admin => depot}/items/item_change.jinja (100%) rename templates/{admin => depot}/items/item_change_form.jinja (100%) rename templates/{admin => depot}/items/item_create.jinja (100%) rename templates/{admin => depot}/items/item_detail.jinja (100%) rename templates/{admin => depot}/items/item_list.jinja (100%) rename templates/{admin => depot}/items/items.jinja (100%) rename templates/{admin => depot}/login.html (100%) rename templates/{admin => depot}/widgets/checkbox_toggle.jinja (100%) rename templates/{admin => depot}/widgets/input_text.jinja (100%) rename templates/{admin => depot}/widgets/input_textarea.jinja (100%) rename templates/{admin => depot}/widgets/password_change.jinja (100%) rename templates/{admin => depot}/widgets/select_labeled.jinja (100%) diff --git a/rear/src/depot/context.rs b/rear/src/depot/context.rs new file mode 100644 index 0000000..41610c1 --- /dev/null +++ b/rear/src/depot/context.rs @@ -0,0 +1,78 @@ +use serde::{Deserialize, Serialize}; + +use super::repository::{RepositoryInfo, RepositoryItem, RepositoryList}; + +// representation of a Model in the template. +#[derive(Deserialize, Serialize)] +pub struct DepotModel { + pub key: String, + pub name: String, + + pub model_url: String, + pub view_only: bool, + + pub add_url: Option, +} + +// representation of a Section in the template. +#[derive(Deserialize, Serialize)] +pub struct DepotSection { + pub key: String, + pub name: String, + + pub section_url: String, + pub models: Vec, +} + +#[derive(Serialize, Deserialize)] +pub struct DepotRequest { + pub path: String, +} + +#[derive(Serialize)] +pub struct DepotContext { + pub base: Option, + pub language_code: Option, + pub language_bidi: Option, + pub user: Option, // Todo: user type + pub depot_url: String, + pub site_url: Option, + pub docsroot: Option, + pub messages: Vec, // Todo: message type + pub title: Option, + pub subtitle: Option, + pub content: String, + + pub request: DepotRequest, + pub sections: Vec, + pub item_model: Option, + pub item_info: Option, + pub item_list: RepositoryList, + pub item: Option, +} + +impl Default for DepotContext { + fn default() -> Self { + DepotContext { + base: None, // TODO: what is this used for? + language_code: Some("en-us".to_string()), // Default language code + language_bidi: Some(false), // Default language bidi + user: None, //UserType::default(), // Assuming UserType has a Default impl + depot_url: "/depot".to_owned(), + site_url: None, + docsroot: None, + messages: Vec::new(), // Empty vector for messages + title: None, + subtitle: None, + content: String::new(), // Empty string for content + sections: Vec::new(), + request: DepotRequest { + path: "".to_owned(), + }, + item_model: None, + item_info: None, + item_list: RepositoryList::Empty, + item: None, + } + } +} diff --git a/rear/src/depot/mod.rs b/rear/src/depot/mod.rs new file mode 100644 index 0000000..96a1e59 --- /dev/null +++ b/rear/src/depot/mod.rs @@ -0,0 +1,31 @@ +use axum::{routing::get, Router}; + +mod context; +pub mod prelude; +mod registry; +pub mod repository; +pub mod state; +pub mod views; +pub mod widgets; + +pub fn routes() -> Router { + Router::new() + .route("/", get(views::index::).post(views::index_action::)) + .route("/app/:app", get(views::list_app::)) + .route( + "/app/:app/model/:model", + get(views::list_item_collection::), + ) + .route( + "/app/:app/model/:model/add", + get(views::new_item::).post(views::create_item::), + ) + .route( + "/app/:app/model/:model/change/:id", + get(views::change_item::).patch(views::update_item::), + ) + .route( + "/app/:app/model/:model/detail/:id", + get(views::view_item_details::), + ) +} diff --git a/rear/src/depot/prelude.rs b/rear/src/depot/prelude.rs new file mode 100644 index 0000000..badea68 --- /dev/null +++ b/rear/src/depot/prelude.rs @@ -0,0 +1,7 @@ +pub use super::context::{DepotContext, DepotModel}; +pub use super::registry::DepotRegistry; +pub use super::repository::{ + DepotModelConfig, DepotRepository, RepoInfo, RepositoryContext, RepositoryInfo, RepositoryItem, + RepositoryList, +}; +pub use super::widgets::Widget; diff --git a/rear/src/depot/registry.rs b/rear/src/depot/registry.rs new file mode 100644 index 0000000..2ddddc0 --- /dev/null +++ b/rear/src/depot/registry.rs @@ -0,0 +1,164 @@ +use std::{collections::HashMap, sync::Arc}; +use tokio::sync::Mutex; + +use super::{ + context::{DepotModel, DepotSection}, + repository::{DepotModelConfig, DepotRepository, DepotRepositoryWrapper, DynDepotRepository}, +}; + +pub struct DepotRegistry { + base_path: String, + sections: HashMap, + models: HashMap, + repositories: HashMap>>, +} + +impl DepotRegistry { + pub fn new(base_path: &str) -> Self { + DepotRegistry { + base_path: base_path.to_owned(), + sections: HashMap::new(), + models: HashMap::new(), + repositories: HashMap::new(), + } + } + + pub fn get_sections(&self) -> Vec { + self.sections + .iter() + .map(|(key, section_info)| self.get_section(key, section_info)) + .collect() + } + + fn get_section(&self, key: &str, section_info: &internal::DepotSectionInfo) -> DepotSection { + let my_models = self.get_models(key); + DepotSection { + key: key.to_owned(), + name: section_info.name.to_owned(), + section_url: format!("/{}/section/{}", self.base_path, key.to_owned()), + models: my_models, + } + } + + pub fn register_section(&mut self, name: &str) -> String { + let key = self.get_key(name); + self.sections.insert( + key.to_owned(), + internal::DepotSectionInfo { + key: key.to_owned(), + name: name.to_owned(), + }, + ); + key + } + + fn get_key(&self, name: &str) -> String { + slug::slugify(name) + } + + fn model_from_model_info(&self, model_info: &internal::DepotModelInfo) -> DepotModel { + let model_url = format!( + "/{}/section/{}/model/{}", + self.base_path, model_info.section_key, model_info.model_key + ); + DepotModel { + key: model_info.model_key.clone(), + name: model_info.name.clone(), + view_only: false, + add_url: Some(format!("{}/add", model_url)), + model_url: model_url, + } + } + + pub fn get_models(&self, section_key: &str) -> Vec { + self.models + .iter() + .filter(|(key, _)| key.starts_with(&format!("{}.", section_key))) + .map(|(_, model)| self.model_from_model_info(model)) + .collect() + } + + pub fn get_model(&self, section_key: &str, model_key: &str) -> Option { + let full_model_key = format!("{}.{}", section_key, model_key); + let internal_model = self.models.get(&full_model_key)?; + Some(self.model_from_model_info(internal_model)) + } + + fn register_model_config(&mut self, model: DepotModelConfig) -> Result { + let local_config = internal::DepotModelInfo::from(model); + if local_config.model_key.is_empty() { + return Err("No model name".to_owned()); + } + let local_config_name = format!("{}.{}", local_config.section_key, local_config.model_key); + if self.models.contains_key(&local_config_name) { + return Err(format!("Model {} already exists", local_config_name)); + } + let full_model_key = local_config_name.clone(); + self.models.insert(local_config_name, local_config); + Ok(full_model_key) + } + + pub fn register_model( + &mut self, + model: DepotModelConfig, + repository: R, + ) -> Result<(), String> { + let model_key = self.register_model_config(model)?; + let repository = DepotRepositoryWrapper::new(repository); + self.repositories + .insert(model_key, Arc::new(Mutex::new(repository))); + Ok(()) + } + + pub fn get_repository( + &self, + section_key: &str, + model_key: &str, + ) -> Result>, String> { + let full_model_key = format!("{}.{}", section_key, model_key); + if let Some(repo) = self.repositories.get(&full_model_key) { + // Clone the Arc to return a reference to the repository + return Ok(Arc::clone(repo)); + } else { + return Err("Couldn't find repository".to_owned()); + } + } +} + +mod internal { + // how the registry saves data internally. + use super::super::repository::DepotModelConfig; + + #[derive(Clone)] + pub(super) struct DepotSectionInfo { + pub key: String, + pub name: String, + } + + #[derive(Clone)] + pub(super) struct DepotModelInfo { + pub section_key: String, + pub model_key: String, + pub name: String, + } + + impl From for DepotModelInfo { + fn from(value: DepotModelConfig) -> Self { + DepotModelInfo { + section_key: value.section_key, + model_key: slug::slugify(value.name.clone()), + name: value.name, + } + } + } + + impl From<(&str, &str)> for DepotModelInfo { + fn from(value: (&str, &str)) -> Self { + DepotModelInfo { + section_key: value.0.to_owned(), + model_key: slug::slugify(value.1.to_owned()), + name: value.1.to_owned(), + } + } + } +} diff --git a/rear/src/depot/repository.rs b/rear/src/depot/repository.rs new file mode 100644 index 0000000..1bb5753 --- /dev/null +++ b/rear/src/depot/repository.rs @@ -0,0 +1,361 @@ +use async_trait::async_trait; +use serde::{Serialize, Serializer}; +use serde_json::Value; +use std::any::Any; +use std::fmt::Debug; +use std::vec::IntoIter; + +use super::context::DepotModel; +use super::widgets::Widget; + +// user uses this configuration object to register another model. +pub struct DepotModelConfig { + pub name: String, + pub section_key: String, +} + +impl From<(&str, &str)> for DepotModelConfig { + fn from(value: (&str, &str)) -> Self { + DepotModelConfig { + section_key: value.0.to_owned(), + name: value.1.to_owned(), + } + } +} + +// May become it's own structure. +pub type RepositoryContext = DepotModel; + +impl RepositoryContext { + pub fn get_default_detail_url(&self, key: &str) -> Option { + Some(format!("{}/detail/{}", self.model_url, key)) + } + + pub fn get_default_change_url(&self, key: &str) -> Option { + Some(format!("{}/change/{}", self.model_url, key)) + } + + pub fn build_item(&self, key: &str, fields: Value) -> RepositoryItem { + RepositoryItem { + detail_url: self.get_default_detail_url(key), + change_url: self.get_default_change_url(key), + fields: fields, + } + } +} + +#[derive(Debug, Serialize)] +pub struct Field { + widget: String, + label: Option, + #[serde(rename = "type")] + field_type: String, + readonly: bool, + required: bool, + options: Value, +} + +impl From for Field { + fn from(value: Widget) -> Self { + Field { + widget: value.widget.to_string(), + label: value.label.map(|s| s.to_string()), + field_type: value.field_type.to_string(), + readonly: value.readonly, + required: value.required, + options: value + .options + .iter() + .map(|(key, val)| (key.to_string(), serde_json::json!(val))) + .collect::>() + .into(), + } + } +} + +#[derive(Serialize)] +pub struct RepositoryItem { + pub fields: Value, + pub detail_url: Option, + pub change_url: Option, +} + +pub enum RepositoryList { + Empty, + List { + values: Vec, + }, + Page { + values: Vec, + offset: usize, + total: usize, + }, + Stream { + values: Vec, + next_index: Option, + }, +} + +impl IntoIterator for RepositoryList { + type Item = RepositoryItem; + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + match self { + RepositoryList::Empty => vec![].into_iter(), + RepositoryList::List { values } => values.into_iter(), + RepositoryList::Page { values, .. } => values.into_iter(), + RepositoryList::Stream { values, .. } => values.into_iter(), + } + } +} + +impl Serialize for RepositoryList { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + RepositoryList::Empty => serializer.serialize_unit(), + RepositoryList::List { values } + | RepositoryList::Page { values, .. } + | RepositoryList::Stream { values, .. } => values.serialize(serializer), + } + } +} + +/// Static initializer for RepositoryInfo. +pub struct RepoInfo { + pub name: &'static str, + pub lookup_key: &'static str, + pub display_list: &'static [&'static str], + pub fields: &'static [&'static str], +} + +impl RepoInfo { + pub fn build(self) -> RepositoryInfo { + self.into() + } +} + +#[derive(Serialize)] +pub struct RepositoryInfo { + name: String, + lookup_key: String, + display_list: Vec, + fields: Vec<(String, Field)>, +} + +impl RepositoryInfo { + pub fn new(name: &str, lookup_key: &str) -> Self { + RepositoryInfo { + name: name.to_owned(), + lookup_key: lookup_key.to_owned(), + display_list: vec![], + fields: vec![], + } + } + + // self mutating builder pattern + pub fn display_list(mut self, display_list: &[&str]) -> Self { + self.display_list = display_list.iter().map(|&e| e.to_string()).collect(); + self + } + + pub fn set_widget>(mut self, name: &str, item: T) -> Self { + let field = item.into(); // Convert the input into a Field + + // Find the index of the existing entry with the same name, if it exists + let pos = self + .fields + .iter() + .position(|(existing_name, _)| existing_name == name); + + match pos { + Some(index) => { + self.fields[index].1 = field; + } + None => { + self.fields.push((name.to_owned(), field)); + } + } + self + } +} + +impl From for RepositoryInfo { + fn from(repo_info: RepoInfo) -> Self { + RepositoryInfo { + name: repo_info.name.to_string(), + lookup_key: repo_info.lookup_key.to_string(), + display_list: repo_info + .display_list + .iter() + .map(|&s| s.to_string()) + .collect(), + fields: repo_info + .fields + .iter() + .map(|x| (x.to_string(), Field::from(Widget::default()))) + .collect(), + } + } +} + +pub trait PrimaryKeyType: Any + Debug + Send + Sync { + fn as_any(&self) -> &dyn Any; +} + +impl PrimaryKeyType for i64 { + fn as_any(&self) -> &dyn Any { + self + } +} + +impl PrimaryKeyType for String { + fn as_any(&self) -> &dyn Any { + self + } +} + +#[async_trait] +pub trait DepotRepository: Send + Sync { + type Key: PrimaryKeyType; + + fn key_from_string(&self, s: String) -> Option; + + async fn info(&self, context: &RepositoryContext) -> RepositoryInfo; + async fn list(&self, context: &RepositoryContext) -> RepositoryList; + async fn get(&self, context: &RepositoryContext, id: &Self::Key) -> Option; + async fn create(&mut self, context: &RepositoryContext, data: Value) -> Option; + async fn update( + &mut self, + context: &RepositoryContext, + id: &Self::Key, + data: Value, + ) -> Option; + async fn replace( + &mut self, + context: &RepositoryContext, + id: &Self::Key, + data: Value, + ) -> Option; + async fn delete(&mut self, context: &RepositoryContext, id: &Self::Key) -> Option; +} + +#[async_trait] +pub(crate) trait DynDepotRepository: Send + Sync { + fn key_from_string(&self, s: String) -> Option>; + async fn info(&self, context: &RepositoryContext) -> RepositoryInfo; + async fn list(&self, context: &RepositoryContext) -> RepositoryList; + async fn get( + &self, + context: &RepositoryContext, + id: &dyn PrimaryKeyType, + ) -> Option; + async fn create(&mut self, context: &RepositoryContext, data: Value) -> Option; + async fn update( + &mut self, + context: &RepositoryContext, + id: &dyn PrimaryKeyType, + data: Value, + ) -> Option; + async fn replace( + &mut self, + context: &RepositoryContext, + id: &dyn PrimaryKeyType, + data: Value, + ) -> Option; + async fn delete( + &mut self, + context: &RepositoryContext, + id: &dyn PrimaryKeyType, + ) -> Option; +} + +pub struct DepotRepositoryWrapper { + inner: T, +} + +impl DepotRepositoryWrapper { + pub fn new(inner: T) -> Self { + Self { inner } + } + + fn key_from_string(&self, s: String) -> Option<::Key> { + self.inner.key_from_string(s) + } +} + +#[async_trait] +impl DynDepotRepository for DepotRepositoryWrapper { + fn key_from_string(&self, s: String) -> Option> { + if let Some(key) = self.inner.key_from_string(s) { + Some(Box::new(key)) + } else { + None + } + } + + async fn info(&self, context: &RepositoryContext) -> RepositoryInfo { + self.inner.info(context).await + } + + async fn list(&self, context: &RepositoryContext) -> RepositoryList { + self.inner.list(context).await + } + + async fn get( + &self, + context: &RepositoryContext, + id: &dyn PrimaryKeyType, + ) -> Option { + if let Some(key) = id.as_any().downcast_ref::() { + self.inner.get(context, key).await + } else { + None + } + } + + async fn create(&mut self, context: &RepositoryContext, data: Value) -> Option { + self.inner.create(context, data).await + } + + async fn update( + &mut self, + context: &RepositoryContext, + id: &dyn PrimaryKeyType, + data: Value, + ) -> Option { + if let Some(key) = id.as_any().downcast_ref::() { + self.inner.update(context, key, data).await + } else { + None + } + } + + async fn replace( + &mut self, + context: &RepositoryContext, + id: &dyn PrimaryKeyType, + data: Value, + ) -> Option { + if let Some(key) = id.as_any().downcast_ref::() { + self.inner.replace(context, key, data).await + } else { + None + } + } + + async fn delete( + &mut self, + context: &RepositoryContext, + id: &dyn PrimaryKeyType, + ) -> Option { + if let Some(key) = id.as_any().downcast_ref::() { + self.inner.delete(context, key).await + } else { + None + } + } +} diff --git a/rear/src/depot/state.rs b/rear/src/depot/state.rs new file mode 100644 index 0000000..ead12c9 --- /dev/null +++ b/rear/src/depot/state.rs @@ -0,0 +1,11 @@ +use crate::service::templates::Templates; +use std::sync::Arc; + +use super::registry::DepotRegistry; + +pub trait DepotState { + fn get_templates(&self) -> &Templates; + fn get_registry(&self) -> SharedDepotRegistry; +} + +pub type SharedDepotRegistry = Arc; diff --git a/rear/src/depot/views.rs b/rear/src/depot/views.rs new file mode 100644 index 0000000..6bd57fd --- /dev/null +++ b/rear/src/depot/views.rs @@ -0,0 +1,294 @@ +use axum::extract::Path; +use axum::http::HeaderMap; +use axum::Form; +use axum::{extract::State, response::IntoResponse}; +use log::info; +use serde_json::Value; + +use super::context::DepotContext; +use super::state::DepotState; + +pub fn base_template(headers: &HeaderMap) -> Option { + let hx_request = headers.get("HX-Request").is_some(); + if hx_request { + Some("depot/base_hx.jinja".to_string()) + } else { + None + } +} + +pub async fn index( + depot: State, + headers: HeaderMap, +) -> impl IntoResponse { + let templates = depot.get_templates(); + let registry = depot.get_registry(); + templates.render_html( + "depot/index.html", + DepotContext { + base: base_template(&headers), + sections: registry.get_sections(), + ..Default::default() + }, + ) +} + +// Index Action is POST to the index site. We can anchor some general business code here. +pub async fn index_action( + depot: State, +) -> impl IntoResponse { + "There is your answer!".to_owned() +} + +pub async fn list_app( + depot: State, + Path(depot_key): Path, +) -> impl IntoResponse { + let templates = depot.get_templates(); + templates.render_html("depot/depot.jinja", ()) +} + +// List Items renders the entire list item page. +pub async fn list_item_collection( + depot: State, + headers: HeaderMap, + Path((depot_key, model_key)): Path<(String, String)>, +) -> impl IntoResponse { + info!("list_item_collection {} for model {}", depot_key, model_key); + let templates = depot.get_templates(); + let registry = depot.get_registry(); + let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) { + let repo = repo.lock().await; + let depot_model = registry + .get_model(&depot_key, &model_key) + .expect("depot Model not found?"); // we will need a proper error route; so something that implements IntoResponse and can be substituted in the unwraps and expects. + DepotContext { + base: base_template(&headers), + sections: registry.get_sections(), + item_info: Some(repo.info(&depot_model).await), + item_list: repo.list(&depot_model).await, + item_model: Some(depot_model), + ..Default::default() + } + } else { + DepotContext { + base: base_template(&headers), + sections: registry.get_sections(), + ..Default::default() + } + }; + templates.render_html("depot/items/item_list.jinja", context) +} + +// Items Action is a POST to an item list. By default these are actions, that work on a list of items as input. +pub async fn item_collection_action( + depot: State, + Path((depot_key, model_key)): Path<(String, String)>, +) -> impl IntoResponse { + "There is your answer!".to_owned() +} + +// Item Details shows one single dataset. +pub async fn view_item_details( + depot: State, + headers: HeaderMap, + Path((depot_key, model_key, id)): Path<(String, String, String)>, +) -> impl IntoResponse { + let templates = depot.get_templates(); + let registry = depot.get_registry(); + let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) { + let repo = repo.lock().await; + let depot_model = registry + .get_model(&depot_key, &model_key) + .expect("depot Model not found?"); + if let Some(key) = repo.key_from_string(id) { + DepotContext { + base: base_template(&headers), + sections: registry.get_sections(), + item_info: Some(repo.info(&depot_model).await), + item_list: repo.list(&depot_model).await, + item: repo.get(&depot_model, key.as_ref()).await, + item_model: Some(depot_model), + ..Default::default() + } + } else { + DepotContext { + base: base_template(&headers), + sections: registry.get_sections(), + ..Default::default() + } + } + } else { + DepotContext { + base: base_template(&headers), + sections: registry.get_sections(), + ..Default::default() + } + }; + templates.render_html("depot/items/item_detail.jinja", context) +} + +pub async fn new_item( + depot: State, + headers: HeaderMap, + Path((depot_key, model_key)): Path<(String, String)>, +) -> impl IntoResponse { + let templates = depot.get_templates(); + let registry = depot.get_registry(); + let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) { + let repo = repo.lock().await; + let depot_model = registry + .get_model(&depot_key, &model_key) + .expect("depot Model not found?"); + DepotContext { + base: base_template(&headers), + sections: registry.get_sections(), + item_info: Some(repo.info(&depot_model).await), + item_list: repo.list(&depot_model).await, + item_model: Some(depot_model), + ..Default::default() + } + } else { + DepotContext { + base: base_template(&headers), + sections: registry.get_sections(), + ..Default::default() + } + }; + templates.render_html("depot/items/item_create.jinja", context) +} + +pub async fn create_item( + depot: State, + headers: HeaderMap, + Path((depot_key, model_key)): Path<(String, String)>, + Form(form): Form, +) -> impl IntoResponse { + let templates = depot.get_templates(); + let registry = depot.get_registry(); + let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) { + let mut repo = repo.lock().await; + let depot_model = registry + .get_model(&depot_key, &model_key) + .expect("Depot Model not found?"); + + // create our item. + let result = repo.create(&depot_model, form).await; + + // TODO: refactor run over these views, way too much repetition. + DepotContext { + base: base_template(&headers), + sections: registry.get_sections(), + item_info: Some(repo.info(&depot_model).await), + item_list: repo.list(&depot_model).await, + item_model: Some(depot_model), + item: result, + ..Default::default() + } + } else { + DepotContext { + base: base_template(&headers), + sections: registry.get_sections(), + ..Default::default() + } + }; + templates.render_html("depot/items/item_create.jinja", context) +} + +/// Change is the GET version. +pub async fn change_item( + depot: State, + headers: HeaderMap, + Path((depot_key, model_key, id)): Path<(String, String, String)>, +) -> impl IntoResponse { + let templates = depot.get_templates(); + let registry = depot.get_registry(); + let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) { + let repo = repo.lock().await; + let depot_model = registry + .get_model(&depot_key, &model_key) + .expect("depot Model not found?"); + if let Some(key) = repo.key_from_string(id) { + DepotContext { + base: base_template(&headers), + sections: registry.get_sections(), + item_info: Some(repo.info(&depot_model).await), + item_list: repo.list(&depot_model).await, + item: repo.get(&depot_model, key.as_ref()).await, + item_model: Some(depot_model), + ..Default::default() + } + } else { + DepotContext { + base: base_template(&headers), + sections: registry.get_sections(), + ..Default::default() + } + } + } else { + DepotContext { + base: base_template(&headers), + sections: registry.get_sections(), + ..Default::default() + } + }; + templates.render_html("depot/items/item_change.jinja", context) +} + +pub async fn update_item( + depot: State, + headers: HeaderMap, + Path((depot_key, model_key, id)): Path<(String, String, String)>, + Form(form): Form, +) -> impl IntoResponse { + let templates = depot.get_templates(); + let registry = depot.get_registry(); + let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) { + let mut repo = repo.lock().await; + let depot_model = registry + .get_model(&depot_key, &model_key) + .expect("depot Model not found?"); + if let Some(key) = repo.key_from_string(id) { + let result = repo.update(&depot_model, key.as_ref(), form).await; + DepotContext { + base: base_template(&headers), + sections: registry.get_sections(), + item_info: Some(repo.info(&depot_model).await), + item_list: repo.list(&depot_model).await, + item: result, + item_model: Some(depot_model), + ..Default::default() + } + } else { + DepotContext { + base: base_template(&headers), + sections: registry.get_sections(), + ..Default::default() + } + } + } else { + DepotContext { + base: base_template(&headers), + sections: registry.get_sections(), + ..Default::default() + } + }; + let response = templates.render_html("depot/items/item_change.jinja", context); + response +} + +// Item Action allows running an action on one single dataset. +pub async fn item_action( + depot: State, + Path((depot_key, model_key, model_id)): Path<(String, String, String)>, +) -> impl IntoResponse { + "There is your answer!".to_owned() +} + +pub async fn debug_view( + depot: State, + Path(data): Path, +) -> impl IntoResponse { + println!("debug: {}", data); + "Debug!".to_owned() +} diff --git a/rear/src/depot/widgets.rs b/rear/src/depot/widgets.rs new file mode 100644 index 0000000..be52349 --- /dev/null +++ b/rear/src/depot/widgets.rs @@ -0,0 +1,79 @@ +use serde::Serialize; + +/// This is a static configuration object. +/// It might be changed in the future to have a dynamic counterpart. +/// +/// ## Example: +/// Creating a simple required and readonly text input field with a label: +/// +/// ```ignore +/// let my_field = Field::widget("/admin/widgets/input_text.jinja") +/// .labelled("Username") +/// .required() +/// .readonly(); +/// ``` +#[derive(Debug, Serialize, Clone, Copy)] +pub struct Widget { + pub widget: &'static str, + pub label: Option<&'static str>, + #[serde(rename = "type")] + pub field_type: &'static str, + pub required: bool, + pub readonly: bool, + pub options: &'static [(&'static str, &'static str)], +} + +impl Widget { + pub fn widget(widget: &'static str) -> Self { + Widget { + widget: widget, + label: None, + field_type: "text", + required: false, + readonly: false, + options: &[], + } + } + + pub fn default() -> Self { + Self::widget("/admin/widgets/input_text.jinja") + } + + pub fn textarea() -> Self { + Self::widget("/admin/widgets/input_textarea.jinja") + } + + pub fn checkbox() -> Self { + Self::widget("/admin/widgets/checkbox_toggle.jinja") + } + + pub fn required(mut self) -> Self { + self.required = true; + self + } + + pub fn readonly(mut self) -> Self { + self.readonly = true; + self + } + + pub fn labeled(mut self, label: &'static str) -> Self { + self.label = Some(label); + self + } + + pub fn as_password(mut self) -> Self { + self.field_type = "password"; + self + } + + pub fn as_hidden(mut self) -> Self { + self.field_type = "hidden"; + self + } + + pub fn options(mut self, options: &'static [(&'static str, &'static str)]) -> Self { + self.options = options; + self + } +} diff --git a/rear/src/lib.rs b/rear/src/lib.rs index 8459cac..e6ebc3e 100644 --- a/rear/src/lib.rs +++ b/rear/src/lib.rs @@ -1,2 +1,2 @@ -pub mod admin; +pub mod depot; pub mod service; diff --git a/rear/src/service/templates.rs b/rear/src/service/templates.rs index b759502..3b577c8 100644 --- a/rear/src/service/templates.rs +++ b/rear/src/service/templates.rs @@ -3,7 +3,23 @@ use axum::response::Html; use minijinja::{path_loader, Environment, Value}; use minijinja_autoreload::AutoReloader; use pulldown_cmark::Event; -use std::sync::Arc; +use std::{path::Path, sync::Arc}; + +pub fn composite_loader>( + dirs: Vec

, +) -> impl Fn(&str) -> Result, minijinja::Error> + Send + Sync + 'static { + let loaders: Vec<_> = dirs.into_iter().map(|dir| path_loader(dir)).collect(); + move |name| { + for loader in &loaders { + match loader(name) { + Ok(Some(template)) => return Ok(Some(template)), + Ok(None) => continue, + Err(err) => return Err(err.into()), + } + } + Ok(None) + } +} #[derive(Clone)] pub struct Templates { @@ -15,7 +31,8 @@ impl Templates { let reloader = AutoReloader::new(move |notifier| { let mut environment = Environment::new(); let template_path = "templates"; - environment.set_loader(path_loader(&template_path)); + let loader = composite_loader(vec![template_path]); + environment.set_loader(loader); environment.add_filter("none", none); environment.add_filter("markdown", markdown); environment.add_filter("yesno", filter_yesno); diff --git a/rear_auth/src/user_admin_repository.rs b/rear_auth/src/user_admin_repository.rs index e7c0c7c..0ca05c4 100644 --- a/rear_auth/src/user_admin_repository.rs +++ b/rear_auth/src/user_admin_repository.rs @@ -1,14 +1,13 @@ use async_trait::async_trait; use log::debug; -use rear::admin::domain::*; -use rear::admin::state::AdminRegistry; +use rear::depot::prelude::*; use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, ModelTrait, Set}; use serde_json::Value; use crate::models::UserRepository; #[async_trait] -impl AdminRepository for UserRepository { +impl DepotRepository for UserRepository { type Key = i64; fn key_from_string(&self, s: String) -> Option { @@ -202,11 +201,11 @@ impl AdminRepository for UserRepository { } } -pub fn register(registry: &mut AdminRegistry, db: DatabaseConnection) -> UserRepository { - let app_key = registry.register_app("Auth"); +pub fn register(registry: &mut DepotRegistry, db: DatabaseConnection) -> UserRepository { + let section_key = registry.register_section("Auth"); let repo = UserRepository::new(db); - let model_config = AdminModelConfig { - app_key: app_key, + let model_config = DepotModelConfig { + section_key: section_key, name: "User".to_owned(), }; let model_result = registry.register_model(model_config, repo.clone()); diff --git a/rear_auth/src/views.rs b/rear_auth/src/views.rs index ff4a56b..ff11dda 100644 --- a/rear_auth/src/views.rs +++ b/rear_auth/src/views.rs @@ -23,7 +23,7 @@ pub struct NextUrl { next: Option, } -pub fn routes() -> Router +pub fn routes() -> Router where { Router::new() .route("/login", post(self::post::login)) @@ -44,7 +44,7 @@ mod post { Ok(None) => { messages.error("Invalid credentials"); - let mut login_url = "/admin/login".to_string(); + let mut login_url = "/depot/login".to_string(); if let Some(next) = creds.next { login_url = format!("{}?next={}", login_url, next); }; @@ -70,7 +70,7 @@ mod post { pub async fn logout(mut auth_session: AuthSession) -> impl IntoResponse { match auth_session.logout().await { - Ok(_) => Redirect::to("/admin/login").into_response(), + Ok(_) => Redirect::to("/depot/login").into_response(), Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), } } @@ -80,7 +80,7 @@ mod get { use super::*; use axum::extract::State; - pub async fn login( + pub async fn login( messages: Messages, admin: State, Query(NextUrl { next }): Query, @@ -90,7 +90,7 @@ mod get { messages: messages.into_iter().collect(), next, }; - templates.render_html("admin/login.html", context) + templates.render_html("depot/login.html", context) } } diff --git a/src/admin_examples/empty_repository.rs b/src/admin_examples/empty_repository.rs index 91e6eed..f0327c6 100644 --- a/src/admin_examples/empty_repository.rs +++ b/src/admin_examples/empty_repository.rs @@ -1,5 +1,5 @@ -use crate::admin::domain::*; use async_trait::async_trait; +use rear::depot::prelude::*; use serde_json::Value; struct Repository {} @@ -7,7 +7,7 @@ struct Repository {} impl Repository {} #[async_trait] -impl AdminRepository for Repository { +impl DepotRepository for Repository { type Key = i64; fn key_from_string(&self, s: String) -> Option { diff --git a/src/admin_examples/file_repository.rs b/src/admin_examples/file_repository.rs index 206dce9..a50bcc3 100644 --- a/src/admin_examples/file_repository.rs +++ b/src/admin_examples/file_repository.rs @@ -1,6 +1,5 @@ -use crate::admin::domain::*; use async_trait::async_trait; -use rear::admin::state::AdminRegistry; +use rear::depot::prelude::*; use serde_json::Value; use std::path::PathBuf; @@ -18,7 +17,7 @@ impl FileRepository { } #[async_trait] -impl AdminRepository for FileRepository { +impl DepotRepository for FileRepository { type Key = String; fn key_from_string(&self, s: String) -> Option { @@ -101,11 +100,11 @@ impl AdminRepository for FileRepository { } } -pub fn register(registry: &mut AdminRegistry, path: &str) { - let app_key = registry.register_app("Files"); +pub fn register(registry: &mut DepotRegistry, path: &str) { + let section_key = registry.register_section("Files"); let repo = FileRepository::new(path); - let model_config = AdminModelConfig { - app_key: app_key, + let model_config = DepotModelConfig { + section_key: section_key, name: "Files".to_owned(), }; let model_result = registry.register_model(model_config, repo); diff --git a/src/admin_examples/static_repository.rs b/src/admin_examples/static_repository.rs index d62b32e..02f8554 100644 --- a/src/admin_examples/static_repository.rs +++ b/src/admin_examples/static_repository.rs @@ -1,7 +1,6 @@ -use crate::admin::domain::*; -use crate::admin::state::AdminRegistry; +use crate::depot::prelude::*; use async_trait::async_trait; -use log::{debug, warn}; +use log::debug; use serde_json::{json, Value}; /// This is a showcase implementation with a static repository @@ -35,7 +34,7 @@ impl MyStaticRepository { } #[async_trait] -impl AdminRepository for MyStaticRepository { +impl DepotRepository for MyStaticRepository { type Key = i64; fn key_from_string(&self, s: String) -> Option { @@ -156,11 +155,11 @@ impl AdminRepository for MyStaticRepository { } } -pub fn register(registry: &mut AdminRegistry) { - let app_key = registry.register_app("Example App"); +pub fn register(registry: &mut DepotRegistry) { + let section_key = registry.register_section("Example App"); let repo = MyStaticRepository::new(); - let model_config = AdminModelConfig { - app_key: app_key, + let model_config = DepotModelConfig { + section_key: section_key, name: "ExampleModel".to_owned(), }; let model_result = registry.register_model(model_config, repo); diff --git a/src/main.rs b/src/main.rs index 01ee940..50ea6a0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,7 +23,7 @@ use axum_login::AuthManagerLayerBuilder; use axum_messages::MessagesManagerLayer; use dotenvy::dotenv; use log::info; -use rear::admin; +use rear::depot; use rear::service::{handlers, templates}; use std::env; use std::net::SocketAddr; @@ -53,7 +53,7 @@ async fn main() { // Prepare Application State Members let tmpl = templates::Templates::initialize().expect("Template Engine could not be loaded."); - let mut admin = admin::state::AdminRegistry::new("admin"); + let mut admin = depot::prelude::DepotRegistry::new("admin"); // Register Admin Apps static_repository::register(&mut admin); @@ -79,7 +79,7 @@ async fn main() { //.merge(admin_router) .nest( "/admin", - admin::routes() + depot::routes() .route_layer(login_required!( rear_auth::models::UserRepository, login_url = "/admin/login" diff --git a/src/state.rs b/src/state.rs index 40783db..ce6c9fe 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,12 +1,12 @@ use axum::extract::FromRef; -use rear::admin::state::{AdminState, SharedAdminRegistry}; +use rear::depot::state::{DepotState, SharedDepotRegistry}; use rear::service::templates; #[derive(Clone)] pub struct AppState { pub templates: templates::Templates, - pub admin: SharedAdminRegistry, + pub admin: SharedDepotRegistry, } impl FromRef for templates::Templates { @@ -15,12 +15,12 @@ impl FromRef for templates::Templates { } } -impl AdminState for AppState { +impl DepotState for AppState { fn get_templates(&self) -> &templates::Templates { &self.templates } - fn get_registry(&self) -> SharedAdminRegistry { + fn get_registry(&self) -> SharedDepotRegistry { self.admin.clone() } } diff --git a/static/admin/admin.css b/static/rear/depot.css similarity index 100% rename from static/admin/admin.css rename to static/rear/depot.css diff --git a/static/admin/teddy_bear.png b/static/rear/teddy_bear.png similarity index 100% rename from static/admin/teddy_bear.png rename to static/rear/teddy_bear.png diff --git a/templates/admin/index.html b/templates/admin/index.html deleted file mode 100644 index 3b398b5..0000000 --- a/templates/admin/index.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends "admin/base.jinja" %} - -{% block content %} -{% include "admin/dashboard.jinja" %} -

- Some text - another text - Third text -
-{% endblock %} \ No newline at end of file diff --git a/templates/admin/base.jinja b/templates/depot/base.jinja similarity index 82% rename from templates/admin/base.jinja rename to templates/depot/base.jinja index ae39cf8..011d0f4 100644 --- a/templates/admin/base.jinja +++ b/templates/depot/base.jinja @@ -6,7 +6,7 @@ {% include "fomantic.html" %} - + {% block extrastyle %}{% endblock %} {% block extrahead %}{% endblock %} @@ -42,14 +42,14 @@ {% endblock usertools %}
-
+
@@ -62,7 +62,7 @@ {% block sidebar %}