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"
|
||||
dependencies = [
|
||||
"memo-map",
|
||||
"percent-encoding",
|
||||
"self_cell",
|
||||
"serde",
|
||||
]
|
||||
|
@ -18,7 +18,11 @@ barrel = { version = "0.7.0", optional = true, features = ["pg"] }
|
||||
diesel = { version = "2.1.3", features = ["serde_json", "postgres"] }
|
||||
dotenvy = "0.15.7"
|
||||
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"
|
||||
once_cell = "1.18.0"
|
||||
pulldown-cmark = "0.9.3"
|
||||
|
2
Justfile
2
Justfile
@ -2,7 +2,7 @@ default:
|
||||
@just hello
|
||||
|
||||
run:
|
||||
@cargo run main
|
||||
@cargo run --bin miniweb
|
||||
|
||||
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 service;
|
||||
pub mod state;
|
||||
|
12
src/main.rs
12
src/main.rs
@ -1,3 +1,4 @@
|
||||
mod admin;
|
||||
mod howto;
|
||||
mod service;
|
||||
mod state;
|
||||
@ -8,6 +9,7 @@ use axum::{
|
||||
extract::State, handler::HandlerWithoutStateExt, response::IntoResponse, routing::get, Router,
|
||||
};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
async fn home(templates: State<templates::Templates>) -> impl IntoResponse {
|
||||
templates.render_html("index.html", ())
|
||||
@ -21,12 +23,20 @@ async fn hello_world(templates: State<templates::Templates>) -> impl IntoRespons
|
||||
async fn main() {
|
||||
// Prepare App State
|
||||
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
|
||||
let app = Router::new()
|
||||
.route("/", get(home))
|
||||
.route("/hello", get(hello_world))
|
||||
.merge(admin_router)
|
||||
.nest("/howto", howto::routes())
|
||||
.route_service("/static/*file", handlers::static_handler.into_service())
|
||||
.fallback(handlers::not_found_handler)
|
||||
|
@ -17,6 +17,12 @@ impl Templates {
|
||||
let template_path = "templates";
|
||||
environment.set_loader(path_loader(&template_path));
|
||||
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);
|
||||
Ok(environment)
|
||||
});
|
||||
@ -58,3 +64,23 @@ fn markdown(value: String) -> Value {
|
||||
pulldown_cmark::html::push_html(&mut html, parser);
|
||||
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 crate::admin::state::AdminRegistry;
|
||||
use crate::service::templates;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub templates: templates::Templates,
|
||||
pub admin: Arc<AdminRegistry>,
|
||||
}
|
||||
|
||||
impl FromRef<AppState> for templates::Templates {
|
||||
@ -12,3 +16,9 @@ impl FromRef<AppState> for templates::Templates {
|
||||
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