diff --git a/NOTES.md b/NOTES.md index ce5eadb..5b2e747 100644 --- a/NOTES.md +++ b/NOTES.md @@ -1,7 +1,14 @@ -# SeaORM +# NOTES +This is a heavily opinionated logbook I keep here until there is something usable. -## Some Rant about ORM Implementations +Please keep in mind, that I sometimes rant in writing, but learn later that I was wrong. + +It was not supposed to be read by anyone else than me. + +## SeaORM + +### Some Rant about ORM Implementations #### Entity, ActiveModel, Model @@ -43,7 +50,7 @@ It is better to just see your storage layer as a global database layer, and impl It's not like that is a bad thing, given this is also one of the downsides I witness in django projects, where models start to become swiss army knives around a domain topic. -## Accepting Fate +### Accepting My Fate #### Generating Entities from a Database @@ -135,3 +142,36 @@ As I started by quickly using a django admin template with CSS to jump start the - implement views for app - implement a static implementation for repository - eventually AdminRegistry needs to become an `Arc>`, if internal data needs to change *after* creating the main state. + +## CSS Library Brainhurt + +### Bootstrap + + Everybody says it is evil. I am not sure why yet. + +### Pico.css + + maybe it would be better to go for something simple? + +### UiKit + + in general really nice, but I got annoyed by uk- prefixes pretty fast. + It would however seem to align with hx- prefixes of htmx? + +### Tailwind/DaisyUI + + Tailwind supposed to make you uber designer. + + Strongly suggested by professionals like theo/t3, however I kind of started to see everything he says as an anti-pattern. Jonathan, you are rubbing off. + + I kind of want less "class"es not more. I understand why tailwind might be good. But then again, I don't get why I should decorate each tag with classes. + +### Semantic/Fomantic + + Semantic seems aligned with the idea of htmx and hyperscript a lot in it's philosophy of naming things. + It comes with jQuery basement, which is also a good recall to how a htmx site might work in the end. + + Not having access to the main repo since 2 years, and being developed by the community is both a red flag and a good sign: there is an active community, but hampered potential. + + I think it is important to go into form validation or interactive things quickly with htmx to properly sort out which to use. + diff --git a/TODOS.md b/TODOS.md new file mode 100644 index 0000000..0a84920 --- /dev/null +++ b/TODOS.md @@ -0,0 +1,11 @@ + + +# TODOs + + +## General + - [ ] we need the ability to have multiple template directories. + - [ ] develop a django-css independent theme for the admin. + - [ ] better 404 handling + - [ ] better 500 handling + \ No newline at end of file diff --git a/src/admin/domain.rs b/src/admin/domain.rs index 27861ee..86df10b 100644 --- a/src/admin/domain.rs +++ b/src/admin/domain.rs @@ -1,5 +1,7 @@ +pub use config::AdminModelConfig; pub use dto::AdminApp; pub use dto::AdminModel; +pub use repository::{AdminRepository, RepositoryList}; mod auth { @@ -12,6 +14,14 @@ mod auth { struct AdminActionLog {} } +mod config { + // user uses this configuration object to register another model. + pub struct AdminModelConfig { + pub name: String, + pub app_key: String, + } +} + mod dto { use serde::{Deserialize, Serialize}; @@ -26,12 +36,6 @@ mod dto { pub add_url: Option, } - impl AdminModel { - pub fn object_name(&self) -> &'static str { - "hi there" - } - } - #[derive(Deserialize, Serialize)] pub struct AdminApp { pub key: String, @@ -40,10 +44,59 @@ mod dto { pub app_url: String, pub models: Vec, } +} - impl AdminApp { - pub fn app_label(&self) -> &'static str { - "gogo" +pub mod repository { + use serde::{Serialize, Serializer}; + use serde_json::Value; + use std::vec::IntoIter; + + 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 = Value; + 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), + } + } + } + + pub trait AdminRepository: Send + Sync { + fn get_item(&self, id: usize) -> Option; + fn get_list(&self) -> RepositoryList; + } } diff --git a/src/admin/example.rs b/src/admin/example.rs index 48b8be0..b8e629d 100644 --- a/src/admin/example.rs +++ b/src/admin/example.rs @@ -1,6 +1,7 @@ // implementation of static repository -use super::state::{config::AdminModelConfig, AdminRegistry, AdminRepository}; +use super::domain::{AdminModelConfig, AdminRepository, RepositoryList}; +use super::state::AdminRegistry; use serde_json::{json, Value}; struct MyStaticRepository {} @@ -13,11 +14,13 @@ impl AdminRepository for MyStaticRepository { })) } - fn get_list(&self) -> Value { - json!([ - {"name": "Strange", "age": 150 }, - {"name": "Adam", "age": 12} - ]) + fn get_list(&self) -> RepositoryList { + RepositoryList::List { + values: vec![ + json!({"name": "Strange", "age": 150 }), + json!({"name": "Adam", "age": 12}), + ], + } } } diff --git a/src/admin/state.rs b/src/admin/state.rs index 5f5a752..8d52cbf 100644 --- a/src/admin/state.rs +++ b/src/admin/state.rs @@ -1,16 +1,9 @@ -use crate::admin::domain::{AdminApp, AdminModel}; -use axum::Router; -use serde_json::Value; +use super::domain::{AdminApp, AdminModel, AdminModelConfig, AdminRepository}; use std::collections::HashMap; use std::sync::Arc; pub type AdminState = Arc; -pub trait AdminRepository: Send + Sync { - fn get_item(&self, id: usize) -> Option; - fn get_list(&self) -> Value; -} - // main registry. pub struct AdminRegistry { base_path: String, @@ -41,7 +34,7 @@ impl AdminRegistry { AdminApp { name: key.to_owned(), key: node.name.to_owned(), - app_url: key.to_owned(), + app_url: format!("/{}/app/{}", self.base_path, key.to_owned()), models: my_models, } } @@ -67,7 +60,7 @@ impl AdminRegistry { key: internal_model.model_key.clone(), name: internal_model.name.clone(), admin_url: format!( - "{}/app/{}/model/{}", + "/{}/app/{}/model/{}", self.base_path, internal_model.app_key, internal_model.model_key ), view_only: false, @@ -91,7 +84,7 @@ impl AdminRegistry { Some(self.model_from_internal(internal_model)) } - fn register_model_config(&mut self, model: config::AdminModelConfig) -> Result { + fn register_model_config(&mut self, model: AdminModelConfig) -> Result { let local_config = internal::AdminModel::from(model); if local_config.model_key.is_empty() { return Err("No model name".to_owned()); @@ -107,7 +100,7 @@ impl AdminRegistry { pub fn register_model( &mut self, - model: config::AdminModelConfig, + model: AdminModelConfig, repository: R, ) -> Result<(), String> { let model_key = self.register_model_config(model)?; @@ -124,15 +117,10 @@ impl AdminRegistry { } } -pub mod config { - // user uses this configuration object to register another model. - pub struct AdminModelConfig { - pub name: String, - pub app_key: String, - } -} - mod internal { + // how the registry saves data internally. + + use super::super::domain::AdminModelConfig; #[derive(Clone)] pub struct AdminApp { pub key: String, @@ -146,8 +134,8 @@ mod internal { pub name: String, } - impl From for AdminModel { - fn from(value: super::config::AdminModelConfig) -> Self { + impl From for AdminModel { + fn from(value: AdminModelConfig) -> Self { AdminModel { app_key: value.app_key, model_key: slug::slugify(value.name.clone()), diff --git a/src/admin/views.rs b/src/admin/views.rs index 5a5e227..919ff56 100644 --- a/src/admin/views.rs +++ b/src/admin/views.rs @@ -10,6 +10,8 @@ use crate::service::templates; use serde::{Deserialize, Serialize}; use serde_json::Value; +use super::domain::RepositoryList; + #[derive(Deserialize)] pub struct Question { question: String, @@ -26,11 +28,12 @@ pub struct AdminRequest { pub path: String, } -#[derive(Deserialize, Serialize)] +#[derive(Serialize)] pub struct AdminContext { pub language_code: Option, pub language_bidi: Option, pub user: Option, // Todo: user type + pub admin_url: String, pub site_url: Option, pub docsroot: Option, pub messages: Vec, // Todo: message type @@ -40,7 +43,7 @@ pub struct AdminContext { pub request: AdminRequest, pub available_apps: Vec, - pub item_list: Option, + pub item_list: RepositoryList, pub item: Option, } @@ -50,6 +53,7 @@ impl Default for AdminContext { 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 + admin_url: "/admin".to_owned(), site_url: None, docsroot: None, messages: Vec::new(), // Empty vector for messages @@ -60,7 +64,7 @@ impl Default for AdminContext { request: AdminRequest { path: "".to_owned(), }, - item_list: None, + item_list: RepositoryList::Empty, item: None, } } @@ -88,7 +92,7 @@ pub async fn list_app( templates: State, Path(app_key): Path, ) -> impl IntoResponse { - templates.render_html("admin/app_list.html", ()) + templates.render_html("admin/app_list.jinja", ()) } // List Items renders the entire list item page. @@ -101,15 +105,17 @@ pub async fn list_item_collection( let context = if let Ok(repo) = registry.get_repository(&model_key) { // we should consider using Vec instead in get_list. AdminContext { - item_list: Some(repo.get_list()), + available_apps: registry.get_apps(), + item_list: repo.get_list(), ..Default::default() } } else { AdminContext { + available_apps: registry.get_apps(), ..Default::default() } }; - templates.render_html("admin/items/item_list.html", context) + templates.render_html("admin/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. @@ -125,7 +131,7 @@ pub async fn item_details( templates: State, Path((app_key, model_key, id)): Path<(String, String, String)>, ) -> impl IntoResponse { - templates.render_html("admin/items/item_detail.html", ()) + templates.render_html("admin/items/item_detail.jinja", ()) } // Item Action allows running an action on one single dataset. diff --git a/src/main.rs b/src/main.rs index be5db81..c31452f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,7 @@ use axum::{ use dotenvy::dotenv; use log::info; use std::env; -use std::net::{Ipv4Addr, SocketAddr}; +use std::net::SocketAddr; use std::sync::Arc; async fn home(templates: State) -> impl IntoResponse { @@ -60,7 +60,7 @@ async fn main() { .unwrap_or("3000".to_string()) .parse() .expect("Port expected in APP_PORT"); - // the listen_addr is the address we bind to. + // the listen_addr is the address we bind to. This might be multiple domains, like 0.0.0.0 let listen_addr = SocketAddr::from((app_host, app_port)); // the server addr is a concrete address the user can connect to. let server_addr = if app_host.is_unspecified() { diff --git a/src/service/templates.rs b/src/service/templates.rs index 7994b11..db16069 100644 --- a/src/service/templates.rs +++ b/src/service/templates.rs @@ -16,6 +16,7 @@ impl Templates { let mut environment = Environment::new(); let template_path = "templates"; environment.set_loader(path_loader(&template_path)); + environment.add_filter("none", none); environment.add_filter("markdown", markdown); environment.add_filter("yesno", filter_yesno); environment.add_function("static", tpl_static); @@ -78,9 +79,17 @@ fn tpl_translate(value: String) -> Value { } fn tpl_to_be_implemented(value: String) -> Value { - Value::from_safe_string("".into()) + Value::from_safe_string(" -
- - {% if not is_popup %} - - {% block header %} - - {% endblock %} - - {% block nav_breadcrumbs %} - - {% endblock %} - {% endif %} - -
- {% if not is_popup and is_nav_sidebar_enabled %} - {% block nav_sidebar %} - {% include "admin/nav_sidebar.html" %} - {% endblock %} - {% endif %} -
- {% block messages %} - {% if messages %} -
    {% for message in messages %} - {{ message|capfirst }} - {% endfor %}
