├── js
└── admin-menu.js
├── demo
├── sample
│ ├── __init__.py
│ ├── migrations
│ │ ├── __init__.py
│ │ ├── 0002_auto_20200627_0950.py
│ │ └── 0001_initial.py
│ ├── static
│ │ └── logo.png
│ ├── models.py
│ └── admin.py
├── world
│ ├── __init__.py
│ ├── management
│ │ ├── __init__.py
│ │ └── commands
│ │ │ ├── __init__.py
│ │ │ └── load_world_data.py
│ ├── migrations
│ │ ├── __init__.py
│ │ ├── 0002_auto_20200627_0950.py
│ │ └── 0001_initial.py
│ ├── apps.py
│ ├── admin.py
│ └── models.py
├── kitchensink
│ ├── __init__.py
│ ├── urls.py
│ ├── wsgi.py
│ └── settings.py
├── requirements.txt
├── db.sqlite3
└── manage.py
├── admin_menu
├── templatetags
│ ├── __init__.py
│ ├── custom_admin_logo.py
│ ├── custom_admin_css.py
│ └── custom_admin_menu.py
├── __init__.py
├── templates
│ └── admin
│ │ └── base_site.html
└── sass
│ └── admin-menu.scss
├── requirements-dev.txt
├── screenshots
├── form.png
├── login.png
├── ui-red.png
├── drop-down.png
├── ui-dark.png
└── ui-green.png
├── MANIFEST.in
├── Makefile
├── .github
└── workflows
│ └── pythonpublish.yml
├── LICENSE
├── setup.py
├── .gitignore
└── README.md
/js/admin-menu.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/sample/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/world/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/kitchensink/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/world/management/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/world/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/admin_menu/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/sample/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/world/management/commands/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | build
2 | twine
3 |
--------------------------------------------------------------------------------
/admin_menu/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = '1.6'
2 |
--------------------------------------------------------------------------------
/demo/requirements.txt:
--------------------------------------------------------------------------------
1 | django~=4.0
2 | requests
3 |
--------------------------------------------------------------------------------
/demo/db.sqlite3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cdrx/django-admin-menu/HEAD/demo/db.sqlite3
--------------------------------------------------------------------------------
/screenshots/form.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cdrx/django-admin-menu/HEAD/screenshots/form.png
--------------------------------------------------------------------------------
/screenshots/login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cdrx/django-admin-menu/HEAD/screenshots/login.png
--------------------------------------------------------------------------------
/screenshots/ui-red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cdrx/django-admin-menu/HEAD/screenshots/ui-red.png
--------------------------------------------------------------------------------
/screenshots/drop-down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cdrx/django-admin-menu/HEAD/screenshots/drop-down.png
--------------------------------------------------------------------------------
/screenshots/ui-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cdrx/django-admin-menu/HEAD/screenshots/ui-dark.png
--------------------------------------------------------------------------------
/screenshots/ui-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cdrx/django-admin-menu/HEAD/screenshots/ui-green.png
--------------------------------------------------------------------------------
/demo/sample/static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cdrx/django-admin-menu/HEAD/demo/sample/static/logo.png
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | recursive-include admin_menu *
2 | recursive-exclude * *.pyc __pycache__ .DS_Store
3 | include README.md
4 | include LICENSE
5 |
--------------------------------------------------------------------------------
/demo/world/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class WorldApp(AppConfig):
5 | name = 'world'
6 | verbose_name = "Planet Earth"
7 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | setup:
2 | python3 -m pip install -r requirements-dev.txt
3 |
4 | build:
5 | python3 -m build
6 |
7 | release:
8 | python3 -m twine upload dist/*
9 |
10 | .PHONY: setup build release
11 |
--------------------------------------------------------------------------------
/admin_menu/templatetags/custom_admin_logo.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from django.conf import settings
3 |
4 | register = template.Library()
5 |
6 |
7 | @register.simple_tag
8 | def get_custom_logo():
9 | return getattr(settings, 'ADMIN_LOGO', None)
10 |
--------------------------------------------------------------------------------
/demo/kitchensink/urls.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.urls import path
3 | from django.views.generic import RedirectView
4 |
5 |
6 | urlpatterns = [
7 | path('admin/', admin.site.urls),
8 | path('/', RedirectView.as_view(url='/admin/')),
9 | ]
10 |
--------------------------------------------------------------------------------
/demo/kitchensink/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for kitchensink project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "kitchensink.settings")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/demo/world/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from world.models import City
4 |
5 |
6 | class CityAdmin(admin.ModelAdmin):
7 | search_fields = ('name', 'country__name')
8 | list_display = ('name', 'country', 'capital', 'continent')
9 | list_filter = ('capital',)
10 | list_per_page = 5
11 | fieldsets = [
12 | (None, {'fields': ['name', 'country', 'capital']}),
13 | ('Statistics', {
14 | 'description': 'EnclosedInput widget examples',
15 | 'fields': ['area', 'population']}),
16 | ]
17 |
18 | def continent(self, obj):
19 | return obj.country.continent
20 |
21 |
22 | admin.site.register(City, CityAdmin)
23 |
--------------------------------------------------------------------------------
/demo/world/migrations/0002_auto_20200627_0950.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0 on 2020-06-27 09:50
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('world', '0001_initial'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='continent',
16 | name='code',
17 | field=models.CharField(default='EU', help_text='Two letter continent code', max_length=2),
18 | preserve_default=False,
19 | ),
20 | migrations.AlterField(
21 | model_name='country',
22 | name='continent',
23 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='world.Continent'),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/demo/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "kitchensink.settings")
7 | try:
8 | from django.core.management import execute_from_command_line
9 | except ImportError:
10 | # The above import may fail for some other reason. Ensure that the
11 | # issue is really that Django is missing to avoid masking other
12 | # exceptions on Python 2.
13 | try:
14 | import django
15 | except ImportError:
16 | raise ImportError(
17 | "Couldn't import Django. Are you sure it's installed and "
18 | "available on your PYTHONPATH environment variable? Did you "
19 | "forget to activate a virtual environment?"
20 | )
21 | raise
22 | execute_from_command_line(sys.argv)
23 |
--------------------------------------------------------------------------------
/demo/sample/migrations/0002_auto_20200627_0950.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0 on 2020-06-27 09:50
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('world', '0002_auto_20200627_0950'),
11 | ('sample', '0001_initial'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name='kitchensink',
17 | name='linked_foreign_key',
18 | field=models.ForeignKey(limit_choices_to={'continent__name': 'Europe'}, on_delete=django.db.models.deletion.CASCADE, related_name='foreign_key_linked', to='world.Country'),
19 | ),
20 | migrations.AlterField(
21 | model_name='kitchensink',
22 | name='raw_id_field',
23 | field=models.ForeignKey(blank=True, help_text='Regular raw ID field', null=True, on_delete=django.db.models.deletion.SET_NULL, to='world.Country'),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/.github/workflows/pythonpublish.yml:
--------------------------------------------------------------------------------
1 | # This workflows will upload a Python Package using Twine when a release is created
2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
3 |
4 | name: Upload Python Package
5 |
6 | on:
7 | release:
8 | types: [created, published]
9 |
10 | jobs:
11 | deploy:
12 |
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v2
17 | - name: Set up Python
18 | uses: actions/setup-python@v1
19 | with:
20 | python-version: '3.x'
21 | - name: Install dependencies
22 | run: |
23 | python -m pip install --upgrade pip
24 | pip install setuptools wheel twine
25 | - name: Build and publish
26 | env:
27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
29 | run: |
30 | python setup.py sdist bdist_wheel
31 | twine upload dist/*
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Chris R
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/demo/world/management/commands/load_world_data.py:
--------------------------------------------------------------------------------
1 | from functools import lru_cache
2 |
3 | import requests
4 | from django.core.management.base import BaseCommand
5 |
6 | from world.models import Country, Continent
7 |
8 |
9 | @lru_cache(maxsize=10)
10 | def get_content(name):
11 | code = name.upper()[0:2]
12 | return Continent.objects.get_or_create(name=name, code=code)[0]
13 |
14 |
15 | class Command(BaseCommand):
16 | help = 'Loads sample data from restcountries.eu'
17 |
18 | def handle(self, *args, **options):
19 | data = requests.get("https://restcountries.eu/rest/v2/all").json()
20 |
21 | for c in data:
22 | country, _ = Country.objects.update_or_create(code=c.get('alpha2Code'), defaults={
23 | 'name': c.get('name'),
24 | 'continent': get_content(c.get('region')),
25 | 'area': c.get('area'),
26 | 'population': c.get('population'),
27 | })
28 |
29 | country.city_set.update_or_create(name=c.get('capital'), defaults={
30 | 'capital': True
31 | })
32 |
33 | print(".", end='')
34 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 |
4 | def readme():
5 | with open('README.md') as f:
6 | return f.read()
7 |
8 |
9 | setup(name='django-admin-menu',
10 | version=__import__('admin_menu').__version__,
11 | description='A Django admin theme with a horizontal, tabbed navigation bar',
12 | long_description=readme(),
13 | long_description_content_type="text/markdown",
14 | url='http://github.com/cdrx/django-admin-menu',
15 | author='Chris Rose',
16 | license='MIT',
17 | packages=['admin_menu'],
18 | install_requires=[
19 | 'libsass>=0.20,<=1.0'
20 | ],
21 | zip_safe=False,
22 | keywords=['django', 'admin', 'theme', 'interface', 'menu', 'navigation'],
23 | include_package_data=True,
24 | classifiers=[
25 | 'Development Status :: 5 - Production/Stable',
26 | 'Environment :: Web Environment',
27 | 'Framework :: Django',
28 | 'Intended Audience :: Developers',
29 | 'Operating System :: OS Independent',
30 | 'Programming Language :: Python',
31 | 'Programming Language :: Python :: 3',
32 | 'Programming Language :: Python :: 3.5',
33 | ],
34 | )
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 |
27 | # PyInstaller
28 | # Usually these files are written by a python script from a template
29 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
30 | *.manifest
31 | *.spec
32 |
33 | # Installer logs
34 | pip-log.txt
35 | pip-delete-this-directory.txt
36 |
37 | # Unit test / coverage reports
38 | htmlcov/
39 | .tox/
40 | .coverage
41 | .coverage.*
42 | .cache
43 | nosetests.xml
44 | coverage.xml
45 | *,cover
46 | .hypothesis/
47 |
48 | # Translations
49 | *.mo
50 | *.pot
51 |
52 | # Django stuff:
53 | *.log
54 | local_settings.py
55 |
56 | # Flask stuff:
57 | instance/
58 | .webassets-cache
59 |
60 | # Scrapy stuff:
61 | .scrapy
62 |
63 | # Sphinx documentation
64 | docs/_build/
65 |
66 | # PyBuilder
67 | target/
68 |
69 | # IPython Notebook
70 | .ipynb_checkpoints
71 |
72 | # pyenv
73 | .python-version
74 |
75 | # celery beat schedule file
76 | celerybeat-schedule
77 |
78 | # dotenv
79 | .env
80 |
81 | # virtualenv
82 | venv/
83 | ENV/
84 |
85 | # Spyder project settings
86 | .spyderproject
87 |
88 | # Rope project settings
89 | .ropeproject
90 |
91 | node_modules
92 |
93 | .idea
94 |
--------------------------------------------------------------------------------
/demo/world/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 |
4 | class Continent(models.Model):
5 | name = models.CharField(max_length=256)
6 | code = models.CharField(max_length=2, help_text='Two letter continent code')
7 |
8 | def __str__(self):
9 | return self.name
10 |
11 | class Meta:
12 | ordering = ['name']
13 |
14 |
15 | class Country(models.Model):
16 | name = models.CharField(max_length=256)
17 | code = models.CharField(max_length=2, help_text='ISO 3166-1 alpha-2 - two character country code')
18 | independence_day = models.DateField(blank=True, null=True)
19 | continent = models.ForeignKey(Continent, null=True, on_delete=models.SET_NULL)
20 | area = models.BigIntegerField(blank=True, null=True)
21 | population = models.BigIntegerField(blank=True, null=True)
22 | order = models.PositiveIntegerField(default=0)
23 | description = models.TextField(blank=True, help_text='Try and enter few some more lines')
24 | architecture = models.TextField(blank=True)
25 |
26 | def __str__(self):
27 | return self.name
28 |
29 | class Meta:
30 | ordering = ['name']
31 | verbose_name_plural = "Countries"
32 |
33 |
34 | class City(models.Model):
35 | name = models.CharField(max_length=64)
36 | country = models.ForeignKey(Country, on_delete=models.CASCADE)
37 | capital = models.BooleanField()
38 | area = models.BigIntegerField(blank=True, null=True)
39 | population = models.BigIntegerField(blank=True, null=True)
40 |
41 | def __str__(self):
42 | return self.name
43 |
44 | class Meta:
45 | verbose_name_plural = "Cities"
46 | unique_together = ('name', 'country')
47 |
--------------------------------------------------------------------------------
/admin_menu/templatetags/custom_admin_css.py:
--------------------------------------------------------------------------------
1 | import os.path
2 | from collections import OrderedDict
3 |
4 | import sass
5 | from django import template
6 | from django.conf import settings
7 |
8 | register = template.Library()
9 |
10 | _compiled_sass = None
11 |
12 |
13 | def sass_variable_defaults():
14 | return (
15 | ('background', 'white'),
16 | ('primary-color', '#205280'),
17 | ('primary-text', '#d6d5d2'),
18 | ('secondary-color', '#3B75AD'),
19 | ('secondary-text', 'white'),
20 | ('tertiary-color', '#F2F9FC'),
21 | ('tertiary-text', 'black'),
22 | ('breadcrumb-color', 'whitesmoke'),
23 | ('breadcrumb-text', 'black'),
24 | ('focus-color', '#eaeaea'),
25 | ('focus-text', '#666'),
26 | ('primary-button', '#26904A'),
27 | ('primary-button-text', 'white'),
28 | ('secondary-button', '#999'),
29 | ('secondary-button-text', 'white'),
30 | ('link-color', '#333'),
31 | ('link-color-hover', 'lighten($link-color, 20%)'),
32 | ('logo-width', 'auto'),
33 | ('logo-height', '35px'),
34 | )
35 |
36 |
37 | def sass_variables():
38 | variables = OrderedDict(sass_variable_defaults())
39 | custom = getattr(settings, 'ADMIN_STYLE', {})
40 | for v, c in custom.items():
41 | variables[v] = c
42 |
43 | sass = ""
44 | for v, c in variables.items():
45 | sass += "$%s: %s;\n" % (
46 | v, c
47 | )
48 |
49 | return sass
50 |
51 |
52 | def get_sass_source():
53 | src = os.path.join(os.path.dirname(__file__), '..', 'sass', 'admin-menu.scss')
54 | with open(src) as f:
55 | sass = f.read()
56 |
57 | variables = sass_variables()
58 |
59 | return "%s\n\n%s" % (
60 | variables,
61 | sass
62 | )
63 |
64 |
65 | @register.simple_tag
66 | def get_custom_admin_css():
67 | if settings.DEBUG:
68 | return sass.compile(string=get_sass_source())
69 |
70 | # cache the css in production
71 | global _compiled_sass
72 | if not _compiled_sass:
73 | _compiled_sass = sass.compile(string=get_sass_source())
74 | return _compiled_sass
75 |
--------------------------------------------------------------------------------
/admin_menu/templates/admin/base_site.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/base.html" %}
2 |
3 | {% load static i18n custom_admin_menu custom_admin_logo custom_admin_css %}
4 |
5 | {% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
6 |
7 | {% block blockbots %}
8 | {{ block.super }}
9 |
13 | {% endblock %}
14 |
15 | {% block nav-global %}
16 | {% get_admin_menu as menu %}
17 |
18 | {% if menu %}
19 |
20 |
40 | {% endif %}
41 |
42 | {% for title, item in menu.items %}
43 | {% if item.active and item.children|length > 1 %}
44 |
45 |
46 | {% for sub in item.children %}
47 | - {{ sub.title }}
48 | {% endfor %}
49 |
50 |
51 | {% endif %}
52 | {% endfor %}
53 |
54 | {% endblock %}
55 |
56 | {% block branding %}
57 | {% get_custom_logo as brandmark %}
58 | {% if brandmark %}
59 |
60 | {% else %}
61 |
62 | {% endif %}
63 | {% endblock %}
64 |
--------------------------------------------------------------------------------
/demo/world/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10 on 2016-12-02 15:40
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 | import django.db.models.deletion
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | initial = True
12 |
13 | dependencies = [
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='City',
19 | fields=[
20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21 | ('name', models.CharField(max_length=64)),
22 | ('capital', models.BooleanField()),
23 | ('area', models.BigIntegerField(blank=True, null=True)),
24 | ('population', models.BigIntegerField(blank=True, null=True)),
25 | ],
26 | options={
27 | 'verbose_name_plural': 'Cities',
28 | },
29 | ),
30 | migrations.CreateModel(
31 | name='Continent',
32 | fields=[
33 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
34 | ('name', models.CharField(max_length=256)),
35 | ],
36 | options={
37 | 'ordering': ['name'],
38 | },
39 | ),
40 | migrations.CreateModel(
41 | name='Country',
42 | fields=[
43 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
44 | ('name', models.CharField(max_length=256)),
45 | ('code', models.CharField(help_text='ISO 3166-1 alpha-2 - two character country code', max_length=2)),
46 | ('independence_day', models.DateField(blank=True, null=True)),
47 | ('area', models.BigIntegerField(blank=True, null=True)),
48 | ('population', models.BigIntegerField(blank=True, null=True)),
49 | ('order', models.PositiveIntegerField(default=0)),
50 | ('description', models.TextField(blank=True, help_text='Try and enter few some more lines')),
51 | ('architecture', models.TextField(blank=True)),
52 | ('continent', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='world.Continent')),
53 | ],
54 | options={
55 | 'ordering': ['name'],
56 | 'verbose_name_plural': 'Countries',
57 | },
58 | ),
59 | migrations.AddField(
60 | model_name='city',
61 | name='country',
62 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='world.Country'),
63 | ),
64 | migrations.AlterUniqueTogether(
65 | name='city',
66 | unique_together=set([('name', 'country')]),
67 | ),
68 | ]
69 |
--------------------------------------------------------------------------------
/demo/kitchensink/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for kitchensink project.
3 |
4 | Generated by 'django-admin startproject' using Django 1.10.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.10/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/1.10/ref/settings/
11 | """
12 |
13 | import os
14 |
15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17 |
18 | # Quick-start development settings - unsuitable for production
19 | # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
20 |
21 | # SECURITY WARNING: keep the secret key used in production secret!
22 | SECRET_KEY = '*ek!vegh7=9-bwj(e_)(h171=*t+dm=%w_cr4sbc$=@ay(!&r#'
23 |
24 | # SECURITY WARNING: don't run with debug turned on in production!
25 | DEBUG = True
26 |
27 | ALLOWED_HOSTS = []
28 |
29 | # Application definition
30 |
31 | INSTALLED_APPS = [
32 | 'admin_menu',
33 |
34 | 'django.contrib.admin',
35 | 'django.contrib.auth',
36 | 'django.contrib.contenttypes',
37 | 'django.contrib.sessions',
38 | 'django.contrib.messages',
39 | 'django.contrib.staticfiles',
40 |
41 | 'sample',
42 | 'world.apps.WorldApp'
43 | ]
44 |
45 | MIDDLEWARE = [
46 | 'django.middleware.security.SecurityMiddleware',
47 | 'django.contrib.sessions.middleware.SessionMiddleware',
48 | 'django.middleware.common.CommonMiddleware',
49 | 'django.middleware.csrf.CsrfViewMiddleware',
50 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
51 | 'django.contrib.messages.middleware.MessageMiddleware',
52 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
53 | ]
54 |
55 | ROOT_URLCONF = 'kitchensink.urls'
56 |
57 | TEMPLATES = [
58 | {
59 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
60 | 'DIRS': [],
61 | 'APP_DIRS': True,
62 | 'OPTIONS': {
63 | 'context_processors': [
64 | 'django.template.context_processors.debug',
65 | 'django.template.context_processors.request',
66 | 'django.contrib.auth.context_processors.auth',
67 | 'django.contrib.messages.context_processors.messages',
68 | ],
69 | },
70 | },
71 | ]
72 |
73 | WSGI_APPLICATION = 'kitchensink.wsgi.application'
74 |
75 | # Database
76 | # https://docs.djangoproject.com/en/1.10/ref/settings/#databases
77 |
78 | DATABASES = {
79 | 'default': {
80 | 'ENGINE': 'django.db.backends.sqlite3',
81 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
82 | }
83 | }
84 |
85 | # Password validation
86 | # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
87 |
88 | AUTH_PASSWORD_VALIDATORS = [
89 |
90 | ]
91 |
92 | # Internationalization
93 | # https://docs.djangoproject.com/en/1.10/topics/i18n/
94 |
95 | LANGUAGE_CODE = 'en-us'
96 |
97 | TIME_ZONE = 'UTC'
98 |
99 | USE_I18N = True
100 |
101 | USE_L10N = True
102 |
103 | USE_TZ = True
104 |
105 | # Static files (CSS, JavaScript, Images)
106 | # https://docs.djangoproject.com/en/1.10/howto/static-files/
107 |
108 | STATIC_URL = '/static/'
109 |
110 | ADMIN_LOGO = 'logo.png'
111 |
112 | ADMIN_STYLE = {
113 | 'primary-color': '#2B3746',
114 | 'secondary-color': '#354151',
115 | 'tertiary-color': '#F2F9FC',
116 | 'logo-width': 'auto',
117 | 'logo-height': '35px'
118 | }
119 |
120 | MENU_WEIGHT = {
121 | 'Auth': 100
122 | }
123 |
--------------------------------------------------------------------------------
/demo/sample/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | from world.models import Country
4 |
5 | TYPE_CHOICES = ((1, 'Awesome'), (2, 'Good'), (3, 'Normal'), (4, 'Bad'))
6 | TYPE_CHOICES2 = ((1, 'Hot'), (2, 'Normal'), (3, 'Cold'))
7 | TYPE_CHOICES3 = ((1, 'Tall'), (2, 'Normal'), (3, 'Short'))
8 |
9 |
10 | class KitchenSink(models.Model):
11 | name = models.CharField(max_length=64)
12 | help_text = models.CharField(max_length=64, help_text="Enter fully qualified name")
13 | multiple_in_row = models.CharField(max_length=64, help_text='Help text for multiple')
14 | multiple2 = models.CharField(max_length=10, blank=True)
15 | textfield = models.TextField(blank=True, verbose_name='Autosized textarea', help_text='Try and enter few some more lines')
16 |
17 | file = models.FileField(upload_to='.', blank=True)
18 | readonly_field = models.CharField(max_length=127, default='Some value here')
19 |
20 | date = models.DateField(blank=True, null=True)
21 | date_and_time = models.DateTimeField(blank=True, null=True)
22 |
23 | date_widget = models.DateField(blank=True, null=True)
24 | datetime_widget = models.DateTimeField(blank=True, null=True)
25 |
26 | boolean = models.BooleanField(default=True)
27 | boolean_with_help = models.BooleanField(help_text="Boolean field with help text")
28 |
29 | horizontal_choices = models.SmallIntegerField(choices=TYPE_CHOICES, default=1, help_text='Horizontal choices look like this')
30 | vertical_choices = models.SmallIntegerField(choices=TYPE_CHOICES2, default=2, help_text="Some help on vertical choices")
31 | choices = models.SmallIntegerField(choices=TYPE_CHOICES3, default=3, help_text="Help text")
32 | hidden_checkbox = models.BooleanField()
33 | hidden_choice = models.SmallIntegerField(choices=TYPE_CHOICES3, default=2, blank=True)
34 | hidden_charfield = models.CharField(max_length=64, blank=True)
35 | hidden_charfield2 = models.CharField(max_length=64, blank=True)
36 |
37 | country = models.ForeignKey(Country, related_name='foreign_key_country', on_delete=models.CASCADE)
38 | linked_foreign_key = models.ForeignKey(Country, limit_choices_to={'continent__name': 'Europe'}, related_name='foreign_key_linked', on_delete=models.CASCADE)
39 | raw_id_field = models.ForeignKey(Country, help_text='Regular raw ID field', null=True, blank=True, on_delete=models.SET_NULL)
40 |
41 | enclosed1 = models.CharField(max_length=64, blank=True)
42 | enclosed2 = models.CharField(max_length=64, blank=True)
43 |
44 | def __unicode__(self):
45 | return self.name
46 |
47 |
48 | # Inline model for KitchenSink
49 | class Fridge(models.Model):
50 | kitchensink = models.ForeignKey(KitchenSink, on_delete=models.CASCADE)
51 | name = models.CharField(max_length=64)
52 | type = models.SmallIntegerField(choices=TYPE_CHOICES3)
53 | description = models.TextField(blank=True)
54 | is_quiet = models.BooleanField()
55 | order = models.PositiveIntegerField()
56 |
57 | class Meta:
58 | ordering = ('order',)
59 |
60 | def __unicode__(self):
61 | return self.name
62 |
63 |
64 | # Inline model for KitchenSink
65 | class Microwave(models.Model):
66 | kitchensink = models.ForeignKey(KitchenSink, on_delete=models.CASCADE)
67 | name = models.CharField(max_length=64)
68 | type = models.SmallIntegerField(choices=TYPE_CHOICES3, default=2,
69 | help_text='Choose wisely')
70 | is_compact = models.BooleanField()
71 | order = models.PositiveIntegerField()
72 |
73 | class Meta:
74 | ordering = ('order',)
75 |
76 | def __unicode__(self):
77 | return self.name
78 |
--------------------------------------------------------------------------------
/demo/sample/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Inlines for KitchenSink
4 | from sample.models import Fridge, Microwave, KitchenSink
5 | from world.models import Country, Continent, City
6 |
7 |
8 | class CountryInline(admin.StackedInline):
9 | model = Country
10 | fields = ('name', 'code', 'population',)
11 | extra = 1
12 | verbose_name_plural = 'Countries'
13 |
14 |
15 | @admin.register(Continent)
16 | class ContinentAdmin(admin.ModelAdmin):
17 | search_fields = ('name',)
18 | list_display = ('name', 'countries')
19 | inlines = (CountryInline,)
20 |
21 | def countries(self, obj):
22 | return len(obj.country_set.all())
23 |
24 |
25 | class CityInline(admin.TabularInline):
26 | model = City
27 | extra = 3
28 | verbose_name_plural = 'Cities'
29 | suit_classes = 'suit-tab suit-tab-cities'
30 |
31 |
32 | @admin.register(Country)
33 | class CountryAdmin(admin.ModelAdmin):
34 | search_fields = ('name', 'code')
35 | list_display = ('name', 'code', 'continent', 'independence_day')
36 | list_filter = ('continent',)
37 | date_hierarchy = 'independence_day'
38 | list_select_related = True
39 |
40 | inlines = (CityInline,)
41 |
42 | fieldsets = [
43 | (None, {
44 | 'fields': ['name', 'continent', 'code', 'independence_day']
45 | }),
46 | ('Statistics', {
47 | 'fields': ['area', 'population']}),
48 | ('Textarea', {
49 | 'description': 'Textarea widget example',
50 | 'fields': ['description']}),
51 | ('Architecture', {
52 | 'fields': ['architecture']}),
53 | ]
54 |
55 |
56 | class FridgeInline(admin.TabularInline):
57 | model = Fridge
58 | extra = 1
59 | verbose_name_plural = 'Fridges (Tabular inline)'
60 |
61 |
62 | class MicrowaveInline(admin.StackedInline):
63 | model = Microwave
64 | extra = 1
65 | verbose_name_plural = 'Microwaves (Stacked inline)'
66 |
67 |
68 | @admin.register(KitchenSink)
69 | class KitchenSinkAdmin(admin.ModelAdmin):
70 | inlines = (FridgeInline, MicrowaveInline)
71 | search_fields = ['name']
72 | radio_fields = {"horizontal_choices": admin.HORIZONTAL,
73 | 'vertical_choices': admin.VERTICAL}
74 | list_editable = ('boolean',)
75 | list_filter = ('choices', 'date')
76 | readonly_fields = ('readonly_field',)
77 | raw_id_fields = ('raw_id_field',)
78 | fieldsets = [
79 | (None, {'fields': ['name', 'help_text', 'textfield',
80 | ('multiple_in_row', 'multiple2'),
81 | 'file', 'readonly_field']}),
82 | ('Date and time', {
83 | 'fields': ['date_widget', 'datetime_widget']}),
84 |
85 | ('Foreign key relations',
86 | {'description': 'Original select and linked select feature',
87 | 'fields': ['country', 'linked_foreign_key', 'raw_id_field']}),
88 |
89 | ('EnclosedInput widget',
90 | {
91 | 'fields': ['enclosed1', 'enclosed2']}),
92 |
93 | ('Boolean and choices',
94 | {'fields': ['boolean', 'boolean_with_help', 'choices',
95 | 'horizontal_choices', 'vertical_choices']}),
96 |
97 | ('Collapsed settings', {
98 | 'classes': ('collapse',),
99 | 'fields': ['hidden_checkbox', 'hidden_choice']}),
100 | ('And one more collapsable', {
101 | 'classes': ('collapse',),
102 | 'fields': ['hidden_charfield', 'hidden_charfield2']}),
103 |
104 | ]
105 | list_display = (
106 | 'name', 'help_text', 'choices', 'horizontal_choices', 'boolean')
107 |
108 | def get_formsets(self, request, obj=None):
109 | """
110 | Set extra=0 for inlines if object already exists
111 | """
112 | for inline in self.get_inline_instances(request):
113 | formset = inline.get_formset(request, obj)
114 | if obj:
115 | formset.extra = 0
116 | yield formset
117 |
--------------------------------------------------------------------------------
/demo/sample/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10 on 2016-12-02 15:41
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 | import django.db.models.deletion
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | initial = True
12 |
13 | dependencies = [
14 | ('world', '0001_initial'),
15 | ]
16 |
17 | operations = [
18 | migrations.CreateModel(
19 | name='Fridge',
20 | fields=[
21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22 | ('name', models.CharField(max_length=64)),
23 | ('type', models.SmallIntegerField(choices=[(1, 'Tall'), (2, 'Normal'), (3, 'Short')])),
24 | ('description', models.TextField(blank=True)),
25 | ('is_quiet', models.BooleanField()),
26 | ('order', models.PositiveIntegerField()),
27 | ],
28 | options={
29 | 'ordering': ('order',),
30 | },
31 | ),
32 | migrations.CreateModel(
33 | name='KitchenSink',
34 | fields=[
35 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
36 | ('name', models.CharField(max_length=64)),
37 | ('help_text', models.CharField(help_text='Enter fully qualified name', max_length=64)),
38 | ('multiple_in_row', models.CharField(help_text='Help text for multiple', max_length=64)),
39 | ('multiple2', models.CharField(blank=True, max_length=10)),
40 | ('textfield', models.TextField(blank=True, help_text='Try and enter few some more lines', verbose_name='Autosized textarea')),
41 | ('file', models.FileField(blank=True, upload_to='.')),
42 | ('readonly_field', models.CharField(default='Some value here', max_length=127)),
43 | ('date', models.DateField(blank=True, null=True)),
44 | ('date_and_time', models.DateTimeField(blank=True, null=True)),
45 | ('date_widget', models.DateField(blank=True, null=True)),
46 | ('datetime_widget', models.DateTimeField(blank=True, null=True)),
47 | ('boolean', models.BooleanField(default=True)),
48 | ('boolean_with_help', models.BooleanField(help_text='Boolean field with help text')),
49 | ('horizontal_choices', models.SmallIntegerField(choices=[(1, 'Awesome'), (2, 'Good'), (3, 'Normal'), (4, 'Bad')], default=1, help_text='Horizontal choices look like this')),
50 | ('vertical_choices', models.SmallIntegerField(choices=[(1, 'Hot'), (2, 'Normal'), (3, 'Cold')], default=2, help_text='Some help on vertical choices')),
51 | ('choices', models.SmallIntegerField(choices=[(1, 'Tall'), (2, 'Normal'), (3, 'Short')], default=3, help_text='Help text')),
52 | ('hidden_checkbox', models.BooleanField()),
53 | ('hidden_choice', models.SmallIntegerField(blank=True, choices=[(1, 'Tall'), (2, 'Normal'), (3, 'Short')], default=2)),
54 | ('hidden_charfield', models.CharField(blank=True, max_length=64)),
55 | ('hidden_charfield2', models.CharField(blank=True, max_length=64)),
56 | ('enclosed1', models.CharField(blank=True, max_length=64)),
57 | ('enclosed2', models.CharField(blank=True, max_length=64)),
58 | ('country', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='foreign_key_country', to='world.Country')),
59 | ('linked_foreign_key', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='foreign_key_linked', to='world.Country')),
60 | ('raw_id_field', models.ForeignKey(blank=True, help_text='Regular raw ID field', null=True, on_delete=django.db.models.deletion.CASCADE, to='world.Country')),
61 | ],
62 | ),
63 | migrations.CreateModel(
64 | name='Microwave',
65 | fields=[
66 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
67 | ('name', models.CharField(max_length=64)),
68 | ('type', models.SmallIntegerField(choices=[(1, 'Tall'), (2, 'Normal'), (3, 'Short')], default=2, help_text='Choose wisely')),
69 | ('is_compact', models.BooleanField()),
70 | ('order', models.PositiveIntegerField()),
71 | ('kitchensink', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sample.KitchenSink')),
72 | ],
73 | options={
74 | 'ordering': ('order',),
75 | },
76 | ),
77 | migrations.AddField(
78 | model_name='fridge',
79 | name='kitchensink',
80 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sample.KitchenSink'),
81 | ),
82 | ]
83 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Django Admin Menu Theme
2 |
3 | An alternative theme for the Django admin that has a horizontal navigation bar with drop down menus for your models. Fully themeable from `settings.py`.
4 |
5 | [](https://pepy.tech/project/django-admin-menu)
6 |
7 | 
8 |
9 | ## Installation
10 |
11 | Install the package:
12 |
13 | ```
14 | pip install django-admin-menu
15 | ```
16 |
17 | Then add `admin_menu` to your `INSTALLED_APPS` setting, **before `django.contrib.admin`** (or it wont work). For example:
18 |
19 | ```
20 | INSTALLED_APPS = [
21 | 'admin_menu',
22 | 'django.contrib.admin',
23 | ...
24 | ]
25 | ```
26 |
27 | ## Settings
28 |
29 | There are a couple of options you can adjust in your `settings.py` to influence the theme.
30 |
31 | To adjust the logo, change:
32 | ```
33 | ADMIN_LOGO = 'logo.png'
34 | ```
35 |
36 | The logo is used in the top left of each page and on the login page.
37 |
38 | You can adjust the order of the menu items with the `MENU_WEIGHT` setting:
39 |
40 | ```
41 | MENU_WEIGHT = {
42 | 'World': 20,
43 | 'Auth': 4,
44 | 'Sample': 5
45 | }
46 | ```
47 |
48 | Items with a higher weight will be pushed to the end of the menu. You don't have to fill in all the menu items, just the ones you would like to adjust the position of.
49 |
50 | ### ModelAdmin Settings
51 |
52 | There are a few settings on your `ModelAdmin` class to adjust the menu:
53 |
54 | ```
55 | class MyAdmin(admin.ModelAdmin):
56 | menu_title = "Users"
57 | menu_group = "Staff"
58 | ```
59 |
60 | will change the title for this model to `Users` and place it on a drop down titled `Staff`.
61 |
62 | You can use the same `menu_group` on multiple `ModelAdmin` classes and they will be grouped on the same menu.
63 |
64 | ## Screenshots
65 |
66 | 
67 | 
68 | 
69 |
70 | ## Theming
71 |
72 | To adjust the theme, you can add and edit these options in your project's `settings.py` file:
73 |
74 | ```
75 | ADMIN_STYLE = {
76 | 'primary-color': '#164B36',
77 | 'secondary-color': '#092117',
78 | 'tertiary-color': '#51B48E'
79 | }
80 | ```
81 |
82 | These variables are usually enough to add a brand flavour to the admin. There are other variables you can add, to change text colour etc. These are listed under Custom Theme.
83 |
84 | ### Dark Theme
85 |
86 | 
87 |
88 | Add to your settings.py:
89 |
90 | ```
91 | ADMIN_STYLE = {
92 | 'primary-color': '#2B3746',
93 | 'secondary-color': '#354151',
94 | 'tertiary-color': '#F2F9FC'
95 | }
96 | ```
97 |
98 | ### Django Theme
99 |
100 | 
101 |
102 | Add to your settings.py:
103 |
104 | ```
105 | ADMIN_STYLE = {
106 | 'primary-color': '#164B36',
107 | 'secondary-color': '#092117',
108 | 'tertiary-color': '#51B48E'
109 | }
110 | ```
111 |
112 | ### Red Theme
113 |
114 | 
115 |
116 | Add to your settings.py:
117 |
118 | ```
119 | ADMIN_STYLE = {
120 | 'primary-color': '#B42D33',
121 | 'secondary-color': '#000000',
122 | 'tertiary-color': '#333333'
123 | }
124 | ```
125 |
126 | ### Custom Themes
127 |
128 | You can customise the theme however you like, using these available variables:
129 |
130 | ```
131 | ADMIN_STYLE = {
132 | 'background': 'white',
133 | 'primary-color': '#205280',
134 | 'primary-text': '#d6d5d2',
135 | 'secondary-color': '#3B75AD',
136 | 'secondary-text': 'white',
137 | 'tertiary-color': '#F2F9FC',
138 | 'tertiary-text': 'black',
139 | 'breadcrumb-color': 'whitesmoke',
140 | 'breadcrumb-text': 'black',
141 | 'focus-color': '#eaeaea',
142 | 'focus-text': '#666',
143 | 'primary-button': '#26904A',
144 | 'primary-button-text':' white',
145 | 'secondary-button': '#999',
146 | 'secondary-button-text': 'white',
147 | 'link-color': '#333',
148 | 'link-color-hover': 'lighten($link-color, 20%)',
149 | 'logo-width': 'auto',
150 | 'logo-height': '35px'
151 | }
152 | ```
153 |
154 | ## History
155 |
156 | #### [1.0] - 2016-12-05
157 | First release, works.
158 |
159 | #### [1.1] - 2016-12-16
160 | Added theming support.
161 |
162 | #### [1.2] - 2020-04-06
163 | * Added support for Django 3.0+.
164 | * Made the `ADMIN_LOGO` setting optional.
165 | * Allowed adjusting admin logo size with `logo-width` and `logo-height` style settings.
166 |
167 | #### [1.3] - 2020-06-27
168 | * Reworked the pagination style to look more inline with the table style
169 | * Fixed an issue where the `verbose_name` from the `AppConfig` class wasn't used in the menu
170 |
171 | #### [1.4] - 2020-11-16
172 | * Fixed a bug where the Dashboard icon would be highlighted even if another tab was active
173 | * Display the admin title as text if no logo is defined in the settings
174 |
175 | #### [1.5] - 2021-02-14
176 | * Fixed compatibility with Django 3+ (thanks to arturgsb)
177 |
178 | #### [1.6] - 2022-02-01
179 | * Fixed compatibility with Django 4
180 | * Added support for Django's view permission (thanks @cobia)
181 | * Fixed menu height on mobile (thanks @mojek)
182 |
183 | #### [unreleased]
184 |
185 | ## License
186 |
187 | MIT
188 |
--------------------------------------------------------------------------------
/admin_menu/sass/admin-menu.scss:
--------------------------------------------------------------------------------
1 | // variables come from the templatetag
2 |
3 | body {
4 | background-color: $background;
5 | }
6 |
7 | #container {
8 | height: auto !important;
9 | min-height: auto !important;
10 | }
11 |
12 | #toggle-nav-sidebar, #nav-sidebar {
13 | display: none !important;
14 | }
15 |
16 | #header {
17 | background-color: $primary-color;
18 |
19 | #site-name a, #user-tools {
20 | color: darken($primary-text, 10%);
21 | }
22 |
23 | #branding {
24 | img {
25 | margin-top: 8px;
26 | height: $logo-height;
27 | width: $logo-width;
28 | }
29 | }
30 | }
31 |
32 | #admin_menu_container {
33 | background-color: $primary-color;
34 | padding: 10px 40px 0 40px;
35 | min-height: 40px;
36 |
37 | .tabs {
38 | clear: left;
39 | background-color: $primary-color;
40 |
41 | ul {
42 | position: relative;
43 | float: left;
44 | margin: 0;
45 | padding: 0;
46 |
47 | li {
48 | list-style: none;
49 | position: relative;
50 | float: left;
51 | margin: 0;
52 | padding: 0;
53 |
54 | a {
55 | display: block;
56 | color: $secondary-text;
57 | background-color: $secondary-color;
58 |
59 | padding: 10px 20px;
60 |
61 | border-right: 1px solid darken($secondary-color, 3%);
62 | border-bottom: none;
63 |
64 | &:hover {
65 | background: lighten($secondary-color, 3%);
66 | text-decoration: none;
67 | }
68 | }
69 |
70 | &.active a {
71 | background: $breadcrumb-color;
72 | color: $breadcrumb-text;
73 | }
74 |
75 | &.active.has-child a {
76 | background: $tertiary-color;
77 | }
78 |
79 | &.has-child a {
80 | padding-right: 30px;
81 | }
82 |
83 | &:first-child a {
84 | border-radius: 4px 0 0 0;
85 | }
86 |
87 | &:last-child a {
88 | border-radius: 0 4px 0 0;
89 | }
90 |
91 | &.active {
92 | .arrow {
93 | border-top: 5px solid $breadcrumb-text;
94 | }
95 | }
96 |
97 | &:hover ul {
98 | display: block;
99 | }
100 |
101 | .arrow {
102 | position: absolute;
103 | margin: auto 10px;
104 |
105 | right: 0;
106 | top: 0;
107 | bottom: 0;
108 |
109 | width: 0;
110 | height: 0;
111 | border-left: 5px solid transparent;
112 | border-right: 5px solid transparent;
113 | border-top: 5px solid $secondary-text;
114 | }
115 | }
116 |
117 | // drop down menu
118 | li:hover > ul {
119 | display: block
120 | }
121 |
122 | ul {
123 | display: none;
124 | position: absolute;
125 | top: 100%;
126 | left: -1px;
127 | background: #fff;
128 | padding: 0;
129 | z-index: 9;
130 | box-shadow: 0px 3px 10px rgba(0, 0, 0, 0.2);
131 |
132 | li {
133 | float: none;
134 | width: 150px
135 | }
136 |
137 | a {
138 | line-height: 120%;
139 | padding: 10px 15px;
140 |
141 | border-radius: 0 !important;
142 | border: none;
143 | }
144 | }
145 | }
146 | }
147 | }
148 |
149 | .login {
150 | #header {
151 | // fix some of the css for the login page
152 | height: 40px !important;
153 | }
154 |
155 | div.breadcrumbs {
156 | display: none;
157 | }
158 |
159 | input[type=submit] {
160 | background-color: $primary-button !important;
161 | color: $primary-button-text !important;
162 | }
163 |
164 | form {
165 | .submit-row {
166 | background: none !important;
167 | border: none !important;
168 | }
169 | }
170 | }
171 |
172 | #secondary-nav {
173 | background-color: $tertiary-color;
174 | padding: 0px 40px;
175 | height: 40px;
176 | overflow: hidden; // FIXME: find a better way of wrapping the 2ndary nav
177 | border-bottom: 1px solid darken($tertiary-color, 10%);
178 |
179 | ul {
180 | margin: 0;
181 | padding: 0;
182 |
183 | li {
184 | list-style: none;
185 |
186 | float: left;
187 | margin: 0;
188 | padding: 0;
189 |
190 | &.active a {
191 | font-weight: bold;
192 | }
193 |
194 | a {
195 | margin: 0;
196 |
197 | display: block;
198 | color: $tertiary-text;
199 |
200 | padding: 10px 20px;
201 |
202 | &:hover {
203 | color: darken($tertiary-text, 10%);
204 | }
205 | }
206 | }
207 | }
208 | }
209 |
210 | div.breadcrumbs {
211 | height: 40px;
212 |
213 | background-color: $breadcrumb-color;
214 | border-bottom: 1px solid darken($breadcrumb-color, 10%);
215 |
216 | color: $breadcrumb-text;
217 |
218 | // hide the breadcrumb text
219 | text-indent: -1000em;
220 |
221 | a {
222 | color: $breadcrumb-text;
223 | }
224 |
225 | a:hover {
226 | text-decoration: underline;
227 | color: darken($breadcrumb-text, 15%);
228 | }
229 | }
230 |
231 | ul.messagelist {
232 | float: left;
233 | width: 100%;
234 | margin-bottom: 30px;
235 | }
236 |
237 |
238 | body.dashboard #content h1 {
239 | display: none;
240 | }
241 |
242 | #content {
243 | h1 {
244 | float: none;
245 | position: absolute;
246 |
247 | //font-weight: light;
248 |
249 | margin-top: -66px;
250 | left: 40px;
251 | }
252 |
253 | .object-tools {
254 | float: none;
255 | position: absolute;
256 |
257 | margin-top: -66px;
258 | right: 40px;
259 |
260 | .addlink {
261 | background-color: $primary-button;
262 | color: $primary-button-text;
263 | }
264 | }
265 |
266 | #content-main {
267 | .module {
268 | //background-color: $breadcrumb-color;
269 | border: 1px solid darken($breadcrumb-color, 5%);
270 | border-radius: 3px;
271 |
272 | h2, caption {
273 | background: $focus-color;
274 | color: $focus-text;
275 | border-bottom: 1px solid darken($focus-color, 5%);
276 |
277 | a {
278 | color: $focus-text;
279 | }
280 | }
281 | }
282 |
283 | #changelist {
284 | a {
285 | color: $link-color;
286 |
287 | &:hover {
288 | color: $link-color-hover;
289 | }
290 | }
291 |
292 | &.module {
293 | border: none;
294 | }
295 | }
296 |
297 | form {
298 | .submit-row {
299 | background-color: $breadcrumb-color;
300 | border: 1px solid darken($breadcrumb-color, 5%);
301 |
302 | input {
303 | background-color: $secondary-button;
304 | color: $secondary-button-text;
305 | }
306 |
307 | .default {
308 | background-color: $primary-button;
309 | color: $primary-button-text;
310 | }
311 | }
312 | }
313 |
314 | .messagelist {
315 | float: none;
316 | margin-bottom: 20px;
317 |
318 | li {
319 | padding: 10px 10px 10px 40px;
320 |
321 | &.error, &.warning, &.success {
322 | background-position-x: 12px;
323 | }
324 |
325 | }
326 | }
327 |
328 | }
329 | }
330 |
--------------------------------------------------------------------------------
/admin_menu/templatetags/custom_admin_menu.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from collections import OrderedDict
3 |
4 | from django import template
5 | from django.conf import settings
6 | from django.contrib import admin
7 | from django.urls import resolve, reverse, NoReverseMatch
8 | from django.utils.text import capfirst
9 | from django.utils.translation import gettext_lazy as _
10 | from django.apps import apps
11 | from django import VERSION
12 |
13 | register = template.Library()
14 |
15 | PERM = 'view' if (VERSION[0] == 2 and VERSION[1] > 0) or (VERSION[0] > 2) else 'change'
16 |
17 |
18 | class MenuItem:
19 | url = None
20 | title = None
21 | children = None
22 | weight = 10
23 | active = False
24 |
25 | def __init__(self, *args, **kwargs):
26 | for k, v in kwargs.items():
27 | setattr(self, k, v)
28 | if self.children is None:
29 | self.children = list()
30 |
31 | def __repr__(self):
32 | return "