├── .gitignore ├── .travis.yml ├── AUTHORS ├── CHANGELOG.txt ├── DESCRIPTION ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── dashboard_app ├── __init__.py ├── admin.py ├── dashboard_widgets.py ├── decorators.py ├── exceptions.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── settings.py ├── static │ └── dashboard_app │ │ ├── css │ │ ├── libs │ │ │ └── bootstrap.css │ │ └── styles.css │ │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ └── glyphicons-halflings-regular.woff │ │ └── js │ │ ├── dashboard.js │ │ └── libs │ │ └── bootstrap.min.js ├── templates │ └── dashboard_app │ │ ├── dashboard.html │ │ ├── partials │ │ └── widget.html │ │ └── widgets │ │ └── dummy_widget.html ├── tests │ ├── .coveragerc │ ├── __init__.py │ ├── factories.py │ ├── mixins.py │ ├── models_tests.py │ ├── runtests.py │ ├── south_settings.py │ ├── test_app │ │ ├── __init__.py │ │ ├── models.py │ │ └── templates │ │ │ ├── 400.html │ │ │ └── 500.html │ ├── test_settings.py │ ├── test_widget_app │ │ ├── __init__.py │ │ ├── dashboard_widgets.py │ │ ├── models.py │ │ └── templates │ │ │ └── test_widget_app │ │ │ ├── dummy_widget.html │ │ │ └── dummy_widget2.html │ ├── urls.py │ ├── views_tests.py │ ├── widget_base_tests.py │ └── widget_pool_tests.py ├── urls.py ├── view_mixins.py ├── views.py ├── widget_base.py └── widget_pool.py ├── docs └── README.md ├── hooks └── pre-commit ├── manage.py ├── setup.cfg ├── setup.py └── test_requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | *.pyc 3 | *.coverage 4 | *coverage/ 5 | db.sqlite 6 | dist/ 7 | docs/_build/ 8 | app_media/ 9 | app_static/ 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | install: 5 | - pip install -r test_requirements.txt --use-mirrors 6 | - pip install coveralls 7 | script: 8 | - cd dashboard_app/tests 9 | - ./runtests.py 10 | - mv .coverage ../../ 11 | - cd ../../ 12 | after_success: 13 | - coveralls 14 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Current or previous core committers 2 | 3 | Martin Brochhaus 4 | 5 | Contributors (in alphabetical order) 6 | 7 | * Your name could stand here :) 8 | * shubham2892 9 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | === (ongoing) === 2 | 3 | === 0.2.1 === 4 | 5 | - Fixed bug when calling HTTPS sites 6 | 7 | === 0.2 === 8 | 9 | - Added RemoteWidget and DashboardGetRemoteWidgetView. This allows to render 10 | a widget that is hosted on another server/dashboard_app instance. 11 | 12 | === 0.1 === 13 | - Initial commit 14 | 15 | 16 | # Suggested file syntax: 17 | # 18 | # === (ongoing) === 19 | # - this is always on top of the file 20 | # - when you release a new version, you rename the last `(ongoing)` to the new 21 | # version and add a new `=== (ongoing) ===` to the top of the file 22 | # 23 | # === 1.0 === 24 | # - a major version is created when the software reached a milestone and is 25 | # feature complete 26 | # 27 | # === 0.2 === 28 | # - a minor version is created when new features or significant changes have 29 | # been made to the software. 30 | # 31 | # === 0.1.1 == 32 | # - for bugfix releases, fixing typos in the docs, restructuring things, simply 33 | # anything that doesn't really change the behaviour of the software you 34 | # might use the third digit which is also sometimes called the build number. 35 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | A reusable Django app for displaying a dashboard with a fluid grid of widgets. 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2014 Martin Brochhaus 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include LICENSE 3 | include DESCRIPTION 4 | include CHANGELOG.txt 5 | include README.md 6 | graft dashboard_app 7 | global-exclude *.orig *.pyc *.log *.swp 8 | prune dashboard_app/tests/coverage 9 | prune dashboard_app/.ropeproject 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | develop: setup-git 2 | pip install "file://`pwd`#egg=dashboard_app[dev]" 3 | pip install -e . 4 | pip install -r test_requirements.txt 5 | 6 | setup-git: 7 | git config branch.autosetuprebase always 8 | cd .git/hooks && ln -sf ../../hooks/* ./ 9 | 10 | lint-python: 11 | @echo "Linting Python files" 12 | PYFLAKES_NODOCTEST=1 flake8 dashboard_app 13 | @echo "" 14 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django Dashboard App 2 | ==================== 3 | 4 | A reusable Django app for displaying a dashboard with a fluid grid of widgets. 5 | 6 | Let's say you control 20 different web apps and you want to have a dashboard 7 | that shows the user count of each app as a graph. The graphs should be updated 8 | every minute so that you can see immediately when a sudden spike of new user 9 | signups happens. 10 | 11 | There are two ways: 12 | 13 | 1. Your apps provide an API endpoint for your dashboard so that the dashboard 14 | can poll that endpoint every minute and get the current user count. 15 | 2. Your dashboard provides an endpoint that can be called by your apps whenever 16 | a new user signs up. 17 | 18 | Ultimately, both methods should be possible with this app. Currently only the 19 | first way is implemented. 20 | 21 | The dashboard itself will consist of many plugins. Each plugin is a reusable 22 | Django app of it's own. This allows you to write any kind of plugin for any 23 | kind of service. 24 | 25 | Note: A while ago I already created 26 | https://github.com/bitmazk/django-metrics-dashboard to solve this exact problem 27 | but I wanted to have socket.io support, which was a bad idea. It didn't really 28 | work out nicely and kept crashing so that I abandoned the project in the end. 29 | This is my second try, this time I'm using old-school polling once per minute 30 | via AJAX. 31 | 32 | 33 | Installation 34 | ============ 35 | 36 | To get the latest stable release from PyPi 37 | 38 | .. code-block:: bash 39 | 40 | pip install django-dashboard-app 41 | 42 | To get the latest commit from GitHub 43 | 44 | .. code-block:: bash 45 | 46 | pip install -e git+git://github.com/bitmazk/django-dashboard-app.git#egg=dashboard_app 47 | 48 | TODO: Describe further installation steps (edit / remove the examples below): 49 | 50 | Add ``dashboard_app`` and ``django.contrib.humanize`` to your 51 | ``INSTALLED_APPS``: 52 | 53 | .. code-block:: python 54 | 55 | INSTALLED_APPS = ( 56 | ..., 57 | 'django.contrib.humanize', 58 | 'dashboard_app', 59 | ) 60 | 61 | Add the ``dashboard_app`` URLs to your ``urls.py`` 62 | 63 | .. code-block:: python 64 | 65 | urlpatterns = patterns('', 66 | ... 67 | url(r'^dashboard/', include('dashboard_app.urls')), 68 | ) 69 | 70 | Don't forget to migrate your database 71 | 72 | .. code-block:: bash 73 | 74 | ./manage.py migrate dashboard_app 75 | 76 | 77 | Usage 78 | ----- 79 | 80 | When you first visit the main URL for your dashboard, you will see an error 81 | widget saying "No widgets found". This means we need to teach Django which 82 | widgets to display. 83 | 84 | In your Django project-app-folder (assuming Django >1.5 project layout) create 85 | a ``dashboard_widgets.py`` file (you can put that file into any app folder that 86 | is part of ``INSTALLED_APPS``). 87 | 88 | Add the following code to that file: 89 | 90 | .. code-block:: python 91 | 92 | """Widgets for the ACME project.""" 93 | from dashboard_app.dashboard_widgets import DummyWidget 94 | from dashboard_app.widget_pool import dashboard_widget_pool 95 | 96 | 97 | dashboard_widget_pool.register_widget(DummyWidget, position=1) 98 | 99 | When you call your main dashboard URL now, you should see the dummy widget 100 | displaying the current date and time. The last update time resembles the time 101 | when the widget did write data to the database last time. Since this widget 102 | never writes any data, this time will always be the time when you first loaded 103 | the widget under this name. 104 | 105 | Build Your Widget 106 | ----------------- 107 | 108 | First you need to decide where your widget code should live. If you are very 109 | sure that your widgets will always be bound to the project and never be 110 | released as open source or re-used in other projects of yours, you can 111 | implement your widgets in the ``dashboard_widgets.py`` file that you have 112 | created in your Django project-app already. 113 | 114 | If you think that your widget will be usefull for many projects, you should 115 | create it as a reusable app and therefore create a new app-folder. Let's 116 | assume that your project is called ``ACME`` and you want to create a widget 117 | to display the current user count. First create the following files: 118 | 119 | .. code-block:: text 120 | 121 | -- dashboard_acme_users 122 | ---- __init__.py 123 | ---- models.py 124 | ---- dashboard_widgets.py 125 | 126 | Your widget apps should always be named like ``dashboard_yourthing`` so that 127 | it is easier to find them all on Google/Github. The ``__init__.py`` file will 128 | turn the app into a Python module and the ``models.py`` file is needed to turn 129 | the module into a potential Django app that can be discovered by the 130 | ``INSTALLED_APPS`` setting. The ``dashboard_widgets.py`` file is the file 131 | where you will implement your custom widget. 132 | 133 | Put the following code into that file: 134 | 135 | .. code-block:: python 136 | 137 | """Widgets for the dashboard_acme_users app.""" 138 | from dashboard_app.widget_base import DashboardWidgetBase 139 | 140 | from django.contrib.auth.models import User 141 | 142 | 143 | class UserCountWidget(DashboardWidgetBase): 144 | """Displays the total amount of users currently in the database.""" 145 | template_name = 'dashboard_acme_users/widgets/user_count.html' 146 | 147 | def get_context_data(self): 148 | ctx = super(UserCountWidget, self).get_context_data() 149 | count = User.objects.all().count() 150 | ctx.update({'value': count, }) 151 | return ctx 152 | 153 | You basically just have to decide on a nice widget name (here: 154 | ``UserCountWidget``) and a template name. We suggest to put the widgets into 155 | a subfolder called ``your_app_name/widgets`` and name the template after the 156 | widget's class name (here: ``user_count.html``). 157 | 158 | Now you want to display something. In our case it is the current user count. 159 | Therefore we must override the ``get_context_data`` method and return the 160 | current user count. 161 | 162 | Now you need to register your new widget in the ``dashboard_widgets.py`` file 163 | that you used earlier to register the DummyWidget: 164 | 165 | .. code-block:: python 166 | 167 | """Widgets for the ACME project.""" 168 | from dashboard_app.dashboard_widgets import DummyWidget 169 | from dashboard_app.widget_pool import dashboard_widget_pool 170 | 171 | from dashboard_acme_users import dashboard_widgets as widgets 172 | 173 | 174 | dashboard_widget_pool.register_widget(DummyWidget, position=1) 175 | dashboard_widget_pool.register_widget(widgets.UserCountWidget, position=2) 176 | 177 | When you visit your main dashboard URL you should see two widgets now. 178 | 179 | TODO: Describe how to save widget data to the database and render charts 180 | 181 | Contribute 182 | ---------- 183 | 184 | If you want to contribute to this project, please perform the following steps 185 | 186 | .. code-block:: bash 187 | 188 | # Fork this repository 189 | # Clone your fork 190 | mkvirtualenv -p python2.7 django-dashboard-app 191 | make develop 192 | 193 | git co -b feature_branch master 194 | # Implement your feature and tests 195 | git add . && git commit 196 | git push -u origin feature_branch 197 | # Send us a pull request for your feature branch 198 | -------------------------------------------------------------------------------- /dashboard_app/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __version__ = '0.2.1' 3 | -------------------------------------------------------------------------------- /dashboard_app/admin.py: -------------------------------------------------------------------------------- 1 | """Admin classes for the dashboard_app app.""" 2 | from django.contrib import admin 3 | 4 | from . import models 5 | 6 | 7 | class DashboardWidgetLastUpdateAdmin(admin.ModelAdmin): 8 | list_display = ['widget_name', 'last_update', ] 9 | search_fields = ['widget_name', ] 10 | 11 | 12 | class DashboardWidgetSettingsAdmin(admin.ModelAdmin): 13 | list_display = ['widget_name', 'setting_name', 'value'] 14 | search_fields = ['widget_name', 'setting_name', 'value', ] 15 | 16 | 17 | admin.site.register( 18 | models.DashboardWidgetLastUpdate, DashboardWidgetLastUpdateAdmin) 19 | admin.site.register( 20 | models.DashboardWidgetSettings, DashboardWidgetSettingsAdmin) 21 | -------------------------------------------------------------------------------- /dashboard_app/dashboard_widgets.py: -------------------------------------------------------------------------------- 1 | """Built-in widgets for the dashboard_app.""" 2 | from django.utils.timezone import now 3 | 4 | from dashboard_app.widget_base import DashboardWidgetBase 5 | 6 | 7 | class DummyWidget(DashboardWidgetBase): 8 | """Use this widget to test your installation.""" 9 | template_name = 'dashboard_app/widgets/dummy_widget.html' 10 | 11 | def get_context_data(self, **kwargs): 12 | ctx = super(DummyWidget, self).get_context_data(**kwargs) 13 | ctx.update({'value': now(), }) 14 | return ctx 15 | 16 | 17 | class RemoteWidget(DashboardWidgetBase): 18 | """Widget that renders a widget from another dashboard_app instance.""" 19 | def __init__(self, url, token, remote_widget_name, **kwargs): 20 | """ 21 | Initiates the widget. 22 | 23 | :param url: The URL of the remote dashboard api 24 | (i.e. http://example.com/dashboard/api/widget/) 25 | :param token: The API token you have been given by the admin of the 26 | remote server in order to obtain this widget. 27 | :remote_widget_name: The widget name as registered on the remote 28 | server. 29 | 30 | """ 31 | self.url = url 32 | self.token = token 33 | self.remote_widget_name = remote_widget_name 34 | super(RemoteWidget, self).__init__(**kwargs) 35 | -------------------------------------------------------------------------------- /dashboard_app/decorators.py: -------------------------------------------------------------------------------- 1 | """Decorators for the dashboard_app.""" 2 | from django.conf import settings 3 | from django.contrib.auth.decorators import ( 4 | user_passes_test, 5 | ) 6 | from django.core.exceptions import PermissionDenied 7 | 8 | from . import settings as app_settings 9 | 10 | 11 | def permission_required(perm, login_url=None, raise_exception=False): 12 | """ 13 | Re-implementation of the permission_required decorator, honors settings. 14 | 15 | If ``DASHBOARD_REQUIRE_LOGIN`` is False, this decorator will always return 16 | ``True``, otherwise it will check for the permission as usual. 17 | 18 | """ 19 | def check_perms(user): 20 | if not getattr(settings, 'DASHBOARD_REQUIRE_LOGIN', 21 | app_settings.REQUIRE_LOGIN): 22 | return True 23 | # First check if the user has the permission (even anon users) 24 | if user.has_perm(perm): 25 | return True 26 | # In case the 403 handler should be called raise the exception 27 | if raise_exception: # pragma: no cover 28 | raise PermissionDenied 29 | # As the last resort, show the login form 30 | return False 31 | return user_passes_test(check_perms, login_url=login_url) 32 | -------------------------------------------------------------------------------- /dashboard_app/exceptions.py: -------------------------------------------------------------------------------- 1 | """Exceptions for the dashboard_app.""" 2 | 3 | 4 | class WidgetAlreadyRegistered(Exception): 5 | pass 6 | -------------------------------------------------------------------------------- /dashboard_app/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # -*- coding: utf-8 -*- 3 | from south.utils import datetime_utils as datetime 4 | from south.db import db 5 | from south.v2 import SchemaMigration 6 | from django.db import models 7 | 8 | 9 | class Migration(SchemaMigration): 10 | 11 | def forwards(self, orm): 12 | # Adding model 'DashboardWidgetLastUpdate' 13 | db.create_table(u'dashboard_app_dashboardwidgetlastupdate', ( 14 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 15 | ('widget_name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=128)), 16 | ('last_update', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, auto_now_add=True, blank=True)), 17 | )) 18 | db.send_create_signal(u'dashboard_app', ['DashboardWidgetLastUpdate']) 19 | 20 | # Adding model 'DashboardWidgetSettings' 21 | db.create_table(u'dashboard_app_dashboardwidgetsettings', ( 22 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 23 | ('widget_name', self.gf('django.db.models.fields.CharField')(max_length=128)), 24 | ('setting_name', self.gf('django.db.models.fields.CharField')(max_length=128)), 25 | ('value', self.gf('django.db.models.fields.CharField')(max_length=4000)), 26 | )) 27 | db.send_create_signal(u'dashboard_app', ['DashboardWidgetSettings']) 28 | 29 | # Adding unique constraint on 'DashboardWidgetSettings', fields ['widget_name', 'setting_name'] 30 | db.create_unique(u'dashboard_app_dashboardwidgetsettings', ['widget_name', 'setting_name']) 31 | 32 | 33 | def backwards(self, orm): 34 | # Removing unique constraint on 'DashboardWidgetSettings', fields ['widget_name', 'setting_name'] 35 | db.delete_unique(u'dashboard_app_dashboardwidgetsettings', ['widget_name', 'setting_name']) 36 | 37 | # Deleting model 'DashboardWidgetLastUpdate' 38 | db.delete_table(u'dashboard_app_dashboardwidgetlastupdate') 39 | 40 | # Deleting model 'DashboardWidgetSettings' 41 | db.delete_table(u'dashboard_app_dashboardwidgetsettings') 42 | 43 | 44 | models = { 45 | u'dashboard_app.dashboardwidgetlastupdate': { 46 | 'Meta': {'object_name': 'DashboardWidgetLastUpdate'}, 47 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 48 | 'last_update': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'auto_now_add': 'True', 'blank': 'True'}), 49 | 'widget_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}) 50 | }, 51 | u'dashboard_app.dashboardwidgetsettings': { 52 | 'Meta': {'unique_together': "(('widget_name', 'setting_name'),)", 'object_name': 'DashboardWidgetSettings'}, 53 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 54 | 'setting_name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 55 | 'value': ('django.db.models.fields.CharField', [], {'max_length': '4000'}), 56 | 'widget_name': ('django.db.models.fields.CharField', [], {'max_length': '128'}) 57 | } 58 | } 59 | 60 | complete_apps = ['dashboard_app'] 61 | -------------------------------------------------------------------------------- /dashboard_app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-dashboard-app/ed98f2bca91a4ced36d0dd1aa1baee78e989cf64/dashboard_app/migrations/__init__.py -------------------------------------------------------------------------------- /dashboard_app/models.py: -------------------------------------------------------------------------------- 1 | """Models for the dashboard_app.""" 2 | from django.db import models 3 | from django.utils.translation import ugettext_lazy as _ 4 | 5 | 6 | class DashboardWidgetLastUpdate(models.Model): 7 | """ 8 | Table that holds the last update date for each widget. 9 | 10 | :widget_name: This should be a name that uniquely identifies a widget. 11 | :last_update: DateTime when this widget was last updated. 12 | 13 | """ 14 | widget_name = models.CharField( 15 | unique=True, 16 | max_length=128, 17 | verbose_name=_('Widget name'), 18 | ) 19 | 20 | last_update = models.DateTimeField( 21 | auto_now=True, 22 | verbose_name=_('Last update'), 23 | ) 24 | 25 | 26 | class DashboardWidgetSettings(models.Model): 27 | """ 28 | Table that can hold all settings for all widgets. 29 | 30 | :widget_name: This should be a name that uniquely identifies a widget. 31 | :setting_name: This is the name of the setting to be saved. 32 | :value: This is the value of the setting. 33 | 34 | """ 35 | class Meta: 36 | unique_together = ('widget_name', 'setting_name', ) 37 | 38 | # This permission is actually not relevant to the model but defines if 39 | # a user has access to the dashboard view. It's just added to this 40 | # model for convenience, it could just as well be on any other model. 41 | permissions = ( 42 | ('can_view_dashboard', 'Can view the dashboard'), 43 | ) 44 | 45 | widget_name = models.CharField( 46 | max_length=128, 47 | verbose_name=_('Widget name'), 48 | ) 49 | 50 | setting_name = models.CharField( 51 | max_length=128, 52 | verbose_name=_('Setting name'), 53 | ) 54 | 55 | value = models.CharField( 56 | max_length=4000, 57 | verbose_name=_('Setting name'), 58 | ) 59 | 60 | def __unicode__(self): 61 | return '{0} of {1}'.format(self.setting_name, self.widget_name) 62 | -------------------------------------------------------------------------------- /dashboard_app/settings.py: -------------------------------------------------------------------------------- 1 | """Default values for settings of the dashboard_app.""" 2 | REQUIRE_LOGIN = True 3 | -------------------------------------------------------------------------------- /dashboard_app/static/dashboard_app/css/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #FAFAFA; 3 | padding-top: 2em; 4 | } 5 | -------------------------------------------------------------------------------- /dashboard_app/static/dashboard_app/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-dashboard-app/ed98f2bca91a4ced36d0dd1aa1baee78e989cf64/dashboard_app/static/dashboard_app/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /dashboard_app/static/dashboard_app/fonts/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | -------------------------------------------------------------------------------- /dashboard_app/static/dashboard_app/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-dashboard-app/ed98f2bca91a4ced36d0dd1aa1baee78e989cf64/dashboard_app/static/dashboard_app/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /dashboard_app/static/dashboard_app/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-dashboard-app/ed98f2bca91a4ced36d0dd1aa1baee78e989cf64/dashboard_app/static/dashboard_app/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /dashboard_app/static/dashboard_app/js/dashboard.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | 3 | "use strict"; 4 | 5 | var interval; 6 | 7 | function init() { 8 | interval = setInterval(find_outdated, 60000); 9 | find_outdated(); 10 | } 11 | 12 | function find_outdated() { 13 | $.get(get_last_updates_url, function(data) { 14 | // We expect a dict of widget_names and last update times 15 | for (var widget_name in data) { 16 | // We have saved the latest last update time in a hidden field 17 | var last_update = $('#' + widget_name).find('.lastUpdate').val(); 18 | if (last_update != data[widget_name]) { 19 | // We compare the latest last update time with the one returned 20 | // by the AJAX call. If it is different, we re-render the 21 | // widget 22 | reload_widget(widget_name); 23 | } 24 | } 25 | }); 26 | } 27 | 28 | function reload_widget(widget_name) { 29 | var get_data = {'name': widget_name}; 30 | $('#' + widget_name).find('.panel-body').html('