- {% endif %} - {% endblock messages %} - -
- {% block pretitle %}{% endblock %} - {% block content_title %}{% if title %}

{{ title }}

{% endif %}{% endblock %} - {% block content_subtitle %}{% if subtitle %}

{{ subtitle }}

{% endif %}{% endblock %} - {% block content %} - {% block object_tools %}{% endblock %} - {{ content }} - {% endblock %} - {% block sidebar %}{% endblock %} -
-
- - {% block footer %}{% endblock %} -
-
-
- - - - - - - - - - - \ No newline at end of file diff --git a/templates/admin/base.jinja b/templates/admin/base.jinja new file mode 100644 index 0000000..9250926 --- /dev/null +++ b/templates/admin/base.jinja @@ -0,0 +1,161 @@ + +{% set is_nav_sidebar_enabled = true %} + + + + {% block title %}{{ title|none('Admiral') }}{% endblock %} + + + + + + + + + + {% block dark_mode_vars %}{% endblock %} + {% block extrastyle %}{% endblock %} + {% block extrahead %}{% endblock %} + + {% block responsive %} + + {% endblock %} + + {% block blockbots %} + + {% endblock %} + + + + {% block body %} + + {% block header %} + + {% endblock %} + + {% block sidebar %} + + {% endblock sidebar %} + +
+
+ {% block content %} + {% endblock content %} +
+
+ + {% endblock body %} + {% block bodyjs %} + + + {% endblock bodyjs %} + + + \ No newline at end of file diff --git a/templates/admin/dashboard.jinja b/templates/admin/dashboard.jinja new file mode 100644 index 0000000..9f798e4 --- /dev/null +++ b/templates/admin/dashboard.jinja @@ -0,0 +1,13 @@ +
+
+
+
+

