├── metrics_dashboard ├── tests │ ├── __init__.py │ ├── integration_tests │ │ ├── __init__.py │ │ └── views_tests.py │ ├── test_widget_app │ │ ├── __init__.py │ │ ├── models.py │ │ ├── templates │ │ │ └── test_widget_app │ │ │ │ ├── dummy_widget2.html │ │ │ │ └── dummy_widget.html │ │ └── dashboard_widgets.py │ ├── test_templates │ │ ├── registration │ │ │ └── login.html │ │ └── README.md │ ├── test_settings.py │ ├── factories.py │ ├── models_tests.py │ ├── mixins.py │ ├── urls.py │ ├── south_settings.py │ ├── update_widget_data_tests.py │ ├── runtests.py │ ├── settings.py │ ├── widget_base_tests.py │ └── widget_pool_tests.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── update_widget_data.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── static │ └── metrics_dashboard │ │ ├── css │ │ ├── styles.less │ │ └── libs │ │ │ ├── jquery.gridster.css │ │ │ └── gridster.styles.css │ │ └── js │ │ └── libs │ │ ├── modernizr-2.6.2.min.js │ │ ├── jquery.gridster.min.js │ │ └── jquery.gridster.with-extras.min.js ├── __init__.py ├── settings.py ├── exceptions.py ├── urls.py ├── messenger.py ├── decorators.py ├── models.py ├── templates │ └── metrics_dashboard │ │ ├── dashboard_base.html │ │ └── dashboard.html ├── widget_pool.py ├── views.py └── widget_base.py ├── CHANGELOG.txt ├── fabfile ├── __init__.py ├── fabric_settings.py.sample └── local.py ├── .gitignore ├── DESCRIPTION ├── AUTHORS ├── manage.py ├── MANIFEST.in ├── docs ├── source │ ├── index.rst │ └── conf.py ├── make.bat └── Makefile ├── setup.py ├── requirements.txt ├── LICENSE └── README.rst /metrics_dashboard/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /metrics_dashboard/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /metrics_dashboard/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /metrics_dashboard/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /metrics_dashboard/tests/integration_tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /metrics_dashboard/tests/test_widget_app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /metrics_dashboard/tests/test_widget_app/models.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | === 0.1 === 2 | 3 | - Initial commit 4 | -------------------------------------------------------------------------------- /metrics_dashboard/static/metrics_dashboard/css/styles.less: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fabfile/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from .local import * 3 | -------------------------------------------------------------------------------- /metrics_dashboard/tests/test_templates/registration/login.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fabfile/fabric_settings.py.sample: -------------------------------------------------------------------------------- 1 | PROJECT_NAME = 'metrics_dashboard' 2 | -------------------------------------------------------------------------------- /metrics_dashboard/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __version__ = '0.1' 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info/ 3 | coverage/ 4 | db.sqlite 5 | dist/ 6 | docs/build/ 7 | fabric_settings.py 8 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | A reusable app to display any number of widgets on a dashboard, showing real 2 | time data about whatever you want. 3 | -------------------------------------------------------------------------------- /metrics_dashboard/settings.py: -------------------------------------------------------------------------------- 1 | """Default values for settings of the ``django-metrics-dashboard`` app.""" 2 | REQUIRE_LOGIN = True 3 | -------------------------------------------------------------------------------- /metrics_dashboard/exceptions.py: -------------------------------------------------------------------------------- 1 | """Exceptions for the ``django-metrics-dashboard`` app.""" 2 | 3 | 4 | class WidgetAlreadyRegistered(Exception): 5 | pass 6 | -------------------------------------------------------------------------------- /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 | * Jonathan Barratt (reduxionist) 9 | -------------------------------------------------------------------------------- /metrics_dashboard/tests/test_settings.py: -------------------------------------------------------------------------------- 1 | """Settings that need to be set in order to run the tests.""" 2 | from settings import * # NOQA 3 | 4 | INSTALLED_APPS.remove('metrics_dashboard.tests.test_widget_app') 5 | INSTALLED_APPS.append('test_widget_app') 6 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == '__main__': 6 | os.environ.setdefault( 7 | 'DJANGO_SETTINGS_MODULE', 'metrics_dashboard.tests.south_settings') 8 | from django.core.management import execute_from_command_line 9 | execute_from_command_line(sys.argv) 10 | -------------------------------------------------------------------------------- /metrics_dashboard/tests/test_templates/README.md: -------------------------------------------------------------------------------- 1 | This folder contains templates that we need in our tests but that are not part 2 | of the app. 3 | 4 | For example, one test checks if a view redirects to the login view. Therefore 5 | we need the `login.html` template but it is not in the scope of this app to 6 | provide such a template. 7 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include LICENSE 3 | include DESCRIPTION 4 | include CHANGELOG.txt 5 | include README.md 6 | graft metrics_dashboard 7 | graft fabfile 8 | prune metrics_dashboard/tests/coverage 9 | recursive-exclude metrics_dashboard *.orig *.pyc .ropeproject 10 | recursive-exclude fabfile *.orig *.pyc fabric_settings.py 11 | -------------------------------------------------------------------------------- /metrics_dashboard/tests/factories.py: -------------------------------------------------------------------------------- 1 | """Factories for the ``django-metrics-dashboard`` app.""" 2 | import factory 3 | 4 | from metrics_dashboard.models import DashboardWidgetSettings 5 | 6 | 7 | class DashboardWidgetSettingsFactory(factory.Factory): 8 | FACTORY_FOR = DashboardWidgetSettings 9 | 10 | widget_name = 'my_widget' 11 | setting_name = 'my_setting' 12 | value = '1' 13 | -------------------------------------------------------------------------------- /fabfile/local.py: -------------------------------------------------------------------------------- 1 | """Fabric task that eases development of the project.""" 2 | from fabric.api import local 3 | 4 | from .fabric_settings import PROJECT_NAME 5 | 6 | 7 | def flake8(): 8 | """Runs flake8 check against all files.""" 9 | local('flake8 --statistics metrics_dashboard/.') 10 | 11 | 12 | def test(): 13 | """Runs the tests.""" 14 | local('./{0}/tests/runtests.py'.format(PROJECT_NAME)) 15 | -------------------------------------------------------------------------------- /metrics_dashboard/urls.py: -------------------------------------------------------------------------------- 1 | """URLs for the ``django-metrics-dashboard`` app.""" 2 | from django.conf.urls.defaults import patterns, url 3 | 4 | from metrics_dashboard.views import DashboardView, DashboardAPIWidgetView 5 | 6 | 7 | urlpatterns = patterns( 8 | '', 9 | url(r'^$', 10 | DashboardView.as_view(), 11 | name='dashboard_view',), 12 | 13 | url(r'^api/widget/(?P\w+)/', 14 | DashboardAPIWidgetView.as_view(), 15 | name='dashboard_api_widget',), 16 | ) 17 | -------------------------------------------------------------------------------- /metrics_dashboard/messenger.py: -------------------------------------------------------------------------------- 1 | """Utility functions to send messages to ``django-socketio-messenger``.""" 2 | import requests 3 | 4 | from django.conf import settings 5 | 6 | 7 | def broadcast_channel(channel, message): 8 | """ 9 | Forwards a message to your ``django-socketio-messenger`` installation. 10 | 11 | """ 12 | url = settings.DASHBOARD_MESSENGER_URL 13 | payload = {'channel': channel, 'message': message, } 14 | response = requests.post(url, data=payload) 15 | return response 16 | -------------------------------------------------------------------------------- /metrics_dashboard/tests/models_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the models of the ``django-metrics-dashboard`` app.""" 2 | from django.test import TestCase 3 | 4 | from metrics_dashboard.tests.factories import DashboardWidgetSettingsFactory 5 | 6 | 7 | class DashboardWidgetSettingsTestCase(TestCase): 8 | """Tests for the ``DashboardWidgetSettings`` model class.""" 9 | longMessage = True 10 | 11 | def test_model(self): 12 | instance = DashboardWidgetSettingsFactory() 13 | self.assertTrue(instance.pk) 14 | -------------------------------------------------------------------------------- /metrics_dashboard/tests/mixins.py: -------------------------------------------------------------------------------- 1 | """Mixins for the tests of the ``django-metrics-dashboard`` app.""" 2 | from metrics_dashboard.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 | -------------------------------------------------------------------------------- /metrics_dashboard/tests/test_widget_app/templates/test_widget_app/dummy_widget2.html: -------------------------------------------------------------------------------- 1 | 7 |
8 | 9 | 10 | 15 | 16 |
11 |

{{ value }}

12 |

This widget does nothing, really.

13 |

It's just... big.

14 |
17 |
18 | -------------------------------------------------------------------------------- /metrics_dashboard/tests/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | This ``urls.py`` is only used when running the tests via ``runtests.py``. 3 | As you know, every app must be hooked into your main ``urls.py`` so that 4 | you can actually reach the app's views (provided it has any views, of course). 5 | 6 | """ 7 | from django.conf.urls.defaults import include, patterns, url 8 | 9 | 10 | urlpatterns = patterns( 11 | '', 12 | url(r'^accounts/login/$', 13 | 'django.contrib.auth.views.login', 14 | name='auth_login'), 15 | 16 | url(r'^', include('metrics_dashboard.urls')), 17 | ) 18 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. Django Metrics Dashboard documentation master file, created by 2 | sphinx-quickstart on Fri Dec 14 07:59:50 2012. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Django Metrics Dashboard's documentation! 7 | ==================================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | 23 | -------------------------------------------------------------------------------- /metrics_dashboard/tests/test_widget_app/templates/test_widget_app/dummy_widget.html: -------------------------------------------------------------------------------- 1 | 10 |
11 | 12 | 13 | 18 | 19 |
14 |

Last update

15 |

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

16 |

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

17 |
20 |
21 | -------------------------------------------------------------------------------- /metrics_dashboard/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 .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 | -------------------------------------------------------------------------------- /metrics_dashboard/tests/update_widget_data_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the ``update_widget_data`` admin command.""" 2 | from django.core.management import call_command 3 | from django.test import TestCase 4 | 5 | from mock import patch 6 | 7 | from metrics_dashboard.widget_pool import dashboard_widget_pool 8 | 9 | 10 | class UpdateWidgetDataTestCase(TestCase): 11 | """Tests for the ``update_widget_data`` admin command.""" 12 | def test_command(self): 13 | """update_widget_data should run without errors""" 14 | widget = dashboard_widget_pool.widgets['DummyWidget'] 15 | with patch.object(widget, 'update_widget_data', 16 | return_value=None) as mock_method: 17 | call_command('update_widget_data') 18 | mock_method.assert_called_once() 19 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | import metrics_dashboard 4 | 5 | 6 | def read(fname): 7 | try: 8 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 9 | except IOError: 10 | return '' 11 | 12 | 13 | setup( 14 | name="django-metrics-dashboard", 15 | version=metrics_dashboard.__version__, 16 | description=read('DESCRIPTION'), 17 | long_description=read('README.rst'), 18 | license='The MIT License', 19 | platforms=['OS Independent'], 20 | keywords='django, reusable, metrix, dashboard', 21 | author='Martin Brochhaus', 22 | author_email='mbrochh@gmail.com', 23 | url="https://github.com/bitmazk/django-metrics-dashboard", 24 | packages=find_packages(), 25 | include_package_data=True, 26 | ) 27 | -------------------------------------------------------------------------------- /metrics_dashboard/management/commands/update_widget_data.py: -------------------------------------------------------------------------------- 1 | """Custom admin command that updates the data for all registered widgets.""" 2 | from django.conf import settings 3 | from django.core.management.base import BaseCommand 4 | 5 | from django_libs.decorators import lockfile 6 | 7 | from metrics_dashboard.widget_pool import dashboard_widget_pool 8 | 9 | 10 | class Command(BaseCommand): 11 | help = 'Updates the data for all registered widgets.' 12 | 13 | @lockfile(getattr( 14 | settings, 'UPDATE_WIDGET_DATA_LOCKFILE', 'update_widget_data')) 15 | def handle(self, *args, **options): 16 | for widget_name, widget in dashboard_widget_pool.get_widgets().items(): 17 | if widget.should_update(): 18 | widget.update_widget_data() 19 | widget.set_last_update() 20 | message = 'Successfully updated {0}\n' 21 | else: 22 | message = 'No update needed for {0}\n' 23 | self.stdout.write(message.format(widget.get_name())) 24 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Install these requirements if you wish to contribute to this project. 2 | 3 | # =========================================================================== 4 | # Packages essential to this app. Needed by anyone who wants to use this app. 5 | # =========================================================================== 6 | Django==1.4.3 7 | django-libs==0.13 8 | South==0.7.6 9 | django-load==1.0.0 10 | requests==0.14.2 11 | django-socketio==0.3.5 12 | lockfile==0.9.1 13 | 14 | 15 | # =========================================================================== 16 | # Packages for building the docs 17 | # =========================================================================== 18 | Sphinx==1.1.3 19 | 20 | 21 | # ============================================================== 22 | # Packages needed for running the tests. Needed by contributors. 23 | # ============================================================== 24 | Fabric==1.4.4 25 | nose==1.2.1 26 | django-nose==1.1 27 | coverage==3.6b1 28 | django-coverage==1.2.2 29 | django-jasmine==0.3.2 30 | selenium==2.28.0 31 | factory_boy==1.2.0 32 | watchdog==0.6.0 33 | mock==1.0.1 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2012 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 | -------------------------------------------------------------------------------- /metrics_dashboard/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 os 11 | import sys 12 | 13 | from django.conf import settings 14 | import test_settings 15 | 16 | 17 | if not settings.configured: 18 | settings.configure(**test_settings.__dict__) 19 | 20 | 21 | from django_coverage.coverage_runner import CoverageRunner 22 | from django_nose import NoseTestSuiteRunner 23 | 24 | 25 | class NoseCoverageTestRunner(CoverageRunner, NoseTestSuiteRunner): 26 | """Custom test runner that uses nose and coverage""" 27 | pass 28 | 29 | 30 | def runtests(*test_args): 31 | failures = NoseCoverageTestRunner(verbosity=2, interactive=True).run_tests( 32 | test_args) 33 | sys.exit(failures) 34 | 35 | 36 | if __name__ == '__main__': 37 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) 38 | runtests(*sys.argv[1:]) 39 | -------------------------------------------------------------------------------- /metrics_dashboard/decorators.py: -------------------------------------------------------------------------------- 1 | """Decorators for the ``django-metrics-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 metrics_dashboard 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: 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 | -------------------------------------------------------------------------------- /metrics_dashboard/models.py: -------------------------------------------------------------------------------- 1 | """Models for the ``django-metrics-dashboard`` app.""" 2 | from django.db import models 3 | from django.utils.translation import ugettext_lazy as _ 4 | 5 | 6 | class DashboardWidgetSettings(models.Model): 7 | """ 8 | Table that can hold all settings for all widgets. 9 | 10 | :widget_name: This should be a name that uniquely identifies a widget. 11 | :setting_name: This is the name of the setting to be saved. 12 | :value: This is the value of the setting. 13 | """ 14 | class Meta: 15 | unique_together = ('widget_name', 'setting_name', ) 16 | 17 | # This permission is actually not relevant to the model but defines if 18 | # a user has access to the dashboard view. It's just added to this 19 | # model for convenience, it could just as well be on any other model. 20 | permissions = ( 21 | ('can_view_dashboard', 'Can view the dashboard'), 22 | ) 23 | 24 | widget_name = models.CharField( 25 | max_length=128, 26 | verbose_name=_('Widget name'), 27 | ) 28 | 29 | setting_name = models.CharField( 30 | max_length=128, 31 | verbose_name=_('Setting name'), 32 | ) 33 | 34 | value = models.CharField( 35 | max_length=4000, 36 | verbose_name=_('Setting name'), 37 | ) 38 | 39 | def __unicode__(self): 40 | return '{0} of {1}'.format(self.setting_name, self.widget_name) 41 | -------------------------------------------------------------------------------- /metrics_dashboard/tests/test_widget_app/dashboard_widgets.py: -------------------------------------------------------------------------------- 1 | """ 2 | DummyWidget implementation used by the tests. 3 | 4 | """ 5 | from django.utils.translation import ugettext_lazy as _ 6 | from django.utils.timezone import datetime, now 7 | 8 | from metrics_dashboard.messenger import broadcast_channel 9 | from metrics_dashboard.widget_base import DashboardWidgetBase 10 | from metrics_dashboard.widget_pool import dashboard_widget_pool 11 | 12 | 13 | class DummyWidget(DashboardWidgetBase): 14 | """This widget is used by the tests.""" 15 | template_name = 'test_widget_app/dummy_widget.html' 16 | settings = { 17 | 'VALUE': { 18 | 'verbose_name': _('Value'), 19 | } 20 | } 21 | 22 | time_format = '%d.%m.%Y %H:%M:%S' 23 | sizex = 1 24 | sizey = 1 25 | 26 | def get_context_data(self): 27 | ctx = super(DummyWidget, self).get_context_data() 28 | value = self.get_setting('VALUE').value 29 | date = datetime.strptime(value, self.time_format) 30 | ctx.update({ 31 | 'value': date, 32 | }) 33 | return ctx 34 | 35 | def update_widget_data(self): 36 | value = now().strftime(self.time_format) 37 | self.save_setting('VALUE', value) 38 | broadcast_channel(self.get_name(), 'update') 39 | 40 | 41 | class DummyWidget2(DashboardWidgetBase): 42 | """This widget is used by the tests.""" 43 | template_name = 'test_widget_app/dummy_widget2.html' 44 | sizex = 2 45 | sizey = 1 46 | 47 | def get_context_data(self): 48 | ctx = super(DummyWidget2, self).get_context_data() 49 | ctx.update({ 50 | 'value': 'Barfoo', 51 | }) 52 | return ctx 53 | 54 | def update_widget_data(self): 55 | pass 56 | 57 | 58 | dashboard_widget_pool.register_widget(DummyWidget) 59 | dashboard_widget_pool.register_widget(DummyWidget2) 60 | -------------------------------------------------------------------------------- /metrics_dashboard/templates/metrics_dashboard/dashboard_base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% block title %}Django Metrics Dashboard{% endblock %} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | {% block "main" %} 25 |

