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:
parent
8396026722
commit
1721ba9c4a
@ -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!
|
||||
================
|
||||
|
||||
|
@ -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
|
||||
---------
|
||||
|
||||
|
@ -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
0
horizon/conf/__init__.py
Normal file
0
horizon/conf/dash_template/__init__.py
Normal file
0
horizon/conf/dash_template/__init__.py
Normal file
13
horizon/conf/dash_template/dashboard.py
Normal file
13
horizon/conf/dash_template/dashboard.py
Normal 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 }})
|
3
horizon/conf/dash_template/models.py
Normal file
3
horizon/conf/dash_template/models.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""
|
||||
Stub file to work around django bug: https://code.djangoproject.com/ticket/7198
|
||||
"""
|
@ -0,0 +1 @@
|
||||
/* Additional CSS for {{ dash_name }}. */
|
@ -0,0 +1 @@
|
||||
/* Additional JavaScript for {{ dash_name }}. */
|
11
horizon/conf/dash_template/templates/dash_name/base.html
Normal file
11
horizon/conf/dash_template/templates/dash_name/base.html
Normal 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 %}
|
0
horizon/conf/panel_template/__init__.py
Normal file
0
horizon/conf/panel_template/__init__.py
Normal file
3
horizon/conf/panel_template/models.py
Normal file
3
horizon/conf/panel_template/models.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""
|
||||
Stub file to work around django bug: https://code.djangoproject.com/ticket/7198
|
||||
"""
|
13
horizon/conf/panel_template/panel.py
Normal file
13
horizon/conf/panel_template/panel.py
Normal 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 }})
|
12
horizon/conf/panel_template/templates/panel_name/index.html
Normal file
12
horizon/conf/panel_template/templates/panel_name/index.html
Normal 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 %}
|
7
horizon/conf/panel_template/tests.py
Normal file
7
horizon/conf/panel_template/tests.py
Normal 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)
|
7
horizon/conf/panel_template/urls.py
Normal file
7
horizon/conf/panel_template/urls.py
Normal 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'),
|
||||
)
|
10
horizon/conf/panel_template/views.py
Normal file
10
horizon/conf/panel_template/views.py
Normal 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
46
horizon/loaders.py
Normal 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()
|
0
horizon/management/__init__.py
Normal file
0
horizon/management/__init__.py
Normal file
0
horizon/management/commands/__init__.py
Normal file
0
horizon/management/commands/__init__.py
Normal file
49
horizon/management/commands/startdash.py
Normal file
49
horizon/management/commands/startdash.py
Normal 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)
|
89
horizon/management/commands/startpanel.py
Normal file
89
horizon/management/commands/startpanel.py
Normal 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)
|
@ -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.
|
||||
"""
|
||||
|
@ -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']
|
||||
|
||||
|
@ -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 = (
|
||||
|
24
run_tests.sh
24
run_tests.sh
@ -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
|
||||
|
@ -3,6 +3,7 @@ coverage
|
||||
django-nose
|
||||
mox
|
||||
nose
|
||||
nose-exclude
|
||||
pep8
|
||||
pylint
|
||||
distribute>=0.6.24
|
||||
|
Loading…
Reference in New Issue
Block a user