├── 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 |
21 |
22 | 38 |
39 |
40 | {% endif %} 41 | 42 | {% for title, item in menu.items %} 43 | {% if item.active and item.children|length > 1 %} 44 |
45 | 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 |

{{ site_header|default:_('Django administration') }}

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 | [![Downloads](https://pepy.tech/badge/django-admin-menu)](https://pepy.tech/project/django-admin-menu) 6 | 7 | ![screenshot](screenshots/drop-down.png) 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 | ![screenshot](screenshots/login.png) 67 | ![screenshot](screenshots/form.png) 68 | ![screenshot](screenshots/drop-down.png) 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 | ![screenshot](screenshots/ui-dark.png) 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 | ![screenshot](screenshots/ui-green.png) 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 | ![screenshot](screenshots/ui-red.png) 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 "" % (self.url, self.title) 33 | 34 | class MenuGroup(MenuItem): 35 | pass 36 | 37 | 38 | def get_admin_site(context): 39 | try: 40 | current_resolver = resolve(context.get('request').path) 41 | index_resolver = resolve(reverse('%s:index' % current_resolver.namespaces[0])) 42 | 43 | if hasattr(index_resolver.func, 'admin_site'): 44 | return index_resolver.func.admin_site 45 | 46 | for func_closure in index_resolver.func.__closure__: 47 | if isinstance(func_closure.cell_contents, admin.AdminSite): 48 | return func_closure.cell_contents 49 | except: 50 | pass 51 | 52 | return admin.site 53 | 54 | 55 | def get_app_list(context, order=True): 56 | admin_site = get_admin_site(context) 57 | request = context['request'] 58 | 59 | app_dict = {} 60 | for model, model_admin in admin_site._registry.items(): 61 | app_label = model._meta.app_label 62 | try: 63 | has_module_perms = model_admin.has_module_permission(request) 64 | except AttributeError: 65 | has_module_perms = request.user.has_module_perms(app_label) # Fix Django < 1.8 issue 66 | 67 | if has_module_perms: 68 | perms = model_admin.get_model_perms(request) 69 | 70 | # Check whether user has any perm for this module. 71 | # If so, add the module to the model_list. 72 | if True in perms.values(): 73 | info = (app_label, model._meta.model_name) 74 | model_dict = { 75 | 'name': capfirst(model._meta.verbose_name_plural), 76 | 'object_name': model._meta.object_name, 77 | 'perms': perms, 78 | 'model_admin': model_admin, 79 | } 80 | if perms.get(PERM, False): 81 | try: 82 | model_dict['admin_url'] = reverse('admin:%s_%s_changelist' % info, current_app=admin_site.name) 83 | except NoReverseMatch: 84 | pass 85 | if perms.get('add', False): 86 | try: 87 | model_dict['add_url'] = reverse('admin:%s_%s_add' % info, current_app=admin_site.name) 88 | except NoReverseMatch: 89 | pass 90 | if app_label in app_dict: 91 | app_dict[app_label]['models'].append(model_dict) 92 | else: 93 | try: 94 | name = model._meta.app_config.verbose_name 95 | except NameError: 96 | name = app_label.title() 97 | app_dict[app_label] = { 98 | 'name': name, 99 | 'app_label': app_label, 100 | 'app_url': reverse( 101 | 'admin:app_list', 102 | kwargs={'app_label': app_label}, 103 | current_app=admin_site.name, 104 | ), 105 | 'has_module_perms': has_module_perms, 106 | 'models': [model_dict], 107 | } 108 | 109 | # Sort the apps alphabetically. 110 | app_list = list(app_dict.values()) 111 | 112 | if order: 113 | app_list.sort(key=lambda x: x['name'].lower()) 114 | 115 | # Sort the models alphabetically within each app. 116 | for app in app_list: 117 | app['models'].sort(key=lambda x: x['name']) 118 | 119 | return app_list 120 | 121 | 122 | def get_group_weight(title): 123 | weights = getattr(settings, 'MENU_WEIGHT', {}) 124 | return weights.get(title, 10) 125 | 126 | 127 | def make_menu_item(url, title, weight=10): 128 | return MenuItem(url=url, title=title, weight=weight) 129 | 130 | 131 | def make_menu_group(title, children=None, weight=None): 132 | return MenuGroup(title=title, children=children, weight=weight or get_group_weight(title)) 133 | 134 | 135 | @register.simple_tag(takes_context=True) 136 | def get_admin_menu(context): 137 | request = context['request'] 138 | apps = get_app_list(context, True) 139 | 140 | menu = OrderedDict({ 141 | _('Dashboard'): make_menu_group(_('Dashboard'), weight=1, children=[ 142 | make_menu_item(reverse('admin:index'), _('Dashboard'), weight=0) 143 | ]) 144 | }) 145 | 146 | for app in apps: 147 | if not app['has_module_perms']: 148 | continue 149 | 150 | for model in app['models']: 151 | if not model['perms'][PERM]: 152 | continue 153 | 154 | model_admin = model['model_admin'] 155 | title = capfirst(getattr(model_admin, 'menu_group', app['name'])) 156 | if title not in menu: 157 | menu[title] = make_menu_group(title) 158 | 159 | group = menu[title] 160 | submenu = make_menu_item( 161 | url=model['admin_url'], 162 | title=capfirst(getattr(model_admin, 'menu_title', model['name'])), 163 | weight=getattr(model_admin, 'menu_order', 10) 164 | ) 165 | group.children.append(submenu) 166 | 167 | extra = getattr(model_admin, 'extra_menu_items', []) 168 | extra_func = getattr(model_admin, 'get_extra_menu_items', None) 169 | if extra_func: 170 | extra = extra_func(request) 171 | 172 | for item in extra: 173 | if len(item) == 2: 174 | url, extra_title = item 175 | weight = 1 176 | else: 177 | url, extra_title, extra_group, weight = item 178 | if extra_group not in menu: 179 | menu[extra_group] = make_menu_group(extra_group) 180 | group = menu[extra_group] 181 | 182 | submenu = make_menu_item( 183 | url=url, 184 | title=capfirst(extra_title), 185 | weight=weight 186 | ) 187 | group.children.append(submenu) 188 | 189 | # sort the menu by weight 190 | menu = OrderedDict(sorted(menu.items(), key=lambda x: x[1].weight)) 191 | 192 | for title in reversed(list(menu.keys())): 193 | menu[title].children.sort(key=lambda x: x.weight) 194 | 195 | for idx, sub in enumerate(menu[title].children): 196 | if idx == 0: 197 | menu[title].url = sub.url 198 | if sub.url == reverse('admin:index'): 199 | if request.path == sub.url: 200 | sub.active = True 201 | menu[title].active = True 202 | else: 203 | if request.path.startswith(sub.url): 204 | sub.active = True 205 | menu[title].active = True 206 | 207 | return menu 208 | --------------------------------------------------------------------------------