making admin work with a state, however it probably should go into a middleware instead.
This commit is contained in:
parent
26a1bee083
commit
d89ff40d33
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -541,6 +541,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "80084fa3099f58b7afab51e5f92e24c2c2c68dcad26e96ad104bd6011570461d"
|
checksum = "80084fa3099f58b7afab51e5f92e24c2c2c68dcad26e96ad104bd6011570461d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memo-map",
|
"memo-map",
|
||||||
|
"percent-encoding",
|
||||||
"self_cell",
|
"self_cell",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
@ -18,7 +18,11 @@ barrel = { version = "0.7.0", optional = true, features = ["pg"] }
|
|||||||
diesel = { version = "2.1.3", features = ["serde_json", "postgres"] }
|
diesel = { version = "2.1.3", features = ["serde_json", "postgres"] }
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
mime_guess = "2.0.4"
|
mime_guess = "2.0.4"
|
||||||
minijinja = { version = "1.0.8", features = ["loader"] }
|
minijinja = { version = "1.0.8", features = [
|
||||||
|
"loader",
|
||||||
|
"builtins",
|
||||||
|
"urlencode",
|
||||||
|
] }
|
||||||
minijinja-autoreload = "1.0.8"
|
minijinja-autoreload = "1.0.8"
|
||||||
once_cell = "1.18.0"
|
once_cell = "1.18.0"
|
||||||
pulldown-cmark = "0.9.3"
|
pulldown-cmark = "0.9.3"
|
||||||
|
2
Justfile
2
Justfile
@ -2,7 +2,7 @@ default:
|
|||||||
@just hello
|
@just hello
|
||||||
|
|
||||||
run:
|
run:
|
||||||
@cargo run main
|
@cargo run --bin miniweb
|
||||||
|
|
||||||
bin args='':
|
bin args='':
|
||||||
@cargo run --bin {{args}}
|
@cargo run --bin {{args}}
|
||||||
|
35
src/admin/domain.rs
Normal file
35
src/admin/domain.rs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
pub use dto::AdminApp;
|
||||||
|
pub use dto::AdminModel;
|
||||||
|
|
||||||
|
mod auth {
|
||||||
|
|
||||||
|
struct AdminUser {}
|
||||||
|
|
||||||
|
struct AdminRole {}
|
||||||
|
|
||||||
|
struct AdminGroup {}
|
||||||
|
|
||||||
|
struct AdminActionLog {}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod dto {
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct AdminModel {
|
||||||
|
pub name: String,
|
||||||
|
pub object_name: String,
|
||||||
|
pub admin_url: String,
|
||||||
|
pub view_only: bool,
|
||||||
|
|
||||||
|
pub add_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct AdminApp {
|
||||||
|
pub name: String,
|
||||||
|
pub app_label: String,
|
||||||
|
pub app_url: String,
|
||||||
|
pub models: Vec<AdminModel>,
|
||||||
|
}
|
||||||
|
}
|
13
src/admin/mod.rs
Normal file
13
src/admin/mod.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
use axum::{routing::get, Router};
|
||||||
|
|
||||||
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
pub mod domain;
|
||||||
|
pub mod state;
|
||||||
|
pub mod views;
|
||||||
|
|
||||||
|
/*
|
||||||
|
pub fn routes() -> Router<AppState> {
|
||||||
|
Router::new().route("/", get(views::index).post(views::index_action))
|
||||||
|
}
|
||||||
|
*/
|
158
src/admin/state.rs
Normal file
158
src/admin/state.rs
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
use crate::admin::domain::{AdminApp, AdminModel};
|
||||||
|
use axum::routing::MethodRouter;
|
||||||
|
use axum::{async_trait, response::IntoResponse};
|
||||||
|
use axum::{routing::get, Router};
|
||||||
|
use core::future::Future;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub type AdminState = Arc<AdminRegistry>;
|
||||||
|
|
||||||
|
// main registry.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AdminRegistry {
|
||||||
|
base_path: String,
|
||||||
|
apps: HashMap<String, internal::DataNode>,
|
||||||
|
models: HashMap<String, internal::DataNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AdminRegistry {
|
||||||
|
pub fn new(base_path: &str) -> Self {
|
||||||
|
AdminRegistry {
|
||||||
|
base_path: base_path.to_owned(),
|
||||||
|
apps: HashMap::new(),
|
||||||
|
models: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
||||||
|
self.apps
|
||||||
|
.iter()
|
||||||
|
.map(|(key, node)| self.get_app(key, node))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_app(&self, name: &str, node: &internal::DataNode) -> AdminApp {
|
||||||
|
let label = node.label.as_ref().unwrap_or(&String::new()).clone();
|
||||||
|
AdminApp {
|
||||||
|
name: name.to_owned(),
|
||||||
|
app_label: label,
|
||||||
|
app_url: "".to_owned(),
|
||||||
|
models: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_app(&mut self, name: &str, app_label: &str, app_url: &str) {
|
||||||
|
self.apps.insert(
|
||||||
|
name.to_owned(),
|
||||||
|
internal::DataNode {
|
||||||
|
object_name: name.to_owned(),
|
||||||
|
label: Some(app_label.to_owned()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_models(&self) -> Vec<AdminModel> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_models_for_app(&self, name: &str) -> Vec<AdminModel> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_model(&mut self, name: &str, config: AdminModelConfig) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// user uses this configuration object to register another model.
|
||||||
|
pub struct AdminModelConfig {
|
||||||
|
pub app: Arc<str>,
|
||||||
|
pub custom_index_handler: Option<MethodRouter>,
|
||||||
|
}
|
||||||
|
|
||||||
|
mod internal {
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DataNode {
|
||||||
|
pub object_name: String,
|
||||||
|
pub label: Option<String>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
171
src/admin/views.rs
Normal file
171
src/admin/views.rs
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::{extract::State, response::IntoResponse, Form};
|
||||||
|
|
||||||
|
use crate::admin::domain::{AdminApp, AdminModel};
|
||||||
|
use crate::admin::state;
|
||||||
|
use crate::service::templates;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct Question {
|
||||||
|
question: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct ExampleData {
|
||||||
|
title: String,
|
||||||
|
content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct AdminRequest {
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct AdminContext {
|
||||||
|
pub language_code: Option<String>,
|
||||||
|
pub language_bidi: Option<bool>,
|
||||||
|
pub user: Option<String>, // Todo: user type
|
||||||
|
pub site_url: Option<String>,
|
||||||
|
pub docsroot: Option<String>,
|
||||||
|
pub messages: Vec<String>, // Todo: message type
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub subtitle: Option<String>,
|
||||||
|
pub content: String,
|
||||||
|
|
||||||
|
pub request: AdminRequest,
|
||||||
|
pub available_apps: Vec<AdminApp>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AdminContext {
|
||||||
|
fn default() -> Self {
|
||||||
|
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
|
||||||
|
site_url: None,
|
||||||
|
docsroot: None,
|
||||||
|
messages: Vec::new(), // Empty vector for messages
|
||||||
|
title: None,
|
||||||
|
subtitle: None,
|
||||||
|
content: String::new(), // Empty string for content
|
||||||
|
available_apps: Vec::new(),
|
||||||
|
request: AdminRequest {
|
||||||
|
path: "".to_owned(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
templates: State<templates::Templates>,
|
||||||
|
administration: State<Arc<state::AdminRegistry>>,
|
||||||
|
) -> 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,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut available = vec![core_app];
|
||||||
|
available.extend(administration.get_apps());
|
||||||
|
|
||||||
|
templates.render_html(
|
||||||
|
"admin/index.html",
|
||||||
|
AdminContext {
|
||||||
|
available_apps: available,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index Action is POST to the index site. We can anchor some general business code here.
|
||||||
|
pub async fn index_action(Form(example_data): Form<ExampleData>) -> impl IntoResponse {
|
||||||
|
"There is your answer!".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
// List Items renders the entire list item page.
|
||||||
|
pub async fn list_item_collection(templates: State<templates::Templates>) -> impl IntoResponse {
|
||||||
|
templates.render_html("admin/list_items.html", ())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(Form(question): Form<Question>) -> impl IntoResponse {
|
||||||
|
"There is your answer!".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item Details shows one single dataset.
|
||||||
|
pub async fn item_details(templates: State<templates::Templates>) -> impl IntoResponse {
|
||||||
|
templates.render_html("admin/item_detail.html", ())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item Action allows running an action on one single dataset.
|
||||||
|
pub async fn item_action(Form(question): Form<Question>) -> impl IntoResponse {
|
||||||
|
"There is your answer!".to_owned()
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
pub mod admin;
|
||||||
pub mod schema;
|
pub mod schema;
|
||||||
pub mod service;
|
pub mod service;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
12
src/main.rs
12
src/main.rs
@ -1,3 +1,4 @@
|
|||||||
|
mod admin;
|
||||||
mod howto;
|
mod howto;
|
||||||
mod service;
|
mod service;
|
||||||
mod state;
|
mod state;
|
||||||
@ -8,6 +9,7 @@ use axum::{
|
|||||||
extract::State, handler::HandlerWithoutStateExt, response::IntoResponse, routing::get, Router,
|
extract::State, handler::HandlerWithoutStateExt, response::IntoResponse, routing::get, Router,
|
||||||
};
|
};
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
async fn home(templates: State<templates::Templates>) -> impl IntoResponse {
|
async fn home(templates: State<templates::Templates>) -> impl IntoResponse {
|
||||||
templates.render_html("index.html", ())
|
templates.render_html("index.html", ())
|
||||||
@ -21,12 +23,20 @@ async fn hello_world(templates: State<templates::Templates>) -> impl IntoRespons
|
|||||||
async fn main() {
|
async fn main() {
|
||||||
// Prepare App State
|
// Prepare App State
|
||||||
let tmpl = templates::Templates::initialize().expect("Template Engine could not be loaded.");
|
let tmpl = templates::Templates::initialize().expect("Template Engine could not be loaded.");
|
||||||
let state: AppState = AppState { templates: tmpl };
|
let mut admin = admin::state::AdminRegistry::new("admin");
|
||||||
|
admin.register_app("auth", "Authorities", "auth");
|
||||||
|
let admin_router = admin.generate_router();
|
||||||
|
|
||||||
|
let state: AppState = AppState {
|
||||||
|
templates: tmpl,
|
||||||
|
admin: Arc::new(admin),
|
||||||
|
};
|
||||||
|
|
||||||
// Application Route
|
// Application Route
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/", get(home))
|
.route("/", get(home))
|
||||||
.route("/hello", get(hello_world))
|
.route("/hello", get(hello_world))
|
||||||
|
.merge(admin_router)
|
||||||
.nest("/howto", howto::routes())
|
.nest("/howto", howto::routes())
|
||||||
.route_service("/static/*file", handlers::static_handler.into_service())
|
.route_service("/static/*file", handlers::static_handler.into_service())
|
||||||
.fallback(handlers::not_found_handler)
|
.fallback(handlers::not_found_handler)
|
||||||
|
@ -17,6 +17,12 @@ impl Templates {
|
|||||||
let template_path = "templates";
|
let template_path = "templates";
|
||||||
environment.set_loader(path_loader(&template_path));
|
environment.set_loader(path_loader(&template_path));
|
||||||
environment.add_filter("markdown", markdown);
|
environment.add_filter("markdown", markdown);
|
||||||
|
environment.add_filter("yesno", filter_yesno);
|
||||||
|
environment.add_function("static", tpl_static);
|
||||||
|
environment.add_function("url", tpl_url);
|
||||||
|
environment.add_function("csrf_token", tpl_to_be_implemented);
|
||||||
|
environment.add_function("translate", tpl_translate);
|
||||||
|
|
||||||
notifier.watch_path(template_path, true);
|
notifier.watch_path(template_path, true);
|
||||||
Ok(environment)
|
Ok(environment)
|
||||||
});
|
});
|
||||||
@ -58,3 +64,23 @@ fn markdown(value: String) -> Value {
|
|||||||
pulldown_cmark::html::push_html(&mut html, parser);
|
pulldown_cmark::html::push_html(&mut html, parser);
|
||||||
Value::from_safe_string(html)
|
Value::from_safe_string(html)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn filter_yesno(value: String, yesno: String) -> String {
|
||||||
|
"".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tpl_url(value: String) -> Value {
|
||||||
|
Value::from_safe_string(value.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tpl_translate(value: String) -> Value {
|
||||||
|
Value::from_safe_string(value.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tpl_to_be_implemented(value: String) -> Value {
|
||||||
|
Value::from_safe_string("".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tpl_static(value: String) -> Value {
|
||||||
|
Value::from_safe_string("".into())
|
||||||
|
}
|
||||||
|
10
src/state.rs
10
src/state.rs
@ -1,10 +1,14 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::extract::FromRef;
|
use axum::extract::FromRef;
|
||||||
|
|
||||||
|
use crate::admin::state::AdminRegistry;
|
||||||
use crate::service::templates;
|
use crate::service::templates;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub templates: templates::Templates,
|
pub templates: templates::Templates,
|
||||||
|
pub admin: Arc<AdminRegistry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromRef<AppState> for templates::Templates {
|
impl FromRef<AppState> for templates::Templates {
|
||||||
@ -12,3 +16,9 @@ impl FromRef<AppState> for templates::Templates {
|
|||||||
app_state.templates.clone()
|
app_state.templates.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FromRef<AppState> for Arc<AdminRegistry> {
|
||||||
|
fn from_ref(app_state: &AppState) -> Arc<AdminRegistry> {
|
||||||
|
app_state.admin.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2633
static/django_admin/css/combined.css
Normal file
2633
static/django_admin/css/combined.css
Normal file
File diff suppressed because it is too large
Load Diff
38
templates/admin/app_list.html
Normal file
38
templates/admin/app_list.html
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{% if 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 %}">
|
||||||
|
<table>
|
||||||
|
<caption>
|
||||||
|
<a href="{{ app.app_url }}" class="section" title="Models in the {{ name }} application">{{ app.name }}</a>
|
||||||
|
</caption>
|
||||||
|
{% for model in app.models %}
|
||||||
|
<tr class="model-{{ model.object_name|lower }}{% if model.admin_url in request.path|urlencode %} current-model{% endif %}">
|
||||||
|
{% 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>
|
||||||
|
{% else %}
|
||||||
|
<th scope="row">{{ model.name }}</th>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if model.add_url %}
|
||||||
|
<td><a href="{{ model.add_url }}" class="addlink">{{ translate( 'Add') }}</a></td>
|
||||||
|
{% else %}
|
||||||
|
<td></td>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if model.admin_url and show_changelinks %}
|
||||||
|
{% if model.view_only %}
|
||||||
|
<td><a href="{{ model.admin_url }}" class="viewlink">{{ translate( 'View') }}</a></td>
|
||||||
|
{% else %}
|
||||||
|
<td><a href="{{ model.admin_url }}" class="changelink">{{ translate( 'Change') }}</a></td>
|
||||||
|
{% endif %}
|
||||||
|
{% elif show_changelinks %}
|
||||||
|
<td></td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p>{{ translate( 'You don’t have permission to view or edit anything.') }}</p>
|
||||||
|
{% endif %}
|
127
templates/admin/base.html
Normal file
127
templates/admin/base.html
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
{% set is_nav_sidebar_enabled = true %}
|
||||||
|
<html lang="{{ language_code|default("en-us") }}"
|
||||||
|
dir="{{ LANGUAGE_BIDI|yesno('rtl,ltr,auto') }}">
|
||||||
|
<head>
|
||||||
|
<title>{% block title %}{% endblock %}</title>
|
||||||
|
<link rel="stylesheet" href="{% block stylesheet %}{{ static("admin/css/base.css") }}{% endblock %}">
|
||||||
|
<link rel="stylesheet" href="/static/django_admin/css/combined.css">
|
||||||
|
{% block dark_mode_vars %}
|
||||||
|
<link rel="stylesheet" href="{{ static("admin/css/dark_mode.css") }}">
|
||||||
|
<script src="{{ static("admin/js/theme.js") }}" defer></script>
|
||||||
|
{% endblock %}
|
||||||
|
{% if not is_popup and is_nav_sidebar_enabled %}
|
||||||
|
<link rel="stylesheet" href="{{ static("admin/css/nav_sidebar.css") }}">
|
||||||
|
<script src="{{ static('admin/js/nav_sidebar.js') }}" defer></script>
|
||||||
|
{% endif %}
|
||||||
|
{% block extrastyle %}{% endblock %}
|
||||||
|
{% if LANGUAGE_BIDI %}<link rel="stylesheet" href="{% block stylesheet_rtl %}{{ static("admin/css/rtl.css") }}{% endblock %}">{% endif %}
|
||||||
|
{% block extrahead %}{% endblock %}
|
||||||
|
{% block responsive %}
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="{{ static("admin/css/responsive.css") }}">
|
||||||
|
{% if LANGUAGE_BIDI %}<link rel="stylesheet" href="{{ static("admin/css/responsive_rtl.css") }}">{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
{% block blockbots %}<meta name="robots" content="NONE,NOARCHIVE">{% endblock %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="{% if is_popup %}popup {% endif %}{% block bodyclass %}{% endblock %}"
|
||||||
|
data-admin-utc-offset="{# now("Z") #}">
|
||||||
|
<a href="#content-start" class="skip-to-content-link">{{ translate('Skip to main content') }}</a>
|
||||||
|
<!-- Container -->
|
||||||
|
<div id="container">
|
||||||
|
|
||||||
|
{% if not is_popup %}
|
||||||
|
<!-- Header -->
|
||||||
|
{% block header %}
|
||||||
|
<header id="header">
|
||||||
|
<div id="branding">
|
||||||
|
{% block branding %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
{% block usertools %}
|
||||||
|
{% if has_permission %}
|
||||||
|
<div id="user-tools">
|
||||||
|
{% block welcome_msg %}
|
||||||
|
{{ translate('Welcome,') }}
|
||||||
|
<strong>{{ user.get_short_name or user.get_username }}</strong>.
|
||||||
|
{% endblock %}
|
||||||
|
{% block userlinks %}
|
||||||
|
{% if site_url %}
|
||||||
|
<a href="{{ site_url }}">{{ translate('View site') }}</a> /
|
||||||
|
{% endif %}
|
||||||
|
{% if user.is_active and user.is_staff %}
|
||||||
|
{% set docsroot = url('django-admindocs-docroot') %}
|
||||||
|
{% if docsroot %}
|
||||||
|
<a href="{{ docsroot }}">{{ translate('Documentation') }}</a> /
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% if user.has_usable_password %}
|
||||||
|
<a href="{{ url('admin:password_change') }}">{{ translate('Change password') }}</a> /
|
||||||
|
{% endif %}
|
||||||
|
<form id="logout-form" method="post" action="{{ url('admin:logout') }}">
|
||||||
|
{{ csrf_token() }}
|
||||||
|
<button type="submit">{{ translate('Log out') }}</button>
|
||||||
|
</form>
|
||||||
|
{% include "admin/color_theme_toggle.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
{% block nav_global %}{% endblock %}
|
||||||
|
</header>
|
||||||
|
{% endblock %}
|
||||||
|
<!-- END Header -->
|
||||||
|
{% block nav_breadcrumbs %}
|
||||||
|
<nav aria-label="{{ translate('Breadcrumbs') }}">
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
<div class="breadcrumbs">
|
||||||
|
<a href="{{ url( 'admin:index') }}">{{ translate('Home') }}</a>
|
||||||
|
{% if title %} › {{ title }}{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
</nav>
|
||||||
|
{% endblock %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="main" id="main">
|
||||||
|
{% if not is_popup and is_nav_sidebar_enabled %}
|
||||||
|
{% block nav_sidebar %}
|
||||||
|
{% include "admin/nav_sidebar.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
{% endif %}
|
||||||
|
<main id="content-start" class="content" tabindex="-1">
|
||||||
|
{% block messages %}
|
||||||
|
{% if messages %}
|
||||||
|
<ul class="messagelist">{% for message in messages %}
|
||||||
|
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message|capfirst }}</li>
|
||||||
|
{% endfor %}</ul>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock messages %}
|
||||||
|
<!-- Content -->
|
||||||
|
<div id="content" class="{% block coltype %}colM{% endblock %}">
|
||||||
|
{% block pretitle %}{% endblock %}
|
||||||
|
{% block content_title %}{% if title %}<h1>{{ title }}</h1>{% endif %}{% endblock %}
|
||||||
|
{% block content_subtitle %}{% if subtitle %}<h2>{{ subtitle }}</h2>{% endif %}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
{% block object_tools %}{% endblock %}
|
||||||
|
{{ content }}
|
||||||
|
{% endblock %}
|
||||||
|
{% block sidebar %}{% endblock %}
|
||||||
|
<br class="clear">
|
||||||
|
</div>
|
||||||
|
<!-- END Content -->
|
||||||
|
{% block footer %}<div id="footer"></div>{% endblock %}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- END Container -->
|
||||||
|
|
||||||
|
<!-- SVGs -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="base-svgs">
|
||||||
|
<symbol viewBox="0 0 24 24" width="1rem" height="1rem" id="icon-auto"><path d="M0 0h24v24H0z" fill="currentColor"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2V4a8 8 0 1 0 0 16z"/></symbol>
|
||||||
|
<symbol viewBox="0 0 24 24" width="1rem" height="1rem" id="icon-moon"><path d="M0 0h24v24H0z" fill="currentColor"/><path d="M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2h.1A6.979 6.979 0 0 0 10 7zm-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938 7.999 7.999 0 0 0 4 12z"/></symbol>
|
||||||
|
<symbol viewBox="0 0 24 24" width="1rem" height="1rem" id="icon-sun"><path d="M0 0h24v24H0z" fill="currentColor"/><path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-2a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85l1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z"/></symbol>
|
||||||
|
</svg>
|
||||||
|
<!-- END SVGs -->
|
||||||
|
</body>
|
||||||
|
</html>
|
1
templates/admin/index.html
Normal file
1
templates/admin/index.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
{% extends "admin/base.html" %}
|
9
templates/admin/nav_sidebar.html
Normal file
9
templates/admin/nav_sidebar.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<button class="sticky toggle-nav-sidebar" id="toggle-nav-sidebar" aria-label="{{ translate('Toggle navigation') }}"></button>
|
||||||
|
<nav class="sticky" id="nav-sidebar1" aria-label="{{ translate('Sidebar') }}">
|
||||||
|
<input type="search" id="nav-filter"
|
||||||
|
placeholder="{{ translate('Start typing to filter…') }}"
|
||||||
|
aria-label="{{ translate('Filter navigation items') }}">
|
||||||
|
{% set app_list=available_apps %}
|
||||||
|
{% set show_changelinks=false %}
|
||||||
|
{% include 'admin/app_list.html' %}
|
||||||
|
</nav>
|
Loading…
Reference in New Issue
Block a user