营养校准、后台美化、静态走对象存储 CDN、标题可编辑、admin 拼音搜索
- 菜品营养按食物成分表+烹饪方式校准(calibrate_nutrition 命令) - 后台接入 jazzmin 主题美化,移除 Google Fonts(模板覆盖) - 前端/后台静态资源指向 qnycdn 对象存储 CDN(vite base + DJANGO_STATIC_URL) - 菜单标题学校名称/周数可编辑,日期随周数计算 - admin 搜索支持拼音首字母/全拼(Dish.pinyin 索引,pypinyin) - Dockerfile 使用清华 pip 镜像源
This commit is contained in:
+3
-1
@@ -1,7 +1,9 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
PIP_NO_CACHE_DIR=1
|
||||
PIP_NO_CACHE_DIR=1 \
|
||||
PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple \
|
||||
PIP_TRUSTED_HOST=pypi.tuna.tsinghua.edu.cn
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ ALLOWED_HOSTS = [h.strip() for h in os.environ.get('DJANGO_ALLOWED_HOSTS', '*').
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'jazzmin',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
@@ -59,7 +60,7 @@ ROOT_URLCONF = 'config.urls'
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'DIRS': [BASE_DIR / 'templates'],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
@@ -119,8 +120,9 @@ USE_TZ = True
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/6.0/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
STATIC_URL = os.environ.get('DJANGO_STATIC_URL', '/xx/admin/')
|
||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||
STATICFILES_DIRS = [BASE_DIR / 'static']
|
||||
|
||||
CORS_ALLOWED_ORIGINS = [
|
||||
'http://localhost:5173',
|
||||
@@ -129,6 +131,7 @@ CORS_ALLOWED_ORIGINS = [
|
||||
'http://xxcdn.mymoyu.top',
|
||||
'https://xx.mymoyu.top',
|
||||
'https://xxcdn.mymoyu.top',
|
||||
'https://qnycdn.mymoyu.top',
|
||||
]
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
@@ -139,5 +142,59 @@ REST_FRAMEWORK = {
|
||||
|
||||
CSRF_TRUSTED_ORIGINS = [h.strip() for h in os.environ.get(
|
||||
'DJANGO_CSRF_TRUSTED_ORIGINS',
|
||||
'http://xx.mymoyu.top,http://xxcdn.mymoyu.top,https://xx.mymoyu.top,https://xxcdn.mymoyu.top',
|
||||
'http://xx.mymoyu.top,http://xxcdn.mymoyu.top,https://xx.mymoyu.top,https://xxcdn.mymoyu.top,https://qnycdn.mymoyu.top',
|
||||
).split(',')]
|
||||
|
||||
JAZZMIN_SETTINGS = {
|
||||
'site_title': '学校订餐菜单管理系统',
|
||||
'site_header': '学校订餐菜单生成器',
|
||||
'site_brand': '订餐菜单管理',
|
||||
'welcome_sign': '欢迎使用学校订餐菜单管理系统',
|
||||
'copyright': '上海外国语大学附属宝山双语学校',
|
||||
'search_model': ['meals.Dish', 'auth.User'],
|
||||
'show_sidebar': True,
|
||||
'navigation_expanded': True,
|
||||
'hide_apps': [],
|
||||
'hide_models': [],
|
||||
'icons': {
|
||||
'meals.Dish': 'fas fa-utensils',
|
||||
'auth.User': 'fas fa-user',
|
||||
'auth.Group': 'fas fa-users',
|
||||
},
|
||||
'default_icon_parents': 'fas fa-chevron-circle-right',
|
||||
'default_icon_children': 'fas fa-circle',
|
||||
'show_ui_builder': False,
|
||||
'changeform_format': 'horizontal_tabs',
|
||||
'related_modal_active': True,
|
||||
'custom_css': None,
|
||||
'custom_js': None,
|
||||
'theme': 'flatly',
|
||||
'dark_mode_theme': 'darkly',
|
||||
'header_classes': 'bg-primary',
|
||||
'order_with_respect_to': ['meals', 'auth'],
|
||||
}
|
||||
|
||||
JAZZMIN_UI_TWEAKS = {
|
||||
'navbar_small_text': False,
|
||||
'footer_small_text': False,
|
||||
'body_small_text': False,
|
||||
'brand_small_text': False,
|
||||
'brand_colour': False,
|
||||
'accent': 'accent-primary',
|
||||
'navbar': 'navbar-white navbar-light',
|
||||
'no_navbar_border': False,
|
||||
'navbar_fixed': True,
|
||||
'layout_boxed': False,
|
||||
'footer_fixed': False,
|
||||
'sidebar_fixed': True,
|
||||
'sidebar': 'sidebar-dark-primary',
|
||||
'sidebar_nav_small_text': False,
|
||||
'sidebar_disable_expand': False,
|
||||
'sidebar_nav_child_indent': False,
|
||||
'sidebar_nav_compact_style': False,
|
||||
'sidebar_nav_legacy_style': False,
|
||||
'sidebar_nav_flat_style': False,
|
||||
'theme': 'flatly',
|
||||
'dark_mode_theme': 'darkly',
|
||||
'button_classes': {'primary': 'btn-outline-primary', 'secondary': 'btn-outline-secondary', 'info': 'btn-outline-info', 'warning': 'btn-outline-warning', 'danger': 'btn-outline-danger', 'success': 'btn-outline-success'},
|
||||
}
|
||||
|
||||
+31
-3
@@ -1,4 +1,5 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html, mark_safe
|
||||
|
||||
from .models import Dish
|
||||
|
||||
@@ -15,10 +16,10 @@ def deactivate_dishes(modeladmin, request, queryset):
|
||||
|
||||
@admin.register(Dish)
|
||||
class DishAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'cuisine', 'dish_type', 'ingredient_detail', 'protein', 'fat', 'calorie', 'is_active', 'created_at')
|
||||
list_display = ('name', 'cuisine', 'dish_type_display', 'ingredient_detail', 'protein', 'fat', 'calorie', 'nutri_badge', 'is_active', 'created_at')
|
||||
list_filter = ('dish_type', 'cuisine', 'is_active')
|
||||
search_fields = ('name', 'ingredient_detail', 'cuisine')
|
||||
list_editable = ('cuisine', 'dish_type', 'ingredient_detail', 'protein', 'fat', 'calorie', 'is_active')
|
||||
search_fields = ('name', 'ingredient_detail', 'cuisine', 'pinyin')
|
||||
list_editable = ('cuisine', 'ingredient_detail', 'protein', 'fat', 'calorie', 'is_active')
|
||||
list_per_page = 50
|
||||
actions = [activate_dishes, deactivate_dishes]
|
||||
fieldsets = (
|
||||
@@ -26,3 +27,30 @@ class DishAdmin(admin.ModelAdmin):
|
||||
('营养信息', {'fields': ('ingredient_detail', 'protein', 'fat', 'calorie')}),
|
||||
('其他', {'fields': ('description',), 'classes': ('collapse',)}),
|
||||
)
|
||||
|
||||
@admin.display(description='营养提示', ordering='calorie')
|
||||
def nutri_badge(self, obj):
|
||||
badges = []
|
||||
if obj.dish_type == 'meat' and obj.fat >= 20:
|
||||
badges.append('<span class="badge badge-danger">高脂</span>')
|
||||
if obj.dish_type == 'meat' and obj.calorie >= 300:
|
||||
badges.append('<span class="badge badge-warning">高热量</span>')
|
||||
if obj.dish_type in ('meat', 'veg') and obj.protein >= 15:
|
||||
badges.append('<span class="badge badge-success">高蛋白</span>')
|
||||
if obj.dish_type == 'veg' and obj.fat <= 3 and obj.calorie <= 60:
|
||||
badges.append('<span class="badge badge-info">清淡</span>')
|
||||
return mark_safe(' '.join(badges)) if badges else '—'
|
||||
|
||||
@admin.display(description='类型')
|
||||
def dish_type_display(self, obj):
|
||||
colors = {'meat': '#d9534f', 'veg': '#5cb85c', 'soup': '#5bc0de', 'staple': '#f0ad4e'}
|
||||
return format_html(
|
||||
'<span style="color:{};font-weight:600">{}</span>',
|
||||
colors.get(obj.dish_type, '#999'),
|
||||
dict(Dish.TYPE_CHOICES).get(obj.dish_type, obj.dish_type),
|
||||
)
|
||||
|
||||
|
||||
admin.site.site_header = '学校订餐菜单管理系统'
|
||||
admin.site.site_title = '学校订餐菜单管理'
|
||||
admin.site.index_title = '菜单管理后台'
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
from django.db import migrations, models
|
||||
from pypinyin import Style, lazy_pinyin
|
||||
|
||||
|
||||
def fill_pinyin(apps, schema_editor):
|
||||
Dish = apps.get_model('meals', 'Dish')
|
||||
for dish in Dish.objects.all():
|
||||
full = ''.join(lazy_pinyin(dish.name))
|
||||
initials = ''.join(lazy_pinyin(dish.name, style=Style.FIRST_LETTER))
|
||||
dish.pinyin = f'{full} {initials}'
|
||||
dish.save(update_fields=['pinyin'])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('meals', '0004_dish_cuisine'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='dish',
|
||||
name='pinyin',
|
||||
field=models.CharField(blank=True, editable=False, max_length=200, verbose_name='拼音索引'),
|
||||
),
|
||||
migrations.RunPython(fill_pinyin, migrations.RunPython.noop),
|
||||
]
|
||||
@@ -1,5 +1,7 @@
|
||||
from django.db import models
|
||||
|
||||
from pypinyin import Style, lazy_pinyin
|
||||
|
||||
|
||||
class Dish(models.Model):
|
||||
TYPE_CHOICES = [
|
||||
@@ -29,6 +31,7 @@ class Dish(models.Model):
|
||||
calorie = models.IntegerField('热量(kcal)', default=0)
|
||||
description = models.CharField('备注', max_length=200, blank=True)
|
||||
is_active = models.BooleanField('启用', default=True)
|
||||
pinyin = models.CharField('拼音索引', max_length=200, blank=True, editable=False)
|
||||
created_at = models.DateTimeField('创建时间', auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
@@ -38,3 +41,10 @@ class Dish(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.get_dish_type_display()}: {self.name}'
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.name:
|
||||
full = ''.join(lazy_pinyin(self.name))
|
||||
initials = ''.join(lazy_pinyin(self.name, style=Style.FIRST_LETTER))
|
||||
self.pinyin = f'{full} {initials}'
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
+19
-7
@@ -223,23 +223,35 @@ def generate_week_menu(payload, menu_key='a'):
|
||||
return week
|
||||
|
||||
|
||||
def build_menu_title(label):
|
||||
today = date.today()
|
||||
week_no = today.isocalendar().week
|
||||
monday = today - timedelta(days=today.weekday())
|
||||
def build_menu_title(label, school_name=None, week_no=None):
|
||||
school_name = school_name or SCHOOL_NAME
|
||||
iso = date.today().isocalendar()
|
||||
try:
|
||||
week_no = int(week_no) if week_no is not None else iso.week
|
||||
except (TypeError, ValueError):
|
||||
week_no = iso.week
|
||||
if not 1 <= week_no <= 53:
|
||||
week_no = iso.week
|
||||
try:
|
||||
monday = date.fromisocalendar(iso.year, week_no, 1)
|
||||
except ValueError:
|
||||
monday = date.today() - timedelta(days=date.today().weekday())
|
||||
friday = monday + timedelta(days=4)
|
||||
date_range = f'{monday.month}.{monday.day}--{friday.month}.{friday.day}'
|
||||
return f'{SCHOOL_NAME}[第{week_no}周]{date_range}营养{label}菜单'
|
||||
return f'{school_name}[第{week_no}周]{date_range}营养{label}菜单'
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
def generate_menu(request):
|
||||
data = request.data
|
||||
school = data.get('school_name') or SCHOOL_NAME
|
||||
week_no = data.get('week_no')
|
||||
menus = {}
|
||||
for label in ('A', 'B'):
|
||||
key = label.lower()
|
||||
menus[key] = {
|
||||
'title': build_menu_title(label),
|
||||
'week': generate_week_menu(request.data, menu_key=key),
|
||||
'title': build_menu_title(label, school, week_no),
|
||||
'week': generate_week_menu(data, menu_key=key),
|
||||
}
|
||||
return Response(menus)
|
||||
|
||||
|
||||
@@ -3,3 +3,5 @@ djangorestframework==3.17.1
|
||||
django-cors-headers==4.9.0
|
||||
openpyxl==3.1.5
|
||||
gunicorn==23.0.0
|
||||
django-jazzmin==3.0.5
|
||||
pypinyin==0.54.0
|
||||
|
||||
@@ -0,0 +1,429 @@
|
||||
{% load i18n static jazzmin admin_urls %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
{% get_current_language_bidi as LANGUAGE_BIDI %}
|
||||
{% get_jazzmin_settings request as jazzmin_settings %}
|
||||
{% get_jazzmin_ui_tweaks as jazzmin_ui %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ LANGUAGE_CODE|default:"en-us" }}" {% if LANGUAGE_BIDI %}dir="rtl"{% endif %}>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<!-- Prevent admin panel being crawled by search engines -->
|
||||
<meta name="robots" content="none, noarchive">
|
||||
<script>
|
||||
(function() {
|
||||
var mode = localStorage.getItem('jazzmin-theme-mode') || '{{ jazzmin_ui.default_theme_mode|default:"light" }}';
|
||||
if (mode === 'auto') {
|
||||
mode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
document.documentElement.setAttribute('data-bs-theme', mode);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<title>{% block title %}{{ title }} | {{ jazzmin_settings.site_title }}{% endblock %}</title>
|
||||
|
||||
<!-- Font Awesome Icons -->
|
||||
<link rel="stylesheet" href="{% static "vendor/fontawesome-free/css/all.min.css" %}">
|
||||
|
||||
<!-- Bootstrap and adminLTE -->
|
||||
<link rel="stylesheet" href="{% static "vendor/adminlte/css/adminlte.min.css" %}" id="adminlte-css">
|
||||
|
||||
<!-- Bootswatch theme -->
|
||||
<link rel="stylesheet" {% if jazzmin_ui.theme.name != 'default' %}href="{{ jazzmin_ui.theme.src }}"{% endif %} id="jazzmin-theme" data-theme-base="{% static 'vendor/bootswatch' %}" />
|
||||
{% if jazzmin_settings.show_theme_chooser %}
|
||||
<script>
|
||||
(function() {
|
||||
var savedTheme = localStorage.getItem('jazzmin-theme');
|
||||
if (savedTheme && savedTheme !== 'default') {
|
||||
var link = document.getElementById('jazzmin-theme');
|
||||
link.setAttribute('href', link.getAttribute('data-theme-base') + '/' + savedTheme + '/bootstrap.min.css');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<!-- Custom fixes for django -->
|
||||
<link rel="stylesheet" href="{% static "jazzmin/css/main.css" %}">
|
||||
|
||||
{% if jazzmin_settings.custom_css %}
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="{% static jazzmin_settings.custom_css %}">
|
||||
{% endif %}
|
||||
|
||||
<!-- favicons -->
|
||||
<link rel="shortcut icon" href="{% static jazzmin_settings.site_icon %}" type="image/png">
|
||||
<link rel="icon" href="{% static jazzmin_settings.site_icon %}" sizes="32x32" type="image/png">
|
||||
|
||||
{% if jazzmin_settings.use_google_fonts_cdn %}
|
||||
<!-- Google Font: Source Sans Pro -->
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% block extrastyle %} {% endblock %}
|
||||
{% block extrahead %} {% endblock %}
|
||||
</head>
|
||||
<body class="layout-fixed{% if jazzmin_settings.show_sidebar %} sidebar-expand-lg sidebar-open{% else %} no-sidebar{% endif %} bg-body-tertiary {% sidebar_status request %} {% if is_popup %}popup {% endif %}{% block bodyclass %}{% endblock %} {{ jazzmin_ui.body_classes }}" data-admin-utc-offset="{% now "Z" %}">
|
||||
|
||||
<div class="app-wrapper">
|
||||
|
||||
{% if not is_popup %}
|
||||
<nav class="app-header navbar navbar-expand bg-body {{ jazzmin_ui.navbar_classes }}" id="jazzy-navbar">
|
||||
<div class="container-fluid">
|
||||
<ul class="navbar-nav">
|
||||
|
||||
{% if jazzmin_settings.show_sidebar %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-lte-toggle="sidebar" href="#" role="button"><i class="fas fa-bars"></i></a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a href="{% url 'admin:index' %}" class="brand-link">
|
||||
<img src="{% static jazzmin_settings.site_logo %}" alt="{{ jazzmin_settings.site_header }} Logo" class="{{ jazzmin_settings.site_logo_classes }} brand-image" style="opacity: .8; margin: 0 0 0 5px;">
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% get_top_menu user request.current_app|default:"admin" as top_menu %}
|
||||
{% for link in top_menu %}
|
||||
<li class="nav-item d-none d-sm-inline-block{% if link.children %} dropdown{% endif %}">
|
||||
{% if link.children %}
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{{ link.name }}
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
{% for child in link.children %}
|
||||
<a class="dropdown-item" href="{{ child.url }}" {% if link.new_window %}target="_blank"{% endif %}>{{ child.name }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<a href="{{ link.url }}" class="nav-link" {% if link.new_window %}target="_blank"{% endif %}>{{ link.name }}</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% if jazzmin_settings.search_model %}
|
||||
{% for search_model in jazzmin_settings.search_models_parsed %}
|
||||
<form action="{{ search_model.search_url }}" method="GET" class="d-flex ms-2">
|
||||
<div class="input-group input-group-sm">
|
||||
<input class="form-control form-control-navbar" name="q" type="search" placeholder="{% trans 'Search' %} {{ search_model.search_name }}..." aria-label="{% trans 'Search' %} {{ search_model.search_name }}...">
|
||||
<button class="btn btn-navbar" type="submit">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<ul class="navbar-nav ms-auto">
|
||||
|
||||
{% if jazzmin_settings.show_ui_builder %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-widget="control-sidebar" data-slide="true" href="#" role="button">
|
||||
<i class="fas fa-th-large"></i>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if 'django.contrib.admindocs'|app_is_installed %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'django-admindocs-docroot' %}" role="button">
|
||||
<i class="fas fa-book"></i>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if jazzmin_settings.show_theme_chooser %}
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link btn" data-bs-toggle="dropdown" href="#" title="{% trans 'Theme' %}">
|
||||
<i class="fas fa-palette"></i>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-end" id="jazzy-theme-chooser">
|
||||
<span class="dropdown-header">{% trans 'Theme' %}</span>
|
||||
<div class="dropdown-divider"></div>
|
||||
<div class="px-3 pb-2">
|
||||
<select class="form-select form-select-sm" id="jazzmin-theme-select">
|
||||
{% for theme_name in jazzmin_ui.theme_list %}
|
||||
<option value="{{ theme_name }}" {% if theme_name == jazzmin_ui.theme.name %}selected{% endif %}>
|
||||
{{ theme_name|title }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="dropdown-divider"></div>
|
||||
<span class="dropdown-header">{% trans 'Color Scheme' %}</span>
|
||||
<div class="dropdown-divider"></div>
|
||||
<div class="px-3 pb-2">
|
||||
<select class="form-select form-select-sm" id="jazzmin-mode-select">
|
||||
<option value="light">{% trans 'Light' %}</option>
|
||||
<option value="dark">{% trans 'Dark' %}</option>
|
||||
<option value="auto">{% trans 'Auto' %}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if jazzmin_settings.language_chooser %}
|
||||
{% get_available_languages as LANGUAGES %}
|
||||
{% get_language_info_list for LANGUAGES as languages %}
|
||||
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link btn" data-bs-toggle="dropdown" href="#" title="Choose language">
|
||||
<i class="fas fa-globe" aria-hidden="true"></i>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-end" id="jazzy-languagemenu">
|
||||
<form action="{% url 'set_language' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<input name="next" type="hidden" value="{{ request.get_full_path|remove_lang:LANGUAGE_CODE }}" />
|
||||
{% for language in languages %}
|
||||
<button
|
||||
type="submit"
|
||||
name="language"
|
||||
value="{{ language.code }}"
|
||||
class="dropdown-item {% if language.code == LANGUAGE_CODE %}active{% endif %}"
|
||||
lang="{{ language.code }}"
|
||||
>
|
||||
{{ language.name_local|title }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</form>
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link btn" data-bs-toggle="dropdown" href="#" title="{{ request.user }}">
|
||||
<i class="far fa-user" aria-hidden="true"></i>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-end" id="jazzy-usermenu">
|
||||
<span class="dropdown-header">{% trans 'Account' %}</span>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a href="{% url 'admin:password_change' %}" class="dropdown-item">
|
||||
<i class="fas fa-key me-2"></i> {% trans 'Change password' %}
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<form id="logout-form" method="post" action="{% url 'admin:logout' %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="dropdown-item"><i class="fas fa-users me-2"></i> {% translate 'Log out' %}</button>
|
||||
</form>
|
||||
{% get_user_menu user request.current_app|default:"admin" as user_menu %}
|
||||
{% for link in user_menu %}
|
||||
<div class="dropdown-divider"></div>
|
||||
<a href="{{ link.url }}" class="dropdown-item" {% if link.new_window %}target="_blank"{% endif %}>
|
||||
<i class="{{ link.icon }} me-2"></i> {% trans link.name %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
<div class="dropdown-divider"></div>
|
||||
{% if perms|can_view_self %}
|
||||
<a href="{% jazzy_admin_url request.user request.current_app|default:"admin" %}" class="dropdown-item dropdown-footer">{% trans 'See Profile' %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
{% block sidebar %}
|
||||
{% if jazzmin_settings.show_sidebar %}
|
||||
{% get_side_menu as side_menu_list %}
|
||||
|
||||
<aside class="app-sidebar bg-body-secondary shadow {{ jazzmin_ui.sidebar_classes }}" id="jazzy-sidebar" data-bs-theme="dark">
|
||||
<div class="sidebar-brand">
|
||||
<a href="{% url 'admin:index' %}" class="brand-link {{ jazzmin_ui.brand_classes }}" id="jazzy-logo">
|
||||
<img src="{% static jazzmin_settings.site_logo %}" alt="{{ jazzmin_settings.site_header }} Logo" class="{{ jazzmin_settings.site_logo_classes }} brand-image opacity-75 shadow">
|
||||
<span class="brand-text fw-light">{{ jazzmin_settings.site_brand }}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-wrapper">
|
||||
<div class="user-panel d-flex">
|
||||
<div class="image">
|
||||
{% if jazzmin_settings|has_jazzmin_setting:"user_avatar" %}
|
||||
<img src="{% get_user_avatar request.user %}" width="160px" class="img-circle elevation-2" alt="User Image">
|
||||
{% else %}
|
||||
<i class="fas fa-inverse user-profile fa-user-circle"></i>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="info">
|
||||
{% if perms|can_view_self %}
|
||||
<a href="{% jazzy_admin_url request.user request.current_app|default:"admin" %}" class="d-block">{{ request.user }}</a>
|
||||
{% else %}
|
||||
<span class="d-block opacity-75">{{ request.user }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="mt-2">
|
||||
<ul class="nav sidebar-menu flex-column {{ jazzmin_ui.sidebar_list_classes }}" data-lte-toggle="treeview" role="navigation" aria-label="Main navigation" data-accordion="false" id="jazzy-navigation">
|
||||
|
||||
<li class="nav-item">
|
||||
<a href="{% url 'admin:index' %}" class="nav-link">
|
||||
<i class="nav-icon fas fa-th-large"></i>
|
||||
<p>{% trans 'Dashboard' %}</p>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if jazzmin_settings.navigation_expanded %}
|
||||
{% for app in side_menu_list %}
|
||||
<li class="nav-header">{{ app.name }}</li>
|
||||
{% for model in app.models %}
|
||||
<li class="nav-item">
|
||||
{% if model.url %}
|
||||
<a href="{{ model.url }}" class="nav-link">
|
||||
<i class="nav-icon {{ model.icon }}"></i> <p>{{ model.name }}</p>
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="nav-link disabled">
|
||||
<i class="nav-icon {{ model.icon }}"></i> <p>{{ model.name }}</p>
|
||||
</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for app in side_menu_list %}
|
||||
<li class="nav-item has-treeview">
|
||||
<a href="#" class="nav-link">
|
||||
<i class="nav-icon {{ app.icon }}"></i>
|
||||
<p>{{ app.name|truncatechars:21 }} <i class="nav-arrow fas fa-chevron-right" aria-hidden="true"></i></p>
|
||||
</a>
|
||||
<ul class="nav nav-treeview">
|
||||
{% for model in app.models %}
|
||||
<li class="nav-item">
|
||||
<a href="{% if model.url %}{{ model.url }}{% else %}javascript:void(0){% endif %}" class="nav-link">
|
||||
<i class="nav-icon {{ model.icon }}"></i>
|
||||
<p>{{ model.name }}</p>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
|
||||
<main class="app-main" {% if is_popup %}style="margin-left:0; padding-top: 20px;"{% endif %}>
|
||||
{% block page_content %}
|
||||
{% if not is_popup %}
|
||||
<div class="app-content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<h3 class="mb-0">{% block content_title %}{% endblock %}</h3>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="float-sm-end">{% block breadcrumbs %}{% endblock %}</div>
|
||||
{% block page_actions %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="app-content">
|
||||
<div class="container-fluid">
|
||||
{% block messages %}
|
||||
{% for message in messages %}
|
||||
{% if message.tags == 'success' %}
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="icon fa fa-check"></i> {{ message|capfirst }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% elif message.tags == 'error' %}
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="icon fa fa-ban"></i> {{ message|capfirst }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% elif message.tags == 'warning' %}
|
||||
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
||||
<i class="icon fa fa-exclamation-triangle"></i> {{ message|capfirst }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% elif message.tags == 'info' %}
|
||||
<div class="alert alert-info alert-dismissible fade show" role="alert">
|
||||
<i class="icon fa fa-info"></i> {{ message|capfirst }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endblock messages %}
|
||||
<div class="row">
|
||||
{% block content %} {% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
</main>
|
||||
|
||||
{% block footer %}
|
||||
{% if not is_popup %}
|
||||
<footer class="app-footer {{ jazzmin_ui.footer_classes }}">
|
||||
<div class="float-end d-none d-sm-inline">
|
||||
<b>{% trans 'Jazzmin version' %}</b> {% get_jazzmin_version %}
|
||||
</div>
|
||||
{% autoescape off %}
|
||||
<strong>{% trans 'Copyright' %} © {% now 'Y' %} {{ jazzmin_settings.copyright }}.</strong> {% trans 'All rights reserved.' %}
|
||||
{% endautoescape %}
|
||||
</footer>
|
||||
{% if jazzmin_settings.show_ui_builder %}
|
||||
{% include 'jazzmin/includes/ui_builder_panel.html' %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
</div>
|
||||
|
||||
{% if jazzmin_settings.show_ui_builder %}
|
||||
<div id="codeBox" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{% trans 'UI Configuration' %}</h4>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{% trans 'Copy this info your settings file to persist these UI changes' %}</p>
|
||||
<pre><code></code></pre>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn {{ jazzmin_ui.button_classes.danger }}" data-bs-dismiss="modal">{% trans 'Close' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- jQuery -->
|
||||
<script src="{% static "admin/js/vendor/jquery/jquery.js" %}"></script>
|
||||
<!-- Bootstrap 5 -->
|
||||
<script src="{% static "vendor/bootstrap/js/bootstrap.bundle.min.js" %}"></script>
|
||||
<!-- AdminLTE App -->
|
||||
<script src="{% static "vendor/adminlte/js/adminlte.min.js" %}"></script>
|
||||
<!-- Django customisations -->
|
||||
<script src="{% static "jazzmin/js/main.js" %}"></script>
|
||||
|
||||
{% if jazzmin_settings.custom_js %}
|
||||
<script src="{% static jazzmin_settings.custom_js %}"></script>
|
||||
{% endif %}
|
||||
|
||||
{% if jazzmin_settings.show_ui_builder %}
|
||||
<script>
|
||||
window.ui_changes = {{ jazzmin_ui.raw|as_json|safe }};
|
||||
</script>
|
||||
<script src="{% static "jazzmin/js/ui-builder.js" %}"></script>
|
||||
{% endif %}
|
||||
|
||||
{% block extrajs %}{% endblock %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
{% load i18n static jazzmin admin_urls %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
{% get_current_language_bidi as LANGUAGE_BIDI %}
|
||||
{% get_jazzmin_settings request as jazzmin_settings %}
|
||||
{% get_jazzmin_ui_tweaks as jazzmin_ui %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ LANGUAGE_CODE|default:"en-us" }}" {% if LANGUAGE_BIDI %}dir="rtl"{% endif %}>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<script>
|
||||
(function() {
|
||||
var mode = localStorage.getItem('jazzmin-theme-mode') || '{{ jazzmin_ui.default_theme_mode|default:"light" }}';
|
||||
if (mode === 'auto') {
|
||||
mode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
document.documentElement.setAttribute('data-bs-theme', mode);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<title>{% block title %}{{ title }} | {{ jazzmin_settings.site_title }}{% endblock %}</title>
|
||||
|
||||
<!-- Font Awesome Icons -->
|
||||
<link rel="stylesheet" href="{% static "vendor/fontawesome-free/css/all.min.css" %}">
|
||||
|
||||
<!-- Bootstrap and adminLTE -->
|
||||
<link rel="stylesheet" href="{% static "vendor/adminlte/css/adminlte.min.css" %}">
|
||||
|
||||
<!-- Bootswatch theme -->
|
||||
{% if jazzmin_ui.theme.name != 'default' %}
|
||||
<link rel="stylesheet" href="{{ jazzmin_ui.theme.src }}" id="jazzmin-theme" />
|
||||
{% endif %}
|
||||
|
||||
<!-- Custom fixes for django -->
|
||||
<link rel="stylesheet" href="{% static "jazzmin/css/main.css" %}">
|
||||
|
||||
{% if jazzmin_settings.custom_css %}
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="{% static jazzmin_settings.custom_css %}">
|
||||
{% endif %}
|
||||
|
||||
<!-- favicons -->
|
||||
<link rel="shortcut icon" href="{% static jazzmin_settings.site_icon %}" type="image/png">
|
||||
<link rel="icon" href="{% static jazzmin_settings.site_icon %}" sizes="32x32" type="image/png">
|
||||
|
||||
{% if jazzmin_settings.use_google_fonts_cdn %}
|
||||
<!-- Google Font: Source Sans Pro -->
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% block extrastyle %} {% endblock %}
|
||||
{% block extrahead %} {% endblock %}
|
||||
</head>
|
||||
<body class="login-page bg-body-secondary">
|
||||
|
||||
<div class="login-box">
|
||||
<div class="login-logo">
|
||||
{% if jazzmin_settings.login_logo_dark %}
|
||||
<img class="logo-light" src="{% static jazzmin_settings.login_logo %}" alt="{{ jazzmin_settings.site_header }}">
|
||||
<img class="logo-dark" src="{% static jazzmin_settings.login_logo_dark %}" alt="{{ jazzmin_settings.site_header }}">
|
||||
{% elif jazzmin_settings.login_logo %}
|
||||
<img src="{% static jazzmin_settings.login_logo %}" alt="{{ jazzmin_settings.site_header }}">
|
||||
{% else %}
|
||||
<a href="{% url 'admin:index' %}"><b>{{ jazzmin_settings.site_header }}</b></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body login-card-body">
|
||||
{% block content %} {% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- jQuery -->
|
||||
<script src="{% static "admin/js/vendor/jquery/jquery.js" %}"></script>
|
||||
<!-- Bootstrap 5 -->
|
||||
<script src="{% static 'vendor/bootstrap/js/bootstrap.bundle.min.js' %}"></script>
|
||||
<!-- AdminLTE App -->
|
||||
<script src="{% static 'vendor/adminlte/js/adminlte.min.js' %}"></script>
|
||||
|
||||
{% if jazzmin_settings.custom_js %}
|
||||
<script src="{% static jazzmin_settings.custom_js %}"></script>
|
||||
{% endif %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user