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
This commit is contained in:
Gabriel Hurley 2012-04-26 19:22:51 -07:00
parent 8396026722
commit 1721ba9c4a
27 changed files with 385 additions and 10 deletions

View File

@ -43,6 +43,48 @@ tests by using the ``--skip-selenium`` flag::
This isn't recommended, but can be a timesaver when you only need to run
the code tests and not the frontend tests during development.
Using Dashboard and Panel Templates
===================================
Horizon has a set of convenient management commands for creating new
dashboards and panels based on basic templates.
Dashboards
----------
To create a new dashboard, run the following:
./run_tests.sh -m startdash <dash_name>
This will create a directory with the given dashboard name, a ``dashboard.py``
module with the basic dashboard code filled in, and various other common
"boilerplate" code.
Available options:
* --target: the directory in which the dashboard files should be created.
Default: A new directory within the current directory.
Panels
------
To create a new panel, run the following:
./run_tests -m startpanel <panel_name> --dashboard=<dashboard_path>
This will create a directory with the given panel name, and ``panel.py``
module with the basic panel code filled in, and various other common
"boilerplate" code.
Available options:
* -d, --dashboard: The dotted python path to your dashboard app (the module
which containers the ``dashboard.py`` file.).
* --target: the directory in which the panel files should be created.
If the value is ``auto`` the panel will be created as a new directory inside
the dashboard module's directory structure. Default: A new directory within
the current directory.
Give me metrics!
================

View File

@ -31,6 +31,17 @@ Creating a dashboard
incorporate it into an existing dashboard. See the section
:ref:`overrides <overrides>` later in this document.
The quick version
-----------------
Horizon provides a custom management command to create a typical base
dashboard structure for you. The following command generates most of the
boilerplate code explained below::
./run_tests.sh -m startdash visualizations
It's still recommended that you read the rest of this section to understand
what that command creates and why.
Structure
---------
@ -116,13 +127,32 @@ but it could also go elsewhere, such as in an override file (see below).
Creating a panel
================
Now that we have our dashboard written, we can also create our panel.
Now that we have our dashboard written, we can also create our panel. We'll
call it "flocking".
.. note::
You don't need to write a custom dashboard to add a panel. The structure
here is for the sake of completeness in the tutorial.
The quick version
-----------------
Horizon provides a custom management command to create a typical base
panel structure for you. The following command generates most of the
boilerplate code explained below::
./run_tests.sh -m startpanel flocking --dashboard=visualizations --target=auto
The ``dashboard`` argument is required, and tells the command which dashboard
this panel will be registered with. The ``target`` argument is optional, and
respects ``auto`` as a special value which means that the files for the panel
should be created inside the dashboard module as opposed to the current
directory (the default).
It's still recommended that you read the rest of this section to understand
what that command creates and why.
Structure
---------

View File

@ -26,6 +26,7 @@ import collections
import copy
import inspect
import logging
import os
from django.conf import settings
from django.conf.urls.defaults import patterns, url, include
@ -37,6 +38,7 @@ from django.utils.importlib import import_module
from django.utils.module_loading import module_has_submodule
from django.utils.translation import ugettext as _
from horizon import loaders
from horizon.decorators import (require_auth, require_roles,
require_services, _current_component)
@ -541,12 +543,26 @@ class Dashboard(Registry, HorizonComponent):
@classmethod
def register(cls, panel):
""" Registers a :class:`~horizon.Panel` with this dashboard. """
return Horizon.register_panel(cls, panel)
panel_class = Horizon.register_panel(cls, panel)
# Support template loading from panel template directories.
panel_mod = import_module(panel.__module__)
panel_dir = os.path.dirname(panel_mod.__file__)
template_dir = os.path.join(panel_dir, "templates")
if os.path.exists(template_dir):
key = os.path.join(cls.slug, panel.slug)
loaders.panel_template_dirs[key] = template_dir
return panel_class
@classmethod
def unregister(cls, panel):
""" Unregisters a :class:`~horizon.Panel` from this dashboard. """
return Horizon.unregister_panel(cls, panel)
success = Horizon.unregister_panel(cls, panel)
if success:
# Remove the panel's template directory.
key = os.path.join(cls.slug, panel.slug)
if key in loaders.panel_template_dirs:
del loaders.panel_template_dirs[key]
return success
class Workflow(object):

