Skip to content

Commit 1721ba9

Browse files
committed
Adds dash/panel app templates, mgmt commands, template loader.
Implements blueprint scaffolding. Using custom management commands you can now create the majority of the boilerplate code for a new dashboard or panel from a set of basic templates with a single command. See the docs for more info. Additionally, in support of the new commands (and inherent codified directory structure) there's a new template loader included which can load templates from "templates" directories in any registered panel. Change-Id: I1df5eb152cb18694dc89d562799c8d3e8950ca6f
1 parent 8396026 commit 1721ba9

File tree

27 files changed

+385
-10
lines changed

27 files changed

+385
-10
lines changed

docs/source/ref/run_tests.rst

+42
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,48 @@ tests by using the ``--skip-selenium`` flag::
4343
This isn't recommended, but can be a timesaver when you only need to run
4444
the code tests and not the frontend tests during development.
4545

46+
Using Dashboard and Panel Templates
47+
===================================
48+
49+
Horizon has a set of convenient management commands for creating new
50+
dashboards and panels based on basic templates.
51+
52+
Dashboards
53+
----------
54+
55+
To create a new dashboard, run the following:
56+
57+
./run_tests.sh -m startdash <dash_name>
58+
59+
This will create a directory with the given dashboard name, a ``dashboard.py``
60+
module with the basic dashboard code filled in, and various other common
61+
"boilerplate" code.
62+
63+
Available options:
64+
65+
* --target: the directory in which the dashboard files should be created.
66+
Default: A new directory within the current directory.
67+
68+
Panels
69+
------
70+
71+
To create a new panel, run the following:
72+
73+
./run_tests -m startpanel <panel_name> --dashboard=<dashboard_path>
74+
75+
This will create a directory with the given panel name, and ``panel.py``
76+
module with the basic panel code filled in, and various other common
77+
"boilerplate" code.
78+
79+
Available options:
80+
81+
* -d, --dashboard: The dotted python path to your dashboard app (the module
82+
which containers the ``dashboard.py`` file.).
83+
* --target: the directory in which the panel files should be created.
84+
If the value is ``auto`` the panel will be created as a new directory inside
85+
the dashboard module's directory structure. Default: A new directory within
86+
the current directory.
87+
4688
Give me metrics!
4789
================
4890

docs/source/topics/tutorial.rst

+31-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@ Creating a dashboard
3131
incorporate it into an existing dashboard. See the section
3232
:ref:`overrides <overrides>` later in this document.
3333

34+
The quick version
35+
-----------------
36+
37+
Horizon provides a custom management command to create a typical base
38+
dashboard structure for you. The following command generates most of the
39+
boilerplate code explained below::
40+
41+
./run_tests.sh -m startdash visualizations
42+
43+
It's still recommended that you read the rest of this section to understand
44+
what that command creates and why.
3445

3546
Structure
3647
---------
@@ -116,13 +127,32 @@ but it could also go elsewhere, such as in an override file (see below).
116127
Creating a panel
117128
================
118129

119-
Now that we have our dashboard written, we can also create our panel.
130+
Now that we have our dashboard written, we can also create our panel. We'll
131+
call it "flocking".
120132

121133
.. note::
122134

123135
You don't need to write a custom dashboard to add a panel. The structure
124136
here is for the sake of completeness in the tutorial.
125137

138+
The quick version
139+
-----------------
140+
141+
Horizon provides a custom management command to create a typical base
142+
panel structure for you. The following command generates most of the
143+
boilerplate code explained below::
144+
145+
./run_tests.sh -m startpanel flocking --dashboard=visualizations --target=auto
146+
147+
The ``dashboard`` argument is required, and tells the command which dashboard
148+
this panel will be registered with. The ``target`` argument is optional, and
149+
respects ``auto`` as a special value which means that the files for the panel
150+
should be created inside the dashboard module as opposed to the current
151+
directory (the default).
152+
153+
It's still recommended that you read the rest of this section to understand
154+
what that command creates and why.
155+
126156
Structure
127157
---------
128158

horizon/base.py

+18-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import copy
2727
import inspect
2828
import logging
29+
import os
2930

