refactor: trying to align my naming into something specific. removing dynamic routing code.

This commit is contained in:
Gabor Körber 2024-01-03 14:50:00 +01:00
parent fb72a5deab
commit eef65cfdd0
7 changed files with 98 additions and 167 deletions

View File

@ -96,3 +96,37 @@ impl AdminRepository for UserRepository {
} }
``` ```
## Finding Names
Ultimately the goal will be to rename even "app" and "model" as concepts later on - maybe.
As I started by quickly using a django admin template with CSS to jump start the admin interface, with the hope to create something that is very much also compatible with existing django admin templates in projects, I ran quickly into naming maelstrom.
#### Django-Admin:
| key | description |
| --- | ----------- |
| app_label | This refers to the name of the Django application that contains the model. It's usually set to the name of the directory that holds the app. In Django's admin interface and internal workings, app_label is used to distinguish models from different apps that might have the same model name. |
| model_name | This is the name of the model class in lowercase. Django uses model_name as a convenient way to refer to models, particularly in URL patterns and in the database layer, where the table name is typically derived from the app_label and model_name. |
| object_name | object_name is the name of the model class itself, typically starting with a capital letter (following Python's class naming conventions). This is more often used in Django's internals and templates. |
| verbose_name | This is a human-readable name for the model, which can be set in the model's Meta class. If not set explicitly, Django generates it automatically by converting the object_name from CamelCase to space-separated words. verbose_name is used in the Django admin interface and any place where a more readable model name is needed. |
| verbose_name_plural | Similar to verbose_name, this is the plural form of the human-readable name of the model. It's used in the Django admin interface where a plural form of the model's name is appropriate, like in list views. |
#### miniweb-admin:
| key | description |
| --- | ----------- |
| app_key | refers to the lowercase slug representation of the app, which might be represented by an Arc<str> in the future |
| model_key | refers to the lowercase slug representation of the model, which might be represented by an Arc<str> in the future |
| app_name | represents the human readable representation of the app |
| model_name | represents the human readable representation of the model |
#### Replacing:
| django-admin | miniweb-admin | explanation |
| ------------ | ------------- | ----------- |
| object_name\|lower | key, app_key, model_key | |
| app_label | key, app_key | |

View File

@ -17,19 +17,33 @@ mod dto {
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub struct AdminModel { pub struct AdminModel {
pub key: String,
pub name: String, pub name: String,
pub object_name: String,
pub admin_url: String, pub admin_url: String,
pub view_only: bool, pub view_only: bool,
pub add_url: Option<String>, pub add_url: Option<String>,
} }
impl AdminModel {
pub fn object_name(&self) -> &'static str {
"hi there"
}
}
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub struct AdminApp { pub struct AdminApp {
pub key: String,
pub name: String, pub name: String,
pub app_label: String,
pub app_url: String, pub app_url: String,
pub models: Vec<AdminModel>, pub models: Vec<AdminModel>,
} }
impl AdminApp {
pub fn app_label(&self) -> &'static str {
"gogo"
}
}
} }

View File

@ -9,13 +9,13 @@ pub mod views;
pub fn routes() -> Router<AppState> { pub fn routes() -> Router<AppState> {
Router::new() Router::new()
.route("/", get(views::index).post(views::index_action)) .route("/", get(views::index).post(views::index_action))
.route("/app/:app_name", get(views::list_app)) .route("/app/:app_key", get(views::list_app))
.route( .route(
"/app/:app_name/:model_name", "/app/:app_key/models/:model_key",
get(views::list_item_collection), get(views::list_item_collection),
) )
.route( .route(
"/app/:app_name/:model_name/detail/:model_id", "/app/:app_key/models/:model_key/detail/:id",
get(views::item_details), get(views::item_details),
) )
} }

View File

