├── .gitignore ├── README.md ├── django_admin_chart_js ├── django_admin_chart_js │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── manage.py └── web │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ ├── 0001_initial.py │ └── __init__.py │ ├── models.py │ ├── templates │ └── admin │ │ └── web │ │ └── emailsubscriber │ │ └── change_list.html │ ├── tests.py │ └── views.py └── images ├── chart_loading.gif ├── custom_message.png ├── dynamic_chart.png ├── hardcoded_chart.png ├── list_subscribers.png └── where_to_place_the_chart.png /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/python 3 | # Edit at https://www.gitignore.io/?templates=python 4 | 5 | ### Python ### 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | pip-wheel-metadata/ 29 | share/python-wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # celery beat schedule file 99 | celerybeat-schedule 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # End of https://www.gitignore.io/api/python 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![img](./images/chart_loading.gif) 2 | 3 | # Django admin + Chart.js 4 | 5 | An example repo outlining how to combine Django admin and Chart.js as described 6 | in https://findwork.dev/blog/adding-charts-to-django-admin/. 7 | -------------------------------------------------------------------------------- /django_admin_chart_js/django_admin_chart_js/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danihodovic/django_admin_chart_js/8e4f5f6b99a3e575ab07ce77f43ac717fa5befee/django_admin_chart_js/django_admin_chart_js/__init__.py -------------------------------------------------------------------------------- /django_admin_chart_js/django_admin_chart_js/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for django_admin_chart_js project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.2.4. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.2/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 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = "4_d^rf_t4s!@%y!lgm&+5_=-sv)8$xf-pvl-rrr5m)f=rw==7(" 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 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 | "web", 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | "django.middleware.security.SecurityMiddleware", 45 | "django.contrib.sessions.middleware.SessionMiddleware", 46 | "django.middleware.common.CommonMiddleware", 47 | "django.middleware.csrf.CsrfViewMiddleware", 48 | "django.contrib.auth.middleware.AuthenticationMiddleware", 49 | "django.contrib.messages.middleware.MessageMiddleware", 50 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 51 | ] 52 | 53 | ROOT_URLCONF = "django_admin_chart_js.urls" 54 | 55 | TEMPLATES = [ 56 | { 57 | "BACKEND": "django.template.backends.django.DjangoTemplates", 58 | "DIRS": [], 59 | "APP_DIRS": True, 60 | "OPTIONS": { 61 | "context_processors": [ 62 | "django.template.context_processors.debug", 63 | "django.template.context_processors.request", 64 | "django.contrib.auth.context_processors.auth", 65 | "django.contrib.messages.context_processors.messages", 66 | ] 67 | }, 68 | } 69 | ] 70 | 71 | WSGI_APPLICATION = "django_admin_chart_js.wsgi.application" 72 | 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases 76 | 77 | DATABASES = { 78 | "default": { 79 | "ENGINE": "django.db.backends.sqlite3", 80 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"), 81 | } 82 | } 83 | 84 | 85 | # Password validation 86 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators 87 | 88 | AUTH_PASSWORD_VALIDATORS = [ 89 | { 90 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" 91 | }, 92 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, 93 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, 94 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, 95 | ] 96 | 97 | 98 | # Internationalization 99 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 100 | 101 | LANGUAGE_CODE = "en-us" 102 | 103 | TIME_ZONE = "UTC" 104 | 105 | USE_I18N = True 106 | 107 | USE_L10N = True 108 | 109 | USE_TZ = True 110 | 111 | 112 | # Static files (CSS, JavaScript, Images) 113 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 114 | 115 | STATIC_URL = "/static/" 116 | -------------------------------------------------------------------------------- /django_admin_chart_js/django_admin_chart_js/urls.py: -------------------------------------------------------------------------------- 1 | """django_admin_chart_js URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path 18 | 19 | urlpatterns = [ 20 | path('admin/', admin.site.urls), 21 | ] 22 | -------------------------------------------------------------------------------- /django_admin_chart_js/django_admin_chart_js/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_admin_chart_js 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/2.2/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', 'django_admin_chart_js.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /django_admin_chart_js/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_admin_chart_js.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /django_admin_chart_js/web/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danihodovic/django_admin_chart_js/8e4f5f6b99a3e575ab07ce77f43ac717fa5befee/django_admin_chart_js/web/__init__.py -------------------------------------------------------------------------------- /django_admin_chart_js/web/admin.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.contrib import admin 4 | from django.core.serializers.json import DjangoJSONEncoder 5 | from django.db.models import Count 6 | from django.db.models.functions import TruncDay 7 | from django.http import JsonResponse 8 | from django.urls import path 9 | 10 | from .models import EmailSubscriber 11 | 12 | 13 | @admin.register(EmailSubscriber) 14 | class EmailSubscriberAdmin(admin.ModelAdmin): 15 | list_display = ("id", "email", "created_at") 16 | ordering = ("-created_at",) 17 | 18 | # Inject chart data on page load in the ChangeList view 19 | def changelist_view(self, request, extra_context=None): 20 | chart_data = self.chart_data() 21 | as_json = json.dumps(list(chart_data), cls=DjangoJSONEncoder) 22 | extra_context = extra_context or {"chart_data": as_json} 23 | return super().changelist_view(request, extra_context=extra_context) 24 | 25 | def get_urls(self): 26 | urls = super().get_urls() 27 | extra_urls = [ 28 | path("chart_data/", self.admin_site.admin_view(self.chart_data_endpoint)) 29 | ] 30 | # NOTE! Our custom urls have to go before the default urls, because they 31 | # default ones match anything. 32 | return extra_urls + urls 33 | 34 | # JSON endpoint for generating chart data that is used for dynamic loading 35 | # via JS. 36 | def chart_data_endpoint(self, request): 37 | chart_data = self.chart_data() 38 | return JsonResponse(list(chart_data), safe=False) 39 | 40 | def chart_data(self): 41 | return ( 42 | EmailSubscriber.objects.annotate(date=TruncDay("created_at")) 43 | .values("date") 44 | .annotate(y=Count("id")) 45 | .order_by("-date") 46 | ) 47 | -------------------------------------------------------------------------------- /django_admin_chart_js/web/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class WebConfig(AppConfig): 5 | name = 'web' 6 | -------------------------------------------------------------------------------- /django_admin_chart_js/web/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2019-08-08 09:52 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='EmailSubscriber', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('email', models.EmailField(max_length=254)), 19 | ('created_at', models.DateTimeField()), 20 | ], 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /django_admin_chart_js/web/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danihodovic/django_admin_chart_js/8e4f5f6b99a3e575ab07ce77f43ac717fa5befee/django_admin_chart_js/web/migrations/__init__.py -------------------------------------------------------------------------------- /django_admin_chart_js/web/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class EmailSubscriber(models.Model): 5 | email = models.EmailField() 6 | created_at = models.DateTimeField() 7 | -------------------------------------------------------------------------------- /django_admin_chart_js/web/templates/admin/web/emailsubscriber/change_list.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_list.html" %} 2 | {% load static %} 3 | 4 | {% block extrahead %} 5 | {{ block.super }} 6 | 7 | 8 | 70 | {% endblock %} 71 | 72 | {% block content %} 73 | 74 |
75 | 76 |
77 | 78 | 79 | 80 | {{ block.super }} 81 | {% endblock %} 82 | -------------------------------------------------------------------------------- /django_admin_chart_js/web/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /django_admin_chart_js/web/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /images/chart_loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danihodovic/django_admin_chart_js/8e4f5f6b99a3e575ab07ce77f43ac717fa5befee/images/chart_loading.gif -------------------------------------------------------------------------------- /images/custom_message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danihodovic/django_admin_chart_js/8e4f5f6b99a3e575ab07ce77f43ac717fa5befee/images/custom_message.png -------------------------------------------------------------------------------- /images/dynamic_chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danihodovic/django_admin_chart_js/8e4f5f6b99a3e575ab07ce77f43ac717fa5befee/images/dynamic_chart.png -------------------------------------------------------------------------------- /images/hardcoded_chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danihodovic/django_admin_chart_js/8e4f5f6b99a3e575ab07ce77f43ac717fa5befee/images/hardcoded_chart.png -------------------------------------------------------------------------------- /images/list_subscribers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danihodovic/django_admin_chart_js/8e4f5f6b99a3e575ab07ce77f43ac717fa5befee/images/list_subscribers.png -------------------------------------------------------------------------------- /images/where_to_place_the_chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danihodovic/django_admin_chart_js/8e4f5f6b99a3e575ab07ce77f43ac717fa5befee/images/where_to_place_the_chart.png --------------------------------------------------------------------------------