瀏覽代碼

Merge branch 'dev'

Denis K 9 年之前
父節點
當前提交
321ae7ea85
共有 78 個文件被更改,包括 6766 次插入958 次删除
  1. 28 0
      CHANGELOG.rst
  2. 1 1
      jet/__init__.py
  3. 0 0
      jet/dashboard/__init__.py
  4. 21 6
      jet/dashboard/dashboard.py
  5. 0 0
      jet/dashboard/dashboard_modules/__init__.py
  6. 404 0
      jet/dashboard/dashboard_modules/google_analytics.py
  7. 54 0
      jet/dashboard/dashboard_modules/google_analytics_views.py
  8. 331 0
      jet/dashboard/dashboard_modules/yandex_metrika.py
  9. 64 0
      jet/dashboard/dashboard_modules/yandex_metrika_views.py
  10. 132 0
      jet/dashboard/forms.py
  11. 33 0
      jet/dashboard/migrations/0001_initial.py
  12. 0 0
      jet/dashboard/migrations/__init__.py
  13. 57 0
      jet/dashboard/models.py
  14. 11 9
      jet/dashboard/modules.py
  15. 5 0
      jet/dashboard/settings.py
  16. 48 0
      jet/dashboard/south_migrations/0001_initial.py
  17. 0 0
      jet/dashboard/south_migrations/__init__.py
  18. 35 0
      jet/dashboard/static/jet.dashboard/dashboard_modules/google_analytics.js
  19. 35 0
      jet/dashboard/static/jet.dashboard/dashboard_modules/yandex_metrika.js
  20. 55 0
      jet/dashboard/static/jet.dashboard/vendor/chart.js/CONTRIBUTING.md
  21. 3477 0
      jet/dashboard/static/jet.dashboard/vendor/chart.js/Chart.js
  22. 9 0
      jet/dashboard/static/jet.dashboard/vendor/chart.js/Chart.min.js
  23. 7 0
      jet/dashboard/static/jet.dashboard/vendor/chart.js/LICENSE.md
  24. 20 0
      jet/dashboard/static/jet.dashboard/vendor/chart.js/README.md
  25. 2 2
      jet/dashboard/templates/admin/app_index.html
  26. 35 0
      jet/dashboard/templates/admin/index.html
  27. 4 4
      jet/dashboard/templates/jet.dashboard/dashboard.html
  28. 1 1
      jet/dashboard/templates/jet.dashboard/dashboard_tools.html
  29. 3 3
      jet/dashboard/templates/jet.dashboard/module.html
  30. 0 0
      jet/dashboard/templates/jet.dashboard/modules/app_list.html
  31. 0 0
      jet/dashboard/templates/jet.dashboard/modules/feed.html
  32. 38 0
      jet/dashboard/templates/jet.dashboard/modules/google_analytics_period_visitors.html
  33. 33 0
      jet/dashboard/templates/jet.dashboard/modules/google_analytics_visitors_chart.html
  34. 26 0
      jet/dashboard/templates/jet.dashboard/modules/google_analytics_visitors_totals.html
  35. 0 0
      jet/dashboard/templates/jet.dashboard/modules/link_list.html
  36. 0 0
      jet/dashboard/templates/jet.dashboard/modules/model_list.html
  37. 0 0
      jet/dashboard/templates/jet.dashboard/modules/recent_actions.html
  38. 38 0
      jet/dashboard/templates/jet.dashboard/modules/yandex_metrika_period_visitors.html
  39. 33 0
      jet/dashboard/templates/jet.dashboard/modules/yandex_metrika_visitors_chart.html
  40. 26 0
      jet/dashboard/templates/jet.dashboard/modules/yandex_metrika_visitors_totals.html
  41. 4 4
      jet/dashboard/templates/jet.dashboard/update_module.html
  42. 0 0
      jet/dashboard/templates/jet.dashboard/update_module_fieldset.html
  43. 0 0
      jet/dashboard/templatetags/__init__.py
  44. 12 0
      jet/dashboard/templatetags/jet_dashboard_tags.py
  45. 53 0
      jet/dashboard/urls.py
  46. 17 0
      jet/dashboard/utils.py
  47. 223 0
      jet/dashboard/views.py
  48. 5 129
      jet/forms.py
  49. 二進制
      jet/locale/en/LC_MESSAGES/django.mo
  50. 476 199
      jet/locale/en/LC_MESSAGES/django.po
  51. 二進制
      jet/locale/ru/LC_MESSAGES/django.mo
  52. 492 205
      jet/locale/ru/LC_MESSAGES/django.po
  53. 17 0
      jet/migrations/0002_delete_userdashboardmodule.py
  54. 2 28
      jet/models.py
  55. 0 4
      jet/settings.py
  56. 50 0
      jet/south_migrations/0002_auto__del_userdashboardmodule.py
  57. 2 0
      jet/static/jet/css/_changelist.scss
  58. 7 3
      jet/static/jet/css/_content.scss
  59. 67 3
      jet/static/jet/css/_dashboard.scss
  60. 4 0
      jet/static/jet/css/_forms.scss
  61. 4 2
      jet/static/jet/css/_table.scss
  62. 49 10
      jet/static/jet/css/themes/default/base.css
  63. 0 0
      jet/static/jet/css/themes/default/base.css.map
  64. 49 10
      jet/static/jet/css/themes/green/base.css
  65. 0 0
      jet/static/jet/css/themes/green/base.css.map
  66. 24 15
      jet/static/jet/js/main.js
  67. 0 0
      jet/static/jet/js/main.min.js
  68. 6 4
      jet/templates/admin/base.html
  69. 101 26
      jet/templates/admin/index.html
  70. 1 8
      jet/templatetags/jet_tags.py
  71. 2 2
      jet/tests/dashboard.py
  72. 1 0
      jet/tests/settings.py
  73. 4 4
      jet/tests/test_dashboard.py
  74. 15 14
      jet/tests/test_views.py
  75. 1 0
      jet/tests/urls.py
  76. 1 33
      jet/urls.py
  77. 8 20
      jet/utils.py
  78. 3 209
      jet/views.py

+ 28 - 0
CHANGELOG.rst

@@ -1,6 +1,34 @@
 Changelog
 =========
 
+0.0.7
+-----
+* [Feature] Added Google Analytics visitors totals dashboard widget
+* [Feature] Added Google Analytics visitors chart dashboard widget
+* [Feature] Added Google Analytics period visitors dashboard widget
+* [Feature] Added Yandex Metrika visitors totals dashboard widget
+* [Feature] Added Yandex Metrika visitors chart dashboard widget
+* [Feature] Added Yandex Metrika period visitors dashboard widget
+* [Feature] Animated ajax loaded modules height on load
+* [Feature] Added initial docs
+* [Feature] Added ability to use custom checkboxes without labels styled
+* [Feature] Added ability to specify optional modules urls
+* [Feature] Added pop/update module settings methods
+* [Feature] Added module contrast style
+* [Feature] Added module custom style property
+* [Feature] Pass module to module settings form
+* [Feature] Set dashboard widgets minimum width
+* [Feature] Added dashboard widgets class helpers
+* [Fix] Fixed toggle all checkbox
+* [Fix] Fixed 500 when module class cannot be loaded
+* [Fix] Fixed datetime json encoder
+* [Fix] Fixed double shadow for tables in dashboard modules
+* [Fix] Fixed tables forced alignment
+* [Fix] Fixed dashboard ul layout
+* [Fix] Fixed language code formatting for js
+* [Fix] Fixed 500 when adding module if no module type specified
+
+
 0.0.6
 -----
 

+ 1 - 1
jet/__init__.py

@@ -1 +1 @@
-VERSION = '0.0.6'
+VERSION = '0.0.7'

+ 0 - 0
jet/dashboard/__init__.py


+ 21 - 6
jet/dashboard.py → jet/dashboard/dashboard.py

@@ -1,8 +1,8 @@
 from importlib import import_module
 from django.core.urlresolvers import resolve, reverse
 from django.template.loader import render_to_string
-from jet import modules
-from jet.models import UserDashboardModule
+from jet.dashboard import modules
+from jet.dashboard.models import UserDashboardModule
 from django.core.context_processors import csrf
 from django.utils.translation import ugettext_lazy as _
 from jet.ordered_set import OrderedSet
@@ -84,8 +84,9 @@ class Dashboard(object):
 
         for module_model in module_models:
             module_cls = module_model.load_module()
-            module = module_cls(model=module_model, context=self.context)
-            loaded_modules.append(module)
+            if module_cls is not None:
+                module = module_cls(model=module_model, context=self.context)
+                loaded_modules.append(module)
 
         self.modules = loaded_modules
 
@@ -98,7 +99,7 @@ class Dashboard(object):
         })
         context.update(csrf(context['request']))
 
-        return render_to_string('jet/dashboard/dashboard.html', context)
+        return render_to_string('jet.dashboard/dashboard.html', context)
 
     def render_tools(self):
         context = self.context
@@ -109,7 +110,7 @@ class Dashboard(object):
         })
         context.update(csrf(context['request']))
 
-        return render_to_string('jet/dashboard/dashboard_tools.html', context)
+        return render_to_string('jet.dashboard/dashboard_tools.html', context)
 
     def media(self):
         unique_css = OrderedSet()
@@ -244,3 +245,17 @@ class DefaultAppIndexDashboard(AppIndexDashboard):
             order=0
         ))
 
+
+class DashboardUrls(object):
+    _urls = []
+
+    def get_urls(self):
+        return self._urls
+
+    def register_url(self, url):
+        self._urls.append(url)
+
+    def register_urls(self, urls):
+        self._urls.extend(urls)
+
+urls = DashboardUrls()

+ 0 - 0
jet/dashboard/dashboard_modules/__init__.py


+ 404 - 0
jet/dashboard/dashboard_modules/google_analytics.py

@@ -0,0 +1,404 @@
+# encoding: utf-8
+import datetime
+import json
+from django import forms
+from django.core.urlresolvers import reverse
+from django.forms import Widget
+from django.utils import formats
+from django.utils.html import format_html
+from django.utils.safestring import mark_safe
+from googleapiclient.discovery import build
+import httplib2
+from jet.dashboard.modules import DashboardModule
+from oauth2client.client import flow_from_clientsecrets, OAuth2Credentials, AccessTokenRefreshError, Storage
+from django.utils.translation import ugettext_lazy as _
+from django.conf import settings
+
+try:
+    from django.utils.encoding import force_unicode
+except ImportError:
+    from django.utils.encoding import force_text as force_unicode
+
+try:
+    from django.forms.utils import flatatt
+except ImportError:
+    from django.forms.util import flatatt
+
+JET_MODULE_GOOGLE_ANALYTICS_CLIENT_SECRETS_FILE = getattr(
+    settings,
+    'JET_MODULE_GOOGLE_ANALYTICS_CLIENT_SECRETS_FILE',
+    ''
+)
+
+
+class ModuleCredentialStorage(Storage):
+    def __init__(self, module):
+        self.module = module
+
+    def locked_get(self):
+        pass
+
+    def locked_put(self, credentials):
+        pass
+
+    def locked_delete(self):
+        pass
+
+    def get(self):
+        try:
+            settings = json.loads(self.module.settings)
+            credential = settings['credential']
+            return OAuth2Credentials.from_json(credential)
+        except (ValueError, KeyError):
+            return None
+
+    def put(self, credentials):
+        self.module.update_settings({'credential': credentials.to_json()})
+
+    def delete(self):
+        self.module.pop_settings(('credential',))
+
+
+class GoogleAnalyticsClient:
+    credential = None
+    analytics_service = None
+
+    def __init__(self, storage=None, redirect_uri=None):
+        self.FLOW = flow_from_clientsecrets(
+            JET_MODULE_GOOGLE_ANALYTICS_CLIENT_SECRETS_FILE,
+            scope='https://www.googleapis.com/auth/analytics.readonly',
+            redirect_uri=redirect_uri
+        )
+
+        if storage is not None:
+            credential = storage.get()
+            credential.set_store(storage)
+            self.set_credential(credential)
+
+    def get_oauth_authorize_url(self, state=''):
+        self.FLOW.params['state'] = state
+        authorize_url = self.FLOW.step1_get_authorize_url()
+        return authorize_url
+
+    def set_credential(self, credential):
+        self.credential = credential
+        self.set_analytics_service(self.credential)
+
+    def set_credential_from_request(self, request):
+        self.set_credential(self.FLOW.step2_exchange(request.GET))
+
+    def set_analytics_service(self, credential):
+        http = httplib2.Http()
+        http = credential.authorize(http)
+        self.analytics_service = build('analytics', 'v3', http=http)
+
+    def api_profiles(self):
+        if self.analytics_service is None:
+            return None, None
+
+        try:
+            profiles = self.analytics_service.management().profiles().list(
+                accountId='~all',
+                webPropertyId='~all'
+            ).execute()
+
+            return profiles['items'], None
+        except (TypeError, KeyError) as e:
+            return None, e
+
+    def api_ga(self, profile_id, date1, date2, group=None):
+        if self.analytics_service is None:
+            return None, None
+
+        if group == 'day':
+            dimensions = 'ga:date'
+        elif group == 'week':
+            dimensions = 'ga:year,ga:week'
+        elif group == 'month':
+            dimensions = 'ga:year,ga:month'
+        else:
+            dimensions = ''
+
+        try:
+            data = self.analytics_service.data().ga().get(
+                ids='ga:' + profile_id,
+                start_date=date1.strftime('%Y-%m-%d'),
+                end_date=date2.strftime('%Y-%m-%d'),
+                metrics='ga:users,ga:sessions,ga:pageviews',
+                dimensions=dimensions
+            ).execute()
+
+            return data, None
+        except TypeError as e:
+            return None, e
+
+
+class CredentialWidget(Widget):
+    module = None
+
+    def render(self, name, value, attrs=None):
+        if value and len(value) > 0:
+            link = '<a href="%s">%s</a>' % (
+                reverse('jet-dashboard:google-analytics-revoke', kwargs={'pk': self.module.model.pk}),
+                _('Revoke access')
+            )
+        else:
+            link = '<a href="%s">%s</a>' % (
+                reverse('jet-dashboard:google-analytics-grant', kwargs={'pk': self.module.model.pk}),
+                _('Grant access')
+            )
+
+        attrs = self.build_attrs({
+            'type': 'hidden',
+            'name': 'credential',
+        })
+        attrs['value'] = force_unicode(value) if value else ''
+
+        return format_html('%s<input{} />' % link, flatatt(attrs))
+
+
+class GoogleAnalyticsSettingsForm(forms.Form):
+    credential = forms.CharField(label=_('Access'), widget=CredentialWidget)
+    counter = forms.ChoiceField(label=_('Counter'))
+    period = forms.ChoiceField(label=_('Statistics period'), choices=(
+        (0, _('Today')),
+        (6, _('Last week')),
+        (30, _('Last month')),
+        (31 * 3 - 1, _('Last quarter')),
+        (364, _('Last year')),
+    ))
+
+    def set_module(self, module):
+        self.fields['credential'].widget.module = module
+        self.set_counter_choices(module)
+
+    def set_counter_choices(self, module):
+        counters = module.counters()
+        if counters is not None:
+            self.fields['counter'].choices = (('', '-- %s --' % _('none')),)
+            self.fields['counter'].choices.extend(map(lambda x: (x['id'], x['websiteUrl']), counters))
+        else:
+            label = _('grant access first') if module.credential is None else _('counters loading failed')
+            self.fields['counter'].choices = (('', '-- %s -- ' % label),)
+
+
+class GoogleAnalyticsChartSettingsForm(GoogleAnalyticsSettingsForm):
+    show = forms.ChoiceField(label=_('Show'), choices=(
+        ('ga:users', _('Users')),
+        ('ga:sessions', _('Sessions')),
+        ('ga:pageviews', _('Views')),
+    ))
+    group = forms.ChoiceField(label=_('Group'), choices=(
+        ('day', _('By day')),
+        ('week', _('By week')),
+        ('month', _('By month')),
+    ))
+
+
+class GoogleAnalyticsPeriodVisitorsSettingsForm(GoogleAnalyticsSettingsForm):
+    group = forms.ChoiceField(label=_('Group'), choices=(
+        ('day', _('By day')),
+        ('week', _('By week')),
+        ('month', _('By month')),
+    ))
+
+
+class GoogleAnalyticsBase(DashboardModule):
+    settings_form = GoogleAnalyticsSettingsForm
+    ajax_load = True
+    contrast = True
+    period = None
+    credential = None
+    counter = None
+    error = None
+    storage = None
+
+    def __init__(self, title=None, period=None, **kwargs):
+        kwargs.update({'period': period})
+        super(GoogleAnalyticsBase, self).__init__(title, **kwargs)
+
+    def settings_dict(self):
+        return {
+            'period': self.period,
+            'credential': self.credential,
+            'counter': self.counter
+        }
+
+    def load_settings(self, settings):
+        try:
+            self.period = int(settings.get('period'))
+        except TypeError:
+            self.period = 0
+        self.credential = settings.get('credential')
+        self.storage = ModuleCredentialStorage(self.model)
+        self.counter = settings.get('counter')
+
+    def init_with_context(self, context):
+        raise NotImplementedError('subclasses of GoogleAnalytics must provide a init_with_context() method')
+
+    def counters(self):
+        try:
+            client = GoogleAnalyticsClient(self.storage)
+            profiles, exception = client.api_profiles()
+            return profiles
+        except Exception:
+            return None
+
+    def get_grouped_date(self, data, group):
+        if group == 'week':
+            date = datetime.datetime.strptime(
+                '%s-%s-%s' % (data['ga_year'], data['ga_week'], '0'),
+                '%Y-%W-%w'
+            )
+        elif group == 'month':
+            date = datetime.datetime.strptime(data['ga_year'] + data['ga_month'], '%Y%m')
+        else:
+            date = datetime.datetime.strptime(data['ga_date'], '%Y%m%d')
+        return date
+
+    def format_grouped_date(self, data, group):
+        date = self.get_grouped_date(data, group)
+
+        if group == 'week':
+            date = u'%s — %s' % (
+                (date - datetime.timedelta(days=6)).strftime('%d.%m'),
+                date.strftime('%d.%m')
+            )
+        elif group == 'month':
+            date = date.strftime('%b, %Y')
+        else:
+            date = formats.date_format(date, 'DATE_FORMAT')
+        return date
+
+    def counter_attached(self):
+        if self.credential is None:
+            self.error = mark_safe(_('Please <a href="%s">attach Google account and choose Google Analytics counter</a> to start using widget') % reverse('jet-dashboard:update_module', kwargs={'pk': self.model.pk}))
+            return False
+        elif self.counter is None:
+            self.error = mark_safe(_('Please <a href="%s">select Google Analytics counter</a> to start using widget') % reverse('jet-dashboard:update_module', kwargs={'pk': self.model.pk}))
+            return False
+        else:
+            return True
+
+    def api_ga(self, group=None):
+        if self.counter_attached():
+            date1 = datetime.datetime.utcnow() - datetime.timedelta(days=self.period)
+            date2 = datetime.datetime.utcnow()
+
+            try:
+                client = GoogleAnalyticsClient(self.storage)
+                result, exception = client.api_ga(self.counter, date1, date2, group)
+
+                if exception is not None:
+                        raise exception
+
+                return result
+            except Exception as e:
+                error = _('API request failed.')
+                if isinstance(e, AccessTokenRefreshError):
+                    error += _(' Try to <a href="%s">revoke and grant access</a> again') % reverse('jet-dashboard:update_module', kwargs={'pk': self.model.pk})
+                self.error = mark_safe(error)
+
+
+class GoogleAnalyticsVisitorsTotals(GoogleAnalyticsBase):
+    title = _('Google Analytics visitors totals')
+    template = 'jet.dashboard/modules/google_analytics_visitors_totals.html'
+
+    def __init__(self, title=None, period=None, **kwargs):
+        kwargs.update({'period': period})
+        super(GoogleAnalyticsVisitorsTotals, self).__init__(title, **kwargs)
+
+    def init_with_context(self, context):
+        result = self.api_ga()
+
+        if result is not None:
+            try:
+                self.children.append({'title': _('users'), 'value': result['totalsForAllResults']['ga:users']})
+                self.children.append({'title': _('sessions'), 'value': result['totalsForAllResults']['ga:sessions']})
+                self.children.append({'title': _('views'), 'value': result['totalsForAllResults']['ga:pageviews']})
+            except KeyError:
+                self.error = _('Bad server response')
+
+
+class GoogleAnalyticsVisitorsChart(GoogleAnalyticsBase):
+    title = _('Google Analytics visitors chart')
+    template = 'jet.dashboard/modules/google_analytics_visitors_chart.html'
+    style = 'overflow-x: auto;'
+    show = None
+    group = None
+    settings_form = GoogleAnalyticsChartSettingsForm
+
+    class Media:
+        js = ('jet.dashboard/vendor/chart.js/Chart.min.js', 'jet.dashboard/dashboard_modules/google_analytics.js')
+
+    def __init__(self, title=None, period=None, show=None, group=None, **kwargs):
+        kwargs.update({'period': period, 'show': show, 'group': group})
+        super(GoogleAnalyticsVisitorsChart, self).__init__(title, **kwargs)
+
+    def settings_dict(self):
+        settings = super(GoogleAnalyticsVisitorsChart, self).settings_dict()
+        settings['show'] = self.show
+        settings['group'] = self.group
+        return settings
+
+    def load_settings(self, settings):
+        super(GoogleAnalyticsVisitorsChart, self).load_settings(settings)
+        self.show = settings.get('show')
+        self.group = settings.get('group')
+
+    def init_with_context(self, context):
+        result = self.api_ga(self.group)
+
+        if result is not None:
+            try:
+                for data in result['rows']:
+                    row_data = {}
+
+                    i = 0
+                    for column in result['columnHeaders']:
+                        row_data[column['name'].replace(':', '_')] = data[i]
+                        i += 1
+
+                    date = self.get_grouped_date(row_data, self.group)
+                    self.children.append((date, row_data[self.show.replace(':', '_')]))
+            except KeyError:
+                self.error = _('Bad server response')
+
+
+class GoogleAnalyticsPeriodVisitors(GoogleAnalyticsBase):
+    title = _('Google Analytics period visitors')
+    template = 'jet.dashboard/modules/google_analytics_period_visitors.html'
+    group = None
+    contrast = False
+    settings_form = GoogleAnalyticsPeriodVisitorsSettingsForm
+
+    def __init__(self, title=None, period=None, group=None, **kwargs):
+        kwargs.update({'period': period, 'group': group})
+        super(GoogleAnalyticsPeriodVisitors, self).__init__(title, **kwargs)
+
+    def settings_dict(self):
+        settings = super(GoogleAnalyticsPeriodVisitors, self).settings_dict()
+        settings['group'] = self.group
+        return settings
+
+    def load_settings(self, settings):
+        super(GoogleAnalyticsPeriodVisitors, self).load_settings(settings)
+        self.group = settings.get('group')
+
+    def init_with_context(self, context):
+        result = self.api_ga(self.group)
+
+        if result is not None:
+            try:
+                for data in reversed(result['rows']):
+                    row_data = {}
+
+                    i = 0
+                    for column in result['columnHeaders']:
+                        row_data[column['name'].replace(':', '_')] = data[i]
+                        i += 1
+
+                    date = self.format_grouped_date(row_data, self.group)
+                    self.children.append((date, row_data))
+            except KeyError:
+                self.error = _('Bad server response')

+ 54 - 0
jet/dashboard/dashboard_modules/google_analytics_views.py

@@ -0,0 +1,54 @@
+from django.core.urlresolvers import reverse
+from django.conf.urls import url
+from django.contrib import messages
+from django.shortcuts import redirect
+from httplib2 import ServerNotFoundError
+from jet.dashboard.dashboard_modules.google_analytics import GoogleAnalyticsClient, ModuleCredentialStorage
+from jet.dashboard.models import UserDashboardModule
+from jet.dashboard import dashboard
+from django.http import HttpResponse
+from oauth2client.client import FlowExchangeError
+from django.utils.translation import ugettext_lazy as _
+
+
+def google_analytics_grant_view(request, pk):
+    redirect_uri = request.build_absolute_uri(reverse('jet-dashboard:google-analytics-callback'))
+    client = GoogleAnalyticsClient(redirect_uri=redirect_uri)
+    return redirect(client.get_oauth_authorize_url(pk))
+
+
+def google_analytics_revoke_view(request, pk):
+    try:
+        module = UserDashboardModule.objects.get(pk=pk)
+        ModuleCredentialStorage(module).delete()
+        return redirect(reverse('jet-dashboard:update_module', kwargs={'pk': module.pk}))
+    except UserDashboardModule.DoesNotExist:
+        return HttpResponse(_('Module not found'))
+
+
+def google_analytics_callback_view(request):
+    module = None
+
+    try:
+        state = request.GET['state']
+        module = UserDashboardModule.objects.get(pk=state)
+
+        redirect_uri = request.build_absolute_uri(reverse('jet-dashboard:google-analytics-callback'))
+        client = GoogleAnalyticsClient(redirect_uri=redirect_uri)
+        client.set_credential_from_request(request)
+
+        ModuleCredentialStorage(module).put(client.credential)
+    except (FlowExchangeError, ValueError, ServerNotFoundError):
+        messages.error(request, _('API request failed.'))
+    except KeyError:
+        return HttpResponse(_('Bad arguments'))
+    except UserDashboardModule.DoesNotExist:
+        return HttpResponse(_('Module not found'))
+
+    return redirect(reverse('jet-dashboard:update_module', kwargs={'pk': module.pk}))
+
+dashboard.urls.register_urls([
+    url(r'^google-analytics/grant/(?P<pk>\d+)/$', google_analytics_grant_view, name='google-analytics-grant'),
+    url(r'^google-analytics/revoke/(?P<pk>\d+)/$', google_analytics_revoke_view, name='google-analytics-revoke'),
+    url(r'^google-analytics/callback/', google_analytics_callback_view, name='google-analytics-callback'),
+])

+ 331 - 0
jet/dashboard/dashboard_modules/yandex_metrika.py

@@ -0,0 +1,331 @@
+# encoding: utf-8
+import datetime
+import json
+from django import forms
+from django.core.urlresolvers import reverse
+from django.forms import Widget
+from django.utils import formats
+from django.utils.html import format_html
+from django.utils.safestring import mark_safe
+from jet.dashboard.modules import DashboardModule
+from django.utils.translation import ugettext_lazy as _
+from django.conf import settings
+
+try:
+    from urllib import request
+    from urllib.parse import urlencode
+    from urllib.error import URLError, HTTPError
+except ImportError:
+    import urllib2 as request
+    from urllib2 import URLError, HTTPError
+    from urllib import urlencode
+
+JET_MODULE_YANDEX_METRIKA_CLIENT_ID = getattr(settings, 'JET_MODULE_YANDEX_METRIKA_CLIENT_ID', '')
+JET_MODULE_YANDEX_METRIKA_CLIENT_SECRET = getattr(settings, 'JET_MODULE_YANDEX_METRIKA_CLIENT_SECRET', '')
+
+
+class YandexMetrikaClient:
+    OAUTH_BASE_URL = 'https://oauth.yandex.ru/'
+    API_BASE_URL = 'https://api-metrika.yandex.ru/'
+    CLIENT_ID = JET_MODULE_YANDEX_METRIKA_CLIENT_ID
+    CLIENT_SECRET = JET_MODULE_YANDEX_METRIKA_CLIENT_SECRET
+
+    def __init__(self, access_token=None):
+        self.access_token = access_token
+
+    def request(self, base_url, url, data=None, headers=None):
+        url = '%s%s' % (base_url, url)
+
+        if data is not None:
+            data = urlencode(data).encode()
+
+        if headers is None:
+            headers = {}
+
+        req = request.Request(url, data, headers)
+
+        try:
+            f = request.urlopen(req)
+            result = f.read().decode()
+            result = json.loads(result)
+        except URLError as e:
+            return None, e
+
+        return result, None
+
+    def get_oauth_authorize_url(self, state=''):
+        return '%sauthorize' \
+               '?response_type=code' \
+               '&state=%s' \
+               '&client_id=%s' % (self.OAUTH_BASE_URL, state, self.CLIENT_ID)
+
+    def oauth_request(self, url, data=None):
+        return self.request(self.OAUTH_BASE_URL, url, data)
+
+    def oath_token_request(self, code):
+        data = {
+            'grant_type': 'authorization_code',
+            'code': code,
+            'client_id': self.CLIENT_ID,
+            'client_secret': self.CLIENT_SECRET
+        }
+        return self.oauth_request('token', data)
+
+    def api_request(self, url, data=None):
+        headers = None
+        if self.access_token is not None:
+            headers = {'Authorization': 'OAuth %s' % self.access_token}
+        return self.request(self.API_BASE_URL, url, data, headers)
+
+    def api_counters_request(self):
+        return self.api_request('counters.json')
+
+    def api_stat_traffic_summary(self, counter, date1, date2, group=None):
+        if group is None:
+            group = 'day'
+        return self.api_request('stat/traffic/summary.json?id=%s&date1=%s&date2=%s&group=%s' % (
+            counter,
+            date1.strftime('%Y%m%d'),
+            date2.strftime('%Y%m%d'),
+            group
+        ))
+
+
+class AccessTokenWidget(Widget):
+    module = None
+
+    def render(self, name, value, attrs=None):
+        if value and len(value) > 0:
+            link = '<a href="%s">%s</a>' % (
+                reverse('jet-dashboard:yandex-metrika-revoke', kwargs={'pk': self.module.model.pk}),
+                _('Revoke access')
+            )
+        else:
+            link = '<a href="%s">%s</a>' % (
+                reverse('jet-dashboard:yandex-metrika-grant', kwargs={'pk': self.module.model.pk}),
+                _('Grant access')
+            )
+
+        if value is None:
+            value = ''
+
+        return format_html('%s<input type="hidden" name="access_token" value="%s">' % (link, value))
+
+
+class YandexMetrikaSettingsForm(forms.Form):
+    access_token = forms.CharField(label=_('Access'), widget=AccessTokenWidget)
+    counter = forms.ChoiceField(label=_('Counter'))
+    period = forms.ChoiceField(label=_('Statistics period'), choices=(
+        (0, _('Today')),
+        (6, _('Last week')),
+        (30, _('Last month')),
+        (31 * 3 - 1, _('Last quarter')),
+        (364, _('Last year')),
+    ))
+
+    def set_module(self, module):
+        self.fields['access_token'].widget.module = module
+        self.set_counter_choices(module)
+
+    def set_counter_choices(self, module):
+        counters = module.counters()
+        if counters is not None:
+            self.fields['counter'].choices = (('', '-- %s --' % _('none')),)
+            self.fields['counter'].choices.extend(map(lambda x: (x['id'], x['site']), counters))
+        else:
+            label = _('grant access first') if module.access_token is None else _('counters loading failed')
+            self.fields['counter'].choices = (('', '-- %s -- ' % label),)
+
+
+class YandexMetrikaChartSettingsForm(YandexMetrikaSettingsForm):
+    show = forms.ChoiceField(label=_('Show'), choices=(
+        ('visitors', _('Visitors')),
+        ('visits', _('Visits')),
+        ('page_views', _('Views')),
+    ))
+    group = forms.ChoiceField(label=_('Group'), choices=(
+        ('day', _('By day')),
+        ('week', _('By week')),
+        ('month', _('By month')),
+    ))
+
+
+class YandexMetrikaPeriodVisitorsSettingsForm(YandexMetrikaSettingsForm):
+    group = forms.ChoiceField(label=_('Group'), choices=(
+        ('day', _('By day')),
+        ('week', _('By week')),
+        ('month', _('By month')),
+    ))
+
+
+class YandexMetrikaBase(DashboardModule):
+    settings_form = YandexMetrikaSettingsForm
+    ajax_load = True
+    contrast = True
+    period = None
+    access_token = None
+    expires_in = None
+    token_type = None
+    counter = None
+    error = None
+
+    def settings_dict(self):
+        return {
+            'period': self.period,
+            'access_token': self.access_token,
+            'expires_in': self.expires_in,
+            'token_type': self.token_type,
+            'counter': self.counter
+        }
+
+    def load_settings(self, settings):
+        try:
+            self.period = int(settings.get('period'))
+        except TypeError:
+            self.period = 0
+        self.access_token = settings.get('access_token')
+        self.expires_in = settings.get('expires_in')
+        self.token_type = settings.get('token_type')
+        self.counter = settings.get('counter')
+
+    def init_with_context(self, context):
+        raise NotImplementedError('subclasses of YandexMetrika must provide a init_with_context() method')
+
+    def counters(self):
+        client = YandexMetrikaClient(self.access_token)
+        counters, exception = client.api_counters_request()
+
+        if counters is not None:
+            return counters['counters']
+        else:
+            return None
+
+    def format_grouped_date(self, date, group):
+        if group == 'week':
+            date = u'%s — %s' % (
+                (date - datetime.timedelta(days=7)).strftime('%d.%m'),
+                date.strftime('%d.%m')
+            )
+        elif group == 'month':
+            date = date.strftime('%b, %Y')
+        else:
+            date = formats.date_format(date, 'DATE_FORMAT')
+        return date
+
+    def counter_attached(self):
+        if self.access_token is None:
+            self.error = mark_safe(_('Please <a href="%s">attach Yandex account and choose Yandex Metrika counter</a> to start using widget') % reverse('jet-dashboard:update_module', kwargs={'pk': self.model.pk}))
+            return False
+        elif self.counter is None:
+            self.error = mark_safe(_('Please <a href="%s">select Yandex Metrika counter</a> to start using widget') % reverse('jet-dashboard:update_module', kwargs={'pk': self.model.pk}))
+            return False
+        else:
+            return True
+
+    def api_stat_traffic_summary(self, group=None):
+        if self.counter_attached():
+            date1 = datetime.datetime.utcnow() - datetime.timedelta(days=self.period)
+            date2 = datetime.datetime.utcnow()
+
+            client = YandexMetrikaClient(self.access_token)
+            result, exception = client.api_stat_traffic_summary(self.counter, date1, date2, group)
+
+            if exception is not None:
+                error = _('API request failed.')
+                if isinstance(exception, HTTPError) and exception.code == 403:
+                    error += _(' Try to <a href="%s">revoke and grant access</a> again') % reverse('jet-dashboard:update_module', kwargs={'pk': self.model.pk})
+                self.error = mark_safe(error)
+            else:
+                return result
+
+
+class YandexMetrikaVisitorsTotals(YandexMetrikaBase):
+    title = _('Yandex Metrika visitors totals')
+    template = 'jet.dashboard/modules/yandex_metrika_visitors_totals.html'
+
+    def __init__(self, title=None, period=None, **kwargs):
+        kwargs.update({'period': period})
+        super(YandexMetrikaVisitorsTotals, self).__init__(title, **kwargs)
+
+    def init_with_context(self, context):
+        result = self.api_stat_traffic_summary()
+
+        if result is not None:
+            try:
+                self.children.append({'title': _('visitors'), 'value': result['totals']['visitors']})
+                self.children.append({'title': _('visits'), 'value': result['totals']['visits']})
+                self.children.append({'title': _('views'), 'value': result['totals']['page_views']})
+            except KeyError:
+                self.error = _('Bad server response')
+
+
+class YandexMetrikaVisitorsChart(YandexMetrikaBase):
+    title = _('Yandex Metrika visitors chart')
+    template = 'jet.dashboard/modules/yandex_metrika_visitors_chart.html'
+    style = 'overflow-x: auto;'
+    show = None
+    group = None
+    settings_form = YandexMetrikaChartSettingsForm
+
+    class Media:
+        js = ('jet.dashboard/vendor/chart.js/Chart.min.js', 'jet.dashboard/dashboard_modules/yandex_metrika.js')
+
+    def __init__(self, title=None, period=None, show=None, group=None, **kwargs):
+        kwargs.update({'period': period, 'show': show, 'group': group})
+        super(YandexMetrikaVisitorsChart, self).__init__(title, **kwargs)
+
+    def settings_dict(self):
+        settings = super(YandexMetrikaVisitorsChart, self).settings_dict()
+        settings['show'] = self.show
+        settings['group'] = self.group
+        return settings
+
+    def load_settings(self, settings):
+        super(YandexMetrikaVisitorsChart, self).load_settings(settings)
+        self.show = settings.get('show')
+        self.group = settings.get('group')
+
+    def init_with_context(self, context):
+        result = self.api_stat_traffic_summary(self.group)
+
+        if result is not None:
+            try:
+                for data in result['data']:
+                    date = datetime.datetime.strptime(data['date'], '%Y%m%d')
+                    key = self.show if self.show is not None else 'visitors'
+                    self.children.append((date, data[key]))
+            except KeyError:
+                self.error = _('Bad server response')
+
+
+class YandexMetrikaPeriodVisitors(YandexMetrikaBase):
+    title = _('Yandex Metrika period visitors')
+    template = 'jet.dashboard/modules/yandex_metrika_period_visitors.html'
+    group = None
+    contrast = False
+    settings_form = YandexMetrikaPeriodVisitorsSettingsForm
+
+    def __init__(self, title=None, period=None, group=None, **kwargs):
+        kwargs.update({'period': period, 'group': group})
+        super(YandexMetrikaPeriodVisitors, self).__init__(title, **kwargs)
+
+    def settings_dict(self):
+        settings = super(YandexMetrikaPeriodVisitors, self).settings_dict()
+        settings['group'] = self.group
+        return settings
+
+    def load_settings(self, settings):
+        super(YandexMetrikaPeriodVisitors, self).load_settings(settings)
+        self.group = settings.get('group')
+
+    def init_with_context(self, context):
+        result = self.api_stat_traffic_summary(self.group)
+
+        if result is not None:
+            try:
+                for data in reversed(result['data']):
+                    date = datetime.datetime.strptime(data['date'], '%Y%m%d')
+                    date = self.format_grouped_date(date, self.group)
+                    self.children.append((date, data))
+            except KeyError:
+                self.error = _('Bad server response')

+ 64 - 0
jet/dashboard/dashboard_modules/yandex_metrika_views.py