Loading...

'); 31 | $.get(render_widget_url, get_data, function(data) { 32 | $('#' + widget_name).replaceWith(data); 33 | }); 34 | } 35 | 36 | $(document).ready(init); 37 | 38 | }(jQuery)) 39 | -------------------------------------------------------------------------------- /dashboard_app/static/dashboard_app/js/libs/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.1.0 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.isLoading=!1};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",f.resetText||d.data("resetText",d[e]()),d[e](f[b]||this.options[b]),setTimeout(a.proxy(function(){"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},b.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}a&&this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}if(e.hasClass("active"))return this.sliding=!1;var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});return this.$element.trigger(j),j.isDefaultPrevented()?void 0:(this.sliding=!0,f&&this.pause(),this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid.bs.carousel",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")?(e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid.bs.carousel")},0)}).emulateTransitionEnd(1e3*d.css("transition-duration").slice(0,-1))):(d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid.bs.carousel")),f&&this.cycle(),this)};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("collapse in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?void this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);!e&&f.toggle&&"show"==c&&(c=!c),e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(jQuery),+function(a){"use strict";function b(b){a(d).remove(),a(e).each(function(){var d=c(a(this)),e={relatedTarget:this};d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown",e)),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown",e))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(''}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof c&&c;(e||"destroy"!=c)&&(e||d.data("bs.popover",e=new b(this,f)),"string"==typeof c&&e[c]())})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(jQuery),+function(a){"use strict";function b(c,d){var e,f=a.proxy(this.process,this);this.$element=a(a(c).is("body")?window:c),this.$body=a("body"),this.$scrollElement=this.$element.on("scroll.bs.scroll-spy.data-api",f),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||(e=a(c).attr("href"))&&e.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.offsets=a([]),this.targets=a([]),this.activeTarget=null,this.refresh(),this.process()}b.DEFAULTS={offset:10},b.prototype.refresh=function(){var b=this.$element[0]==window?"offset":"position";this.offsets=a([]),this.targets=a([]);{var c=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[b]().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})}},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);if(g&&b<=e[0])return g!=(a=f[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parentsUntil(this.options.target,".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate.bs.scrollspy")};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},b.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e).emulateTransitionEnd(150):e(),f.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(jQuery),+function(a){"use strict";var b=function(c,d){this.options=a.extend({},b.DEFAULTS,d),this.$window=a(window).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(c),this.affixed=this.unpin=this.pinnedOffset=null,this.checkPosition()};b.RESET="affix affix-top affix-bottom",b.DEFAULTS={offset:0},b.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(b.RESET).addClass("affix");var a=this.$window.scrollTop(),c=this.$element.offset();return this.pinnedOffset=c.top-a},b.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"top"==this.affixed&&(e.top+=d),"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top(this.$element)),"function"==typeof h&&(h=f.bottom(this.$element));var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=c-h?"bottom":null!=g&&g>=d?"top":!1;if(this.affixed!==i){this.unpin&&this.$element.css("top","");var j="affix"+(i?"-"+i:""),k=a.Event(j+".bs.affix");this.$element.trigger(k),k.isDefaultPrevented()||(this.affixed=i,this.unpin="bottom"==i?this.getPinnedOffset():null,this.$element.removeClass(b.RESET).addClass(j).trigger(a.Event(j.replace("affix","affixed"))),"bottom"==i&&this.$element.offset({top:c-h-this.$element.height()}))}}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof c&&c;e||d.data("bs.affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(jQuery); -------------------------------------------------------------------------------- /dashboard_app/templates/dashboard_app/dashboard.html: -------------------------------------------------------------------------------- 1 | {% load static future %} 2 | 3 | 4 | 5 | 6 | 7 | Dashboard 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 |
22 |
23 |
24 | {% for widget_name, widget, position in widgets %} 25 | {% cycle "col1" "col2" "col3" as column silent %} 26 | {% if column == "col1" %} 27 |
28 | {% endif %} 29 | 30 |
31 | {% include widget.template_name with widget=widget %} 32 |
33 | 34 | {% if column == "col3" or forloop.last %} 35 |
36 | {% endif %} 37 | {% empty %} 38 |
39 |
40 |