Hello World!

+

This is the example placeholder that should become the welcoming dashboard for the admin. + It should show various statistics and also a detailed overview of all your models. + +

+
+
+
+
\ No newline at end of file diff --git a/templates/admin/django_base.html b/templates/admin/django_base.html new file mode 100644 index 0000000..453e5ea --- /dev/null +++ b/templates/admin/django_base.html @@ -0,0 +1,133 @@ + +{% set is_nav_sidebar_enabled = true %} + + + + {% block title %}{% endblock %} + + {% block dark_mode_vars %}{% endblock %} + {% block extrastyle %}{% endblock %} + {% block extrahead %}{% endblock %} + + {% block responsive %} + + {% endblock %} + + {% block blockbots %} + + {% endblock %} + + + + {{ translate('Skip to main content') }} + +
+ + {% if not is_popup %} + + {% block header %} + + {% endblock %} + + {% block nav_breadcrumbs %} + + {% endblock %} + {% endif %} + +
+ {% if not is_popup and is_nav_sidebar_enabled %} + {% block nav_sidebar %} + {% include "admin/nav_sidebar.html" %} + {% endblock %} + {% endif %} +
+ {% block messages %} + {% if messages %} +
    {% for message in messages %} + {{ message|capfirst }} + {% endfor %} +