@@ -0,0 +1,64 @@
+from django.conf.urls import url
+from django.contrib import messages
+from django.core.urlresolvers import reverse
+from django.http import HttpResponse
+from django.shortcuts import redirect
+from jet.dashboard.dashboard_modules.yandex_metrika import YandexMetrikaClient
+from jet.dashboard.models import UserDashboardModule
+from jet.dashboard import dashboard
+from django.utils.translation import ugettext_lazy as _
+
+
+def yandex_metrika_grant_view(request, pk):
+    client = YandexMetrikaClient()
+    return redirect(client.get_oauth_authorize_url(pk))
+
+
+def yandex_metrika_revoke_view(request, pk):
+    try:
+        module = UserDashboardModule.objects.get(pk=pk)
+        module.pop_settings(('access_token', 'expires_in', 'token_type', 'counter'))
+        return redirect(reverse('jet-dashboard:update_module', kwargs={'pk': module.pk}))
+    except UserDashboardModule.DoesNotExist:
+        return HttpResponse(_('Module not found'))
+
+
+def yandex_metrika_callback_view(request):
+    try:
+        state = request.GET['state']
+        code = request.GET['code']
+
+        module = UserDashboardModule.objects.get(pk=state)
+
+        client = YandexMetrikaClient()
+        result, exception = client.oath_token_request(code)
+
+        if result is None:
+            messages.error(request, _('API request failed.'))
+        else:
+            module.update_settings(result)
+
+        return redirect(reverse('jet-dashboard:update_module', kwargs={'pk': module.pk}))
+    except KeyError:
+        return HttpResponse(_('Bad arguments'))
+    except UserDashboardModule.DoesNotExist:
+        return HttpResponse(_('Module not found'))
+
+
+dashboard.urls.register_urls([
+    url(
+        r'^yandex-metrika/grant/(?P<pk>\d+)/$',
+        yandex_metrika_grant_view,
+        name='yandex-metrika-grant'
+    ),
+    url(
+        r'^yandex-metrika/revoke/(?P<pk>\d+)/$',
+        yandex_metrika_revoke_view,
+        name='yandex-metrika-revoke'
+    ),
+    url(
+        r'^yandex-metrika/callback/$',
+        yandex_metrika_callback_view,
+        name='yandex-metrika-callback'
+    ),
+])

+ 132 - 0
jet/dashboard/forms.py

@@ -0,0 +1,132 @@
+import json
+from django import forms
+from django.core.exceptions import ValidationError
+from jet.dashboard.models import UserDashboardModule
+from jet.dashboard.utils import get_current_dashboard
+
+
+class UpdateDashboardModulesForm(forms.Form):
+    app_label = forms.CharField(required=False)
+    modules = forms.CharField()
+    modules_objects = []
+
+    def __init__(self, request, *args, **kwargs):
+        self.request = request
+        super(UpdateDashboardModulesForm, self).__init__(*args, **kwargs)
+
+    def clean(self):
+        data = super(UpdateDashboardModulesForm, self).clean()
+
+        try:
+            modules = json.loads(data['modules'])
+
+            for module in modules:
+                db_module = UserDashboardModule.objects.get(
+                    user=self.request.user.pk,
+                    app_label=data['app_label'] if data['app_label'] else None,
+                    pk=module['id']
+                )
+
+                column = module['column']
+                order = module['order']
+
+                if db_module.column != column or db_module.order != order:
+                    db_module.column = column
+                    db_module.order = order
+
+                    self.modules_objects.append(db_module)
+        except Exception:
+            raise ValidationError('error')
+
+        return data
+
+    def save(self):
+        for module in self.modules_objects:
+            module.save()
+
+
+class AddUserDashboardModuleForm(forms.ModelForm):
+    type = forms.CharField()
+    module = forms.IntegerField()
+    module_cls = None
+
+    def __init__(self, request, *args, **kwargs):
+        self.request = request
+        super(AddUserDashboardModuleForm, self).__init__(*args, **kwargs)
+
+    class Meta:
+        model = UserDashboardModule
+        fields = ['app_label']
+
+    def clean_app_label(self):
+        data = self.cleaned_data['app_label']
+        return data if data != '' else None
+
+    def clean(self):
+        data = super(AddUserDashboardModuleForm, self).clean()
+
+        if 'app_label' in data:
+            index_dashboard_cls = get_current_dashboard('app_index' if data['app_label'] else 'index')
+            index_dashboard = index_dashboard_cls({'request': self.request}, app_label=data['app_label'])
+
+            if 'type' in data:
+                if data['type'] == 'children':
+                    module = index_dashboard.children[data['module']]
+                elif data['type'] == 'available_children':
+                    module = index_dashboard.available_children[data['module']]()
+                else:
+                    raise ValidationError('error')
+
+                self.module_cls = module
+        return data
+
+    def save(self, commit=True):
+        self.instance.title = self.module_cls.title
+        self.instance.module = self.module_cls.fullname()
+        self.instance.user = self.request.user.pk
+        self.instance.column = 0
+        self.instance.order = -1
+        self.instance.settings = self.module_cls.dump_settings()
+        self.instance.children = self.module_cls.dump_children()
+
+        return super(AddUserDashboardModuleForm, self).save(commit)
+
+
+class UpdateDashboardModuleCollapseForm(forms.ModelForm):
+    def __init__(self, request, *args, **kwargs):
+        self.request = request
+        super(UpdateDashboardModuleCollapseForm, self).__init__(*args, **kwargs)
+
+    class Meta:
+        model = UserDashboardModule
+        fields = ['collapsed']
+
+    def clean(self):
+        data = super(UpdateDashboardModuleCollapseForm, self).clean()
+
+        if self.instance.user != self.request.user.pk:
+            raise ValidationError('error')
+
+        return data
+
+
+class RemoveDashboardModuleForm(forms.ModelForm):
+    def __init__(self, request, *args, **kwargs):
+        self.request = request
+        super(RemoveDashboardModuleForm, self).__init__(*args, **kwargs)
+
+    class Meta:
+        model = UserDashboardModule
+        fields = []
+
+    def clean(self):
+        cleaned_data = super(RemoveDashboardModuleForm, self).clean()
+
+        if self.instance.user != self.request.user.pk:
+            raise ValidationError('error')
+
+        return cleaned_data
+
+    def save(self, commit=True):
+        if commit:
+            self.instance.delete()

+ 33 - 0
jet/dashboard/migrations/0001_initial.py

@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='UserDashboardModule',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)),
+                ('title', models.CharField(verbose_name='Title', max_length=255)),
+                ('module', models.CharField(verbose_name='module', max_length=255)),
+                ('app_label', models.CharField(verbose_name='application name', max_length=255, blank=True, null=True)),
+                ('user', models.PositiveIntegerField(verbose_name='user')),
+                ('column', models.PositiveIntegerField(verbose_name='column')),
+                ('order', models.IntegerField(verbose_name='order')),
+                ('settings', models.TextField(verbose_name='settings', blank=True, default='')),
+                ('children', models.TextField(verbose_name='children', blank=True, default='')),
+                ('collapsed', models.BooleanField(verbose_name='collapsed', default=False)),
+            ],
+            options={
+                'verbose_name': 'user dashboard module',
+                'verbose_name_plural': 'user dashboard modules',
+                'ordering': ('column', 'order'),
+            },
+        ),
+    ]

+ 0 - 0
jet/dashboard/migrations/__init__.py


+ 57 - 0
jet/dashboard/models.py

@@ -0,0 +1,57 @@
+from importlib import import_module
+import json
+from django.db import models
+from django.utils.encoding import python_2_unicode_compatible
+from django.utils.translation import ugettext_lazy as _
+from jet.utils import LazyDateTimeEncoder
+
+
+@python_2_unicode_compatible
+class UserDashboardModule(models.Model):
+    title = models.CharField(verbose_name=_('Title'), max_length=255)
+    module = models.CharField(verbose_name=_('module'), max_length=255)
+    app_label = models.CharField(verbose_name=_('application name'), max_length=255, null=True, blank=True)
+    user = models.PositiveIntegerField(verbose_name=_('user'))
+    column = models.PositiveIntegerField(verbose_name=_('column'))
+    order = models.IntegerField(verbose_name=_('order'))
+    settings = models.TextField(verbose_name=_('settings'), default='', blank=True)
+    children = models.TextField(verbose_name=_('children'), default='', blank=True)
+    collapsed = models.BooleanField(verbose_name=_('collapsed'), default=False)
+
+    class Meta:
+        verbose_name = _('user dashboard module')
+        verbose_name_plural = _('user dashboard modules')
+        ordering = ('column', 'order')
+
+    def __str__(self):
+        return self.module
+
+    def load_module(self):
+        try:
+            package, module_name = self.module.rsplit('.', 1)
+            package = import_module(package)
+            module = getattr(package, module_name)
+
+            return module
+        except AttributeError:
+            return None
+
+    def pop_settings(self, pop_settings):
+        settings = json.loads(self.settings)
+
+        for setting in pop_settings:
+            if setting in settings:
+                settings.pop(setting)
+
+        self.settings = json.dumps(settings, cls=LazyDateTimeEncoder)
+        self.save()
+
+    def update_settings(self, update_settings):
+        settings = json.loads(self.settings)
+
+        settings.update(update_settings)
+
+        self.settings = json.dumps(settings, cls=LazyDateTimeEncoder)
+        self.save()
+
+

+ 11 - 9
jet/modules.py → jet/dashboard/modules.py

@@ -4,12 +4,12 @@ from django.contrib.admin.models import LogEntry
 from django.db.models import Q
 from django.template.loader import render_to_string
 from django.utils.translation import ugettext_lazy as _
-from jet.utils import get_app_list, LazyEncoder
+from jet.utils import get_app_list, LazyDateTimeEncoder
 import datetime
 
 
 class DashboardModule(object):
-    template = 'jet/dashboard/module.html'
+    template = 'jet.dashboard/module.html'
     enabled = True
     draggable = True
     collapsible = True
@@ -29,6 +29,8 @@ class DashboardModule(object):
     column = None
     order = None
     ajax_load = False
+    contrast = False
+    style = False
 
     class Media:
         css = ()
@@ -67,13 +69,13 @@ class DashboardModule(object):
     def dump_settings(self, settings=None):
         settings = settings or self.settings_dict()
         if settings:
-            return json.dumps(settings, cls=LazyEncoder)
+            return json.dumps(settings, cls=LazyDateTimeEncoder)
         else:
             return ''
 
     def dump_children(self):
         if self.store_children():
-            return json.dumps(self.children, cls=LazyEncoder)
+            return json.dumps(self.children, cls=LazyDateTimeEncoder)
         else:
             return ''
 
@@ -121,7 +123,7 @@ class LinkListSettingsForm(forms.Form):
 
 class LinkList(DashboardModule):
     title = _('Links')
-    template = 'jet/dashboard/modules/link_list.html'
+    template = 'jet.dashboard/modules/link_list.html'
     layout = 'stacked'
     settings_form = LinkListSettingsForm
     child_form = LinkListItemForm
@@ -156,7 +158,7 @@ class LinkList(DashboardModule):
 
 class AppList(DashboardModule):
     title = _('Applications')
-    template = 'jet/dashboard/modules/app_list.html'
+    template = 'jet.dashboard/modules/app_list.html'
     models = None
     exclude = None
     hide_empty = True
@@ -197,7 +199,7 @@ class AppList(DashboardModule):
 
 class ModelList(DashboardModule):
     title = _('Models')
-    template = 'jet/dashboard/modules/model_list.html'
+    template = 'jet.dashboard/modules/model_list.html'
     models = None
     exclude = None
     hide_empty = True
@@ -238,7 +240,7 @@ class RecentActionsSettingsForm(forms.Form):
 
 class RecentActions(DashboardModule):
     title = _('Recent Actions')
-    template = 'jet/dashboard/modules/recent_actions.html'
+    template = 'jet.dashboard/modules/recent_actions.html'
     limit = 10
     include_list = None
     exclude_list = None
@@ -310,7 +312,7 @@ class FeedSettingsForm(forms.Form):
 
 class Feed(DashboardModule):
     title = _('RSS Feed')
-    template = 'jet/dashboard/modules/feed.html'
+    template = 'jet.dashboard/modules/feed.html'
     feed_url = None
     limit = None
     settings_form = FeedSettingsForm

+ 5 - 0
jet/dashboard/settings.py

@@ -0,0 +1,5 @@
+from django.conf import settings
+
+# Dashboard
+JET_INDEX_DASHBOARD = getattr(settings, 'JET_INDEX_DASHBOARD', 'jet.dashboard.dashboard.DefaultIndexDashboard')
+JET_APP_INDEX_DASHBOARD = getattr(settings, 'JET_APP_INDEX_DASHBOARD', 'jet.dashboard.dashboard.DefaultAppIndexDashboard')

+ 48 - 0
jet/dashboard/south_migrations/0001_initial.py