0
horizon/conf/__init__.py Normal file
View File

View File

View File

@ -0,0 +1,13 @@
from django.utils.translation import ugettext_lazy as _
import horizon
class {{ dash_name|title }}(horizon.Dashboard):
name = _("{{ dash_name|title }}")
slug = "{{ dash_name|slugify }}"
panels = () # Add your panels here.
default_panel = '' # Specify the slug of the dashboard's default panel.
horizon.register({{ dash_name|title }})

View File

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

View File

@ -0,0 +1 @@
/* Additional CSS for {{ dash_name }}. */

View File

@ -0,0 +1 @@
/* Additional JavaScript for {{ dash_name }}. */

View File

@ -0,0 +1,11 @@
{% load horizon %}{% jstemplate %}[% extends 'base.html' %]
[% block sidebar %]
[% include 'horizon/common/_sidebar.html' %]
[% endblock %]
[% block main %]
[% include "horizon/_messages.html" %]
[% block {{ dash_name }}_main %][% endblock %]
[% endblock %]
{% endjstemplate %}

View File

View File

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

View File

@ -0,0 +1,13 @@
from django.utils.translation import ugettext_lazy as _
import horizon
from {{ dash_path }} import dashboard
class {{ panel_name|title }}(horizon.Panel):
name = _("{{ panel_name|title }}")
slug = "{{ panel_name|slugify }}"
dashboard.register({{ panel_name|title }})

View File

@ -0,0 +1,12 @@
{% load horizon %}{% jstemplate %}[% extends '{{ dash_name }}/base.html' %]
[% load i18n %]
[% block title %][% trans "{{ panel_name|title }}" %][% endblock %]
[% block page_header %]
[% include "horizon/common/_page_header.html" with title=_("{{ panel_name|title }}") %]
[% endblock page_header %]
[% block {{ dash_name }}_main %]
[% endblock %]
{% endjstemplate %}

View File

@ -0,0 +1,7 @@
from horizon import test
class {{ panel_name|title}}Tests(test.TestCase):
# Unit tests for {{ panel_name }}.
def test_me(self):
self.assertTrue(1 + 1 == 2)

View File

@ -0,0 +1,7 @@
from django.conf.urls.defaults import patterns, url
from .views import IndexView
urlpatterns = patterns('',
url(r'^$', IndexView.as_view(), name='index'),
)

View File

@ -0,0 +1,10 @@
from horizon import views
class IndexView(views.APIView):
# A very simple class-based view...
template_name = '{{ panel_name }}/index.html'
def get_data(self, request, context, *args, **kwargs):
# Add data to the context here...
return context

46
horizon/loaders.py Normal file
View File

@ -0,0 +1,46 @@
"""
Wrapper for loading templates from "templates" directories in panel modules.
"""
import os
from django.conf import settings
from django.template.base import TemplateDoesNotExist
from django.template.loader import BaseLoader
from django.utils._os import safe_join
# Set up a cache of the panel directories to search.
panel_template_dirs = {}
class TemplateLoader(BaseLoader):
is_usable = True
def get_template_sources(self, template_name):
dash_name, panel_name, remainder = template_name.split(os.path.sep, 2)
key = os.path.join(dash_name, panel_name)
if key in panel_template_dirs:
template_dir = panel_template_dirs[key]
try:
yield safe_join(template_dir, panel_name, remainder)
except UnicodeDecodeError:
# The template dir name wasn't valid UTF-8.
raise
except ValueError:
# The joined path was located outside of template_dir.
pass
def load_template_source(self, template_name, template_dirs=None):
for path in self.get_template_sources(template_name):
try:
file = open(path)
try:
return (file.read().decode(settings.FILE_CHARSET), path)
finally:
file.close()
except IOError:
pass
raise TemplateDoesNotExist(template_name)
_loader = TemplateLoader()

View File

View File

