diff --git a/Cargo.lock b/Cargo.lock index 6130519..296b15e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -306,7 +306,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", ] [[package]] @@ -317,13 +317,13 @@ checksum = "e1d90cd0b264dfdd8eb5bad0a2c217c1f88fa96a8573f40e7b12de23fb468f46" [[package]] name = "async-trait" -version = "0.1.73" +version = "0.1.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", ] [[package]] @@ -574,7 +574,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", "syn_derive", ] @@ -693,7 +693,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", ] [[package]] @@ -1572,7 +1572,7 @@ checksum = "ce243b1bfa62ffc028f1cc3b6034ec63d649f3031bc8a4fbbb004e1ac17d1f68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", ] [[package]] @@ -1825,6 +1825,7 @@ name = "miniweb" version = "0.1.0" dependencies = [ "anyhow", + "async-trait", "axum 0.7.4", "barrel", "derive_builder", @@ -2032,7 +2033,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", ] [[package]] @@ -2083,7 +2084,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", ] [[package]] @@ -2153,7 +2154,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", ] [[package]] @@ -2284,9 +2285,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.67" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] @@ -2325,9 +2326,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -2510,7 +2511,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.37", + "syn 2.0.48", "walkdir", ] @@ -2620,7 +2621,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", ] [[package]] @@ -2678,7 +2679,7 @@ dependencies = [ "proc-macro2", "quote", "sea-bae", - "syn 2.0.37", + "syn 2.0.48", "unicode-ident", ] @@ -2742,7 +2743,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", "thiserror", ] @@ -2821,7 +2822,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", ] [[package]] @@ -3234,7 +3235,7 @@ version = "0.1.0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", ] [[package]] @@ -3268,9 +3269,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.37" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -3286,7 +3287,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", ] [[package]] @@ -3331,7 +3332,7 @@ checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", ] [[package]] @@ -3415,7 +3416,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", ] [[package]] @@ -3509,7 +3510,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", ] [[package]] @@ -3698,7 +3699,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", "wasm-bindgen-shared", ] @@ -3732,7 +3733,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4032,7 +4033,7 @@ checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 665ed2f..45ed198 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,3 +51,4 @@ env_logger = "0.11" serde_json = "1.0.108" slug = "0.1.5" derive_builder = "0.13.0" +async-trait = "0.1.77" diff --git a/Justfile b/Justfile index ebd95fc..7c8481e 100644 --- a/Justfile +++ b/Justfile @@ -4,6 +4,9 @@ default: @echo "# Miniweb Project" @just --list -u +build: + @cargo build + # Run the project (just run), or commands with --bin (just run ) run args='miniweb': @cargo run --bin {{args}} diff --git a/NOTES.md b/NOTES.md index 5b2e747..8d21adf 100644 --- a/NOTES.md +++ b/NOTES.md @@ -175,3 +175,64 @@ As I started by quickly using a django admin template with CSS to jump start the I think it is important to go into form validation or interactive things quickly with htmx to properly sort out which to use. +### Builder Pattern etc. + + // consider builder: https://docs.rs/derive_builder/latest/derive_builder/ + // downside of builders as macro: no intellisense! + // each repository has to implement a repo info. + //#[derive(Builder)] + //#[builder(setter(into))] + +#### static initializer + +downside of a pub &'static str initializer struct is the rule to avoid any nested types that have any kind of building pattern, because you cannot properly move the data easily, especially if you want to move into enums, like an Option. + +otherwise, self mutating building patterns can stay readonly either with the Copy trait and using self: + +With "Copy": + fn self_mutating(mut self, value) -> Self { + self.value = value; + self + } + +Without Copy: + fn self_mutating_manual(self, value) -> Self { + Struct { + value: value, + ..self + } + } + + +However also works, but seems dirty: + fn self_mutating_instance_mut(&mut self, value) -> Self { + self.value = value; + *self + } + + +#### Cow + +Other remarks: also useful is the usage of Cow, if you want to create internal objects that actuall reuse static data or take dynamic data. + +use std::borrow::Cow; + +#[derive(Debug, Serialize, Clone)] +pub struct Field { + pub widget: Cow<'static, str>, + pub label: Option>, +} + +let static_field = Field::widget(Cow::Borrowed("/admin/widgets/input_text.jinja")); +let dynamic_label = String::from("Dynamic Label"); +let dynamic_field = Field::widget(Cow::Owned(dynamic_label)); + +### Write article about "did you read the fineprint" + + - The bleeding of lifetimes. + - 'static + - Send + - "object safe" + - the async dynamic dispatch. + - the building pattern debacle here + \ No newline at end of file diff --git a/TODOS.md b/TODOS.md index 8f6552d..8cee66c 100644 --- a/TODOS.md +++ b/TODOS.md @@ -33,3 +33,9 @@ auth: - auth middleware. - message the user. -> messaging system + + +## Widgets + +In the widget section, we need to add more abilities to adjust attributes of html elements directly. +Probably options should be transmitted via JSON data. diff --git a/src/admin/domain.rs b/src/admin/domain.rs index f2b3ff9..826662a 100644 --- a/src/admin/domain.rs +++ b/src/admin/domain.rs @@ -2,8 +2,8 @@ pub use config::AdminModelConfig; pub use dto::AdminApp; pub use dto::AdminModel; pub use repository::{ - AdminRepository, LookupKey, RepoInfo, RepositoryInfo, RepositoryInfoBuilder, RepositoryItem, - RepositoryList, + AdminRepository, LookupKey, RepoInfo, RepositoryContext, RepositoryInfo, RepositoryItem, + RepositoryList, Widget, }; mod auth { @@ -50,11 +50,126 @@ mod dto { } pub mod repository { - use super::dto::AdminModel; + use super::dto::{AdminApp, AdminModel}; + use async_trait::async_trait; use derive_builder::Builder; use serde::{Serialize, Serializer}; use serde_json::Value; - use std::vec::IntoIter; + use std::{collections::HashMap, vec::IntoIter}; + + pub type RepositoryContext = AdminModel; + + impl RepositoryContext { + pub fn build_item(&self, key: &str, fields: Value) -> RepositoryItem { + RepositoryItem { + detail_url: Some(format!("{}/detail/{}", self.admin_url, key)), + fields: fields, + } + } + } + + /// 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 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 + } + } + + #[derive(Debug, Serialize)] + 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(PartialEq)] pub enum LookupKey { @@ -156,16 +271,18 @@ pub mod repository { pub fields: &'static [&'static str], } - // consider builder: https://docs.rs/derive_builder/latest/derive_builder/ - // downside of builders as macro: no intellisense! - // each repository has to implement a repo info. - #[derive(Serialize, Builder)] - #[builder(setter(into))] + impl RepoInfo { + pub fn build(self) -> RepositoryInfo { + self.into() + } + } + + #[derive(Serialize)] pub struct RepositoryInfo { name: String, lookup_key: String, display_list: Vec, - fields: Vec, + fields: Vec<(String, Field)>, } impl RepositoryInfo { @@ -178,12 +295,32 @@ pub mod repository { } } - /* // self mutating builder pattern? - pub fn display_list(mut self, display_list: &[&str]) -> RepositoryInfo { + // 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 } - */ + + // set field for a given key. + pub fn set_widget(mut self, name: &str, widget: Widget) -> Self { + // 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) => { + // If found, replace the `Field` part of the tuple + self.fields[index].1 = Field::from(widget); + } + None => { + // If not found, add a new entry + self.fields.push((name.to_owned(), Field::from(widget))); + } + } + self + } } impl From for RepositoryInfo { @@ -196,22 +333,31 @@ pub mod repository { .iter() .map(|&s| s.to_string()) .collect(), - fields: repo_info.fields.iter().map(|&s| s.to_string()).collect(), + fields: repo_info + .fields + .iter() + .map(|x| (x.to_string(), Field::from(Widget::default()))) + .collect(), } } } + #[async_trait] pub trait AdminRepository: Send + Sync { - fn info(&self, model: &AdminModel) -> RepositoryInfo; - fn list(&self, model: &AdminModel) -> RepositoryList; - fn get(&self, model: &AdminModel, id: LookupKey) -> Option; - fn create(&mut self, model: &AdminModel, data: Value) -> Option; - fn update( + async fn info(&self, context: &RepositoryContext) -> RepositoryInfo; + async fn list(&self, context: &RepositoryContext) -> RepositoryList; + async fn get(&self, context: &RepositoryContext, id: LookupKey) -> Option; + async fn create( &mut self, - model: &AdminModel, + context: &RepositoryContext, + data: Value, + ) -> Option; + async fn update( + &mut self, + context: &RepositoryContext, id: LookupKey, data: Value, ) -> Option; - fn delete(&mut self, model: &AdminModel, id: LookupKey) -> Option; + async fn delete(&mut self, context: &RepositoryContext, id: LookupKey) -> Option; } } diff --git a/src/admin/views.rs b/src/admin/views.rs index 2e4a970..6b8b6bc 100644 --- a/src/admin/views.rs +++ b/src/admin/views.rs @@ -70,7 +70,6 @@ impl Default for AdminContext { pub fn base_template(headers: &HeaderMap) -> Option { let hx_request = headers.get("HX-Request").is_some(); if hx_request { - println!("HX."); Some("admin/base_hx.jinja".to_string()) } else { None @@ -123,8 +122,8 @@ pub async fn list_item_collection( AdminContext { base: base_template(&headers), available_apps: registry.get_apps(), - item_info: Some(repo.info(&admin_model)), - item_list: repo.list(&admin_model), + item_info: Some(repo.info(&admin_model).await), + item_list: repo.list(&admin_model).await, item_model: Some(admin_model), ..Default::default() } @@ -163,9 +162,9 @@ pub async fn item_details( AdminContext { base: base_template(&headers), available_apps: registry.get_apps(), - item_info: Some(repo.info(&admin_model)), - item_list: repo.list(&admin_model), - item: repo.get(&admin_model, key), + item_info: Some(repo.info(&admin_model).await), + item_list: repo.list(&admin_model).await, + item: repo.get(&admin_model, key).await, item_model: Some(admin_model), ..Default::default() } @@ -193,8 +192,8 @@ pub async fn new_item( AdminContext { base: base_template(&headers), available_apps: registry.get_apps(), - item_info: Some(repo.info(&admin_model)), - item_list: repo.list(&admin_model), + item_info: Some(repo.info(&admin_model).await), + item_list: repo.list(&admin_model).await, item_model: Some(admin_model), ..Default::default() } @@ -222,14 +221,14 @@ pub async fn create_item( .expect("Admin Model not found?"); // create our item. - let result = repo.create(&admin_model, form); + let result = repo.create(&admin_model, form).await; // TODO: refactor run over these views, way too much repetition. AdminContext { base: base_template(&headers), available_apps: registry.get_apps(), - item_info: Some(repo.info(&admin_model)), - item_list: repo.list(&admin_model), + item_info: Some(repo.info(&admin_model).await), + item_list: repo.list(&admin_model).await, item_model: Some(admin_model), item: result, ..Default::default() diff --git a/src/admin_examples/empty_repository.rs b/src/admin_examples/empty_repository.rs new file mode 100644 index 0000000..0aeb9be --- /dev/null +++ b/src/admin_examples/empty_repository.rs @@ -0,0 +1,51 @@ +use crate::admin::domain::*; +use crate::admin::state::AdminRegistry; +use async_trait::async_trait; +use log::{debug, warn}; +use serde_json::{json, Value}; + +struct Repository {} + +impl Repository {} + +#[async_trait] +impl AdminRepository for Repository { + async fn info(&self, _: &RepositoryContext) -> RepositoryInfo { + RepoInfo { + name: "My Empty Repository", + lookup_key: "id", + display_list: &["id"], + fields: &[], + } + .into() + } + + async fn get(&self, model: &RepositoryContext, id: LookupKey) -> Option { + None + } + + async fn list(&self, model: &RepositoryContext) -> RepositoryList { + RepositoryList::Empty + } + + async fn create( + &mut self, + model: &RepositoryContext, + mut data: Value, + ) -> Option { + None + } + + async fn update( + &mut self, + model: &RepositoryContext, + id: LookupKey, + data: Value, + ) -> Option { + None + } + + async fn delete(&mut self, _: &RepositoryContext, id: LookupKey) -> Option { + None + } +} diff --git a/src/admin_examples/mod.rs b/src/admin_examples/mod.rs index 33a2995..7dd0ecf 100644 --- a/src/admin_examples/mod.rs +++ b/src/admin_examples/mod.rs @@ -1 +1,3 @@ +pub mod empty_repository; pub mod static_repository; +pub mod user_repository; diff --git a/src/admin_examples/static_repository.rs b/src/admin_examples/static_repository.rs index 58ebfc1..9613d7c 100644 --- a/src/admin_examples/static_repository.rs +++ b/src/admin_examples/static_repository.rs @@ -1,10 +1,11 @@ -// implementation of static repository - use crate::admin::domain::*; use crate::admin::state::AdminRegistry; +use async_trait::async_trait; use log::{debug, warn}; use serde_json::{json, Value}; +/// This is a showcase implementation with a static repository +/// It uses a Vec to store it's data. struct MyStaticRepository { /// In Memory Storage for Example. content: Vec, @@ -33,53 +34,47 @@ impl MyStaticRepository { } } +#[async_trait] impl AdminRepository for MyStaticRepository { - fn get(&self, model: &AdminModel, id: LookupKey) -> Option { - if let LookupKey::Integer(id) = id { - let item = self.content.get(id - 1).cloned().unwrap(); - Some(RepositoryItem { - detail_url: Some(format!( - "{}/detail/{}", - model.admin_url, - item.get("id").unwrap() - )), - fields: item, - }) - } else { - warn!("Got non-integer lookup key: {}", id); - None - } - } - - fn list(&self, model: &AdminModel) -> RepositoryList { - RepositoryList::List { - values: self - .content - .clone() - .into_iter() - .map(|item| RepositoryItem { - detail_url: Some(format!( - "{}/detail/{}", - model.admin_url, - item.get("id").unwrap() - )), - fields: item, - }) - .collect(), - } - } - - fn info(&self, _model: &AdminModel) -> RepositoryInfo { + async fn info(&self, _: &RepositoryContext) -> RepositoryInfo { RepoInfo { name: "My Static Repository", lookup_key: "id", display_list: &["id", "name", "level", "age"], fields: &["name", "level", "age", "powers"], } - .into() + .build() + .set_widget("age", Widget::default().as_password().labeled("hi there.")) + .set_widget("name", Widget::textarea().options(&[("disabled", "true")])) } - fn create(&mut self, model: &AdminModel, mut data: Value) -> Option { + async fn get(&self, model: &RepositoryContext, id: LookupKey) -> Option { + if let LookupKey::Integer(id) = id { + let item = self.content.get(id - 1).cloned().unwrap(); + let id = item.get("id").unwrap(); + Some(model.build_item(&*id.to_string(), item)) + } else { + warn!("Got non-integer lookup key: {}", id); + None + } + } + + async fn list(&self, model: &RepositoryContext) -> RepositoryList { + RepositoryList::List { + values: self + .content + .clone() + .into_iter() + .map(|item| model.build_item(&*item.get("id").unwrap().to_string(), item)) + .collect(), + } + } + + async fn create( + &mut self, + model: &RepositoryContext, + mut data: Value, + ) -> Option { debug!("Asked to create: {}", data); let new_id = self.next_id; @@ -93,13 +88,15 @@ impl AdminRepository for MyStaticRepository { self.content.push(data.clone()); // Return the newly created item - Some(RepositoryItem { - detail_url: Some(format!("{}/detail/{}", model.admin_url, new_id)), - fields: data, - }) + Some(model.build_item(&*new_id.to_string(), data)) } - fn update(&mut self, model: &AdminModel, id: LookupKey, data: Value) -> Option { + async fn update( + &mut self, + model: &RepositoryContext, + id: LookupKey, + data: Value, + ) -> Option { debug!("I would now update: {}, {}", id, data); // First, find the index of the item to update @@ -130,7 +127,7 @@ impl AdminRepository for MyStaticRepository { None } - fn delete(&mut self, _model: &AdminModel, id: LookupKey) -> Option { + async fn delete(&mut self, _: &RepositoryContext, id: LookupKey) -> Option { debug!("Would delete: {}", id); let item_index = self.content.iter().position(|item| { diff --git a/src/admin_examples/user_repository.rs b/src/admin_examples/user_repository.rs new file mode 100644 index 0000000..738aff7 --- /dev/null +++ b/src/admin_examples/user_repository.rs @@ -0,0 +1,52 @@ +use crate::admin::domain::AdminRepository; +use crate::admin::domain::*; +use crate::admin::state::AdminRegistry; +use async_trait::async_trait; +use log::{debug, warn}; +use serde_json::{json, Value}; + +struct UserRepository {} + +impl UserRepository {} + +#[async_trait] +impl AdminRepository for UserRepository { + async fn info(&self, _: &RepositoryContext) -> RepositoryInfo { + RepoInfo { + name: "User", + lookup_key: "id", + display_list: &["id"], + fields: &[], + } + .into() + } + + async fn get(&self, model: &RepositoryContext, id: LookupKey) -> Option { + None + } + + async fn list(&self, model: &RepositoryContext) -> RepositoryList { + RepositoryList::Empty + } + + async fn create( + &mut self, + model: &RepositoryContext, + mut data: Value, + ) -> Option { + None + } + + async fn update( + &mut self, + model: &RepositoryContext, + id: LookupKey, + data: Value, + ) -> Option { + None + } + + async fn delete(&mut self, _: &RepositoryContext, id: LookupKey) -> Option { + None + } +} diff --git a/templates/admin/django_base.html b/templates/admin/django_base.html index 453e5ea..72f073f 100644 --- a/templates/admin/django_base.html +++ b/templates/admin/django_base.html @@ -35,12 +35,12 @@ {% if has_permission %}
{% block welcome_msg %} - {{ translate('Welcome,') }} + {{ translate('Welcome,') }} {{ user.get_short_name or user.get_username }}. {% endblock %} {% block userlinks %} {% if site_url %} - {{ translate('View site') }} / + {{ translate('View site') }} / {% endif %} {% if user.is_active and user.is_staff %} {% set docsroot = url('django-admindocs-docroot') %} @@ -70,6 +70,12 @@ {% endblock %} @@ -79,7 +85,7 @@
{% if not is_popup and is_nav_sidebar_enabled %} {% block nav_sidebar %} - {% include "admin/nav_sidebar.html" %} + {% include "admin/nav_sidebar.html" %} {% endblock %} {% endif %}
diff --git a/templates/admin/index.html b/templates/admin/index.html index 9590228..3b398b5 100644 --- a/templates/admin/index.html +++ b/templates/admin/index.html @@ -2,4 +2,9 @@ {% block content %} {% include "admin/dashboard.jinja" %} +
+ Some text + another text + Third text +
{% endblock %} \ No newline at end of file diff --git a/templates/admin/items/item_create.jinja b/templates/admin/items/item_create.jinja index 101a5bb..af1a4af 100644 --- a/templates/admin/items/item_create.jinja +++ b/templates/admin/items/item_create.jinja @@ -1,26 +1,20 @@ {% extends base|none("admin/base.jinja") %} -{% macro input(name, value="", type="text") -%} - -{%- endmacro %} - {% block content %} +{% from "/admin/items/items.jinja" import field_widget %}