@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+from south.utils import datetime_utils as datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding model 'UserDashboardModule'
+        db.create_table(u'dashboard_userdashboardmodule', (
+            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('title', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('module', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('app_label', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
+            ('user', self.gf('django.db.models.fields.PositiveIntegerField')()),
+            ('column', self.gf('django.db.models.fields.PositiveIntegerField')()),
+            ('order', self.gf('django.db.models.fields.IntegerField')()),
+            ('settings', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
+            ('children', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
+            ('collapsed', self.gf('django.db.models.fields.BooleanField')(default=False)),
+        ))
+        db.send_create_signal(u'dashboard', ['UserDashboardModule'])
+
+
+    def backwards(self, orm):
+        # Deleting model 'UserDashboardModule'
+        db.delete_table(u'dashboard_userdashboardmodule')
+
+
+    models = {
+        u'dashboard.userdashboardmodule': {
+            'Meta': {'ordering': "('column', 'order')", 'object_name': 'UserDashboardModule'},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'children': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+            'collapsed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'column': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'module': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'order': ('django.db.models.fields.IntegerField', [], {}),
+            'settings': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user': ('django.db.models.fields.PositiveIntegerField', [], {})
+        }
+    }
+
+    complete_apps = ['dashboard']

+ 0 - 0
jet/dashboard/south_migrations/__init__.py


+ 35 - 0
jet/dashboard/static/jet.dashboard/dashboard_modules/google_analytics.js

@@ -0,0 +1,35 @@
+(function ($) {
+    $.fn.extend( {
+        googleAnalyticsChart: function() {
+            var $chart = $(this);
+            var ctx = $chart.get(0).getContext("2d");
+            var $data = $chart.find('.chart-data');
+            var $dataItems = $data.find('.chart-data-item');
+            var labels = [];
+            var data = [];
+
+            $dataItems.each(function() {
+                labels.push($(this).data('date'));
+                data.push($(this).data('value'));
+            });
+
+            new Chart(ctx).Line({
+                labels: labels,
+                datasets: [
+                    {
+                        fillColor: $chart.find('.chart-fillColor').css('color'),
+                        strokeColor: $chart.find('.chart-strokeColor').css('color'),
+                        pointColor: $chart.find('.chart-pointColor').css('color'),
+                        pointHighlightFill: $chart.find('.chart-pointHighlightFill').css('color'),
+                        responsive: true,
+                        data: data
+                    }
+                ]
+            }, {
+                scaleGridLineColor: $chart.find('.chart-scaleGridLineColor').css('color'),
+                scaleLineColor: $chart.find('.chart-scaleLineColor').css('color'),
+                scaleFontColor: $chart.find('.chart-scaleFontColor').css('color')
+            });
+        }
+    });
+})(jet.jQuery);

+ 35 - 0
jet/dashboard/static/jet.dashboard/dashboard_modules/yandex_metrika.js

@@ -0,0 +1,35 @@
+(function ($) {
+    $.fn.extend( {
+        yandexMetrikaChart: function() {
+            var $chart = $(this);
+            var ctx = $chart.get(0).getContext("2d");
+            var $data = $chart.find('.chart-data');
+            var $dataItems = $data.find('.chart-data-item');
+            var labels = [];
+            var data = [];
+
+            $dataItems.each(function() {
+                labels.push($(this).data('date'));
+                data.push($(this).data('value'));
+            });
+
+            new Chart(ctx).Line({
+                labels: labels,
+                datasets: [
+                    {
+                        fillColor: $chart.find('.chart-fillColor').css('color'),
+                        strokeColor: $chart.find('.chart-strokeColor').css('color'),
+                        pointColor: $chart.find('.chart-pointColor').css('color'),
+                        pointHighlightFill: $chart.find('.chart-pointHighlightFill').css('color'),
+                        responsive: true,
+                        data: data
+                    }
+                ]
+            }, {
+                scaleGridLineColor: $chart.find('.chart-scaleGridLineColor').css('color'),
+                scaleLineColor: $chart.find('.chart-scaleLineColor').css('color'),
+                scaleFontColor: $chart.find('.chart-scaleFontColor').css('color')
+            });
+        }
+    });
+})(jet.jQuery);

+ 55 - 0
jet/dashboard/static/jet.dashboard/vendor/chart.js/CONTRIBUTING.md

@@ -0,0 +1,55 @@
+Contributing to Chart.js
+========================
+
+Contributions to Chart.js are welcome and encouraged, but please have a look through the guidelines in this document before raising an issue, or writing code for the project.
+
+
+Using issues
+------------
+
+The [issue tracker](https://github.com/nnnick/Chart.js/issues) is the preferred channel for reporting bugs, requesting new features and submitting pull requests.
+
+If you're suggesting a new chart type, please take a look at [writing new chart types](https://github.com/nnnick/Chart.js/blob/master/docs/06-Advanced.md#writing-new-chart-types) in the documentation, and some of the [community extensions](https://github.com/nnnick/Chart.js/blob/master/docs/06-Advanced.md#community-extensions) that have been created already.
+
+To keep the library lightweight for everyone, it's unlikely we'll add many more chart types to the core of Chart.js, but issues are a good medium to design and spec out how new chart types could work and look.
+
+Please do not use issues for support requests. For help using Chart.js, please take a look at the [`chartjs`](http://stackoverflow.com/questions/tagged/chartjs) tag on Stack Overflow.
+
+
+Reporting bugs
+--------------
+
+Well structured, detailed bug reports are hugely valuable for the project.
+
+Guidlines for reporting bugs:
+
+ - Check the issue search to see if it has already been reported
+ - Isolate the problem to a simple test case
+ - Provide a demonstration of the problem on [JS Bin](http://jsbin.com) or similar
+
+Please provide any additional details associated with the bug, if it's browser or screen density specific, or only happens with a certain configuration or data.
+
+
+Pull requests
+-------------
+
+Clear, concise pull requests are excellent at continuing the project's community driven growth. But please review [these guidelines](https://github.com/blog/1943-how-to-write-the-perfect-pull-request) and the guidelines below before starting work on the project.
+
+Guidlines:
+
+ - Please create an issue first:
+   - For bugs, we can discuss the fixing approach
+   - For enhancements, we can discuss if it is within the project scope and avoid duplicate effort
+ - Please make changes to the files in [`/src`](https://github.com/nnnick/Chart.js/tree/master/src), not `Chart.js` or `Chart.min.js` in the repo root directory, this avoids merge conflicts
+ - Tabs for indentation, not spaces please
+ - If adding new functionality, please also update the relevant `.md` file in [`/docs`](https://github.com/nnnick/Chart.js/tree/master/docs)
+ - Please make your commits in logical sections with clear commit messages
+
+Joining the Project
+-------------
+ - Active committers and contributors are invited to introduce yourself and request commit access to this project.  Please send an email to hello@nickdownie.com or file an issue. 
+
+License
+-------
+
+By contributing your code, you agree to license your contribution under the [MIT license](https://github.com/nnnick/Chart.js/blob/master/LICENSE.md).

+ 3477 - 0
jet/dashboard/static/jet.dashboard/vendor/chart.js/Chart.js

@@ -0,0 +1,3477 @@
+/*!
+ * Chart.js
+ * http://chartjs.org/
+ * Version: 1.0.2
+ *
+ * Copyright 2015 Nick Downie
+ * Released under the MIT license
+ * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md
+ */
+
+
+(function(){
+
+	"use strict";
+
+	//Declare root variable - window in the browser, global on the server
+	var root = this,
+		previous = root.Chart;
+
+	//Occupy the global variable of Chart, and create a simple base class
+	var Chart = function(context){
+		var chart = this;
+		this.canvas = context.canvas;
+
+		this.ctx = context;
+
+		//Variables global to the chart
+		var computeDimension = function(element,dimension)
+		{
+			if (element['offset'+dimension])
+			{
+				return element['offset'+dimension];
+			}
+			else
+			{
+				return document.defaultView.getComputedStyle(element).getPropertyValue(dimension);
+			}
+		}
+
+		var width = this.width = computeDimension(context.canvas,'Width');
+		var height = this.height = computeDimension(context.canvas,'Height');
+
+		// Firefox requires this to work correctly
+		context.canvas.width  = width;
+		context.canvas.height = height;
+
+		var width = this.width = context.canvas.width;
+		var height = this.height = context.canvas.height;
+		this.aspectRatio = this.width / this.height;
+		//High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale.
+		helpers.retinaScale(this);
+
+		return this;
+	};
+	//Globally expose the defaults to allow for user updating/changing
+	Chart.defaults = {
+		global: {
+			// Boolean - Whether to animate the chart
+			animation: true,
+
+			// Number - Number of animation steps
+			animationSteps: 60,
+
+			// String - Animation easing effect
+			animationEasing: "easeOutQuart",
+
+			// Boolean - If we should show the scale at all
+			showScale: true,
+
+			// Boolean - If we want to override with a hard coded scale
+			scaleOverride: false,
+
+			// ** Required if scaleOverride is true **
+			// Number - The number of steps in a hard coded scale
+			scaleSteps: null,
+			// Number - The value jump in the hard coded scale
+			scaleStepWidth: null,
+			// Number - The scale starting value
+			scaleStartValue: null,
+
+			// String - Colour of the scale line
+			scaleLineColor: "rgba(0,0,0,.1)",
+
+			// Number - Pixel width of the scale line
+			scaleLineWidth: 1,
+
+			// Boolean - Whether to show labels on the scale
+			scaleShowLabels: true,
+
+			// Interpolated JS string - can access value
+			scaleLabel: "<%=value%>",
+
+			// Boolean - Whether the scale should stick to integers, and not show any floats even if drawing space is there
+			scaleIntegersOnly: true,
+
+			// Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value
+			scaleBeginAtZero: false,
+
+			// String - Scale label font declaration for the scale label
+			scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
+
+			// Number - Scale label font size in pixels
+			scaleFontSize: 12,
+
+			// String - Scale label font weight style
+			scaleFontStyle: "normal",
+
+			// String - Scale label font colour
+			scaleFontColor: "#666",
+
+			// Boolean - whether or not the chart should be responsive and resize when the browser does.
+			responsive: false,
+
+			// Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container
+			maintainAspectRatio: true,
+
+			// Boolean - Determines whether to draw tooltips on the canvas or not - attaches events to touchmove & mousemove
+			showTooltips: true,
+
+			// Boolean - Determines whether to draw built-in tooltip or call custom tooltip function
+			customTooltips: false,
+
+			// Array - Array of string names to attach tooltip events
+			tooltipEvents: ["mousemove", "touchstart", "touchmove", "mouseout"],
+
+			// String - Tooltip background colour
+			tooltipFillColor: "rgba(0,0,0,0.8)",
+
+			// String - Tooltip label font declaration for the scale label
+			tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
+
+			// Number - Tooltip label font size in pixels
+			tooltipFontSize: 14,
+
+			// String - Tooltip font weight style
+			tooltipFontStyle: "normal",
+
+			// String - Tooltip label font colour
+			tooltipFontColor: "#fff",
+
+			// String - Tooltip title font declaration for the scale label
+			tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
+
+			// Number - Tooltip title font size in pixels
+			tooltipTitleFontSize: 14,
+
+			// String - Tooltip title font weight style
+			tooltipTitleFontStyle: "bold",
+
+			// String - Tooltip title font colour
+			tooltipTitleFontColor: "#fff",
+
+			// Number - pixel width of padding around tooltip text
+			tooltipYPadding: 6,
+
+			// Number - pixel width of padding around tooltip text
+			tooltipXPadding: 6,
+
+			// Number - Size of the caret on the tooltip
+			tooltipCaretSize: 8,
+
+			// Number - Pixel radius of the tooltip border
+			tooltipCornerRadius: 6,
+
+			// Number - Pixel offset from point x to tooltip edge
+			tooltipXOffset: 10,
+
+			// String - Template string for single tooltips
+			tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>",
+
+			// String - Template string for single tooltips
+			multiTooltipTemplate: "<%= value %>",
+
+			// String - Colour behind the legend colour block
+			multiTooltipKeyBackground: '#fff',
+
+			// Function - Will fire on animation progression.
+			onAnimationProgress: function(){},
+
+			// Function - Will fire on animation completion.
+			onAnimationComplete: function(){}
+
+		}
+	};
+
+	//Create a dictionary of chart types, to allow for extension of existing types
+	Chart.types = {};
+
+	//Global Chart helpers object for utility methods and classes
+	var helpers = Chart.helpers = {};
+
+		//-- Basic js utility methods
+	var each = helpers.each = function(loopable,callback,self){
+			var additionalArgs = Array.prototype.slice.call(arguments, 3);
+			// Check to see if null or undefined firstly.
+			if (loopable){
+				if (loopable.length === +loopable.length){
+					var i;
+					for (i=0; i<loopable.length; i++){
+						callback.apply(self,[loopable[i], i].concat(additionalArgs));
+					}
+				}
+				else{
+					for (var item in loopable){
+						callback.apply(self,[loopable[item],item].concat(additionalArgs));
+					}
+				}
+			}
+		},
+		clone = helpers.clone = function(obj){
+			var objClone = {};
+			each(obj,function(value,key){
+				if (obj.hasOwnProperty(key)) objClone[key] = value;
+			});
+			return objClone;
+		},
+		extend = helpers.extend = function(base){
+			each(Array.prototype.slice.call(arguments,1), function(extensionObject) {
+				each(extensionObject,function(value,key){
+					if (extensionObject.hasOwnProperty(key)) base[key] = value;
+				});
+			});
+			return base;
+		},
+		merge = helpers.merge = function(base,master){
+			//Merge properties in left object over to a shallow clone of object right.
+			var args = Array.prototype.slice.call(arguments,0);
+			args.unshift({});
+			return extend.apply(null, args);
+		},
+		indexOf = helpers.indexOf = function(arrayToSearch, item){
+			if (Array.prototype.indexOf) {
+				return arrayToSearch.indexOf(item);
+			}
+			else{
+				for (var i = 0; i < arrayToSearch.length; i++) {
+					if (arrayToSearch[i] === item) return i;
+				}
+				return -1;
+			}
+		},
+		where = helpers.where = function(collection, filterCallback){
+			var filtered = [];
+
+			helpers.each(collection, function(item){
+				if (filterCallback(item)){
+					filtered.push(item);
+				}
+			});
+
+			return filtered;
+		},
+		findNextWhere = helpers.findNextWhere = function(arrayToSearch, filterCallback, startIndex){
+			// Default to start of the array
+			if (!startIndex){
+				startIndex = -1;
+			}
+			for (var i = startIndex + 1; i < arrayToSearch.length; i++) {
+				var currentItem = arrayToSearch[i];
+				if (filterCallback(currentItem)){
+					return currentItem;
+				}
+			}
+		},
+		findPreviousWhere = helpers.findPreviousWhere = function(arrayToSearch, filterCallback, startIndex){
+			// Default to end of the array
+			if (!startIndex){
+				startIndex = arrayToSearch.length;
+			}
+			for (var i = startIndex - 1; i >= 0; i--) {
+				var currentItem = arrayToSearch[i];
+				if (filterCallback(currentItem)){
+					return currentItem;
+				}
+			}
+		},
+		inherits = helpers.inherits = function(extensions){
+			//Basic javascript inheritance based on the model created in Backbone.js
+			var parent = this;
+			var ChartElement = (extensions && extensions.hasOwnProperty("constructor")) ? extensions.constructor : function(){ return parent.apply(this, arguments); };
+
+			var Surrogate = function(){ this.constructor = ChartElement;};
+			Surrogate.prototype = parent.prototype;
+			ChartElement.prototype = new Surrogate();
+
+			ChartElement.extend = inherits;
+
+			if (extensions) extend(ChartElement.prototype, extensions);
+
+			ChartElement.__super__ = parent.prototype;
+
+			return ChartElement;
+		},
+		noop = helpers.noop = function(){},
+		uid = helpers.uid = (function(){
+			var id=0;
+			return function(){
+				return "chart-" + id++;
+			};
+		})(),
+		warn = helpers.warn = function(str){
+			//Method for warning of errors
+			if (window.console && typeof window.console.warn == "function") console.warn(str);
+		},
+		amd = helpers.amd = (typeof define == 'function' && define.amd),
+		//-- Math methods
+		isNumber = helpers.isNumber = function(n){
+			return !isNaN(parseFloat(n)) && isFinite(n);
+		},
+		max = helpers.max = function(array){
+			return Math.max.apply( Math, array );
+		},
+		min = helpers.min = function(array){
+			return Math.min.apply( Math, array );
+		},
+		cap = helpers.cap = function(valueToCap,maxValue,minValue){
+			if(isNumber(maxValue)) {
+				if( valueToCap > maxValue ) {
+					return maxValue;
+				}
+			}
+			else if(isNumber(minValue)){
+				if ( valueToCap < minValue ){
+					return minValue;
+				}
+			}
+			return valueToCap;
+		},
+		getDecimalPlaces = helpers.getDecimalPlaces = function(num){
+			if (num%1!==0 && isNumber(num)){
+				return num.toString().split(".")[1].length;
+			}
+			else {
+				return 0;
+			}
+		},
+		toRadians = helpers.radians = function(degrees){
+			return degrees * (Math.PI/180);
+		},
+		// Gets the angle from vertical upright to the point about a centre.
+		getAngleFromPoint = helpers.getAngleFromPoint = function(centrePoint, anglePoint){
+			var distanceFromXCenter = anglePoint.x - centrePoint.x,
+				distanceFromYCenter = anglePoint.y - centrePoint.y,
+				radialDistanceFromCenter = Math.sqrt( distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter);
+
+
+			var angle = Math.PI * 2 + Math.atan2(distanceFromYCenter, distanceFromXCenter);
+
+			//If the segment is in the top left quadrant, we need to add another rotation to the angle
+			if (distanceFromXCenter < 0 && distanceFromYCenter < 0){
+				angle += Math.PI*2;
+			}
+
+			return {
+				angle: angle,
+				distance: radialDistanceFromCenter
+			};
+		},
+		aliasPixel = helpers.aliasPixel = function(pixelWidth){
+			return (pixelWidth % 2 === 0) ? 0 : 0.5;
+		},
+		splineCurve = helpers.splineCurve = function(FirstPoint,MiddlePoint,AfterPoint,t){
+			//Props to Rob Spencer at scaled innovation for his post on splining between points
+			//http://scaledinnovation.com/analytics/splines/aboutSplines.html
+			var d01=Math.sqrt(Math.pow(MiddlePoint.x-FirstPoint.x,2)+Math.pow(MiddlePoint.y-FirstPoint.y,2)),
+				d12=Math.sqrt(Math.pow(AfterPoint.x-MiddlePoint.x,2)+Math.pow(AfterPoint.y-MiddlePoint.y,2)),
+				fa=t*d01/(d01+d12),// scaling factor for triangle Ta
+				fb=t*d12/(d01+d12);
+			return {
+				inner : {
+					x : MiddlePoint.x-fa*(AfterPoint.x-FirstPoint.x),
+					y : MiddlePoint.y-fa*(AfterPoint.y-FirstPoint.y)
+				},
+				outer : {
+					x: MiddlePoint.x+fb*(AfterPoint.x-FirstPoint.x),
+					y : MiddlePoint.y+fb*(AfterPoint.y-FirstPoint.y)
+				}
+			};
+		},
+		calculateOrderOfMagnitude = helpers.calculateOrderOfMagnitude = function(val){
+			return Math.floor(Math.log(val) / Math.LN10);
+		},
+		calculateScaleRange = helpers.calculateScaleRange = function(valuesArray, drawingSize, textSize, startFromZero, integersOnly){
+
+			//Set a minimum step of two - a point at the top of the graph, and a point at the base
+			var minSteps = 2,
+				maxSteps = Math.floor(drawingSize/(textSize * 1.5)),
+				skipFitting = (minSteps >= maxSteps);
+
+			var maxValue = max(valuesArray),
+				minValue = min(valuesArray);
+
+			// We need some degree of seperation here to calculate the scales if all the values are the same
+			// Adding/minusing 0.5 will give us a range of 1.
+			if (maxValue === minValue){
+				maxValue += 0.5;
+				// So we don't end up with a graph with a negative start value if we've said always start from zero
+				if (minValue >= 0.5 && !startFromZero){
+					minValue -= 0.5;
+				}
+				else{
+					// Make up a whole number above the values
+					maxValue += 0.5;
+				}
+			}
+
+			var	valueRange = Math.abs(maxValue - minValue),
+				rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange),
+				graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude),
+				graphMin = (startFromZero) ? 0 : Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude),
+				graphRange = graphMax - graphMin,
+				stepValue = Math.pow(10, rangeOrderOfMagnitude),
+				numberOfSteps = Math.round(graphRange / stepValue);
+
+			//If we have more space on the graph we'll use it to give more definition to the data
+			while((numberOfSteps > maxSteps || (numberOfSteps * 2) < maxSteps) && !skipFitting) {
+				if(numberOfSteps > maxSteps){
+					stepValue *=2;
+					numberOfSteps = Math.round(graphRange/stepValue);
+					// Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps.
+					if (numberOfSteps % 1 !== 0){
+						skipFitting = true;
+					}
+				}
+				//We can fit in double the amount of scale points on the scale
+				else{
+					//If user has declared ints only, and the step value isn't a decimal
+					if (integersOnly && rangeOrderOfMagnitude >= 0){
+						//If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float
+						if(stepValue/2 % 1 === 0){
+							stepValue /=2;
+							numberOfSteps = Math.round(graphRange/stepValue);
+						}
+						//If it would make it a float break out of the loop
+						else{
+							break;
+						}
+					}
+					//If the scale doesn't have to be an int, make the scale more granular anyway.
+					else{
+						stepValue /=2;
+						numberOfSteps = Math.round(graphRange/stepValue);
+					}
+
+				}
+			}
+
+			if (skipFitting){
+				numberOfSteps = minSteps;
+				stepValue = graphRange / numberOfSteps;
+			}
+
+			return {
+				steps : numberOfSteps,
+				stepValue : stepValue,
+				min : graphMin,
+				max	: graphMin + (numberOfSteps * stepValue)
+			};
+
+		},
+		/* jshint ignore:start */
+		// Blows up jshint errors based on the new Function constructor
+		//Templating methods
+		//Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/
+		template = helpers.template = function(templateString, valuesObject){
+
+			// If templateString is function rather than string-template - call the function for valuesObject
+
+			if(templateString instanceof Function){
+			 	return templateString(valuesObject);
+		 	}
+
+			var cache = {};
+			function tmpl(str, data){
+				// Figure out if we're getting a template, or if we need to
+				// load the template - and be sure to cache the result.
+				var fn = !/\W/.test(str) ?
+				cache[str] = cache[str] :
+
+				// Generate a reusable function that will serve as a template
+				// generator (and which will be cached).
+				new Function("obj",
+					"var p=[],print=function(){p.push.apply(p,arguments);};" +
+
+					// Introduce the data as local variables using with(){}
+					"with(obj){p.push('" +
+
+					// Convert the template into pure JavaScript
+					str
+						.replace(/[\r\t\n]/g, " ")
+						.split("<%").join("\t")
+						.replace(/((^|%>)[^\t]*)'/g, "$1\r")
+						.replace(/\t=(.*?)%>/g, "',$1,'")
+						.split("\t").join("');")
+						.split("%>").join("p.push('")
+						.split("\r").join("\\'") +
+					"');}return p.join('');"
+				);
+
+				// Provide some basic currying to the user
+				return data ? fn( data ) : fn;
+			}
+			return tmpl(templateString,valuesObject);
+		},
+		/* jshint ignore:end */
+		generateLabels = helpers.generateLabels = function(templateString,numberOfSteps,graphMin,stepValue){
+			var labelsArray = new Array(numberOfSteps);
+			if (labelTemplateString){
+				each(labelsArray,function(val,index){
+					labelsArray[index] = template(templateString,{value: (graphMin + (stepValue*(index+1)))});
+				});
+			}
+			return labelsArray;
+		},
+		//--Animation methods
+		//Easing functions adapted from Robert Penner's easing equations
+		//http://www.robertpenner.com/easing/
+		easingEffects = helpers.easingEffects = {
+			linear: function (t) {
+				return t;
+			},
+			easeInQuad: function (t) {
+				return t * t;
+			},
+			easeOutQuad: function (t) {
+				return -1 * t * (t - 2);
+			},
+			easeInOutQuad: function (t) {
+				if ((t /= 1 / 2) < 1) return 1 / 2 * t * t;
+				return -1 / 2 * ((--t) * (t - 2) - 1);
+			},
+			easeInCubic: function (t) {
+				return t * t * t;
+			},
+			easeOutCubic: function (t) {
+				return 1 * ((t = t / 1 - 1) * t * t + 1);
+			},
+			easeInOutCubic: function (t) {
+				if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t;
+				return 1 / 2 * ((t -= 2) * t * t + 2);
+			},
+			easeInQuart: function (t) {
+				return t * t * t * t;
+			},
+			easeOutQuart: function (t) {
+				return -1 * ((t = t / 1 - 1) * t * t * t - 1);
+			},
+			easeInOutQuart: function (t) {
+				if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t;
+				return -1 / 2 * ((t -= 2) * t * t * t - 2);
+			},
+			easeInQuint: function (t) {
+				return 1 * (t /= 1) * t * t * t * t;
+			},
+			easeOutQuint: function (t) {
+				return 1 * ((t = t / 1 - 1) * t * t * t * t + 1);
+			},
+			easeInOutQuint: function (t) {
+				if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t * t;
+				return 1 / 2 * ((t -= 2) * t * t * t * t + 2);
+			},
+			easeInSine: function (t) {
+				return -1 * Math.cos(t / 1 * (Math.PI / 2)) + 1;
+			},
+			easeOutSine: function (t) {
+				return 1 * Math.sin(t / 1 * (Math.PI / 2));
+			},
+			easeInOutSine: function (t) {
+				return -1 / 2 * (Math.cos(Math.PI * t / 1) - 1);
+			},
+			easeInExpo: function (t) {
+				return (t === 0) ? 1 : 1 * Math.pow(2, 10 * (t / 1 - 1));
+			},
+			easeOutExpo: function (t) {
+				return (t === 1) ? 1 : 1 * (-Math.pow(2, -10 * t / 1) + 1);
+			},
+			easeInOutExpo: function (t) {
+				if (t === 0) return 0;
+				if (t === 1) return 1;
+				if ((t /= 1 / 2) < 1) return 1 / 2 * Math.pow(2, 10 * (t - 1));
+				return 1 / 2 * (-Math.pow(2, -10 * --t) + 2);
+			},
+			easeInCirc: function (t) {
+				if (t >= 1) return t;
+				return -1 * (Math.sqrt(1 - (t /= 1) * t) - 1);
+			},
+			easeOutCirc: function (t) {
+				return 1 * Math.sqrt(1 - (t = t / 1 - 1) * t);
+			},
+			easeInOutCirc: function (t) {
+				if ((t /= 1 / 2) < 1) return -1 / 2 * (Math.sqrt(1 - t * t) - 1);
+				return 1 / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1);
+			},
+			easeInElastic: function (t) {
+				var s = 1.70158;
+				var p = 0;
+				var a = 1;
+				if (t === 0) return 0;
+				if ((t /= 1) == 1) return 1;
+				if (!p) p = 1 * 0.3;
+				if (a < Math.abs(1)) {
+					a = 1;
+					s = p / 4;
+				} else s = p / (2 * Math.PI) * Math.asin(1 / a);
+				return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p));
+			},
+			easeOutElastic: function (t) {
+				var s = 1.70158;
+				var p = 0;
+				var a = 1;
+				if (t === 0) return 0;
+				if ((t /= 1) == 1) return 1;
+				if (!p) p = 1 * 0.3;
+				if (a < Math.abs(1)) {
+					a = 1;
+					s = p / 4;
+				} else s = p / (2 * Math.PI) * Math.asin(1 / a);
+				return a * Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1;
+			},
+			easeInOutElastic: function (t) {
+				var s = 1.70158;
+				var p = 0;
+				var a = 1;
+				if (t === 0) return 0;
+				if ((t /= 1 / 2) == 2) return 1;
+				if (!p) p = 1 * (0.3 * 1.5);
+				if (a < Math.abs(1)) {
+					a = 1;
+					s = p / 4;
+				} else s = p / (2 * Math.PI) * Math.asin(1 / a);
+				if (t < 1) return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p));
+				return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1;
+			},
+			easeInBack: function (t) {
+				var s = 1.70158;
+				return 1 * (t /= 1) * t * ((s + 1) * t - s);
+			},
+			easeOutBack: function (t) {
+				var s = 1.70158;
+				return 1 * ((t = t / 1 - 1) * t * ((s + 1) * t + s) + 1);
+			},
+			easeInOutBack: function (t) {
+				var s = 1.70158;
+				if ((t /= 1 / 2) < 1) return 1 / 2 * (t * t * (((s *= (1.525)) + 1) * t - s));
+				return 1 / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2);
+			},
+			easeInBounce: function (t) {
+				return 1 - easingEffects.easeOutBounce(1 - t);
+			},
+			easeOutBounce: function (t) {
+				if ((t /= 1) < (1 / 2.75)) {
+					return 1 * (7.5625 * t * t);
+				} else if (t < (2 / 2.75)) {
+					return 1 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75);
+				} else if (t < (2.5 / 2.75)) {
+					return 1 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375);
+				} else {
+					return 1 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375);
+				}
+			},
+			easeInOutBounce: function (t) {
+				if (t < 1 / 2) return easingEffects.easeInBounce(t * 2) * 0.5;
+				return easingEffects.easeOutBounce(t * 2 - 1) * 0.5 + 1 * 0.5;
+			}
+		},
+		//Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
+		requestAnimFrame = helpers.requestAnimFrame = (function(){
+			return window.requestAnimationFrame ||
+				window.webkitRequestAnimationFrame ||
+				window.mozRequestAnimationFrame ||
+				window.oRequestAnimationFrame ||
+				window.msRequestAnimationFrame ||
+				function(callback) {
+					return window.setTimeout(callback, 1000 / 60);
+				};
+		})(),
+		cancelAnimFrame = helpers.cancelAnimFrame = (function(){
+			return window.cancelAnimationFrame ||
+				window.webkitCancelAnimationFrame ||
+				window.mozCancelAnimationFrame ||
+				window.oCancelAnimationFrame ||
+				window.msCancelAnimationFrame ||
+				function(callback) {
+					return window.clearTimeout(callback, 1000 / 60);
+				};
+		})(),
+		animationLoop = helpers.animationLoop = function(callback,totalSteps,easingString,onProgress,onComplete,chartInstance){
+
+			var currentStep = 0,
+				easingFunction = easingEffects[easingString] || easingEffects.linear;
+
+			var animationFrame = function(){
+				currentStep++;
+				var stepDecimal = currentStep/totalSteps;
+				var easeDecimal = easingFunction(stepDecimal);
+
+				callback.call(chartInstance,easeDecimal,stepDecimal, currentStep);
+				onProgress.call(chartInstance,easeDecimal,stepDecimal);
+				if (currentStep < totalSteps){
+					chartInstance.animationFrame = requestAnimFrame(animationFrame);
+				} else{
+					onComplete.apply(chartInstance);
+				}
+			};
+			requestAnimFrame(animationFrame);
+		},
+		//-- DOM methods
+		getRelativePosition = helpers.getRelativePosition = function(evt){
+			var mouseX, mouseY;
+			var e = evt.originalEvent || evt,
+				canvas = evt.currentTarget || evt.srcElement,
+				boundingRect = canvas.getBoundingClientRect();
+
+			if (e.touches){
+				mouseX = e.touches[0].clientX - boundingRect.left;
+				mouseY = e.touches[0].clientY - boundingRect.top;
+
+			}
+			else{
+				mouseX = e.clientX - boundingRect.left;
+				mouseY = e.clientY - boundingRect.top;
+			}
+
+			return {
+				x : mouseX,
+				y : mouseY
+			};
+
+		},
+		addEvent = helpers.addEvent = function(node,eventType,method){
+			if (node.addEventListener){
+				node.addEventListener(eventType,method);
+			} else if (node.attachEvent){
+				node.attachEvent("on"+eventType, method);
+			} else {
+				node["on"+eventType] = method;
+			}
+		},
+		removeEvent = helpers.removeEvent = function(node, eventType, handler){
+			if (node.removeEventListener){
+				node.removeEventListener(eventType, handler, false);
+			} else if (node.detachEvent){
+				node.detachEvent("on"+eventType,handler);
+			} else{
+				node["on" + eventType] = noop;
+			}
+		},
+		bindEvents = helpers.bindEvents = function(chartInstance, arrayOfEvents, handler){
+			// Create the events object if it's not already present
+			if (!chartInstance.events) chartInstance.events = {};
+
+			each(arrayOfEvents,function(eventName){
+				chartInstance.events[eventName] = function(){
+					handler.apply(chartInstance, arguments);
+				};
+				addEvent(chartInstance.chart.canvas,eventName,chartInstance.events[eventName]);
+			});
+		},
+		unbindEvents = helpers.unbindEvents = function (chartInstance, arrayOfEvents) {
+			each(arrayOfEvents, function(handler,eventName){
+				removeEvent(chartInstance.chart.canvas, eventName, handler);
+			});
+		},
+		getMaximumWidth = helpers.getMaximumWidth = function(domNode){
+			var container = domNode.parentNode;
+			// TODO = check cross browser stuff with this.
+			return container.clientWidth;
+		},
+		getMaximumHeight = helpers.getMaximumHeight = function(domNode){
+			var container = domNode.parentNode;
+			// TODO = check cross browser stuff with this.
+			return container.clientHeight;
+		},
+		getMaximumSize = helpers.getMaximumSize = helpers.getMaximumWidth, // legacy support
+		retinaScale = helpers.retinaScale = function(chart){
+			var ctx = chart.ctx,
+				width = chart.canvas.width,
+				height = chart.canvas.height;
+
+			if (window.devicePixelRatio) {
+				ctx.canvas.style.width = width + "px";
+				ctx.canvas.style.height = height + "px";
+				ctx.canvas.height = height * window.devicePixelRatio;
+				ctx.canvas.width = width * window.devicePixelRatio;
+				ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
+			}
+		},
+		//-- Canvas methods
+		clear = helpers.clear = function(chart){
+			chart.ctx.clearRect(0,0,chart.width,chart.height);
+		},
+		fontString = helpers.fontString = function(pixelSize,fontStyle,fontFamily){
+			return fontStyle + " " + pixelSize+"px " + fontFamily;
+		},
+		longestText = helpers.longestText = function(ctx,font,arrayOfStrings){
+			ctx.font = font;
+			var longest = 0;
+			each(arrayOfStrings,function(string){
+				var textWidth = ctx.measureText(string).width;
+				longest = (textWidth > longest) ? textWidth : longest;
+			});
+			return longest;
+		},
+		drawRoundedRectangle = helpers.drawRoundedRectangle = function(ctx,x,y,width,height,radius){
+			ctx.beginPath();
+			ctx.moveTo(x + radius, y);
+			ctx.lineTo(x + width - radius, y);
+			ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
+			ctx.lineTo(x + width, y + height - radius);
+			ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
+			ctx.lineTo(x + radius, y + height);
+			ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
+			ctx.lineTo(x, y + radius);
+			ctx.quadraticCurveTo(x, y, x + radius, y);
+			ctx.closePath();
+		};
+
+
+	//Store a reference to each instance - allowing us to globally resize chart instances on window resize.
+	//Destroy method on the chart will remove the instance of the chart from this reference.
+	Chart.instances = {};
+
+	Chart.Type = function(data,options,chart){
+		this.options = options;
+		this.chart = chart;
+		this.id = uid();
+		//Add the chart instance to the global namespace
+		Chart.instances[this.id] = this;
+
+		// Initialize is always called when a chart type is created
+		// By default it is a no op, but it should be extended
+		if (options.responsive){
+			this.resize();
+		}
+		this.initialize.call(this,data);
+	};
+
+	//Core methods that'll be a part of every chart type
+	extend(Chart.Type.prototype,{
+		initialize : function(){return this;},
+		clear : function(){
+			clear(this.chart);
+			return this;
+		},
+		stop : function(){
+			// Stops any current animation loop occuring
+			cancelAnimFrame(this.animationFrame);
+			return this;
+		},
+		resize : function(callback){
+			this.stop();
+			var canvas = this.chart.canvas,
+				newWidth = getMaximumWidth(this.chart.canvas),
+				newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : getMaximumHeight(this.chart.canvas);
+
+			canvas.width = this.chart.width = newWidth;
+			canvas.height = this.chart.height = newHeight;
+
+			retinaScale(this.chart);
+
+			if (typeof callback === "function"){
+				callback.apply(this, Array.prototype.slice.call(arguments, 1));
+			}
+			return this;
+		},
+		reflow : noop,
+		render : function(reflow){
+			if (reflow){
+				this.reflow();
+			}
+			if (this.options.animation && !reflow){
+				helpers.animationLoop(
+					this.draw,
+					this.options.animationSteps,
+					this.options.animationEasing,
+					this.options.onAnimationProgress,
+					this.options.onAnimationComplete,
+					this
+				);
+			}
+			else{
+				this.draw();
+				this.options.onAnimationComplete.call(this);
+			}
+			return this;
+		},
+		generateLegend : function(){
+			return template(this.options.legendTemplate,this);
+		},
+		destroy : function(){
+			this.clear();
+			unbindEvents(this, this.events);
+			var canvas = this.chart.canvas;
+
+			// Reset canvas height/width attributes starts a fresh with the canvas context
+			canvas.width = this.chart.width;
+			canvas.height = this.chart.height;
+
+			// < IE9 doesn't support removeProperty
+			if (canvas.style.removeProperty) {
+				canvas.style.removeProperty('width');
+				canvas.style.removeProperty('height');
+			} else {
+				canvas.style.removeAttribute('width');
+				canvas.style.removeAttribute('height');
+			}
+
+			delete Chart.instances[this.id];
+		},
+		showTooltip : function(ChartElements, forceRedraw){
+			// Only redraw the chart if we've actually changed what we're hovering on.
+			if (typeof this.activeElements === 'undefined') this.activeElements = [];
+
+			var isChanged = (function(Elements){
+				var changed = false;
+
+				if (Elements.length !== this.activeElements.length){
+					changed = true;
+					return changed;
+				}
+
+				each(Elements, function(element, index){
+					if (element !== this.activeElements[index]){
+						changed = true;
+					}
+				}, this);
+				return changed;
+			}).call(this, ChartElements);
+
+			if (!isChanged && !forceRedraw){
+				return;
+			}
+			else{
+				this.activeElements = ChartElements;
+			}
+			this.draw();
+			if(this.options.customTooltips){
+				this.options.customTooltips(false);
+			}
+			if (ChartElements.length > 0){
+				// If we have multiple datasets, show a MultiTooltip for all of the data points at that index
+				if (this.datasets && this.datasets.length > 1) {
+					var dataArray,
+						dataIndex;
+
+					for (var i = this.datasets.length - 1; i >= 0; i--) {
+						dataArray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments;
+						dataIndex = indexOf(dataArray, ChartElements[0]);
+						if (dataIndex !== -1){
+							break;
+						}
+					}
+					var tooltipLabels = [],
+						tooltipColors = [],
+						medianPosition = (function(index) {
+
+							// Get all the points at that particular index
+							var Elements = [],
+								dataCollection,
+								xPositions = [],
+								yPositions = [],
+								xMax,
+								yMax,
+								xMin,
+								yMin;
+							helpers.each(this.datasets, function(dataset){
+								dataCollection = dataset.points || dataset.bars || dataset.segments;
+								if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()){
+									Elements.push(dataCollection[dataIndex]);
+								}
+							});
+
+							helpers.each(Elements, function(element) {
+								xPositions.push(element.x);
+								yPositions.push(element.y);
+
+
+								//Include any colour information about the element
+								tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, element));
+								tooltipColors.push({
+									fill: element._saved.fillColor || element.fillColor,
+									stroke: element._saved.strokeColor || element.strokeColor
+								});
+
+							}, this);
+
+							yMin = min(yPositions);
+							yMax = max(yPositions);
+
+							xMin = min(xPositions);
+							xMax = max(xPositions);
+
+							return {
+								x: (xMin > this.chart.width/2) ? xMin : xMax,
+								y: (yMin + yMax)/2
+							};
+						}).call(this, dataIndex);
+
+					new Chart.MultiTooltip({
+						x: medianPosition.x,
+						y: medianPosition.y,
+						xPadding: this.options.tooltipXPadding,
+						yPadding: this.options.tooltipYPadding,
+						xOffset: this.options.tooltipXOffset,
+						fillColor: this.options.tooltipFillColor,
+						textColor: this.options.tooltipFontColor,
+						fontFamily: this.options.tooltipFontFamily,
+						fontStyle: this.options.tooltipFontStyle,
+						fontSize: this.options.tooltipFontSize,
+						titleTextColor: this.options.tooltipTitleFontColor,
+						titleFontFamily: this.options.tooltipTitleFontFamily,
+						titleFontStyle: this.options.tooltipTitleFontStyle,
+						titleFontSize: this.options.tooltipTitleFontSize,
+						cornerRadius: this.options.tooltipCornerRadius,
+						labels: tooltipLabels,
+						legendColors: tooltipColors,
+						legendColorBackground : this.options.multiTooltipKeyBackground,
+						title: ChartElements[0].label,
+						chart: this.chart,
+						ctx: this.chart.ctx,
+						custom: this.options.customTooltips
+					}).draw();
+
+				} else {
+					each(ChartElements, function(Element) {
+						var tooltipPosition = Element.tooltipPosition();
+						new Chart.Tooltip({
+							x: Math.round(tooltipPosition.x),
+							y: Math.round(tooltipPosition.y),
+							xPadding: this.options.tooltipXPadding,
+							yPadding: this.options.tooltipYPadding,
+							fillColor: this.options.tooltipFillColor,
+							textColor: this.options.tooltipFontColor,
+							fontFamily: this.options.tooltipFontFamily,
+							fontStyle: this.options.tooltipFontStyle,
+							fontSize: this.options.tooltipFontSize,
+							caretHeight: this.options.tooltipCaretSize,
+							cornerRadius: this.options.tooltipCornerRadius,
+							text: template(this.options.tooltipTemplate, Element),
+							chart: this.chart,
+							custom: this.options.customTooltips
+						}).draw();
+					}, this);
+				}
+			}
+			return this;
+		},
+		toBase64Image : function(){
+			return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments);
+		}
+	});
+
+	Chart.Type.extend = function(extensions){
+
+		var parent = this;
+
+		var ChartType = function(){
+			return parent.apply(this,arguments);
+		};
+
+		//Copy the prototype object of the this class
+		ChartType.prototype = clone(parent.prototype);
+		//Now overwrite some of the properties in the base class with the new extensions
+		extend(ChartType.prototype, extensions);
+
+		ChartType.extend = Chart.Type.extend;
+
+		if (extensions.name || parent.prototype.name){
+
+			var chartName = extensions.name || parent.prototype.name;
+			//Assign any potential default values of the new chart type
+
+			//If none are defined, we'll use a clone of the chart type this is being extended from.
+			//I.e. if we extend a line chart, we'll use the defaults from the line chart if our new chart
+			//doesn't define some defaults of their own.
+
+			var baseDefaults = (Chart.defaults[parent.prototype.name]) ? clone(Chart.defaults[parent.prototype.name]) : {};
+
+			Chart.defaults[chartName] = extend(baseDefaults,extensions.defaults);
+
+			Chart.types[chartName] = ChartType;
+
+			//Register this new chart type in the Chart prototype
+			Chart.prototype[chartName] = function(data,options){
+				var config = merge(Chart.defaults.global, Chart.defaults[chartName], options || {});
+				return new ChartType(data,config,this);
+			};
+		} else{
+			warn("Name not provided for this chart, so it hasn't been registered");
+		}
+		return parent;
+	};
+
+	Chart.Element = function(configuration){
+		extend(this,configuration);
+		this.initialize.apply(this,arguments);
+		this.save();
+	};
+	extend(Chart.Element.prototype,{
+		initialize : function(){},
+		restore : function(props){
+			if (!props){
+				extend(this,this._saved);
+			} else {
+				each(props,function(key){
+					this[key] = this._saved[key];
+				},this);
+			}
+			return this;
+		},
+		save : function(){
+			this._saved = clone(this);
+			delete this._saved._saved;
+			return this;
+		},
+		update : function(newProps){
+			each(newProps,function(value,key){
+				this._saved[key] = this[key];
+				this[key] = value;
+			},this);
+			return this;
+		},
+		transition : function(props,ease){
+			each(props,function(value,key){
+				this[key] = ((value - this._saved[key]) * ease) + this._saved[key];
+			},this);
+			return this;
+		},
+		tooltipPosition : function(){
+			return {
+				x : this.x,
+				y : this.y
+			};
+		},
+		hasValue: function(){
+			return isNumber(this.value);
+		}
+	});
+
+	Chart.Element.extend = inherits;
+
+
+	Chart.Point = Chart.Element.extend({
+		display: true,
+		inRange: function(chartX,chartY){
+			var hitDetectionRange = this.hitDetectionRadius + this.radius;
+			return ((Math.pow(chartX-this.x, 2)+Math.pow(chartY-this.y, 2)) < Math.pow(hitDetectionRange,2));
+		},
+		draw : function(){
+			if (this.display){
+				var ctx = this.ctx;
+				ctx.beginPath();
+
+				ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2);
+				ctx.closePath();
+
+				ctx.strokeStyle = this.strokeColor;
+				ctx.lineWidth = this.strokeWidth;
+
+				ctx.fillStyle = this.fillColor;
+
+				ctx.fill();
+				ctx.stroke();
+			}
+
+
+			//Quick debug for bezier curve splining
+			//Highlights control points and the line between them.
+			//Handy for dev - stripped in the min version.
+
+			// ctx.save();
+			// ctx.fillStyle = "black";
+			// ctx.strokeStyle = "black"
+			// ctx.beginPath();
+			// ctx.arc(this.controlPoints.inner.x,this.controlPoints.inner.y, 2, 0, Math.PI*2);
+			// ctx.fill();
+
+			// ctx.beginPath();
+			// ctx.arc(this.controlPoints.outer.x,this.controlPoints.outer.y, 2, 0, Math.PI*2);
+			// ctx.fill();
+
+			// ctx.moveTo(this.controlPoints.inner.x,this.controlPoints.inner.y);
+			// ctx.lineTo(this.x, this.y);
+			// ctx.lineTo(this.controlPoints.outer.x,this.controlPoints.outer.y);
+			// ctx.stroke();
+
+			// ctx.restore();
+
+
+
+		}
+	});
+
+	Chart.Arc = Chart.Element.extend({
+		inRange : function(chartX,chartY){
+
+			var pointRelativePosition = helpers.getAngleFromPoint(this, {
+				x: chartX,
+				y: chartY
+			});
+
+			//Check if within the range of the open/close angle
+			var betweenAngles = (pointRelativePosition.angle >= this.startAngle && pointRelativePosition.angle <= this.endAngle),
+				withinRadius = (pointRelativePosition.distance >= this.innerRadius && pointRelativePosition.distance <= this.outerRadius);
+
+			return (betweenAngles && withinRadius);
+			//Ensure within the outside of the arc centre, but inside arc outer
+		},
+		tooltipPosition : function(){
+			var centreAngle = this.startAngle + ((this.endAngle - this.startAngle) / 2),
+				rangeFromCentre = (this.outerRadius - this.innerRadius) / 2 + this.innerRadius;
+			return {
+				x : this.x + (Math.cos(centreAngle) * rangeFromCentre),
+				y : this.y + (Math.sin(centreAngle) * rangeFromCentre)
+			};
+		},
+		draw : function(animationPercent){
+
+			var easingDecimal = animationPercent || 1;
+
+			var ctx = this.ctx;
+
+			ctx.beginPath();
+
+			ctx.arc(this.x, this.y, this.outerRadius, this.startAngle, this.endAngle);
+
+			ctx.arc(this.x, this.y, this.innerRadius, this.endAngle, this.startAngle, true);
+
+			ctx.closePath();
+			ctx.strokeStyle = this.strokeColor;
+			ctx.lineWidth = this.strokeWidth;
+
+			ctx.fillStyle = this.fillColor;
+
+			ctx.fill();
+			ctx.lineJoin = 'bevel';
+
+			if (this.showStroke){
+				ctx.stroke();
+			}
+		}
+	});
+
+	Chart.Rectangle = Chart.Element.extend({
+		draw : function(){
+			var ctx = this.ctx,
+				halfWidth = this.width/2,
+				leftX = this.x - halfWidth,
+				rightX = this.x + halfWidth,
+				top = this.base - (this.base - this.y),
+				halfStroke = this.strokeWidth / 2;
+
+			// Canvas doesn't allow us to stroke inside the width so we can
+			// adjust the sizes to fit if we're setting a stroke on the line
+			if (this.showStroke){
+				leftX += halfStroke;
+				rightX -= halfStroke;
+				top += halfStroke;
+			}
+
+			ctx.beginPath();
+
+			ctx.fillStyle = this.fillColor;
+			ctx.strokeStyle = this.strokeColor;
+			ctx.lineWidth = this.strokeWidth;
+
+			// It'd be nice to keep this class totally generic to any rectangle
+			// and simply specify which border to miss out.
+			ctx.moveTo(leftX, this.base);
+			ctx.lineTo(leftX, top);
+			ctx.lineTo(rightX, top);
+			ctx.lineTo(rightX, this.base);
+			ctx.fill();
+			if (this.showStroke){
+				ctx.stroke();
+			}
+		},
+		height : function(){
+			return this.base - this.y;
+		},
+		inRange : function(chartX,chartY){
+			return (chartX >= this.x - this.width/2 && chartX <= this.x + this.width/2) && (chartY >= this.y && chartY <= this.base);
+		}
+	});
+
+	Chart.Tooltip = Chart.Element.extend({
+		draw : function(){
+
+			var ctx = this.chart.ctx;
+
+			ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily);
+
+			this.xAlign = "center";
+			this.yAlign = "above";
+
+			//Distance between the actual element.y position and the start of the tooltip caret
+			var caretPadding = this.caretPadding = 2;
+
+			var tooltipWidth = ctx.measureText(this.text).width + 2*this.xPadding,
+				tooltipRectHeight = this.fontSize + 2*this.yPadding,
+				tooltipHeight = tooltipRectHeight + this.caretHeight + caretPadding;
+
+			if (this.x + tooltipWidth/2 >this.chart.width){
+				this.xAlign = "left";
+			} else if (this.x - tooltipWidth/2 < 0){
+				this.xAlign = "right";
+			}
+
+			if (this.y - tooltipHeight < 0){
+				this.yAlign = "below";
+			}
+
+
+			var tooltipX = this.x - tooltipWidth/2,
+				tooltipY = this.y - tooltipHeight;
+
+			ctx.fillStyle = this.fillColor;
+
+			// Custom Tooltips
+			if(this.custom){
+				this.custom(this);
+			}
+			else{
+				switch(this.yAlign)
+				{
+				case "above":
+					//Draw a caret above the x/y
+					ctx.beginPath();
+					ctx.moveTo(this.x,this.y - caretPadding);
+					ctx.lineTo(this.x + this.caretHeight, this.y - (caretPadding + this.caretHeight));
+					ctx.lineTo(this.x - this.caretHeight, this.y - (caretPadding + this.caretHeight));
+					ctx.closePath();
+					ctx.fill();
+					break;
+				case "below":
+					tooltipY = this.y + caretPadding + this.caretHeight;
+					//Draw a caret below the x/y
+					ctx.beginPath();
+					ctx.moveTo(this.x, this.y + caretPadding);
+					ctx.lineTo(this.x + this.caretHeight, this.y + caretPadding + this.caretHeight);
+					ctx.lineTo(this.x - this.caretHeight, this.y + caretPadding + this.caretHeight);
+					ctx.closePath();
+					ctx.fill();
+					break;
+				}
+
+				switch(this.xAlign)
+				{
+				case "left":
+					tooltipX = this.x - tooltipWidth + (this.cornerRadius + this.caretHeight);
+					break;
+				case "right":
+					tooltipX = this.x - (this.cornerRadius + this.caretHeight);
+					break;
+				}
+
+				drawRoundedRectangle(ctx,tooltipX,tooltipY,tooltipWidth,tooltipRectHeight,this.cornerRadius);
+
+				ctx.fill();
+
+				ctx.fillStyle = this.textColor;
+				ctx.textAlign = "center";
+				ctx.textBaseline = "middle";
+				ctx.fillText(this.text, tooltipX + tooltipWidth/2, tooltipY + tooltipRectHeight/2);
+			}
+		}
+	});
+
+	Chart.MultiTooltip = Chart.Element.extend({
+		initialize : function(){
+			this.font = fontString(this.fontSize,this.fontStyle,this.fontFamily);
+
+			this.titleFont = fontString(this.titleFontSize,this.titleFontStyle,this.titleFontFamily);
+
+			this.height = (this.labels.length * this.fontSize) + ((this.labels.length-1) * (this.fontSize/2)) + (this.yPadding*2) + this.titleFontSize *1.5;
+
+			this.ctx.font = this.titleFont;
+
+			var titleWidth = this.ctx.measureText(this.title).width,
+				//Label has a legend square as well so account for this.
+				labelWidth = longestText(this.ctx,this.font,this.labels) + this.fontSize + 3,
+				longestTextWidth = max([labelWidth,titleWidth]);
+
+			this.width = longestTextWidth + (this.xPadding*2);
+
+
+			var halfHeight = this.height/2;
+
+			//Check to ensure the height will fit on the canvas
+			if (this.y - halfHeight < 0 ){
+				this.y = halfHeight;
+			} else if (this.y + halfHeight > this.chart.height){
+				this.y = this.chart.height - halfHeight;
+			}
+
+			//Decide whether to align left or right based on position on canvas
+			if (this.x > this.chart.width/2){
+				this.x -= this.xOffset + this.width;
+			} else {
+				this.x += this.xOffset;
+			}
+
+
+		},
+		getLineHeight : function(index){
+			var baseLineHeight = this.y - (this.height/2) + this.yPadding,
+				afterTitleIndex = index-1;
+
+			//If the index is zero, we're getting the title
+			if (index === 0){
+				return baseLineHeight + this.titleFontSize/2;
+			} else{
+				return baseLineHeight + ((this.fontSize*1.5*afterTitleIndex) + this.fontSize/2) + this.titleFontSize * 1.5;
+			}
+
+		},
+		draw : function(){
+			// Custom Tooltips
+			if(this.custom){
+				this.custom(this);
+			}
+			else{
+				drawRoundedRectangle(this.ctx,this.x,this.y - this.height/2,this.width,this.height,this.cornerRadius);
+				var ctx = this.ctx;
+				ctx.fillStyle = this.fillColor;
+				ctx.fill();
+				ctx.closePath();
+
+				ctx.textAlign = "left";
+				ctx.textBaseline = "middle";
+				ctx.fillStyle = this.titleTextColor;
+				ctx.font = this.titleFont;
+
+				ctx.fillText(this.title,this.x + this.xPadding, this.getLineHeight(0));
+
+				ctx.font = this.font;
+				helpers.each(this.labels,function(label,index){
+					ctx.fillStyle = this.textColor;
+					ctx.fillText(label,this.x + this.xPadding + this.fontSize + 3, this.getLineHeight(index + 1));
+
+					//A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas)
+					//ctx.clearRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize);
+					//Instead we'll make a white filled block to put the legendColour palette over.
+
+					ctx.fillStyle = this.legendColorBackground;
+					ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize);
+
+					ctx.fillStyle = this.legendColors[index].fill;
+					ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize);
+
+
+				},this);
+			}
+		}
+	});
+
+	Chart.Scale = Chart.Element.extend({
+		initialize : function(){
+			this.fit();
+		},
+		buildYLabels : function(){
+			this.yLabels = [];
+
+			var stepDecimalPlaces = getDecimalPlaces(this.stepValue);
+
+			for (var i=0; i<=this.steps; i++){
+				this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)}));
+			}
+			this.yLabelWidth = (this.display && this.showLabels) ? longestText(this.ctx,this.font,this.yLabels) : 0;
+		},
+		addXLabel : function(label){
+			this.xLabels.push(label);
+			this.valuesCount++;
+			this.fit();
+		},
+		removeXLabel : function(){
+			this.xLabels.shift();
+			this.valuesCount--;
+			this.fit();
+		},
+		// Fitting loop to rotate x Labels and figure out what fits there, and also calculate how many Y steps to use
+		fit: function(){
+			// First we need the width of the yLabels, assuming the xLabels aren't rotated
+
+			// To do that we need the base line at the top and base of the chart, assuming there is no x label rotation
+			this.startPoint = (this.display) ? this.fontSize : 0;
+			this.endPoint = (this.display) ? this.height - (this.fontSize * 1.5) - 5 : this.height; // -5 to pad labels
+
+			// Apply padding settings to the start and end point.
+			this.startPoint += this.padding;
+			this.endPoint -= this.padding;
+
+			// Cache the starting height, so can determine if we need to recalculate the scale yAxis
+			var cachedHeight = this.endPoint - this.startPoint,
+				cachedYLabelWidth;
+
+			// Build the current yLabels so we have an idea of what size they'll be to start
+			/*
+			 *	This sets what is returned from calculateScaleRange as static properties of this class:
+			 *
+				this.steps;
+				this.stepValue;
+				this.min;
+				this.max;
+			 *
+			 */
+			this.calculateYRange(cachedHeight);
+
+			// With these properties set we can now build the array of yLabels
+			// and also the width of the largest yLabel
+			this.buildYLabels();
+
+			this.calculateXLabelRotation();
+
+			while((cachedHeight > this.endPoint - this.startPoint)){
+				cachedHeight = this.endPoint - this.startPoint;
+				cachedYLabelWidth = this.yLabelWidth;
+
+				this.calculateYRange(cachedHeight);
+				this.buildYLabels();
+
+				// Only go through the xLabel loop again if the yLabel width has changed
+				if (cachedYLabelWidth < this.yLabelWidth){
+					this.calculateXLabelRotation();
+				}
+			}
+
+		},
+		calculateXLabelRotation : function(){
+			//Get the width of each grid by calculating the difference
+			//between x offsets between 0 and 1.
+
+			this.ctx.font = this.font;
+
+			var firstWidth = this.ctx.measureText(this.xLabels[0]).width,
+				lastWidth = this.ctx.measureText(this.xLabels[this.xLabels.length - 1]).width,
+				firstRotated,
+				lastRotated;
+
+
+			this.xScalePaddingRight = lastWidth/2 + 3;
+			this.xScalePaddingLeft = (firstWidth/2 > this.yLabelWidth + 10) ? firstWidth/2 : this.yLabelWidth + 10;
+
+			this.xLabelRotation = 0;
+			if (this.display){
+				var originalLabelWidth = longestText(this.ctx,this.font,this.xLabels),
+					cosRotation,
+					firstRotatedWidth;
+				this.xLabelWidth = originalLabelWidth;
+				//Allow 3 pixels x2 padding either side for label readability
+				var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6;
+
+				//Max label rotate should be 90 - also act as a loop counter
+				while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)){
+					cosRotation = Math.cos(toRadians(this.xLabelRotation));
+
+					firstRotated = cosRotation * firstWidth;
+					lastRotated = cosRotation * lastWidth;
+
+					// We're right aligning the text now.
+					if (firstRotated + this.fontSize / 2 > this.yLabelWidth + 8){
+						this.xScalePaddingLeft = firstRotated + this.fontSize / 2;
+					}
+					this.xScalePaddingRight = this.fontSize/2;
+
+
+					this.xLabelRotation++;
+					this.xLabelWidth = cosRotation * originalLabelWidth;
+
+				}
+				if (this.xLabelRotation > 0){
+					this.endPoint -= Math.sin(toRadians(this.xLabelRotation))*originalLabelWidth + 3;
+				}
+			}
+			else{
+				this.xLabelWidth = 0;
+				this.xScalePaddingRight = this.padding;
+				this.xScalePaddingLeft = this.padding;
+			}
+
+		},
+		// Needs to be overidden in each Chart type
+		// Otherwise we need to pass all the data into the scale class
+		calculateYRange: noop,
+		drawingArea: function(){
+			return this.startPoint - this.endPoint;
+		},
+		calculateY : function(value){
+			var scalingFactor = this.drawingArea() / (this.min - this.max);
+			return this.endPoint - (scalingFactor * (value - this.min));
+		},
+		calculateX : function(index){
+			var isRotated = (this.xLabelRotation > 0),
+				// innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding,
+				innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight),
+				valueWidth = innerWidth/Math.max((this.valuesCount - ((this.offsetGridLines) ? 0 : 1)), 1),
+				valueOffset = (valueWidth * index) + this.xScalePaddingLeft;
+
+			if (this.offsetGridLines){
+				valueOffset += (valueWidth/2);
+			}
+
+			return Math.round(valueOffset);
+		},
+		update : function(newProps){
+			helpers.extend(this, newProps);
+			this.fit();
+		},
+		draw : function(){
+			var ctx = this.ctx,
+				yLabelGap = (this.endPoint - this.startPoint) / this.steps,
+				xStart = Math.round(this.xScalePaddingLeft);
+			if (this.display){
+				ctx.fillStyle = this.textColor;
+				ctx.font = this.font;
+				each(this.yLabels,function(labelString,index){
+					var yLabelCenter = this.endPoint - (yLabelGap * index),
+						linePositionY = Math.round(yLabelCenter),
+						drawHorizontalLine = this.showHorizontalLines;
+
+					ctx.textAlign = "right";
+					ctx.textBaseline = "middle";
+					if (this.showLabels){
+						ctx.fillText(labelString,xStart - 10,yLabelCenter);
+					}
+
+					// This is X axis, so draw it
+					if (index === 0 && !drawHorizontalLine){
+						drawHorizontalLine = true;
+					}
+
+					if (drawHorizontalLine){
+						ctx.beginPath();
+					}
+
+					if (index > 0){
+						// This is a grid line in the centre, so drop that
+						ctx.lineWidth = this.gridLineWidth;
+						ctx.strokeStyle = this.gridLineColor;
+					} else {
+						// This is the first line on the scale
+						ctx.lineWidth = this.lineWidth;
+						ctx.strokeStyle = this.lineColor;
+					}
+
+					linePositionY += helpers.aliasPixel(ctx.lineWidth);
+
+					if(drawHorizontalLine){
+						ctx.moveTo(xStart, linePositionY);
+						ctx.lineTo(this.width, linePositionY);
+						ctx.stroke();
+						ctx.closePath();
+					}
+
+					ctx.lineWidth = this.lineWidth;
+					ctx.strokeStyle = this.lineColor;
+					ctx.beginPath();
+					ctx.moveTo(xStart - 5, linePositionY);
+					ctx.lineTo(xStart, linePositionY);
+					ctx.stroke();
+					ctx.closePath();
+
+				},this);
+
+				each(this.xLabels,function(label,index){
+					var xPos = this.calculateX(index) + aliasPixel(this.lineWidth),
+						// Check to see if line/bar here and decide where to place the line
+						linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth),
+						isRotated = (this.xLabelRotation > 0),
+						drawVerticalLine = this.showVerticalLines;
+
+					// This is Y axis, so draw it
+					if (index === 0 && !drawVerticalLine){
+						drawVerticalLine = true;
+					}
+
+					if (drawVerticalLine){
+						ctx.beginPath();
+					}
+
+					if (index > 0){
+						// This is a grid line in the centre, so drop that
+						ctx.lineWidth = this.gridLineWidth;
+						ctx.strokeStyle = this.gridLineColor;
+					} else {
+						// This is the first line on the scale
+						ctx.lineWidth = this.lineWidth;
+						ctx.strokeStyle = this.lineColor;
+					}
+
+					if (drawVerticalLine){
+						ctx.moveTo(linePos,this.endPoint);
+						ctx.lineTo(linePos,this.startPoint - 3);
+						ctx.stroke();
+						ctx.closePath();
+					}
+
+
+					ctx.lineWidth = this.lineWidth;
+					ctx.strokeStyle = this.lineColor;
+
+
+					// Small lines at the bottom of the base grid line
+					ctx.beginPath();
+					ctx.moveTo(linePos,this.endPoint);
+					ctx.lineTo(linePos,this.endPoint + 5);
+					ctx.stroke();
+					ctx.closePath();
+
+					ctx.save();
+					ctx.translate(xPos,(isRotated) ? this.endPoint + 12 : this.endPoint + 8);
+					ctx.rotate(toRadians(this.xLabelRotation)*-1);
+					ctx.font = this.font;
+					ctx.textAlign = (isRotated) ? "right" : "center";
+					ctx.textBaseline = (isRotated) ? "middle" : "top";
+					ctx.fillText(label, 0, 0);
+					ctx.restore();
+				},this);
+
+			}
+		}
+
+	});
+
+	Chart.RadialScale = Chart.Element.extend({
+		initialize: function(){
+			this.size = min([this.height, this.width]);
+			this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2);
+		},
+		calculateCenterOffset: function(value){
+			// Take into account half font size + the yPadding of the top value
+			var scalingFactor = this.drawingArea / (this.max - this.min);
+
+			return (value - this.min) * scalingFactor;
+		},
+		update : function(){
+			if (!this.lineArc){
+				this.setScaleSize();
+			} else {
+				this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2);
+			}
+			this.buildYLabels();
+		},
+		buildYLabels: function(){
+			this.yLabels = [];
+
+			var stepDecimalPlaces = getDecimalPlaces(this.stepValue);
+
+			for (var i=0; i<=this.steps; i++){
+				this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)}));
+			}
+		},
+		getCircumference : function(){
+			return ((Math.PI*2) / this.valuesCount);
+		},
+		setScaleSize: function(){
+			/*
+			 * Right, this is really confusing and there is a lot of maths going on here
+			 * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9
+			 *
+			 * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif
+			 *
+			 * Solution:
+			 *
+			 * We assume the radius of the polygon is half the size of the canvas at first
+			 * at each index we check if the text overlaps.
+			 *
+			 * Where it does, we store that angle and that index.
+			 *
+			 * After finding the largest index and angle we calculate how much we need to remove
+			 * from the shape radius to move the point inwards by that x.
+			 *
+			 * We average the left and right distances to get the maximum shape radius that can fit in the box
+			 * along with labels.
+			 *
+			 * Once we have that, we can find the centre point for the chart, by taking the x text protrusion
+			 * on each side, removing that from the size, halving it and adding the left x protrusion width.
+			 *
+			 * This will mean we have a shape fitted to the canvas, as large as it can be with the labels
+			 * and position it in the most space efficient manner
+			 *
+			 * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif
+			 */
+
+
+			// Get maximum radius of the polygon. Either half the height (minus the text width) or half the width.
+			// Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points
+			var largestPossibleRadius = min([(this.height/2 - this.pointLabelFontSize - 5), this.width/2]),
+				pointPosition,
+				i,
+				textWidth,
+				halfTextWidth,
+				furthestRight = this.width,
+				furthestRightIndex,
+				furthestRightAngle,
+				furthestLeft = 0,
+				furthestLeftIndex,
+				furthestLeftAngle,
+				xProtrusionLeft,
+				xProtrusionRight,
+				radiusReductionRight,
+				radiusReductionLeft,
+				maxWidthRadius;
+			this.ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily);
+			for (i=0;i<this.valuesCount;i++){
+				// 5px to space the text slightly out - similar to what we do in the draw function.
+				pointPosition = this.getPointPosition(i, largestPossibleRadius);
+				textWidth = this.ctx.measureText(template(this.templateString, { value: this.labels[i] })).width + 5;
+				if (i === 0 || i === this.valuesCount/2){
+					// If we're at index zero, or exactly the middle, we're at exactly the top/bottom
+					// of the radar chart, so text will be aligned centrally, so we'll half it and compare
+					// w/left and right text sizes
+					halfTextWidth = textWidth/2;
+					if (pointPosition.x + halfTextWidth > furthestRight) {
+						furthestRight = pointPosition.x + halfTextWidth;
+						furthestRightIndex = i;
+					}
+					if (pointPosition.x - halfTextWidth < furthestLeft) {
+						furthestLeft = pointPosition.x - halfTextWidth;
+						furthestLeftIndex = i;
+					}
+				}
+				else if (i < this.valuesCount/2) {
+					// Less than half the values means we'll left align the text
+					if (pointPosition.x + textWidth > furthestRight) {
+						furthestRight = pointPosition.x + textWidth;
+						furthestRightIndex = i;
+					}
+				}
+				else if (i > this.valuesCount/2){
+					// More than half the values means we'll right align the text
+					if (pointPosition.x - textWidth < furthestLeft) {
+						furthestLeft = pointPosition.x - textWidth;
+						furthestLeftIndex = i;
+					}
+				}
+			}
+
+			xProtrusionLeft = furthestLeft;
+
+			xProtrusionRight = Math.ceil(furthestRight - this.width);
+
+			furthestRightAngle = this.getIndexAngle(furthestRightIndex);
+
+			furthestLeftAngle = this.getIndexAngle(furthestLeftIndex);
+
+			radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI/2);
+
+			radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI/2);
+
+			// Ensure we actually need to reduce the size of the chart
+			radiusReductionRight = (isNumber(radiusReductionRight)) ? radiusReductionRight : 0;
+			radiusReductionLeft = (isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0;
+
+			this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight)/2;
+
+			//this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2])
+			this.setCenterPoint(radiusReductionLeft, radiusReductionRight);
+
+		},
+		setCenterPoint: function(leftMovement, rightMovement){
+
+			var maxRight = this.width - rightMovement - this.drawingArea,
+				maxLeft = leftMovement + this.drawingArea;
+
+			this.xCenter = (maxLeft + maxRight)/2;
+			// Always vertically in the centre as the text height doesn't change
+			this.yCenter = (this.height/2);
+		},
+
+		getIndexAngle : function(index){
+			var angleMultiplier = (Math.PI * 2) / this.valuesCount;
+			// Start from the top instead of right, so remove a quarter of the circle
+
+			return index * angleMultiplier - (Math.PI/2);
+		},
+		getPointPosition : function(index, distanceFromCenter){
+			var thisAngle = this.getIndexAngle(index);
+			return {
+				x : (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter,
+				y : (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter
+			};
+		},
+		draw: function(){
+			if (this.display){
+				var ctx = this.ctx;
+				each(this.yLabels, function(label, index){
+					// Don't draw a centre value
+					if (index > 0){
+						var yCenterOffset = index * (this.drawingArea/this.steps),
+							yHeight = this.yCenter - yCenterOffset,
+							pointPosition;
+
+						// Draw circular lines around the scale
+						if (this.lineWidth > 0){
+							ctx.strokeStyle = this.lineColor;
+							ctx.lineWidth = this.lineWidth;
+
+							if(this.lineArc){
+								ctx.beginPath();
+								ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI*2);
+								ctx.closePath();
+								ctx.stroke();
+							} else{
+								ctx.beginPath();
+								for (var i=0;i<this.valuesCount;i++)
+								{
+									pointPosition = this.getPointPosition(i, this.calculateCenterOffset(this.min + (index * this.stepValue)));
+									if (i === 0){
+										ctx.moveTo(pointPosition.x, pointPosition.y);
+									} else {
+										ctx.lineTo(pointPosition.x, pointPosition.y);
+									}
+								}
+								ctx.closePath();
+								ctx.stroke();
+							}
+						}
+						if(this.showLabels){
+							ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily);
+							if (this.showLabelBackdrop){
+								var labelWidth = ctx.measureText(label).width;
+								ctx.fillStyle = this.backdropColor;
+								ctx.fillRect(
+									this.xCenter - labelWidth/2 - this.backdropPaddingX,
+									yHeight - this.fontSize/2 - this.backdropPaddingY,
+									labelWidth + this.backdropPaddingX*2,
+									this.fontSize + this.backdropPaddingY*2
+								);
+							}
+							ctx.textAlign = 'center';
+							ctx.textBaseline = "middle";
+							ctx.fillStyle = this.fontColor;
+							ctx.fillText(label, this.xCenter, yHeight);
+						}
+					}
+				}, this);
+
+				if (!this.lineArc){
+					ctx.lineWidth = this.angleLineWidth;
+					ctx.strokeStyle = this.angleLineColor;
+					for (var i = this.valuesCount - 1; i >= 0; i--) {
+						if (this.angleLineWidth > 0){
+							var outerPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max));
+							ctx.beginPath();
+							ctx.moveTo(this.xCenter, this.yCenter);
+							ctx.lineTo(outerPosition.x, outerPosition.y);
+							ctx.stroke();
+							ctx.closePath();
+						}
+						// Extra 3px out for some label spacing
+						var pointLabelPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max) + 5);
+						ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily);
+						ctx.fillStyle = this.pointLabelFontColor;
+
+						var labelsCount = this.labels.length,
+							halfLabelsCount = this.labels.length/2,
+							quarterLabelsCount = halfLabelsCount/2,
+							upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount),
+							exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount);
+						if (i === 0){
+							ctx.textAlign = 'center';
+						} else if(i === halfLabelsCount){
+							ctx.textAlign = 'center';
+						} else if (i < halfLabelsCount){
+							ctx.textAlign = 'left';
+						} else {
+							ctx.textAlign = 'right';
+						}
+
+						// Set the correct text baseline based on outer positioning
+						if (exactQuarter){
+							ctx.textBaseline = 'middle';
+						} else if (upperHalf){
+							ctx.textBaseline = 'bottom';
+						} else {
+							ctx.textBaseline = 'top';
+						}
+
+						ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y);
+					}
+				}
+			}
+		}
+	});
+
+	// Attach global event to resize each chart instance when the browser resizes
+	helpers.addEvent(window, "resize", (function(){
+		// Basic debounce of resize function so it doesn't hurt performance when resizing browser.
+		var timeout;
+		return function(){
+			clearTimeout(timeout);
+			timeout = setTimeout(function(){
+				each(Chart.instances,function(instance){
+					// If the responsive flag is set in the chart instance config
+					// Cascade the resize event down to the chart.
+					if (instance.options.responsive){
+						instance.resize(instance.render, true);
+					}
+				});
+			}, 50);
+		};
+	})());
+
+
+	if (amd) {
+		define(function(){
+			return Chart;
+		});
+	} else if (typeof module === 'object' && module.exports) {
+		module.exports = Chart;
+	}
+
+	root.Chart = Chart;
+
+	Chart.noConflict = function(){
+		root.Chart = previous;
+		return Chart;
+	};
+
+}).call(this);
+
+(function(){
+	"use strict";
+
+	var root = this,
+		Chart = root.Chart,
+		helpers = Chart.helpers;
+
+
+	var defaultConfig = {
+		//Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value
+		scaleBeginAtZero : true,
+
+		//Boolean - Whether grid lines are shown across the chart
+		scaleShowGridLines : true,
+
+		//String - Colour of the grid lines
+		scaleGridLineColor : "rgba(0,0,0,.05)",
+
+		//Number - Width of the grid lines
+		scaleGridLineWidth : 1,
+
+		//Boolean - Whether to show horizontal lines (except X axis)
+		scaleShowHorizontalLines: true,
+
+		//Boolean - Whether to show vertical lines (except Y axis)
+		scaleShowVerticalLines: true,
+
+		//Boolean - If there is a stroke on each bar
+		barShowStroke : true,
+
+		//Number - Pixel width of the bar stroke
+		barStrokeWidth : 2,
+
+		//Number - Spacing between each of the X value sets
+		barValueSpacing : 5,
+
+		//Number - Spacing between data sets within X values
+		barDatasetSpacing : 1,
+
+		//String - A legend template
+		legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].fillColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>"
+
+	};
+
+
+	Chart.Type.extend({
+		name: "Bar",
+		defaults : defaultConfig,
+		initialize:  function(data){
+
+			//Expose options as a scope variable here so we can access it in the ScaleClass
+			var options = this.options;
+
+			this.ScaleClass = Chart.Scale.extend({
+				offsetGridLines : true,
+				calculateBarX : function(datasetCount, datasetIndex, barIndex){
+					//Reusable method for calculating the xPosition of a given bar based on datasetIndex & width of the bar
+					var xWidth = this.calculateBaseWidth(),
+						xAbsolute = this.calculateX(barIndex) - (xWidth/2),
+						barWidth = this.calculateBarWidth(datasetCount);
+
+					return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * options.barDatasetSpacing) + barWidth/2;
+				},
+				calculateBaseWidth : function(){
+					return (this.calculateX(1) - this.calculateX(0)) - (2*options.barValueSpacing);
+				},
+				calculateBarWidth : function(datasetCount){
+					//The padding between datasets is to the right of each bar, providing that there are more than 1 dataset
+					var baseWidth = this.calculateBaseWidth() - ((datasetCount - 1) * options.barDatasetSpacing);
+
+					return (baseWidth / datasetCount);
+				}
+			});
+
+			this.datasets = [];
+
+			//Set up tooltip events on the chart
+			if (this.options.showTooltips){
+				helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
+					var activeBars = (evt.type !== 'mouseout') ? this.getBarsAtEvent(evt) : [];
+
+					this.eachBars(function(bar){
+						bar.restore(['fillColor', 'strokeColor']);
+					});
+					helpers.each(activeBars, function(activeBar){
+						activeBar.fillColor = activeBar.highlightFill;
+						activeBar.strokeColor = activeBar.highlightStroke;
+					});
+					this.showTooltip(activeBars);
+				});
+			}
+
+			//Declare the extension of the default point, to cater for the options passed in to the constructor
+			this.BarClass = Chart.Rectangle.extend({
+				strokeWidth : this.options.barStrokeWidth,
+				showStroke : this.options.barShowStroke,
+				ctx : this.chart.ctx
+			});
+
+			//Iterate through each of the datasets, and build this into a property of the chart
+			helpers.each(data.datasets,function(dataset,datasetIndex){
+
+				var datasetObject = {
+					label : dataset.label || null,
+					fillColor : dataset.fillColor,
+					strokeColor : dataset.strokeColor,
+					bars : []
+				};
+
+				this.datasets.push(datasetObject);
+
+				helpers.each(dataset.data,function(dataPoint,index){
+					//Add a new point for each piece of data, passing any required data to draw.
+					datasetObject.bars.push(new this.BarClass({
+						value : dataPoint,
+						label : data.labels[index],
+						datasetLabel: dataset.label,
+						strokeColor : dataset.strokeColor,
+						fillColor : dataset.fillColor,
+						highlightFill : dataset.highlightFill || dataset.fillColor,
+						highlightStroke : dataset.highlightStroke || dataset.strokeColor
+					}));
+				},this);
+
+			},this);
+
+			this.buildScale(data.labels);
+
+			this.BarClass.prototype.base = this.scale.endPoint;
+
+			this.eachBars(function(bar, index, datasetIndex){
+				helpers.extend(bar, {
+					width : this.scale.calculateBarWidth(this.datasets.length),
+					x: this.scale.calculateBarX(this.datasets.length, datasetIndex, index),
+					y: this.scale.endPoint
+				});
+				bar.save();
+			}, this);
+
+			this.render();
+		},
+		update : function(){
+			this.scale.update();
+			// Reset any highlight colours before updating.
+			helpers.each(this.activeElements, function(activeElement){
+				activeElement.restore(['fillColor', 'strokeColor']);
+			});
+
+			this.eachBars(function(bar){
+				bar.save();
+			});
+			this.render();
+		},
+		eachBars : function(callback){
+			helpers.each(this.datasets,function(dataset, datasetIndex){
+				helpers.each(dataset.bars, callback, this, datasetIndex);
+			},this);
+		},
+		getBarsAtEvent : function(e){
+			var barsArray = [],
+				eventPosition = helpers.getRelativePosition(e),
+				datasetIterator = function(dataset){
+					barsArray.push(dataset.bars[barIndex]);
+				},
+				barIndex;
+
+			for (var datasetIndex = 0; datasetIndex < this.datasets.length; datasetIndex++) {
+				for (barIndex = 0; barIndex < this.datasets[datasetIndex].bars.length; barIndex++) {
+					if (this.datasets[datasetIndex].bars[barIndex].inRange(eventPosition.x,eventPosition.y)){
+						helpers.each(this.datasets, datasetIterator);
+						return barsArray;
+					}
+				}
+			}
+
+			return barsArray;
+		},
+		buildScale : function(labels){
+			var self = this;
+
+			var dataTotal = function(){
+				var values = [];
+				self.eachBars(function(bar){
+					values.push(bar.value);
+				});
+				return values;
+			};
+
+			var scaleOptions = {
+				templateString : this.options.scaleLabel,
+				height : this.chart.height,
+				width : this.chart.width,
+				ctx : this.chart.ctx,
+				textColor : this.options.scaleFontColor,
+				fontSize : this.options.scaleFontSize,
+				fontStyle : this.options.scaleFontStyle,
+				fontFamily : this.options.scaleFontFamily,
+				valuesCount : labels.length,
+				beginAtZero : this.options.scaleBeginAtZero,
+				integersOnly : this.options.scaleIntegersOnly,
+				calculateYRange: function(currentHeight){
+					var updatedRanges = helpers.calculateScaleRange(
+						dataTotal(),
+						currentHeight,
+						this.fontSize,
+						this.beginAtZero,
+						this.integersOnly
+					);
+					helpers.extend(this, updatedRanges);
+				},
+				xLabels : labels,
+				font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily),
+				lineWidth : this.options.scaleLineWidth,
+				lineColor : this.options.scaleLineColor,
+				showHorizontalLines : this.options.scaleShowHorizontalLines,
+				showVerticalLines : this.options.scaleShowVerticalLines,
+				gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0,
+				gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)",
+				padding : (this.options.showScale) ? 0 : (this.options.barShowStroke) ? this.options.barStrokeWidth : 0,
+				showLabels : this.options.scaleShowLabels,
+				display : this.options.showScale
+			};
+
+			if (this.options.scaleOverride){
+				helpers.extend(scaleOptions, {
+					calculateYRange: helpers.noop,
+					steps: this.options.scaleSteps,
+					stepValue: this.options.scaleStepWidth,
+					min: this.options.scaleStartValue,
+					max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth)
+				});
+			}
+
+			this.scale = new this.ScaleClass(scaleOptions);
+		},
+		addData : function(valuesArray,label){
+			//Map the values array for each of the datasets
+			helpers.each(valuesArray,function(value,datasetIndex){
+				//Add a new point for each piece of data, passing any required data to draw.
+				this.datasets[datasetIndex].bars.push(new this.BarClass({
+					value : value,
+					label : label,
+					x: this.scale.calculateBarX(this.datasets.length, datasetIndex, this.scale.valuesCount+1),
+					y: this.scale.endPoint,
+					width : this.scale.calculateBarWidth(this.datasets.length),
+					base : this.scale.endPoint,
+					strokeColor : this.datasets[datasetIndex].strokeColor,
+					fillColor : this.datasets[datasetIndex].fillColor
+				}));
+			},this);
+
+			this.scale.addXLabel(label);
+			//Then re-render the chart.
+			this.update();
+		},
+		removeData : function(){
+			this.scale.removeXLabel();
+			//Then re-render the chart.
+			helpers.each(this.datasets,function(dataset){
+				dataset.bars.shift();
+			},this);
+			this.update();
+		},
+		reflow : function(){
+			helpers.extend(this.BarClass.prototype,{
+				y: this.scale.endPoint,
+				base : this.scale.endPoint
+			});
+			var newScaleProps = helpers.extend({
+				height : this.chart.height,
+				width : this.chart.width
+			});
+			this.scale.update(newScaleProps);
+		},
+		draw : function(ease){
+			var easingDecimal = ease || 1;
+			this.clear();
+
+			var ctx = this.chart.ctx;
+
+			this.scale.draw(easingDecimal);
+
+			//Draw all the bars for each dataset
+			helpers.each(this.datasets,function(dataset,datasetIndex){
+				helpers.each(dataset.bars,function(bar,index){
+					if (bar.hasValue()){
+						bar.base = this.scale.endPoint;
+						//Transition then draw
+						bar.transition({
+							x : this.scale.calculateBarX(this.datasets.length, datasetIndex, index),
+							y : this.scale.calculateY(bar.value),
+							width : this.scale.calculateBarWidth(this.datasets.length)
+						}, easingDecimal).draw();
+					}
+				},this);
+
+			},this);
+		}
+	});
+
+
+}).call(this);
+
+(function(){
+	"use strict";
+
+	var root = this,
+		Chart = root.Chart,
+		//Cache a local reference to Chart.helpers
+		helpers = Chart.helpers;
+
+	var defaultConfig = {
+		//Boolean - Whether we should show a stroke on each segment
+		segmentShowStroke : true,
+
+		//String - The colour of each segment stroke
+		segmentStrokeColor : "#fff",
+
+		//Number - The width of each segment stroke
+		segmentStrokeWidth : 2,
+
+		//The percentage of the chart that we cut out of the middle.
+		percentageInnerCutout : 50,
+
+		//Number - Amount of animation steps
+		animationSteps : 100,
+
+		//String - Animation easing effect
+		animationEasing : "easeOutBounce",
+
+		//Boolean - Whether we animate the rotation of the Doughnut
+		animateRotate : true,
+
+		//Boolean - Whether we animate scaling the Doughnut from the centre
+		animateScale : false,
+
+		//String - A legend template
+		legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li><span style=\"background-color:<%=segments[i].fillColor%>\"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>"
+
+	};
+
+
+	Chart.Type.extend({
+		//Passing in a name registers this chart in the Chart namespace
+		name: "Doughnut",
+		//Providing a defaults will also register the deafults in the chart namespace
+		defaults : defaultConfig,
+		//Initialize is fired when the chart is initialized - Data is passed in as a parameter
+		//Config is automatically merged by the core of Chart.js, and is available at this.options
+		initialize:  function(data){
+
+			//Declare segments as a static property to prevent inheriting across the Chart type prototype
+			this.segments = [];
+			this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) -	this.options.segmentStrokeWidth/2)/2;
+
+			this.SegmentArc = Chart.Arc.extend({
+				ctx : this.chart.ctx,
+				x : this.chart.width/2,
+				y : this.chart.height/2
+			});
+
+			//Set up tooltip events on the chart
+			if (this.options.showTooltips){
+				helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
+					var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : [];
+
+					helpers.each(this.segments,function(segment){
+						segment.restore(["fillColor"]);
+					});
+					helpers.each(activeSegments,function(activeSegment){
+						activeSegment.fillColor = activeSegment.highlightColor;
+					});
+					this.showTooltip(activeSegments);
+				});
+			}
+			this.calculateTotal(data);
+
+			helpers.each(data,function(datapoint, index){
+				this.addData(datapoint, index, true);
+			},this);
+
+			this.render();
+		},
+		getSegmentsAtEvent : function(e){
+			var segmentsArray = [];
+
+			var location = helpers.getRelativePosition(e);
+
+			helpers.each(this.segments,function(segment){
+				if (segment.inRange(location.x,location.y)) segmentsArray.push(segment);
+			},this);
+			return segmentsArray;
+		},
+		addData : function(segment, atIndex, silent){
+			var index = atIndex || this.segments.length;
+			this.segments.splice(index, 0, new this.SegmentArc({
+				value : segment.value,
+				outerRadius : (this.options.animateScale) ? 0 : this.outerRadius,
+				innerRadius : (this.options.animateScale) ? 0 : (this.outerRadius/100) * this.options.percentageInnerCutout,
+				fillColor : segment.color,
+				highlightColor : segment.highlight || segment.color,
+				showStroke : this.options.segmentShowStroke,
+				strokeWidth : this.options.segmentStrokeWidth,
+				strokeColor : this.options.segmentStrokeColor,
+				startAngle : Math.PI * 1.5,
+				circumference : (this.options.animateRotate) ? 0 : this.calculateCircumference(segment.value),
+				label : segment.label
+			}));
+			if (!silent){
+				this.reflow();
+				this.update();
+			}
+		},
+		calculateCircumference : function(value){
+			return (Math.PI*2)*(Math.abs(value) / this.total);
+		},
+		calculateTotal : function(data){
+			this.total = 0;
+			helpers.each(data,function(segment){
+				this.total += Math.abs(segment.value);
+			},this);
+		},
+		update : function(){
+			this.calculateTotal(this.segments);
+
+			// Reset any highlight colours before updating.
+			helpers.each(this.activeElements, function(activeElement){
+				activeElement.restore(['fillColor']);
+			});
+
+			helpers.each(this.segments,function(segment){
+				segment.save();
+			});
+			this.render();
+		},
+
+		removeData: function(atIndex){
+			var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1;
+			this.segments.splice(indexToDelete, 1);
+			this.reflow();
+			this.update();
+		},
+
+		reflow : function(){
+			helpers.extend(this.SegmentArc.prototype,{
+				x : this.chart.width/2,
+				y : this.chart.height/2
+			});
+			this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) -	this.options.segmentStrokeWidth/2)/2;
+			helpers.each(this.segments, function(segment){
+				segment.update({
+					outerRadius : this.outerRadius,
+					innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout
+				});
+			}, this);
+		},
+		draw : function(easeDecimal){
+			var animDecimal = (easeDecimal) ? easeDecimal : 1;
+			this.clear();
+			helpers.each(this.segments,function(segment,index){
+				segment.transition({
+					circumference : this.calculateCircumference(segment.value),
+					outerRadius : this.outerRadius,
+					innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout
+				},animDecimal);
+
+				segment.endAngle = segment.startAngle + segment.circumference;
+
+				segment.draw();
+				if (index === 0){
+					segment.startAngle = Math.PI * 1.5;
+				}
+				//Check to see if it's the last segment, if not get the next and update the start angle
+				if (index < this.segments.length-1){
+					this.segments[index+1].startAngle = segment.endAngle;
+				}
+			},this);
+
+		}
+	});
+
+	Chart.types.Doughnut.extend({
+		name : "Pie",
+		defaults : helpers.merge(defaultConfig,{percentageInnerCutout : 0})
+	});
+
+}).call(this);
+(function(){
+	"use strict";
+
+	var root = this,
+		Chart = root.Chart,
+		helpers = Chart.helpers;
+
+	var defaultConfig = {
+
+		///Boolean - Whether grid lines are shown across the chart
+		scaleShowGridLines : true,
+
+		//String - Colour of the grid lines
+		scaleGridLineColor : "rgba(0,0,0,.05)",
+
+		//Number - Width of the grid lines
+		scaleGridLineWidth : 1,
+
+		//Boolean - Whether to show horizontal lines (except X axis)
+		scaleShowHorizontalLines: true,
+
+		//Boolean - Whether to show vertical lines (except Y axis)
+		scaleShowVerticalLines: true,
+
+		//Boolean - Whether the line is curved between points
+		bezierCurve : true,
+
+		//Number - Tension of the bezier curve between points
+		bezierCurveTension : 0.4,
+
+		//Boolean - Whether to show a dot for each point
+		pointDot : true,
+
+		//Number - Radius of each point dot in pixels
+		pointDotRadius : 4,
+
+		//Number - Pixel width of point dot stroke
+		pointDotStrokeWidth : 1,
+
+		//Number - amount extra to add to the radius to cater for hit detection outside the drawn point
+		pointHitDetectionRadius : 20,
+
+		//Boolean - Whether to show a stroke for datasets
+		datasetStroke : true,
+
+		//Number - Pixel width of dataset stroke
+		datasetStrokeWidth : 2,
+
+		//Boolean - Whether to fill the dataset with a colour
+		datasetFill : true,
+
+		//String - A legend template
+		legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].strokeColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>"
+
+	};
+
+
+	Chart.Type.extend({
+		name: "Line",
+		defaults : defaultConfig,
+		initialize:  function(data){
+			//Declare the extension of the default point, to cater for the options passed in to the constructor
+			this.PointClass = Chart.Point.extend({
+				strokeWidth : this.options.pointDotStrokeWidth,
+				radius : this.options.pointDotRadius,
+				display: this.options.pointDot,
+				hitDetectionRadius : this.options.pointHitDetectionRadius,
+				ctx : this.chart.ctx,
+				inRange : function(mouseX){
+					return (Math.pow(mouseX-this.x, 2) < Math.pow(this.radius + this.hitDetectionRadius,2));
+				}
+			});
+
+			this.datasets = [];
+
+			//Set up tooltip events on the chart
+			if (this.options.showTooltips){
+				helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
+					var activePoints = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : [];
+					this.eachPoints(function(point){
+						point.restore(['fillColor', 'strokeColor']);
+					});
+					helpers.each(activePoints, function(activePoint){
+						activePoint.fillColor = activePoint.highlightFill;
+						activePoint.strokeColor = activePoint.highlightStroke;
+					});
+					this.showTooltip(activePoints);
+				});
+			}
+
+			//Iterate through each of the datasets, and build this into a property of the chart
+			helpers.each(data.datasets,function(dataset){
+
+				var datasetObject = {
+					label : dataset.label || null,
+					fillColor : dataset.fillColor,
+					strokeColor : dataset.strokeColor,
+					pointColor : dataset.pointColor,
+					pointStrokeColor : dataset.pointStrokeColor,
+					points : []
+				};
+
+				this.datasets.push(datasetObject);
+
+
+				helpers.each(dataset.data,function(dataPoint,index){
+					//Add a new point for each piece of data, passing any required data to draw.
+					datasetObject.points.push(new this.PointClass({
+						value : dataPoint,
+						label : data.labels[index],
+						datasetLabel: dataset.label,
+						strokeColor : dataset.pointStrokeColor,
+						fillColor : dataset.pointColor,
+						highlightFill : dataset.pointHighlightFill || dataset.pointColor,
+						highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor
+					}));
+				},this);
+
+				this.buildScale(data.labels);
+
+
+				this.eachPoints(function(point, index){
+					helpers.extend(point, {
+						x: this.scale.calculateX(index),
+						y: this.scale.endPoint
+					});
+					point.save();
+				}, this);
+
+			},this);
+
+
+			this.render();
+		},
+		update : function(){
+			this.scale.update();
+			// Reset any highlight colours before updating.
+			helpers.each(this.activeElements, function(activeElement){
+				activeElement.restore(['fillColor', 'strokeColor']);
+			});
+			this.eachPoints(function(point){
+				point.save();
+			});
+			this.render();
+		},
+		eachPoints : function(callback){
+			helpers.each(this.datasets,function(dataset){
+				helpers.each(dataset.points,callback,this);
+			},this);
+		},
+		getPointsAtEvent : function(e){
+			var pointsArray = [],
+				eventPosition = helpers.getRelativePosition(e);
+			helpers.each(this.datasets,function(dataset){
+				helpers.each(dataset.points,function(point){
+					if (point.inRange(eventPosition.x,eventPosition.y)) pointsArray.push(point);
+				});
+			},this);
+			return pointsArray;
+		},
+		buildScale : function(labels){
+			var self = this;
+
+			var dataTotal = function(){
+				var values = [];
+				self.eachPoints(function(point){
+					values.push(point.value);
+				});
+
+				return values;
+			};
+
+			var scaleOptions = {
+				templateString : this.options.scaleLabel,
+				height : this.chart.height,
+				width : this.chart.width,
+				ctx : this.chart.ctx,
+				textColor : this.options.scaleFontColor,
+				fontSize : this.options.scaleFontSize,
+				fontStyle : this.options.scaleFontStyle,
+				fontFamily : this.options.scaleFontFamily,
+				valuesCount : labels.length,
+				beginAtZero : this.options.scaleBeginAtZero,
+				integersOnly : this.options.scaleIntegersOnly,
+				calculateYRange : function(currentHeight){
+					var updatedRanges = helpers.calculateScaleRange(
+						dataTotal(),
+						currentHeight,
+						this.fontSize,
+						this.beginAtZero,
+						this.integersOnly
+					);
+					helpers.extend(this, updatedRanges);
+				},
+				xLabels : labels,
+				font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily),
+				lineWidth : this.options.scaleLineWidth,
+				lineColor : this.options.scaleLineColor,
+				showHorizontalLines : this.options.scaleShowHorizontalLines,
+				showVerticalLines : this.options.scaleShowVerticalLines,
+				gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0,
+				gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)",
+				padding: (this.options.showScale) ? 0 : this.options.pointDotRadius + this.options.pointDotStrokeWidth,
+				showLabels : this.options.scaleShowLabels,
+				display : this.options.showScale
+			};
+
+			if (this.options.scaleOverride){
+				helpers.extend(scaleOptions, {
+					calculateYRange: helpers.noop,
+					steps: this.options.scaleSteps,
+					stepValue: this.options.scaleStepWidth,
+					min: this.options.scaleStartValue,
+					max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth)
+				});
+			}
+
+
+			this.scale = new Chart.Scale(scaleOptions);
+		},
+		addData : function(valuesArray,label){
+			//Map the values array for each of the datasets
+
+			helpers.each(valuesArray,function(value,datasetIndex){
+				//Add a new point for each piece of data, passing any required data to draw.
+				this.datasets[datasetIndex].points.push(new this.PointClass({
+					value : value,
+					label : label,
+					x: this.scale.calculateX(this.scale.valuesCount+1),
+					y: this.scale.endPoint,
+					strokeColor : this.datasets[datasetIndex].pointStrokeColor,
+					fillColor : this.datasets[datasetIndex].pointColor
+				}));
+			},this);
+
+			this.scale.addXLabel(label);
+			//Then re-render the chart.
+			this.update();
+		},
+		removeData : function(){
+			this.scale.removeXLabel();
+			//Then re-render the chart.
+			helpers.each(this.datasets,function(dataset){
+				dataset.points.shift();
+			},this);
+			this.update();
+		},
+		reflow : function(){
+			var newScaleProps = helpers.extend({
+				height : this.chart.height,
+				width : this.chart.width
+			});
+			this.scale.update(newScaleProps);
+		},
+		draw : function(ease){
+			var easingDecimal = ease || 1;
+			this.clear();
+
+			var ctx = this.chart.ctx;
+
+			// Some helper methods for getting the next/prev points
+			var hasValue = function(item){
+				return item.value !== null;
+			},
+			nextPoint = function(point, collection, index){
+				return helpers.findNextWhere(collection, hasValue, index) || point;
+			},
+			previousPoint = function(point, collection, index){
+				return helpers.findPreviousWhere(collection, hasValue, index) || point;
+			};
+
+			this.scale.draw(easingDecimal);
+
+
+			helpers.each(this.datasets,function(dataset){
+				var pointsWithValues = helpers.where(dataset.points, hasValue);
+
+				//Transition each point first so that the line and point drawing isn't out of sync
+				//We can use this extra loop to calculate the control points of this dataset also in this loop
+
+				helpers.each(dataset.points, function(point, index){
+					if (point.hasValue()){
+						point.transition({
+							y : this.scale.calculateY(point.value),
+							x : this.scale.calculateX(index)
+						}, easingDecimal);
+					}
+				},this);
+
+
+				// Control points need to be calculated in a seperate loop, because we need to know the current x/y of the point
+				// This would cause issues when there is no animation, because the y of the next point would be 0, so beziers would be skewed
+				if (this.options.bezierCurve){
+					helpers.each(pointsWithValues, function(point, index){
+						var tension = (index > 0 && index < pointsWithValues.length - 1) ? this.options.bezierCurveTension : 0;
+						point.controlPoints = helpers.splineCurve(
+							previousPoint(point, pointsWithValues, index),
+							point,
+							nextPoint(point, pointsWithValues, index),
+							tension
+						);
+
+						// Prevent the bezier going outside of the bounds of the graph
+
+						// Cap puter bezier handles to the upper/lower scale bounds
+						if (point.controlPoints.outer.y > this.scale.endPoint){
+							point.controlPoints.outer.y = this.scale.endPoint;
+						}
+						else if (point.controlPoints.outer.y < this.scale.startPoint){
+							point.controlPoints.outer.y = this.scale.startPoint;
+						}
+
+						// Cap inner bezier handles to the upper/lower scale bounds
+						if (point.controlPoints.inner.y > this.scale.endPoint){
+							point.controlPoints.inner.y = this.scale.endPoint;
+						}
+						else if (point.controlPoints.inner.y < this.scale.startPoint){
+							point.controlPoints.inner.y = this.scale.startPoint;
+						}
+					},this);
+				}
+
+
+				//Draw the line between all the points
+				ctx.lineWidth = this.options.datasetStrokeWidth;
+				ctx.strokeStyle = dataset.strokeColor;
+				ctx.beginPath();
+
+				helpers.each(pointsWithValues, function(point, index){
+					if (index === 0){
+						ctx.moveTo(point.x, point.y);
+					}
+					else{
+						if(this.options.bezierCurve){
+							var previous = previousPoint(point, pointsWithValues, index);
+
+							ctx.bezierCurveTo(
+								previous.controlPoints.outer.x,
+								previous.controlPoints.outer.y,
+								point.controlPoints.inner.x,
+								point.controlPoints.inner.y,
+								point.x,
+								point.y
+							);
+						}
+						else{
+							ctx.lineTo(point.x,point.y);
+						}
+					}
+				}, this);
+
+				ctx.stroke();
+
+				if (this.options.datasetFill && pointsWithValues.length > 0){
+					//Round off the line by going to the base of the chart, back to the start, then fill.
+					ctx.lineTo(pointsWithValues[pointsWithValues.length - 1].x, this.scale.endPoint);
+					ctx.lineTo(pointsWithValues[0].x, this.scale.endPoint);
+					ctx.fillStyle = dataset.fillColor;
+					ctx.closePath();
+					ctx.fill();
+				}
+
+				//Now draw the points over the line
+				//A little inefficient double looping, but better than the line
+				//lagging behind the point positions
+				helpers.each(pointsWithValues,function(point){
+					point.draw();
+				});
+			},this);
+		}
+	});
+
+
+}).call(this);
+
+(function(){
+	"use strict";
+
+	var root = this,
+		Chart = root.Chart,
+		//Cache a local reference to Chart.helpers
+		helpers = Chart.helpers;
+
+	var defaultConfig = {
+		//Boolean - Show a backdrop to the scale label
+		scaleShowLabelBackdrop : true,
+
+		//String - The colour of the label backdrop
+		scaleBackdropColor : "rgba(255,255,255,0.75)",
+
+		// Boolean - Whether the scale should begin at zero
+		scaleBeginAtZero : true,
+
+		//Number - The backdrop padding above & below the label in pixels
+		scaleBackdropPaddingY : 2,
+
+		//Number - The backdrop padding to the side of the label in pixels
+		scaleBackdropPaddingX : 2,
+
+		//Boolean - Show line for each value in the scale
+		scaleShowLine : true,
+
+		//Boolean - Stroke a line around each segment in the chart
+		segmentShowStroke : true,
+
+		//String - The colour of the stroke on each segement.
+		segmentStrokeColor : "#fff",
+
+		//Number - The width of the stroke value in pixels
+		segmentStrokeWidth : 2,
+
+		//Number - Amount of animation steps
+		animationSteps : 100,
+
+		//String - Animation easing effect.
+		animationEasing : "easeOutBounce",
+
+		//Boolean - Whether to animate the rotation of the chart
+		animateRotate : true,
+
+		//Boolean - Whether to animate scaling the chart from the centre
+		animateScale : false,
+
+		//String - A legend template
+		legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li><span style=\"background-color:<%=segments[i].fillColor%>\"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>"
+	};
+
+
+	Chart.Type.extend({
+		//Passing in a name registers this chart in the Chart namespace
+		name: "PolarArea",
+		//Providing a defaults will also register the deafults in the chart namespace
+		defaults : defaultConfig,
+		//Initialize is fired when the chart is initialized - Data is passed in as a parameter
+		//Config is automatically merged by the core of Chart.js, and is available at this.options
+		initialize:  function(data){
+			this.segments = [];
+			//Declare segment class as a chart instance specific class, so it can share props for this instance
+			this.SegmentArc = Chart.Arc.extend({
+				showStroke : this.options.segmentShowStroke,
+				strokeWidth : this.options.segmentStrokeWidth,
+				strokeColor : this.options.segmentStrokeColor,
+				ctx : this.chart.ctx,
+				innerRadius : 0,
+				x : this.chart.width/2,
+				y : this.chart.height/2
+			});
+			this.scale = new Chart.RadialScale({
+				display: this.options.showScale,
+				fontStyle: this.options.scaleFontStyle,
+				fontSize: this.options.scaleFontSize,
+				fontFamily: this.options.scaleFontFamily,
+				fontColor: this.options.scaleFontColor,
+				showLabels: this.options.scaleShowLabels,
+				showLabelBackdrop: this.options.scaleShowLabelBackdrop,
+				backdropColor: this.options.scaleBackdropColor,
+				backdropPaddingY : this.options.scaleBackdropPaddingY,
+				backdropPaddingX: this.options.scaleBackdropPaddingX,
+				lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0,
+				lineColor: this.options.scaleLineColor,
+				lineArc: true,
+				width: this.chart.width,
+				height: this.chart.height,
+				xCenter: this.chart.width/2,
+				yCenter: this.chart.height/2,
+				ctx : this.chart.ctx,
+				templateString: this.options.scaleLabel,
+				valuesCount: data.length
+			});
+
+			this.updateScaleRange(data);
+
+			this.scale.update();
+
+			helpers.each(data,function(segment,index){
+				this.addData(segment,index,true);
+			},this);
+
+			//Set up tooltip events on the chart
+			if (this.options.showTooltips){
+				helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
+					var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : [];
+					helpers.each(this.segments,function(segment){
+						segment.restore(["fillColor"]);
+					});
+					helpers.each(activeSegments,function(activeSegment){
+						activeSegment.fillColor = activeSegment.highlightColor;
+					});
+					this.showTooltip(activeSegments);
+				});
+			}
+
+			this.render();
+		},
+		getSegmentsAtEvent : function(e){
+			var segmentsArray = [];
+
+			var location = helpers.getRelativePosition(e);
+
+			helpers.each(this.segments,function(segment){
+				if (segment.inRange(location.x,location.y)) segmentsArray.push(segment);
+			},this);
+			return segmentsArray;
+		},
+		addData : function(segment, atIndex, silent){
+			var index = atIndex || this.segments.length;
+
+			this.segments.splice(index, 0, new this.SegmentArc({
+				fillColor: segment.color,
+				highlightColor: segment.highlight || segment.color,
+				label: segment.label,
+				value: segment.value,
+				outerRadius: (this.options.animateScale) ? 0 : this.scale.calculateCenterOffset(segment.value),
+				circumference: (this.options.animateRotate) ? 0 : this.scale.getCircumference(),
+				startAngle: Math.PI * 1.5
+			}));
+			if (!silent){
+				this.reflow();
+				this.update();
+			}
+		},
+		removeData: function(atIndex){
+			var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1;
+			this.segments.splice(indexToDelete, 1);
+			this.reflow();
+			this.update();
+		},
+		calculateTotal: function(data){
+			this.total = 0;
+			helpers.each(data,function(segment){
+				this.total += segment.value;
+			},this);
+			this.scale.valuesCount = this.segments.length;
+		},
+		updateScaleRange: function(datapoints){
+			var valuesArray = [];
+			helpers.each(datapoints,function(segment){
+				valuesArray.push(segment.value);
+			});
+
+			var scaleSizes = (this.options.scaleOverride) ?
+				{
+					steps: this.options.scaleSteps,
+					stepValue: this.options.scaleStepWidth,
+					min: this.options.scaleStartValue,
+					max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth)
+				} :
+				helpers.calculateScaleRange(
+					valuesArray,
+					helpers.min([this.chart.width, this.chart.height])/2,
+					this.options.scaleFontSize,
+					this.options.scaleBeginAtZero,
+					this.options.scaleIntegersOnly
+				);
+
+			helpers.extend(
+				this.scale,
+				scaleSizes,
+				{
+					size: helpers.min([this.chart.width, this.chart.height]),
+					xCenter: this.chart.width/2,
+					yCenter: this.chart.height/2
+				}
+			);
+
+		},
+		update : function(){
+			this.calculateTotal(this.segments);
+
+			helpers.each(this.segments,function(segment){
+				segment.save();
+			});
+			
+			this.reflow();
+			this.render();
+		},
+		reflow : function(){
+			helpers.extend(this.SegmentArc.prototype,{
+				x : this.chart.width/2,
+				y : this.chart.height/2
+			});
+			this.updateScaleRange(this.segments);
+			this.scale.update();
+
+			helpers.extend(this.scale,{
+				xCenter: this.chart.width/2,
+				yCenter: this.chart.height/2
+			});
+
+			helpers.each(this.segments, function(segment){
+				segment.update({
+					outerRadius : this.scale.calculateCenterOffset(segment.value)
+				});
+			}, this);
+
+		},
+		draw : function(ease){
+			var easingDecimal = ease || 1;
+			//Clear & draw the canvas
+			this.clear();
+			helpers.each(this.segments,function(segment, index){
+				segment.transition({
+					circumference : this.scale.getCircumference(),
+					outerRadius : this.scale.calculateCenterOffset(segment.value)
+				},easingDecimal);
+
+				segment.endAngle = segment.startAngle + segment.circumference;
+
+				// If we've removed the first segment we need to set the first one to
+				// start at the top.
+				if (index === 0){
+					segment.startAngle = Math.PI * 1.5;
+				}
+
+				//Check to see if it's the last segment, if not get the next and update the start angle
+				if (index < this.segments.length - 1){
+					this.segments[index+1].startAngle = segment.endAngle;
+				}
+				segment.draw();
+			}, this);
+			this.scale.draw();
+		}
+	});
+
+}).call(this);
+(function(){
+	"use strict";
+
+	var root = this,
+		Chart = root.Chart,
+		helpers = Chart.helpers;
+
+
+
+	Chart.Type.extend({
+		name: "Radar",
+		defaults:{
+			//Boolean - Whether to show lines for each scale point
+			scaleShowLine : true,
+
+			//Boolean - Whether we show the angle lines out of the radar
+			angleShowLineOut : true,
+
+			//Boolean - Whether to show labels on the scale
+			scaleShowLabels : false,
+
+			// Boolean - Whether the scale should begin at zero
+			scaleBeginAtZero : true,
+
+			//String - Colour of the angle line
+			angleLineColor : "rgba(0,0,0,.1)",
+
+			//Number - Pixel width of the angle line
+			angleLineWidth : 1,
+
+			//String - Point label font declaration
+			pointLabelFontFamily : "'Arial'",
+
+			//String - Point label font weight
+			pointLabelFontStyle : "normal",
+
+			//Number - Point label font size in pixels
+			pointLabelFontSize : 10,
+
+			//String - Point label font colour
+			pointLabelFontColor : "#666",
+
+			//Boolean - Whether to show a dot for each point
+			pointDot : true,
+
+			//Number - Radius of each point dot in pixels
+			pointDotRadius : 3,
+
+			//Number - Pixel width of point dot stroke
+			pointDotStrokeWidth : 1,
+
+			//Number - amount extra to add to the radius to cater for hit detection outside the drawn point
+			pointHitDetectionRadius : 20,
+
+			//Boolean - Whether to show a stroke for datasets
+			datasetStroke : true,
+
+			//Number - Pixel width of dataset stroke
+			datasetStrokeWidth : 2,
+
+			//Boolean - Whether to fill the dataset with a colour
+			datasetFill : true,
+
+			//String - A legend template
+			legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].strokeColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>"
+
+		},
+
+		initialize: function(data){
+			this.PointClass = Chart.Point.extend({
+				strokeWidth : this.options.pointDotStrokeWidth,
+				radius : this.options.pointDotRadius,
+				display: this.options.pointDot,
+				hitDetectionRadius : this.options.pointHitDetectionRadius,
+				ctx : this.chart.ctx
+			});
+
+			this.datasets = [];
+
+			this.buildScale(data);
+
+			//Set up tooltip events on the chart
+			if (this.options.showTooltips){
+				helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
+					var activePointsCollection = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : [];
+
+					this.eachPoints(function(point){
+						point.restore(['fillColor', 'strokeColor']);
+					});
+					helpers.each(activePointsCollection, function(activePoint){
+						activePoint.fillColor = activePoint.highlightFill;
+						activePoint.strokeColor = activePoint.highlightStroke;
+					});
+
+					this.showTooltip(activePointsCollection);
+				});
+			}
+
+			//Iterate through each of the datasets, and build this into a property of the chart
+			helpers.each(data.datasets,function(dataset){
+
+				var datasetObject = {
+					label: dataset.label || null,
+					fillColor : dataset.fillColor,
+					strokeColor : dataset.strokeColor,
+					pointColor : dataset.pointColor,
+					pointStrokeColor : dataset.pointStrokeColor,
+					points : []
+				};
+
+				this.datasets.push(datasetObject);
+
+				helpers.each(dataset.data,function(dataPoint,index){
+					//Add a new point for each piece of data, passing any required data to draw.
+					var pointPosition;
+					if (!this.scale.animation){
+						pointPosition = this.scale.getPointPosition(index, this.scale.calculateCenterOffset(dataPoint));
+					}
+					datasetObject.points.push(new this.PointClass({
+						value : dataPoint,
+						label : data.labels[index],
+						datasetLabel: dataset.label,
+						x: (this.options.animation) ? this.scale.xCenter : pointPosition.x,
+						y: (this.options.animation) ? this.scale.yCenter : pointPosition.y,
+						strokeColor : dataset.pointStrokeColor,
+						fillColor : dataset.pointColor,
+						highlightFill : dataset.pointHighlightFill || dataset.pointColor,
+						highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor
+					}));
+				},this);
+
+			},this);
+
+			this.render();
+		},
+		eachPoints : function(callback){
+			helpers.each(this.datasets,function(dataset){
+				helpers.each(dataset.points,callback,this);
+			},this);
+		},
+
+		getPointsAtEvent : function(evt){
+			var mousePosition = helpers.getRelativePosition(evt),
+				fromCenter = helpers.getAngleFromPoint({
+					x: this.scale.xCenter,
+					y: this.scale.yCenter
+				}, mousePosition);
+
+			var anglePerIndex = (Math.PI * 2) /this.scale.valuesCount,
+				pointIndex = Math.round((fromCenter.angle - Math.PI * 1.5) / anglePerIndex),
+				activePointsCollection = [];
+
+			// If we're at the top, make the pointIndex 0 to get the first of the array.
+			if (pointIndex >= this.scale.valuesCount || pointIndex < 0){
+				pointIndex = 0;
+			}
+
+			if (fromCenter.distance <= this.scale.drawingArea){
+				helpers.each(this.datasets, function(dataset){
+					activePointsCollection.push(dataset.points[pointIndex]);
+				});
+			}
+
+			return activePointsCollection;
+		},
+
+		buildScale : function(data){
+			this.scale = new Chart.RadialScale({
+				display: this.options.showScale,
+				fontStyle: this.options.scaleFontStyle,
+				fontSize: this.options.scaleFontSize,
+				fontFamily: this.options.scaleFontFamily,
+				fontColor: this.options.scaleFontColor,
+				showLabels: this.options.scaleShowLabels,
+				showLabelBackdrop: this.options.scaleShowLabelBackdrop,
+				backdropColor: this.options.scaleBackdropColor,
+				backdropPaddingY : this.options.scaleBackdropPaddingY,
+				backdropPaddingX: this.options.scaleBackdropPaddingX,
+				lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0,
+				lineColor: this.options.scaleLineColor,
+				angleLineColor : this.options.angleLineColor,
+				angleLineWidth : (this.options.angleShowLineOut) ? this.options.angleLineWidth : 0,
+				// Point labels at the edge of each line
+				pointLabelFontColor : this.options.pointLabelFontColor,
+				pointLabelFontSize : this.options.pointLabelFontSize,
+				pointLabelFontFamily : this.options.pointLabelFontFamily,
+				pointLabelFontStyle : this.options.pointLabelFontStyle,
+				height : this.chart.height,
+				width: this.chart.width,
+				xCenter: this.chart.width/2,
+				yCenter: this.chart.height/2,
+				ctx : this.chart.ctx,
+				templateString: this.options.scaleLabel,
+				labels: data.labels,
+				valuesCount: data.datasets[0].data.length
+			});
+
+			this.scale.setScaleSize();
+			this.updateScaleRange(data.datasets);
+			this.scale.buildYLabels();
+		},
+		updateScaleRange: function(datasets){
+			var valuesArray = (function(){
+				var totalDataArray = [];
+				helpers.each(datasets,function(dataset){
+					if (dataset.data){
+						totalDataArray = totalDataArray.concat(dataset.data);
+					}
+					else {
+						helpers.each(dataset.points, function(point){
+							totalDataArray.push(point.value);
+						});
+					}
+				});
+				return totalDataArray;
+			})();
+
+
+			var scaleSizes = (this.options.scaleOverride) ?
+				{
+					steps: this.options.scaleSteps,
+					stepValue: this.options.scaleStepWidth,
+					min: this.options.scaleStartValue,
+					max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth)
+				} :
+				helpers.calculateScaleRange(
+					valuesArray,
+					helpers.min([this.chart.width, this.chart.height])/2,
+					this.options.scaleFontSize,
+					this.options.scaleBeginAtZero,
+					this.options.scaleIntegersOnly
+				);
+
+			helpers.extend(
+				this.scale,
+				scaleSizes
+			);
+
+		},
+		addData : function(valuesArray,label){
+			//Map the values array for each of the datasets
+			this.scale.valuesCount++;
+			helpers.each(valuesArray,function(value,datasetIndex){
+				var pointPosition = this.scale.getPointPosition(this.scale.valuesCount, this.scale.calculateCenterOffset(value));
+				this.datasets[datasetIndex].points.push(new this.PointClass({
+					value : value,
+					label : label,
+					x: pointPosition.x,
+					y: pointPosition.y,
+					strokeColor : this.datasets[datasetIndex].pointStrokeColor,
+					fillColor : this.datasets[datasetIndex].pointColor
+				}));
+			},this);
+
+			this.scale.labels.push(label);
+
+			this.reflow();
+
+			this.update();
+		},
+		removeData : function(){
+			this.scale.valuesCount--;
+			this.scale.labels.shift();
+			helpers.each(this.datasets,function(dataset){
+				dataset.points.shift();
+			},this);
+			this.reflow();
+			this.update();
+		},
+		update : function(){
+			this.eachPoints(function(point){
+				point.save();
+			});
+			this.reflow();
+			this.render();
+		},
+		reflow: function(){
+			helpers.extend(this.scale, {
+				width : this.chart.width,
+				height: this.chart.height,
+				size : helpers.min([this.chart.width, this.chart.height]),
+				xCenter: this.chart.width/2,
+				yCenter: this.chart.height/2
+			});
+			this.updateScaleRange(this.datasets);
+			this.scale.setScaleSize();
+			this.scale.buildYLabels();
+		},
+		draw : function(ease){
+			var easeDecimal = ease || 1,
+				ctx = this.chart.ctx;
+			this.clear();
+			this.scale.draw();
+
+			helpers.each(this.datasets,function(dataset){
+
+				//Transition each point first so that the line and point drawing isn't out of sync
+				helpers.each(dataset.points,function(point,index){
+					if (point.hasValue()){
+						point.transition(this.scale.getPointPosition(index, this.scale.calculateCenterOffset(point.value)), easeDecimal);
+					}
+				},this);
+
+
+
+				//Draw the line between all the points
+				ctx.lineWidth = this.options.datasetStrokeWidth;
+				ctx.strokeStyle = dataset.strokeColor;
+				ctx.beginPath();
+				helpers.each(dataset.points,function(point,index){
+					if (index === 0){
+						ctx.moveTo(point.x,point.y);
+					}
+					else{
+						ctx.lineTo(point.x,point.y);
+					}
+				},this);
+				ctx.closePath();
+				ctx.stroke();
+
+				ctx.fillStyle = dataset.fillColor;
+				ctx.fill();
+
+				//Now draw the points over the line
+				//A little inefficient double looping, but better than the line
+				//lagging behind the point positions
+				helpers.each(dataset.points,function(point){
+					if (point.hasValue()){
+						point.draw();
+					}
+				});
+
+			},this);
+
+		}
+
+	});
+
+
+
+
+
+}).call(this);