View File

@ -0,0 +1,49 @@
from optparse import make_option
import os
from django.core.management.base import CommandError
from django.core.management.templates import TemplateCommand
from django.utils.importlib import import_module
import horizon
class Command(TemplateCommand):
template = os.path.join(horizon.__path__[0], "conf", "dash_template")
option_list = TemplateCommand.option_list + (
make_option('--target',
dest='target',
action='store',
default=None,
help='The directory in which the panel '
'should be created. Defaults to the '
'current directory. The value "auto" '
'may also be used to automatically '
'create the panel inside the specified '
'dashboard module.'),)
help = ("Creates a Django app directory structure for a new dashboard "
"with the given name in the current directory or optionally in "
"the given directory.")
def handle(self, dash_name=None, **options):
if dash_name is None:
raise CommandError("You must provide a dashboard name.")
# Use our default template if one isn't specified.
if not options.get("template", None):
options["template"] = self.template
# We have html templates as well, so make sure those are included.
options["extensions"].extend(["html", "js", "css"])
# Check that the app_name cannot be imported.
try:
import_module(dash_name)
except ImportError:
pass
else:
raise CommandError("%r conflicts with the name of an existing "
"Python module and cannot be used as an app "
"name. Please try another name." % dash_name)
super(Command, self).handle('dash', dash_name, **options)

View File

@ -0,0 +1,89 @@
from optparse import make_option
import os
from django.core.management.base import CommandError
from django.core.management.templates import TemplateCommand
from django.utils.importlib import import_module
import horizon
class Command(TemplateCommand):
args = "[name] [dashboard name] [optional destination directory]"
option_list = TemplateCommand.option_list + (
make_option('--dashboard', '-d',
dest='dashboard',
action='store',
default=None,
help='The dotted python path to the '
'dashboard which this panel will be '
'registered with, e.g. '
'"horizon.dashboards.syspanel".'),
make_option('--target',
dest='target',
action='store',
default=None,
help='The directory in which the panel '
'should be created. Defaults to the '
'current directory. The value "auto" '
'may also be used to automatically '
'create the panel inside the specified '
'dashboard module.'),)
template = os.path.join(horizon.__path__[0], "conf", "panel_template")
help = ("Creates a Django app directory structure for a new panel "
"with the given name in the current directory or optionally in "
"the given directory.")
def handle(self, panel_name=None, **options):
if panel_name is None:
raise CommandError("You must provide a panel name.")
if options.get('dashboard') is None:
raise CommandError("You must specify the name of the dashboard "
"this panel will be registered with using the "
"-d or --dashboard option.")
dashboard_path = options.get('dashboard')
dashboard_mod_path = ".".join([dashboard_path, "dashboard"])
# Check the the dashboard.py file in the dashboard app can be imported.
# Add the dashboard information to our options to pass along if all
# goes well.
try:
dashboard_mod = import_module(dashboard_mod_path)
options["dash_path"] = dashboard_path
options["dash_name"] = dashboard_path.split(".")[-1]
except ImportError:
raise CommandError("A dashboard.py module could not be imported "
" from the dashboard at %r."
% options.get("dashboard"))
target = options.pop("target", None)
if target == "auto":
target = os.path.join(os.path.dirname(dashboard_mod.__file__),
panel_name)
if not os.path.exists(target):
try:
os.mkdir(target)
except OSError, exc:
raise CommandError("Unable to create panel directory: %s"
% exc)
# Use our default template if one isn't specified.
if not options.get("template", None):
options["template"] = self.template
# We have html templates as well, so make sure those are included.
options["extensions"].extend(["html"])
# Check that the app_name cannot be imported.
try:
import_module(panel_name)
except ImportError:
pass
else:
raise CommandError("%r conflicts with the name of an existing "
"Python module and cannot be used as an app "
"name. Please try another name." % panel_name)
super(Command, self).handle('panel', panel_name, target, **options)

View File