Hello world! This is HTML5 Boilerplate.

26 | {% endblock %} 27 | 28 | 29 | 30 | 31 | {% block "extrajs" %}{% endblock %} 32 | 33 | 34 | -------------------------------------------------------------------------------- /metrics_dashboard/tests/settings.py: -------------------------------------------------------------------------------- 1 | """Settings that need to be set in order to run the tests.""" 2 | import os 3 | 4 | 5 | DEBUG = True 6 | 7 | 8 | DASHBOARD_REQUIRE_LOGIN = False 9 | DASHBOARD_MESSENGER_URL = 'http://127.0.0.1:9000/broadcast_channel/' 10 | 11 | SOCKETIO_HOST = 'http://127.0.0.1:7070/broadcast_messenger/' 12 | 13 | SITE_ID = 1 14 | 15 | PROJECT_ROOT = os.path.dirname(__file__) 16 | 17 | 18 | DATABASES = { 19 | 'default': { 20 | 'ENGINE': 'django.db.backends.sqlite3', 21 | 'NAME': ':memory:', 22 | } 23 | } 24 | 25 | ROOT_URLCONF = 'metrics_dashboard.tests.urls' 26 | 27 | STATIC_URL = '/static/' 28 | STATIC_ROOT = os.path.join(__file__, '../../../static/') 29 | MEDIA_ROOT = os.path.join(__file__, '../../../media/') 30 | STATICFILES_DIRS = ( 31 | os.path.join(__file__, 'test_static'), 32 | ) 33 | 34 | TEMPLATE_DIRS = ( 35 | os.path.join(os.path.dirname(__file__), '../templates'), 36 | os.path.join(os.path.dirname(__file__), 'test_templates'), 37 | ) 38 | 39 | COVERAGE_REPORT_HTML_OUTPUT_DIR = os.path.join( 40 | os.path.dirname(__file__), 'coverage') 41 | COVERAGE_MODULE_EXCLUDES = [ 42 | 'tests$', 'test_widget_app', 'settings$', 'urls$', 'locale$', 43 | 'migrations', 'fixtures', 'admin$', 'django_extensions', 44 | ] 45 | 46 | EXTERNAL_APPS = [ 47 | 'django.contrib.admin', 48 | 'django.contrib.admindocs', 49 | 'django.contrib.auth', 50 | 'django.contrib.contenttypes', 51 | 'django.contrib.messages', 52 | 'django.contrib.sessions', 53 | 'django.contrib.staticfiles', 54 | 'django.contrib.sitemaps', 55 | 'django.contrib.sites', 56 | 'django_jasmine', 57 | 'django_nose', 58 | 'django_socketio', 59 | ] 60 | 61 | INTERNAL_APPS = [ 62 | 'metrics_dashboard', 63 | 'metrics_dashboard.tests.test_widget_app', 64 | ] 65 | 66 | INSTALLED_APPS = EXTERNAL_APPS + INTERNAL_APPS 67 | COVERAGE_MODULE_EXCLUDES += EXTERNAL_APPS 68 | -------------------------------------------------------------------------------- /metrics_dashboard/static/metrics_dashboard/css/libs/jquery.gridster.css: -------------------------------------------------------------------------------- 1 | /*! gridster.js - v0.1.0 - 2012-10-20 2 | * http://gridster.net/ 3 | * Copyright (c) 2012 ducksboard; Licensed MIT */ 4 | 5 | .gridster { 6 | position:relative; 7 | } 8 | 9 | .gridster > * { 10 | margin: 0 auto; 11 | -webkit-transition: height .4s; 12 | -moz-transition: height .4s; 13 | -o-transition: height .4s; 14 | -ms-transition: height .4s; 15 | transition: height .4s; 16 | } 17 | 18 | .gridster .gs_w{ 19 | z-index: 2; 20 | position: absolute; 21 | } 22 | 23 | .ready .gs_w:not(.preview-holder) { 24 | -webkit-transition: opacity .3s, left .3s, top .3s; 25 | -moz-transition: opacity .3s, left .3s, top .3s; 26 | -o-transition: opacity .3s, left .3s, top .3s; 27 | transition: opacity .3s, left .3s, top .3s; 28 | } 29 | 30 | .ready .gs_w:not(.preview-holder) { 31 | -webkit-transition: opacity .3s, left .3s, top .3s, width .3s, height .3s; 32 | -moz-transition: opacity .3s, left .3s, top .3s, width .3s, height .3s; 33 | -o-transition: opacity .3s, left .3s, top .3s, width .3s, height .3s; 34 | transition: opacity .3s, left .3s, top .3s, width .3s, height .3s; 35 | } 36 | 37 | .gridster .preview-holder { 38 | z-index: 1; 39 | position: absolute; 40 | background-color: #fff; 41 | border-color: #fff; 42 | opacity: 0.3; 43 | } 44 | 45 | .gridster .player-revert { 46 | z-index: 10!important; 47 | -webkit-transition: left .3s, top .3s!important; 48 | -moz-transition: left .3s, top .3s!important; 49 | -o-transition: left .3s, top .3s!important; 50 | transition: left .3s, top .3s!important; 51 | } 52 | 53 | .gridster .dragging { 54 | z-index: 10!important; 55 | -webkit-transition: all 0s !important; 56 | -moz-transition: all 0s !important; 57 | -o-transition: all 0s !important; 58 | transition: all 0s !important; 59 | } 60 | 61 | /* Uncomment this if you set helper : "clone" in draggable options */ 62 | /*.gridster .player { 63 | opacity:0; 64 | }*/ 65 | -------------------------------------------------------------------------------- /metrics_dashboard/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # -*- coding: utf-8 -*- 3 | import 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 'DashboardWidgetSettings' 13 | db.create_table('metrics_dashboard_dashboardwidgetsettings', ( 14 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 15 | ('widget_name', self.gf('django.db.models.fields.CharField')(max_length=128)), 16 | ('setting_name', self.gf('django.db.models.fields.CharField')(max_length=128)), 17 | ('value', self.gf('django.db.models.fields.CharField')(max_length=4000)), 18 | )) 19 | db.send_create_signal('metrics_dashboard', ['DashboardWidgetSettings']) 20 | 21 | # Adding unique constraint on 'DashboardWidgetSettings', fields ['widget_name', 'setting_name'] 22 | db.create_unique('metrics_dashboard_dashboardwidgetsettings', ['widget_name', 'setting_name']) 23 | 24 | 25 | def backwards(self, orm): 26 | # Removing unique constraint on 'DashboardWidgetSettings', fields ['widget_name', 'setting_name'] 27 | db.delete_unique('metrics_dashboard_dashboardwidgetsettings', ['widget_name', 'setting_name']) 28 | 29 | # Deleting model 'DashboardWidgetSettings' 30 | db.delete_table('metrics_dashboard_dashboardwidgetsettings') 31 | 32 | 33 | models = { 34 | 'metrics_dashboard.dashboardwidgetsettings': { 35 | 'Meta': {'unique_together': "(('widget_name', 'setting_name'),)", 'object_name': 'DashboardWidgetSettings'}, 36 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 37 | 'setting_name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 38 | 'value': ('django.db.models.fields.CharField', [], {'max_length': '4000'}), 39 | 'widget_name': ('django.db.models.fields.CharField', [], {'max_length': '128'}) 40 | } 41 | } 42 | 43 | complete_apps = ['metrics_dashboard'] 44 | -------------------------------------------------------------------------------- /metrics_dashboard/tests/integration_tests/views_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the views of the ``django-metrics-dashboard`` app.""" 2 | from django.test import TestCase 3 | from django.test.client import RequestFactory 4 | 5 | from django_libs.tests.factories import UserFactory 6 | from django_libs.tests.mixins import ViewTestMixin 7 | 8 | from metrics_dashboard.tests.mixins import WidgetTestCaseMixin 9 | from metrics_dashboard.views import DashboardView, DashboardAPIWidgetView 10 | 11 | 12 | class DashboardViewTestMixin(object): 13 | def setUp(self): 14 | super(DashboardViewTestMixin, self).setUp() 15 | self.factory = RequestFactory() 16 | self.request = self.factory.get(self.get_url()) 17 | self.request.user = UserFactory() 18 | 19 | 20 | class DashboardAPIWidgetViewTestCase(WidgetTestCaseMixin, 21 | DashboardViewTestMixin, ViewTestMixin, 22 | TestCase): 23 | """Tests for the ``DashboardAPIWidgetView`` view class.""" 24 | def get_view_kwargs(self): 25 | return { 26 | 'widget_name': 'DummyWidget', 27 | } 28 | 29 | def get_view_name(self): 30 | return 'dashboard_api_widget' 31 | 32 | def test_view(self): 33 | """api view should return the correct template and context""" 34 | kwargs = self.get_view_kwargs() 35 | response = DashboardAPIWidgetView().dispatch(self.request, **kwargs) 36 | self.assertTrue('value' in response.context_data) 37 | 38 | 39 | class DashboardViewTestCase(WidgetTestCaseMixin, DashboardViewTestMixin, 40 | ViewTestMixin, TestCase): 41 | def get_view_name(self): 42 | return 'dashboard_view' 43 | 44 | def test_view_anonymous(self): 45 | """View should be callable for anonymous users.""" 46 | self.should_be_callable_when_anonymous() 47 | 48 | def test_view_login_required(self): 49 | """View should not be callable for anonymous if REQUIRE_LOGIN==True.""" 50 | with self.settings(DASHBOARD_REQUIRE_LOGIN=True): 51 | self.should_redirect_to_login_when_anonymous() 52 | 53 | def test_get_context_data(self): 54 | """View should add all registered widgets to the context.""" 55 | self._unregister_widgets() 56 | response = DashboardView().dispatch(self.request) 57 | self.assertTrue('widgets' in response.context_data) 58 | -------------------------------------------------------------------------------- /metrics_dashboard/widget_pool.py: -------------------------------------------------------------------------------- 1 | """Widget Pool for the ``django-metrics-dashboard`` app.""" 2 | from django.core.exceptions import ImproperlyConfigured 3 | 4 | from django_load.core import load 5 | 6 | from metrics_dashboard.exceptions import WidgetAlreadyRegistered 7 | from metrics_dashboard.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 register_widget(self, widget_cls): 45 | """ 46 | Registers the given widget. 47 | 48 | Widgets must inherit ``DashboardWidgetBase`` and you cannot register 49 | the same widget twice. 50 | 51 | :widget_cls: A class that inherits ``DashboardWidgetBase``. 52 | 53 | """ 54 | if not issubclass(widget_cls, DashboardWidgetBase): 55 | raise ImproperlyConfigured( 56 | 'DashboardWidgets must be subclasses of DashboardWidgetBase,' 57 | ' {0} is not.'.format(widget_cls)) 58 | 59 | widget = widget_cls() 60 | widget_name = widget.get_name() 61 | if widget_name in self.widgets: 62 | raise WidgetAlreadyRegistered( 63 | 'Cannot register {0}, a plugin with this name {1} is already ' 64 | 'registered.'.format(widget_cls, widget_name)) 65 | 66 | self.widgets[widget_name] = widget 67 | 68 | def unregister_widget(self, widget_cls): 69 | """Unregisters the given widget.""" 70 | if widget_cls.__name__ in self.widgets: 71 | del self.widgets[widget_cls().get_name()] 72 | 73 | 74 | dashboard_widget_pool = DashboardWidgetPool() 75 | -------------------------------------------------------------------------------- /metrics_dashboard/templates/metrics_dashboard/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends "metrics_dashboard/dashboard_base.html" %} 2 | {% load socketio_tags %} 3 | {% load url from future %} 4 | 5 | {% block "main" %} 6 |
7 |
8 |
    9 | {% for widget_name, widget in widgets.items %} 10 |
  • 11 | {% endfor %} 12 |
13 |
14 |
15 | {% endblock %} 16 | 17 | 18 | {% block "extrajs" %} 19 | {% socketio %} 20 | 76 | {% endblock %} 77 | -------------------------------------------------------------------------------- /metrics_dashboard/views.py: -------------------------------------------------------------------------------- 1 | """Views of the ``django-metrics-dashboard`` app.""" 2 | from django.conf import settings 3 | from django.views.generic import TemplateView 4 | from django.utils.decorators import method_decorator 5 | 6 | from metrics_dashboard.decorators import permission_required 7 | from metrics_dashboard.widget_pool import dashboard_widget_pool 8 | 9 | 10 | class PermissionRequiredViewMixin(object): 11 | """ 12 | Mixin to protect a view and require ``can_view_dashboard`` permission. 13 | 14 | Permission will only be required if the ``DASHBOARD_REQUIRE_LOGIN`` 15 | setting is ``True``. 16 | 17 | """ 18 | @method_decorator( 19 | permission_required('metrics_dashboard.can_view_dashboard')) 20 | def dispatch(self, request, *args, **kwargs): 21 | return super(PermissionRequiredViewMixin, self).dispatch( 22 | request, *args, **kwargs) 23 | 24 | 25 | class DashboardView(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 | It also loads socket.io and reloads an individual widget's template when 33 | the widget's data has been updated. This means, once this view is loaded, 34 | the page doesn't have to be refreshed at all. The widgets will simply 35 | update themselves. 36 | 37 | """ 38 | template_name = 'metrics_dashboard/dashboard.html' 39 | 40 | def get_context_data(self, **kwargs): 41 | ctx = super(DashboardView, self).get_context_data(**kwargs) 42 | widgets = dashboard_widget_pool.get_widgets() 43 | ctx.update({ 44 | 'widgets': widgets, 45 | 'SOCKETIO_HOST': settings.SOCKETIO_HOST, 46 | }) 47 | return ctx 48 | 49 | 50 | class DashboardAPIWidgetView(PermissionRequiredViewMixin, TemplateView): 51 | """ 52 | View to be called via AJAX. Returns the template of a widget. 53 | 54 | This allows us to update widgets individually whenever their data has been 55 | updated. 56 | 57 | """ 58 | def dispatch(self, request, *args, **kwargs): 59 | self.widget = dashboard_widget_pool.get_widgets()[ 60 | kwargs.get('widget_name')] 61 | return super(DashboardAPIWidgetView, self).dispatch( 62 | request, *args, **kwargs) 63 | 64 | def get_context_data(self, **kwargs): 65 | ctx = super(DashboardAPIWidgetView, self).get_context_data(**kwargs) 66 | ctx.update(self.widget.get_context_data()) 67 | return ctx 68 | 69 | def get_template_names(self): 70 | return [self.widget.template_name, ] 71 | -------------------------------------------------------------------------------- /metrics_dashboard/tests/widget_base_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the widget base class of the ``django-metrics-dashboard`` app.""" 2 | from django.test import TestCase 3 | 4 | from metrics_dashboard.widget_base import DashboardWidgetBase 5 | from metrics_dashboard.tests.test_widget_app.dashboard_widgets import ( 6 | DummyWidget, 7 | ) 8 | 9 | 10 | class DashboardWidgetBaseTestCase(TestCase): 11 | """Tests for the ``DashboardWidgetBase`` base class.""" 12 | longMessage = True 13 | 14 | def test_get_context_data(self): 15 | """get_context_data should return the widget name by default.""" 16 | base = DashboardWidgetBase() 17 | result = base.get_context_data() 18 | self.assertEqual(result['widget_name'], base.get_name()) 19 | 20 | def test_get_name(self): 21 | """ 22 | get_name should return the class name when called on a child class. 23 | 24 | """ 25 | widget = DummyWidget() 26 | result = widget.get_name() 27 | self.assertEqual(result, 'DummyWidget') 28 | 29 | def test_update_widget_data_not_implemented(self): 30 | """update_widget_data should throw exception if not implemented.""" 31 | base = DashboardWidgetBase() 32 | self.assertRaises(NotImplementedError, base.update_widget_data) 33 | 34 | def test_save_setting(self): 35 | """save_setting should save the value to the database.""" 36 | widget = DummyWidget() 37 | setting = widget.save_setting('IS_ENABLED', '1') 38 | self.assertTrue(setting.pk, msg=( 39 | 'Should create a new DB entry when saving the setting for the' 40 | ' first time')) 41 | self.assertEqual(setting.value, '1', msg=( 42 | 'Should set the correct value on the new setting object')) 43 | 44 | setting2 = widget.save_setting('IS_ENABLED', '0') 45 | self.assertEqual(setting, setting2, msg=( 46 | 'Should not create a new object if that setting already exists' 47 | ' in the database')) 48 | self.assertEqual(setting2.value, '0', msg=( 49 | 'Should update the setting value on save')) 50 | 51 | def test_get_setting(self): 52 | """get_setting should get the setting from the database.""" 53 | widget = DummyWidget() 54 | setting = widget.get_setting('IS_ENABLED') 55 | self.assertEqual(setting, None, msg=( 56 | 'Should return None if the setting does not exist in the db')) 57 | widget.save_setting('IS_ENABLED', '1') 58 | setting = widget.get_setting('IS_ENABLED') 59 | self.assertEqual(setting.setting_name, 'IS_ENABLED', msg=( 60 | 'Should return the correct setting from the database when called')) 61 | -------------------------------------------------------------------------------- /metrics_dashboard/static/metrics_dashboard/css/libs/gridster.styles.css: -------------------------------------------------------------------------------- 1 | /* ============================================================================ 2 | NORMALIZATION 3 | ============================================================================ */ 4 | * { 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | article, aside, details, figcaption, figure, footer, header, hgroup, nav, section { display: block; } 10 | [hidden] { display: none; } 11 | 12 | ul, ol { 13 | list-style: none; 14 | } 15 | 16 | 17 | /* ============================================================================ 18 | COMMONS 19 | ============================================================================ */ 20 | * { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; } 21 | 22 | body { 23 | background-color: #EEEEEE; 24 | font-family: 'Helvetica Neue', Arial, sans-serif; 25 | -webkit-font-smoothing: antialiased; 26 | font-size: x-small; 27 | color: #666666; 28 | } 29 | 30 | a { 31 | color: #004756; 32 | text-decoration: underline; 33 | font-weight: bold; 34 | } 35 | 36 | .wrapper { 37 | margin: 0 auto; 38 | width: 960px; 39 | } 40 | 41 | 42 | [role='header'] { 43 | padding-top: 78px; 44 | text-align: center; 45 | background: #FFF; 46 | } 47 | 48 | 49 | /* ============================================================================ 50 | GRIDSTER STYLES 51 | ============================================================================ */ 52 | .gridsterSection { 53 | margin: 3em 0; 54 | padding: 7.5em 0 5.5em; 55 | background: #004756; 56 | } 57 | 58 | .gridsterSection:hover .gridster { 59 | opacity: 1; 60 | } 61 | 62 | .gridster { 63 | width: 940px; 64 | margin: 0 auto; 65 | 66 | opacity: .8; 67 | 68 | -webkit-transition: opacity .6s; 69 | -moz-transition: opacity .6s; 70 | -o-transition: opacity .6s; 71 | -ms-transition: opacity .6s; 72 | transition: opacity .6s; 73 | } 74 | 75 | .gridster .gs_w { 76 | background: #FFF; 77 | cursor: pointer; 78 | -webkit-box-shadow: 0 0 5px rgba(0,0,0,0.3); 79 | box-shadow: 0 0 5px rgba(0,0,0,0.3); 80 | } 81 | 82 | .gridster .player { 83 | -webkit-box-shadow: 3px 3px 5px rgba(0,0,0,0.3); 84 | box-shadow: 3px 3px 5px rgba(0,0,0,0.3); 85 | } 86 | 87 | 88 | .gridster .gs_w.try { 89 | background-image: url(../img/sprite.png); 90 | background-repeat: no-repeat; 91 | background-position: 37px -169px; 92 | 93 | } 94 | 95 | .gridster .preview-holder { 96 | border: none!important; 97 | border-radius: 0!important; 98 | background: rgba(255,255,255,.2)!important; 99 | } 100 | -------------------------------------------------------------------------------- /metrics_dashboard/widget_base.py: -------------------------------------------------------------------------------- 1 | """Base DashboardWidget of the ``django-metrics-dashboard`` app.""" 2 | from django.utils.translation import ugettext_lazy as _ 3 | from django.utils.timezone import datetime, now 4 | 5 | from metrics_dashboard.models import DashboardWidgetSettings 6 | 7 | 8 | class DashboardWidgetBase(object): 9 | """All DashboardWidgets must inherit this base class.""" 10 | base_settings = { 11 | 'IS_ENABLED': { 12 | 'verbose_name': _('Is enabled'), 13 | }, 14 | 'LAST_UPDATE': { 15 | 'verbose_name': _('Last update'), 16 | } 17 | } 18 | 19 | settings = {} 20 | 21 | update_time_format = '%d.%m.%Y %H:%M:%S' 22 | update_interval = 1 23 | 24 | def get_context_data(self): 25 | """ 26 | Should return a dictionary of template context variables. 27 | 28 | """ 29 | return { 30 | 'widget_name': self.get_name(), 31 | } 32 | 33 | def get_name(self): 34 | """Returns the class name of this widget.""" 35 | return self.__class__.__name__ 36 | 37 | def get_setting(self, setting_name, default=None): 38 | """ 39 | Returns the setting for this widget from the database. 40 | 41 | :setting_name: The name of the setting. 42 | :default: Optional default value if the setting cannot be found. 43 | 44 | """ 45 | try: 46 | setting = DashboardWidgetSettings.objects.get( 47 | widget_name=self.get_name(), 48 | setting_name=setting_name) 49 | except DashboardWidgetSettings.DoesNotExist: 50 | setting = default 51 | return setting 52 | 53 | def save_setting(self, setting_name, value): 54 | """Saves the setting value into the database.""" 55 | setting = self.get_setting(setting_name) 56 | if setting is None: 57 | setting = DashboardWidgetSettings.objects.create( 58 | widget_name=self.get_name(), 59 | setting_name=setting_name, 60 | value=value) 61 | setting.value = value 62 | setting.save() 63 | return setting 64 | 65 | def set_last_update(self): 66 | """Sets the ``LAST_UPDATE`` setting to ``now()``.""" 67 | self.save_setting('LAST_UPDATE', now().strftime( 68 | self.update_time_format)) 69 | 70 | def should_update(self): 71 | """ 72 | Checks if an update is needed. 73 | 74 | Checks against ``self.update_interval`` and the ``LAST_UPDATE`` setting 75 | if update is needed. 76 | 77 | """ 78 | last_update = self.get_setting('LAST_UPDATE') 79 | if not last_update: 80 | last_update = datetime(1900, 1, 1) 81 | else: 82 | last_update = datetime.strptime( 83 | last_update.value, self.update_time_format) 84 | time_since = now() - last_update 85 | if time_since.seconds < self.update_interval: 86 | return False 87 | return True 88 | 89 | def update_widget_data(self): 90 | """ 91 | Implement this in your widget in order to update the widget's data. 92 | 93 | This is the place where you would call some third party API, retrieve 94 | some data and save it into your widget's model. 95 | 96 | """ 97 | raise NotImplementedError 98 | -------------------------------------------------------------------------------- /metrics_dashboard/tests/widget_pool_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the Widget Pool of the ``django-metrics-dashboard`` app.""" 2 | from django.test import TestCase 3 | 4 | from django.core.exceptions import ImproperlyConfigured 5 | from metrics_dashboard.exceptions import WidgetAlreadyRegistered 6 | from metrics_dashboard.tests.mixins import WidgetTestCaseMixin 7 | from metrics_dashboard.tests.test_widget_app.dashboard_widgets import ( 8 | DummyWidget, 9 | ) 10 | from metrics_dashboard.widget_pool import ( 11 | DashboardWidgetPool, 12 | dashboard_widget_pool, 13 | ) 14 | 15 | 16 | class FalseWidget(object): 17 | """ 18 | This class will not be accepted as a widget. 19 | 20 | Because it does not inherit ``WidgetBase``. 21 | 22 | """ 23 | pass 24 | 25 | 26 | class DashboardWidgetPoolTestCase(WidgetTestCaseMixin, TestCase): 27 | """Tests for the ``WidgetPool`` class.""" 28 | longMessage = True 29 | 30 | def test_instantiates_on_import(self): 31 | """ 32 | Should instantiate WidgetPool when module is imported. 33 | 34 | """ 35 | self.assertEqual( 36 | dashboard_widget_pool.__class__, DashboardWidgetPool, msg=( 37 | 'When importing from `widget_pool`, an instance of' 38 | ' `WidgetPool` should be created')) 39 | 40 | def test_register_false_widget(self): 41 | """ 42 | register_widget should raise exception if widget does not inherit 43 | 44 | ``DashboardWidgetBase``. 45 | 46 | """ 47 | self.assertRaises( 48 | ImproperlyConfigured, dashboard_widget_pool.register_widget, 49 | FalseWidget) 50 | 51 | def test_register_widget(self): 52 | """register_widget should add the widget to ``self.widgets``.""" 53 | self._unregister_widgets() 54 | dashboard_widget_pool.register_widget(DummyWidget) 55 | self.assertTrue('DummyWidget' in dashboard_widget_pool.widgets) 56 | 57 | def test_register_already_registered(self): 58 | """ 59 | register_widget should raise exception if widget is already registered. 60 | 61 | """ 62 | self._unregister_widgets() 63 | dashboard_widget_pool.register_widget(DummyWidget) 64 | self.assertRaises( 65 | WidgetAlreadyRegistered, dashboard_widget_pool.register_widget, 66 | DummyWidget) 67 | 68 | def test_unregister_widget(self): 69 | """ 70 | unregister_widget should be remove the widget from ``self.widgets``. 71 | 72 | """ 73 | self._unregister_widgets() 74 | dashboard_widget_pool.register_widget(DummyWidget) 75 | dashboard_widget_pool.unregister_widget(DummyWidget) 76 | self.assertEqual(dashboard_widget_pool.widgets, {}) 77 | 78 | def test_1_discover_widgets(self): 79 | """ 80 | discover_widgets Should find widgets in INSTALLED_APPS. 81 | 82 | When called again, it should not nothing. 83 | 84 | This test must be executed first before any other test messes around 85 | with the registered widgets. 86 | 87 | """ 88 | dashboard_widget_pool.discover_widgets() 89 | self.assertTrue('DummyWidget' in dashboard_widget_pool.widgets) 90 | self.assertTrue('DummyWidget2' in dashboard_widget_pool.widgets) 91 | 92 | dashboard_widget_pool.discover_widgets() 93 | self.assertTrue('DummyWidget' in dashboard_widget_pool.widgets) 94 | self.assertTrue('DummyWidget2' in dashboard_widget_pool.widgets) 95 | 96 | def test_get_widgets(self): 97 | """get_widgets should discover widgets and return them.""" 98 | widgets = dashboard_widget_pool.get_widgets() 99 | self.assertEqual(widgets, dashboard_widget_pool.widgets) 100 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\DjangoMetricsDashboard.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\DjangoMetricsDashboard.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DjangoMetricsDashboard.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DjangoMetricsDashboard.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/DjangoMetricsDashboard" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DjangoMetricsDashboard" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django Metrics Dashboard 2 | ======================== 3 | 4 | A reusable Django app that allows you to display a dashboard with any number 5 | of widgets to show any data you care about. The widgets are updated via 6 | socket.io, so you never need to refresh your dashboard. 7 | 8 | You need to setup a socket.io-server that accepts incoming subscriptions and 9 | sends out broadcasts when widgets need an update. For this we have created 10 | https://github.com/bitmazk/django-socketio-messenger 11 | 12 | TODO: Write a blog post about how to set this up on Webfaction. 13 | 14 | Prerequisites 15 | ------------- 16 | 17 | You need at least the following packages in your virtualenv: 18 | 19 | * Django 1.4.3 20 | * South 21 | * django-libs 22 | * django-load 23 | * requests 24 | * django-socketio 25 | * lockfile 26 | 27 | Installation 28 | ------------ 29 | 30 | To get the latest stable release from PyPi:: 31 | 32 | $ pip install django-metrics-dashboard 33 | 34 | To get the latest commit from GitHub:: 35 | 36 | $ pip install -e git://github.com/bitmazk/django-metrics-dashboard.git#egg=metrics_dashboard 37 | 38 | Add the app to your ``INSTALLED_APPS``:: 39 | 40 | INSTALLED_APPS = [ 41 | ... 42 | 'metrics_dashboard', 43 | ] 44 | 45 | Hook the app into your main ``urls.py``:: 46 | 47 | urlpatterns += patterns('', 48 | ... 49 | url(r'^dashboard/', include('metrics_dashboard.urls')), 50 | ) 51 | 52 | Run the south migrations to create the app's database tables:: 53 | 54 | $ ./manage.py migrate metrics_dashboard 55 | 56 | 57 | Settings 58 | -------- 59 | 60 | DASHBOARD_REQUIRE_LOGIN 61 | +++++++++++++++++++++++ 62 | 63 | *Default*: ``True`` 64 | 65 | When you set this to false, anyone can access the dashboard. If you are 66 | displaying sensitive metrics, you might want to leave this at ``True``. 67 | 68 | 69 | DASHBOARD_MESSENGER_URL 70 | +++++++++++++++++++++++ 71 | 72 | *Default*: No default, you have to set this. 73 | 74 | Set this to the API endpoint of your ``django-socketio-messenger`` 75 | installation. A valid value should look like this:: 76 | 77 | http://[:]/broadcast_channel/ 78 | 79 | Depending on your setup you might or might not need to specify a port. 80 | 81 | We need this because all messages going through socketio must be sent from 82 | the same process. However, this app needs to broadcast messages from an 83 | admin command which get's executed from a cron job, therefore that command 84 | would be a different process than your wsgi process. As a simple (silly and 85 | hackish) solution we created `django-socketio-messenger `_ 86 | which is really just another mini Django app that functions as your socket.io 87 | server. Therefore this app would send HTTP requests to your 88 | ``django-socketio-messenger`` which then would broadcast those messages to 89 | your connected socket.io subscribers. 90 | 91 | 92 | Usage 93 | ----- 94 | 95 | For now: Install it and go visit the URL :) More features coming soon. 96 | 97 | 98 | Creating widgets 99 | ---------------- 100 | 101 | * See https://github.com/bitmazk/md-pypispy-users as an example. 102 | * Create a new Django app. Per convention, you should call your app something 103 | like ``md_yourwidgetname``. This way we can easily search 104 | PyPi for ``md_`` and will find all widgets that have been 105 | published. 106 | * Give it a file ``dashboard_widget.py`` 107 | * Implement your widget. It should inherit ``DashboardWidgetBase`` 108 | * Your widget needs the following implementations: 109 | * a ``template_name`` attribute, just like any Django view 110 | * ``sizex`` & ``sizey`` attributes that define the widget size 111 | * an ``update_interval`` attribute in seconds. 112 | * a ``get_context_data`` method. It should return a dictionary 113 | of template context variables 114 | * a ``update_widget_data`` method. It should get data from a 3rd party API 115 | and save it to the widget's model. Then it should send a message to the 116 | widget's socket.io channel so that the subscribed browsers know that the 117 | widget has new data and needs an update. 118 | * Register your widget with the ``dashboard_widget_pool``. 119 | 120 | Example ``dashboard_widgets.py``:: 121 | 122 | from metrics_dashboard.widget_base import DashboardWidgetBase 123 | from metrics_dashboard.widget_pool import dashboard_widget_pool 124 | 125 | class DummyWidget(DashboardWidgetBase): 126 | """This widget is used by the tests.""" 127 | template_name = 'dashboardwidget_dummy/dummy_widget.html' 128 | sizex = 2 129 | sizey = 1 130 | update_interval = 60 131 | 132 | def get_context_data(self): 133 | return { 134 | 'value': 'Foobar', 135 | } 136 | 137 | def update_widget_data(self): 138 | # TODO: add example implementation here. 139 | 140 | dashboard_widget_pool.register_widget(DummyWidget) 141 | 142 | 143 | Contribute 144 | ---------- 145 | 146 | If you want to contribute to this project, please perform the following steps:: 147 | 148 | # Fork this repository 149 | # Clone your fork 150 | $ mkvirtualenv -p python2.7 django-metrics-dashboard 151 | $ pip install -r requirements.txt 152 | 153 | $ git co -b feature_branch master 154 | # Implement your feature and tests 155 | $ git add . && git commit 156 | $ git push -u origin feature_branch 157 | # Send us a pull request for your feature branch 158 | 159 | 160 | Testing 161 | ------- 162 | 163 | If you want to contribute to this project you can run the tests without setting 164 | up a Django project. Just clone this repository and execute the 165 | ``runtests.py``:: 166 | 167 | $ ./metrics_dashboard/tests/runtests.py 168 | 169 | Sometimes a new feature needs new South migrations, in this case you should 170 | do the following:: 171 | 172 | $ rm db.sqlite 173 | $ ./manage.py syncdb --migrate 174 | $ ./manage.py schemamigration metrics_dashboard --auto 175 | 176 | 177 | Discuss 178 | ------- 179 | 180 | If you have questions or issues, please open an issue on GitHub. 181 | 182 | If we don't react quickly, please don't hesitate to ping me on Twitter 183 | (`@mbrochh `_) 184 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Django Metrics Dashboard documentation build configuration file, created by 4 | # sphinx-quickstart on Fri Dec 14 07:59:50 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'Django Metrics Dashboard' 44 | copyright = u'2012, Martin Brochhaus' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '0.1' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '0.1' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = [] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'default' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | #html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | #html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = None 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'DjangoMetricsDashboarddoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | latex_elements = { 173 | # The paper size ('letterpaper' or 'a4paper'). 174 | #'papersize': 'letterpaper', 175 | 176 | # The font size ('10pt', '11pt' or '12pt'). 177 | #'pointsize': '10pt', 178 | 179 | # Additional stuff for the LaTeX preamble. 180 | #'preamble': '', 181 | } 182 | 183 | # Grouping the document tree into LaTeX files. List of tuples 184 | # (source start file, target name, title, author, documentclass [howto/manual]). 185 | latex_documents = [ 186 | ('index', 'DjangoMetricsDashboard.tex', u'Django Metrics Dashboard Documentation', 187 | u'Martin Brochhaus', 'manual'), 188 | ] 189 | 190 | # The name of an image file (relative to this directory) to place at the top of 191 | # the title page. 192 | #latex_logo = None 193 | 194 | # For "manual" documents, if this is true, then toplevel headings are parts, 195 | # not chapters. 196 | #latex_use_parts = False 197 | 198 | # If true, show page references after internal links. 199 | #latex_show_pagerefs = False 200 | 201 | # If true, show URL addresses after external links. 202 | #latex_show_urls = False 203 | 204 | # Documents to append as an appendix to all manuals. 205 | #latex_appendices = [] 206 | 207 | # If false, no module index is generated. 208 | #latex_domain_indices = True 209 | 210 | 211 | # -- Options for manual page output -------------------------------------------- 212 | 213 | # One entry per manual page. List of tuples 214 | # (source start file, name, description, authors, manual section). 215 | man_pages = [ 216 | ('index', 'djangometricsdashboard', u'Django Metrics Dashboard Documentation', 217 | [u'Martin Brochhaus'], 1) 218 | ] 219 | 220 | # If true, show URL addresses after external links. 221 | #man_show_urls = False 222 | 223 | 224 | # -- Options for Texinfo output ------------------------------------------------ 225 | 226 | # Grouping the document tree into Texinfo files. List of tuples 227 | # (source start file, target name, title, author, 228 | # dir menu entry, description, category) 229 | texinfo_documents = [ 230 | ('index', 'DjangoMetricsDashboard', u'Django Metrics Dashboard Documentation', 231 | u'Martin Brochhaus', 'DjangoMetricsDashboard', 'One line description of project.', 232 | 'Miscellaneous'), 233 | ] 234 | 235 | # Documents to append as an appendix to all manuals. 236 | #texinfo_appendices = [] 237 | 238 | # If false, no module index is generated. 239 | #texinfo_domain_indices = True 240 | 241 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 242 | #texinfo_show_urls = 'footnote' 243 | 244 | 245 | # Example configuration for intersphinx: refer to the Python standard library. 246 | intersphinx_mapping = {'http://docs.python.org/': None} 247 | -------------------------------------------------------------------------------- /metrics_dashboard/static/metrics_dashboard/js/libs/modernizr-2.6.2.min.js: -------------------------------------------------------------------------------- 1 | /* Modernizr 2.6.2 (Custom Build) | MIT & BSD 2 | * Build: http://modernizr.com/download/#-fontface-backgroundsize-borderimage-borderradius-boxshadow-flexbox-hsla-multiplebgs-opacity-rgba-textshadow-cssanimations-csscolumns-generatedcontent-cssgradients-cssreflections-csstransforms-csstransforms3d-csstransitions-applicationcache-canvas-canvastext-draganddrop-hashchange-history-audio-video-indexeddb-input-inputtypes-localstorage-postmessage-sessionstorage-websockets-websqldatabase-webworkers-geolocation-inlinesvg-smil-svg-svgclippaths-touch-webgl-shiv-mq-cssclasses-addtest-prefixed-teststyles-testprop-testallprops-hasevent-prefixes-domprefixes-load 3 | */ 4 | ;window.Modernizr=function(a,b,c){function D(a){j.cssText=a}function E(a,b){return D(n.join(a+";")+(b||""))}function F(a,b){return typeof a===b}function G(a,b){return!!~(""+a).indexOf(b)}function H(a,b){for(var d in a){var e=a[d];if(!G(e,"-")&&j[e]!==c)return b=="pfx"?e:!0}return!1}function I(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:F(f,"function")?f.bind(d||b):f}return!1}function J(a,b,c){var d=a.charAt(0).toUpperCase()+a.slice(1),e=(a+" "+p.join(d+" ")+d).split(" ");return F(b,"string")||F(b,"undefined")?H(e,b):(e=(a+" "+q.join(d+" ")+d).split(" "),I(e,b,c))}function K(){e.input=function(c){for(var d=0,e=c.length;d',a,""].join(""),l.id=h,(m?l:n).innerHTML+=f,n.appendChild(l),m||(n.style.background="",n.style.overflow="hidden",k=g.style.overflow,g.style.overflow="hidden",g.appendChild(n)),i=c(l,a),m?l.parentNode.removeChild(l):(n.parentNode.removeChild(n),g.style.overflow=k),!!i},z=function(b){var c=a.matchMedia||a.msMatchMedia;if(c)return c(b).matches;var d;return y("@media "+b+" { #"+h+" { position: absolute; } }",function(b){d=(a.getComputedStyle?getComputedStyle(b,null):b.currentStyle)["position"]=="absolute"}),d},A=function(){function d(d,e){e=e||b.createElement(a[d]||"div"),d="on"+d;var f=d in e;return f||(e.setAttribute||(e=b.createElement("div")),e.setAttribute&&e.removeAttribute&&(e.setAttribute(d,""),f=F(e[d],"function"),F(e[d],"undefined")||(e[d]=c),e.removeAttribute(d))),e=null,f}var a={select:"input",change:"input",submit:"form",reset:"form",error:"img",load:"img",abort:"img"};return d}(),B={}.hasOwnProperty,C;!F(B,"undefined")&&!F(B.call,"undefined")?C=function(a,b){return B.call(a,b)}:C=function(a,b){return b in a&&F(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=w.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(w.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(w.call(arguments)))};return e}),s.flexbox=function(){return J("flexWrap")},s.canvas=function(){var a=b.createElement("canvas");return!!a.getContext&&!!a.getContext("2d")},s.canvastext=function(){return!!e.canvas&&!!F(b.createElement("canvas").getContext("2d").fillText,"function")},s.webgl=function(){return!!a.WebGLRenderingContext},s.touch=function(){var c;return"ontouchstart"in a||a.DocumentTouch&&b instanceof DocumentTouch?c=!0:y(["@media (",n.join("touch-enabled),("),h,")","{#modernizr{top:9px;position:absolute}}"].join(""),function(a){c=a.offsetTop===9}),c},s.geolocation=function(){return"geolocation"in navigator},s.postmessage=function(){return!!a.postMessage},s.websqldatabase=function(){return!!a.openDatabase},s.indexedDB=function(){return!!J("indexedDB",a)},s.hashchange=function(){return A("hashchange",a)&&(b.documentMode===c||b.documentMode>7)},s.history=function(){return!!a.history&&!!history.pushState},s.draganddrop=function(){var a=b.createElement("div");return"draggable"in a||"ondragstart"in a&&"ondrop"in a},s.websockets=function(){return"WebSocket"in a||"MozWebSocket"in a},s.rgba=function(){return D("background-color:rgba(150,255,150,.5)"),G(j.backgroundColor,"rgba")},s.hsla=function(){return D("background-color:hsla(120,40%,100%,.5)"),G(j.backgroundColor,"rgba")||G(j.backgroundColor,"hsla")},s.multiplebgs=function(){return D("background:url(https://),url(https://),red url(https://)"),/(url\s*\(.*?){3}/.test(j.background)},s.backgroundsize=function(){return J("backgroundSize")},s.borderimage=function(){return J("borderImage")},s.borderradius=function(){return J("borderRadius")},s.boxshadow=function(){return J("boxShadow")},s.textshadow=function(){return b.createElement("div").style.textShadow===""},s.opacity=function(){return E("opacity:.55"),/^0.55$/.test(j.opacity)},s.cssanimations=function(){return J("animationName")},s.csscolumns=function(){return J("columnCount")},s.cssgradients=function(){var a="background-image:",b="gradient(linear,left top,right bottom,from(#9f9),to(white));",c="linear-gradient(left top,#9f9, white);";return D((a+"-webkit- ".split(" ").join(b+a)+n.join(c+a)).slice(0,-a.length)),G(j.backgroundImage,"gradient")},s.cssreflections=function(){return J("boxReflect")},s.csstransforms=function(){return!!J("transform")},s.csstransforms3d=function(){var a=!!J("perspective");return a&&"webkitPerspective"in g.style&&y("@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}",function(b,c){a=b.offsetLeft===9&&b.offsetHeight===3}),a},s.csstransitions=function(){return J("transition")},s.fontface=function(){var a;return y('@font-face {font-family:"font";src:url("https://")}',function(c,d){var e=b.getElementById("smodernizr"),f=e.sheet||e.styleSheet,g=f?f.cssRules&&f.cssRules[0]?f.cssRules[0].cssText:f.cssText||"":"";a=/src/i.test(g)&&g.indexOf(d.split(" ")[0])===0}),a},s.generatedcontent=function(){var a;return y(["#",h,"{font:0/0 a}#",h,':after{content:"',l,'";visibility:hidden;font:3px/1 a}'].join(""),function(b){a=b.offsetHeight>=3}),a},s.video=function(){var a=b.createElement("video"),c=!1;try{if(c=!!a.canPlayType)c=new Boolean(c),c.ogg=a.canPlayType('video/ogg; codecs="theora"').replace(/^no$/,""),c.h264=a.canPlayType('video/mp4; codecs="avc1.42E01E"').replace(/^no$/,""),c.webm=a.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/^no$/,"")}catch(d){}return c},s.audio=function(){var a=b.createElement("audio"),c=!1;try{if(c=!!a.canPlayType)c=new Boolean(c),c.ogg=a.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),c.mp3=a.canPlayType("audio/mpeg;").replace(/^no$/,""),c.wav=a.canPlayType('audio/wav; codecs="1"').replace(/^no$/,""),c.m4a=(a.canPlayType("audio/x-m4a;")||a.canPlayType("audio/aac;")).replace(/^no$/,"")}catch(d){}return c},s.localstorage=function(){try{return localStorage.setItem(h,h),localStorage.removeItem(h),!0}catch(a){return!1}},s.sessionstorage=function(){try{return sessionStorage.setItem(h,h),sessionStorage.removeItem(h),!0}catch(a){return!1}},s.webworkers=function(){return!!a.Worker},s.applicationcache=function(){return!!a.applicationCache},s.svg=function(){return!!b.createElementNS&&!!b.createElementNS(r.svg,"svg").createSVGRect},s.inlinesvg=function(){var a=b.createElement("div");return a.innerHTML="",(a.firstChild&&a.firstChild.namespaceURI)==r.svg},s.smil=function(){return!!b.createElementNS&&/SVGAnimate/.test(m.call(b.createElementNS(r.svg,"animate")))},s.svgclippaths=function(){return!!b.createElementNS&&/SVGClipPath/.test(m.call(b.createElementNS(r.svg,"clipPath")))};for(var L in s)C(s,L)&&(x=L.toLowerCase(),e[x]=s[L](),v.push((e[x]?"":"no-")+x));return e.input||K(),e.addTest=function(a,b){if(typeof a=="object")for(var d in a)C(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof f!="undefined"&&f&&(g.className+=" "+(b?"":"no-")+a),e[a]=b}return e},D(""),i=k=null,function(a,b){function k(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function l(){var a=r.elements;return typeof a=="string"?a.split(" "):a}function m(a){var b=i[a[g]];return b||(b={},h++,a[g]=h,i[h]=b),b}function n(a,c,f){c||(c=b);if(j)return c.createElement(a);f||(f=m(c));var g;return f.cache[a]?g=f.cache[a].cloneNode():e.test(a)?g=(f.cache[a]=f.createElem(a)).cloneNode():g=f.createElem(a),g.canHaveChildren&&!d.test(a)?f.frag.appendChild(g):g}function o(a,c){a||(a=b);if(j)return a.createDocumentFragment();c=c||m(a);var d=c.frag.cloneNode(),e=0,f=l(),g=f.length;for(;e",f="hidden"in a,j=a.childNodes.length==1||function(){b.createElement("a");var a=b.createDocumentFragment();return typeof a.cloneNode=="undefined"||typeof a.createDocumentFragment=="undefined"||typeof a.createElement=="undefined"}()}catch(c){f=!0,j=!0}})();var r={elements:c.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video",shivCSS:c.shivCSS!==!1,supportsUnknownElements:j,shivMethods:c.shivMethods!==!1,type:"default",shivDocument:q,createElement:n,createDocumentFragment:o};a.html5=r,q(b)}(this,b),e._version=d,e._prefixes=n,e._domPrefixes=q,e._cssomPrefixes=p,e.mq=z,e.hasEvent=A,e.testProp=function(a){return H([a])},e.testAllProps=J,e.testStyles=y,e.prefixed=function(a,b,c){return b?J(a,b,c):J(a,"pfx")},g.className=g.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(f?" js "+v.join(" "):""),e}(this,this.document),function(a,b,c){function d(a){return"[object Function]"==o.call(a)}function e(a){return"string"==typeof a}function f(){}function g(a){return!a||"loaded"==a||"complete"==a||"uninitialized"==a}function h(){var a=p.shift();q=1,a?a.t?m(function(){("c"==a.t?B.injectCss:B.injectJs)(a.s,0,a.a,a.x,a.e,1)},0):(a(),h()):q=0}function i(a,c,d,e,f,i,j){function k(b){if(!o&&g(l.readyState)&&(u.r=o=1,!q&&h(),l.onload=l.onreadystatechange=null,b)){"img"!=a&&m(function(){t.removeChild(l)},50);for(var d in y[c])y[c].hasOwnProperty(d)&&y[c][d].onload()}}var j=j||B.errorTimeout,l=b.createElement(a),o=0,r=0,u={t:d,s:c,e:f,a:i,x:j};1===y[c]&&(r=1,y[c]=[]),"object"==a?l.data=c:(l.src=c,l.type=a),l.width=l.height="0",l.onerror=l.onload=l.onreadystatechange=function(){k.call(this,r)},p.splice(e,0,u),"img"!=a&&(r||2===y[c]?(t.insertBefore(l,s?null:n),m(k,j)):y[c].push(l))}function j(a,b,c,d,f){return q=0,b=b||"j",e(a)?i("c"==b?v:u,a,b,this.i++,c,d,f):(p.splice(this.i++,0,a),1==p.length&&h()),this}function k(){var a=B;return a.loader={load:j,i:0},a}var l=b.documentElement,m=a.setTimeout,n=b.getElementsByTagName("script")[0],o={}.toString,p=[],q=0,r="MozAppearance"in l.style,s=r&&!!b.createRange().compareNode,t=s?l:n.parentNode,l=a.opera&&"[object Opera]"==o.call(a.opera),l=!!b.attachEvent&&!l,u=r?"object":l?"script":"img",v=l?"script":u,w=Array.isArray||function(a){return"[object Array]"==o.call(a)},x=[],y={},z={timeout:function(a,b){return b.length&&(a.timeout=b[0]),a}},A,B;B=function(a){function b(a){var a=a.split("!"),b=x.length,c=a.pop(),d=a.length,c={url:c,origUrl:c,prefixes:a},e,f,g;for(f=0;f=a.x1&&b.x1<=a.x2||b.x2>=a.x1&&b.x2<=a.x2||a.x1>=b.x1&&a.x2<=b.x2)c=!0;if(b.y1>=a.y1&&b.y1<=a.y2||b.y2>=a.y1&&b.y2<=a.y2||a.y1>=b.y1&&a.y2<=b.y2)d=!0;return c&&d},g.detect_overlapping_region=function(a,b){var c="",d="";return a.y1>b.cy&&a.y1b.y1&&a.y2b.cx&&a.x1b.x1&&a.x2this.player_max_left?e=this.player_max_left:e=g&&(b=c+30,b0&&(f.scrollTop(b),this.scrollOffset=this.scrollOffset-30))},j.calculate_positions=function(a){this.window_height=f.height()},j.drag_handler=function(b){var c=b.target.nodeName;if(this.disabled||b.which!==1&&!g)return;if(this.ignore_drag(b))return;var d=this,e=!0;return this.$player=a(b.currentTarget),this.el_init_pos=this.get_actual_pos(this.$player),this.mouse_init_pos=this.get_mouse_pos(b),this.offsetY=this.mouse_init_pos.top-this.el_init_pos.top,this.$body.on(h.move,function(a){var b=d.get_mouse_pos(a),c=Math.abs(b.left-d.mouse_init_pos.left),f=Math.abs(b.top-d.mouse_init_pos.top);return c>d.options.distance||f>d.options.distance?e?(e=!1,d.on_dragstart.call(d,a),!1):(d.is_dragging===!0&&d.on_dragmove.call(d,a),!1):!1}),!1},j.on_dragstart=function(b){b.preventDefault(),this.drag_start=!0,this.is_dragging=!0;var d=this.$container.offset();return this.baseX=Math.round(d.left),this.baseY=Math.round(d.top),this.doc_height=a(c).height(),this.options.helper==="clone"?(this.$helper=this.$player.clone().appendTo(this.$container).addClass("helper"),this.helper=!0):this.helper=!1,this.scrollOffset=0,this.el_init_offset=this.$player.offset(),this.player_width=this.$player.width(),this.player_height=this.$player.height(),this.player_max_left=this.$container.width()-this.player_width+this.options.offset_left,this.options.start&&this.options.start.call(this.$player,b,{helper:this.helper?this.$helper:this.$player}),!1},j.on_dragmove=function(a){var b=this.get_offset(a);this.options.autoscroll&&this.manage_scroll(b),(this.helper?this.$helper:this.$player).css({position:"absolute",left:b.left,top:b.top});var c={position:{left:b.left,top:b.top}};return this.options.drag&&this.options.drag.call(this.$player,a,c),!1},j.on_dragstop=function(a){var b=this.get_offset(a);this.drag_start=!1;var c={position:{left:b.left,top:b.top}};return this.options.stop&&this.options.stop.call(this.$player,a,c),this.helper&&this.$helper.remove(),!1},j.on_select_start=function(a){if(this.disabled)return;if(this.ignore_drag(a))return;return!1},j.enable=function(){this.disabled=!1},j.disable=function(){this.disabled=!0},j.destroy=function(){this.disable(),a.removeData(this.$container,"drag")},j.ignore_drag=function(b){return this.options.handle?!a(b.target).is(this.options.handle):a.inArray(b.target.nodeName,this.options.ignore_dragging)>=0},a.fn.drag=function(b){return this.each(function(){a.data(this,"drag")||a.data(this,"drag",new i(this,b))})}}(jQuery,window,document),function(a,b,c,d){function f(b,c){this.options=a.extend(!0,e,c),this.$el=a(b),this.$wrapper=this.$el.parent(),this.$widgets=this.$el.children(this.options.widget_selector).addClass("gs_w"),this.widgets=[],this.$changed=a([]),this.wrapper_width=this.$wrapper.width(),this.min_widget_width=this.options.widget_margins[0]*2+this.options.widget_base_dimensions[0],this.min_widget_height=this.options.widget_margins[1]*2+this.options.widget_base_dimensions[1],this.init()}var e={namespace:"",widget_selector:"li",widget_margins:[10,10],widget_base_dimensions:[400,225],extra_rows:0,extra_cols:0,min_cols:1,min_rows:15,max_size_x:6,autogenerate_stylesheet:!0,avoid_overlapped_widgets:!0,serialize_params:function(a,b){return{col:b.col,row:b.row,size_x:b.size_x,size_y:b.size_y}},collision:{},draggable:{distance:4}};f.generated_stylesheets=[];var g=f.prototype;g.init=function(){this.generate_grid_and_stylesheet(),this.get_widgets_from_DOM(),this.set_dom_grid_height(),this.$wrapper.addClass("ready"),this.draggable(),a(b).bind("resize",throttle(a.proxy(this.recalculate_faux_grid,this),200))},g.disable=function(){return this.$wrapper.find(".player-revert").removeClass("player-revert"),this.drag_api.disable(),this},g.enable=function(){return this.drag_api.enable(),this},g.add_widget=function(b,c,d,e,f){var g;c||(c=1),d||(d=1),!e&!f?g=this.next_position(c,d):(g={col:e,row:f},this.empty_cells(e,f,c,d));var h=a(b).attr({"data-col":g.col,"data-row":g.row,"data-sizex":c,"data-sizey":d}).addClass("gs_w").appendTo(this.$el).hide();return this.$widgets=this.$widgets.add(h),this.register_widget(h),this.add_faux_rows(g.size_y),this.set_dom_grid_height(),h.fadeIn()},g.resize_widget=function(b,c,d){var e=b.coords().grid;c||(c=e.size_x),d||(d=e.size_y),c>this.cols&&(c=this.cols);var f=this.get_cells_occupied(e),g=e.size_x,h=e.size_y,i=e.col,j=i,k=c>g,l=d>h;if(i+c-1>this.cols){var m=i+(c-1)-this.cols,n=i-m;j=Math.max(1,n)}var o={col:j,row:e.row,size_x:c,size_y:d},p=this.get_cells_occupied(o),q=[];a.each(f.cols,function(b,c){a.inArray(c,p.cols)===-1&&q.push(c)});var r=[];a.each(p.cols,function(b,c){a.inArray(c,f.cols)===-1&&r.push(c)});var s=[];a.each(f.rows,function(b,c){a.inArray(c,p.rows)===-1&&s.push(c)});var t=[];a.each(p.rows,function(b,c){a.inArray(c,f.rows)===-1&&t.push(c)}),this.remove_from_gridmap(e);if(r.length){var u=[j,e.row,c,Math.min(h,d),b];this.empty_cells.apply(this,u)}if(t.length){var v=[j,e.row,c,d,b];this.empty_cells.apply(this,v)}e.col=j,e.size_x=c,e.size_y=d,this.add_to_gridmap(o,b),b.data("coords").update({width:c*this.options.widget_base_dimensions[0]+(c-1)*this.options.widget_margins[0]*2,height:d*this.options.widget_base_dimensions[1]+(d-1)*this.options.widget_margins[1]*2}),d>h&&this.add_faux_rows(d-h),c>g&&this.add_faux_cols(c-g),b.attr({"data-col":j,"data-sizex":c,"data-sizey":d});if(q.length){var w=[q[0],e.row,q.length,Math.min(h,d),b];this.remove_empty_cells.apply(this,w)}if(s.length){var x=[j,e.row,c,d,b];this.remove_empty_cells.apply(this,x)}return b},g.empty_cells=function(b,c,d,e,f){var g=this.widgets_below({col:b,row:c-e,size_x:d,size_y:e});return g.not(f).each(a.proxy(function(b,d){var f=a(d).coords().grid;if(!(f.row<=c+e-1))return;var g=c+e-f.row;this.move_widget_down(a(d),g)},this)),this.set_dom_grid_height(),this},g.remove_empty_cells=function(b,c,d,e,f){var g=this.widgets_below({col:b,row:c,size_x:d,size_y:e});return g.not(f).each(a.proxy(function(b,c){this.move_widget_up(a(c),e)},this)),this.set_dom_grid_height(),this},g.next_position=function(a,b){a||(a=1),b||(b=1);var c=this.gridmap,d=c.length,e=[],f;for(var g=1;g",{"class":"preview-holder","data-row":this.$player.attr("data-row"),"data-col":this.$player.attr("data-col"),css:{width:e.width,height:e.height}}).appendTo(this.$el),this.options.draggable.start&&this.options.draggable.start.call(this,b,c)},g.on_drag=function(a,b){if(this.$player===null)return!1;var c={left:b.position.left+this.baseX,top:b.position.top+this.baseY};this.colliders_data=this.collision_api.get_closest_colliders(c),this.on_overlapped_column_change(this.on_start_overlapping_column,this.on_stop_overlapping_column),this.on_overlapped_row_change(this.on_start_overlapping_row,this.on_stop_overlapping_row),this.helper&&this.$player&&this.$player.css({left:b.position.left,top:b.position.top}),this.options.draggable.drag&&this.options.draggable.drag.call(this,a,b)},g.on_stop_drag=function(a,b){this.$helper.add(this.$player).add(this.$wrapper).removeClass("dragging"),b.position.left=b.position.left+this.baseX,b.position.top=b.position.top+this.baseY,this.colliders_data=this.collision_api.get_closest_colliders(b.position),this.on_overlapped_column_change(this.on_start_overlapping_column,this.on_stop_overlapping_column),this.on_overlapped_row_change(this.on_start_overlapping_row,this.on_stop_overlapping_row),this.$player.addClass("player-revert").removeClass("player").attr({"data-col":this.placeholder_grid_data.col,"data-row":this.placeholder_grid_data.row}).css({left:"",top:""}),this.$changed=this.$changed.add(this.$player),this.cells_occupied_by_player=this.get_cells_occupied(this.placeholder_grid_data),this.set_cells_player_occupies(this.placeholder_grid_data.col,this.placeholder_grid_data.row),this.$player.coords().grid.row=this.placeholder_grid_data.row,this.$player.coords().grid.col=this.placeholder_grid_data.col,this.options.draggable.stop&&this.options.draggable.stop.call(this,a,b),this.$preview_holder.remove(),this.$player=null,this.$helper=null,this.placeholder_grid_data={},this.player_grid_data={},this.cells_occupied_by_placeholder={},this.cells_occupied_by_player={},this.set_dom_grid_height()},g.on_overlapped_column_change=function(b,c){if(!this.colliders_data.length)return;var d=this.get_targeted_columns(this.colliders_data[0].el.data.col),e=this.last_cols.length,f=d.length,g;for(g=0;gc.row?1:-1}),b},g.sort_by_row_and_col_asc=function(a){return a=a.sort(function(a,b){return a.row>b.row||a.row===b.row&&a.col>b.col?1:-1}),a},g.sort_by_col_asc=function(a){return a=a.sort(function(a,b){return a.col>b.col?1:-1}),a},g.sort_by_row_desc=function(a){return a=a.sort(function(a,b){return a.row+a.size_y=0&&a.inArray(c,d.rows)>=0},g.is_placeholder_in=function(b,c){var d=this.cells_occupied_by_placeholder||{};return this.is_placeholder_in_col(b)&&a.inArray(c,d.rows)>=0},g.is_placeholder_in_col=function(b){var c=this.cells_occupied_by_placeholder||[];return a.inArray(b,c.cols)>=0},g.is_empty=function(a,b){return typeof this.gridmap[a]!="undefined"&&typeof this.gridmap[a][b]!="undefined"&&this.gridmap[a][b]===!1?!0:!1},g.is_occupied=function(a,b){return this.gridmap[a]?this.gridmap[a][b]?!0:!1:!1},g.is_widget=function(a,b){var c=this.gridmap[a];return c?(c=c[b],c?c:!1):!1},g.is_widget_under_player=function(a,b){return this.is_widget(a,b)?this.is_player_in(a,b):!1},g.get_widgets_under_player=function(b){b||(b=this.cells_occupied_by_player||{cols:[],rows:[]});var c=a([]);return a.each(b.cols,a.proxy(function(d,e){a.each(b.rows,a.proxy(function(a,b){this.is_widget(e,b)&&(c=c.add(this.gridmap[e][b]))},this))},this)),c},g.set_placeholder=function(b,c){var d=a.extend({},this.placeholder_grid_data),e=this.widgets_below({col:d.col,row:d.row,size_y:d.size_y,size_x:d.size_x}),f=b+d.size_x-1;f>this.cols&&(b=b-(f-b));var g=this.placeholder_grid_data.row0)if(this.is_empty(a,h)||this.is_player(a,h)||this.is_widget(a,h)&&g[h].is(f))d[a].push(h),e=h0){if(this.is_widget(f,h)&&!this.is_player_in(f,h)&&!g[h].is(a.el))break;!this.is_player(f,h)&&!this.is_placeholder_in(f,h)&&!this.is_player_in(f,h)&&d[f].push(h),h=b?a[d[0]]:!1},g.get_widgets_overlapped=function(){var b,c=a([]),d=[],e=this.cells_occupied_by_player.rows.slice(0);return e.reverse(),a.each(this.cells_occupied_by_player.cols,a.proxy(function(b,f){a.each(e,a.proxy(function(b,e){if(!this.gridmap[f])return!0;var g=this.gridmap[f][e];this.is_occupied(f,e)&&!this.is_player(g)&&a.inArray(g,d)===-1&&(c=c.add(g),d.push(g))},this))},this)),c},g.on_start_overlapping_column=function(a){this.set_player(a,!1)},g.on_start_overlapping_row=function(a){this.set_player(!1,a)},g.on_stop_overlapping_column=function(a){this.set_player(a,!1);var b=this;this.for_each_widget_below(a,this.cells_occupied_by_player.rows[0],function(a,c){b.move_widget_up(this,b.player_grid_data.size_y)})},g.on_stop_overlapping_row=function(a){this.set_player(!1,a);var b=this,c=this.cells_occupied_by_player.cols;for(var d=0,e=c.length;d0&&this.move_widget_down(d,f)},this)),h.row=i,this.update_widget_position(h,b),b.attr("data-row",h.row),this.$changed=this.$changed.add(b),f.push(b)}},g.can_go_up_to_row=function(b,c,d){var e=this.gridmap,f=!0,g=[],h=b.row,i;this.for_each_column_occupied(b,function(a){var b=e[a];g[a]=[],i=h;while(i--)if(this.is_empty(a,i)&&!this.is_placeholder_in(a,i))g[a].push(i);else break;if(!g[a].length)return f=!1,!0});if(!f)return!1;i=d;for(i=1;i0?c:0},g.widgets_below=function(b){var c=a.isPlainObject(b)?b:b.coords().grid,d=this,e=this.gridmap,f=c.row+c.size_y-1,g=a([]);return this.for_each_column_occupied(c,function(b){d.for_each_widget_below(b,f,function(b,c){if(!d.is_player(this)&&a.inArray(this,g)===-1)return g=g.add(this),!0})}),this.sort_by_row_asc(g)},g.set_cells_player_occupies=function(a,b){return this.remove_from_gridmap(this.placeholder_grid_data),this.placeholder_grid_data.col=a,this.placeholder_grid_data.row=b,this.add_to_gridmap(this.placeholder_grid_data,this.$player),this},g.empty_cells_player_occupies=function(){return this.remove_from_gridmap(this.placeholder_grid_data),this},g.can_go_up=function(a){var b=a.coords().grid,c=b.row,d=c-1,e=this.gridmap,f=[],g=!0;return c===1?!1:(this.for_each_column_occupied(b,function(a){var b=this.is_widget(a,d);if(this.is_occupied(a,d)||this.is_player(a,d)||this.is_placeholder_in(a,d)||this.is_player_in(a,d))return g=!1,!0}),g)},g.can_move_to=function(a,b,c,d){var e=this.gridmap,f=a.el,g={size_y:a.size_y,size_x:a.size_x,col:b,row:c},h=!0,i=b+a.size_x-1;return i>this.cols?!1:d&&d0&&this.is_widget(d,m)&&a.inArray(g[d][m],l)===-1){h=f.call(g[d][m],d,m),l.push(g[d][m]);if(h)break}},"for_each/below":function(){for(m=e+1,i=g[d].length;m=1;e--)for(a=b[e].length-1;a>=1;a--)if(this.is_widget(e,a)){c.push(a),d[a]=e;break}var f=Math.max.apply(Math,c);return this.highest_occupied_cell={col:d[f],row:f},this.highest_occupied_cell},g.get_widgets_from=function(b,c){var d=this.gridmap,e=a();return b&&(e=e.add(this.$widgets.filter(function(){var c=a(this).attr("data-col");return c===b||c>b}))),c&&(e=e.add(this.$widgets.filter(function(){var b=a(this).attr("data-row");return b===c||b>c}))),e},g.set_dom_grid_height=function(){var a=this.get_highest_occupied_cell().row;return this.$el.css("height",a*this.min_widget_height),this},g.generate_stylesheet=function(b){var c="",d=this.options.max_size_x,e=0,g=0,h,i;b||(b={}),b.cols||(b.cols=this.cols),b.rows||(b.rows=this.rows),b.namespace||(b.namespace=this.options.namespace),b.widget_base_dimensions||(b.widget_base_dimensions=this.options.widget_base_dimensions),b.widget_margins||(b.widget_margins=this.options.widget_margins),b.min_widget_width=b.widget_margins[0]*2+b.widget_base_dimensions[0],b.min_widget_height=b.widget_margins[1]*2+b.widget_base_dimensions[1];var j=a.param(b);if(a.inArray(j,f.generated_stylesheets)>=0)return!1;f.generated_stylesheets.push(j);for(h=b.cols;h>=0;h--)c+=b.namespace+' [data-col="'+(h+1)+'"] { left:'+(h*b.widget_base_dimensions[0]+h*b.widget_margins[0]+(h+1)*b.widget_margins[0])+"px;} ";for(h=b.rows;h>=0;h--)c+=b.namespace+' [data-row="'+(h+1)+'"] { top:'+(h*b.widget_base_dimensions[1]+h*b.widget_margins[1]+(h+1)*b.widget_margins[1])+"px;} ";for(var k=1;k<=b.rows;k++)c+=b.namespace+' [data-sizey="'+k+'"] { height:'+(k*b.widget_base_dimensions[1]+(k-1)*b.widget_margins[1]*2)+"px;}";for(var l=1;l<=d;l++)c+=b.namespace+' [data-sizex="'+l+'"] { width:'+(l*b.widget_base_dimensions[0]+(l-1)*b.widget_margins[0]*2)+"px;}";return this.add_style_tag(c)},g.add_style_tag=function(a){var b=c,d=b.createElement("style");return b.getElementsByTagName("head")[0].appendChild(d),d.setAttribute("type","text/css"),d.styleSheet?d.styleSheet.cssText=a:d.appendChild(c.createTextNode(a)),this},g.generate_faux_grid=function(a,b){this.faux_grid=[],this.gridmap=[];var c,d;for(c=b;c>0;c--){this.gridmap[c]=[];for(d=a;d>0;d--)this.add_faux_cell(d,c)}return this},g.add_faux_cell=function(b,c){var d=a({left:this.baseX+(c-1)*this.min_widget_width,top:this.baseY+(b-1)*this.min_widget_height,width:this.min_widget_width,height:this.min_widget_height,col:c,row:b,original_col:c,original_row:b}).coords();return a.isArray(this.gridmap[c])||(this.gridmap[c]=[]),this.gridmap[c][b]=!1,this.faux_grid.push(d),this},g.add_faux_rows=function(a){var b=this.rows,c=b+(a||1);for(var d=c;d>b;d--)for(var e=this.cols;e>=1;e--)this.add_faux_cell(d,e);return this.rows=c,this.options.autogenerate_stylesheet&&this.generate_stylesheet(),this},g.add_faux_cols=function(a){var b=this.cols,c=b+(a||1);for(var d=b;d=1;e--)this.add_faux_cell(e,d);return this.cols=c,this.options.autogenerate_stylesheet&&this.generate_stylesheet(),this},g.recalculate_faux_grid=function(){var c=this.$wrapper.width();return this.baseX=(a(b).width()-c)/2,this.baseY=this.$wrapper.offset().top,a.each(this.faux_grid,a.proxy(function(a,b){this.faux_grid[a]=b.update({left:this.baseX+(b.data.col-1)*this.min_widget_width,top:this.baseY+(b.data.row-1)*this.min_widget_height})},this)),this},g.get_widgets_from_DOM=function(){return this.$widgets.each(a.proxy(function(b,c){this.register_widget(a(c))},this)),this},g.generate_grid_and_stylesheet=function(){var c=this.$wrapper.width(),d=this.$wrapper.height(),e=Math.floor(c/this.min_widget_width)+this.options.extra_cols,f=this.$widgets.map(function(){return a(this).attr("data-col")});f=Array.prototype.slice.call(f,0),f.length||(f=[0]);var g=Math.max.apply(Math,f),h=this.options.extra_rows;return this.$widgets.each(function(b,c){h+=+a(c).attr("data-sizey")}),this.cols=Math.max(g,e,this.options.min_cols),this.rows=Math.max(h,this.options.min_rows),this.baseX=(a(b).width()-c)/2,this.baseY=this.$wrapper.offset().top,this.options.autogenerate_stylesheet&&this.generate_stylesheet(),this.generate_faux_grid(this.rows,this.cols)},a.fn.gridster=function(b){return this.each(function(){a(this).data("gridster")||a(this).data("gridster",new f(this,b))})},a.Gridster=g}(jQuery,window,document); -------------------------------------------------------------------------------- /metrics_dashboard/static/metrics_dashboard/js/libs/jquery.gridster.with-extras.min.js: -------------------------------------------------------------------------------- 1 | /*! gridster.js - v0.1.0 - 2012-10-20 2 | * http://gridster.net/ 3 | * Copyright (c) 2012 ducksboard; Licensed MIT */ 4 | (function(a,b,c,d){function e(b){return b[0]&&a.isPlainObject(b[0])?this.data=b[0]:this.el=b,this.isCoords=!0,this.coords={},this.init(),this}var f=e.prototype;f.init=function(){this.set(),this.original_coords=this.get()},f.set=function(a,b){var c=this.el;c&&!a&&(this.data=c.offset(),this.data.width=c.width(),this.data.height=c.height());if(c&&a&&!b){var d=c.offset();this.data.top=d.top,this.data.left=d.left}var e=this.data;return this.coords.x1=e.left,this.coords.y1=e.top,this.coords.x2=e.left+e.width,this.coords.y2=e.top+e.height,this.coords.cx=e.left+e.width/2,this.coords.cy=e.top+e.height/2,this.coords.width=e.width,this.coords.height=e.height,this.coords.el=c||!1,this},f.update=function(b){if(!b&&!this.el)return this;if(b){var c=a.extend({},this.data,b);return this.data=c,this.set(!0,!0)}return this.set(!0),this},f.get=function(){return this.coords},a.fn.coords=function(){if(this.data("coords"))return this.data("coords");var a=new e(this,arguments[0]);return this.data("coords",a),a}})(jQuery,window,document),function(a,b,c,d){function f(b,c,d){this.options=a.extend(e,d),this.$element=b,this.last_colliders=[],this.last_colliders_coords=[],typeof c=="string"||c instanceof jQuery?this.$colliders=a(c,this.options.colliders_context).not(this.$element):this.colliders=a(c),this.init()}var e={colliders_context:c.body},g=f.prototype;g.init=function(){this.find_collisions()},g.overlaps=function(a,b){var c=!1,d=!1;if(b.x1>=a.x1&&b.x1<=a.x2||b.x2>=a.x1&&b.x2<=a.x2||a.x1>=b.x1&&a.x2<=b.x2)c=!0;if(b.y1>=a.y1&&b.y1<=a.y2||b.y2>=a.y1&&b.y2<=a.y2||a.y1>=b.y1&&a.y2<=b.y2)d=!0;return c&&d},g.detect_overlapping_region=function(a,b){var c="",d="";return a.y1>b.cy&&a.y1b.y1&&a.y2b.cx&&a.x1b.x1&&a.x2this.player_max_left?e=this.player_max_left:e=g&&(b=c+30,b0&&(f.scrollTop(b),this.scrollOffset=this.scrollOffset-30))},j.calculate_positions=function(a){this.window_height=f.height()},j.drag_handler=function(b){var c=b.target.nodeName;if(this.disabled||b.which!==1&&!g)return;if(this.ignore_drag(b))return;var d=this,e=!0;return this.$player=a(b.currentTarget),this.el_init_pos=this.get_actual_pos(this.$player),this.mouse_init_pos=this.get_mouse_pos(b),this.offsetY=this.mouse_init_pos.top-this.el_init_pos.top,this.$body.on(h.move,function(a){var b=d.get_mouse_pos(a),c=Math.abs(b.left-d.mouse_init_pos.left),f=Math.abs(b.top-d.mouse_init_pos.top);return c>d.options.distance||f>d.options.distance?e?(e=!1,d.on_dragstart.call(d,a),!1):(d.is_dragging===!0&&d.on_dragmove.call(d,a),!1):!1}),!1},j.on_dragstart=function(b){b.preventDefault(),this.drag_start=!0,this.is_dragging=!0;var d=this.$container.offset();return this.baseX=Math.round(d.left),this.baseY=Math.round(d.top),this.doc_height=a(c).height(),this.options.helper==="clone"?(this.$helper=this.$player.clone().appendTo(this.$container).addClass("helper"),this.helper=!0):this.helper=!1,this.scrollOffset=0,this.el_init_offset=this.$player.offset(),this.player_width=this.$player.width(),this.player_height=this.$player.height(),this.player_max_left=this.$container.width()-this.player_width+this.options.offset_left,this.options.start&&this.options.start.call(this.$player,b,{helper:this.helper?this.$helper:this.$player}),!1},j.on_dragmove=function(a){var b=this.get_offset(a);this.options.autoscroll&&this.manage_scroll(b),(this.helper?this.$helper:this.$player).css({position:"absolute",left:b.left,top:b.top});var c={position:{left:b.left,top:b.top}};return this.options.drag&&this.options.drag.call(this.$player,a,c),!1},j.on_dragstop=function(a){var b=this.get_offset(a);this.drag_start=!1;var c={position:{left:b.left,top:b.top}};return this.options.stop&&this.options.stop.call(this.$player,a,c),this.helper&&this.$helper.remove(),!1},j.on_select_start=function(a){if(this.disabled)return;if(this.ignore_drag(a))return;return!1},j.enable=function(){this.disabled=!1},j.disable=function(){this.disabled=!0},j.destroy=function(){this.disable(),a.removeData(this.$container,"drag")},j.ignore_drag=function(b){return this.options.handle?!a(b.target).is(this.options.handle):a.inArray(b.target.nodeName,this.options.ignore_dragging)>=0},a.fn.drag=function(b){return this.each(function(){a.data(this,"drag")||a.data(this,"drag",new i(this,b))})}}(jQuery,window,document),function(a,b,c,d){function f(b,c){this.options=a.extend(!0,e,c),this.$el=a(b),this.$wrapper=this.$el.parent(),this.$widgets=this.$el.children(this.options.widget_selector).addClass("gs_w"),this.widgets=[],this.$changed=a([]),this.wrapper_width=this.$wrapper.width(),this.min_widget_width=this.options.widget_margins[0]*2+this.options.widget_base_dimensions[0],this.min_widget_height=this.options.widget_margins[1]*2+this.options.widget_base_dimensions[1],this.init()}var e={namespace:"",widget_selector:"li",widget_margins:[10,10],widget_base_dimensions:[400,225],extra_rows:0,extra_cols:0,min_cols:1,min_rows:15,max_size_x:6,autogenerate_stylesheet:!0,avoid_overlapped_widgets:!0,serialize_params:function(a,b){return{col:b.col,row:b.row,size_x:b.size_x,size_y:b.size_y}},collision:{},draggable:{distance:4}};f.generated_stylesheets=[];var g=f.prototype;g.init=function(){this.generate_grid_and_stylesheet(),this.get_widgets_from_DOM(),this.set_dom_grid_height(),this.$wrapper.addClass("ready"),this.draggable(),a(b).bind("resize",throttle(a.proxy(this.recalculate_faux_grid,this),200))},g.disable=function(){return this.$wrapper.find(".player-revert").removeClass("player-revert"),this.drag_api.disable(),this},g.enable=function(){return this.drag_api.enable(),this},g.add_widget=function(b,c,d,e,f){var g;c||(c=1),d||(d=1),!e&!f?g=this.next_position(c,d):(g={col:e,row:f},this.empty_cells(e,f,c,d));var h=a(b).attr({"data-col":g.col,"data-row":g.row,"data-sizex":c,"data-sizey":d}).addClass("gs_w").appendTo(this.$el).hide();return this.$widgets=this.$widgets.add(h),this.register_widget(h),this.add_faux_rows(g.size_y),this.set_dom_grid_height(),h.fadeIn()},g.resize_widget=function(b,c,d){var e=b.coords().grid;c||(c=e.size_x),d||(d=e.size_y),c>this.cols&&(c=this.cols);var f=this.get_cells_occupied(e),g=e.size_x,h=e.size_y,i=e.col,j=i,k=c>g,l=d>h;if(i+c-1>this.cols){var m=i+(c-1)-this.cols,n=i-m;j=Math.max(1,n)}var o={col:j,row:e.row,size_x:c,size_y:d},p=this.get_cells_occupied(o),q=[];a.each(f.cols,function(b,c){a.inArray(c,p.cols)===-1&&q.push(c)});var r=[];a.each(p.cols,function(b,c){a.inArray(c,f.cols)===-1&&r.push(c)});var s=[];a.each(f.rows,function(b,c){a.inArray(c,p.rows)===-1&&s.push(c)});var t=[];a.each(p.rows,function(b,c){a.inArray(c,f.rows)===-1&&t.push(c)}),this.remove_from_gridmap(e);if(r.length){var u=[j,e.row,c,Math.min(h,d),b];this.empty_cells.apply(this,u)}if(t.length){var v=[j,e.row,c,d,b];this.empty_cells.apply(this,v)}e.col=j,e.size_x=c,e.size_y=d,this.add_to_gridmap(o,b),b.data("coords").update({width:c*this.options.widget_base_dimensions[0]+(c-1)*this.options.widget_margins[0]*2,height:d*this.options.widget_base_dimensions[1]+(d-1)*this.options.widget_margins[1]*2}),d>h&&this.add_faux_rows(d-h),c>g&&this.add_faux_cols(c-g),b.attr({"data-col":j,"data-sizex":c,"data-sizey":d});if(q.length){var w=[q[0],e.row,q.length,Math.min(h,d),b];this.remove_empty_cells.apply(this,w)}if(s.length){var x=[j,e.row,c,d,b];this.remove_empty_cells.apply(this,x)}return b},g.empty_cells=function(b,c,d,e,f){var g=this.widgets_below({col:b,row:c-e,size_x:d,size_y:e});return g.not(f).each(a.proxy(function(b,d){var f=a(d).coords().grid;if(!(f.row<=c+e-1))return;var g=c+e-f.row;this.move_widget_down(a(d),g)},this)),this.set_dom_grid_height(),this},g.remove_empty_cells=function(b,c,d,e,f){var g=this.widgets_below({col:b,row:c,size_x:d,size_y:e});return g.not(f).each(a.proxy(function(b,c){this.move_widget_up(a(c),e)},this)),this.set_dom_grid_height(),this},g.next_position=function(a,b){a||(a=1),b||(b=1);var c=this.gridmap,d=c.length,e=[],f;for(var g=1;g",{"class":"preview-holder","data-row":this.$player.attr("data-row"),"data-col":this.$player.attr("data-col"),css:{width:e.width,height:e.height}}).appendTo(this.$el),this.options.draggable.start&&this.options.draggable.start.call(this,b,c)},g.on_drag=function(a,b){if(this.$player===null)return!1;var c={left:b.position.left+this.baseX,top:b.position.top+this.baseY};this.colliders_data=this.collision_api.get_closest_colliders(c),this.on_overlapped_column_change(this.on_start_overlapping_column,this.on_stop_overlapping_column),this.on_overlapped_row_change(this.on_start_overlapping_row,this.on_stop_overlapping_row),this.helper&&this.$player&&this.$player.css({left:b.position.left,top:b.position.top}),this.options.draggable.drag&&this.options.draggable.drag.call(this,a,b)},g.on_stop_drag=function(a,b){this.$helper.add(this.$player).add(this.$wrapper).removeClass("dragging"),b.position.left=b.position.left+this.baseX,b.position.top=b.position.top+this.baseY,this.colliders_data=this.collision_api.get_closest_colliders(b.position),this.on_overlapped_column_change(this.on_start_overlapping_column,this.on_stop_overlapping_column),this.on_overlapped_row_change(this.on_start_overlapping_row,this.on_stop_overlapping_row),this.$player.addClass("player-revert").removeClass("player").attr({"data-col":this.placeholder_grid_data.col,"data-row":this.placeholder_grid_data.row}).css({left:"",top:""}),this.$changed=this.$changed.add(this.$player),this.cells_occupied_by_player=this.get_cells_occupied(this.placeholder_grid_data),this.set_cells_player_occupies(this.placeholder_grid_data.col,this.placeholder_grid_data.row),this.$player.coords().grid.row=this.placeholder_grid_data.row,this.$player.coords().grid.col=this.placeholder_grid_data.col,this.options.draggable.stop&&this.options.draggable.stop.call(this,a,b),this.$preview_holder.remove(),this.$player=null,this.$helper=null,this.placeholder_grid_data={},this.player_grid_data={},this.cells_occupied_by_placeholder={},this.cells_occupied_by_player={},this.set_dom_grid_height()},g.on_overlapped_column_change=function(b,c){if(!this.colliders_data.length)return;var d=this.get_targeted_columns(this.colliders_data[0].el.data.col),e=this.last_cols.length,f=d.length,g;for(g=0;gc.row?1:-1}),b},g.sort_by_row_and_col_asc=function(a){return a=a.sort(function(a,b){return a.row>b.row||a.row===b.row&&a.col>b.col?1:-1}),a},g.sort_by_col_asc=function(a){return a=a.sort(function(a,b){return a.col>b.col?1:-1}),a},g.sort_by_row_desc=function(a){return a=a.sort(function(a,b){return a.row+a.size_y=0&&a.inArray(c,d.rows)>=0},g.is_placeholder_in=function(b,c){var d=this.cells_occupied_by_placeholder||{};return this.is_placeholder_in_col(b)&&a.inArray(c,d.rows)>=0},g.is_placeholder_in_col=function(b){var c=this.cells_occupied_by_placeholder||[];return a.inArray(b,c.cols)>=0},g.is_empty=function(a,b){return typeof this.gridmap[a]!="undefined"&&typeof this.gridmap[a][b]!="undefined"&&this.gridmap[a][b]===!1?!0:!1},g.is_occupied=function(a,b){return this.gridmap[a]?this.gridmap[a][b]?!0:!1:!1},g.is_widget=function(a,b){var c=this.gridmap[a];return c?(c=c[b],c?c:!1):!1},g.is_widget_under_player=function(a,b){return this.is_widget(a,b)?this.is_player_in(a,b):!1},g.get_widgets_under_player=function(b){b||(b=this.cells_occupied_by_player||{cols:[],rows:[]});var c=a([]);return a.each(b.cols,a.proxy(function(d,e){a.each(b.rows,a.proxy(function(a,b){this.is_widget(e,b)&&(c=c.add(this.gridmap[e][b]))},this))},this)),c},g.set_placeholder=function(b,c){var d=a.extend({},this.placeholder_grid_data),e=this.widgets_below({col:d.col,row:d.row,size_y:d.size_y,size_x:d.size_x}),f=b+d.size_x-1;f>this.cols&&(b=b-(f-b));var g=this.placeholder_grid_data.row0)if(this.is_empty(a,h)||this.is_player(a,h)||this.is_widget(a,h)&&g[h].is(f))d[a].push(h),e=h0){if(this.is_widget(f,h)&&!this.is_player_in(f,h)&&!g[h].is(a.el))break;!this.is_player(f,h)&&!this.is_placeholder_in(f,h)&&!this.is_player_in(f,h)&&d[f].push(h),h=b?a[d[0]]:!1},g.get_widgets_overlapped=function(){var b,c=a([]),d=[],e=this.cells_occupied_by_player.rows.slice(0);return e.reverse(),a.each(this.cells_occupied_by_player.cols,a.proxy(function(b,f){a.each(e,a.proxy(function(b,e){if(!this.gridmap[f])return!0;var g=this.gridmap[f][e];this.is_occupied(f,e)&&!this.is_player(g)&&a.inArray(g,d)===-1&&(c=c.add(g),d.push(g))},this))},this)),c},g.on_start_overlapping_column=function(a){this.set_player(a,!1)},g.on_start_overlapping_row=function(a){this.set_player(!1,a)},g.on_stop_overlapping_column=function(a){this.set_player(a,!1);var b=this;this.for_each_widget_below(a,this.cells_occupied_by_player.rows[0],function(a,c){b.move_widget_up(this,b.player_grid_data.size_y)})},g.on_stop_overlapping_row=function(a){this.set_player(!1,a);var b=this,c=this.cells_occupied_by_player.cols;for(var d=0,e=c.length;d0&&this.move_widget_down(d,f)},this)),h.row=i,this.update_widget_position(h,b),b.attr("data-row",h.row),this.$changed=this.$changed.add(b),f.push(b)}},g.can_go_up_to_row=function(b,c,d){var e=this.gridmap,f=!0,g=[],h=b.row,i;this.for_each_column_occupied(b,function(a){var b=e[a];g[a]=[],i=h;while(i--)if(this.is_empty(a,i)&&!this.is_placeholder_in(a,i))g[a].push(i);else break;if(!g[a].length)return f=!1,!0});if(!f)return!1;i=d;for(i=1;i0?c:0},g.widgets_below=function(b){var c=a.isPlainObject(b)?b:b.coords().grid,d=this,e=this.gridmap,f=c.row+c.size_y-1,g=a([]);return this.for_each_column_occupied(c,function(b){d.for_each_widget_below(b,f,function(b,c){if(!d.is_player(this)&&a.inArray(this,g)===-1)return g=g.add(this),!0})}),this.sort_by_row_asc(g)},g.set_cells_player_occupies=function(a,b){return this.remove_from_gridmap(this.placeholder_grid_data),this.placeholder_grid_data.col=a,this.placeholder_grid_data.row=b,this.add_to_gridmap(this.placeholder_grid_data,this.$player),this},g.empty_cells_player_occupies=function(){return this.remove_from_gridmap(this.placeholder_grid_data),this},g.can_go_up=function(a){var b=a.coords().grid,c=b.row,d=c-1,e=this.gridmap,f=[],g=!0;return c===1?!1:(this.for_each_column_occupied(b,function(a){var b=this.is_widget(a,d);if(this.is_occupied(a,d)||this.is_player(a,d)||this.is_placeholder_in(a,d)||this.is_player_in(a,d))return g=!1,!0}),g)},g.can_move_to=function(a,b,c,d){var e=this.gridmap,f=a.el,g={size_y:a.size_y,size_x:a.size_x,col:b,row:c},h=!0,i=b+a.size_x-1;return i>this.cols?!1:d&&d0&&this.is_widget(d,m)&&a.inArray(g[d][m],l)===-1){h=f.call(g[d][m],d,m),l.push(g[d][m]);if(h)break}},"for_each/below":function(){for(m=e+1,i=g[d].length;m=1;e--)for(a=b[e].length-1;a>=1;a--)if(this.is_widget(e,a)){c.push(a),d[a]=e;break}var f=Math.max.apply(Math,c);return this.highest_occupied_cell={col:d[f],row:f},this.highest_occupied_cell},g.get_widgets_from=function(b,c){var d=this.gridmap,e=a();return b&&(e=e.add(this.$widgets.filter(function(){var c=a(this).attr("data-col");return c===b||c>b}))),c&&(e=e.add(this.$widgets.filter(function(){var b=a(this).attr("data-row");return b===c||b>c}))),e},g.set_dom_grid_height=function(){var a=this.get_highest_occupied_cell().row;return this.$el.css("height",a*this.min_widget_height),this},g.generate_stylesheet=function(b){var c="",d=this.options.max_size_x,e=0,g=0,h,i;b||(b={}),b.cols||(b.cols=this.cols),b.rows||(b.rows=this.rows),b.namespace||(b.namespace=this.options.namespace),b.widget_base_dimensions||(b.widget_base_dimensions=this.options.widget_base_dimensions),b.widget_margins||(b.widget_margins=this.options.widget_margins),b.min_widget_width=b.widget_margins[0]*2+b.widget_base_dimensions[0],b.min_widget_height=b.widget_margins[1]*2+b.widget_base_dimensions[1];var j=a.param(b);if(a.inArray(j,f.generated_stylesheets)>=0)return!1;f.generated_stylesheets.push(j);for(h=b.cols;h>=0;h--)c+=b.namespace+' [data-col="'+(h+1)+'"] { left:'+(h*b.widget_base_dimensions[0]+h*b.widget_margins[0]+(h+1)*b.widget_margins[0])+"px;} ";for(h=b.rows;h>=0;h--)c+=b.namespace+' [data-row="'+(h+1)+'"] { top:'+(h*b.widget_base_dimensions[1]+h*b.widget_margins[1]+(h+1)*b.widget_margins[1])+"px;} ";for(var k=1;k<=b.rows;k++)c+=b.namespace+' [data-sizey="'+k+'"] { height:'+(k*b.widget_base_dimensions[1]+(k-1)*b.widget_margins[1]*2)+"px;}";for(var l=1;l<=d;l++)c+=b.namespace+' [data-sizex="'+l+'"] { width:'+(l*b.widget_base_dimensions[0]+(l-1)*b.widget_margins[0]*2)+"px;}";return this.add_style_tag(c)},g.add_style_tag=function(a){var b=c,d=b.createElement("style");return b.getElementsByTagName("head")[0].appendChild(d),d.setAttribute("type","text/css"),d.styleSheet?d.styleSheet.cssText=a:d.appendChild(c.createTextNode(a)),this},g.generate_faux_grid=function(a,b){this.faux_grid=[],this.gridmap=[];var c,d;for(c=b;c>0;c--){this.gridmap[c]=[];for(d=a;d>0;d--)this.add_faux_cell(d,c)}return this},g.add_faux_cell=function(b,c){var d=a({left:this.baseX+(c-1)*this.min_widget_width,top:this.baseY+(b-1)*this.min_widget_height,width:this.min_widget_width,height:this.min_widget_height,col:c,row:b,original_col:c,original_row:b}).coords();return a.isArray(this.gridmap[c])||(this.gridmap[c]=[]),this.gridmap[c][b]=!1,this.faux_grid.push(d),this},g.add_faux_rows=function(a){var b=this.rows,c=b+(a||1);for(var d=c;d>b;d--)for(var e=this.cols;e>=1;e--)this.add_faux_cell(d,e);return this.rows=c,this.options.autogenerate_stylesheet&&this.generate_stylesheet(),this},g.add_faux_cols=function(a){var b=this.cols,c=b+(a||1);for(var d=b;d=1;e--)this.add_faux_cell(e,d);return this.cols=c,this.options.autogenerate_stylesheet&&this.generate_stylesheet(),this},g.recalculate_faux_grid=function(){var c=this.$wrapper.width();return this.baseX=(a(b).width()-c)/2,this.baseY=this.$wrapper.offset().top,a.each(this.faux_grid,a.proxy(function(a,b){this.faux_grid[a]=b.update({left:this.baseX+(b.data.col-1)*this.min_widget_width,top:this.baseY+(b.data.row-1)*this.min_widget_height})},this)),this},g.get_widgets_from_DOM=function(){return this.$widgets.each(a.proxy(function(b,c){this.register_widget(a(c))},this)),this},g.generate_grid_and_stylesheet=function(){var c=this.$wrapper.width(),d=this.$wrapper.height(),e=Math.floor(c/this.min_widget_width)+this.options.extra_cols,f=this.$widgets.map(function(){return a(this).attr("data-col")});f=Array.prototype.slice.call(f,0),f.length||(f=[0]);var g=Math.max.apply(Math,f),h=this.options.extra_rows;return this.$widgets.each(function(b,c){h+=+a(c).attr("data-sizey")}),this.cols=Math.max(g,e,this.options.min_cols),this.rows=Math.max(h,this.options.min_rows),this.baseX=(a(b).width()-c)/2,this.baseY=this.$wrapper.offset().top,this.options.autogenerate_stylesheet&&this.generate_stylesheet(),this.generate_faux_grid(this.rows,this.cols)},a.fn.gridster=function(b){return this.each(function(){a(this).data("gridster")||a(this).data("gridster",new f(this,b))})},a.Gridster=g}(jQuery,window,document),function(a,b,c,d){var e=a.Gridster;e.widgets_in_col=function(a){if(!this.gridmap[a])return!1;for(var b=this.gridmap[a].length-1;b>=0;b--)if(this.is_widget(a,b)!==!1)return!0;return!1},e.widgets_in_row=function(a){for(var b=this.gridmap.length;b>=1;b--)if(this.is_widget(b,a)!==!1)return!0;return!1},e.widgets_in_range=function(b,c,d,e){var f=[],g=[],h=a([]),i,j,k,l;for(i=d;i>=b;i--)for(j=e;j>=c;j--)k=this.is_widget(i,j),k!==!1&&(l=k.data("coords").grid,l.col>=b&&l.col<=d&&l.row>=c&&l.row<=e&&(h=h.add(k)));return h},e.get_bottom_most_occupied_cell=function(){var a=0,b=0;return this.for_each_cell(function(c,d,e){c&&e>a&&(a=e,b=d)}),{col:b,row:a}},e.get_right_most_occupied_cell=function(){var a=0,b=0;return this.for_each_cell(function(c,d,e){if(c)return a=e,b=d,!1}),{col:b,row:a}},e.for_each_cell=function(a,b){b||(b=this.gridmap);var c=b.length,d=b[1].length;e:for(var f=c-1;f>=1;f--)for(var g=d-1;g>=1;g--){var h=b[f]&&b[f][g];if(a){if(a.call(this,h,f,g)===!1)break e;continue}}},e.next_position_in_range=function(a,b,c){a||(a=1),b||(b=1);var d=this.gridmap,e=d.length,f=[],g;for(var h=1;h=1?this.sort_by_col_asc(f)[0]:!1},e.closest_to_right=function(a,b){if(!this.gridmap[a])return!1;var c=this.gridmap.length-1;for(var d=a;d<=c;d++)if(this.gridmap[d][b])return{col:d,row:b};return!1},e.closest_to_left=function(a,b){var c=this.gridmap.length-1;if(!this.gridmap[a])return!1;for(var d=a;d>=1;d--)if(this.gridmap[d][b])return{col:d,row:b};return!1}}(jQuery,window,document); --------------------------------------------------------------------------------