File diff suppressed because it is too large
+ 9 - 0
jet/dashboard/static/jet.dashboard/vendor/chart.js/Chart.min.js


+ 7 - 0
jet/dashboard/static/jet.dashboard/vendor/chart.js/LICENSE.md

@@ -0,0 +1,7 @@
+Copyright (c) 2013-2015 Nick Downie
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 20 - 0
jet/dashboard/static/jet.dashboard/vendor/chart.js/README.md

@@ -0,0 +1,20 @@
+# Chart.js 
+
+[![Build Status](https://travis-ci.org/nnnick/Chart.js.svg?branch=master)](https://travis-ci.org/nnnick/Chart.js) [![Code Climate](https://codeclimate.com/github/nnnick/Chart.js/badges/gpa.svg)](https://codeclimate.com/github/nnnick/Chart.js)
+
+
+*Simple HTML5 Charts using the canvas element* [chartjs.org](http://www.chartjs.org)
+
+## Documentation
+
+You can find documentation at [chartjs.org/docs](http://www.chartjs.org/docs/). The markdown files that build the site are available under `/docs`. Please note - in some of the json examples of configuration you might notice some liquid tags - this is just for the generating the site html, please disregard.
+
+## Bugs, issues and contributing
+
+Before submitting an issue or a pull request to the project, please take a moment to look over the [contributing guidelines](https://github.com/nnnick/Chart.js/blob/master/CONTRIBUTING.md) first.
+
+For support using Chart.js, please post questions with the [`chartjs` tag on Stack Overflow](http://stackoverflow.com/questions/tagged/chartjs).
+
+## License
+
+Chart.js is available under the [MIT license](http://opensource.org/licenses/MIT).

+ 2 - 2
jet/templates/admin/app_index.html → jet/dashboard/templates/admin/app_index.html

@@ -1,5 +1,5 @@
 {% extends "admin/base_site.html" %}
-{% load i18n static jet_tags %}
+{% load i18n static jet_dashboard_tags %}
 
 {% block html %}
     {% get_dashboard 'app_index' as dashboard %}
@@ -24,7 +24,7 @@
 
 {% block coltype %}colMS{% endblock %}
 
-{% block bodyclass %}{{ block.super }} app-{{ app_label }} dashboard{% endblock %}
+{% block bodyclass %}{{ block.super }} app-{{ app_label }}{% endblock %}
 
 {% if not is_popup %}
     {% block breadcrumbs %}

+ 35 - 0
jet/dashboard/templates/admin/index.html

@@ -0,0 +1,35 @@
+{% extends "admin/base_site.html" %}
+{% load i18n admin_static jet_dashboard_tags static %}
+
+{% block html %}
+    {% get_dashboard 'index' as dashboard %}
+    {{ block.super }}
+{% endblock %}
+
+{% block extrastyle %}
+    {{ block.super }}
+    <link rel="stylesheet" type="text/css" href="{% static "admin/css/dashboard.css" %}" />
+
+    {% for css in dashboard.media.css %}
+        <link href="{% static css %}" rel="stylesheet" />
+    {% endfor %}
+{% endblock %}
+
+{% block extrahead %}
+    {{ block.super }}
+    {% for js in dashboard.media.js %}
+        <script src="{% static js %}"></script>
+    {% endfor %}
+{% endblock %}
+
+{% block coltype %}colMS{% endblock %}
+
+{% block sidebar %}{% endblock %}
+
+{% block top-right %}
+    {{ dashboard.render_tools }}
+{% endblock %}
+
+{% block content %}
+    {{ dashboard.render }}
+{% endblock %}

+ 4 - 4
jet/templates/jet/dashboard/dashboard.html → jet/dashboard/templates/jet.dashboard/dashboard.html

@@ -6,7 +6,7 @@
             <div class="dashboard-column{% if forloop.first %} first{% endif %}">
                 {% for module in modules %}
                     {% if module.model.column == i %}
-                        {% include "jet/dashboard/module.html" with module=module %}
+                        {% include "jet.dashboard/module.html" with module=module %}
                     {% endif %}
                 {% endfor %}
             </div>
@@ -18,19 +18,19 @@
     <p>{% trans "Are you sure want to delete this widget?" %}</p>
 </div>
 
-<form action="{% url "jet:update_dashboard_modules" %}" method="POST" id="update-dashboard-modules-form">
+<form action="{% url "jet-dashboard:update_dashboard_modules" %}" method="POST" id="update-dashboard-modules-form">
     {% csrf_token %}
     <input type="hidden" name="app_label" value="{% if app_label %}{{ app_label }}{% endif %}">
     <input type="hidden" name="modules">
 </form>
 
-<form action="{% url "jet:update_dashboard_module_collapse" %}" method="POST" id="update-dashboard-module-collapse-form">
+<form action="{% url "jet-dashboard:update_dashboard_module_collapse" %}" method="POST" id="update-dashboard-module-collapse-form">
     {% csrf_token %}
     <input type="hidden" name="id">
     <input type="hidden" name="collapsed">
 </form>
 
-<form action="{% url "jet:remove_dashboard_module" %}" method="POST" id="remove-dashboard-module-form">
+<form action="{% url "jet-dashboard:remove_dashboard_module" %}" method="POST" id="remove-dashboard-module-form">
     {% csrf_token %}
     <input type="hidden" name="id">
 </form>

+ 1 - 1
jet/templates/jet/dashboard/dashboard_tools.html → jet/dashboard/templates/jet.dashboard/dashboard_tools.html

@@ -1,7 +1,7 @@
 {% load i18n %}
 
 <div class="background-form">
-    <form action="{% url "jet:add_user_dashboard_module" %}" method="POST" id="add-dashboard-module-form">
+    <form action="{% url "jet-dashboard:add_user_dashboard_module" %}" method="POST" id="add-dashboard-module-form">
         {% csrf_token %}
         <select class="add-dashboard" name="module">
             <option>{% trans "widgets" %}</option>

+ 3 - 3
jet/templates/jet/dashboard/module.html → jet/dashboard/templates/jet.dashboard/module.html

@@ -1,6 +1,6 @@
 {% load i18n %}
 
-<div class="dashboard-item{% if module.collapsible %} collapsible{% endif %}{% if module.model.collapsed %} collapsed{% endif %}{% if module.deletable %} deletable{% endif %}{% if module.ajax_load %} ajax{% endif %}"{% if module.ajax_load %} data-ajax-url="{% url "jet:load_dashboard_module" pk=module.model.id %}"{% endif %} data-module-id="{{ module.model.id }}">
+<div class="dashboard-item{% if module.collapsible %} collapsible{% endif %}{% if module.model.collapsed %} collapsed{% endif %}{% if module.deletable %} deletable{% endif %}{% if module.ajax_load %} ajax{% endif %}"{% if module.ajax_load %} data-ajax-url="{% url "jet-dashboard:load_dashboard_module" pk=module.model.id %}"{% endif %} data-module-id="{{ module.model.id }}">
     <div class="dashboard-item-header">
         <span class="dashboard-item-header-drag icon-grid"></span>
         <span class="dashboard-item-header-title">
@@ -16,7 +16,7 @@
             {% endif %}
 
             <span class="dashboard-item-header-buttons">
-                <a href="{% url "jet:update_module" pk=module.model.id %}" title="{% trans "Change" %}"><span class="icon-edit"></span></a>
+                <a href="{% url "jet-dashboard:update_module" pk=module.model.id %}" title="{% trans "Change" %}"><span class="icon-edit"></span></a>
 
                 {% if module.deletable %}
                     <a href="#" title="{% trans "Delete" %}" class="dashboard-item-remove"><span class="icon-cross"></span></a>
@@ -25,7 +25,7 @@
         </span>
     </div>
 
-    <div class="dashboard-item-content">
+    <div class="dashboard-item-content{% if module.contrast %} contrast{% endif %}"{% if module.style %} style="{{ module.style }}"{% endif %}>
         {% if module.ajax_load %}
             <div class="loading-indicator-wrapper">
                 <span class="icon-refresh loading-indicator"></span>

+ 0 - 0
jet/templates/jet/dashboard/modules/app_list.html → jet/dashboard/templates/jet.dashboard/modules/app_list.html


+ 0 - 0
jet/templates/jet/dashboard/modules/feed.html → jet/dashboard/templates/jet.dashboard/modules/feed.html


+ 38 - 0
jet/dashboard/templates/jet.dashboard/modules/google_analytics_period_visitors.html

@@ -0,0 +1,38 @@
+{% load i18n %}
+
+
+{% if module.error %}
+    <ul>
+        <li>
+            {{ module.error }}
+        </li>
+    </ul>
+{% elif module.children %}
+    <table class="table">
+        <thead>
+            <tr>
+                <th>{% trans "Date" %}</th>
+                <th>{% trans "Users" %}</th>
+                <th>{% trans "Sessions" %}</th>
+                <th>{% trans "Views" %}</th>
+            </tr>
+        </thead>
+        <tbody>
+            {% for data in module.children %}
+                <tr>
+                    <th>{{ data.0 }}</th>
+                    <td width="1" align="center">{{ data.1.ga_users }}</td>
+                    <td width="1" align="center">{{ data.1.ga_sessions }}</td>
+                    <td width="1" align="center">{{ data.1.ga_pageviews }}</td>
+                </tr>
+            {% endfor %}
+        </tbody>
+    </table>
+{% else %}
+    <ul>
+        <li>
+            {% trans "Nothing to show" %}
+        </li>
+    </ul>
+{% endif %}
+

+ 33 - 0
jet/dashboard/templates/jet.dashboard/modules/google_analytics_visitors_chart.html

@@ -0,0 +1,33 @@
+{% load i18n %}
+
+{% if module.error %}
+    <ul>
+        <li>
+            {{ module.error }}
+        </li>
+    </ul>
+{% elif module.children %}
+    <div class="padding center">
+        <canvas id="chart_{{ module.model.pk }}" style="width: 100%;">
+            <div class="chart-fillColor"></div>
+            <div class="chart-strokeColor"></div>
+            <div class="chart-pointColor"></div>
+            <div class="chart-pointHighlightFill"></div>
+            <div class="chart-scaleGridLineColor"></div>
+            <div class="chart-scaleLineColor"></div>
+            <div class="chart-scaleFontColor"></div>
+            <div class="chart-data">
+                {% for data in module.children %}
+                    <div class="chart-data-item" data-date="{{ data.0|date:"d/m" }}" data-value="{{ data.1 }}"></div>
+                {% endfor %}
+            </div>
+        </canvas>
+        <script>jet.jQuery('#chart_{{ module.model.pk }}').googleAnalyticsChart();</script>
+    </div>
+{% else %}
+    <ul>
+        <li>
+            {% trans "Nothing to show" %}
+        </li>
+    </ul>
+{% endif %}

+ 26 - 0
jet/dashboard/templates/jet.dashboard/modules/google_analytics_visitors_totals.html

@@ -0,0 +1,26 @@
+{% load i18n %}
+
+{% if module.error %}
+    <ul>
+        <li>
+            {{ module.error }}
+        </li>
+    </ul>
+{% elif module.children %}
+    <div class="padding center">
+        <ul class="inline bordered">
+            {% for statistic in module.children %}
+                <li>
+                    <div class="big">{{ statistic.value }}</div>
+                    <div class="dim">{{ statistic.title }}</div>
+                </li>
+            {% endfor %}
+        </ul>
+    </div>
+{% else %}
+    <ul>
+        <li>
+            {% trans "Nothing to show" %}
+        </li>
+    </ul>
+{% endif %}

+ 0 - 0
jet/templates/jet/dashboard/modules/link_list.html → jet/dashboard/templates/jet.dashboard/modules/link_list.html


+ 0 - 0
jet/templates/jet/dashboard/modules/model_list.html → jet/dashboard/templates/jet.dashboard/modules/model_list.html


+ 0 - 0
jet/templates/jet/dashboard/modules/recent_actions.html → jet/dashboard/templates/jet.dashboard/modules/recent_actions.html


+ 38 - 0
jet/dashboard/templates/jet.dashboard/modules/yandex_metrika_period_visitors.html

@@ -0,0 +1,38 @@
+{% load i18n %}
+
+
+{% if module.error %}
+    <ul>
+        <li>
+            {{ module.error }}
+        </li>
+    </ul>
+{% elif module.children %}
+    <table class="table">
+        <thead>
+            <tr>
+                <th>{% trans "Date" %}</th>
+                <th>{% trans "Visitors" %}</th>
+                <th>{% trans "Visits" %}</th>
+                <th>{% trans "Views" %}</th>
+            </tr>
+        </thead>
+        <tbody>
+            {% for data in module.children %}
+                <tr>
+                    <th>{{ data.0 }}</th>
+                    <td width="1" align="center">{{ data.1.visitors }}</td>
+                    <td width="1" align="center">{{ data.1.visits }}</td>
+                    <td width="1" align="center">{{ data.1.page_views }}</td>
+                </tr>
+            {% endfor %}
+        </tbody>
+    </table>
+{% else %}
+    <ul>
+        <li>
+            {% trans "Nothing to show" %}
+        </li>
+    </ul>
+{% endif %}
+

+ 33 - 0
jet/dashboard/templates/jet.dashboard/modules/yandex_metrika_visitors_chart.html

@@ -0,0 +1,33 @@
+{% load i18n %}
+
+{% if module.error %}
+    <ul>
+        <li>
+            {{ module.error }}
+        </li>
+    </ul>
+{% elif module.children %}
+    <div class="padding center">
+        <canvas id="chart_{{ module.model.pk }}" style="width: 100%;">
+            <div class="chart-fillColor"></div>
+            <div class="chart-strokeColor"></div>
+            <div class="chart-pointColor"></div>
+            <div class="chart-pointHighlightFill"></div>
+            <div class="chart-scaleGridLineColor"></div>
+            <div class="chart-scaleLineColor"></div>
+            <div class="chart-scaleFontColor"></div>
+            <div class="chart-data">
+                {% for data in module.children %}
+                    <div class="chart-data-item" data-date="{{ data.0|date:"d/m" }}" data-value="{{ data.1 }}"></div>
+                {% endfor %}
+            </div>
+        </canvas>
+        <script>jet.jQuery('#chart_{{ module.model.pk }}').yandexMetrikaChart();</script>
+    </div>
+{% else %}
+    <ul>
+        <li>
+            {% trans "Nothing to show" %}
+        </li>
+    </ul>
+{% endif %}

+ 26 - 0
jet/dashboard/templates/jet.dashboard/modules/yandex_metrika_visitors_totals.html

@@ -0,0 +1,26 @@
+{% load i18n %}
+
+{% if module.error %}
+    <ul>
+        <li>
+            {{ module.error }}
+        </li>
+    </ul>
+{% elif module.children %}
+    <div class="padding center">
+        <ul class="inline bordered">
+            {% for statistic in module.children %}
+                <li>
+                    <div class="big">{{ statistic.value }}</div>
+                    <div class="dim">{{ statistic.title }}</div>
+                </li>
+            {% endfor %}
+        </ul>
+    </div>
+{% else %}
+    <ul>
+        <li>
+            {% trans "Nothing to show" %}
+        </li>
+    </ul>
+{% endif %}

+ 4 - 4
jet/templates/jet/dashboard/update_module.html → jet/dashboard/templates/jet.dashboard/update_module.html

@@ -58,10 +58,10 @@
                     </fieldset>
                 {% endif %}
 
-                {% include "jet/dashboard/update_module_fieldset.html" with form=form %}
+                {% include "jet.dashboard/update_module_fieldset.html" with form=form %}
 
                 {% if settings_form %}
-                    {% include "jet/dashboard/update_module_fieldset.html" with form=settings_form %}
+                    {% include "jet.dashboard/update_module_fieldset.html" with form=settings_form %}
                 {% endif %}
             </div>
 
@@ -104,13 +104,13 @@
                                 {% for form in children_formset %}
                                     <div class="stacked {% if forloop.first %}selected {% endif %}inline-related {% if forloop.last %} last-related{% endif %}" id="{{ children_formset.prefix }}-{{ forloop.counter0 }}">
                                         {% if form.non_field_errors %}{{ form.non_field_errors }}{% endif %}
-                                        {% include "jet/dashboard/update_module_fieldset.html" with form=form %}
+                                        {% include "jet.dashboard/update_module_fieldset.html" with form=form %}
                                     </div>
                                 {% endfor %}
                                 {% with form=children_formset.empty_form %}
                                     <div class="stacked inline-related empty-form last-related" id="{{ children_formset.prefix }}-empty">
                                         {% if form.non_field_errors %}{{ form.non_field_errors }}{% endif %}
-                                        {% include "jet/dashboard/update_module_fieldset.html" with form=form %}
+                                        {% include "jet.dashboard/update_module_fieldset.html" with form=form %}
                                     </div>
                                 {% endwith %}
                             </div>

+ 0 - 0
jet/templates/jet/dashboard/update_module_fieldset.html → jet/dashboard/templates/jet.dashboard/update_module_fieldset.html


+ 0 - 0
jet/dashboard/templatetags/__init__.py


+ 12 - 0
jet/dashboard/templatetags/jet_dashboard_tags.py

@@ -0,0 +1,12 @@
+from __future__ import unicode_literals
+from django import template
+from jet.dashboard.utils import get_current_dashboard
+
+register = template.Library()
+
+
+@register.assignment_tag(takes_context=True)
+def get_dashboard(context, location):
+    dashboard_cls = get_current_dashboard(location)
+    dashboard = dashboard_cls(context)
+    return dashboard

+ 53 - 0
jet/dashboard/urls.py

@@ -0,0 +1,53 @@
+from django.conf.urls import patterns, url
+from django.views.i18n import javascript_catalog
+from jet.dashboard import dashboard
+from jet.dashboard.views import update_dashboard_modules_view, add_user_dashboard_module_view, \
+    update_dashboard_module_collapse_view, remove_dashboard_module_view, UpdateDashboardModuleView, \
+    load_dashboard_module_view, reset_dashboard_view
+
+urlpatterns = patterns(
+    '',
+    url(
+        r'^module/(?P<pk>\d+)/$',
+        UpdateDashboardModuleView.as_view(),
+        name='update_module'
+    ),
+    url(
+        r'^update_dashboard_modules/$',
+        update_dashboard_modules_view,
+        name='update_dashboard_modules'
+    ),
+    url(
+        r'^add_user_dashboard_module/$',
+        add_user_dashboard_module_view,
+        name='add_user_dashboard_module'
+    ),
+    url(
+        r'^update_dashboard_module_collapse/$',
+        update_dashboard_module_collapse_view,
+        name='update_dashboard_module_collapse'
+    ),
+    url(
+        r'^remove_dashboard_module/$',
+        remove_dashboard_module_view,
+        name='remove_dashboard_module'
+    ),
+    url(
+        r'^load_dashboard_module/(?P<pk>\d+)/$',
+        load_dashboard_module_view,
+        name='load_dashboard_module'
+    ),
+    url(
+        r'^reset_dashboard/((?P<app_label>[^/]+)/)?$',
+        reset_dashboard_view,
+        name='reset_dashboard'
+    ),
+    url(
+        r'^jsi18n/$',
+        javascript_catalog,
+        {'packages': ('jet',)},
+        name='jsi18n'
+    ),
+)
+
+urlpatterns += dashboard.urls.get_urls()

+ 17 - 0
jet/dashboard/utils.py

@@ -0,0 +1,17 @@
+from importlib import import_module
+from jet.dashboard import settings
+
+
+def get_current_dashboard(location):
+    if location == 'index':
+        path = settings.JET_INDEX_DASHBOARD
+    elif location == 'app_index':
+        path = settings.JET_APP_INDEX_DASHBOARD
+    else:
+        raise ValueError('Unknown dashboard location: %s' % location)
+
+    module, cls = path.rsplit('.', 1)
+    module = import_module(module)
+    index_dashboard_cls = getattr(module, cls)
+
+    return index_dashboard_cls

+ 223 - 0
jet/dashboard/views.py

@@ -0,0 +1,223 @@
+from django.contrib import messages
+from django.core.urlresolvers import reverse
+from django.forms.formsets import formset_factory
+from django.shortcuts import redirect
+from django.views.decorators.http import require_POST, require_GET
+from jet.dashboard.forms import UpdateDashboardModulesForm, AddUserDashboardModuleForm, \
+    UpdateDashboardModuleCollapseForm, RemoveDashboardModuleForm
+from jet.dashboard.models import UserDashboardModule
+from jet.utils import JsonResponse, get_app_list, SuccessMessageMixin
+from django.views.generic import UpdateView
+from django.utils.translation import ugettext_lazy as _
+
+
+class UpdateDashboardModuleView(SuccessMessageMixin, UpdateView):
+    model = UserDashboardModule
+    fields = ('title',)
+    template_name = 'jet.dashboard/update_module.html'
+    success_message = _('Widget was successfully updated')
+    object = None
+    module = None
+
+    def get_success_url(self):
+        if self.object.app_label:
+            return reverse('admin:app_list', kwargs={'app_label': self.object.app_label})
+        else:
+            return reverse('admin:index')
+
+    def get_module(self):
+        object = self.object if getattr(self, 'object', None) is not None else self.get_object()
+        return object.load_module()
+
+    def get_settings_form_kwargs(self):
+        kwargs = {
+            'initial': self.module.settings
+        }
+
+        if self.request.method in ('POST', 'PUT'):
+            kwargs.update({
+                'data': self.request.POST,
+                'files': self.request.FILES,
+            })
+        return kwargs
+
+    def get_settings_form(self):
+        if self.module.settings_form:
+            form = self.module.settings_form(**self.get_settings_form_kwargs())
+            if hasattr(form, 'set_module'):
+                form.set_module(self.module)
+            return form
+
+    def get_children_formset_kwargs(self):
+        kwargs = {
+            'initial': self.module.children,
+            'prefix': 'children',
+        }
+
+        if self.request.method in ('POST', 'PUT'):
+            kwargs.update({
+                'data': self.request.POST,
+                'files': self.request.FILES,
+            })
+        return kwargs
+
+    def get_children_formset(self):
+        if self.module.child_form:
+            return formset_factory(self.module.child_form, can_delete=True, extra=1)(**self.get_children_formset_kwargs())
+
+    def clean_children_data(self, children):
+        children = list(filter(
+            lambda item: isinstance(item, dict) and item and item.get('DELETE') is not True,
+            children
+        ))
+        for item in children:
+            item.pop('DELETE')
+        return children
+
+    def get_current_app(self):
+        app_list = get_app_list({'request': self.request})
+
+        for app in app_list:
+            if app.get('app_label', app.get('name')) == self.object.app_label:
+                return app
+
+    def get_context_data(self, **kwargs):
+        data = super(UpdateDashboardModuleView, self).get_context_data(**kwargs)
+        data['title'] = _('Change')
+        data['module'] = self.module
+        data['settings_form'] = self.get_settings_form()
+        data['children_formset'] = self.get_children_formset()
+        data['child_name'] = self.module.child_name if self.module.child_name else _('Items')
+        data['child_name_plural'] = self.module.child_name_plural if self.module.child_name_plural else _('Items')
+        data['app'] = self.get_current_app()
+        return data
+
+    def dispatch(self, request, *args, **kwargs):
+        self.object = self.get_object()
+        self.module = self.get_module()(model=self.object)
+        return super(UpdateDashboardModuleView, self).dispatch(request, *args, **kwargs)
+
+    def post(self, request, *args, **kwargs):
+        settings_form = self.get_settings_form()
+        children_formset = self.get_children_formset()
+
+        data = request.POST.copy()
+
+        if settings_form:
+            if settings_form.is_valid():
+                settings = settings_form.cleaned_data
+                data['settings'] = self.module.dump_settings(settings)
+            else:
+                return self.form_invalid(self.get_form(self.get_form_class()))
+
+        if children_formset:
+            if children_formset.is_valid():
+                self.module.children = self.clean_children_data(children_formset.cleaned_data)
+                data['children'] = self.module.dump_children()
+            else:
+                return self.form_invalid(self.get_form(self.get_form_class()))
+
+        request.POST = data
+
+        return super(UpdateDashboardModuleView, self).post(request, *args, **kwargs)
+
+    def form_valid(self, form):
+        if 'settings' in form.data:
+            form.instance.settings = form.data['settings']
+        if 'children' in form.data:
+            form.instance.children = form.data['children']
+        return super(UpdateDashboardModuleView, self).form_valid(form)
+
+
+@require_POST
+def update_dashboard_modules_view(request):
+    result = {'error': False}
+    form = UpdateDashboardModulesForm(request, request.POST)
+
+    if form.is_valid():
+        form.save()
+    else:
+        result['error'] = True
+
+    return JsonResponse(result)
+
+
+@require_POST
+def add_user_dashboard_module_view(request):
+    result = {'error': False}
+    form = AddUserDashboardModuleForm(request, request.POST)
+
+    if form.is_valid():
+        module = form.save()
+        result['id'] = module.pk
+        messages.success(request, _('Widget has been successfully added'))
+
+        if module.app_label:
+            result['success_url'] = reverse('admin:app_list', kwargs={'app_label': module.app_label})
+        else:
+            result['success_url'] = reverse('admin:index')
+    else:
+        result['error'] = True
+
+    return JsonResponse(result)
+
+
+@require_POST
+def update_dashboard_module_collapse_view(request):
+    result = {'error': False}
+
+    try:
+        instance = UserDashboardModule.objects.get(pk=request.POST.get('id'))
+        form = UpdateDashboardModuleCollapseForm(request, request.POST, instance=instance)
+
+        if form.is_valid():
+            module = form.save()
+            result['collapsed'] = module.collapsed
+        else:
+            result['error'] = True
+    except UserDashboardModule.DoesNotExist:
+        result['error'] = True
+
+    return JsonResponse(result)
+
+
+@require_POST
+def remove_dashboard_module_view(request):
+    result = {'error': False}
+
+    try:
+        instance = UserDashboardModule.objects.get(pk=request.POST.get('id'))
+        form = RemoveDashboardModuleForm(request, request.POST, instance=instance)
+
+        if form.is_valid():
+            form.save()
+        else:
+            result['error'] = True
+    except UserDashboardModule.DoesNotExist:
+        result['error'] = True
+
+    return JsonResponse(result)
+
+
+@require_GET
+def load_dashboard_module_view(request, pk):
+    result = {'error': False}
+
+    try:
+        instance = UserDashboardModule.objects.get(pk=pk)
+        module_cls = instance.load_module()
+        module = module_cls(model=instance, context={'request': request})
+        result['html'] = module.render()
+    except UserDashboardModule.DoesNotExist:
+        result['error'] = True
+
+    return JsonResponse(result)
+
+
+def reset_dashboard_view(request, app_label=None):
+    UserDashboardModule.objects.filter(user=request.user.pk, app_label=app_label).delete()
+    if app_label:
+        url = reverse('admin:app_list', kwargs={'app_label': app_label})
+    else:
+        url = reverse('admin:index')
+    return redirect(url)

+ 5 - 129
jet/forms.py

@@ -2,15 +2,16 @@ import json
 from django import forms
 from django.core.exceptions import ValidationError
 from django.db.models import Q
+import operator
+from jet.models import Bookmark, PinnedApplication
+from jet.utils import get_model_instance_label
+from functools import reduce
+
 try:
     from django.apps import apps
     get_model = apps.get_model
 except ImportError:
     from django.db.models.loading import get_model
-import operator
-from jet.models import Bookmark, PinnedApplication, UserDashboardModule
-from jet.utils import get_current_dashboard, get_model_instance_label
-from functools import reduce
 
 
 class AddBookmarkForm(forms.ModelForm):
@@ -83,131 +84,6 @@ class ToggleApplicationPinForm(forms.ModelForm):
                 return True
 
 
-class UpdateDashboardModulesForm(forms.Form):
-    app_label = forms.CharField(required=False)
-    modules = forms.CharField()
-    modules_objects = []
-
-    def __init__(self, request, *args, **kwargs):
-        self.request = request
-        super(UpdateDashboardModulesForm, self).__init__(*args, **kwargs)
-
-    def clean(self):
-        data = super(UpdateDashboardModulesForm, self).clean()
-
-        try:
-            modules = json.loads(data['modules'])
-
-            for module in modules:
-                db_module = UserDashboardModule.objects.get(
-                    user=self.request.user.pk,
-                    app_label=data['app_label'] if data['app_label'] else None,
-                    pk=module['id']
-                )
-
-                column = module['column']
-                order = module['order']
-
-                if db_module.column != column or db_module.order != order:
-                    db_module.column = column
-                    db_module.order = order
-
-                    self.modules_objects.append(db_module)
-        except Exception:
-            raise ValidationError('error')
-
-        return data
-
-    def save(self):
-        for module in self.modules_objects:
-            module.save()
-
-
-class AddUserDashboardModuleForm(forms.ModelForm):
-    type = forms.CharField()
-    module = forms.IntegerField()
-    module_cls = None
-
-    def __init__(self, request, *args, **kwargs):
-        self.request = request
-        super(AddUserDashboardModuleForm, self).__init__(*args, **kwargs)
-
-    class Meta:
-        model = UserDashboardModule
-        fields = ['app_label']
-
-    def clean_app_label(self):
-        data = self.cleaned_data['app_label']
-        return data if data != '' else None
-
-    def clean(self):
-        data = super(AddUserDashboardModuleForm, self).clean()
-
-        index_dashboard_cls = get_current_dashboard('app_index' if data['app_label'] else 'index')
-        index_dashboard = index_dashboard_cls({'request': self.request}, app_label=data['app_label'])
-
-        if data['type'] == 'children':
-            module = index_dashboard.children[data['module']]
-        elif data['type'] == 'available_children':
-            module = index_dashboard.available_children[data['module']]()
-        else:
-            raise ValidationError('error')
-
-        self.module_cls = module
-        return data
-
-    def save(self, commit=True):
-        self.instance.title = self.module_cls.title
-        self.instance.module = self.module_cls.fullname()
-        self.instance.user = self.request.user.pk
-        self.instance.column = 0
-        self.instance.order = -1
-        self.instance.settings = self.module_cls.dump_settings()
-        self.instance.children = self.module_cls.dump_children()
-
-        return super(AddUserDashboardModuleForm, self).save(commit)
-
-
-class UpdateDashboardModuleCollapseForm(forms.ModelForm):
-    def __init__(self, request, *args, **kwargs):
-        self.request = request
-        super(UpdateDashboardModuleCollapseForm, self).__init__(*args, **kwargs)
-
-    class Meta:
-        model = UserDashboardModule
-        fields = ['collapsed']
-
-    def clean(self):
-        data = super(UpdateDashboardModuleCollapseForm, self).clean()
-
-        if self.instance.user != self.request.user.pk:
-            raise ValidationError('error')
-
-        return data
-
-
-class RemoveDashboardModuleForm(forms.ModelForm):
-    def __init__(self, request, *args, **kwargs):
-        self.request = request
-        super(RemoveDashboardModuleForm, self).__init__(*args, **kwargs)
-
-    class Meta:
-        model = UserDashboardModule
-        fields = []
-
-    def clean(self):
-        cleaned_data = super(RemoveDashboardModuleForm, self).clean()
-
-        if self.instance.user != self.request.user.pk:
-            raise ValidationError('error')
-
-        return cleaned_data
-
-    def save(self, commit=True):
-        if commit:
-            self.instance.delete()
-
-
 class ModelLookupForm(forms.Form):
     app_label = forms.CharField()
     model = forms.CharField()

二進制
jet/locale/en/LC_MESSAGES/django.mo


+ 476 - 199
jet/locale/en/LC_MESSAGES/django.po

@@ -6,7 +6,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2015-08-07 13:25+0000\n"
+"POT-Creation-Date: 2015-09-07 13:42+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -15,178 +15,546 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: dashboard.py:157
+#: dashboard/dashboard.py:159
 msgid "Quick links"
 msgstr ""
 
-#: dashboard.py:163
+#: dashboard/dashboard.py:165
 msgid "Return to site"
 msgstr ""
 
-#: dashboard.py:164 templates/admin/base.html:85
+#: dashboard/dashboard.py:166 templates/admin/base.html:77
 msgid "Change password"
 msgstr ""
 
-#: dashboard.py:166 templates/admin/base.html:89
+#: dashboard/dashboard.py:168 templates/admin/base.html:81
 msgid "Log out"
 msgstr ""
 
-#: dashboard.py:174 modules.py:158 templates/admin/sidebar.html:23
+#: dashboard/dashboard.py:176 dashboard/modules.py:160
+#: templates/admin/base.html:157 templates/admin/index.html:14
 msgid "Applications"
 msgstr ""
 
-#: dashboard.py:182
+#: dashboard/dashboard.py:184
 msgid "Administration"
 msgstr ""
 
-#: dashboard.py:190 modules.py:238
+#: dashboard/dashboard.py:192 dashboard/modules.py:242
+#: templates/admin/index.html:68
 msgid "Recent Actions"
 msgstr ""
 
-#: dashboard.py:198
+#: dashboard/dashboard.py:200
 msgid "Latest Django News"
 msgstr ""
 
-#: dashboard.py:207
+#: dashboard/dashboard.py:209
 msgid "Support"
 msgstr ""
 
-#: dashboard.py:210
+#: dashboard/dashboard.py:212
 msgid "Django documentation"
 msgstr ""
 
-#: dashboard.py:215
+#: dashboard/dashboard.py:217
 msgid "Django \"django-users\" mailing list"
 msgstr ""
 
-#: dashboard.py:220
+#: dashboard/dashboard.py:222
 msgid "Django irc channel"
 msgstr ""
 
-#: dashboard.py:235
+#: dashboard/dashboard.py:237
 msgid "Application models"
 msgstr ""
 
-#: models.py:8 modules.py:113 templates/admin/sidebar.html:57
-msgid "URL"
+#: dashboard/dashboard_modules/google_analytics.py:143
+#: dashboard/dashboard_modules/yandex_metrika.py:101
+msgid "Revoke access"
 msgstr ""
 
-#: models.py:9
-msgid "title"
+#: dashboard/dashboard_modules/google_analytics.py:148
+#: dashboard/dashboard_modules/yandex_metrika.py:106
+msgid "Grant access"
 msgstr ""
 
-#: models.py:10 models.py:24 models.py:40
-msgid "user"
+#: dashboard/dashboard_modules/google_analytics.py:161
+#: dashboard/dashboard_modules/yandex_metrika.py:113
+msgid "Access"
 msgstr ""
 
-#: models.py:11 models.py:25
-msgid "date created"
+#: dashboard/dashboard_modules/google_analytics.py:162
+#: dashboard/dashboard_modules/yandex_metrika.py:114
+msgid "Counter"
 msgstr ""
 
-#: models.py:14
-msgid "bookmark"
+#: dashboard/dashboard_modules/google_analytics.py:163
+#: dashboard/dashboard_modules/yandex_metrika.py:115
+msgid "Statistics period"
 msgstr ""
 
-#: models.py:15 templates/admin/sidebar.html:70
-msgid "bookmarks"
+#: dashboard/dashboard_modules/google_analytics.py:164
+#: dashboard/dashboard_modules/yandex_metrika.py:116
+msgid "Today"
 msgstr ""
 
-#: models.py:23 models.py:39
-msgid "application name"
+#: dashboard/dashboard_modules/google_analytics.py:165
+#: dashboard/dashboard_modules/yandex_metrika.py:117
+msgid "Last week"
 msgstr ""
 
-#: models.py:28
-msgid "pinned application"
+#: dashboard/dashboard_modules/google_analytics.py:166
+#: dashboard/dashboard_modules/yandex_metrika.py:118
+msgid "Last month"
 msgstr ""
 
-#: models.py:29
-msgid "pinned applications"
+#: dashboard/dashboard_modules/google_analytics.py:167
+#: dashboard/dashboard_modules/yandex_metrika.py:119
+msgid "Last quarter"
+msgstr ""
+
+#: dashboard/dashboard_modules/google_analytics.py:168
+#: dashboard/dashboard_modules/yandex_metrika.py:120
+msgid "Last year"
+msgstr ""
+
+#: dashboard/dashboard_modules/google_analytics.py:178
+#: dashboard/dashboard_modules/yandex_metrika.py:130
+msgid "none"
+msgstr ""
+
+#: dashboard/dashboard_modules/google_analytics.py:181
+#: dashboard/dashboard_modules/yandex_metrika.py:133
+msgid "grant access first"
+msgstr ""
+
+#: dashboard/dashboard_modules/google_analytics.py:181
+#: dashboard/dashboard_modules/yandex_metrika.py:133
+msgid "counters loading failed"
+msgstr ""
+
+#: dashboard/dashboard_modules/google_analytics.py:186
+#: dashboard/dashboard_modules/yandex_metrika.py:138
+msgid "Show"
+msgstr ""
+
+#: dashboard/dashboard_modules/google_analytics.py:187
+#: dashboard/templates/jet.dashboard/modules/google_analytics_period_visitors.html:15
+msgid "Users"
+msgstr ""
+
+#: dashboard/dashboard_modules/google_analytics.py:188
+#: dashboard/templates/jet.dashboard/modules/google_analytics_period_visitors.html:16
+msgid "Sessions"
+msgstr ""
+
+#: dashboard/dashboard_modules/google_analytics.py:189
+#: dashboard/dashboard_modules/yandex_metrika.py:141
+#: dashboard/templates/jet.dashboard/modules/google_analytics_period_visitors.html:17
+#: dashboard/templates/jet.dashboard/modules/yandex_metrika_period_visitors.html:17
+msgid "Views"
+msgstr ""
+
+#: dashboard/dashboard_modules/google_analytics.py:191
+#: dashboard/dashboard_modules/google_analytics.py:199
+#: dashboard/dashboard_modules/yandex_metrika.py:143
+#: dashboard/dashboard_modules/yandex_metrika.py:151
+msgid "Group"
+msgstr ""
+
+#: dashboard/dashboard_modules/google_analytics.py:192
+#: dashboard/dashboard_modules/google_analytics.py:200
+#: dashboard/dashboard_modules/yandex_metrika.py:144
+#: dashboard/dashboard_modules/yandex_metrika.py:152
+msgid "By day"
+msgstr ""
+
+#: dashboard/dashboard_modules/google_analytics.py:193
+#: dashboard/dashboard_modules/google_analytics.py:201
+#: dashboard/dashboard_modules/yandex_metrika.py:145
+#: dashboard/dashboard_modules/yandex_metrika.py:153
+msgid "By week"
+msgstr ""
+
+#: dashboard/dashboard_modules/google_analytics.py:194
+#: dashboard/dashboard_modules/google_analytics.py:202
+#: dashboard/dashboard_modules/yandex_metrika.py:146
+#: dashboard/dashboard_modules/yandex_metrika.py:154
+msgid "By month"
+msgstr ""
+
+#: dashboard/dashboard_modules/google_analytics.py:275
+#, python-format
+msgid ""
+"Please <a href=\"%s\">attach Google account and choose Google Analytics "
+"counter</a> to start using widget"
 msgstr ""
 
-#: models.py:37 modules.py:114 templates/admin/sidebar.html:55
+#: dashboard/dashboard_modules/google_analytics.py:278
+#, python-format
+msgid ""
+"Please <a href=\"%s\">select Google Analytics counter</a> to start using "
+"widget"
+msgstr ""
+
+#: dashboard/dashboard_modules/google_analytics.py:297
+#: dashboard/dashboard_modules/google_analytics_views.py:42
+#: dashboard/dashboard_modules/yandex_metrika.py:231
+#: dashboard/dashboard_modules/yandex_metrika_views.py:37
+msgid "API request failed."
+msgstr ""
+
+#: dashboard/dashboard_modules/google_analytics.py:299
+#: dashboard/dashboard_modules/yandex_metrika.py:233
+#, python-format
+msgid " Try to <a href=\"%s\">revoke and grant access</a> again"
+msgstr ""
+
+#: dashboard/dashboard_modules/google_analytics.py:304
+msgid "Google Analytics visitors totals"
+msgstr ""
+
+#: dashboard/dashboard_modules/google_analytics.py:316
+msgid "users"
+msgstr ""
+
+#: dashboard/dashboard_modules/google_analytics.py:317
+msgid "sessions"
+msgstr ""
+
+#: dashboard/dashboard_modules/google_analytics.py:318
+#: dashboard/dashboard_modules/yandex_metrika.py:254
+msgid "views"
+msgstr ""
+
+#: dashboard/dashboard_modules/google_analytics.py:320
+#: dashboard/dashboard_modules/google_analytics.py:365
+#: dashboard/dashboard_modules/google_analytics.py:404
+#: dashboard/dashboard_modules/yandex_metrika.py:256
+#: dashboard/dashboard_modules/yandex_metrika.py:295
+#: dashboard/dashboard_modules/yandex_metrika.py:328
+msgid "Bad server response"
+msgstr ""
+
+#: dashboard/dashboard_modules/google_analytics.py:324
+msgid "Google Analytics visitors chart"
+msgstr ""
+
+#: dashboard/dashboard_modules/google_analytics.py:369
+msgid "Google Analytics period visitors"
+msgstr ""
+
+#: dashboard/dashboard_modules/google_analytics_views.py:26
+#: dashboard/dashboard_modules/google_analytics_views.py:46
+#: dashboard/dashboard_modules/yandex_metrika_views.py:23
+#: dashboard/dashboard_modules/yandex_metrika_views.py:45
+msgid "Module not found"
+msgstr ""
+
+#: dashboard/dashboard_modules/google_analytics_views.py:44
+#: dashboard/dashboard_modules/yandex_metrika_views.py:43
+msgid "Bad arguments"
+msgstr ""
+
+#: dashboard/dashboard_modules/yandex_metrika.py:139
+#: dashboard/templates/jet.dashboard/modules/yandex_metrika_period_visitors.html:15
+msgid "Visitors"
+msgstr ""
+
+#: dashboard/dashboard_modules/yandex_metrika.py:140
+#: dashboard/templates/jet.dashboard/modules/yandex_metrika_period_visitors.html:16
+msgid "Visits"
+msgstr ""
+
+#: dashboard/dashboard_modules/yandex_metrika.py:214
+#, python-format
+msgid ""
+"Please <a href=\"%s\">attach Yandex account and choose Yandex Metrika "
+"counter</a> to start using widget"
+msgstr ""
+
+#: dashboard/dashboard_modules/yandex_metrika.py:217
+#, python-format
+msgid ""
+"Please <a href=\"%s\">select Yandex Metrika counter</a> to start using widget"
+msgstr ""
+
+#: dashboard/dashboard_modules/yandex_metrika.py:240
+msgid "Yandex Metrika visitors totals"
+msgstr ""
+
+#: dashboard/dashboard_modules/yandex_metrika.py:252
+msgid "visitors"
+msgstr ""
+
+#: dashboard/dashboard_modules/yandex_metrika.py:253
+msgid "visits"
+msgstr ""
+
+#: dashboard/dashboard_modules/yandex_metrika.py:260
+msgid "Yandex Metrika visitors chart"
+msgstr ""
+
+#: dashboard/dashboard_modules/yandex_metrika.py:299
+msgid "Yandex Metrika period visitors"
+msgstr ""
+
+#: dashboard/models.py:11 dashboard/modules.py:116
+#: templates/admin/base.html:189
 msgid "Title"
 msgstr ""
 
-#: models.py:38
+#: dashboard/models.py:12
 msgid "module"
 msgstr ""
 
-#: models.py:41
+#: dashboard/models.py:13 models.py:28
+msgid "application name"
+msgstr ""
+
+#: dashboard/models.py:14 models.py:14 models.py:29
+msgid "user"
+msgstr ""
+
+#: dashboard/models.py:15
 msgid "column"
 msgstr ""
 
-#: models.py:42
+#: dashboard/models.py:16
 msgid "order"
 msgstr ""
 
-#: models.py:43
+#: dashboard/models.py:17
 msgid "settings"
 msgstr ""
 
-#: models.py:44
+#: dashboard/models.py:18
 msgid "children"
 msgstr ""
 
-#: models.py:45
+#: dashboard/models.py:19
 msgid "collapsed"
 msgstr ""
 
-#: models.py:48
+#: dashboard/models.py:22
 msgid "user dashboard module"
 msgstr ""
 
-#: models.py:49
+#: dashboard/models.py:23
 msgid "user dashboard modules"
 msgstr ""
 
-#: modules.py:115
+#: dashboard/modules.py:115 models.py:12 templates/admin/base.html:191
+msgid "URL"
+msgstr ""
+
+#: dashboard/modules.py:117
 msgid "External link"
 msgstr ""
 
-#: modules.py:119
+#: dashboard/modules.py:121
 msgid "Layout"
 msgstr ""
 
-#: modules.py:119
+#: dashboard/modules.py:121
 msgid "Stacked"
 msgstr ""
 
-#: modules.py:119
+#: dashboard/modules.py:121
 msgid "Inline"
 msgstr ""
 
-#: modules.py:123 modules.py:129
+#: dashboard/modules.py:125 dashboard/modules.py:131
 msgid "Links"
 msgstr ""
 
-#: modules.py:128
+#: dashboard/modules.py:130
 msgid "Link"
 msgstr ""
 
-#: modules.py:198
+#: dashboard/modules.py:201
 msgid "Models"
 msgstr ""
 
-#: modules.py:234 modules.py:305
+#: dashboard/modules.py:238 dashboard/modules.py:309
 msgid "Items limit"
 msgstr ""
 
-#: modules.py:306
+#: dashboard/modules.py:310
 msgid "Feed URL"
 msgstr ""
 
-#: modules.py:310
+#: dashboard/modules.py:314
 msgid "RSS Feed"
 msgstr ""
 
-#: modules.py:352
+#: dashboard/modules.py:356
 msgid "You must install the FeedParser python module"
 msgstr ""
 
-#: modules.py:357
+#: dashboard/modules.py:361
 msgid "You must provide a valid feed URL"
 msgstr ""
 
+#: dashboard/templates/admin/app_index.html:32
+#: dashboard/templates/jet.dashboard/update_module.html:14
+#: templates/admin/base.html:92 templates/admin/base.html.py:141
+#: templates/admin/change_form.html:17 templates/admin/change_list.html:33
+#: templates/admin/delete_confirmation.html:8
+#: templates/admin/delete_selected_confirmation.html:8
+#: templates/admin/object_history.html:6
+#: templates/registration/logged_out.html:4
+#: templates/registration/password_change_done.html:6
+#: templates/registration/password_change_form.html:6
+msgid "Home"
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/dashboard.html:17
+msgid "Delete widget"
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/dashboard.html:18
+msgid "Are you sure want to delete this widget?"
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/dashboard_tools.html:7
+msgid "widgets"
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/dashboard_tools.html:8
+msgid "available"
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/dashboard_tools.html:13
+msgid "inititals"
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/dashboard_tools.html:18
+#: dashboard/templates/jet.dashboard/modules/app_list.html:18
+#: dashboard/templates/jet.dashboard/modules/model_list.html:8
+#: templates/admin/change_form.html:20 templates/admin/index.html:34
+msgid "Add"
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/module.html:19
+#: dashboard/templates/jet.dashboard/modules/app_list.html:24
+#: dashboard/templates/jet.dashboard/modules/model_list.html:14
+#: dashboard/views.py:86 templates/admin/edit_inline/stacked.html:44
+#: templates/admin/edit_inline/tabular.html:39 templates/admin/index.html:40
+msgid "Change"
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/module.html:22
+#: templates/admin/delete_confirmation.html:12
+#: templates/admin/related_widget_wrapper.html:24
+#: templates/admin/submit_line.html:11
+msgid "Delete"
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/modules/app_list.html:7
+#: dashboard/templates/jet.dashboard/modules/app_list.html:10
+#: templates/admin/index.html:23 templates/admin/index.html.py:26
+#, python-format
+msgid "Models in the %(name)s application"
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/modules/feed.html:13
+#: dashboard/templates/jet.dashboard/modules/google_analytics_period_visitors.html:34
+#: dashboard/templates/jet.dashboard/modules/google_analytics_visitors_chart.html:30
+#: dashboard/templates/jet.dashboard/modules/google_analytics_visitors_totals.html:23
+#: dashboard/templates/jet.dashboard/modules/link_list.html:26
+#: dashboard/templates/jet.dashboard/modules/yandex_metrika_period_visitors.html:34
+#: dashboard/templates/jet.dashboard/modules/yandex_metrika_visitors_chart.html:30
+#: dashboard/templates/jet.dashboard/modules/yandex_metrika_visitors_totals.html:23
+msgid "Nothing to show"
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/modules/google_analytics_period_visitors.html:14
+#: dashboard/templates/jet.dashboard/modules/yandex_metrika_period_visitors.html:14
+msgid "Date"
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/modules/recent_actions.html:6
+#: templates/admin/index.html:75
+msgid "None available"
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/modules/recent_actions.html:29
+#: templates/admin/index.html:98
+msgid "Unknown content"
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/update_module.html:35
+#: templates/admin/change_form.html:62
+msgid "General"
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/update_module.html:56
+#: templates/admin/change_list.html:61
+#: templates/admin/edit_inline/stacked.html:8
+#: templates/admin/edit_inline/tabular.html:10
+#: templates/admin/includes/fieldset.html:9 templates/admin/login.html:27
+#: templates/registration/password_change_form.html:21
+msgid "Please correct the errors below."
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/update_module.html:126
+#: templates/admin/base.html:211 templates/admin/base.html.py:218
+#: templates/admin/edit_inline/stacked.html:21
+#: templates/admin/edit_inline/stacked.html:75
+#: templates/admin/edit_inline/tabular.html:100
+msgid "Remove"
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/update_module.html:127
+#: templates/admin/edit_inline/stacked.html:76
+#: templates/admin/edit_inline/tabular.html:99
+#, python-format
+msgid "Add another %(verbose_name)s"
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/update_module.html:135
+#: templates/admin/pagination.html:20 templates/admin/submit_line.html:4
+msgid "Save"
+msgstr ""
+
+#: dashboard/views.py:18
+msgid "Widget was successfully updated"
+msgstr ""
+
+#: dashboard/views.py:90 dashboard/views.py:91
+msgid "Items"
+msgstr ""
+
+#: dashboard/views.py:153
+msgid "Widget has been successfully added"
+msgstr ""
+
+#: models.py:13
+msgid "title"
+msgstr ""
+
+#: models.py:15 models.py:30
+msgid "date created"
+msgstr ""
+
+#: models.py:18
+msgid "bookmark"
+msgstr ""
+
+#: models.py:19 templates/admin/base.html:204
+msgid "bookmarks"
+msgstr ""
+
+#: models.py:33
+msgid "pinned application"
+msgstr ""
+
+#: models.py:34
+msgid "pinned applications"
+msgstr ""
+
 #: templates/admin/actions.html:8
 msgid "Run the selected action"
 msgstr ""
@@ -208,59 +576,54 @@ msgstr ""
 msgid "Clear selection"
 msgstr ""
 
-#: templates/admin/app_index.html:30 templates/admin/base.html:100
-#: templates/admin/change_form.html:22 templates/admin/change_list.html:33
-#: templates/admin/delete_confirmation.html:8
-#: templates/admin/delete_selected_confirmation.html:8
-#: templates/admin/sidebar.html:7
-#: templates/jet/dashboard/update_module.html:13
-#: templates/registration/logged_out.html:4
-#: templates/registration/password_change_done.html:6
-#: templates/registration/password_change_form.html:11
-msgid "Home"
+#: templates/admin/base.html:146
+msgid "View site"
 msgstr ""
 
-#: templates/admin/change_form.html:25
-#: templates/jet/dashboard/dashboard_tools.html:18
-#: templates/jet/dashboard/modules/app_list.html:13
-#: templates/jet/dashboard/modules/model_list.html:8
-msgid "Add"
+#: templates/admin/base.html:186 templates/admin/base.html.py:203
+msgid "Add bookmark"
+msgstr ""
+
+#: templates/admin/base.html:199
+msgid "Delete bookmark"
+msgstr ""
+
+#: templates/admin/base.html:200
+msgid "Are you sure want to delete this bookmark?"
+msgstr ""
+
+#: templates/admin/base.html:226
+msgid "Documentation"
+msgstr ""
+
+#: templates/admin/base.html:249
+msgid "Application page"
+msgstr ""
+
+#: templates/admin/base_site.html:3
+msgid "Django site admin"
 msgstr ""
 
-#: templates/admin/change_form.html:40
+#: templates/admin/change_form.html:35 templates/admin/object_history.html:10
 msgid "History"
 msgstr ""
 
-#: templates/admin/change_form.html:43
+#: templates/admin/change_form.html:38
 #: templates/admin/edit_inline/stacked.html:49
 #: templates/admin/edit_inline/tabular.html:45
 msgid "View on site"
 msgstr ""
 
-#: templates/admin/change_form.html:67
-#: templates/jet/dashboard/update_module.html:34
-msgid "General"
-msgstr ""
-
 #: templates/admin/change_list.html:51
 #, python-format
 msgid "Add %(name)s"
 msgstr ""
 
 #: templates/admin/change_list.html:61 templates/admin/login.html:27
-#: templates/registration/password_change_form.html:26
+#: templates/registration/password_change_form.html:21
 msgid "Please correct the error below."
 msgstr ""
 
-#: templates/admin/change_list.html:61
-#: templates/admin/edit_inline/stacked.html:8
-#: templates/admin/edit_inline/tabular.html:10
-#: templates/admin/includes/fieldset.html:10 templates/admin/login.html:27
-#: templates/jet/dashboard/update_module.html:53
-#: templates/registration/password_change_form.html:26
-msgid "Please correct the errors below."
-msgstr ""
-
 #: templates/admin/change_list_results.html:17
 #, python-format
 msgid "Sorting priority: %(priority_number)s"
@@ -278,12 +641,6 @@ msgstr ""
 msgid "try to reset filters"
 msgstr ""
 
-#: templates/admin/delete_confirmation.html:12
-#: templates/admin/related_widget_wrapper.html:24
-#: templates/admin/submit_line.html:11 templates/jet/dashboard/module.html:22
-msgid "Delete"
-msgstr ""
-
 #: templates/admin/delete_confirmation.html:18
 #, python-format
 msgid ""
@@ -347,29 +704,6 @@ msgid ""
 "following objects and their related items will be deleted:"
 msgstr ""
 
-#: templates/admin/edit_inline/stacked.html:21
-#: templates/admin/edit_inline/stacked.html:75
-#: templates/admin/edit_inline/tabular.html:100
-#: templates/admin/sidebar.html:77 templates/admin/sidebar.html.py:84
-#: templates/jet/dashboard/update_module.html:123
-msgid "Remove"
-msgstr ""
-
-#: templates/admin/edit_inline/stacked.html:44
-#: templates/admin/edit_inline/tabular.html:39
-#: templates/jet/dashboard/module.html:19
-#: templates/jet/dashboard/modules/app_list.html:19
-#: templates/jet/dashboard/modules/model_list.html:14 views.py:83
-msgid "Change"
-msgstr ""
-
-#: templates/admin/edit_inline/stacked.html:76
-#: templates/admin/edit_inline/tabular.html:99
-#: templates/jet/dashboard/update_module.html:124
-#, python-format
-msgid "Add another %(verbose_name)s"
-msgstr ""
-
 #: templates/admin/edit_inline/tabular.html:25
 msgid "Delete?"
 msgstr ""
@@ -383,6 +717,10 @@ msgstr ""
 msgid "Summary"
 msgstr ""
 
+#: templates/admin/index.html:56
+msgid "You don't have permission to edit anything."
+msgstr ""
+
 #: templates/admin/login.html:16
 msgid "<span class=\"bright\">Admin</span> Site"
 msgstr ""
@@ -395,13 +733,26 @@ msgstr ""
 msgid "Forgotten your password or username?"
 msgstr ""
 
-#: templates/admin/pagination.html:7 templates/admin/search_form.html:22
-msgid "Show all"
+#: templates/admin/object_history.html:21
+msgid "Date/time"
 msgstr ""
 
-#: templates/admin/pagination.html:20 templates/admin/submit_line.html:4
-#: templates/jet/dashboard/update_module.html:132
-msgid "Save"
+#: templates/admin/object_history.html:22
+msgid "User"
+msgstr ""
+
+#: templates/admin/object_history.html:23
+msgid "Action"
+msgstr ""
+
+#: templates/admin/object_history.html:37
+msgid ""
+"This object doesn't have a change history. It probably wasn't added via this "
+"admin site."
+msgstr ""
+
+#: templates/admin/pagination.html:7 templates/admin/search_form.html:20
+msgid "Show all"
 msgstr ""
 
 #: templates/admin/related_widget_wrapper.html:8
@@ -419,46 +770,22 @@ msgstr ""
 msgid "Delete selected %(model)s"
 msgstr ""
 
-#: templates/admin/search_form.html:8 templates/admin/search_form.html:19
+#: templates/admin/search_form.html:6 templates/admin/search_form.html:17
 msgid "Search"
 msgstr ""
 
-#: templates/admin/search_form.html:22
+#: templates/admin/search_form.html:20
 #, python-format
 msgid "%(counter)s result"
 msgid_plural "%(counter)s results"
 msgstr[0] ""
 msgstr[1] ""
 
-#: templates/admin/search_form.html:22
+#: templates/admin/search_form.html:20
 #, python-format
 msgid "%(full_result_count)s total"
 msgstr ""
 
-#: templates/admin/sidebar.html:12
-msgid "View site"
-msgstr ""
-
-#: templates/admin/sidebar.html:52 templates/admin/sidebar.html.py:69
-msgid "Add bookmark"
-msgstr ""
-
-#: templates/admin/sidebar.html:65
-msgid "Delete bookmark"
-msgstr ""
-
-#: templates/admin/sidebar.html:66
-msgid "Are you sure want to delete this bookmark?"
-msgstr ""
-
-#: templates/admin/sidebar.html:93
-msgid "Documentation"
-msgstr ""
-
-#: templates/admin/sidebar.html:113
-msgid "Application page"
-msgstr ""
-
 #: templates/admin/submit_line.html:5
 msgid "Save as new"
 msgstr ""
@@ -471,44 +798,6 @@ msgstr ""
 msgid "Save and continue editing"
 msgstr ""
 
-#: templates/jet/dashboard/dashboard.html:17
-msgid "Delete widget"
-msgstr ""
-
-#: templates/jet/dashboard/dashboard.html:18
-msgid "Are you sure want to delete this widget?"
-msgstr ""
-
-#: templates/jet/dashboard/dashboard_tools.html:7
-msgid "widgets"
-msgstr ""
-
-#: templates/jet/dashboard/dashboard_tools.html:8
-msgid "available"
-msgstr ""
-
-#: templates/jet/dashboard/dashboard_tools.html:13
-msgid "inititals"
-msgstr ""
-
-#: templates/jet/dashboard/modules/app_list.html:6
-#, python-format
-msgid "Models in the %(name)s application"
-msgstr ""
-
-#: templates/jet/dashboard/modules/feed.html:13
-#: templates/jet/dashboard/modules/link_list.html:26
-msgid "Nothing to show"
-msgstr ""
-
-#: templates/jet/dashboard/modules/recent_actions.html:6
-msgid "None available"
-msgstr ""
-
-#: templates/jet/dashboard/modules/recent_actions.html:26
-msgid "Unknown content"
-msgstr ""
-
 #: templates/registration/logged_out.html:9
 msgid "Thanks for spending some quality time with the Web site today."
 msgstr ""
@@ -518,7 +807,7 @@ msgid "Log in again"
 msgstr ""
 
 #: templates/registration/password_change_done.html:7
-#: templates/registration/password_change_form.html:12
+#: templates/registration/password_change_form.html:7
 msgid "Password change"
 msgstr ""
 
@@ -526,24 +815,12 @@ msgstr ""
 msgid "Your password was changed."
 msgstr ""
 
-#: templates/registration/password_change_form.html:30
+#: templates/registration/password_change_form.html:25
 msgid ""
 "Please enter your old password, for security's sake, and then enter your new "
 "password twice so we can verify you typed it in correctly."
 msgstr ""
 
-#: templates/registration/password_change_form.html:55
+#: templates/registration/password_change_form.html:50
 msgid "Change my password"
 msgstr ""
-
-#: views.py:18
-msgid "Widget was successfully updated"
-msgstr ""
-
-#: views.py:87 views.py:88
-msgid "Items"
-msgstr ""
-
-#: views.py:195
-msgid "Widget has been successfully added"
-msgstr ""

二進制
jet/locale/ru/LC_MESSAGES/django.mo


+ 492 - 205
jet/locale/ru/LC_MESSAGES/django.po

@@ -6,7 +6,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2015-08-07 13:25+0000\n"
+"POT-Creation-Date: 2015-09-07 13:42+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,178 +17,552 @@ msgstr ""
 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
 
-#: dashboard.py:157
+#: dashboard/dashboard.py:159
 msgid "Quick links"
 msgstr "Быстрые ссылки"
 
-#: dashboard.py:163
+#: dashboard/dashboard.py:165
 msgid "Return to site"
 msgstr "Вернуться на сайт"
 
-#: dashboard.py:164 templates/admin/base.html:85
+#: dashboard/dashboard.py:166 templates/admin/base.html:77
 msgid "Change password"
 msgstr ""
 
-#: dashboard.py:166 templates/admin/base.html:89
+#: dashboard/dashboard.py:168 templates/admin/base.html:81
 msgid "Log out"
 msgstr ""
 
-#: dashboard.py:174 modules.py:158 templates/admin/sidebar.html:23
+#: dashboard/dashboard.py:176 dashboard/modules.py:160
+#: templates/admin/base.html:157 templates/admin/index.html:14
 msgid "Applications"
 msgstr "Приложения"
 
-#: dashboard.py:182
+#: dashboard/dashboard.py:184
 msgid "Administration"
 msgstr "Администрирование"
 
-#: dashboard.py:190 modules.py:238
+#: dashboard/dashboard.py:192 dashboard/modules.py:242
+#: templates/admin/index.html:68
 msgid "Recent Actions"
 msgstr "Последние действия"
 
-#: dashboard.py:198
+#: dashboard/dashboard.py:200
 msgid "Latest Django News"
 msgstr "Новости от Django"
 
-#: dashboard.py:207
+#: dashboard/dashboard.py:209
 msgid "Support"
 msgstr "Поддержка"
 
-#: dashboard.py:210
+#: dashboard/dashboard.py:212
 msgid "Django documentation"
 msgstr "Документация по Django"
 
-#: dashboard.py:215
+#: dashboard/dashboard.py:217
 msgid "Django \"django-users\" mailing list"
 msgstr "Гугл-группа \"django-users\""
 
-#: dashboard.py:220
+#: dashboard/dashboard.py:222
 msgid "Django irc channel"
 msgstr "IRC канал Django"
 
-#: dashboard.py:235
+#: dashboard/dashboard.py:237
 msgid "Application models"
 msgstr "Модели приложения"
 
-#: models.py:8 modules.py:113 templates/admin/sidebar.html:57
-msgid "URL"
-msgstr "URL"
-
-#: models.py:9
-msgid "title"
+#: dashboard/dashboard_modules/google_analytics.py:143
+#: dashboard/dashboard_modules/yandex_metrika.py:101
+msgid "Revoke access"
+msgstr "Убрать доступ"
+
+#: dashboard/dashboard_modules/google_analytics.py:148
+#: dashboard/dashboard_modules/yandex_metrika.py:106
+msgid "Grant access"
+msgstr "Предоставить доступ"
+
+#: dashboard/dashboard_modules/google_analytics.py:161
+#: dashboard/dashboard_modules/yandex_metrika.py:113
+msgid "Access"
+msgstr "Доступ"
+
+#: dashboard/dashboard_modules/google_analytics.py:162
+#: dashboard/dashboard_modules/yandex_metrika.py:114
+msgid "Counter"
+msgstr "Счетчик"
+
+#: dashboard/dashboard_modules/google_analytics.py:163
+#: dashboard/dashboard_modules/yandex_metrika.py:115
+msgid "Statistics period"
+msgstr "Статистика за период"
+
+#: dashboard/dashboard_modules/google_analytics.py:164
+#: dashboard/dashboard_modules/yandex_metrika.py:116
+msgid "Today"
+msgstr "Сегодня"
+
+#: dashboard/dashboard_modules/google_analytics.py:165
+#: dashboard/dashboard_modules/yandex_metrika.py:117
+msgid "Last week"
+msgstr "Последняя неделя"
+
+#: dashboard/dashboard_modules/google_analytics.py:166
+#: dashboard/dashboard_modules/yandex_metrika.py:118
+msgid "Last month"
+msgstr "Последний месяц"
+
+#: dashboard/dashboard_modules/google_analytics.py:167
+#: dashboard/dashboard_modules/yandex_metrika.py:119
+msgid "Last quarter"
+msgstr "Последний квартал"
+
+#: dashboard/dashboard_modules/google_analytics.py:168
+#: dashboard/dashboard_modules/yandex_metrika.py:120
+msgid "Last year"
+msgstr "Последний год"
+
+#: dashboard/dashboard_modules/google_analytics.py:178
+#: dashboard/dashboard_modules/yandex_metrika.py:130
+msgid "none"
+msgstr "не указано"
+
+#: dashboard/dashboard_modules/google_analytics.py:181
+#: dashboard/dashboard_modules/yandex_metrika.py:133
+msgid "grant access first"
+msgstr "сначала предоставьте доступ"
+
+#: dashboard/dashboard_modules/google_analytics.py:181
+#: dashboard/dashboard_modules/yandex_metrika.py:133
+msgid "counters loading failed"
+msgstr "не удалось загрузить счетчики"
+
+#: dashboard/dashboard_modules/google_analytics.py:186
+#: dashboard/dashboard_modules/yandex_metrika.py:138
+msgid "Show"
+msgstr "Показывать"
+
+#: dashboard/dashboard_modules/google_analytics.py:187
+#: dashboard/templates/jet.dashboard/modules/google_analytics_period_visitors.html:15
+msgid "Users"
+msgstr "Пользователи"
+
+#: dashboard/dashboard_modules/google_analytics.py:188
+#: dashboard/templates/jet.dashboard/modules/google_analytics_period_visitors.html:16
+msgid "Sessions"
+msgstr "Сессии"
+
+#: dashboard/dashboard_modules/google_analytics.py:189
+#: dashboard/dashboard_modules/yandex_metrika.py:141
+#: dashboard/templates/jet.dashboard/modules/google_analytics_period_visitors.html:17
+#: dashboard/templates/jet.dashboard/modules/yandex_metrika_period_visitors.html:17
+msgid "Views"
+msgstr "Просмотры"
+
+#: dashboard/dashboard_modules/google_analytics.py:191
+#: dashboard/dashboard_modules/google_analytics.py:199
+#: dashboard/dashboard_modules/yandex_metrika.py:143
+#: dashboard/dashboard_modules/yandex_metrika.py:151
+msgid "Group"
+msgstr "Группировать"
+
+#: dashboard/dashboard_modules/google_analytics.py:192
+#: dashboard/dashboard_modules/google_analytics.py:200
+#: dashboard/dashboard_modules/yandex_metrika.py:144
+#: dashboard/dashboard_modules/yandex_metrika.py:152
+msgid "By day"
+msgstr "По дням"
+
+#: dashboard/dashboard_modules/google_analytics.py:193
+#: dashboard/dashboard_modules/google_analytics.py:201
+#: dashboard/dashboard_modules/yandex_metrika.py:145
+#: dashboard/dashboard_modules/yandex_metrika.py:153
+msgid "By week"
+msgstr "По неделям"
+
+#: dashboard/dashboard_modules/google_analytics.py:194
+#: dashboard/dashboard_modules/google_analytics.py:202
+#: dashboard/dashboard_modules/yandex_metrika.py:146
+#: dashboard/dashboard_modules/yandex_metrika.py:154
+msgid "By month"
+msgstr "По месяцам"
+
+#: dashboard/dashboard_modules/google_analytics.py:275
+#, python-format
+msgid ""
+"Please <a href=\"%s\">attach Google account and choose Google Analytics "
+"counter</a> to start using widget"
 msgstr ""
+"Пожалуйста <a href=\"%s\">прикрепите аккаунт Google и выберите счетчик "
+"Google Analytics</a> для виджета"
 
-#: models.py:10 models.py:24 models.py:40
-msgid "user"
+#: dashboard/dashboard_modules/google_analytics.py:278
+#, python-format
+msgid ""
+"Please <a href=\"%s\">select Google Analytics counter</a> to start using "
+"widget"
 msgstr ""
+"Пожалуйста <a href=\"%s\">выберите счетчик Google Analytics</a> для виджета"
 
-#: models.py:11 models.py:25
-msgid "date created"
+#: dashboard/dashboard_modules/google_analytics.py:297
+#: dashboard/dashboard_modules/google_analytics_views.py:42
+#: dashboard/dashboard_modules/yandex_metrika.py:231
+#: dashboard/dashboard_modules/yandex_metrika_views.py:37
+msgid "API request failed."
+msgstr "Ошибка запроса к API."
+
+#: dashboard/dashboard_modules/google_analytics.py:299
+#: dashboard/dashboard_modules/yandex_metrika.py:233
+#, python-format
+msgid " Try to <a href=\"%s\">revoke and grant access</a> again"
+msgstr " Попробуйте <a href=\"%s\">убрать и предоставить доступ</a> заново"
+
+#: dashboard/dashboard_modules/google_analytics.py:304
+msgid "Google Analytics visitors totals"
+msgstr "Данные о посещениях Google Analytics"
+
+#: dashboard/dashboard_modules/google_analytics.py:316
+msgid "users"
+msgstr "пользователи"
+
+#: dashboard/dashboard_modules/google_analytics.py:317
+msgid "sessions"
+msgstr "сессии"
+
+#: dashboard/dashboard_modules/google_analytics.py:318
+#: dashboard/dashboard_modules/yandex_metrika.py:254
+msgid "views"
+msgstr "просмотры"
+
+#: dashboard/dashboard_modules/google_analytics.py:320
+#: dashboard/dashboard_modules/google_analytics.py:365
+#: dashboard/dashboard_modules/google_analytics.py:404
+#: dashboard/dashboard_modules/yandex_metrika.py:256
+#: dashboard/dashboard_modules/yandex_metrika.py:295
+#: dashboard/dashboard_modules/yandex_metrika.py:328
+msgid "Bad server response"
+msgstr "Некорректный ответ сервера"
+
+#: dashboard/dashboard_modules/google_analytics.py:324
+msgid "Google Analytics visitors chart"
+msgstr "График посещений Google Analytics"
+
+#: dashboard/dashboard_modules/google_analytics.py:369
+msgid "Google Analytics period visitors"
+msgstr "Посещения Google Analytics за период"
+
+#: dashboard/dashboard_modules/google_analytics_views.py:26
+#: dashboard/dashboard_modules/google_analytics_views.py:46
+#: dashboard/dashboard_modules/yandex_metrika_views.py:23
+#: dashboard/dashboard_modules/yandex_metrika_views.py:45
+msgid "Module not found"
+msgstr "Модуль не найден"
+
+#: dashboard/dashboard_modules/google_analytics_views.py:44
+#: dashboard/dashboard_modules/yandex_metrika_views.py:43
+msgid "Bad arguments"
+msgstr "Некорректные аргументы"
+
+#: dashboard/dashboard_modules/yandex_metrika.py:139
+#: dashboard/templates/jet.dashboard/modules/yandex_metrika_period_visitors.html:15
+msgid "Visitors"
+msgstr "Посетители"
+
+#: dashboard/dashboard_modules/yandex_metrika.py:140
+#: dashboard/templates/jet.dashboard/modules/yandex_metrika_period_visitors.html:16
+msgid "Visits"
+msgstr "Визиты"
+
+#: dashboard/dashboard_modules/yandex_metrika.py:214
+#, python-format
+msgid ""
+"Please <a href=\"%s\">attach Yandex account and choose Yandex Metrika "
+"counter</a> to start using widget"
 msgstr ""
+"Пожалуйста <a href=\"%s\">прикрепите аккаунт Яндекс и выберите счетчик "
+"Яндекс Метрики</a> для виджета"
 
-#: models.py:14
-msgid "bookmark"
+#: dashboard/dashboard_modules/yandex_metrika.py:217
+#, python-format
+msgid ""
+"Please <a href=\"%s\">select Yandex Metrika counter</a> to start using widget"
 msgstr ""
+"Пожалуйста <a href=\"%s\">выберите счетчик Яндекс Метрики</a> для виджета"
 
-#: models.py:15 templates/admin/sidebar.html:70
-msgid "bookmarks"
-msgstr "закладки"
+#: dashboard/dashboard_modules/yandex_metrika.py:240
+msgid "Yandex Metrika visitors totals"
+msgstr "Данные о посещениях Яндекс Метрики"
 
-#: models.py:23 models.py:39
-msgid "application name"
-msgstr ""
+#: dashboard/dashboard_modules/yandex_metrika.py:252
+msgid "visitors"
+msgstr "посетители"
 
-#: models.py:28
-msgid "pinned application"
-msgstr ""
+#: dashboard/dashboard_modules/yandex_metrika.py:253
+msgid "visits"
+msgstr "визиты"
 
-#: models.py:29
-msgid "pinned applications"
-msgstr ""
+#: dashboard/dashboard_modules/yandex_metrika.py:260
+msgid "Yandex Metrika visitors chart"
+msgstr "График посещений Яндекс Метрики"
+
+#: dashboard/dashboard_modules/yandex_metrika.py:299
+msgid "Yandex Metrika period visitors"
+msgstr "Посещения Яндекс Метрики за период"
 
-#: models.py:37 modules.py:114 templates/admin/sidebar.html:55
+#: dashboard/models.py:11 dashboard/modules.py:116
+#: templates/admin/base.html:189
 msgid "Title"
 msgstr "Название"
 
-#: models.py:38
+#: dashboard/models.py:12
 msgid "module"
 msgstr ""
 
-#: models.py:41
+#: dashboard/models.py:13 models.py:28
+msgid "application name"
+msgstr ""
+
+#: dashboard/models.py:14 models.py:14 models.py:29
+msgid "user"
+msgstr ""
+
+#: dashboard/models.py:15
 msgid "column"
 msgstr ""
 
-#: models.py:42
+#: dashboard/models.py:16
 msgid "order"
 msgstr ""
 
-#: models.py:43
+#: dashboard/models.py:17
 msgid "settings"
 msgstr ""
 
-#: models.py:44
+#: dashboard/models.py:18
 msgid "children"
 msgstr ""
 
-#: models.py:45
+#: dashboard/models.py:19
 msgid "collapsed"
 msgstr ""
 
-#: models.py:48
+#: dashboard/models.py:22
 msgid "user dashboard module"
 msgstr ""
 
-#: models.py:49
+#: dashboard/models.py:23
 msgid "user dashboard modules"
 msgstr ""
 
-#: modules.py:115
+#: dashboard/modules.py:115 models.py:12 templates/admin/base.html:191
+msgid "URL"
+msgstr "URL"
+
+#: dashboard/modules.py:117
 msgid "External link"
 msgstr "Внешняя ссылка"
 
-#: modules.py:119
+#: dashboard/modules.py:121
 msgid "Layout"
 msgstr "Отображение"
 
-#: modules.py:119
+#: dashboard/modules.py:121
 msgid "Stacked"
 msgstr "Списком"
 
-#: modules.py:119
+#: dashboard/modules.py:121
 msgid "Inline"
 msgstr "В строчку"
 
-#: modules.py:123 modules.py:129
+#: dashboard/modules.py:125 dashboard/modules.py:131
 msgid "Links"
 msgstr "Ссылки"
 
-#: modules.py:128
+#: dashboard/modules.py:130
 msgid "Link"
 msgstr "Ссылка"
 
-#: modules.py:198
+#: dashboard/modules.py:201
 msgid "Models"
 msgstr "Модели"
 
-#: modules.py:234 modules.py:305
+#: dashboard/modules.py:238 dashboard/modules.py:309
 msgid "Items limit"
 msgstr "Количество элементов"
 
-#: modules.py:306
+#: dashboard/modules.py:310
 msgid "Feed URL"
 msgstr "URL потока"
 
-#: modules.py:310
+#: dashboard/modules.py:314
 msgid "RSS Feed"
 msgstr "RSS поток"
 
-#: modules.py:352
+#: dashboard/modules.py:356
 msgid "You must install the FeedParser python module"
 msgstr "Необходимо установить python модуль FeedParser"
 
-#: modules.py:357
+#: dashboard/modules.py:361
 msgid "You must provide a valid feed URL"
 msgstr "Необходимо указать корректный URL потока"
 
+#: dashboard/templates/admin/app_index.html:32
+#: dashboard/templates/jet.dashboard/update_module.html:14
+#: templates/admin/base.html:92 templates/admin/base.html.py:141
+#: templates/admin/change_form.html:17 templates/admin/change_list.html:33
+#: templates/admin/delete_confirmation.html:8
+#: templates/admin/delete_selected_confirmation.html:8
+#: templates/admin/object_history.html:6
+#: templates/registration/logged_out.html:4
+#: templates/registration/password_change_done.html:6
+#: templates/registration/password_change_form.html:6
+msgid "Home"
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/dashboard.html:17
+msgid "Delete widget"
+msgstr "Удалить виджет"
+
+#: dashboard/templates/jet.dashboard/dashboard.html:18
+msgid "Are you sure want to delete this widget?"
+msgstr "Вы точно хотите удалить этот виджет?"
+
+#: dashboard/templates/jet.dashboard/dashboard_tools.html:7
+msgid "widgets"
+msgstr "виджеты"
+
+#: dashboard/templates/jet.dashboard/dashboard_tools.html:8
+msgid "available"
+msgstr "доступные"
+
+#: dashboard/templates/jet.dashboard/dashboard_tools.html:13
+msgid "inititals"
+msgstr "изначальные"
+
+#: dashboard/templates/jet.dashboard/dashboard_tools.html:18
+#: dashboard/templates/jet.dashboard/modules/app_list.html:18
+#: dashboard/templates/jet.dashboard/modules/model_list.html:8
+#: templates/admin/change_form.html:20 templates/admin/index.html:34
+msgid "Add"
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/module.html:19
+#: dashboard/templates/jet.dashboard/modules/app_list.html:24
+#: dashboard/templates/jet.dashboard/modules/model_list.html:14
+#: dashboard/views.py:86 templates/admin/edit_inline/stacked.html:44
+#: templates/admin/edit_inline/tabular.html:39 templates/admin/index.html:40
+msgid "Change"
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/module.html:22
+#: templates/admin/delete_confirmation.html:12
+#: templates/admin/related_widget_wrapper.html:24
+#: templates/admin/submit_line.html:11
+msgid "Delete"
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/modules/app_list.html:7
+#: dashboard/templates/jet.dashboard/modules/app_list.html:10
+#: templates/admin/index.html:23 templates/admin/index.html.py:26
+#, python-format
+msgid "Models in the %(name)s application"
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/modules/feed.html:13
+#: dashboard/templates/jet.dashboard/modules/google_analytics_period_visitors.html:34
+#: dashboard/templates/jet.dashboard/modules/google_analytics_visitors_chart.html:30
+#: dashboard/templates/jet.dashboard/modules/google_analytics_visitors_totals.html:23
+#: dashboard/templates/jet.dashboard/modules/link_list.html:26
+#: dashboard/templates/jet.dashboard/modules/yandex_metrika_period_visitors.html:34
+#: dashboard/templates/jet.dashboard/modules/yandex_metrika_visitors_chart.html:30
+#: dashboard/templates/jet.dashboard/modules/yandex_metrika_visitors_totals.html:23
+msgid "Nothing to show"
+msgstr "Содержимое отсутствует"
+
+#: dashboard/templates/jet.dashboard/modules/google_analytics_period_visitors.html:14
+#: dashboard/templates/jet.dashboard/modules/yandex_metrika_period_visitors.html:14
+msgid "Date"
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/modules/recent_actions.html:6
+#: templates/admin/index.html:75
+msgid "None available"
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/modules/recent_actions.html:29
+#: templates/admin/index.html:98
+msgid "Unknown content"
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/update_module.html:35
+#: templates/admin/change_form.html:62
+msgid "General"
+msgstr "Общее"
+
+#: dashboard/templates/jet.dashboard/update_module.html:56
+#: templates/admin/change_list.html:61
+#: templates/admin/edit_inline/stacked.html:8
+#: templates/admin/edit_inline/tabular.html:10
+#: templates/admin/includes/fieldset.html:9 templates/admin/login.html:27
+#: templates/registration/password_change_form.html:21
+msgid "Please correct the errors below."
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/update_module.html:126
+#: templates/admin/base.html:211 templates/admin/base.html.py:218
+#: templates/admin/edit_inline/stacked.html:21
+#: templates/admin/edit_inline/stacked.html:75
+#: templates/admin/edit_inline/tabular.html:100
+msgid "Remove"
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/update_module.html:127
+#: templates/admin/edit_inline/stacked.html:76
+#: templates/admin/edit_inline/tabular.html:99
+#, python-format
+msgid "Add another %(verbose_name)s"
+msgstr ""
+
+#: dashboard/templates/jet.dashboard/update_module.html:135
+#: templates/admin/pagination.html:20 templates/admin/submit_line.html:4
+msgid "Save"
+msgstr ""
+
+#: dashboard/views.py:18
+msgid "Widget was successfully updated"
+msgstr "Виджет успешно изменен"
+
+#: dashboard/views.py:90 dashboard/views.py:91
+msgid "Items"
+msgstr "Элементы"
+
+#: dashboard/views.py:153
+msgid "Widget has been successfully added"
+msgstr "Виджет успешно добавлен"
+
+#: models.py:13
+msgid "title"
+msgstr ""
+
+#: models.py:15 models.py:30
+msgid "date created"
+msgstr ""
+
+#: models.py:18
+msgid "bookmark"
+msgstr ""
+
+#: models.py:19 templates/admin/base.html:204
+msgid "bookmarks"
+msgstr "закладки"
+
+#: models.py:33
+msgid "pinned application"
+msgstr ""
+
+#: models.py:34
+msgid "pinned applications"
+msgstr ""
+
 #: templates/admin/actions.html:8
 msgid "Run the selected action"
 msgstr ""
@@ -210,59 +584,56 @@ msgstr ""
 msgid "Clear selection"
 msgstr ""
 
-#: templates/admin/app_index.html:30 templates/admin/base.html:100
-#: templates/admin/change_form.html:22 templates/admin/change_list.html:33
-#: templates/admin/delete_confirmation.html:8
-#: templates/admin/delete_selected_confirmation.html:8
-#: templates/admin/sidebar.html:7
-#: templates/jet/dashboard/update_module.html:13
-#: templates/registration/logged_out.html:4
-#: templates/registration/password_change_done.html:6
-#: templates/registration/password_change_form.html:11
-msgid "Home"
+#: templates/admin/base.html:146
+msgid "View site"
 msgstr ""
 
-#: templates/admin/change_form.html:25
-#: templates/jet/dashboard/dashboard_tools.html:18
-#: templates/jet/dashboard/modules/app_list.html:13
-#: templates/jet/dashboard/modules/model_list.html:8
-msgid "Add"
+#: templates/admin/base.html:186 templates/admin/base.html.py:203
+msgid "Add bookmark"
+msgstr "Добавить закладку"
+
+#: templates/admin/base.html:199
+msgid "Delete bookmark"
+msgstr "Удалить закладку"
+
+#: templates/admin/base.html:200
+msgid "Are you sure want to delete this bookmark?"
+msgstr "Вы точно хотите удалить эту закладку?"
+
+#: templates/admin/base.html:226
+msgid "Documentation"
 msgstr ""
 
-#: templates/admin/change_form.html:40
+#: templates/admin/base.html:249
+msgid "Application page"
+msgstr "Страница приложения"
+
+#: templates/admin/base_site.html:3
+#, fuzzy
+#| msgid "Django irc channel"
+msgid "Django site admin"
+msgstr "IRC канал Django"
+
+#: templates/admin/change_form.html:35 templates/admin/object_history.html:10
 msgid "History"
 msgstr ""
 
-#: templates/admin/change_form.html:43
+#: templates/admin/change_form.html:38
 #: templates/admin/edit_inline/stacked.html:49
 #: templates/admin/edit_inline/tabular.html:45
 msgid "View on site"
 msgstr ""
 
-#: templates/admin/change_form.html:67
-#: templates/jet/dashboard/update_module.html:34
-msgid "General"
-msgstr "Общее"
-
 #: templates/admin/change_list.html:51
 #, python-format
 msgid "Add %(name)s"
 msgstr ""
 
 #: templates/admin/change_list.html:61 templates/admin/login.html:27
-#: templates/registration/password_change_form.html:26
+#: templates/registration/password_change_form.html:21
 msgid "Please correct the error below."
 msgstr ""
 
-#: templates/admin/change_list.html:61
-#: templates/admin/edit_inline/stacked.html:8
-#: templates/admin/edit_inline/tabular.html:10
-#: templates/admin/includes/fieldset.html:10 templates/admin/login.html:27
-#: templates/jet/dashboard/update_module.html:53
-#: templates/registration/password_change_form.html:26
-msgid "Please correct the errors below."
-msgstr ""
-
 #: templates/admin/change_list_results.html:17
 #, python-format
 msgid "Sorting priority: %(priority_number)s"
@@ -280,12 +651,6 @@ msgstr ""
 msgid "try to reset filters"
 msgstr "попробуйте сбросить фильтры"
 
-#: templates/admin/delete_confirmation.html:12
-#: templates/admin/related_widget_wrapper.html:24
-#: templates/admin/submit_line.html:11 templates/jet/dashboard/module.html:22
-msgid "Delete"
-msgstr ""
-
 #: templates/admin/delete_confirmation.html:18
 #, python-format
 msgid ""
@@ -349,29 +714,6 @@ msgid ""
 "following objects and their related items will be deleted:"
 msgstr ""
 
-#: templates/admin/edit_inline/stacked.html:21
-#: templates/admin/edit_inline/stacked.html:75
-#: templates/admin/edit_inline/tabular.html:100
-#: templates/admin/sidebar.html:77 templates/admin/sidebar.html.py:84
-#: templates/jet/dashboard/update_module.html:123
-msgid "Remove"
-msgstr ""
-
-#: templates/admin/edit_inline/stacked.html:44
-#: templates/admin/edit_inline/tabular.html:39
-#: templates/jet/dashboard/module.html:19
-#: templates/jet/dashboard/modules/app_list.html:19
-#: templates/jet/dashboard/modules/model_list.html:14 views.py:83
-msgid "Change"
-msgstr ""
-
-#: templates/admin/edit_inline/stacked.html:76
-#: templates/admin/edit_inline/tabular.html:99
-#: templates/jet/dashboard/update_module.html:124
-#, python-format
-msgid "Add another %(verbose_name)s"
-msgstr ""
-
 #: templates/admin/edit_inline/tabular.html:25
 msgid "Delete?"
 msgstr ""
@@ -385,6 +727,10 @@ msgstr ""
 msgid "Summary"
 msgstr ""
 
+#: templates/admin/index.html:56
+msgid "You don't have permission to edit anything."
+msgstr ""
+
 #: templates/admin/login.html:16
 msgid "<span class=\"bright\">Admin</span> Site"
 msgstr "Панель <span class=\"bright\">Администратора</span>"
@@ -397,13 +743,28 @@ msgstr ""
 msgid "Forgotten your password or username?"
 msgstr ""
 
-#: templates/admin/pagination.html:7 templates/admin/search_form.html:22
-msgid "Show all"
+#: templates/admin/object_history.html:21
+msgid "Date/time"
 msgstr ""
 
-#: templates/admin/pagination.html:20 templates/admin/submit_line.html:4
-#: templates/jet/dashboard/update_module.html:132
-msgid "Save"
+#: templates/admin/object_history.html:22
+msgid "User"
+msgstr ""
+
+#: templates/admin/object_history.html:23
+#, fuzzy
+#| msgid "Applications"
+msgid "Action"
+msgstr "Приложения"
+
+#: templates/admin/object_history.html:37
+msgid ""
+"This object doesn't have a change history. It probably wasn't added via this "
+"admin site."
+msgstr ""
+
+#: templates/admin/pagination.html:7 templates/admin/search_form.html:20
+msgid "Show all"
 msgstr ""
 
 #: templates/admin/related_widget_wrapper.html:8
@@ -421,11 +782,11 @@ msgstr ""
 msgid "Delete selected %(model)s"
 msgstr ""
 
-#: templates/admin/search_form.html:8 templates/admin/search_form.html:19
+#: templates/admin/search_form.html:6 templates/admin/search_form.html:17
 msgid "Search"
 msgstr ""
 
-#: templates/admin/search_form.html:22
+#: templates/admin/search_form.html:20
 #, python-format
 msgid "%(counter)s result"
 msgid_plural "%(counter)s results"
@@ -433,35 +794,11 @@ msgstr[0] ""
 msgstr[1] ""
 msgstr[2] ""
 
-#: templates/admin/search_form.html:22
+#: templates/admin/search_form.html:20
 #, python-format
 msgid "%(full_result_count)s total"
 msgstr ""
 
-#: templates/admin/sidebar.html:12
-msgid "View site"
-msgstr ""
-
-#: templates/admin/sidebar.html:52 templates/admin/sidebar.html.py:69
-msgid "Add bookmark"
-msgstr "Добавить закладку"
-
-#: templates/admin/sidebar.html:65
-msgid "Delete bookmark"
-msgstr "Удалить закладку"
-
-#: templates/admin/sidebar.html:66
-msgid "Are you sure want to delete this bookmark?"
-msgstr "Вы точно хотите удалить эту закладку?"
-
-#: templates/admin/sidebar.html:93
-msgid "Documentation"
-msgstr ""
-
-#: templates/admin/sidebar.html:113
-msgid "Application page"
-msgstr "Страница приложения"
-
 #: templates/admin/submit_line.html:5
 msgid "Save as new"
 msgstr ""
@@ -474,44 +811,6 @@ msgstr ""
 msgid "Save and continue editing"
 msgstr ""
 
-#: templates/jet/dashboard/dashboard.html:17
-msgid "Delete widget"
-msgstr "Удалить виджет"
-
-#: templates/jet/dashboard/dashboard.html:18
-msgid "Are you sure want to delete this widget?"
-msgstr "Вы точно хотите удалить этот виджет?"
-
-#: templates/jet/dashboard/dashboard_tools.html:7
-msgid "widgets"
-msgstr "виджеты"
-
-#: templates/jet/dashboard/dashboard_tools.html:8
-msgid "available"
-msgstr "доступные"
-
-#: templates/jet/dashboard/dashboard_tools.html:13
-msgid "inititals"
-msgstr "изначальные"
-
-#: templates/jet/dashboard/modules/app_list.html:6
-#, python-format
-msgid "Models in the %(name)s application"
-msgstr ""
-
-#: templates/jet/dashboard/modules/feed.html:13
-#: templates/jet/dashboard/modules/link_list.html:26
-msgid "Nothing to show"
-msgstr "Содержимое отсутствует"
-
-#: templates/jet/dashboard/modules/recent_actions.html:6
-msgid "None available"
-msgstr ""
-
-#: templates/jet/dashboard/modules/recent_actions.html:26
-msgid "Unknown content"
-msgstr ""
-
 #: templates/registration/logged_out.html:9
 msgid "Thanks for spending some quality time with the Web site today."
 msgstr ""
@@ -521,7 +820,7 @@ msgid "Log in again"
 msgstr ""
 
 #: templates/registration/password_change_done.html:7
-#: templates/registration/password_change_form.html:12
+#: templates/registration/password_change_form.html:7
 msgid "Password change"
 msgstr ""
 
@@ -529,24 +828,12 @@ msgstr ""
 msgid "Your password was changed."
 msgstr ""
 
-#: templates/registration/password_change_form.html:30
+#: templates/registration/password_change_form.html:25
 msgid ""
 "Please enter your old password, for security's sake, and then enter your new "
 "password twice so we can verify you typed it in correctly."
 msgstr ""
 
-#: templates/registration/password_change_form.html:55
+#: templates/registration/password_change_form.html:50
 msgid "Change my password"
 msgstr ""
-
-#: views.py:18
-msgid "Widget was successfully updated"
-msgstr "Виджет успешно изменен"
-
-#: views.py:87 views.py:88
-msgid "Items"
-msgstr "Элементы"
-
-#: views.py:195
-msgid "Widget has been successfully added"
-msgstr "Виджет успешно добавлен"

+ 17 - 0
jet/migrations/0002_delete_userdashboardmodule.py

@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('jet', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.DeleteModel(
+            name='UserDashboardModule',
+        ),
+    ]

+ 2 - 28
jet/models.py

@@ -1,8 +1,10 @@
 from importlib import import_module
+import json
 from django.db import models
 from django.utils import timezone
 from django.utils.encoding import python_2_unicode_compatible
 from django.utils.translation import ugettext_lazy as _
+from jet.utils import LazyDateTimeEncoder
 
 
 @python_2_unicode_compatible
@@ -35,31 +37,3 @@ class PinnedApplication(models.Model):
     def __str__(self):
         return self.app_label
 
-
-@python_2_unicode_compatible
-class UserDashboardModule(models.Model):
-    title = models.CharField(verbose_name=_('Title'), max_length=255)
-    module = models.CharField(verbose_name=_('module'), max_length=255)
-    app_label = models.CharField(verbose_name=_('application name'), max_length=255, null=True, blank=True)
-    user = models.PositiveIntegerField(verbose_name=_('user'))
-    column = models.PositiveIntegerField(verbose_name=_('column'))
-    order = models.IntegerField(verbose_name=_('order'))
-    settings = models.TextField(verbose_name=_('settings'), default='', blank=True)
-    children = models.TextField(verbose_name=_('children'), default='', blank=True)
-    collapsed = models.BooleanField(verbose_name=_('collapsed'), default=False)
-
-    class Meta:
-        verbose_name = _('user dashboard module')
-        verbose_name_plural = _('user dashboard modules')
-        ordering = ('column', 'order')
-
-    def __str__(self):
-        return self.module
-
-    def load_module(self):
-        package, module_name = self.module.rsplit('.', 1)
-        package = import_module(package)
-        module = getattr(package, module_name)
-
-        return module
-

+ 0 - 4
jet/settings.py

@@ -2,7 +2,3 @@ from django.conf import settings
 
 # Theme
 JET_THEME = getattr(settings, 'JET_THEME', 'default')
-
-# Dashboard
-JET_INDEX_DASHBOARD = getattr(settings, 'JET_INDEX_DASHBOARD', 'jet.dashboard.DefaultIndexDashboard')
-JET_APP_INDEX_DASHBOARD = getattr(settings, 'JET_APP_INDEX_DASHBOARD', 'jet.dashboard.DefaultAppIndexDashboard')

+ 50 - 0
jet/south_migrations/0002_auto__del_userdashboardmodule.py

@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+from south.utils import datetime_utils as datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Deleting model 'UserDashboardModule'
+        db.delete_table(u'jet_userdashboardmodule')
+
+
+    def backwards(self, orm):
+        # Adding model 'UserDashboardModule'
+        db.create_table(u'jet_userdashboardmodule', (
+            ('collapsed', self.gf('django.db.models.fields.BooleanField')(default=False)),
+            ('module', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('user', self.gf('django.db.models.fields.PositiveIntegerField')()),
+            ('children', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
+            ('title', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('column', self.gf('django.db.models.fields.PositiveIntegerField')()),
+            ('settings', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
+            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('app_label', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
+            ('order', self.gf('django.db.models.fields.IntegerField')()),
+        ))
+        db.send_create_signal(u'jet', ['UserDashboardModule'])
+
+
+    models = {
+        u'jet.bookmark': {
+            'Meta': {'ordering': "('date_add',)", 'object_name': 'Bookmark'},
+            'date_add': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'url': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
+            'user': ('django.db.models.fields.PositiveIntegerField', [], {})
+        },
+        u'jet.pinnedapplication': {
+            'Meta': {'ordering': "('date_add',)", 'object_name': 'PinnedApplication'},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date_add': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'user': ('django.db.models.fields.PositiveIntegerField', [], {})
+        }
+    }
+
+    complete_apps = ['jet']

+ 2 - 0
jet/static/jet/css/_changelist.scss

@@ -20,6 +20,7 @@
 
   table {
     @extend .table;
+    box-shadow: 0 2px 0 0 $content-border2-color;
     width: 100%;
   }
 
@@ -59,5 +60,6 @@
 
 table#change-history {
   @extend .table;
+  box-shadow: 0 2px 0 0 $content-border2-color;
   width: 100%;
 }

+ 7 - 3
jet/static/jet/css/_content.scss

@@ -4,6 +4,10 @@
   background-color: $background-color;
 }
 
+.dashboard #content {
+  width: 500px;
+}
+
 .small {
   font-size: 12px;
 }
@@ -72,10 +76,10 @@ ul.object-tools {
   }
 }
 
-.context-sidebar {
+.content-sidebar {
   float: right;
-  margin-left: 20px;
-  max-width: 33%;
+  width: 18em;
+  margin: 0 20px;
 }
 
 .dashboard .module table td a {

+ 67 - 3
jet/static/jet/css/_dashboard.scss

@@ -17,6 +17,7 @@
 
     &-wrapper {
       float: left;
+      min-width: 200px;
     }
 
     &.first {
@@ -91,6 +92,15 @@
       box-shadow: 0 2px 0 0 $content-border2-color;
       overflow: hidden;
 
+      &.contrast {
+        background: $content-contrast2-background-color;
+        color: $content-contrast2-text-color;
+
+        .loading-indicator {
+          color: $content-contrast2-text-color;
+        }
+      }
+
       ul:not(.inline) {
         @extend .clear-list;
 
@@ -139,12 +149,11 @@
 
       ul.inline {
         @extend .clear-list;
-        display: inline;
+        display: inline-block;
 
         li {
-          display: inline;
+          display: inline-block;
           margin-left: 10px;
-          line-height: 30px;
 
           &:first-child {
             margin-left: 0;
@@ -154,6 +163,16 @@
             white-space: nowrap;
           }
         }
+
+        &.bordered li {
+          border-left: 1px solid $content-border2-color;
+          margin-left: 0;
+          padding: 0 10px;
+
+          &:first-child {
+            border-left: 0;
+          }
+        }
       }
 
       .padding {
@@ -163,6 +182,51 @@
       .center {
         text-align: center;
       }
+
+      .big {
+        font-size: 20px;
+        font-weight: bold;
+      }
+
+      .highlight {
+        color: $link-color;
+      }
+
+      .dim {
+        color: $dim-text-color;
+      }
+
+      canvas {
+        .chart {
+          &-fillColor {
+            color: transparentize($hover-link-color, 0.75);
+          }
+
+          &-strokeColor {
+            color: $hover-link-color;
+          }
+
+          &-pointColor {
+            color: $content-contrast2-text-color
+          }
+
+          &-pointHighlightFill {
+            color: $hover-link-color;
+          }
+
+          &-scaleGridLineColor {
+            color: transparentize($black-color, 0.9);
+          }
+
+          &-scaleLineColor {
+            color: transparentize($black-color, 0.9);
+          }
+
+          &-scaleFontColor {
+            color: $content-contrast2-text-color
+          }
+        }
+      }
     }
 
     &-collapse .icon-arrow-up {

+ 4 - 0
jet/static/jet/css/_forms.scss

@@ -97,6 +97,10 @@ textarea {
 input[type=checkbox] {
   display: none;
 
+  &#action-toggle {
+    display: none !important;
+  }
+
   + label:before {
     @include font-icon;
 

+ 4 - 2
jet/static/jet/css/_table.scss

@@ -4,7 +4,6 @@
   background: $content-background-color;
   border-collapse: collapse;
   border-radius: 4px;
-  box-shadow: 0 2px 0 0 $content-border2-color;
   margin-bottom: 2px;
   overflow-x: auto;
   width: 100%;
@@ -117,9 +116,12 @@
         border-bottom: 0;
       }
 
+      & th {
+        text-align: left;
+      }
+
       & th, & td {
         padding: 8px;
-        text-align: left;
         font-size: 13px;
 
         &.action-checkbox {

+ 49 - 10
jet/static/jet/css/themes/default/base.css

@@ -643,7 +643,6 @@ a:hover {
   background: #fff;
   border-collapse: collapse;
   border-radius: 4px;
-  box-shadow: 0 2px 0 0 #d0dbe6;
   margin-bottom: 2px;
   overflow-x: auto;
   width: 100%; }
@@ -723,9 +722,10 @@ a:hover {
     border-bottom: 1px solid #f4f4f4; }
     .table tbody tr:last-child, #changelist table tbody tr:last-child, table#change-history tbody tr:last-child {
       border-bottom: 0; }
+    .table tbody tr th, #changelist table tbody tr th, table#change-history tbody tr th {
+      text-align: left; }
     .table tbody tr th, #changelist table tbody tr th, table#change-history tbody tr th, .table tbody tr td, #changelist table tbody tr td, table#change-history tbody tr td {
       padding: 8px;
-      text-align: left;
       font-size: 13px; }
       .table tbody tr th.action-checkbox, #changelist table tbody tr th.action-checkbox, table#change-history tbody tr th.action-checkbox, .table tbody tr td.action-checkbox, #changelist table tbody tr td.action-checkbox, table#change-history tbody tr td.action-checkbox {
         text-align: center; }
@@ -2313,6 +2313,8 @@ textarea {
 
 input[type=checkbox] {
   display: none; }
+  input[type=checkbox]#action-toggle {
+    display: none !important; }
   input[type=checkbox] + label:before {
     font-family: 'jet-icons';
     speak: none;
@@ -2987,6 +2989,7 @@ input[type=checkbox] {
 #changelist .results {
   overflow-x: auto; }
 #changelist table {
+  box-shadow: 0 2px 0 0 #d0dbe6;
   width: 100%; }
 #changelist .changelist-footer {
   margin-top: 20px;
@@ -3009,6 +3012,7 @@ input[type=checkbox] {
       display: none; }
 
 table#change-history {
+  box-shadow: 0 2px 0 0 #d0dbe6;
   width: 100%; }
 
 /*
@@ -4591,6 +4595,9 @@ table#change-history {
 .content {
   background-color: #ecf2f6; }
 
+.dashboard #content {
+  width: 500px; }
+
 .small, .module .form-row .help, .module .form-row .errorlist, .changeform .tabular.inline-related .module .errorlist, .login-form .form-row .errorlist {
   font-size: 12px; }
 
@@ -4643,10 +4650,10 @@ ul.object-tools {
       margin-top: 0;
       line-height: normal; }
 
-.context-sidebar {
+.content-sidebar {
   float: right;
-  margin-left: 20px;
-  max-width: 33%; }
+  width: 18em;
+  margin: 0 20px; }
 
 .dashboard .module table td a {
   display: inline !important; }
@@ -4992,7 +4999,8 @@ ul.object-tools {
     min-height: 100px;
     border-radius: 4px; }
     .dashboard-column-wrapper {
-      float: left; }
+      float: left;
+      min-width: 200px; }
     .dashboard-column.first {
       margin-left: 0; }
     .dashboard-column.active {
@@ -5039,6 +5047,11 @@ ul.object-tools {
       border-radius: 4px;
       box-shadow: 0 2px 0 0 #d0dbe6;
       overflow: hidden; }
+      .dashboard-item-content.contrast {
+        background: #59677e;
+        color: #fff; }
+        .dashboard-item-content.contrast .loading-indicator {
+          color: #fff; }
       .dashboard-item-content ul:not(.inline) li {
         border-bottom: 1px solid #f4f4f4;
         font-size: 13px;
@@ -5066,19 +5079,45 @@ ul.object-tools {
         .dashboard-item-content ul:not(.inline) li .warning {
           color: #c14747; }
       .dashboard-item-content ul.inline {
-        display: inline; }
+        display: inline-block; }
         .dashboard-item-content ul.inline li {
-          display: inline;
-          margin-left: 10px;
-          line-height: 30px; }
+          display: inline-block;
+          margin-left: 10px; }
           .dashboard-item-content ul.inline li:first-child {
             margin-left: 0; }
           .dashboard-item-content ul.inline li .nowrap {
             white-space: nowrap; }
+        .dashboard-item-content ul.inline.bordered li {
+          border-left: 1px solid #d0dbe6;
+          margin-left: 0;
+          padding: 0 10px; }
+          .dashboard-item-content ul.inline.bordered li:first-child {
+            border-left: 0; }
       .dashboard-item-content .padding {
         padding: 10px; }
       .dashboard-item-content .center {
         text-align: center; }
+      .dashboard-item-content .big {
+        font-size: 20px;
+        font-weight: bold; }
+      .dashboard-item-content .highlight {
+        color: #47bac1; }
+      .dashboard-item-content .dim {
+        color: #d0dbe6; }
+      .dashboard-item-content canvas .chart-fillColor {
+        color: rgba(99, 154, 245, 0.25); }
+      .dashboard-item-content canvas .chart-strokeColor {
+        color: #639af5; }
+      .dashboard-item-content canvas .chart-pointColor {
+        color: #fff; }
+      .dashboard-item-content canvas .chart-pointHighlightFill {
+        color: #639af5; }
+      .dashboard-item-content canvas .chart-scaleGridLineColor {
+        color: rgba(0, 0, 0, 0.1); }
+      .dashboard-item-content canvas .chart-scaleLineColor {
+        color: rgba(0, 0, 0, 0.1); }
+      .dashboard-item-content canvas .chart-scaleFontColor {
+        color: #fff; }
     .dashboard-item-collapse .icon-arrow-up {
       display: inline; }
     .dashboard-item-collapse .icon-arrow-down {

File diff suppressed because it is too large
+ 0 - 0
jet/static/jet/css/themes/default/base.css.map


+ 49 - 10
jet/static/jet/css/themes/green/base.css

@@ -674,7 +674,6 @@ a:hover {
   background: white;
   border-collapse: collapse;
   border-radius: 4px;
-  box-shadow: 0 2px 0 0 #cceae4;
   margin-bottom: 2px;
   overflow-x: auto;
   width: 100%; }
@@ -754,9 +753,10 @@ a:hover {
     border-bottom: 1px solid #f5f3f4; }
     .table tbody tr:last-child, #changelist table tbody tr:last-child, table#change-history tbody tr:last-child {
       border-bottom: 0; }
+    .table tbody tr th, #changelist table tbody tr th, table#change-history tbody tr th {
+      text-align: left; }
     .table tbody tr th, #changelist table tbody tr th, table#change-history tbody tr th, .table tbody tr td, #changelist table tbody tr td, table#change-history tbody tr td {
       padding: 8px;
-      text-align: left;
       font-size: 13px; }
       .table tbody tr th.action-checkbox, #changelist table tbody tr th.action-checkbox, table#change-history tbody tr th.action-checkbox, .table tbody tr td.action-checkbox, #changelist table tbody tr td.action-checkbox, table#change-history tbody tr td.action-checkbox {
         text-align: center; }
@@ -2344,6 +2344,8 @@ textarea {
 
 input[type=checkbox] {
   display: none; }
+  input[type=checkbox]#action-toggle {
+    display: none !important; }
   input[type=checkbox] + label:before {
     font-family: 'jet-icons';
     speak: none;
@@ -3018,6 +3020,7 @@ input[type=checkbox] {
 #changelist .results {
   overflow-x: auto; }
 #changelist table {
+  box-shadow: 0 2px 0 0 #cceae4;
   width: 100%; }
 #changelist .changelist-footer {
   margin-top: 20px;
@@ -3040,6 +3043,7 @@ input[type=checkbox] {
       display: none; }
 
 table#change-history {
+  box-shadow: 0 2px 0 0 #cceae4;
   width: 100%; }
 
 /*
@@ -4622,6 +4626,9 @@ table#change-history {
 .content {
   background-color: #eff6f5; }
 
+.dashboard #content {
+  width: 500px; }
+
 .small, .module .form-row .help, .module .form-row .errorlist, .changeform .tabular.inline-related .module .errorlist, .login-form .form-row .errorlist {
   font-size: 12px; }
 
@@ -4674,10 +4681,10 @@ ul.object-tools {
       margin-top: 0;
       line-height: normal; }
 
-.context-sidebar {
+.content-sidebar {
   float: right;
-  margin-left: 20px;
-  max-width: 33%; }
+  width: 18em;
+  margin: 0 20px; }
 
 .dashboard .module table td a {
   display: inline !important; }
@@ -5023,7 +5030,8 @@ ul.object-tools {
     min-height: 100px;
     border-radius: 4px; }
     .dashboard-column-wrapper {
-      float: left; }
+      float: left;
+      min-width: 200px; }
     .dashboard-column.first {
       margin-left: 0; }
     .dashboard-column.active {
@@ -5070,6 +5078,11 @@ ul.object-tools {
       border-radius: 4px;
       box-shadow: 0 2px 0 0 #cceae4;
       overflow: hidden; }
+      .dashboard-item-content.contrast {
+        background: #3c706b;
+        color: white; }
+        .dashboard-item-content.contrast .loading-indicator {
+          color: white; }
       .dashboard-item-content ul:not(.inline) li {
         border-bottom: 1px solid #f5f3f4;
         font-size: 13px;
@@ -5097,19 +5110,45 @@ ul.object-tools {
         .dashboard-item-content ul:not(.inline) li .warning {
           color: #c14747; }
       .dashboard-item-content ul.inline {
-        display: inline; }
+        display: inline-block; }
         .dashboard-item-content ul.inline li {
-          display: inline;
-          margin-left: 10px;
-          line-height: 30px; }
+          display: inline-block;
+          margin-left: 10px; }
           .dashboard-item-content ul.inline li:first-child {
             margin-left: 0; }
           .dashboard-item-content ul.inline li .nowrap {
             white-space: nowrap; }
+        .dashboard-item-content ul.inline.bordered li {
+          border-left: 1px solid #cceae4;
+          margin-left: 0;
+          padding: 0 10px; }
+          .dashboard-item-content ul.inline.bordered li:first-child {
+            border-left: 0; }
       .dashboard-item-content .padding {
         padding: 10px; }
       .dashboard-item-content .center {
         text-align: center; }
+      .dashboard-item-content .big {
+        font-size: 20px;
+        font-weight: bold; }
+      .dashboard-item-content .highlight {
+        color: #7FB1DC; }
+      .dashboard-item-content .dim {
+        color: #cceae4; }
+      .dashboard-item-content canvas .chart-fillColor {
+        color: rgba(68, 183, 139, 0.25); }
+      .dashboard-item-content canvas .chart-strokeColor {
+        color: #44b78b; }
+      .dashboard-item-content canvas .chart-pointColor {
+        color: white; }
+      .dashboard-item-content canvas .chart-pointHighlightFill {
+        color: #44b78b; }
+      .dashboard-item-content canvas .chart-scaleGridLineColor {
+        color: rgba(0, 0, 0, 0.1); }
+      .dashboard-item-content canvas .chart-scaleLineColor {
+        color: rgba(0, 0, 0, 0.1); }
+      .dashboard-item-content canvas .chart-scaleFontColor {
+        color: white; }
     .dashboard-item-collapse .icon-arrow-up {
       display: inline; }
     .dashboard-item-collapse .icon-arrow-down {

File diff suppressed because it is too large
+ 0 - 0
jet/static/jet/css/themes/green/base.css.map


+ 24 - 15
jet/static/jet/js/main.js

@@ -37,7 +37,7 @@
             var uniqueCheckboxIdPrefix = 'unique_checkbox_id_';
 
             var addLabelToCheckbox = function($checkbox) {
-                var checkboxId = uniqueCheckboxIdPrefix + uniqueCheckboxIdCounter++;
+                var checkboxId = $checkbox.attr('id') ? $checkbox.attr('id') : uniqueCheckboxIdPrefix + uniqueCheckboxIdCounter++;
                 var $label = $('<label>').attr('for', checkboxId);
 
                 $checkbox.hide().attr('id', checkboxId);
@@ -46,7 +46,7 @@
 
             var addLabelToCheckboxes = function() {
                 var $containers = $('.action-checkbox, .action-checkbox-column').add('.tabular.inline-related .form-row');
-                var $checkboxes = $containers.find('input[type="checkbox"]');
+                var $checkboxes = $containers.find('input[type="checkbox"]').add('.checkbox-without-label');
 
                 $checkboxes.each(function() {
                     addLabelToCheckbox($(this));
@@ -781,21 +781,23 @@
                 var $typeInput = $form.find('[name="type"]');
                 var type = $form.find('[name="module"] option:selected').data('type');
 
-                $typeInput.val(type);
+                if (type) {
+                    $typeInput.val(type);
 
-                $.ajax({
-                    url: $form.attr('action'),
-                    method: $form.attr('method'),
-                    dataType: 'json',
-                    data: $form.serialize(),
-                    success: function (result) {
-                        if (result.error) {
-                            return;
-                        }
+                    $.ajax({
+                        url: $form.attr('action'),
+                        method: $form.attr('method'),
+                        dataType: 'json',
+                        data: $form.serialize(),
+                        success: function (result) {
+                            if (result.error) {
+                                return;
+                            }
 
-                        document.location = result.success_url;
-                    }
-                });
+                            document.location = result.success_url;
+                        }
+                    });
+                }
 
                 e.preventDefault();
             });
@@ -817,7 +819,14 @@
                             return;
                         }
 
+                        var oldHeight = $content.height();
                         $content.html(result.html);
+                        var newHeight = $content.height();
+
+                        $content.height(oldHeight);
+                        $content.animate({
+                            height: newHeight
+                        }, 250);
                     },
                     error: function() {
                         $content.empty();

File diff suppressed because it is too large
+ 0 - 0
jet/static/jet/js/main.min.js


+ 6 - 4
jet/templates/admin/base.html

@@ -1,5 +1,5 @@
 {% load i18n admin_static jet_tags %}
-{% get_current_language_bidi as LANGUAGE_BIDI %}{% format_current_language LANGUAGE_CODE as LANGUAGE_CODE %}{% get_current_theme as THEME %}{% get_current_language as LANGUAGE_CODE %}
+{% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %}{% format_current_language LANGUAGE_CODE as LANGUAGE_CODE %}{% get_current_theme as THEME %}
 {% block html %}<!DOCTYPE html>
 <html lang="{{ LANGUAGE_CODE|default:"en-us" }}" {% if LANGUAGE_BIDI %}dir="rtl"{% endif %}>
 <head>
@@ -110,14 +110,16 @@
                     {% endif %}
                 {% endblock messages %}
 
+            <div class="content-sidebar">
+                        {% block sidebar %}{% endblock %}
+                    </div>
+
                 <div id="content" class="cf {% block coltype %}colM{% endblock %}">
                     {% block pretitle %}{% endblock %}
                     {% if is_popup %}
                         {% block content_title %}{% if title %}<h1>{{ title }}</h1>{% endif %}{% endblock %}
                     {% endif %}
-                    <div class="context-sidebar">
-                        {% block sidebar %}{% endblock %}
-                    </div>
+
                     {% block content %}
                     {% block object-tools %}{% endblock %}
                     {{ content }}

+ 101 - 26
jet/templates/admin/index.html

@@ -1,37 +1,112 @@
 {% extends "admin/base_site.html" %}
-{% load i18n admin_static jet_tags static %}
+{% load i18n admin_static log %}
 
-{% block html %}
-    {% get_dashboard 'index' as dashboard %}
-    {{ block.super }}
-{% endblock %}
+{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static "admin/css/dashboard.css" %}" />{% endblock %}
 
-{% block extrastyle %}
-    {{ block.super }}
-    <link rel="stylesheet" type="text/css" href="{% static "admin/css/dashboard.css" %}" />
+{% block coltype %}colMS{% endblock %}
 
-    {% for css in dashboard.media.css %}
-        <link href="{% static css %}" rel="stylesheet" />
-    {% endfor %}
-{% endblock %}
+{% block bodyclass %}{{ block.super }} dashboard{% endblock %}
+
+{% block content %}
+    <div class="dashboard-item">
+        <div class="dashboard-item-header">
+            <span class="dashboard-item-header-title">
+                {% trans "Applications" %}
+            </span>
+        </div>
+        <div class="dashboard-item-content">
+            {% if app_list %}
+                <ul>
+                    {% for app in app_list %}
+                        <li class="contrast">
+                            {% if app.name != app.app_label|capfirst|escape %}
+                                <a href="{{ app.app_url }}" title="{% blocktrans with name=app.name %}Models in the {{ name }} application{% endblocktrans %}">{{ app.name }}</a>
+                            {% else %}
+                                {% trans app.app_label as app_label %}
+                                <a href="{{ app.app_url }}" title="{% blocktrans with name=app_label %}Models in the {{ name }} application{% endblocktrans %}">{{ app_label }}</a>
+                            {% endif %}
+                        </li>
+
+                        {% for model in app.models %}
+                            <li>
+                                <span class="float-right">
+                                    {% if model.add_url %}
+                                        <a href="{{ model.add_url }}" class="addlink" title="{% trans 'Add' %}"><span class="icon-add3"></span></a>
+                                    {% else %}
+                                        &nbsp;
+                                    {% endif %}
 
-{% block extrahead %}
-    {{ block.super }}
-    {% for js in dashboard.media.js %}
-        <script src="{% static js %}"></script>
-    {% endfor %}
+                                    {% if model.admin_url %}
+                                        <a href="{{ model.admin_url }}" class="changelink" title="{% trans 'Change' %}"><span class="icon-edit"></span></a>
+                                    {% else %}
+                                        &nbsp;
+                                    {% endif %}
+                                </span>
+
+                                {% if model.admin_url %}
+                                    <a href="{{ model.admin_url }}">{{ model.name }}</a>
+                                {% else %}
+                                    {{ model.name }}
+                                {% endif %}
+                            </li>
+                        {% endfor %}
+                    {% endfor %}
+                </ul>
+            {% else %}
+                <p>{% trans "You don't have permission to edit anything." %}</p>
+            {% endif %}
+        </div>
+    </div>
 {% endblock %}
 
-{% block coltype %}colMS{% endblock %}
+{% block sidebar %}
+    {% get_admin_log 10 as admin_log for_user user %}
 
-{% block bodyclass %}{{ block.super }} dashboard{% endblock %}
+    <div class="dashboard-item">
+        <div class="dashboard-item-header">
+            <span class="dashboard-item-header-title">
+                {% trans 'Recent Actions' %}
+            </span>
+        </div>
+        <div class="dashboard-item-content">
+            <ul>
+                {% if not admin_log %}
+                    <li>
+                        {% trans 'None available' %}
+                    </li>
+                {% else %}
+                    {% for entry in admin_log %}
+                        <li class="nowrap">
+                            <span class="float-right">
+                                <span class="icon-user tooltip" title="{{ entry.user }}"></span>
+                                <span class="icon-clock tooltip" title="{{ entry.action_time }}"></span>
+                            </span>
 
-{% block sidebar %}{% endblock %}
+                            {% if entry.is_addition %}
+                                <span class="icon-add3"></span>
+                            {% endif %}
+                            {% if entry.is_change %}
+                                <span class="icon-edit"></span>
+                            {% endif %}
+                            {% if entry.is_deletion %}
+                                <span class="icon-cross"></span>
+                            {% endif %}
 
-{% block top-right %}
-    {{ dashboard.render_tools }}
-{% endblock %}
+                            {% if entry.content_type %}
+                                <span class="mini quiet">{% filter capfirst %}{{ entry.content_type }}{% endfilter %}</span>
+                            {% else %}
+                                <span class="mini quiet">{% trans 'Unknown content' %}</span>
+                            {% endif %}
 
-{% block content %}
-    {{ dashboard.render }}
-{% endblock %}
+                            {% if entry.is_deletion or not entry.get_admin_url %}
+                                {{ entry.object_repr }}
+                            {% else %}
+                                <a href="{{ entry.get_admin_url }}">{{ entry.object_repr }}</a>
+                            {% endif %}
+                        </li>
+                    {% endfor %}
+                {% endif %}
+            </ul>
+        </div>
+    </div>
+{% endblock %}

+ 1 - 8
jet/templatetags/jet_tags.py

@@ -8,7 +8,7 @@ from django.template import loader, Context
 from jet import settings
 from jet.models import Bookmark, PinnedApplication
 import re
-from jet.utils import get_app_list, get_model_instance_label, get_current_dashboard
+from jet.utils import get_app_list, get_model_instance_label
 
 register = template.Library()
 
@@ -218,13 +218,6 @@ def jet_add_preserved_filters(context, url, popup=False, to_field=None):
         return url
 
 
-@register.assignment_tag(takes_context=True)
-def get_dashboard(context, location):
-    dashboard_cls = get_current_dashboard(location)
-    dashboard = dashboard_cls(context)
-    return dashboard
-
-
 @register.filter()
 def if_onetoone(formset):
     return getattr(formset, 'fk') and isinstance(formset.fk, OneToOneField)

+ 2 - 2
jet/tests/dashboard.py

@@ -1,5 +1,5 @@
-from jet import modules
-from jet.dashboard import Dashboard
+from jet.dashboard import modules
+from jet.dashboard.dashboard import Dashboard
 
 
 class TestIndexDashboard(Dashboard):

+ 1 - 0
jet/tests/settings.py

@@ -9,6 +9,7 @@ DEBUG_PROPAGATE_EXCEPTIONS = True
 ROOT_URLCONF = 'jet.tests.urls'
 
 INSTALLED_APPS = (
+    'jet.dashboard',
     'jet',
     'django.contrib.auth',
     'django.contrib.contenttypes',

+ 4 - 4
jet/tests/test_dashboard.py

@@ -1,7 +1,7 @@
 from django.contrib.auth.models import User
 from django.test import TestCase, Client
-from jet.modules import LinkList, RecentActions
-from jet.models import UserDashboardModule
+from jet.dashboard.modules import LinkList, RecentActions
+from jet.dashboard.models import UserDashboardModule
 from jet.tests.dashboard import TestIndexDashboard
 
 
@@ -25,7 +25,7 @@ class DashboardTestCase(TestCase):
     def _init_dashboard(self):
         UserDashboardModule.objects.create(
             title='',
-            module='jet.modules.LinkList',
+            module='jet.dashboard.modules.LinkList',
             app_label=None,
             user=self.admin_user.pk,
             column=0,
@@ -33,7 +33,7 @@ class DashboardTestCase(TestCase):
         )
         UserDashboardModule.objects.create(
             title='',
-            module='jet.modules.RecentActions',
+            module='jet.dashboard.modules.RecentActions',
             app_label=None,
             user=self.admin_user.pk,
             column=0,

+ 15 - 14
jet/tests/test_views.py

@@ -2,8 +2,9 @@ import json
 from django.contrib.auth.models import User
 from django.core.urlresolvers import reverse
 from django.test import TestCase, Client
-from jet.modules import LinkList
-from jet.models import UserDashboardModule, Bookmark
+from jet.dashboard.modules import LinkList
+from jet.models import Bookmark
+from jet.dashboard.models import UserDashboardModule
 
 
 class ViewsTestCase(TestCase):
@@ -24,7 +25,7 @@ class ViewsTestCase(TestCase):
         new_layout = 'stacked'
         module = UserDashboardModule.objects.create(
             title=title,
-            module='jet.modules.LinkList',
+            module='jet.dashboard.modules.LinkList',
             app_label=None,
             user=self.admin_user.pk,
             column=0,
@@ -33,7 +34,7 @@ class ViewsTestCase(TestCase):
             children='[]'
         )
 
-        response = self.admin.get(reverse('jet:update_module', kwargs={'pk': module.pk}))
+        response = self.admin.get(reverse('jet-dashboard:update_module', kwargs={'pk': module.pk}))
         self.assertEqual(response.status_code, 200)
         self.assertTrue(response.context['module'] is not None)
         self.assertTrue(isinstance(response.context['module'], LinkList))
@@ -56,7 +57,7 @@ class ViewsTestCase(TestCase):
             '_save': 'Save'
         }
 
-        self.admin.post(reverse('jet:update_module', kwargs={'pk': module.pk}), post)
+        self.admin.post(reverse('jet-dashboard:update_module', kwargs={'pk': module.pk}), post)
         self.assertEqual(response.status_code, 200)
         module = UserDashboardModule.objects.get(pk=module.pk)
         settings = json.loads(module.settings)
@@ -115,7 +116,7 @@ class ViewsTestCase(TestCase):
         app_label = None
         module_0 = UserDashboardModule.objects.create(
             title='',
-            module='jet.modules.LinkList',
+            module='jet.dashboard.modules.LinkList',
             app_label=app_label,
             user=self.admin_user.pk,
             column=0,
@@ -123,13 +124,13 @@ class ViewsTestCase(TestCase):
         )
         module_1 = UserDashboardModule.objects.create(
             title='',
-            module='jet.modules.LinkList',
+            module='jet.dashboard.modules.LinkList',
             app_label=app_label,
             user=self.admin_user.pk,
             column=0,
             order=1
         )
-        response = self.admin.post(reverse('jet:update_dashboard_modules'), {
+        response = self.admin.post(reverse('jet-dashboard:update_dashboard_modules'), {
             'app_label': '',
             'modules': json.dumps([
                 {'id': module_0.pk, 'column': 0, 'order': 1},
@@ -151,7 +152,7 @@ class ViewsTestCase(TestCase):
         module_1.delete()
 
     def test_add_user_dashboard_module_view(self):
-        response = self.admin.post(reverse('jet:add_user_dashboard_module'), {
+        response = self.admin.post(reverse('jet-dashboard:add_user_dashboard_module'), {
             'app_label': '',
             'type': 'available_children',
             'module': 0
@@ -167,13 +168,13 @@ class ViewsTestCase(TestCase):
     def test_update_dashboard_module_collapse_view(self):
         module = UserDashboardModule.objects.create(
             title='',
-            module='jet.modules.LinkList',
+            module='jet.dashboard.modules.LinkList',
             app_label=None,
             user=self.admin_user.pk,
             column=0,
             order=0
         )
-        response = self.admin.post(reverse('jet:update_dashboard_module_collapse'), {
+        response = self.admin.post(reverse('jet-dashboard:update_dashboard_module_collapse'), {
             'id': module.pk, 'collapsed': True
         })
         self.assertEqual(response.status_code, 200)
@@ -182,7 +183,7 @@ class ViewsTestCase(TestCase):
         self.assertTrue(response['collapsed'])
 
         module = UserDashboardModule.objects.get(pk=module.pk)
-        response = self.admin.post(reverse('jet:update_dashboard_module_collapse'), {
+        response = self.admin.post(reverse('jet-dashboard:update_dashboard_module_collapse'), {
             'id': module.pk, 'collapsed': False
         })
         self.assertEqual(response.status_code, 200)
@@ -195,13 +196,13 @@ class ViewsTestCase(TestCase):
     def test_remove_dashboard_module_view(self):
         module = UserDashboardModule.objects.create(
             title='',
-            module='jet.modules.LinkList',
+            module='jet.dashboard.modules.LinkList',
             app_label=None,
             user=self.admin_user.pk,
             column=0,
             order=0
         )
-        response = self.admin.post(reverse('jet:remove_dashboard_module'), {'id': module.pk})
+        response = self.admin.post(reverse('jet-dashboard:remove_dashboard_module'), {'id': module.pk})
         self.assertEqual(response.status_code, 200)
         response = json.loads(response.content.decode())
         self.assertFalse(response['error'])

+ 1 - 0
jet/tests/urls.py

@@ -6,6 +6,7 @@ admin.autodiscover()
 urlpatterns = patterns(
     '',
     url(r'^jet/', include('jet.urls', 'jet')),
+    url(r'^jet/dashboard/', include('jet.dashboard.urls', 'jet-dashboard')),
     url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
     url(r'^admin/', include(admin.site.urls)),
 )

+ 1 - 33
jet/urls.py

@@ -1,16 +1,9 @@
 from django.conf.urls import patterns, url
 from django.views.i18n import javascript_catalog
-from jet.views import add_bookmark_view, remove_bookmark_view, toggle_application_pin_view, \
-    update_dashboard_modules_view, add_user_dashboard_module_view, update_dashboard_module_collapse_view, \
-    remove_dashboard_module_view, UpdateDashboardModuleView, load_dashboard_module_view, model_lookup_view
+from jet.views import add_bookmark_view, remove_bookmark_view, toggle_application_pin_view, model_lookup_view
 
 urlpatterns = patterns(
     '',
-    url(
-        r'^module/(?P<pk>\d+)/$',
-        UpdateDashboardModuleView.as_view(),
-        name='update_module'
-    ),
     url(
         r'^add_bookmark/$',
         add_bookmark_view,
@@ -26,31 +19,6 @@ urlpatterns = patterns(
         toggle_application_pin_view,
         name='toggle_application_pin'
     ),
-    url(
-        r'^update_dashboard_modules/$',
-        update_dashboard_modules_view,
-        name='update_dashboard_modules'
-    ),
-    url(
-        r'^add_user_dashboard_module/$',
-        add_user_dashboard_module_view,
-        name='add_user_dashboard_module'
-    ),
-    url(
-        r'^update_dashboard_module_collapse/$',
-        update_dashboard_module_collapse_view,
-        name='update_dashboard_module_collapse'
-    ),
-    url(
-        r'^remove_dashboard_module/$',
-        remove_dashboard_module_view,
-        name='remove_dashboard_module'
-    ),
-    url(
-        r'^load_dashboard_module/(?P<pk>\d+)/$',
-        load_dashboard_module_view,
-        name='load_dashboard_module'
-    ),
     url(
         r'^model_lookup/$',
         model_lookup_view,

+ 8 - 20
jet/utils.py

@@ -1,3 +1,4 @@
+import datetime
 from importlib import import_module
 import json
 from django.core.serializers.json import DjangoJSONEncoder
@@ -5,11 +6,11 @@ from django.http import HttpResponse
 from django.core.urlresolvers import reverse, resolve
 from django.contrib import admin
 from django.contrib.admin import AdminSite
-from django.utils.encoding import force_text
 from django.utils.encoding import smart_text
-from django.utils.functional import Promise
 from jet import settings
 from django.contrib import messages
+from django.utils.encoding import force_text
+from django.utils.functional import Promise
 
 
 class JsonResponse(HttpResponse):
@@ -58,26 +59,13 @@ def get_admin_site_name(context):
     return get_admin_site(context).name
 
 
-def get_current_dashboard(location):
-    if location == 'index':
-        path = settings.JET_INDEX_DASHBOARD
-    elif location == 'app_index':
-        path = settings.JET_APP_INDEX_DASHBOARD
-    else:
-        raise ValueError('Unknown dashboard location: %s' % location)
-
-    module, cls = path.rsplit('.', 1)
-    module = import_module(module)
-    index_dashboard_cls = getattr(module, cls)
-
-    return index_dashboard_cls
-
-
-class LazyEncoder(json.JSONEncoder):
+class LazyDateTimeEncoder(json.JSONEncoder):
     def default(self, obj):
-        if isinstance(obj, Promise):
+        if isinstance(obj, datetime.datetime) or isinstance(obj, datetime.date):
+            return obj.isoformat()
+        elif isinstance(obj, Promise):
             return force_text(obj)
-        return obj
+        return json.JSONEncoder.default(self, obj)
 
 
 def get_model_instance_label(instance):

+ 3 - 209
jet/views.py

@@ -1,128 +1,7 @@
-from django.contrib import messages
-from django.core.urlresolvers import reverse
-from django.forms.formsets import formset_factory
 from django.views.decorators.http import require_POST, require_GET
-from jet.forms import AddBookmarkForm, RemoveBookmarkForm, ToggleApplicationPinForm, UpdateDashboardModulesForm, \
-    AddUserDashboardModuleForm, UpdateDashboardModuleCollapseForm, RemoveDashboardModuleForm, ModelLookupForm
-from jet.models import Bookmark, UserDashboardModule
-from jet.utils import JsonResponse, get_app_list, SuccessMessageMixin
-from django.views.generic import UpdateView
-from django.utils.translation import ugettext_lazy as _
-
-
-class UpdateDashboardModuleView(SuccessMessageMixin, UpdateView):
-    model = UserDashboardModule
-    fields = ('title',)
-    template_name = 'jet/dashboard/update_module.html'
-    success_message = _('Widget was successfully updated')
-    object = None
-    module = None
-
-    def get_success_url(self):
-        if self.object.app_label:
-            return reverse('admin:app_list', kwargs={'app_label': self.object.app_label})
-        else:
-            return reverse('admin:index')
-
-    def get_module(self):
-        object = self.object if getattr(self, 'object', None) is not None else self.get_object()
-        return object.load_module()
-
-    def get_settings_form_kwargs(self):
-        kwargs = {
-            'initial': self.module.settings
-        }
-
-        if self.request.method in ('POST', 'PUT'):
-            kwargs.update({
-                'data': self.request.POST,
-                'files': self.request.FILES,
-            })
-        return kwargs
-
-    def get_settings_form(self):
-        if self.module.settings_form:
-            return self.module.settings_form(**self.get_settings_form_kwargs())
-
-    def get_children_formset_kwargs(self):
-        kwargs = {
-            'initial': self.module.children,
-            'prefix': 'children',
-        }
-
-        if self.request.method in ('POST', 'PUT'):
-            kwargs.update({
-                'data': self.request.POST,
-                'files': self.request.FILES,
-            })
-        return kwargs
-
-    def get_children_formset(self):
-        if self.module.child_form:
-            return formset_factory(self.module.child_form, can_delete=True, extra=1)(**self.get_children_formset_kwargs())
-
-    def clean_children_data(self, children):
-        children = list(filter(
-            lambda item: isinstance(item, dict) and item and item.get('DELETE') is not True,
-            children
-        ))
-        for item in children:
-            item.pop('DELETE')
-        return children
-
-    def get_current_app(self):
-        app_list = get_app_list({'request': self.request})
-
-        for app in app_list:
-            if app.get('app_label', app.get('name')) == self.object.app_label:
-                return app
-
-    def get_context_data(self, **kwargs):
-        data = super(UpdateDashboardModuleView, self).get_context_data(**kwargs)
-        data['title'] = _('Change')
-        data['module'] = self.module
-        data['settings_form'] = self.get_settings_form()
-        data['children_formset'] = self.get_children_formset()
-        data['child_name'] = self.module.child_name if self.module.child_name else _('Items')
-        data['child_name_plural'] = self.module.child_name_plural if self.module.child_name_plural else _('Items')
-        data['app'] = self.get_current_app()
-        return data
-
-    def dispatch(self, request, *args, **kwargs):
-        self.object = self.get_object()
-        self.module = self.get_module()(model=self.object)
-        return super(UpdateDashboardModuleView, self).dispatch(request, *args, **kwargs)
-
-    def post(self, request, *args, **kwargs):
-        settings_form = self.get_settings_form()
-        children_formset = self.get_children_formset()
-
-        data = request.POST.copy()
-
-        if settings_form:
-            if settings_form.is_valid():
-                settings = settings_form.cleaned_data
-                data['settings'] = self.module.dump_settings(settings)
-            else:
-                return self.form_invalid(self.get_form(self.get_form_class()))
-
-        if children_formset:
-            if children_formset.is_valid():
-                self.module.children = self.clean_children_data(children_formset.cleaned_data)
-                data['children'] = self.module.dump_children()
-            else:
-                return self.form_invalid(self.get_form(self.get_form_class()))
-
-        request.POST = data
-
-        return super(UpdateDashboardModuleView, self).post(request, *args, **kwargs)
-
-    def form_valid(self, form):
-        if 'settings' in form.data:
-            form.instance.settings = form.data['settings']
-        if 'children' in form.data:
-            form.instance.children = form.data['children']
-        return super(UpdateDashboardModuleView, self).form_valid(form)
+from jet.forms import AddBookmarkForm, RemoveBookmarkForm, ToggleApplicationPinForm, ModelLookupForm
+from jet.models import Bookmark
+from jet.utils import JsonResponse
 
 
 @require_POST
@@ -171,91 +50,6 @@ def toggle_application_pin_view(request):
     return JsonResponse(result)
 
 
-@require_POST
-def update_dashboard_modules_view(request):
-    result = {'error': False}
-    form = UpdateDashboardModulesForm(request, request.POST)
-
-    if form.is_valid():
-        form.save()
-    else:
-        result['error'] = True
-
-    return JsonResponse(result)
-
-
-@require_POST
-def add_user_dashboard_module_view(request):
-    result = {'error': False}
-    form = AddUserDashboardModuleForm(request, request.POST)
-
-    if form.is_valid():
-        module = form.save()
-        result['id'] = module.pk
-        messages.success(request, _('Widget has been successfully added'))
-
-        if module.app_label:
-            result['success_url'] = reverse('admin:app_list', kwargs={'app_label': module.app_label})
-        else:
-            result['success_url'] = reverse('admin:index')
-    else:
-        result['error'] = True
-
-    return JsonResponse(result)
-
-
-@require_POST
-def update_dashboard_module_collapse_view(request):
-    result = {'error': False}
-
-    try:
-        instance = UserDashboardModule.objects.get(pk=request.POST.get('id'))
-        form = UpdateDashboardModuleCollapseForm(request, request.POST, instance=instance)
-
-        if form.is_valid():
-            module = form.save()
-            result['collapsed'] = module.collapsed
-        else:
-            result['error'] = True
-    except UserDashboardModule.DoesNotExist:
-        result['error'] = True
-
-    return JsonResponse(result)
-
-
-@require_POST
-def remove_dashboard_module_view(request):
-    result = {'error': False}
-
-    try:
-        instance = UserDashboardModule.objects.get(pk=request.POST.get('id'))
-        form = RemoveDashboardModuleForm(request, request.POST, instance=instance)
-
-        if form.is_valid():
-            form.save()
-        else:
-            result['error'] = True
-    except UserDashboardModule.DoesNotExist:
-        result['error'] = True
-
-    return JsonResponse(result)
-
-
-@require_GET
-def load_dashboard_module_view(request, pk):
-    result = {'error': False}
-
-    try:
-        instance = UserDashboardModule.objects.get(pk=pk)
-        module_cls = instance.load_module()
-        module = module_cls(model=instance, context={'request': request})
-        result['html'] = module.render()
-    except UserDashboardModule.DoesNotExist:
-        result['error'] = True
-
-    return JsonResponse(result)
-
-
 @require_GET
 def model_lookup_view(request):
     result = {'error': False}

Some files were not shown because too many files changed in this diff