Create {{item_model.name}} in {{item_info.name}}

+ {% set fields = item_info.fields %} - {% for field in fields %} - {% if item %} - {% set field_value = item.fields[field]|none("") %} - {% else %} - {% set field_value = "" %} - {% endif %} - -
- - {{ input(field, field_value) }} -
+ {% for field_name, field_defs in fields %} + {% if item %} + {% set field_value = item.fields[field_name]|none("") %} + {% else %} + {% set field_value = "" %} + {% endif %} + {{ field_widget(field_name, field_defs, field_value) }} {% endfor %}
diff --git a/templates/admin/items/item_detail.jinja b/templates/admin/items/item_detail.jinja index 149af85..bd273cd 100644 --- a/templates/admin/items/item_detail.jinja +++ b/templates/admin/items/item_detail.jinja @@ -3,9 +3,9 @@ {% block content %} {% if item %} -{{item.fields}} + {{item.fields}} {% else %} -No Item found. + No Item found. {% endif %} {% endblock content %} \ No newline at end of file diff --git a/templates/admin/items/items.jinja b/templates/admin/items/items.jinja new file mode 100644 index 0000000..0544d70 --- /dev/null +++ b/templates/admin/items/items.jinja @@ -0,0 +1,15 @@ + +{% macro field_widget(name, definitions, value) -%} +{% set field = { + 'type': definitions.type, + 'name': name, + 'value': value, + 'placeholder': definitions.placeholder, + 'label': definitions.label, + 'readonly': definitions.readonly, + 'required': definitions.required, + 'options': definitions.options, +} +%} +{% include definitions.widget %} +{%- endmacro %} \ No newline at end of file diff --git a/templates/admin/widgets/input_text.jinja b/templates/admin/widgets/input_text.jinja new file mode 100644 index 0000000..410d332 --- /dev/null +++ b/templates/admin/widgets/input_text.jinja @@ -0,0 +1,4 @@ +
+ + +
\ No newline at end of file diff --git a/templates/admin/widgets/input_textarea.jinja b/templates/admin/widgets/input_textarea.jinja new file mode 100644 index 0000000..334e99a --- /dev/null +++ b/templates/admin/widgets/input_textarea.jinja @@ -0,0 +1,12 @@ +
+ + + +
\ No newline at end of file