No widgets found

41 |
42 |
43 | Whoops! Looks like you have not registered any widget, yet. 44 |
45 |
46 | {% endfor %} 47 |
48 |
49 |
50 | 51 | 52 | 53 | 54 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /dashboard_app/templates/dashboard_app/partials/widget.html: -------------------------------------------------------------------------------- 1 | {% load humanize %} 2 |
3 |
4 |

{% block widget_title %}{{ widget.get_title }}{% endblock %}

5 |
6 |
7 | {% block widget_body %}

Loading widget...

{% endblock %} 8 |
9 | 15 |
16 | -------------------------------------------------------------------------------- /dashboard_app/templates/dashboard_app/widgets/dummy_widget.html: -------------------------------------------------------------------------------- 1 | {% extends "dashboard_app/partials/widget.html" %} 2 | 3 | {% block widget_body %} 4 | {% if is_rendered %} 5 | {% if error %} 6 |

{{ error }}

7 | {% else %} 8 |

{{ value|date:"d. M. y" }}

9 |

{{ value|date:"H:i:s" }}

10 | {% endif %} 11 | {% else %} 12 | {{ block.super }} 13 | {% endif %} 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /dashboard_app/tests/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = 3 | dashboard_app 4 | branch = True 5 | omit = 6 | ./* 7 | ../urls.py 8 | ../migrations/* 9 | 10 | [report] 11 | exclude_lines = 12 | pragma: no cover 13 | def __repr__ 14 | raise AssertionError 15 | raise NotImplementedError 16 | if __name__ == .__main__.: 17 | -------------------------------------------------------------------------------- /dashboard_app/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-dashboard-app/ed98f2bca91a4ced36d0dd1aa1baee78e989cf64/dashboard_app/tests/__init__.py -------------------------------------------------------------------------------- /dashboard_app/tests/factories.py: -------------------------------------------------------------------------------- 1 | """Factories for the dashboard_app.""" 2 | import factory 3 | 4 | from .. import models 5 | 6 | 7 | class DashboardWidgetLastUpdateFactory(factory.DjangoModelFactory): 8 | FACTORY_FOR = models.DashboardWidgetLastUpdate 9 | 10 | widget_name = factory.Sequence(lambda n: 'widget{0}'.format(n)) 11 | 12 | 13 | class DashboardWidgetSettingsFactory(factory.DjangoModelFactory): 14 | FACTORY_FOR = models.DashboardWidgetSettings 15 | 16 | widget_name = factory.Sequence(lambda n: 'widget{0}'.format(n)) 17 | setting_name = 'setting_name' 18 | value = '1' 19 | -------------------------------------------------------------------------------- /dashboard_app/tests/mixins.py: -------------------------------------------------------------------------------- 1 | """Mixins for the tests of the dashboard_app.""" 2 | from ..widget_pool import dashboard_widget_pool 3 | 4 | 5 | class WidgetTestCaseMixin(object): 6 | """ 7 | Mixin that makes sure to unregister widgets leftover from other tests. 8 | 9 | """ 10 | def _unregister_widgets(self): 11 | # unregister all widgets that might be leftover from other tests 12 | dashboard_widget_pool.widgets = {} 13 | dashboard_widget_pool.discovered = False 14 | -------------------------------------------------------------------------------- /dashboard_app/tests/models_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the models of the ``django-metrics-dashboard`` app.""" 2 | from django.test import TestCase 3 | 4 | from .. import models 5 | from .factories import DashboardWidgetSettingsFactory 6 | 7 | 8 | class DashboardWidgetLastUpdateTestCase(TestCase): 9 | """Tests for the ``DashboardWidgetLastUpdate`` model class.""" 10 | longMessage = True 11 | 12 | def test_model(self): 13 | instance = models.DashboardWidgetLastUpdate() 14 | instance.save() 15 | self.assertTrue(instance.pk) 16 | 17 | 18 | class DashboardWidgetSettingsTestCase(TestCase): 19 | """Tests for the ``DashboardWidgetSettings`` model class.""" 20 | longMessage = True 21 | 22 | def test_model(self): 23 | instance = DashboardWidgetSettingsFactory() 24 | self.assertTrue(instance.pk) 25 | -------------------------------------------------------------------------------- /dashboard_app/tests/runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | This script is a trick to setup a fake Django environment, since this reusable 4 | app will be developed and tested outside any specifiv Django project. 5 | 6 | Via ``settings.configure`` you will be able to set all necessary settings 7 | for your app and run the tests as if you were calling ``./manage.py test``. 8 | 9 | """ 10 | import sys 11 | 12 | from django.conf import settings 13 | 14 | import coverage 15 | 16 | import test_settings 17 | 18 | 19 | if not settings.configured: 20 | settings.configure(**test_settings.__dict__) 21 | 22 | 23 | from django_coverage.coverage_runner import CoverageRunner 24 | from django_nose import NoseTestSuiteRunner 25 | 26 | 27 | class NoseCoverageTestRunner(CoverageRunner, NoseTestSuiteRunner): 28 | """Custom test runner that uses nose and coverage""" 29 | def run_tests(self, *args, **kwargs): 30 | results = super(NoseCoverageTestRunner, self).run_tests( 31 | *args, **kwargs) 32 | coverage._the_coverage.data.write_file('.coverage') 33 | return results 34 | 35 | 36 | def runtests(*test_args): 37 | failures = NoseCoverageTestRunner(verbosity=2, interactive=True).run_tests( 38 | test_args) 39 | sys.exit(failures) 40 | 41 | 42 | if __name__ == '__main__': 43 | runtests(*sys.argv[1:]) 44 | -------------------------------------------------------------------------------- /dashboard_app/tests/south_settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | These settings are used by the ``manage.py`` command. 3 | 4 | With normal tests we want to use the fastest possible way which is an 5 | in-memory sqlite database but if you want to create South migrations you 6 | need a persistant database. 7 | 8 | Unfortunately there seems to be an issue with either South or syncdb so that 9 | defining two routers ("default" and "south") does not work. 10 | 11 | """ 12 | from .test_settings import * # NOQA 13 | 14 | 15 | DATABASES = { 16 | 'default': { 17 | 'ENGINE': 'django.db.backends.sqlite3', 18 | 'NAME': 'db.sqlite', 19 | } 20 | } 21 | 22 | INSTALLED_APPS.append('south', ) 23 | -------------------------------------------------------------------------------- /dashboard_app/tests/test_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-dashboard-app/ed98f2bca91a4ced36d0dd1aa1baee78e989cf64/dashboard_app/tests/test_app/__init__.py -------------------------------------------------------------------------------- /dashboard_app/tests/test_app/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-dashboard-app/ed98f2bca91a4ced36d0dd1aa1baee78e989cf64/dashboard_app/tests/test_app/models.py -------------------------------------------------------------------------------- /dashboard_app/tests/test_app/templates/400.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-dashboard-app/ed98f2bca91a4ced36d0dd1aa1baee78e989cf64/dashboard_app/tests/test_app/templates/400.html -------------------------------------------------------------------------------- /dashboard_app/tests/test_app/templates/500.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-dashboard-app/ed98f2bca91a4ced36d0dd1aa1baee78e989cf64/dashboard_app/tests/test_app/templates/500.html -------------------------------------------------------------------------------- /dashboard_app/tests/test_settings.py: -------------------------------------------------------------------------------- 1 | """Settings that need to be set in order to run the tests.""" 2 | import os 3 | 4 | DEBUG = True 5 | 6 | SITE_ID = 1 7 | 8 | APP_ROOT = os.path.abspath( 9 | os.path.join(os.path.dirname(__file__), '..')) 10 | 11 | 12 | DATABASES = { 13 | 'default': { 14 | 'ENGINE': 'django.db.backends.sqlite3', 15 | 'NAME': ':memory:', 16 | } 17 | } 18 | 19 | ROOT_URLCONF = 'dashboard_app.tests.urls' 20 | 21 | STATIC_URL = '/static/' 22 | STATIC_ROOT = os.path.join(APP_ROOT, '../app_static') 23 | MEDIA_ROOT = os.path.join(APP_ROOT, '../app_media') 24 | STATICFILES_DIRS = ( 25 | os.path.join(APP_ROOT, 'static'), 26 | ) 27 | 28 | TEMPLATE_DIRS = ( 29 | os.path.join(APP_ROOT, 'tests/test_app/templates'), 30 | ) 31 | 32 | COVERAGE_REPORT_HTML_OUTPUT_DIR = os.path.join( 33 | os.path.join(APP_ROOT, 'tests/coverage')) 34 | COVERAGE_MODULE_EXCLUDES = [ 35 | 'tests$', 'settings$', 'urls$', 'locale$', 'test_widget_app$', 36 | 'migrations', 'fixtures', 'admin$', 'django_extensions', 37 | ] 38 | 39 | EXTERNAL_APPS = [ 40 | 'django.contrib.admin', 41 | 'django.contrib.admindocs', 42 | 'django.contrib.auth', 43 | 'django.contrib.contenttypes', 44 | 'django.contrib.humanize', 45 | 'django.contrib.messages', 46 | 'django.contrib.sessions', 47 | 'django.contrib.staticfiles', 48 | 'django.contrib.sitemaps', 49 | 'django.contrib.sites', 50 | 'django_jasmine', 51 | 'django_nose', 52 | ] 53 | 54 | INTERNAL_APPS = [ 55 | 'dashboard_app', 56 | 'dashboard_app.tests.test_widget_app', 57 | ] 58 | 59 | INSTALLED_APPS = EXTERNAL_APPS + INTERNAL_APPS 60 | COVERAGE_MODULE_EXCLUDES += EXTERNAL_APPS 61 | 62 | SECRET_KEY = 'foobar' 63 | -------------------------------------------------------------------------------- /dashboard_app/tests/test_widget_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-dashboard-app/ed98f2bca91a4ced36d0dd1aa1baee78e989cf64/dashboard_app/tests/test_widget_app/__init__.py -------------------------------------------------------------------------------- /dashboard_app/tests/test_widget_app/dashboard_widgets.py: -------------------------------------------------------------------------------- 1 | """ 2 | DummyWidget implementation used by the tests. 3 | 4 | """ 5 | from django.utils.timezone import now 6 | 7 | from dashboard_app.widget_base import DashboardWidgetBase 8 | from dashboard_app.widget_pool import dashboard_widget_pool 9 | 10 | 11 | class DummyWidget(DashboardWidgetBase): 12 | """This widget is used by the tests.""" 13 | template_name = 'test_widget_app/dummy_widget.html' 14 | 15 | def get_context_data(self): 16 | ctx = super(DummyWidget, self).get_context_data() 17 | value_setting = self.get_setting('VALUE') 18 | 19 | error = None 20 | value = None 21 | if value_setting is None: 22 | error = 'Widget has not been saved, yet...' 23 | else: 24 | value = value_setting.value 25 | ctx.update({ 26 | 'value': value, 27 | 'error': error, 28 | }) 29 | return ctx 30 | 31 | def update_widget_data(self): 32 | value = now().strftime(self.time_format) 33 | self.save_setting('VALUE', value) 34 | 35 | 36 | class DummyWidget2(DashboardWidgetBase): 37 | """This widget is used by the tests.""" 38 | template_name = 'test_widget_app/dummy_widget2.html' 39 | 40 | def get_context_data(self): 41 | ctx = super(DummyWidget2, self).get_context_data() 42 | ctx.update({ 43 | 'value': 'Barfoo', 44 | }) 45 | return ctx 46 | 47 | def update_widget_data(self): 48 | pass 49 | 50 | 51 | dashboard_widget_pool.register_widget( 52 | DummyWidget2, widget_name='widget3', position=1) 53 | dashboard_widget_pool.register_widget(DummyWidget2, position=2) 54 | dashboard_widget_pool.register_widget(DummyWidget, position=3) 55 | dashboard_widget_pool.register_widget( 56 | DummyWidget, widget_name='widget4', position=4) 57 | -------------------------------------------------------------------------------- /dashboard_app/tests/test_widget_app/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-dashboard-app/ed98f2bca91a4ced36d0dd1aa1baee78e989cf64/dashboard_app/tests/test_widget_app/models.py -------------------------------------------------------------------------------- /dashboard_app/tests/test_widget_app/templates/test_widget_app/dummy_widget.html: -------------------------------------------------------------------------------- 1 | {% extends "dashboard_app/partials/widget.html" %} 2 | 3 | {% block widget_body %} 4 | {% if is_rendered %} 5 | {% if error %} 6 |

{{ error }}

7 | {% else %} 8 |

{{ value|date:"d. M. y" }}

9 |

{{ value|date:"H:i:s" }}

10 | {% endif %} 11 | {% else %} 12 | {{ block.super }} 13 | {% endif %} 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /dashboard_app/tests/test_widget_app/templates/test_widget_app/dummy_widget2.html: -------------------------------------------------------------------------------- 1 | {% extends "dashboard_app/partials/widget.html" %} 2 | 3 | {% block widget_body %} 4 | {% if is_rendered %} 5 |

This widget does nothing, really.

6 | {% else %} 7 | {{ block.super }} 8 | {% endif %} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /dashboard_app/tests/urls.py: -------------------------------------------------------------------------------- 1 | """URLs to run the tests.""" 2 | from django.conf.urls import patterns, include, url 3 | from django.contrib import admin 4 | 5 | 6 | admin.autodiscover() 7 | 8 | 9 | urlpatterns = patterns( 10 | '', 11 | url(r'^admin/', include(admin.site.urls)), 12 | url(r'^', include('dashboard_app.urls')), 13 | ) 14 | -------------------------------------------------------------------------------- /dashboard_app/tests/views_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the views of the dashboard_app.""" 2 | import time 3 | 4 | from django.test import TestCase 5 | 6 | from django_libs.tests.mixins import ViewRequestFactoryTestMixin 7 | from django_libs.tests.factories import UserFactory 8 | 9 | 10 | from . import mixins 11 | from .. import views 12 | from ..widget_pool import dashboard_widget_pool 13 | 14 | 15 | class DashboardLastUpdateViewTestCase(ViewRequestFactoryTestMixin, 16 | mixins.WidgetTestCaseMixin, 17 | TestCase): 18 | """Tests for the ``DashboardLastUpdateView`` view class.""" 19 | longMessage = True 20 | view_class = views.DashboardLastUpdateView 21 | 22 | def setUp(self): 23 | super(DashboardLastUpdateViewTestCase, self).setUp() 24 | self.superuser = UserFactory(is_superuser=True) 25 | self.normal_user = UserFactory() 26 | 27 | # This ensures that we have one last update in the database 28 | dashboard_widget_pool.get_widgets_that_need_update() 29 | time.sleep(1) 30 | 31 | def test_view(self): 32 | resp = self.get(ajax=True) 33 | self.assertTrue('DummyWidget' in resp.content, msg=( 34 | 'Should return a list all widgets that need an update')) 35 | 36 | 37 | class DashboardViewTestCase(ViewRequestFactoryTestMixin, 38 | mixins.WidgetTestCaseMixin, 39 | TestCase): 40 | """Tests for the ``DashboardView`` view class.""" 41 | longMessage = True 42 | view_class = views.DashboardView 43 | 44 | def setUp(self): 45 | super(DashboardViewTestCase, self).setUp() 46 | self.superuser = UserFactory(is_superuser=True) 47 | self.normal_user = UserFactory() 48 | 49 | def test_security(self): 50 | self.should_redirect_to_login_when_anonymous() 51 | 52 | resp = self.get(user=self.normal_user) 53 | self.assertEqual(resp.status_code, 302, msg=( 54 | "Should redirect to login if the user doesn't have the correct" 55 | " permissions")) 56 | 57 | with self.settings(DASHBOARD_REQUIRE_LOGIN=False): 58 | resp = self.get() 59 | self.assertEqual(resp.status_code, 200, msg=( 60 | 'When REQUIRE_LOGIN is False, anyone should be able to see the' 61 | ' view')) 62 | 63 | def test_view(self): 64 | resp = self.get(user=self.superuser) 65 | self.assertEqual(resp.status_code, 200, msg=( 66 | 'User with correct permissions should be able to see the view')) 67 | 68 | 69 | class DashboardRenderWidgetViewTestCase(ViewRequestFactoryTestMixin, 70 | mixins.WidgetTestCaseMixin, 71 | TestCase,): 72 | """Tests for the DashboardRenderWidgetView.""" 73 | longMessage = True 74 | view_class = views.DashboardRenderWidgetView 75 | 76 | def setUp(self): 77 | super(DashboardRenderWidgetViewTestCase, self).setUp() 78 | dashboard_widget_pool.discover_widgets() 79 | self.superuser = UserFactory(is_superuser=True) 80 | self.normal_user = UserFactory() 81 | self.data = {'name': 'DummyWidget', } 82 | 83 | def test_security(self): 84 | resp = self.get(user=self.normal_user, data=self.data) 85 | self.assertEqual(resp.status_code, 302, msg=( 86 | 'Should redirect to login when user is anonymous')) 87 | 88 | resp = self.get(user=self.normal_user, data=self.data) 89 | self.assertEqual(resp.status_code, 302, msg=( 90 | "Should redirect to login if the user doesn't have the correct" 91 | " permissions")) 92 | 93 | with self.settings(DASHBOARD_REQUIRE_LOGIN=False): 94 | resp = self.get(data=self.data) 95 | self.assertEqual(resp.status_code, 200, msg=( 96 | 'When REQUIRE_LOGIN is False, anyone should be able to see the' 97 | ' view')) 98 | 99 | def test_view(self): 100 | resp = self.get(user=self.superuser, data=self.data) 101 | self.assertEqual(resp.status_code, 200, msg=( 102 | 'User with correct permissions should be able to see the view')) 103 | 104 | 105 | class DashboardGetWidgetViewTestCase(ViewRequestFactoryTestMixin, TestCase): 106 | """Tests for the DashboardGetWidgetView.""" 107 | longMessage = True 108 | view_class = views.DashboardGetWidgetView 109 | -------------------------------------------------------------------------------- /dashboard_app/tests/widget_base_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the widget base class of the dashboard_app.""" 2 | import time 3 | 4 | from django.test import TestCase 5 | 6 | from .. import models 7 | from ..widget_base import DashboardWidgetBase 8 | from .test_widget_app.dashboard_widgets import DummyWidget 9 | 10 | 11 | class DashboardWidgetBaseTestCase(TestCase): 12 | """Tests for the ``DashboardWidgetBase`` base class.""" 13 | longMessage = True 14 | 15 | def test_get_context_data(self): 16 | """get_context_data should return an empty dict by default.""" 17 | base = DashboardWidgetBase() 18 | result = base.get_context_data() 19 | self.assertEqual(result, {'is_rendered': True, }) 20 | 21 | def test_get_name(self): 22 | """ 23 | get_name should return the class name when called on a child class. 24 | 25 | """ 26 | widget = DummyWidget() 27 | result = widget.get_name() 28 | self.assertEqual(result, 'DummyWidget') 29 | 30 | widget = DummyWidget(widget_name='Foobar') 31 | result = widget.get_name() 32 | self.assertEqual(result, 'Foobar') 33 | 34 | def test_get_setting(self): 35 | """get_setting should get the setting from the database.""" 36 | widget = DummyWidget() 37 | setting = widget.get_setting('IS_ENABLED') 38 | self.assertEqual(setting, None, msg=( 39 | 'Should return None if the setting does not exist in the db')) 40 | widget.save_setting('IS_ENABLED', '1') 41 | setting = widget.get_setting('IS_ENABLED') 42 | self.assertEqual(setting.setting_name, 'IS_ENABLED', msg=( 43 | 'Should return the correct setting from the database when called')) 44 | 45 | def test_get_title(self): 46 | """get_title should return the title of the widget.""" 47 | widget = DummyWidget() 48 | result = widget.get_title() 49 | self.assertEqual(result, widget.get_name(), msg=( 50 | 'The default implementation should just return the class name')) 51 | 52 | def test_update_widget_data_not_implemented(self): 53 | """update_widget_data should throw exception if not implemented.""" 54 | base = DashboardWidgetBase() 55 | self.assertRaises(NotImplementedError, base.update_widget_data) 56 | 57 | def test_save_setting(self): 58 | """save_setting should save the value to the database.""" 59 | widget = DummyWidget() 60 | setting = widget.save_setting('IS_ENABLED', '1') 61 | self.assertTrue(setting.pk, msg=( 62 | 'Should create a new DB entry when saving the setting for the' 63 | ' first time')) 64 | self.assertEqual(setting.value, '1', msg=( 65 | 'Should set the correct value on the new setting object')) 66 | 67 | setting2 = widget.save_setting('IS_ENABLED', '0') 68 | self.assertEqual(setting, setting2, msg=( 69 | 'Should not create a new object if that setting already exists' 70 | ' in the database')) 71 | self.assertEqual(setting2.value, '0', msg=( 72 | 'Should update the setting value on save')) 73 | 74 | def test_set_last_update(self): 75 | """ 76 | set_last_update should save the last update in the settings table. 77 | 78 | """ 79 | widget = DummyWidget() 80 | widget.set_last_update() 81 | last_update = models.DashboardWidgetLastUpdate.objects.get( 82 | widget_name=widget.get_name()) 83 | self.assertTrue(last_update.last_update) 84 | 85 | def test_should_update(self): 86 | widget = DummyWidget() 87 | result = widget.should_update() 88 | self.assertFalse(result, msg=( 89 | 'Right after creation it should not update because the last update' 90 | ' date will be set to now and this test will happen faster than' 91 | ' one second later')) 92 | time.sleep(1) 93 | result = widget.should_update() 94 | self.assertTrue(result, msg=( 95 | 'After one second it should return true because the last update' 96 | ' was longer ago than the update interval of the DummyWidget')) 97 | -------------------------------------------------------------------------------- /dashboard_app/tests/widget_pool_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the Widget Pool of the dashboard_app.""" 2 | from django.core.exceptions import ImproperlyConfigured 3 | from django.test import TestCase 4 | 5 | from ..exceptions import WidgetAlreadyRegistered 6 | from ..widget_pool import ( 7 | DashboardWidgetPool, 8 | dashboard_widget_pool, 9 | ) 10 | 11 | from .mixins import WidgetTestCaseMixin 12 | from .test_widget_app.dashboard_widgets import DummyWidget 13 | 14 | 15 | class FalseWidget(object): 16 | """ 17 | This class will not be accepted as a widget. 18 | 19 | Because it does not inherit ``WidgetBase``. 20 | 21 | """ 22 | pass 23 | 24 | 25 | class DashboardWidgetPoolTestCase(WidgetTestCaseMixin, TestCase): 26 | """Tests for the ``WidgetPool`` class.""" 27 | longMessage = True 28 | 29 | def test_instantiates_on_import(self): 30 | """ 31 | Should instantiate WidgetPool when module is imported. 32 | 33 | """ 34 | self.assertEqual( 35 | dashboard_widget_pool.__class__, DashboardWidgetPool, msg=( 36 | 'When importing from `widget_pool`, an instance of' 37 | ' `WidgetPool` should be created')) 38 | 39 | def test_register_false_widget(self): 40 | """ 41 | register_widget should raise exception if widget does not inherit 42 | 43 | ``DashboardWidgetBase``. 44 | 45 | """ 46 | self.assertRaises( 47 | ImproperlyConfigured, dashboard_widget_pool.register_widget, 48 | FalseWidget) 49 | 50 | def test_register_widget(self): 51 | """register_widget should add the widget to ``self.widgets``.""" 52 | self._unregister_widgets() 53 | dashboard_widget_pool.register_widget(DummyWidget) 54 | self.assertTrue('DummyWidget' in dashboard_widget_pool.widgets) 55 | 56 | def test_register_already_registered(self): 57 | """ 58 | register_widget should raise exception if widget is already registered. 59 | 60 | """ 61 | self._unregister_widgets() 62 | dashboard_widget_pool.register_widget(DummyWidget) 63 | self.assertRaises( 64 | WidgetAlreadyRegistered, dashboard_widget_pool.register_widget, 65 | DummyWidget) 66 | 67 | def test_unregister_widget(self): 68 | """ 69 | unregister_widget should be remove the widget from ``self.widgets``. 70 | 71 | """ 72 | self._unregister_widgets() 73 | dashboard_widget_pool.register_widget(DummyWidget) 74 | dashboard_widget_pool.unregister_widget(DummyWidget) 75 | self.assertEqual(dashboard_widget_pool.widgets, {}) 76 | 77 | def test_1_discover_widgets(self): 78 | """ 79 | discover_widgets Should find widgets in INSTALLED_APPS. 80 | 81 | When called again, it should not nothing. 82 | 83 | This test must be executed first before any other test messes around 84 | with the registered widgets. 85 | 86 | """ 87 | dashboard_widget_pool.discover_widgets() 88 | self.assertTrue('DummyWidget' in dashboard_widget_pool.widgets) 89 | self.assertTrue('DummyWidget2' in dashboard_widget_pool.widgets) 90 | 91 | dashboard_widget_pool.discover_widgets() 92 | self.assertTrue('DummyWidget' in dashboard_widget_pool.widgets) 93 | self.assertTrue('DummyWidget2' in dashboard_widget_pool.widgets) 94 | 95 | def test_get_widgets(self): 96 | """get_widgets should discover widgets and return them.""" 97 | widgets = dashboard_widget_pool.get_widgets() 98 | self.assertEqual(widgets, dashboard_widget_pool.widgets) 99 | 100 | def test_get_widgets_sorted(self): 101 | """get_widgets_sorted should return the widgets sorted by position.""" 102 | self._unregister_widgets() 103 | dashboard_widget_pool.register_widget(DummyWidget, position=3) 104 | dashboard_widget_pool.register_widget( 105 | DummyWidget, widget_name='w2', position=2) 106 | dashboard_widget_pool.register_widget( 107 | DummyWidget, widget_name='w3', position=1) 108 | result = dashboard_widget_pool.get_widgets_sorted() 109 | self.assertEqual(result[0][2], 1, msg=( 110 | 'Should return the widget with position 1 first')) 111 | self.assertEqual(result[2][2], 3, msg=( 112 | 'Should return the widget with position 3 last')) 113 | 114 | def test_get_widget(self): 115 | """get_widget should return the given widget.""" 116 | dashboard_widget_pool.discover_widgets() 117 | widget = dashboard_widget_pool.get_widget('DummyWidget') 118 | self.assertEqual(widget.get_name(), 'DummyWidget', msg=( 119 | 'Should return the correct widget')) 120 | -------------------------------------------------------------------------------- /dashboard_app/urls.py: -------------------------------------------------------------------------------- 1 | """URLs for the dashboard_app app.""" 2 | from django.conf.urls import patterns, url 3 | 4 | from . import views 5 | 6 | 7 | urlpatterns = patterns( 8 | '', 9 | url(r'^$', 10 | views.DashboardView.as_view(), 11 | name='dashboard_app_dashboard'), 12 | url(r'^get-outdated/$', 13 | views.DashboardLastUpdateView.as_view(), 14 | name='dashboard_app_get_last_updates'), 15 | url(r'^widget/$', 16 | views.DashboardRenderWidgetView.as_view(), 17 | name='dashboard_app_render_widget'), 18 | url(r'^api/widget/$', 19 | views.DashboardGetRemoteWidgetView.as_view(), 20 | name='dashboard_app_remote_widget'), 21 | ) 22 | -------------------------------------------------------------------------------- /dashboard_app/view_mixins.py: -------------------------------------------------------------------------------- 1 | """Useful mixins for views.""" 2 | import json 3 | 4 | from django import http 5 | from django.utils.decorators import method_decorator 6 | 7 | from .decorators import permission_required 8 | 9 | 10 | class JSONResponseMixin(object): 11 | """Mixin to convert the response content into a JSON response.""" 12 | def render_to_response(self, context): 13 | "Returns a JSON response containing 'context' as payload" 14 | return self.get_json_response(self.convert_context_to_json(context)) 15 | 16 | def get_json_response(self, content, **httpresponse_kwargs): 17 | "Construct an `HttpResponse` object." 18 | return http.HttpResponse(content, 19 | content_type='application/json', 20 | **httpresponse_kwargs) 21 | 22 | def convert_context_to_json(self, context): 23 | "Convert the context dictionary into a JSON object" 24 | return json.dumps(context) 25 | 26 | 27 | class PermissionRequiredViewMixin(object): 28 | """ 29 | Mixin to protect a view and require ``can_view_dashboard`` permission. 30 | 31 | Permission will only be required if the ``DASHBOARD_REQUIRE_LOGIN`` 32 | setting is ``True``. 33 | 34 | """ 35 | @method_decorator( 36 | permission_required('dashboard_app.can_view_dashboard')) 37 | def dispatch(self, request, *args, **kwargs): 38 | return super(PermissionRequiredViewMixin, self).dispatch( 39 | request, *args, **kwargs) 40 | 41 | 42 | class RenderWidgetMixin(object): 43 | """Mixin for views that are supposed to render a widget.""" 44 | def get_context_data(self, **kwargs): 45 | """ 46 | Adds ``is_rendered`` to the context and the widget's context data. 47 | 48 | ``is_rendered`` signals that the AJAX view has been called and that 49 | we are displaying the full widget now. When ``is_rendered`` is not 50 | found in the widget template it means that we are seeing the first 51 | page load and all widgets still have to get their real data from 52 | this AJAX view. 53 | 54 | """ 55 | ctx = super(RenderWidgetMixin, self).get_context_data(**kwargs) 56 | ctx.update({ 57 | 'is_rendered': True, 58 | 'widget': self.widget, 59 | }) 60 | ctx.update(self.widget.get_context_data()) 61 | return ctx 62 | 63 | def get_template_names(self): 64 | return [self.widget.template_name, ] 65 | -------------------------------------------------------------------------------- /dashboard_app/views.py: -------------------------------------------------------------------------------- 1 | """Views for the dashboard_app app.""" 2 | from django.conf import settings 3 | from django.http import Http404, HttpResponse 4 | from django.template.defaultfilters import date 5 | from django.views.generic import TemplateView, View 6 | 7 | import requests 8 | 9 | from . import view_mixins 10 | from .dashboard_widgets import RemoteWidget 11 | from .widget_pool import dashboard_widget_pool 12 | 13 | 14 | class DashboardLastUpdateView(view_mixins.JSONResponseMixin, View): 15 | """Returns a JSON dict of widgets and their last update time.""" 16 | def get(self, request, *args, **kwargs): 17 | widgets = dashboard_widget_pool.get_widgets_that_need_update() 18 | result = {} 19 | for widget in widgets: 20 | result[widget.get_name()] = date( 21 | widget.get_last_update().last_update, "c") 22 | return self.render_to_response(result) 23 | 24 | 25 | class DashboardView(view_mixins.PermissionRequiredViewMixin, TemplateView): 26 | """ 27 | Main view of the app. Displays the metrics dashboard. 28 | 29 | Widgets on the dashboard get loaded individually via AJAX calls against 30 | the ``DashboardAPIWidgetView``. 31 | 32 | """ 33 | template_name = 'dashboard_app/dashboard.html' 34 | 35 | def get_context_data(self, **kwargs): 36 | ctx = super(DashboardView, self).get_context_data(**kwargs) 37 | widgets = dashboard_widget_pool.get_widgets_sorted() 38 | ctx.update({ 39 | 'widgets': widgets, 40 | }) 41 | return ctx 42 | 43 | 44 | class DashboardRenderWidgetView(view_mixins.PermissionRequiredViewMixin, 45 | view_mixins.RenderWidgetMixin, 46 | TemplateView): 47 | """AJAX view that renders any given widget by name.""" 48 | def dispatch(self, request, *args, **kwargs): 49 | self.widget = dashboard_widget_pool.get_widget( 50 | request.GET.get('name')) 51 | if isinstance(self.widget, RemoteWidget): 52 | url = self.widget.url 53 | payload = { 54 | 'token': self.widget.token, 55 | 'name': self.widget.remote_widget_name, 56 | } 57 | r = requests.get(url, params=payload, verify=False) 58 | return HttpResponse(r.text) 59 | return super(DashboardRenderWidgetView, self).dispatch( 60 | request, *args, **kwargs) 61 | 62 | 63 | class DashboardGetRemoteWidgetView(view_mixins.RenderWidgetMixin, 64 | TemplateView): 65 | """ 66 | Returns a widget as requested by a remote server. 67 | 68 | """ 69 | def dispatch(self, request, *args, **kwargs): 70 | self.widget_name = request.GET.get('name') 71 | self.widget = dashboard_widget_pool.get_widget(self.widget_name) 72 | self.token = request.GET.get('token') 73 | self.access = getattr(settings, 'DASHBOARD_REMOTE_ACCESS', {}) 74 | if not self.widget_name in self.access: 75 | raise Http404 76 | if not self.token in self.access[self.widget_name]: 77 | raise Http404 78 | return super(DashboardGetRemoteWidgetView, self).dispatch( 79 | request, *args, **kwargs) 80 | -------------------------------------------------------------------------------- /dashboard_app/widget_base.py: -------------------------------------------------------------------------------- 1 | """Base DashboardWidget of the dashboard_app.""" 2 | from django.utils.timezone import now 3 | 4 | from . import models 5 | 6 | 7 | class DashboardWidgetBase(object): 8 | """All DashboardWidgets must inherit this base class.""" 9 | update_interval = 1 10 | template_name = 'dashboard_app/partials/widget.html' 11 | 12 | def __init__(self, **kwargs): 13 | """ 14 | Allows to initialise the widget with special options. 15 | 16 | :param widget_name: By setting the name, you override the get_name 17 | method and always return that name instead. This allows you to 18 | register the same widget class several times with different names. 19 | 20 | """ 21 | for key, value in kwargs.iteritems(): 22 | setattr(self, key, value) 23 | 24 | def get_context_data(self): 25 | """ 26 | Should return a dictionary of template context variables that are 27 | needed to render this widget. 28 | 29 | """ 30 | return {'is_rendered': True, } 31 | 32 | def get_last_update(self): 33 | """Gets or creates the last update object for this widget.""" 34 | instance, created = \ 35 | models.DashboardWidgetLastUpdate.objects.get_or_create( 36 | widget_name=self.get_name()) 37 | return instance 38 | 39 | def get_name(self): 40 | """ 41 | Returns the class name of this widget. 42 | 43 | Be careful when overriding this. If ``self.widget_name`` is set, you 44 | should always return that in order to allow to register this widget 45 | class several times with different names. 46 | 47 | """ 48 | if hasattr(self, 'widget_name'): 49 | return self.widget_name 50 | return self.__class__.__name__ 51 | 52 | def get_setting(self, setting_name, default=None): 53 | """ 54 | Returns the setting for this widget from the database. 55 | 56 | :setting_name: The name of the setting. 57 | :default: Optional default value if the setting cannot be found. 58 | 59 | """ 60 | try: 61 | setting = models.DashboardWidgetSettings.objects.get( 62 | widget_name=self.get_name(), 63 | setting_name=setting_name) 64 | except models.DashboardWidgetSettings.DoesNotExist: 65 | setting = default 66 | return setting 67 | 68 | def get_title(self): 69 | """ 70 | Returns the title of this widget. 71 | 72 | This should be shown on the dashboard. 73 | 74 | """ 75 | return self.get_name() 76 | 77 | def save_setting(self, setting_name, value): 78 | """Saves the setting value into the database.""" 79 | setting = self.get_setting(setting_name) 80 | if setting is None: 81 | setting = models.DashboardWidgetSettings.objects.create( 82 | widget_name=self.get_name(), 83 | setting_name=setting_name, 84 | value=value) 85 | setting.value = value 86 | setting.save() 87 | return setting 88 | 89 | def set_last_update(self): 90 | """Sets the last update time to ``now()``.""" 91 | last_update = self.get_last_update() 92 | last_update.save() # The model has auto_now_update=True 93 | 94 | def should_update(self): 95 | """ 96 | Checks if an update is needed. 97 | 98 | Checks against ``self.update_interval`` and this widgets 99 | ``DashboardWidgetLastUpdate`` instance if an update is overdue. 100 | 101 | This should be called by 102 | ``DashboardWidgetPool.get_widgets_that_need_update()``, which in turn 103 | should be called by an admin command which should be scheduled every 104 | minute via crontab. 105 | 106 | """ 107 | last_update = self.get_last_update() 108 | time_since = now() - last_update.last_update 109 | if time_since.seconds < self.update_interval: 110 | return False 111 | return True 112 | 113 | def update_widget_data(self): 114 | """ 115 | Implement this in your widget in order to update the widget's data. 116 | 117 | This is the place where you would call some third party API, retrieve 118 | some data and save it into your widget's model. 119 | 120 | """ 121 | raise NotImplementedError 122 | -------------------------------------------------------------------------------- /dashboard_app/widget_pool.py: -------------------------------------------------------------------------------- 1 | """Widget Pool for the dashboard_app.""" 2 | from django.core.exceptions import ImproperlyConfigured 3 | 4 | from django_load.core import load 5 | 6 | from dashboard_app.exceptions import WidgetAlreadyRegistered 7 | from dashboard_app.widget_base import DashboardWidgetBase 8 | 9 | 10 | class DashboardWidgetPool(object): 11 | """ 12 | A pool of registered DashboardWidgets. 13 | 14 | This class should only be instantiated at the end of this file, therefore 15 | serving as a singleton. All other files should just import the instance 16 | created in this file. 17 | 18 | Inspired by 19 | https://github.com/divio/django-cms/blob/develop/cms/plugin_pool.py 20 | 21 | """ 22 | def __init__(self): 23 | self.widgets = {} 24 | self.discovered = False 25 | 26 | def discover_widgets(self): 27 | """ 28 | Searches for widgets in all INSTALLED_APPS. 29 | 30 | This will be called when you call ``get_all_widgets`` for the first 31 | time. 32 | 33 | """ 34 | if self.discovered: 35 | return 36 | load('dashboard_widgets') 37 | self.discovered = True 38 | 39 | def get_widgets(self): 40 | """Discovers all widgets and returns them.""" 41 | self.discover_widgets() 42 | return self.widgets 43 | 44 | def get_widgets_sorted(self): 45 | """Returns the widgets sorted by position.""" 46 | result = [] 47 | for widget_name, widget in self.get_widgets().items(): 48 | result.append((widget_name, widget, widget.position)) 49 | result.sort(key=lambda x: x[2]) 50 | return result 51 | 52 | def get_widget(self, widget_name): 53 | """Returns the widget that matches the given widget name.""" 54 | return self.widgets[widget_name] 55 | 56 | def get_widgets_that_need_update(self): 57 | """ 58 | Returns all widgets that need an update. 59 | 60 | This should be scheduled every minute via crontab. 61 | 62 | """ 63 | result = [] 64 | for widget_name, widget in self.get_widgets().items(): 65 | if widget.should_update(): 66 | result.append(widget) 67 | return result 68 | 69 | def register_widget(self, widget_cls, **widget_kwargs): 70 | """ 71 | Registers the given widget. 72 | 73 | Widgets must inherit ``DashboardWidgetBase`` and you cannot register 74 | the same widget twice. 75 | 76 | :widget_cls: A class that inherits ``DashboardWidgetBase``. 77 | 78 | """ 79 | if not issubclass(widget_cls, DashboardWidgetBase): 80 | raise ImproperlyConfigured( 81 | 'DashboardWidgets must be subclasses of DashboardWidgetBase,' 82 | ' {0} is not.'.format(widget_cls)) 83 | 84 | widget = widget_cls(**widget_kwargs) 85 | widget_name = widget.get_name() 86 | if widget_name in self.widgets: 87 | raise WidgetAlreadyRegistered( 88 | 'Cannot register {0}, a plugin with this name {1} is already ' 89 | 'registered.'.format(widget_cls, widget_name)) 90 | 91 | self.widgets[widget_name] = widget 92 | 93 | def unregister_widget(self, widget_cls): 94 | """Unregisters the given widget.""" 95 | if widget_cls.__name__ in self.widgets: 96 | del self.widgets[widget_cls().get_name()] 97 | 98 | 99 | dashboard_widget_pool = DashboardWidgetPool() 100 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # How to create your Sphinx documentation 2 | 3 | In order to kickstart your Sphinx documentation, please do the following: 4 | 5 | ## Create virtual environment. 6 | 7 | If you haven't done so already, create a virtual environment for this reusable 8 | app like so: 9 | 10 | mkvirtualenv -p python2.7 django-dashboard-app 11 | pip install Sphinx 12 | deactivate 13 | workon django-dashboard-app 14 | sphinx-quickstart 15 | 16 | Answer the questions: 17 | 18 | > Root path for the documentation [.]: 19 | > Separate source and build directories (y/N) [n]: y 20 | > Name prefix for templates and static dir [_]: 21 | > Project name: Django Dashboard App 22 | > Author name(s): Martin Brochhaus 23 | > Project version: 0.1 24 | > Project release [0.1]: 25 | > Source file suffix [.rst]: 26 | > Name of your master document (without suffix) [index]: 27 | > Do you want to use the epub builder (y/N) [n]: 28 | > autodoc: automatically insert docstrings from modules (y/N) [n]: y 29 | > doctest: automatically test code snippets in doctest blocks (y/N) [n]: 30 | > intersphinx: link between Sphinx documentation of different projects (y/N) [n]: y 31 | > todo: write "todo" entries that can be shown or hidden on build (y/N) [n]: y 32 | > coverage: checks for documentation coverage (y/N) [n]: y 33 | > pngmath: include math, rendered as PNG images (y/N) [n]: 34 | > mathjax: include math, rendered in the browser by MathJax (y/N) [n]: 35 | > ifconfig: conditional inclusion of content based on config values (y/N) [n]: y 36 | > viewcode: include links to the source code of documented Python objects (y/N) [n]: y 37 | > Create Makefile? (Y/n) [y]: 38 | > Create Windows command file? (Y/n) [y]: 39 | -------------------------------------------------------------------------------- /hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import glob 4 | import os 5 | import sys 6 | 7 | os.environ['PYFLAKES_NODOCTEST'] = '1' 8 | 9 | # pep8.py uses sys.argv to find setup.cfg 10 | sys.argv = [os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)] 11 | 12 | # git usurbs your bin path for hooks and will always run system python 13 | if 'VIRTUAL_ENV' in os.environ: 14 | site_packages = glob.glob( 15 | '%s/lib/*/site-packages' % os.environ['VIRTUAL_ENV'])[0] 16 | sys.path.insert(0, site_packages) 17 | 18 | 19 | def main(): 20 | from flake8.main import DEFAULT_CONFIG 21 | from flake8.engine import get_style_guide 22 | from flake8.hooks import run 23 | 24 | gitcmd = "git diff-index --cached --name-only HEAD" 25 | 26 | _, files_modified, _ = run(gitcmd) 27 | 28 | # remove non-py files and files which no longer exist 29 | files_modified = filter( 30 | lambda x: x.endswith('.py') and os.path.exists(x), 31 | files_modified) 32 | 33 | flake8_style = get_style_guide(parse_argv=True, config_file=DEFAULT_CONFIG) 34 | report = flake8_style.check_files(files_modified) 35 | 36 | return report.total_errors 37 | 38 | if __name__ == '__main__': 39 | sys.exit(main()) 40 | -------------------------------------------------------------------------------- /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', 7 | 'dashboard_app.tests.south_settings') 8 | 9 | from django.core.management import execute_from_command_line 10 | 11 | execute_from_command_line(sys.argv) 12 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = F999,E501,E128,E124 3 | exclude = .git,*/migrations/*,*/static/CACHE/* 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Python setup file for the dashboard_app app. 4 | 5 | In order to register your app at pypi.python.org, create an account at 6 | pypi.python.org and login, then register your new app like so: 7 | 8 | python setup.py register 9 | 10 | If your name is still free, you can now make your first release but first you 11 | should check if you are uploading the correct files: 12 | 13 | python setup.py sdist 14 | 15 | Inspect the output thoroughly. There shouldn't be any temp files and if your 16 | app includes staticfiles or templates, make sure that they appear in the list. 17 | If something is wrong, you need to edit MANIFEST.in and run the command again. 18 | 19 | If all looks good, you can make your first release: 20 | 21 | python setup.py sdist upload 22 | 23 | For new releases, you need to bump the version number in 24 | dashboard_app/__init__.py and re-run the above command. 25 | 26 | For more information on creating source distributions, see 27 | http://docs.python.org/2/distutils/sourcedist.html 28 | 29 | """ 30 | import os 31 | from setuptools import setup, find_packages 32 | import dashboard_app as app 33 | 34 | 35 | dev_requires = [ 36 | 'flake8', 37 | ] 38 | 39 | install_requires = [ 40 | 'django', 41 | 'django-libs', 42 | 'django-load', 43 | 'requests', 44 | ] 45 | 46 | 47 | def read(fname): 48 | try: 49 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 50 | except IOError: 51 | return '' 52 | 53 | setup( 54 | name="django-dashboard-app", 55 | version=app.__version__, 56 | description=read('DESCRIPTION'), 57 | long_description=read('README.rst'), 58 | license='The MIT License', 59 | platforms=['OS Independent'], 60 | keywords='django, app, reusable, dashboard, grid, widgets', 61 | author='Martin Brochhaus', 62 | author_email='mbrochh@gmail.com', 63 | url="https://github.com/bitmazk/django-dashboard-app", 64 | packages=find_packages(), 65 | include_package_data=True, 66 | install_requires=install_requires, 67 | extras_require={ 68 | 'dev': dev_requires, 69 | }, 70 | ) 71 | -------------------------------------------------------------------------------- /test_requirements.txt: -------------------------------------------------------------------------------- 1 | django-load 2 | django-nose 3 | coverage 4 | django-coverage 5 | django-jasmine 6 | ipdb 7 | flake8 8 | fabric 9 | factory_boy 10 | mock 11 | selenium 12 | south 13 | --------------------------------------------------------------------------------