+ {% endif %} + {% endblock messages %} + +
+ {% block pretitle %}{% endblock %} + {% block content_title %}{% if title %}

{{ title }}

{% endif %}{% endblock %} + {% block content_subtitle %}{% if subtitle %}

{{ subtitle }}

{% endif %}{% endblock %} + {% block content %} + {% block object_tools %}{% endblock %} + {{ content }} + {% endblock %} + {% block sidebar %}{% endblock %} +
+
+ + {% block footer %}{% endblock %} +
+
+
+ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/templates/admin/index.html b/templates/admin/index.html index edc0044..9590228 100644 --- a/templates/admin/index.html +++ b/templates/admin/index.html @@ -1 +1,5 @@ -{% extends "admin/base.html" %} +{% extends "admin/base.jinja" %} + +{% block content %} +{% include "admin/dashboard.jinja" %} +{% endblock %} \ No newline at end of file diff --git a/templates/admin/items/item_detail.html b/templates/admin/items/item_detail.jinja similarity index 100% rename from templates/admin/items/item_detail.html rename to templates/admin/items/item_detail.jinja diff --git a/templates/admin/items/item_list.html b/templates/admin/items/item_list.html deleted file mode 100644 index 92c5e7e..0000000 --- a/templates/admin/items/item_list.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends "admin/base.html" %} - -{% block content %} -{% if item_list %} -
    - {% for item in item_list %} -
  • {{ item.name }}
  • - {% endfor %} -
-{% else %} - No Items found. -{% endif %} -{% endblock content %} \ No newline at end of file diff --git a/templates/admin/items/item_list.jinja b/templates/admin/items/item_list.jinja new file mode 100644 index 0000000..4572249 --- /dev/null +++ b/templates/admin/items/item_list.jinja @@ -0,0 +1,47 @@ +{% extends "admin/base.jinja" %} + +{% block content %} + +{% block search_bar %} +
+ + +
+{% endblock %} + +{% if item_list %} +
+ + + + + + + + + + + + {% for item in item_list %} + + + + + + {% endfor %} + + + +
Table Caption
Header1Header2Header3
{{ item.name}}{{ item.age }}{{ item }}
+
+{% set x="name" %} + + + +
  • {{ item[x] }}
  • + + +{% else %} +No Items found. +{% endif %} +{% endblock content %} \ No newline at end of file diff --git a/templates/admin/nav_sidebar.html b/templates/admin/nav_sidebar.html deleted file mode 100644 index ab8641b..0000000 --- a/templates/admin/nav_sidebar.html +++ /dev/null @@ -1,9 +0,0 @@ - - \ No newline at end of file