3031
from django.conf import settings
3132
from django.conf.urls.defaults import patterns, url, include
@@ -37,6 +38,7 @@
3738
from django.utils.module_loading import module_has_submodule
3839
from django.utils.translation import ugettext as _
3940

41+
from horizon import loaders
4042
from horizon.decorators import (require_auth, require_roles,
4143
require_services, _current_component)
4244

@@ -541,12 +543,26 @@ def _autodiscover(self):
541543
@classmethod
542544
def register(cls, panel):
543545
""" Registers a :class:`~horizon.Panel` with this dashboard. """
544-
return Horizon.register_panel(cls, panel)
546+
panel_class = Horizon.register_panel(cls, panel)
547+
# Support template loading from panel template directories.
548+
panel_mod = import_module(panel.__module__)
549+
panel_dir = os.path.dirname(panel_mod.__file__)
550+
template_dir = os.path.join(panel_dir, "templates")
551+
if os.path.exists(template_dir):
552+
key = os.path.join(cls.slug, panel.slug)
553+
loaders.panel_template_dirs[key] = template_dir
554+
return panel_class
545555

546556
@classmethod
547557
def unregister(cls, panel):
548558
""" Unregisters a :class:`~horizon.Panel` from this dashboard. """
549-
return Horizon.unregister_panel(cls, panel)
559+
success = Horizon.unregister_panel(cls, panel)
560+
if success:
561+
# Remove the panel's template directory.
562+
key = os.path.join(cls.slug, panel.slug)
563+
if key in loaders.panel_template_dirs:
564+
del loaders.panel_template_dirs[key]
565+
return success
550566

551567

552568
class Workflow(object):

horizon/conf/__init__.py

Whitespace-only changes.

horizon/conf/dash_template/__init__.py

Whitespace-only changes.
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from django.utils.translation import ugettext_lazy as _
2+
3+
import horizon
4+
5+
6+
class {{ dash_name|title }}(horizon.Dashboard):
7+
name = _("{{ dash_name|title }}")
8+
slug = "{{ dash_name|slugify }}"
9+
panels = () # Add your panels here.
10+
default_panel = '' # Specify the slug of the dashboard's default panel.
11+
12+
13+
horizon.register({{ dash_name|title }})

horizon/conf/dash_template/models.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""
2+
Stub file to work around django bug: https://code.djangoproject.com/ticket/7198
3+
"""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/* Additional CSS for {{ dash_name }}. */
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/* Additional JavaScript for {{ dash_name }}. */
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{% load horizon %}{% jstemplate %}[% extends 'base.html' %]
2+
3+
[% block sidebar %]
4+
[% include 'horizon/common/_sidebar.html' %]
5+
[% endblock %]
6+
7+
[% block main %]
8+
[% include "horizon/_messages.html" %]
9+
[% block {{ dash_name }}_main %][% endblock %]
10+
[% endblock %]
11+
{% endjstemplate %}

horizon/conf/panel_template/__init__.py

Whitespace-only changes.

horizon/conf/panel_template/models.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""
2+
Stub file to work around django bug: https://code.djangoproject.com/ticket/7198
3+
"""

horizon/conf/panel_template/panel.py

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from django.utils.translation import ugettext_lazy as _
2+
3+
import horizon
4+
5+
from {{ dash_path }} import dashboard
6+
7+
8+
class {{ panel_name|title }}(horizon.Panel):
9+
name = _("{{ panel_name|title }}")
10+
slug = "{{ panel_name|slugify }}"
11+
12+
13+
dashboard.register({{ panel_name|title }})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{% load horizon %}{% jstemplate %}[% extends '{{ dash_name }}/base.html' %]
2+
[% load i18n %]
3+
[% block title %][% trans "{{ panel_name|title }}" %][% endblock %]
4+
5+
[% block page_header %]
6+
[% include "horizon/common/_page_header.html" with title=_("{{ panel_name|title }}") %]
7+
[% endblock page_header %]
8+
9+
[% block {{ dash_name }}_main %]
10+
[% endblock %]
11+
12+
{% endjstemplate %}