@ -132,13 +132,16 @@ class JSTemplateNode(template.Node):
def render(self, context, ):
output = self.nodelist.render(context)
return output.replace('[[', '{{').replace(']]', '}}')
output = output.replace('[[', '{{').replace(']]', '}}')
output = output.replace('[%', '{%').replace('%]', '%}')
return output
@register.tag
def jstemplate(parser, token):
"""
Replaces ``[[`` and ``]]`` with ``{{`` and ``}}`` to avoid conflicts
Replaces ``[[`` and ``]]`` with ``{{`` and ``}}`` and
``[%`` and ``%]`` with ``{%`` and ``%}`` to avoid conflicts
with Django's template engine when using any of the Mustache-based
templating libraries.
"""

View File

@ -72,6 +72,7 @@ SITE_NAME = 'openstack'
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
NOSE_ARGS = ['--nocapture',
'--nologcapture',
'--exclude-dir=horizon/conf/',
'--cover-package=horizon',
'--cover-inclusive']

View File

@ -78,7 +78,8 @@ TEMPLATE_CONTEXT_PROCESSORS = (
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader'
'django.template.loaders.app_directories.Loader',
'horizon.loaders.TemplateLoader'
)
TEMPLATE_DIRS = (

View File

@ -6,7 +6,7 @@ set -o errexit
# Increment me any time the environment should be rebuilt.
# This includes dependncy changes, directory renames, etc.
# Simple integer secuence: 1, 2, 3...
environment_version=16
environment_version=17
#--------------------------------------------------------#
function usage {
@ -21,7 +21,8 @@ function usage {
echo " -f, --force Force a clean re-build of the virtual"
echo " environment. Useful when dependencies have"
echo " been added."
echo " -m, --makemessages Update all translation files."
echo " -m, --manage Run a Django management command."
echo " --makemessages Update all translation files."
echo " -p, --pep8 Just run pep8"
echo " -t, --tabs Check for tab characters in files."
echo " -y, --pylint Just run pylint"
@ -68,6 +69,7 @@ selenium=0
testargs=""
with_coverage=0
makemessages=0
manage=0
# Jenkins sets a "JOB_NAME" variable, if it's not set, we'll make it "default"
[ "$JOB_NAME" ] || JOB_NAME="default"
@ -83,7 +85,8 @@ function process_option {
-t|--tabs) just_tabs=1;;
-q|--quiet) quiet=1;;
-c|--coverage) with_coverage=1;;
-m|--makemessages) makemessages=1;;
-m|--manage) manage=1;;
--makemessages) makemessages=1;;
--with-selenium) selenium=1;;
--docs) just_docs=1;;
--runserver) runserver=1;;
@ -94,6 +97,10 @@ function process_option {
esac
}
function run_management_command {
${command_wrapper} python $root/manage.py $testargs
}
function run_server {
echo "Starting Django development server..."
${command_wrapper} python $root/manage.py runserver $testargs
@ -117,7 +124,7 @@ function run_pylint {
function run_pep8 {
echo "Running pep8 ..."
rm -f pep8.txt
PEP8_EXCLUDE=vcsversion.py
PEP8_EXCLUDE=vcsversion.py,panel_template,dash_template
PEP8_IGNORE=W602
PEP8_OPTIONS="--exclude=$PEP8_EXCLUDE --ignore=$PEP8_IGNORE --repeat"
${command_wrapper} pep8 $PEP8_OPTIONS $included_dirs | perl -ple 's/: ([WE]\d+)/: [$1]/' > pep8.txt || true
@ -188,6 +195,9 @@ function environment_check {
read update_env
if [ "x$update_env" = "xY" -o "x$update_env" = "x" -o "x$update_env" = "xy" ]; then
install_venv
else
# Set our command wrapper anyway.
command_wrapper="${root}/${with_venv}"
fi
fi
}
@ -346,6 +356,12 @@ fi
# ---------EXERCISE THE CODE------------ #
# Run management commands
if [ $manage -eq 1 ]; then
run_management_command
exit $?
fi
# Build the docs
if [ $just_docs -eq 1 ]; then
run_sphinx

View File

@ -3,6 +3,7 @@ coverage
django-nose
mox
nose
nose-exclude
pep8
pylint
distribute>=0.6.24