@ -29,18 +29,6 @@ impl AdminRegistry {
} }
} }
pub fn generate_router<T>(&self) -> Router<T>
where
T: Clone,
T: Send,
T: Sync,
T: 'static,
crate::service::templates::Templates: axum::extract::FromRef<T>,
AdminState: axum::extract::FromRef<T>,
{
routing::generate_routes::<T>(&self)
}
pub fn get_apps(&self) -> Vec<AdminApp> { pub fn get_apps(&self) -> Vec<AdminApp> {
self.apps self.apps
.iter() .iter()
@ -49,37 +37,41 @@ impl AdminRegistry {
} }
fn get_app(&self, name: &str, node: &internal::AdminApp) -> AdminApp { fn get_app(&self, name: &str, node: &internal::AdminApp) -> AdminApp {
let label = node.label.as_ref().unwrap_or(&String::new()).clone();
AdminApp { AdminApp {
name: name.to_owned(), name: name.to_owned(),
app_label: label, key: node.key.to_owned(),
app_url: name.to_owned().to_lowercase(), app_url: name.to_owned().to_lowercase(),
models: vec![], models: vec![],
} }
} }
pub fn register_app(&mut self, name: &str, app_label: &str, app_url: &str) { pub fn register_app(&mut self, name: &str, app_label: &str, app_url: &str) {
let key = self.get_key(name);
self.apps.insert( self.apps.insert(
name.to_owned(), key.to_owned(),
internal::AdminApp { internal::AdminApp {
object_name: name.to_owned(), key: key,
label: Some(app_label.to_owned()), name: name.to_owned(),
}, },
); );
} }
pub fn get_models(&self, app_name: &str) -> Vec<AdminModel> { fn get_key(&self, name: &str) -> String {
slug::slugify(name)
}
pub fn get_models(&self, app_key: &str) -> Vec<AdminModel> {
vec![] vec![]
} }
pub fn get_model(&self, app_name: &str, model_name: &str) -> Option<AdminModel> { pub fn get_model(&self, app_key: &str, model_key: &str) -> Option<AdminModel> {
let full_model_name = format!("{}.{}", app_name, model_name); let full_model_key = format!("{}.{}", app_key, model_key);
let internal_model = self.models.get(&full_model_name)?; let internal_model = self.models.get(&full_model_key)?;
// unfinished: we need to think about model_name vs. model_id vs. entry_id, as "name" is ambiguous. // unfinished: we need to think about model_key vs. model_id vs. entry_id, as "name" is ambiguous.
Some(AdminModel { Some(AdminModel {
object_name: internal_model.model_name.clone(), key: internal_model.model_key.clone(),
name: internal_model.model_name.clone(), name: internal_model.model_key.clone(),
admin_url: "".to_owned(), admin_url: "".to_owned(),
view_only: false, view_only: false,
add_url: None, add_url: None,
@ -88,15 +80,15 @@ impl AdminRegistry {
fn register_model_config(&mut self, model: config::AdminModelConfig) -> Result<String, String> { fn register_model_config(&mut self, model: config::AdminModelConfig) -> Result<String, String> {
let local_config = internal::AdminModel::from(model); let local_config = internal::AdminModel::from(model);
if local_config.model_name.is_empty() { if local_config.model_key.is_empty() {
return Err("No model name".to_owned()); return Err("No model name".to_owned());
} }
let local_config_name = format!("{}.{}", local_config.app_name, local_config.model_name); let local_config_name = format!("{}.{}", local_config.app_key, local_config.model_key);
if self.models.contains_key(&local_config_name) { if self.models.contains_key(&local_config_name) {
return Err(format!("Model {} already exists", local_config_name)); return Err(format!("Model {} already exists", local_config_name));
} }
if let Some(local_config) = self.models.insert(local_config_name, local_config) { if let Some(local_config) = self.models.insert(local_config_name, local_config) {
return Ok(local_config.model_name); return Ok(local_config.model_key);
} }
Err("Model could not be added!".to_owned()) Err("Model could not be added!".to_owned())
} }
@ -106,111 +98,41 @@ impl AdminRegistry {
model: config::AdminModelConfig, model: config::AdminModelConfig,
repository: R, repository: R,
) -> Result<(), String> { ) -> Result<(), String> {
let model_name = self.register_model_config(model)?; let model_key = self.register_model_config(model)?;
self.repositories.insert(model_name, Box::new(repository)); self.repositories.insert(model_key, Box::new(repository));
Ok(()) Ok(())
} }
} }
pub mod config { pub mod config {
use axum::routing::MethodRouter;
// user uses this configuration object to register another model. // user uses this configuration object to register another model.
pub struct AdminModelConfig { pub struct AdminModelConfig {
pub name: String, pub name: String,
pub app_name: String, pub app_key: String,
pub custom_index_handler: Option<MethodRouter>,
} }
} }
mod internal { mod internal {
#[derive(Clone)] #[derive(Clone)]
pub struct AdminApp { pub struct AdminApp {
pub object_name: String, pub key: String,
pub label: Option<String>, pub name: String,
} }
#[derive(Clone)] #[derive(Clone)]
pub struct AdminModel { pub struct AdminModel {
pub app_name: String, pub app_key: String,
pub model_name: String, pub model_key: String,
pub name: String,
} }
impl From<super::config::AdminModelConfig> for AdminModel { impl From<super::config::AdminModelConfig> for AdminModel {
fn from(value: super::config::AdminModelConfig) -> Self { fn from(value: super::config::AdminModelConfig) -> Self {
AdminModel { AdminModel {
app_name: value.app_name, app_key: value.app_key,
model_name: value.name, model_key: value.name.clone(),
name: value.name,
} }
} }
} }
} }
mod routing {
use axum::{routing::get, Router};
pub fn generate_routes<T>(registry: &super::AdminRegistry) -> Router<T>
where
T: Clone,
T: Send,
T: Sync,
T: 'static,
crate::service::templates::Templates: axum::extract::FromRef<T>,
super::AdminState: axum::extract::FromRef<T>,
{
let mut router: Router<T> = Router::new();
let apps = registry.get_apps();
router = router.route(
&format!("/{}", registry.base_path),
get(crate::admin::views::index),
);
for app in apps {
let app_route = format!("/{}/{}", registry.base_path, app.app_url);
// Add a route for the app
router = router.route(
&app_route,
get(move || async move { format!("Admin panel for {}", app.app_label) }),
);
// Add routes for each model in the app
/*for model in data_node.models.iter() {
let model_route = format!("{}/{}", app_route, model.name);
router = router.route(
&model_route,
get(move || async move { format!("Model page for {}", model.name) }),
);
}*/
}
router
}
fn define_example_data(registry: &mut super::AdminRegistry) {
/*let models = vec![
AdminModel {
name: "User".to_owned(),
object_name: "User".to_owned(),
admin_url: "/admin/users/user".to_owned(),
view_only: false,
add_url: Some("/admin/users/user/add".to_owned()),
},
AdminModel {
name: "Group".to_owned(),
object_name: "Group".to_owned(),
admin_url: "/admin/users/group".to_owned(),
view_only: false,
add_url: None,
},
AdminModel {
name: "Permission".to_owned(),
object_name: "Permission".to_owned(),
admin_url: "/admin/users/permission".to_owned(),
view_only: true,
add_url: None,
},
];*/
registry.register_app("auth", "Authorities", "auth");
}
}

View File

@ -60,46 +60,6 @@ impl Default for AdminContext {
} }
} }
// Index is the entry point of Admin. It should display the Dashboard to a logged in user.
pub async fn index_example(templates: State<templates::Templates>) -> impl IntoResponse {
let models = vec![
AdminModel {
name: "User".to_owned(),
object_name: "User".to_owned(),
admin_url: "/admin/users/user".to_owned(),
view_only: false,
add_url: Some("/admin/users/user/add".to_owned()),
},
AdminModel {
name: "Group".to_owned(),
object_name: "Group".to_owned(),
admin_url: "/admin/users/group".to_owned(),
view_only: false,
add_url: None,
},
AdminModel {
name: "Permission".to_owned(),
object_name: "Permission".to_owned(),
admin_url: "/admin/users/permission".to_owned(),
view_only: true,
add_url: None,
},
];
let core_app = AdminApp {
name: "Admin".to_owned(),
app_label: "admin".to_owned(),
app_url: "/admin/users/".to_owned(),
models: models,
};
templates.render_html(
"admin/index.html",
AdminContext {
available_apps: vec![core_app],
..Default::default()
},
)
}
pub async fn index( pub async fn index(
templates: State<templates::Templates>, templates: State<templates::Templates>,
administration: State<Arc<state::AdminRegistry>>, administration: State<Arc<state::AdminRegistry>>,
@ -107,21 +67,21 @@ pub async fn index(
let models = vec![ let models = vec![
AdminModel { AdminModel {
name: "User".to_owned(), name: "User".to_owned(),
object_name: "User".to_owned(), key: "user".to_owned(),
admin_url: "/admin/users/user".to_owned(), admin_url: "/admin/app/users/user".to_owned(),
view_only: false, view_only: false,
add_url: Some("/admin/users/user/add".to_owned()), add_url: Some("/admin/users/user/add".to_owned()),
}, },
AdminModel { AdminModel {
name: "Group".to_owned(), name: "Group".to_owned(),
object_name: "Group".to_owned(), key: "group".to_owned(),
admin_url: "/admin/users/group".to_owned(), admin_url: "/admin/users/group".to_owned(),
view_only: false, view_only: false,
add_url: None, add_url: None,
}, },
AdminModel { AdminModel {
name: "Permission".to_owned(), name: "Permission".to_owned(),
object_name: "Permission".to_owned(), key: "permission".to_owned(),
admin_url: "/admin/users/permission".to_owned(), admin_url: "/admin/users/permission".to_owned(),
view_only: true, view_only: true,
add_url: None, add_url: None,
@ -129,7 +89,7 @@ pub async fn index(
]; ];
let core_app = AdminApp { let core_app = AdminApp {
name: "Admin".to_owned(), name: "Admin".to_owned(),
app_label: "admin".to_owned(), key: "admin".to_owned(),
app_url: "/admin/users/".to_owned(), app_url: "/admin/users/".to_owned(),
models: models, models: models,
}; };
@ -153,7 +113,7 @@ pub async fn index_action(Form(example_data): Form<ExampleData>) -> impl IntoRes
pub async fn list_app( pub async fn list_app(
templates: State<templates::Templates>, templates: State<templates::Templates>,
Path(app_name): Path<String>, Path(app_key): Path<String>,
) -> impl IntoResponse { ) -> impl IntoResponse {
templates.render_html("admin/app_list.html", ()) templates.render_html("admin/app_list.html", ())
} }
@ -161,7 +121,7 @@ pub async fn list_app(
// List Items renders the entire list item page. // List Items renders the entire list item page.
pub async fn list_item_collection( pub async fn list_item_collection(
templates: State<templates::Templates>, templates: State<templates::Templates>,
Path((app_name, model_name)): Path<(String, String)>, Path((app_key, model_key)): Path<(String, String)>,
) -> impl IntoResponse { ) -> impl IntoResponse {
templates.render_html("admin/items/list_items.html", ()) templates.render_html("admin/items/list_items.html", ())
} }
@ -169,7 +129,7 @@ pub async fn list_item_collection(
// Items Action is a POST to an item list. By default these are actions, that work on a list of items as input. // 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( pub async fn item_collection_action(
Form(question): Form<Question>, Form(question): Form<Question>,
Path((app_name, model_name)): Path<(String, String)>, Path((app_key, model_key)): Path<(String, String)>,
) -> impl IntoResponse { ) -> impl IntoResponse {
"There is your answer!".to_owned() "There is your answer!".to_owned()
} }
@ -177,15 +137,15 @@ pub async fn item_collection_action(
// Item Details shows one single dataset. // Item Details shows one single dataset.
pub async fn item_details( pub async fn item_details(
templates: State<templates::Templates>, templates: State<templates::Templates>,
Path((app_name, model_name, model_id)): Path<(String, String, String)>, Path((app_key, model_key, id)): Path<(String, String, String)>,
) -> impl IntoResponse { ) -> impl IntoResponse {
templates.render_html("admin/item_detail.html", ()) templates.render_html("admin/items/item_detail.html", ())
} }
// Item Action allows running an action on one single dataset. // Item Action allows running an action on one single dataset.
pub async fn item_action( pub async fn item_action(
Form(question): Form<Question>, Form(question): Form<Question>,
Path((app_name, model_name, model_id)): Path<(String, String, String)>, Path((app_key, model_key, model_id)): Path<(String, String, String)>,
) -> impl IntoResponse { ) -> impl IntoResponse {
"There is your answer!".to_owned() "There is your answer!".to_owned()
} }

View File

@ -1,12 +1,12 @@
{% if app_list %} {% if app_list %}
{% for app in app_list %} {% for app in app_list %}
<div class="app-{{ app.app_label }} module{% if app.app_url in request.path|urlencode %} current-app{% endif %}"> <div class="app-{{ app.key }} module{% if app.app_url in request.path|urlencode %} current-app{% endif %}">
<table> <table>
<caption> <caption>
<a href="{{ app.app_url }}" class="section" title="Models in the {{ name }} application">{{ app.name }}</a> <a href="{{ app.app_url }}" class="section" title="Models in the {{ name }} application">{{ app.name }}</a>
</caption> </caption>
{% for model in app.models %} {% for model in app.models %}
<tr class="model-{{ model.object_name|lower }}{% if model.admin_url in request.path|urlencode %} current-model{% endif %}"> <tr class="model-{{ model.key }}{% if model.admin_url in request.path|urlencode %} current-model{% endif %}">
{% if model.admin_url %} {% if model.admin_url %}
<th scope="row"><a href="{{ model.admin_url }}"{% if model.admin_url in request.path|urlencode %} aria-current="page"{% endif %}>{{ model.name }}</a></th> <th scope="row"><a href="{{ model.admin_url }}"{% if model.admin_url in request.path|urlencode %} aria-current="page"{% endif %}>{{ model.name }}</a></th>
{% else %} {% else %}

View File

@ -0,0 +1 @@
Item Detail.