horizon/conf/panel_template/tests.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from horizon import test
2+
3+
4+
class {{ panel_name|title}}Tests(test.TestCase):
5+
# Unit tests for {{ panel_name }}.
6+
def test_me(self):
7+
self.assertTrue(1 + 1 == 2)

horizon/conf/panel_template/urls.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from django.conf.urls.defaults import patterns, url
2+
3+
from .views import IndexView
4+
5+
urlpatterns = patterns('',
6+
url(r'^$', IndexView.as_view(), name='index'),
7+
)

horizon/conf/panel_template/views.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from horizon import views
2+
3+
4+
class IndexView(views.APIView):
5+
# A very simple class-based view...
6+
template_name = '{{ panel_name }}/index.html'
7+
8+
def get_data(self, request, context, *args, **kwargs):
9+
# Add data to the context here...
10+
return context

horizon/loaders.py

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""
2+
Wrapper for loading templates from "templates" directories in panel modules.
3+
"""
4+
5+
import os
6+
7+
from django.conf import settings
8+
from django.template.base import TemplateDoesNotExist
9+
from django.template.loader import BaseLoader
10+
from django.utils._os import safe_join
11+
12+
# Set up a cache of the panel directories to search.
13+
panel_template_dirs = {}
14+
15+
16+
class TemplateLoader(BaseLoader):
17+
is_usable = True
18+
19+
def get_template_sources(self, template_name):
20+
dash_name, panel_name, remainder = template_name.split(os.path.sep, 2)
21+
key = os.path.join(dash_name, panel_name)
22+
if key in panel_template_dirs:
23+
template_dir = panel_template_dirs[key]
24+
try:
25+
yield safe_join(template_dir, panel_name, remainder)
26+
except UnicodeDecodeError:
27+
# The template dir name wasn't valid UTF-8.
28+
raise
29+
except ValueError:
30+
# The joined path was located outside of template_dir.
31+
pass
32+
33+
def load_template_source(self, template_name, template_dirs=None):
34+
for path in self.get_template_sources(template_name):
35+
try:
36+
file = open(path)
37+
try:
38+
return (file.read().decode(settings.FILE_CHARSET), path)
39+
finally:
40+
file.close()
41+
except IOError:
42+
pass
43+
raise TemplateDoesNotExist(template_name)
44+
45+
46+
_loader = TemplateLoader()

horizon/management/__init__.py

Whitespace-only changes.

horizon/management/commands/__init__.py

Whitespace-only changes.
+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from optparse import make_option
2+
import os
3+
4+
from django.core.management.base import CommandError
5+
from django.core.management.templates import TemplateCommand
6+
from django.utils.importlib import import_module
7+
8+
import horizon
9+
10+
11+
class Command(TemplateCommand):
12+
template = os.path.join(horizon.__path__[0], "conf", "dash_template")
13+
option_list = TemplateCommand.option_list + (
14+
make_option('--target',
15+
dest='target',
16+
action='store',
17+
default=None,
18+
help='The directory in which the panel '
19+
'should be created. Defaults to the '
20+
'current directory. The value "auto" '
21+
'may also be used to automatically '
22+
'create the panel inside the specified '
23+
'dashboard module.'),)
24+
help = ("Creates a Django app directory structure for a new dashboard "
25+
"with the given name in the current directory or optionally in "
26+
"the given directory.")
27+
28+
def handle(self, dash_name=None, **options):
29+
if dash_name is None:
30+
raise CommandError("You must provide a dashboard name.")
31+
32+
# Use our default template if one isn't specified.
33+
if not options.get("template", None):
34+
options["template"] = self.template
35+
36+
# We have html templates as well, so make sure those are included.
37+
options["extensions"].extend(["html", "js", "css"])
38+
39+
# Check that the app_name cannot be imported.
40+
try:
41+
import_module(dash_name)
42+
except ImportError:
43+
pass
44+
else:
45+
raise CommandError("%r conflicts with the name of an existing "
46+
"Python module and cannot be used as an app "
47+
"name. Please try another name." % dash_name)
48+
49+
super(Command, self).handle('dash', dash_name, **options)

0 commit comments

Comments
 (0)