Removed old code

This commit is contained in:
Serg Melikyan 2013-03-26 19:30:57 +04:00
parent 2677243357
commit ffcaa95cb8
914 changed files with 0 additions and 152019 deletions

28
dashboard/.gitignore vendored
View File

@ -1,28 +0,0 @@
*.pyc
*.swp
*.sqlite3
.environment_version
.selenium_log
.coverage*
.noseids
.DS_STORE
coverage.xml
nosetests.xml
pep8.txt
pylint.txt
reports
horizon.egg-info
openstack_dashboard/local/local_settings.py
openstack_dashboard/local/.secret_key_store
openstack_dashboard/test/.secret_key_store
doc/build/
doc/source/sourcecode
/static/
.venv
.tox
build
dist
AUTHORS
ChangeLog
tags
.idea/

View File

@ -1,4 +0,0 @@
[gerrit]
host=review.openstack.org
port=29418
project=openstack/horizon.git

View File

@ -1,10 +0,0 @@
# Format is:
# <preferred e-mail> <other e-mail 1>
# <preferred e-mail> <other e-mail 2>
<ghe@debian.org> <ghe.rivero@stackops.com>
<jake@ansolabs.com> <admin@jakedahn.com>
<launchpad@markgius.com> <mgius7096@gmail.com>
<yorik.sar@gmail.com> <yorik@ytaraday>
<jeblair@hp.com> <james.blair@rackspace.com>
<ke.wu@ibeca.me> <ke.wu@nebula.com>
Zhongyue Luo <zhongyue.nah@intel.com> <lzyeval@gmail.com>

View File

@ -1,4 +0,0 @@
[pep8]
ignore = E121,E126,E127,E128,W602
exclude = vcsversion.py,panel_template,dash_template,local_settings.py

View File

@ -1,42 +0,0 @@
# The format of this file isn't really documented; just use --generate-rcfile
[MASTER]
# Add <file or directory> to the black list. It should be a base name, not a
# path. You may set this option multiple times.
ignore=test
[Messages Control]
# NOTE(justinsb): We might want to have a 2nd strict pylintrc in future
# C0111: Don't require docstrings on every method
# W0511: TODOs in code comments are fine.
# W0142: *args and **kwargs are fine.
# W0622: Redefining id is fine.
disable=C0111,W0511,W0142,W0622
[Basic]
# Variable names can be 1 to 31 characters long, with lowercase and underscores
variable-rgx=[a-z_][a-z0-9_]{0,30}$
# Argument names can be 2 to 31 characters long, with lowercase and underscores
argument-rgx=[a-z_][a-z0-9_]{1,30}$
# Method names should be at least 3 characters long
# and be lowecased with underscores
method-rgx=([a-z_][a-z0-9_]{2,50}|setUp|tearDown)$
# Module names matching keystone-* are ok (files in bin/)
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+)|(keystone-[a-z0-9_-]+))$
# Don't require docstrings on tests.
no-docstring-rgx=((__.*__)|([tT]est.*)|setUp|tearDown)$
[Design]
max-public-methods=100
min-public-methods=0
max-args=6
[Variables]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
# _ is used by our localization
additional-builtins=_

View File

@ -1,11 +0,0 @@
[main]
host = https://www.transifex.com
[horizon.horizon-translations]
file_filter = horizon/locale/<lang>/LC_MESSAGES/django.po
source_lang = en_US
[horizon.openstack-dashboard-translations]
file_filter = openstack_dashboard/locale/<lang>/LC_MESSAGES/django.po
source_lang = en_US

View File

@ -1,176 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.

View File

@ -1,22 +0,0 @@
recursive-include bin *.js
recursive-include doc *.py *.rst *.css *.js *.html *.conf *.jpg *.gif *.png *.css_t
recursive-include horizon *.html *.css *.js *.csv *.template *.tmpl *.mo *.po
recursive-include openstack_dashboard *.html *.js *.less *.mo *.po *.example *.eot *.svg *.ttf *.woff *.png *.ico *.wsgi *.gif *.csv *.template
recursive-include tools *.py *.sh
include AUTHORS
include ChangeLog
include LICENSE
include Makefile
include manage.py
include openstack-common.conf
include README.rst
include run_tests.sh
include tox.ini
include bin/less/lessc
include doc/Makefile
include doc/source/_templates/.placeholder
include tools/pip-requires
include tools/test-requires
exclude openstack_dashboard/local/local_settings.py

View File

@ -1,24 +0,0 @@
PYTHON=`which python`
DESTDIR=/
PROJECT=horizon
all:
@echo "make test - Run tests"
@echo "make source - Create source package"
@echo "make install - Install on local system"
@echo "make buildrpm - Generate a rpm package"
@echo "make clean - Get rid of scratch and byte files"
source:
$(PYTHON) setup.py sdist $(COMPILE)
install:
$(PYTHON) setup.py install --root $(DESTDIR) $(COMPILE)
buildrpm:
$(PYTHON) setup.py bdist_rpm --post-install=rpm/postinstall --pre-uninstall=rpm/preuninstall
clean:
$(PYTHON) setup.py clean
rm -rf build/ MANIFEST
find . -name '*.pyc' -delete

View File

@ -1,125 +0,0 @@
=============================
Horizon (OpenStack Dashboard)
=============================
Horizon is a Django-based project aimed at providing a complete OpenStack
Dashboard along with an extensible framework for building new dashboards
from reusable components. The ``openstack_dashboard`` module is a reference
implementation of a Django site that uses the ``horizon`` app to provide
web-based interactions with the various OpenStack projects.
For release management:
* https://launchpad.net/horizon
For blueprints and feature specifications:
* https://blueprints.launchpad.net/horizon
For issue tracking:
* https://bugs.launchpad.net/horizon
Dependencies
============
To get started you will need to install Node.js (http://nodejs.org/) on your
machine. Node.js is used with Horizon in order to use LESS
(http://lesscss.org/) for our CSS needs. Horizon is currently using Node.js
v0.6.12.
For Ubuntu use apt to install Node.js::
$ sudo apt-get install nodejs
For other versions of Linux, please see here:: http://nodejs.org/#download for
how to install Node.js on your system.
Getting Started
===============
For local development, first create a virtualenv for the project.
In the ``tools`` directory there is a script to create one for you:
$ python tools/install_venv.py
Alternatively, the ``run_tests.sh`` script will also install the environment
for you and then run the full test suite to verify everything is installed
and functioning correctly.
Now that the virtualenv is created, you need to configure your local
environment. To do this, create a ``local_settings.py`` file in the
``openstack_dashboard/local/`` directory. There is a
``local_settings.py.example`` file there that may be used as a template.
If all is well you should able to run the development server locally:
$ tools/with_venv.sh manage.py runserver
or, as a shortcut::
$ ./run_tests.sh --runserver
Settings Up OpenStack
=====================
The recommended tool for installing and configuring the core OpenStack
components is `Devstack`_. Refer to their documentation for getting
Nova, Keystone, Glance, etc. up and running.
.. _Devstack: http://devstack.org/
.. note::
The minimum required set of OpenStack services running includes the
following:
* Nova (compute, api, scheduler, network, *and* volume services)
* Glance
* Keystone
Optional support is provided for Swift.
Development
===========
For development, start with the getting started instructions above.
Once you have a working virtualenv and all the necessary packages, read on.
If dependencies are added to either ``horizon`` or ``openstack-dashboard``,
they should be added to ``tools/pip-requires``.
The ``run_tests.sh`` script invokes tests and analyses on both of these
components in its process, and it is what Jenkins uses to verify the
stability of the project. If run before an environment is set up, it will
ask if you wish to install one.
To run the unit tests::
$ ./run_tests.sh
Building Contributor Documentation
==================================
This documentation is written by contributors, for contributors.
The source is maintained in the ``doc/source`` folder using
`reStructuredText`_ and built by `Sphinx`_
.. _reStructuredText: http://docutils.sourceforge.net/rst.html
.. _Sphinx: http://sphinx.pocoo.org/
* Building Automatically::
$ ./run_tests.sh --docs
* Building Manually::
$ export DJANGO_SETTINGS_MODULE=local.local_settings
$ python doc/generate_autodoc_index.py
$ sphinx-build -b html doc/source build/sphinx/html
Results are in the `build/sphinx/html` directory

View File

@ -1,139 +0,0 @@
#!/usr/bin/env node
var path = require('path'),
fs = require('fs'),
sys = require('util'),
os = require('os');
var less = require('../lib/less');
var args = process.argv.slice(1);
var options = {
compress: false,
yuicompress: false,
optimization: 1,
silent: false,
paths: [],
color: true,
strictImports: false
};
args = args.filter(function (arg) {
var match;
if (match = arg.match(/^-I(.+)$/)) {
options.paths.push(match[1]);
return false;
}
if (match = arg.match(/^--?([a-z][0-9a-z-]*)(?:=([^\s]+))?$/i)) { arg = match[1] }
else { return arg }
switch (arg) {
case 'v':
case 'version':
sys.puts("lessc " + less.version.join('.') + " (LESS Compiler) [JavaScript]");
process.exit(0);
case 'verbose':
options.verbose = true;
break;
case 's':
case 'silent':
options.silent = true;
break;
case 'strict-imports':
options.strictImports = true;
break;
case 'h':
case 'help':
sys.puts("usage: lessc source [destination]");
process.exit(0);
case 'x':
case 'compress':
options.compress = true;
break;
case 'yui-compress':
options.yuicompress = true;
break;
case 'no-color':
options.color = false;
break;
case 'include-path':
options.paths = match[2].split(os.type().match(/Windows/) ? ';' : ':')
.map(function(p) {
if (p) {
return path.resolve(process.cwd(), p);
}
});
break;
case 'O0': options.optimization = 0; break;
case 'O1': options.optimization = 1; break;
case 'O2': options.optimization = 2; break;
}
});
var input = args[1];
if (input && input != '-') {
input = path.resolve(process.cwd(), input);
}
var output = args[2];
if (output) {
output = path.resolve(process.cwd(), output);
}
var css, fd, tree;
if (! input) {
sys.puts("lessc: no input files");
process.exit(1);
}
var parseLessFile = function (e, data) {
if (e) {
sys.puts("lessc: " + e.message);
process.exit(1);
}
new(less.Parser)({
paths: [path.dirname(input)].concat(options.paths),
optimization: options.optimization,
filename: input,
strictImports: options.strictImports
}).parse(data, function (err, tree) {
if (err) {
less.writeError(err, options);
process.exit(1);
} else {
try {
css = tree.toCSS({
compress: options.compress,
yuicompress: options.yuicompress
});
if (output) {
fd = fs.openSync(output, "w");
fs.writeSync(fd, css, 0, "utf8");
} else {
sys.print(css);
}
} catch (e) {
less.writeError(e, options);
process.exit(2);
}
}
});
};
if (input != '-') {
fs.readFile(input, 'utf-8', parseLessFile);
} else {
process.stdin.resume();
process.stdin.setEncoding('utf8');
var buffer = '';
process.stdin.on('data', function(data) {
buffer += data;
});
process.stdin.on('end', function() {
parseLessFile(false, buffer);
});
}

View File

@ -1,380 +0,0 @@
//
// browser.js - client-side engine
//
var isFileProtocol = (location.protocol === 'file:' ||
location.protocol === 'chrome:' ||
location.protocol === 'chrome-extension:' ||
location.protocol === 'resource:');
less.env = less.env || (location.hostname == '127.0.0.1' ||
location.hostname == '0.0.0.0' ||
location.hostname == 'localhost' ||
location.port.length > 0 ||
isFileProtocol ? 'development'
: 'production');
// Load styles asynchronously (default: false)
//
// This is set to `false` by default, so that the body
// doesn't start loading before the stylesheets are parsed.
// Setting this to `true` can result in flickering.
//
less.async = false;
// Interval between watch polls
less.poll = less.poll || (isFileProtocol ? 1000 : 1500);
//
// Watch mode
//
less.watch = function () { return this.watchMode = true };
less.unwatch = function () { return this.watchMode = false };
if (less.env === 'development') {
less.optimization = 0;
if (/!watch/.test(location.hash)) {
less.watch();
}
less.watchTimer = setInterval(function () {
if (less.watchMode) {
loadStyleSheets(function (e, root, _, sheet, env) {
if (root) {
createCSS(root.toCSS(), sheet, env.lastModified);
}
});
}
}, less.poll);
} else {
less.optimization = 3;
}
var cache;
try {
cache = (typeof(window.localStorage) === 'undefined') ? null : window.localStorage;
} catch (_) {
cache = null;
}
//
// Get all <link> tags with the 'rel' attribute set to "stylesheet/less"
//
var links = document.getElementsByTagName('link');
var typePattern = /^text\/(x-)?less$/;
less.sheets = [];
for (var i = 0; i < links.length; i++) {
if (links[i].rel === 'stylesheet/less' || (links[i].rel.match(/stylesheet/) &&
(links[i].type.match(typePattern)))) {
less.sheets.push(links[i]);
}
}
less.refresh = function (reload) {
var startTime, endTime;
startTime = endTime = new(Date);
loadStyleSheets(function (e, root, _, sheet, env) {
if (env.local) {
log("loading " + sheet.href + " from cache.");
} else {
log("parsed " + sheet.href + " successfully.");
createCSS(root.toCSS(), sheet, env.lastModified);
}
log("css for " + sheet.href + " generated in " + (new(Date) - endTime) + 'ms');
(env.remaining === 0) && log("css generated in " + (new(Date) - startTime) + 'ms');
endTime = new(Date);
}, reload);
loadStyles();
};
less.refreshStyles = loadStyles;
less.refresh(less.env === 'development');
function loadStyles() {
var styles = document.getElementsByTagName('style');
for (var i = 0; i < styles.length; i++) {
if (styles[i].type.match(typePattern)) {
new(less.Parser)().parse(styles[i].innerHTML || '', function (e, tree) {
var css = tree.toCSS();
var style = styles[i];
style.type = 'text/css';
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.innerHTML = css;
}
});
}
}
}
function loadStyleSheets(callback, reload) {
for (var i = 0; i < less.sheets.length; i++) {
loadStyleSheet(less.sheets[i], callback, reload, less.sheets.length - (i + 1));
}
}
function loadStyleSheet(sheet, callback, reload, remaining) {
var url = window.location.href.replace(/[#?].*$/, '');
var href = sheet.href.replace(/\?.*$/, '');
var css = cache && cache.getItem(href);
var timestamp = cache && cache.getItem(href + ':timestamp');
var styles = { css: css, timestamp: timestamp };
// Stylesheets in IE don't always return the full path
if (! /^(https?|file):/.test(href)) {
if (href.charAt(0) == "/") {
href = window.location.protocol + "//" + window.location.host + href;
} else {
href = url.slice(0, url.lastIndexOf('/') + 1) + href;
}
}
var filename = href.match(/([^\/]+)$/)[1];
xhr(sheet.href, sheet.type, function (data, lastModified) {
if (!reload && styles && lastModified &&
(new(Date)(lastModified).valueOf() ===
new(Date)(styles.timestamp).valueOf())) {
// Use local copy
createCSS(styles.css, sheet);
callback(null, null, data, sheet, { local: true, remaining: remaining });
} else {
// Use remote copy (re-parse)
try {
new(less.Parser)({
optimization: less.optimization,
paths: [href.replace(/[\w\.-]+$/, '')],
mime: sheet.type,
filename: filename
}).parse(data, function (e, root) {
if (e) { return error(e, href) }
try {
callback(e, root, data, sheet, { local: false, lastModified: lastModified, remaining: remaining });
removeNode(document.getElementById('less-error-message:' + extractId(href)));
} catch (e) {
error(e, href);
}
});
} catch (e) {
error(e, href);
}
}
}, function (status, url) {
throw new(Error)("Couldn't load " + url + " (" + status + ")");
});
}
function extractId(href) {
return href.replace(/^[a-z]+:\/\/?[^\/]+/, '' ) // Remove protocol & domain
.replace(/^\//, '' ) // Remove root /
.replace(/\?.*$/, '' ) // Remove query
.replace(/\.[^\.\/]+$/, '' ) // Remove file extension
.replace(/[^\.\w-]+/g, '-') // Replace illegal characters
.replace(/\./g, ':'); // Replace dots with colons(for valid id)
}
function createCSS(styles, sheet, lastModified) {
var css;
// Strip the query-string
var href = sheet.href ? sheet.href.replace(/\?.*$/, '') : '';
// If there is no title set, use the filename, minus the extension
var id = 'less:' + (sheet.title || extractId(href));
// If the stylesheet doesn't exist, create a new node
if ((css = document.getElementById(id)) === null) {
css = document.createElement('style');
css.type = 'text/css';
css.media = sheet.media || 'screen';
css.id = id;
document.getElementsByTagName('head')[0].appendChild(css);
}
if (css.styleSheet) { // IE
try {
css.styleSheet.cssText = styles;
} catch (e) {
throw new(Error)("Couldn't reassign styleSheet.cssText.");
}
} else {
(function (node) {
if (css.childNodes.length > 0) {
if (css.firstChild.nodeValue !== node.nodeValue) {
css.replaceChild(node, css.firstChild);
}
} else {
css.appendChild(node);
}
})(document.createTextNode(styles));
}
// Don't update the local store if the file wasn't modified
if (lastModified && cache) {
log('saving ' + href + ' to cache.');
cache.setItem(href, styles);
cache.setItem(href + ':timestamp', lastModified);
}
}
function xhr(url, type, callback, errback) {
var xhr = getXMLHttpRequest();
var async = isFileProtocol ? false : less.async;
if (typeof(xhr.overrideMimeType) === 'function') {
xhr.overrideMimeType('text/css');
}
xhr.open('GET', url, async);
xhr.setRequestHeader('Accept', type || 'text/x-less, text/css; q=0.9, */*; q=0.5');
xhr.send(null);
if (isFileProtocol) {
if (xhr.status === 0 || (xhr.status >= 200 && xhr.status < 300)) {
callback(xhr.responseText);
} else {
errback(xhr.status, url);
}
} else if (async) {
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
handleResponse(xhr, callback, errback);
}
};
} else {
handleResponse(xhr, callback, errback);
}
function handleResponse(xhr, callback, errback) {
if (xhr.status >= 200 && xhr.status < 300) {
callback(xhr.responseText,
xhr.getResponseHeader("Last-Modified"));
} else if (typeof(errback) === 'function') {
errback(xhr.status, url);
}
}
}
function getXMLHttpRequest() {
if (window.XMLHttpRequest) {
return new(XMLHttpRequest);
} else {
try {
return new(ActiveXObject)("MSXML2.XMLHTTP.3.0");
} catch (e) {
log("browser doesn't support AJAX.");
return null;
}
}
}
function removeNode(node) {
return node && node.parentNode.removeChild(node);
}
function log(str) {
if (less.env == 'development' && typeof(console) !== "undefined") { console.log('less: ' + str) }
}
function error(e, href) {
var id = 'less-error-message:' + extractId(href);
var template = '<li><label>{line}</label><pre class="{class}">{content}</pre></li>';
var elem = document.createElement('div'), timer, content, error = [];
var filename = e.filename || href;
elem.id = id;
elem.className = "less-error-message";
content = '<h3>' + (e.message || 'There is an error in your .less file') +
'</h3>' + '<p>in <a href="' + filename + '">' + filename + "</a> ";
var errorline = function (e, i, classname) {
if (e.extract[i]) {
error.push(template.replace(/\{line\}/, parseInt(e.line) + (i - 1))
.replace(/\{class\}/, classname)
.replace(/\{content\}/, e.extract[i]));
}
};
if (e.stack) {
content += '<br/>' + e.stack.split('\n').slice(1).join('<br/>');
} else if (e.extract) {
errorline(e, 0, '');
errorline(e, 1, 'line');
errorline(e, 2, '');
content += 'on line ' + e.line + ', column ' + (e.column + 1) + ':</p>' +
'<ul>' + error.join('') + '</ul>';
}
elem.innerHTML = content;
// CSS for error messages
createCSS([
'.less-error-message ul, .less-error-message li {',
'list-style-type: none;',
'margin-right: 15px;',
'padding: 4px 0;',
'margin: 0;',
'}',
'.less-error-message label {',
'font-size: 12px;',
'margin-right: 15px;',
'padding: 4px 0;',
'color: #cc7777;',
'}',
'.less-error-message pre {',
'color: #dd6666;',
'padding: 4px 0;',
'margin: 0;',
'display: inline-block;',
'}',
'.less-error-message pre.line {',
'color: #ff0000;',
'}',
'.less-error-message h3 {',
'font-size: 20px;',
'font-weight: bold;',
'padding: 15px 0 5px 0;',
'margin: 0;',
'}',
'.less-error-message a {',
'color: #10a',
'}',
'.less-error-message .error {',
'color: red;',
'font-weight: bold;',
'padding-bottom: 2px;',
'border-bottom: 1px dashed red;',
'}'
].join('\n'), { title: 'error-message' });
elem.style.cssText = [
"font-family: Arial, sans-serif",
"border: 1px solid #e00",
"background-color: #eee",
"border-radius: 5px",
"-webkit-border-radius: 5px",
"-moz-border-radius: 5px",
"color: #e00",
"padding: 15px",
"margin-bottom: 15px"
].join(';');
if (less.env == 'development') {
timer = setInterval(function () {
if (document.body) {
if (document.getElementById(id)) {
document.body.replaceChild(elem, document.getElementById(id));
} else {
document.body.insertBefore(elem, document.body.firstChild);
}
clearInterval(timer);
}
}, 10);
}
}

View File

@ -1,152 +0,0 @@
(function (tree) {
tree.colors = {
'aliceblue':'#f0f8ff',
'antiquewhite':'#faebd7',
'aqua':'#00ffff',
'aquamarine':'#7fffd4',
'azure':'#f0ffff',
'beige':'#f5f5dc',
'bisque':'#ffe4c4',
'black':'#000000',
'blanchedalmond':'#ffebcd',
'blue':'#0000ff',
'blueviolet':'#8a2be2',
'brown':'#a52a2a',
'burlywood':'#deb887',
'cadetblue':'#5f9ea0',
'chartreuse':'#7fff00',
'chocolate':'#d2691e',
'coral':'#ff7f50',
'cornflowerblue':'#6495ed',
'cornsilk':'#fff8dc',
'crimson':'#dc143c',
'cyan':'#00ffff',
'darkblue':'#00008b',
'darkcyan':'#008b8b',
'darkgoldenrod':'#b8860b',
'darkgray':'#a9a9a9',
'darkgrey':'#a9a9a9',
'darkgreen':'#006400',
'darkkhaki':'#bdb76b',
'darkmagenta':'#8b008b',
'darkolivegreen':'#556b2f',
'darkorange':'#ff8c00',
'darkorchid':'#9932cc',
'darkred':'#8b0000',
'darksalmon':'#e9967a',
'darkseagreen':'#8fbc8f',
'darkslateblue':'#483d8b',
'darkslategray':'#2f4f4f',
'darkslategrey':'#2f4f4f',
'darkturquoise':'#00ced1',
'darkviolet':'#9400d3',
'deeppink':'#ff1493',
'deepskyblue':'#00bfff',
'dimgray':'#696969',
'dimgrey':'#696969',
'dodgerblue':'#1e90ff',
'firebrick':'#b22222',
'floralwhite':'#fffaf0',
'forestgreen':'#228b22',
'fuchsia':'#ff00ff',
'gainsboro':'#dcdcdc',
'ghostwhite':'#f8f8ff',
'gold':'#ffd700',
'goldenrod':'#daa520',
'gray':'#808080',
'grey':'#808080',
'green':'#008000',
'greenyellow':'#adff2f',
'honeydew':'#f0fff0',
'hotpink':'#ff69b4',
'indianred':'#cd5c5c',
'indigo':'#4b0082',
'ivory':'#fffff0',
'khaki':'#f0e68c',
'lavender':'#e6e6fa',
'lavenderblush':'#fff0f5',
'lawngreen':'#7cfc00',
'lemonchiffon':'#fffacd',
'lightblue':'#add8e6',
'lightcoral':'#f08080',
'lightcyan':'#e0ffff',
'lightgoldenrodyellow':'#fafad2',
'lightgray':'#d3d3d3',
'lightgrey':'#d3d3d3',
'lightgreen':'#90ee90',
'lightpink':'#ffb6c1',
'lightsalmon':'#ffa07a',
'lightseagreen':'#20b2aa',
'lightskyblue':'#87cefa',
'lightslategray':'#778899',
'lightslategrey':'#778899',
'lightsteelblue':'#b0c4de',
'lightyellow':'#ffffe0',
'lime':'#00ff00',
'limegreen':'#32cd32',
'linen':'#faf0e6',
'magenta':'#ff00ff',
'maroon':'#800000',
'mediumaquamarine':'#66cdaa',
'mediumblue':'#0000cd',
'mediumorchid':'#ba55d3',
'mediumpurple':'#9370d8',
'mediumseagreen':'#3cb371',
'mediumslateblue':'#7b68ee',
'mediumspringgreen':'#00fa9a',
'mediumturquoise':'#48d1cc',
'mediumvioletred':'#c71585',
'midnightblue':'#191970',
'mintcream':'#f5fffa',
'mistyrose':'#ffe4e1',
'moccasin':'#ffe4b5',
'navajowhite':'#ffdead',
'navy':'#000080',
'oldlace':'#fdf5e6',
'olive':'#808000',
'olivedrab':'#6b8e23',
'orange':'#ffa500',
'orangered':'#ff4500',
'orchid':'#da70d6',
'palegoldenrod':'#eee8aa',
'palegreen':'#98fb98',
'paleturquoise':'#afeeee',
'palevioletred':'#d87093',
'papayawhip':'#ffefd5',
'peachpuff':'#ffdab9',
'peru':'#cd853f',
'pink':'#ffc0cb',
'plum':'#dda0dd',
'powderblue':'#b0e0e6',
'purple':'#800080',
'red':'#ff0000',
'rosybrown':'#bc8f8f',
'royalblue':'#4169e1',
'saddlebrown':'#8b4513',
'salmon':'#fa8072',
'sandybrown':'#f4a460',
'seagreen':'#2e8b57',
'seashell':'#fff5ee',
'sienna':'#a0522d',
'silver':'#c0c0c0',
'skyblue':'#87ceeb',
'slateblue':'#6a5acd',
'slategray':'#708090',
'slategrey':'#708090',
'snow':'#fffafa',
'springgreen':'#00ff7f',
'steelblue':'#4682b4',
'tan':'#d2b48c',
'teal':'#008080',
'thistle':'#d8bfd8',
'tomato':'#ff6347',
'transparent':'rgba(0,0,0,0)',
'turquoise':'#40e0d0',
'violet':'#ee82ee',
'wheat':'#f5deb3',
'white':'#ffffff',
'whitesmoke':'#f5f5f5',
'yellow':'#ffff00',
'yellowgreen':'#9acd32'
};
})(require('./tree'));

View File

@ -1,355 +0,0 @@
/**
* cssmin.js
* Author: Stoyan Stefanov - http://phpied.com/
* This is a JavaScript port of the CSS minification tool
* distributed with YUICompressor, itself a port
* of the cssmin utility by Isaac Schlueter - http://foohack.com/
* Permission is hereby granted to use the JavaScript version under the same
* conditions as the YUICompressor (original YUICompressor note below).
*/
/*
* YUI Compressor
* http://developer.yahoo.com/yui/compressor/
* Author: Julien Lecomte - http://www.julienlecomte.net/
* Copyright (c) 2011 Yahoo! Inc. All rights reserved.
* The copyrights embodied in the content of this file are licensed
* by Yahoo! Inc. under the BSD (revised) open source license.
*/
var YAHOO = YAHOO || {};
YAHOO.compressor = YAHOO.compressor || {};
/**
* Utility method to replace all data urls with tokens before we start
* compressing, to avoid performance issues running some of the subsequent
* regexes against large strings chunks.
*
* @private
* @method _extractDataUrls
* @param {String} css The input css
* @param {Array} The global array of tokens to preserve
* @returns String The processed css
*/
YAHOO.compressor._extractDataUrls = function (css, preservedTokens) {
// Leave data urls alone to increase parse performance.
var maxIndex = css.length - 1,
appendIndex = 0,
startIndex,
endIndex,
terminator,
foundTerminator,
sb = [],
m,
preserver,
token,
pattern = /url\(\s*(["']?)data\:/g;
// Since we need to account for non-base64 data urls, we need to handle
// ' and ) being part of the data string. Hence switching to indexOf,
// to determine whether or not we have matching string terminators and
// handling sb appends directly, instead of using matcher.append* methods.
while ((m = pattern.exec(css)) !== null) {
startIndex = m.index + 4; // "url(".length()
terminator = m[1]; // ', " or empty (not quoted)
if (terminator.length === 0) {
terminator = ")";
}
foundTerminator = false;
endIndex = pattern.lastIndex - 1;
while(foundTerminator === false && endIndex+1 <= maxIndex) {
endIndex = css.indexOf(terminator, endIndex + 1);
// endIndex == 0 doesn't really apply here
if ((endIndex > 0) && (css.charAt(endIndex - 1) !== '\\')) {
foundTerminator = true;
if (")" != terminator) {
endIndex = css.indexOf(")", endIndex);
}
}
}
// Enough searching, start moving stuff over to the buffer
sb.push(css.substring(appendIndex, m.index));
if (foundTerminator) {
token = css.substring(startIndex, endIndex);
token = token.replace(/\s+/g, "");
preservedTokens.push(token);
preserver = "url(___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___)";
sb.push(preserver);
appendIndex = endIndex + 1;
} else {
// No end terminator found, re-add the whole match. Should we throw/warn here?
sb.push(css.substring(m.index, pattern.lastIndex));
appendIndex = pattern.lastIndex;
}
}
sb.push(css.substring(appendIndex));
return sb.join("");
};
/**
* Utility method to compress hex color values of the form #AABBCC to #ABC.
*
* DOES NOT compress CSS ID selectors which match the above pattern (which would break things).
* e.g. #AddressForm { ... }
*
* DOES NOT compress IE filters, which have hex color values (which would break things).
* e.g. filter: chroma(color="#FFFFFF");
*
* DOES NOT compress invalid hex values.
* e.g. background-color: #aabbccdd
*
* @private
* @method _compressHexColors
* @param {String} css The input css
* @returns String The processed css
*/
YAHOO.compressor._compressHexColors = function(css) {
// Look for hex colors inside { ... } (to avoid IDs) and which don't have a =, or a " in front of them (to avoid filters)
var pattern = /(\=\s*?["']?)?#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])(\}|[^0-9a-f{][^{]*?\})/gi,
m,
index = 0,
isFilter,
sb = [];
while ((m = pattern.exec(css)) !== null) {
sb.push(css.substring(index, m.index));
isFilter = m[1];
if (isFilter) {
// Restore, maintain case, otherwise filter will break
sb.push(m[1] + "#" + (m[2] + m[3] + m[4] + m[5] + m[6] + m[7]));
} else {
if (m[2].toLowerCase() == m[3].toLowerCase() &&
m[4].toLowerCase() == m[5].toLowerCase() &&
m[6].toLowerCase() == m[7].toLowerCase()) {
// Compress.
sb.push("#" + (m[3] + m[5] + m[7]).toLowerCase());
} else {
// Non compressible color, restore but lower case.
sb.push("#" + (m[2] + m[3] + m[4] + m[5] + m[6] + m[7]).toLowerCase());
}
}
index = pattern.lastIndex = pattern.lastIndex - m[8].length;
}
sb.push(css.substring(index));
return sb.join("");
};
YAHOO.compressor.cssmin = function (css, linebreakpos) {
var startIndex = 0,
endIndex = 0,
i = 0, max = 0,
preservedTokens = [],
comments = [],
token = '',
totallen = css.length,
placeholder = '';
css = this._extractDataUrls(css, preservedTokens);
// collect all comment blocks...
while ((startIndex = css.indexOf("/*", startIndex)) >= 0) {
endIndex = css.indexOf("*/", startIndex + 2);
if (endIndex < 0) {
endIndex = totallen;
}
token = css.slice(startIndex + 2, endIndex);
comments.push(token);
css = css.slice(0, startIndex + 2) + "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + (comments.length - 1) + "___" + css.slice(endIndex);
startIndex += 2;
}
// preserve strings so their content doesn't get accidentally minified
css = css.replace(/("([^\\"]|\\.|\\)*")|('([^\\']|\\.|\\)*')/g, function (match) {
var i, max, quote = match.substring(0, 1);
match = match.slice(1, -1);
// maybe the string contains a comment-like substring?
// one, maybe more? put'em back then
if (match.indexOf("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_") >= 0) {
for (i = 0, max = comments.length; i < max; i = i + 1) {
match = match.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", comments[i]);
}
}
// minify alpha opacity in filter strings
match = match.replace(/progid:DXImageTransform\.Microsoft\.Alpha\(Opacity=/gi, "alpha(opacity=");
preservedTokens.push(match);
return quote + "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___" + quote;
});
// strings are safe, now wrestle the comments
for (i = 0, max = comments.length; i < max; i = i + 1) {
token = comments[i];
placeholder = "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___";
// ! in the first position of the comment means preserve
// so push to the preserved tokens keeping the !
if (token.charAt(0) === "!") {
preservedTokens.push(token);
css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___");
continue;
}
// \ in the last position looks like hack for Mac/IE5
// shorten that to /*\*/ and the next one to /**/
if (token.charAt(token.length - 1) === "\\") {
preservedTokens.push("\\");
css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___");
i = i + 1; // attn: advancing the loop
preservedTokens.push("");
css = css.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___");
continue;
}
// keep empty comments after child selectors (IE7 hack)
// e.g. html >/**/ body
if (token.length === 0) {
startIndex = css.indexOf(placeholder);
if (startIndex > 2) {
if (css.charAt(startIndex - 3) === '>') {
preservedTokens.push("");
css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___");
}
}
}
// in all other cases kill the comment
css = css.replace("/*" + placeholder + "*/", "");
}
// Normalize all whitespace strings to single spaces. Easier to work with that way.
css = css.replace(/\s+/g, " ");
// Remove the spaces before the things that should not have spaces before them.
// But, be careful not to turn "p :link {...}" into "p:link{...}"
// Swap out any pseudo-class colons with the token, and then swap back.
css = css.replace(/(^|\})(([^\{:])+:)+([^\{]*\{)/g, function (m) {
return m.replace(":", "___YUICSSMIN_PSEUDOCLASSCOLON___");
});
css = css.replace(/\s+([!{};:>+\(\)\],])/g, '$1');
css = css.replace(/___YUICSSMIN_PSEUDOCLASSCOLON___/g, ":");
// retain space for special IE6 cases
css = css.replace(/:first-(line|letter)(\{|,)/g, ":first-$1 $2");
// no space after the end of a preserved comment
css = css.replace(/\*\/ /g, '*/');
// If there is a @charset, then only allow one, and push to the top of the file.
css = css.replace(/^(.*)(@charset "[^"]*";)/gi, '$2$1');
css = css.replace(/^(\s*@charset [^;]+;\s*)+/gi, '$1');
// Put the space back in some cases, to support stuff like
// @media screen and (-webkit-min-device-pixel-ratio:0){
css = css.replace(/\band\(/gi, "and (");
// Remove the spaces after the things that should not have spaces after them.
css = css.replace(/([!{}:;>+\(\[,])\s+/g, '$1');
// remove unnecessary semicolons
css = css.replace(/;+\}/g, "}");
// Replace 0(px,em,%) with 0.
css = css.replace(/([\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)/gi, "$1$2");
// Replace 0 0 0 0; with 0.
css = css.replace(/:0 0 0 0(;|\})/g, ":0$1");
css = css.replace(/:0 0 0(;|\})/g, ":0$1");
css = css.replace(/:0 0(;|\})/g, ":0$1");
// Replace background-position:0; with background-position:0 0;
// same for transform-origin
css = css.replace(/(background-position|transform-origin|webkit-transform-origin|moz-transform-origin|o-transform-origin|ms-transform-origin):0(;|\})/gi, function(all, prop, tail) {
return prop.toLowerCase() + ":0 0" + tail;
});
// Replace 0.6 to .6, but only when preceded by : or a white-space
css = css.replace(/(:|\s)0+\.(\d+)/g, "$1.$2");
// Shorten colors from rgb(51,102,153) to #336699
// This makes it more likely that it'll get further compressed in the next step.
css = css.replace(/rgb\s*\(\s*([0-9,\s]+)\s*\)/gi, function () {
var i, rgbcolors = arguments[1].split(',');
for (i = 0; i < rgbcolors.length; i = i + 1) {
rgbcolors[i] = parseInt(rgbcolors[i], 10).toString(16);
if (rgbcolors[i].length === 1) {
rgbcolors[i] = '0' + rgbcolors[i];
}
}
return '#' + rgbcolors.join('');
});
// Shorten colors from #AABBCC to #ABC.
css = this._compressHexColors(css);
// border: none -> border:0
css = css.replace(/(border|border-top|border-right|border-bottom|border-right|outline|background):none(;|\})/gi, function(all, prop, tail) {
return prop.toLowerCase() + ":0" + tail;
});
// shorter opacity IE filter
css = css.replace(/progid:DXImageTransform\.Microsoft\.Alpha\(Opacity=/gi, "alpha(opacity=");
// Remove empty rules.
css = css.replace(/[^\};\{\/]+\{\}/g, "");
if (linebreakpos >= 0) {
// Some source control tools don't like it when files containing lines longer
// than, say 8000 characters, are checked in. The linebreak option is used in
// that case to split long lines after a specific column.
startIndex = 0;
i = 0;
while (i < css.length) {
i = i + 1;
if (css[i - 1] === '}' && i - startIndex > linebreakpos) {
css = css.slice(0, i) + '\n' + css.slice(i);
startIndex = i;
}
}
}
// Replace multiple semi-colons in a row by a single one
// See SF bug #1980989
css = css.replace(/;;+/g, ";");
// restore preserved comments and strings
for (i = 0, max = preservedTokens.length; i < max; i = i + 1) {
css = css.replace("___YUICSSMIN_PRESERVED_TOKEN_" + i + "___", preservedTokens[i]);
}
// Trim the final string (for any leading or trailing white spaces)
css = css.replace(/^\s+|\s+$/g, "");
return css;
};
exports.compressor = YAHOO.compressor;

View File

@ -1,228 +0,0 @@
(function (tree) {
tree.functions = {
rgb: function (r, g, b) {
return this.rgba(r, g, b, 1.0);
},
rgba: function (r, g, b, a) {
var rgb = [r, g, b].map(function (c) { return number(c) }),
a = number(a);
return new(tree.Color)(rgb, a);
},
hsl: function (h, s, l) {
return this.hsla(h, s, l, 1.0);
},
hsla: function (h, s, l, a) {
h = (number(h) % 360) / 360;
s = number(s); l = number(l); a = number(a);
var m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s;
var m1 = l * 2 - m2;
return this.rgba(hue(h + 1/3) * 255,
hue(h) * 255,
hue(h - 1/3) * 255,
a);
function hue(h) {
h = h < 0 ? h + 1 : (h > 1 ? h - 1 : h);
if (h * 6 < 1) return m1 + (m2 - m1) * h * 6;
else if (h * 2 < 1) return m2;
else if (h * 3 < 2) return m1 + (m2 - m1) * (2/3 - h) * 6;
else return m1;
}
},
hue: function (color) {
return new(tree.Dimension)(Math.round(color.toHSL().h));
},
saturation: function (color) {
return new(tree.Dimension)(Math.round(color.toHSL().s * 100), '%');
},
lightness: function (color) {
return new(tree.Dimension)(Math.round(color.toHSL().l * 100), '%');
},
alpha: function (color) {
return new(tree.Dimension)(color.toHSL().a);
},
saturate: function (color, amount) {
var hsl = color.toHSL();
hsl.s += amount.value / 100;
hsl.s = clamp(hsl.s);
return hsla(hsl);
},
desaturate: function (color, amount) {
var hsl = color.toHSL();
hsl.s -= amount.value / 100;
hsl.s = clamp(hsl.s);
return hsla(hsl);
},
lighten: function (color, amount) {
var hsl = color.toHSL();
hsl.l += amount.value / 100;
hsl.l = clamp(hsl.l);
return hsla(hsl);
},
darken: function (color, amount) {
var hsl = color.toHSL();
hsl.l -= amount.value / 100;
hsl.l = clamp(hsl.l);
return hsla(hsl);
},
fadein: function (color, amount) {
var hsl = color.toHSL();
hsl.a += amount.value / 100;
hsl.a = clamp(hsl.a);
return hsla(hsl);
},
fadeout: function (color, amount) {
var hsl = color.toHSL();
hsl.a -= amount.value / 100;
hsl.a = clamp(hsl.a);
return hsla(hsl);
},
fade: function (color, amount) {
var hsl = color.toHSL();
hsl.a = amount.value / 100;
hsl.a = clamp(hsl.a);
return hsla(hsl);
},
spin: function (color, amount) {
var hsl = color.toHSL();
var hue = (hsl.h + amount.value) % 360;
hsl.h = hue < 0 ? 360 + hue : hue;
return hsla(hsl);
},
//
// Copyright (c) 2006-2009 Hampton Catlin, Nathan Weizenbaum, and Chris Eppstein
// http://sass-lang.com
//
mix: function (color1, color2, weight) {
var p = weight.value / 100.0;
var w = p * 2 - 1;
var a = color1.toHSL().a - color2.toHSL().a;
var w1 = (((w * a == -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0;
var w2 = 1 - w1;
var rgb = [color1.rgb[0] * w1 + color2.rgb[0] * w2,
color1.rgb[1] * w1 + color2.rgb[1] * w2,
color1.rgb[2] * w1 + color2.rgb[2] * w2];
var alpha = color1.alpha * p + color2.alpha * (1 - p);
return new(tree.Color)(rgb, alpha);
},
greyscale: function (color) {
return this.desaturate(color, new(tree.Dimension)(100));
},
e: function (str) {
return new(tree.Anonymous)(str instanceof tree.JavaScript ? str.evaluated : str);
},
escape: function (str) {
return new(tree.Anonymous)(encodeURI(str.value).replace(/=/g, "%3D").replace(/:/g, "%3A").replace(/#/g, "%23").replace(/;/g, "%3B").replace(/\(/g, "%28").replace(/\)/g, "%29"));
},
'%': function (quoted /* arg, arg, ...*/) {
var args = Array.prototype.slice.call(arguments, 1),
str = quoted.value;
for (var i = 0; i < args.length; i++) {
str = str.replace(/%[sda]/i, function(token) {
var value = token.match(/s/i) ? args[i].value : args[i].toCSS();
return token.match(/[A-Z]$/) ? encodeURIComponent(value) : value;
});
}
str = str.replace(/%%/g, '%');
return new(tree.Quoted)('"' + str + '"', str);
},
round: function (n) {
return this._math('round', n);
},
ceil: function (n) {
return this._math('ceil', n);
},
floor: function (n) {
return this._math('floor', n);
},
_math: function (fn, n) {
if (n instanceof tree.Dimension) {
return new(tree.Dimension)(Math[fn](number(n)), n.unit);
} else if (typeof(n) === 'number') {
return Math[fn](n);
} else {
throw { type: "Argument", message: "argument must be a number" };
}
},
argb: function (color) {
return new(tree.Anonymous)(color.toARGB());
},
percentage: function (n) {
return new(tree.Dimension)(n.value * 100, '%');
},
color: function (n) {
if (n instanceof tree.Quoted) {
return new(tree.Color)(n.value.slice(1));
} else {
throw { type: "Argument", message: "argument must be a string" };
}
},
iscolor: function (n) {
return this._isa(n, tree.Color);
},
isnumber: function (n) {
return this._isa(n, tree.Dimension);
},
isstring: function (n) {
return this._isa(n, tree.Quoted);
},
iskeyword: function (n) {
return this._isa(n, tree.Keyword);
},
isurl: function (n) {
return this._isa(n, tree.URL);
},
ispixel: function (n) {
return (n instanceof tree.Dimension) && n.unit === 'px' ? tree.True : tree.False;
},
ispercentage: function (n) {
return (n instanceof tree.Dimension) && n.unit === '%' ? tree.True : tree.False;
},
isem: function (n) {
return (n instanceof tree.Dimension) && n.unit === 'em' ? tree.True : tree.False;
},
_isa: function (n, Type) {
return (n instanceof Type) ? tree.True : tree.False;
}
};
function hsla(hsla) {
return tree.functions.hsla(hsla.h, hsla.s, hsla.l, hsla.a);
}
function number(n) {
if (n instanceof tree.Dimension) {
return parseFloat(n.unit == '%' ? n.value / 100 : n.value);
} else if (typeof(n) === 'number') {
return n;
} else {
throw {
error: "RuntimeError",
message: "color functions take numbers as parameters"
};
}
}
function clamp(val) {
return Math.min(1, Math.max(0, val));
}
})(require('./tree'));

View File

@ -1,148 +0,0 @@
var path = require('path'),
sys = require('util'),
fs = require('fs');
var less = {
version: [1, 3, 0],
Parser: require('./parser').Parser,
importer: require('./parser').importer,
tree: require('./tree'),
render: function (input, options, callback) {
options = options || {};
if (typeof(options) === 'function') {
callback = options, options = {};
}
var parser = new(less.Parser)(options),
ee;
if (callback) {
parser.parse(input, function (e, root) {
callback(e, root && root.toCSS && root.toCSS(options));
});
} else {
ee = new(require('events').EventEmitter);
process.nextTick(function () {
parser.parse(input, function (e, root) {
if (e) { ee.emit('error', e) }
else { ee.emit('success', root.toCSS(options)) }
});
});
return ee;
}
},
writeError: function (ctx, options) {
options = options || {};
var message = "";
var extract = ctx.extract;
var error = [];
var stylize = options.color ? less.stylize : function (str) { return str };
if (options.silent) { return }
if (ctx.stack) { return sys.error(stylize(ctx.stack, 'red')) }
if (!ctx.hasOwnProperty('index')) {
return sys.error(ctx.stack || ctx.message);
}
if (typeof(extract[0]) === 'string') {
error.push(stylize((ctx.line - 1) + ' ' + extract[0], 'grey'));
}
if (extract[1]) {
error.push(ctx.line + ' ' + extract[1].slice(0, ctx.column)
+ stylize(stylize(stylize(extract[1][ctx.column], 'bold')
+ extract[1].slice(ctx.column + 1), 'red'), 'inverse'));
}
if (typeof(extract[2]) === 'string') {
error.push(stylize((ctx.line + 1) + ' ' + extract[2], 'grey'));
}
error = error.join('\n') + '\033[0m\n';
message += stylize(ctx.type + 'Error: ' + ctx.message, 'red');
ctx.filename && (message += stylize(' in ', 'red') + ctx.filename +
stylize(':' + ctx.line + ':' + ctx.column, 'grey'));
sys.error(message, error);
if (ctx.callLine) {
sys.error(stylize('from ', 'red') + (ctx.filename || ''));
sys.error(stylize(ctx.callLine, 'grey') + ' ' + ctx.callExtract);
}
}
};
['color', 'directive', 'operation', 'dimension',
'keyword', 'variable', 'ruleset', 'element',
'selector', 'quoted', 'expression', 'rule',
'call', 'url', 'alpha', 'import',
'mixin', 'comment', 'anonymous', 'value',
'javascript', 'assignment', 'condition', 'paren',
'media'
].forEach(function (n) {
require('./tree/' + n);
});
less.Parser.importer = function (file, paths, callback, env) {
var pathname;
// TODO: Undo this at some point,
// or use different approach.
paths.unshift('.');
for (var i = 0; i < paths.length; i++) {
try {
pathname = path.join(paths[i], file);
fs.statSync(pathname);
break;
} catch (e) {
pathname = null;
}
}
if (pathname) {
fs.readFile(pathname, 'utf-8', function(e, data) {
if (e) return callback(e);
new(less.Parser)({
paths: [path.dirname(pathname)].concat(paths),
filename: pathname
}).parse(data, function (e, root) {
callback(e, root, data);
});
});
} else {
if (typeof(env.errback) === "function") {
env.errback(file, paths, callback);
} else {
callback({ type: 'File', message: "'" + file + "' wasn't found.\n" });
}
}
}
require('./functions');
require('./colors');
for (var k in less) { exports[k] = less[k] }
// Stylize a string
function stylize(str, style) {
var styles = {
'bold' : [1, 22],
'inverse' : [7, 27],
'underline' : [4, 24],
'yellow' : [33, 39],
'green' : [32, 39],
'red' : [31, 39],
'grey' : [90, 39]
};
return '\033[' + styles[style][0] + 'm' + str +
'\033[' + styles[style][1] + 'm';
}
less.stylize = stylize;

File diff suppressed because it is too large Load Diff

View File

@ -1,62 +0,0 @@
var name;
function loadStyleSheet(sheet, callback, reload, remaining) {
var sheetName = name.slice(0, name.lastIndexOf('/') + 1) + sheet.href;
var input = readFile(sheetName);
var parser = new less.Parser({
paths: [sheet.href.replace(/[\w\.-]+$/, '')]
});
parser.parse(input, function (e, root) {
if (e) {
print("Error: " + e);
quit(1);
}
callback(root, sheet, { local: false, lastModified: 0, remaining: remaining });
});
// callback({}, sheet, { local: true, remaining: remaining });
}
function writeFile(filename, content) {
var fstream = new java.io.FileWriter(filename);
var out = new java.io.BufferedWriter(fstream);
out.write(content);
out.close();
}
// Command line integration via Rhino
(function (args) {
name = args[0];
var output = args[1];
if (!name) {
print('No files present in the fileset; Check your pattern match in build.xml');
quit(1);
}
path = name.split("/");path.pop();path=path.join("/")
var input = readFile(name);
if (!input) {
print('lesscss: couldn\'t open file ' + name);
quit(1);
}
var result;
var parser = new less.Parser();
parser.parse(input, function (e, root) {
if (e) {
quit(1);
} else {
result = root.toCSS();
if (output) {
writeFile(output, result);
print("Written to " + output);
} else {
print(result);
}
quit(0);
}
});
print("done");
}(arguments));

View File

@ -1,17 +0,0 @@
(function (tree) {
tree.find = function (obj, fun) {
for (var i = 0, r; i < obj.length; i++) {
if (r = fun.call(obj, obj[i])) { return r }
}
return null;
};
tree.jsify = function (obj) {
if (Array.isArray(obj.value) && (obj.value.length > 1)) {
return '[' + obj.value.map(function (v) { return v.toCSS(false) }).join(', ') + ']';
} else {
return obj.toCSS(false);
}
};
})(require('./tree'));

View File

@ -1,17 +0,0 @@
(function (tree) {
tree.Alpha = function (val) {
this.value = val;
};
tree.Alpha.prototype = {
toCSS: function () {
return "alpha(opacity=" +
(this.value.toCSS ? this.value.toCSS() : this.value) + ")";
},
eval: function (env) {
if (this.value.eval) { this.value = this.value.eval(env) }
return this;
}
};
})(require('../tree'));

View File

@ -1,13 +0,0 @@
(function (tree) {
tree.Anonymous = function (string) {
this.value = string.value || string;
};
tree.Anonymous.prototype = {
toCSS: function () {
return this.value;
},
eval: function () { return this }
};
})(require('../tree'));

View File

@ -1,17 +0,0 @@
(function (tree) {
tree.Assignment = function (key, val) {
this.key = key;
this.value = val;
};
tree.Assignment.prototype = {
toCSS: function () {
return this.key + '=' + (this.value.toCSS ? this.value.toCSS() : this.value);
},
eval: function (env) {
if (this.value.eval) { this.value = this.value.eval(env) }
return this;
}
};
})(require('../tree'));

View File

@ -1,48 +0,0 @@
(function (tree) {
//
// A function call node.
//
tree.Call = function (name, args, index, filename) {
this.name = name;
this.args = args;
this.index = index;
this.filename = filename;
};
tree.Call.prototype = {
//
// When evaluating a function call,
// we either find the function in `tree.functions` [1],
// in which case we call it, passing the evaluated arguments,
// or we simply print it out as it appeared originally [2].
//
// The *functions.js* file contains the built-in functions.
//
// The reason why we evaluate the arguments, is in the case where
// we try to pass a variable to a function, like: `saturate(@color)`.
// The function should receive the value, not the variable.
//
eval: function (env) {
var args = this.args.map(function (a) { return a.eval(env) });
if (this.name in tree.functions) { // 1.
try {
return tree.functions[this.name].apply(tree.functions, args);
} catch (e) {
throw { type: e.type || "Runtime",
message: "error evaluating function `" + this.name + "`" +
(e.message ? ': ' + e.message : ''),
index: this.index, filename: this.filename };
}
} else { // 2.
return new(tree.Anonymous)(this.name +
"(" + args.map(function (a) { return a.toCSS() }).join(', ') + ")");
}
},
toCSS: function (env) {
return this.eval(env).toCSS();
}
};
})(require('../tree'));

View File

@ -1,101 +0,0 @@
(function (tree) {
//
// RGB Colors - #ff0014, #eee
//
tree.Color = function (rgb, a) {
//
// The end goal here, is to parse the arguments
// into an integer triplet, such as `128, 255, 0`
//
// This facilitates operations and conversions.
//
if (Array.isArray(rgb)) {
this.rgb = rgb;
} else if (rgb.length == 6) {
this.rgb = rgb.match(/.{2}/g).map(function (c) {
return parseInt(c, 16);
});
} else {
this.rgb = rgb.split('').map(function (c) {
return parseInt(c + c, 16);
});
}
this.alpha = typeof(a) === 'number' ? a : 1;
};
tree.Color.prototype = {
eval: function () { return this },
//
// If we have some transparency, the only way to represent it
// is via `rgba`. Otherwise, we use the hex representation,
// which has better compatibility with older browsers.
// Values are capped between `0` and `255`, rounded and zero-padded.
//
toCSS: function () {
if (this.alpha < 1.0) {
return "rgba(" + this.rgb.map(function (c) {
return Math.round(c);
}).concat(this.alpha).join(', ') + ")";
} else {
return '#' + this.rgb.map(function (i) {
i = Math.round(i);
i = (i > 255 ? 255 : (i < 0 ? 0 : i)).toString(16);
return i.length === 1 ? '0' + i : i;
}).join('');
}
},
//
// Operations have to be done per-channel, if not,
// channels will spill onto each other. Once we have
// our result, in the form of an integer triplet,
// we create a new Color node to hold the result.
//
operate: function (op, other) {
var result = [];
if (! (other instanceof tree.Color)) {
other = other.toColor();
}
for (var c = 0; c < 3; c++) {
result[c] = tree.operate(op, this.rgb[c], other.rgb[c]);
}
return new(tree.Color)(result, this.alpha + other.alpha);
},
toHSL: function () {
var r = this.rgb[0] / 255,
g = this.rgb[1] / 255,
b = this.rgb[2] / 255,
a = this.alpha;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2, d = max - min;
if (max === min) {
h = s = 0;
} else {
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return { h: h * 360, s: s, l: l, a: a };
},
toARGB: function () {
var argb = [Math.round(this.alpha * 255)].concat(this.rgb);
return '#' + argb.map(function (i) {
i = Math.round(i);
i = (i > 255 ? 255 : (i < 0 ? 0 : i)).toString(16);
return i.length === 1 ? '0' + i : i;
}).join('');
}
};
})(require('../tree'));

View File

@ -1,14 +0,0 @@
(function (tree) {
tree.Comment = function (value, silent) {
this.value = value;
this.silent = !!silent;
};
tree.Comment.prototype = {
toCSS: function (env) {
return env.compress ? '' : this.value;
},
eval: function () { return this }
};
})(require('../tree'));

View File

@ -1,42 +0,0 @@
(function (tree) {
tree.Condition = function (op, l, r, i, negate) {
this.op = op.trim();
this.lvalue = l;
this.rvalue = r;
this.index = i;
this.negate = negate;
};
tree.Condition.prototype.eval = function (env) {
var a = this.lvalue.eval(env),
b = this.rvalue.eval(env);
var i = this.index, result;
var result = (function (op) {
switch (op) {
case 'and':
return a && b;
case 'or':
return a || b;
default:
if (a.compare) {
result = a.compare(b);
} else if (b.compare) {
result = b.compare(a);
} else {
throw { type: "Type",
message: "Unable to perform comparison",
index: i };
}
switch (result) {
case -1: return op === '<' || op === '=<';
case 0: return op === '=' || op === '>=' || op === '=<';
case 1: return op === '>' || op === '>=';
}
}
})(this.op);
return this.negate ? !result : result;
};
})(require('../tree'));

View File

@ -1,49 +0,0 @@
(function (tree) {
//
// A number with a unit
//
tree.Dimension = function (value, unit) {
this.value = parseFloat(value);
this.unit = unit || null;
};
tree.Dimension.prototype = {
eval: function () { return this },
toColor: function () {
return new(tree.Color)([this.value, this.value, this.value]);
},
toCSS: function () {
var css = this.value + this.unit;
return css;
},
// In an operation between two Dimensions,
// we default to the first Dimension's unit,
// so `1px + 2em` will yield `3px`.
// In the future, we could implement some unit
// conversions such that `100cm + 10mm` would yield
// `101cm`.
operate: function (op, other) {
return new(tree.Dimension)
(tree.operate(op, this.value, other.value),
this.unit || other.unit);
},
// TODO: Perform unit conversion before comparing
compare: function (other) {
if (other instanceof tree.Dimension) {
if (other.value > this.value) {
return -1;
} else if (other.value < this.value) {
return 1;
} else {
return 0;
}
} else {
return -1;
}
}
};
})(require('../tree'));

View File

@ -1,35 +0,0 @@
(function (tree) {
tree.Directive = function (name, value, features) {
this.name = name;
if (Array.isArray(value)) {
this.ruleset = new(tree.Ruleset)([], value);
this.ruleset.allowImports = true;
} else {
this.value = value;
}
};
tree.Directive.prototype = {
toCSS: function (ctx, env) {
if (this.ruleset) {
this.ruleset.root = true;
return this.name + (env.compress ? '{' : ' {\n ') +
this.ruleset.toCSS(ctx, env).trim().replace(/\n/g, '\n ') +
(env.compress ? '}': '\n}\n');
} else {
return this.name + ' ' + this.value.toCSS() + ';\n';
}
},
eval: function (env) {
env.frames.unshift(this);
this.ruleset = this.ruleset && this.ruleset.eval(env);
env.frames.shift();
return this;
},
variable: function (name) { return tree.Ruleset.prototype.variable.call(this.ruleset, name) },
find: function () { return tree.Ruleset.prototype.find.apply(this.ruleset, arguments) },
rulesets: function () { return tree.Ruleset.prototype.rulesets.apply(this.ruleset) }
};
})(require('../tree'));

View File

@ -1,52 +0,0 @@
(function (tree) {
tree.Element = function (combinator, value, index) {
this.combinator = combinator instanceof tree.Combinator ?
combinator : new(tree.Combinator)(combinator);
if (typeof(value) === 'string') {
this.value = value.trim();
} else if (value) {
this.value = value;
} else {
this.value = "";
}
this.index = index;
};
tree.Element.prototype.eval = function (env) {
return new(tree.Element)(this.combinator,
this.value.eval ? this.value.eval(env) : this.value,
this.index);
};
tree.Element.prototype.toCSS = function (env) {
var value = (this.value.toCSS ? this.value.toCSS(env) : this.value);
if (value == '' && this.combinator.value.charAt(0) == '&') {
return '';
} else {
return this.combinator.toCSS(env || {}) + value;
}
};
tree.Combinator = function (value) {
if (value === ' ') {
this.value = ' ';
} else if (value === '& ') {
this.value = '& ';
} else {
this.value = value ? value.trim() : "";
}
};
tree.Combinator.prototype.toCSS = function (env) {
return {
'' : '',
' ' : ' ',
'&' : '',
'& ' : ' ',
':' : ' :',
'+' : env.compress ? '+' : ' + ',
'~' : env.compress ? '~' : ' ~ ',
'>' : env.compress ? '>' : ' > '
}[this.value];
};
})(require('../tree'));

View File

@ -1,23 +0,0 @@
(function (tree) {
tree.Expression = function (value) { this.value = value };
tree.Expression.prototype = {
eval: function (env) {
if (this.value.length > 1) {
return new(tree.Expression)(this.value.map(function (e) {
return e.eval(env);
}));
} else if (this.value.length === 1) {
return this.value[0].eval(env);
} else {
return this;
}
},
toCSS: function (env) {
return this.value.map(function (e) {
return e.toCSS ? e.toCSS(env) : '';
}).join(' ');
}
};
})(require('../tree'));

View File

@ -1,83 +0,0 @@
(function (tree) {
//
// CSS @import node
//
// The general strategy here is that we don't want to wait
// for the parsing to be completed, before we start importing
// the file. That's because in the context of a browser,
// most of the time will be spent waiting for the server to respond.
//
// On creation, we push the import path to our import queue, though
// `import,push`, we also pass it a callback, which it'll call once
// the file has been fetched, and parsed.
//
tree.Import = function (path, imports, features, once, index) {
var that = this;
this.once = once;
this.index = index;
this._path = path;
this.features = features && new(tree.Value)(features);
// The '.less' extension is optional
if (path instanceof tree.Quoted) {
this.path = /\.(le?|c)ss(\?.*)?$/.test(path.value) ? path.value : path.value + '.less';
} else {
this.path = path.value.value || path.value;
}
this.css = /css(\?.*)?$/.test(this.path);
// Only pre-compile .less files
if (! this.css) {
imports.push(this.path, function (e, root, imported) {
if (e) { e.index = index }
if (imported && that.once) that.skip = imported;
that.root = root || new(tree.Ruleset)([], []);
});
}
};
//
// The actual import node doesn't return anything, when converted to CSS.
// The reason is that it's used at the evaluation stage, so that the rules
// it imports can be treated like any other rules.
//
// In `eval`, we make sure all Import nodes get evaluated, recursively, so
// we end up with a flat structure, which can easily be imported in the parent
// ruleset.
//
tree.Import.prototype = {
toCSS: function (env) {
var features = this.features ? ' ' + this.features.toCSS(env) : '';
if (this.css) {
return "@import " + this._path.toCSS() + features + ';\n';
} else {
return "";
}
},
eval: function (env) {
var ruleset, features = this.features && this.features.eval(env);
if (this.skip) return [];
if (this.css) {
return this;
} else {
ruleset = new(tree.Ruleset)([], this.root.rules.slice(0));
for (var i = 0; i < ruleset.rules.length; i++) {
if (ruleset.rules[i] instanceof tree.Import) {
Array.prototype
.splice
.apply(ruleset.rules,
[i, 1].concat(ruleset.rules[i].eval(env)));
}
}
return this.features ? new(tree.Media)(ruleset.rules, this.features.value) : ruleset.rules;
}
}
};
})(require('../tree'));

View File

@ -1,51 +0,0 @@
(function (tree) {
tree.JavaScript = function (string, index, escaped) {
this.escaped = escaped;
this.expression = string;
this.index = index;
};
tree.JavaScript.prototype = {
eval: function (env) {
var result,
that = this,
context = {};
var expression = this.expression.replace(/@\{([\w-]+)\}/g, function (_, name) {
return tree.jsify(new(tree.Variable)('@' + name, that.index).eval(env));
});
try {
expression = new(Function)('return (' + expression + ')');
} catch (e) {
throw { message: "JavaScript evaluation error: `" + expression + "`" ,
index: this.index };
}
for (var k in env.frames[0].variables()) {
context[k.slice(1)] = {
value: env.frames[0].variables()[k].value,
toJS: function () {
return this.value.eval(env).toCSS();
}
};
}
try {
result = expression.call(context);
} catch (e) {
throw { message: "JavaScript evaluation error: '" + e.name + ': ' + e.message + "'" ,
index: this.index };
}
if (typeof(result) === 'string') {
return new(tree.Quoted)('"' + result + '"', result, this.escaped, this.index);
} else if (Array.isArray(result)) {
return new(tree.Anonymous)(result.join(', '));
} else {
return new(tree.Anonymous)(result);
}
}
};
})(require('../tree'));

View File

@ -1,19 +0,0 @@
(function (tree) {
tree.Keyword = function (value) { this.value = value };
tree.Keyword.prototype = {
eval: function () { return this },
toCSS: function () { return this.value },
compare: function (other) {
if (other instanceof tree.Keyword) {
return other.value === this.value ? 0 : 1;
} else {
return -1;
}
}
};
tree.True = new(tree.Keyword)('true');
tree.False = new(tree.Keyword)('false');
})(require('../tree'));

View File

@ -1,114 +0,0 @@
(function (tree) {
tree.Media = function (value, features) {
var el = new(tree.Element)('&', null, 0),
selectors = [new(tree.Selector)([el])];
this.features = new(tree.Value)(features);
this.ruleset = new(tree.Ruleset)(selectors, value);
this.ruleset.allowImports = true;
};
tree.Media.prototype = {
toCSS: function (ctx, env) {
var features = this.features.toCSS(env);
this.ruleset.root = (ctx.length === 0 || ctx[0].multiMedia);
return '@media ' + features + (env.compress ? '{' : ' {\n ') +
this.ruleset.toCSS(ctx, env).trim().replace(/\n/g, '\n ') +
(env.compress ? '}': '\n}\n');
},
eval: function (env) {
if (!env.mediaBlocks) {
env.mediaBlocks = [];
env.mediaPath = [];
}
var blockIndex = env.mediaBlocks.length;
env.mediaPath.push(this);
env.mediaBlocks.push(this);
var media = new(tree.Media)([], []);
media.features = this.features.eval(env);
env.frames.unshift(this.ruleset);
media.ruleset = this.ruleset.eval(env);
env.frames.shift();
env.mediaBlocks[blockIndex] = media;
env.mediaPath.pop();
return env.mediaPath.length === 0 ? media.evalTop(env) :
media.evalNested(env)
},
variable: function (name) { return tree.Ruleset.prototype.variable.call(this.ruleset, name) },
find: function () { return tree.Ruleset.prototype.find.apply(this.ruleset, arguments) },
rulesets: function () { return tree.Ruleset.prototype.rulesets.apply(this.ruleset) },
evalTop: function (env) {
var result = this;
// Render all dependent Media blocks.
if (env.mediaBlocks.length > 1) {
var el = new(tree.Element)('&', null, 0);
var selectors = [new(tree.Selector)([el])];
result = new(tree.Ruleset)(selectors, env.mediaBlocks);
result.multiMedia = true;
}
delete env.mediaBlocks;
delete env.mediaPath;
return result;
},
evalNested: function (env) {
var i, value,
path = env.mediaPath.concat([this]);
// Extract the media-query conditions separated with `,` (OR).
for (i = 0; i < path.length; i++) {
value = path[i].features instanceof tree.Value ?
path[i].features.value : path[i].features;
path[i] = Array.isArray(value) ? value : [value];
}
// Trace all permutations to generate the resulting media-query.
//
// (a, b and c) with nested (d, e) ->
// a and d
// a and e
// b and c and d
// b and c and e
this.features = new(tree.Value)(this.permute(path).map(function (path) {
path = path.map(function (fragment) {
return fragment.toCSS ? fragment : new(tree.Anonymous)(fragment);
});
for(i = path.length - 1; i > 0; i--) {
path.splice(i, 0, new(tree.Anonymous)("and"));
}
return new(tree.Expression)(path);
}));
// Fake a tree-node that doesn't output anything.
return new(tree.Ruleset)([], []);
},
permute: function (arr) {
if (arr.length === 0) {
return [];
} else if (arr.length === 1) {
return arr[0];
} else {
var result = [];
var rest = this.permute(arr.slice(1));
for (var i = 0; i < rest.length; i++) {
for (var j = 0; j < arr[0].length; j++) {
result.push([arr[0][j]].concat(rest[i]));
}
}
return result;
}
}
};
})(require('../tree'));

View File

@ -1,146 +0,0 @@
(function (tree) {
tree.mixin = {};
tree.mixin.Call = function (elements, args, index, filename, important) {
this.selector = new(tree.Selector)(elements);
this.arguments = args;
this.index = index;
this.filename = filename;
this.important = important;
};
tree.mixin.Call.prototype = {
eval: function (env) {
var mixins, args, rules = [], match = false;
for (var i = 0; i < env.frames.length; i++) {
if ((mixins = env.frames[i].find(this.selector)).length > 0) {
args = this.arguments && this.arguments.map(function (a) {
return { name: a.name, value: a.value.eval(env) };
});
for (var m = 0; m < mixins.length; m++) {
if (mixins[m].match(args, env)) {
try {
Array.prototype.push.apply(
rules, mixins[m].eval(env, this.arguments, this.important).rules);
match = true;
} catch (e) {
throw { message: e.message, index: this.index, filename: this.filename, stack: e.stack };
}
}
}
if (match) {
return rules;
} else {
throw { type: 'Runtime',
message: 'No matching definition was found for `' +
this.selector.toCSS().trim() + '(' +
this.arguments.map(function (a) {
return a.toCSS();
}).join(', ') + ")`",
index: this.index, filename: this.filename };
}
}
}
throw { type: 'Name',
message: this.selector.toCSS().trim() + " is undefined",
index: this.index, filename: this.filename };
}
};
tree.mixin.Definition = function (name, params, rules, condition, variadic) {
this.name = name;
this.selectors = [new(tree.Selector)([new(tree.Element)(null, name)])];
this.params = params;
this.condition = condition;
this.variadic = variadic;
this.arity = params.length;
this.rules = rules;
this._lookups = {};
this.required = params.reduce(function (count, p) {
if (!p.name || (p.name && !p.value)) { return count + 1 }
else { return count }
}, 0);
this.parent = tree.Ruleset.prototype;
this.frames = [];
};
tree.mixin.Definition.prototype = {
toCSS: function () { return "" },
variable: function (name) { return this.parent.variable.call(this, name) },
variables: function () { return this.parent.variables.call(this) },
find: function () { return this.parent.find.apply(this, arguments) },
rulesets: function () { return this.parent.rulesets.apply(this) },
evalParams: function (env, args) {
var frame = new(tree.Ruleset)(null, []), varargs, arg;
for (var i = 0, val, name; i < this.params.length; i++) {
arg = args && args[i]
if (arg && arg.name) {
frame.rules.unshift(new(tree.Rule)(arg.name, arg.value.eval(env)));
args.splice(i, 1);
i--;
continue;
}
if (name = this.params[i].name) {
if (this.params[i].variadic && args) {
varargs = [];
for (var j = i; j < args.length; j++) {
varargs.push(args[j].value.eval(env));
}
frame.rules.unshift(new(tree.Rule)(name, new(tree.Expression)(varargs).eval(env)));
} else if (val = (arg && arg.value) || this.params[i].value) {
frame.rules.unshift(new(tree.Rule)(name, val.eval(env)));
} else {
throw { type: 'Runtime', message: "wrong number of arguments for " + this.name +
' (' + args.length + ' for ' + this.arity + ')' };
}
}
}
return frame;
},
eval: function (env, args, important) {
var frame = this.evalParams(env, args), context, _arguments = [], rules, start;
for (var i = 0; i < Math.max(this.params.length, args && args.length); i++) {
_arguments.push((args[i] && args[i].value) || this.params[i].value);
}
frame.rules.unshift(new(tree.Rule)('@arguments', new(tree.Expression)(_arguments).eval(env)));
rules = important ?
this.rules.map(function (r) {
return new(tree.Rule)(r.name, r.value, '!important', r.index);
}) : this.rules.slice(0);
return new(tree.Ruleset)(null, rules).eval({
frames: [this, frame].concat(this.frames, env.frames)
});
},
match: function (args, env) {
var argsLength = (args && args.length) || 0, len, frame;
if (! this.variadic) {
if (argsLength < this.required) { return false }
if (argsLength > this.params.length) { return false }
if ((this.required > 0) && (argsLength > this.params.length)) { return false }
}
if (this.condition && !this.condition.eval({
frames: [this.evalParams(env, args)].concat(env.frames)
})) { return false }
len = Math.min(argsLength, this.arity);
for (var i = 0; i < len; i++) {
if (!this.params[i].name) {
if (args[i].value.eval(env).toCSS() != this.params[i].value.eval(env).toCSS()) {
return false;
}
}
}
return true;
}
};
})(require('../tree'));

View File

@ -1,32 +0,0 @@
(function (tree) {
tree.Operation = function (op, operands) {
this.op = op.trim();
this.operands = operands;
};
tree.Operation.prototype.eval = function (env) {
var a = this.operands[0].eval(env),
b = this.operands[1].eval(env),
temp;
if (a instanceof tree.Dimension && b instanceof tree.Color) {
if (this.op === '*' || this.op === '+') {
temp = b, b = a, a = temp;
} else {
throw { name: "OperationError",
message: "Can't substract or divide a color from a number" };
}
}
return a.operate(this.op, b);
};
tree.operate = function (op, a, b) {
switch (op) {
case '+': return a + b;
case '-': return a - b;
case '*': return a * b;
case '/': return a / b;
}
};
})(require('../tree'));

View File

@ -1,16 +0,0 @@
(function (tree) {
tree.Paren = function (node) {
this.value = node;
};
tree.Paren.prototype = {
toCSS: function (env) {
return '(' + this.value.toCSS(env) + ')';
},
eval: function (env) {
return new(tree.Paren)(this.value.eval(env));
}
};
})(require('../tree'));

View File

@ -1,29 +0,0 @@
(function (tree) {
tree.Quoted = function (str, content, escaped, i) {
this.escaped = escaped;
this.value = content || '';
this.quote = str.charAt(0);
this.index = i;
};
tree.Quoted.prototype = {
toCSS: function () {
if (this.escaped) {
return this.value;
} else {
return this.quote + this.value + this.quote;
}
},
eval: function (env) {
var that = this;
var value = this.value.replace(/`([^`]+)`/g, function (_, exp) {
return new(tree.JavaScript)(exp, that.index, true).eval(env).value;
}).replace(/@\{([\w-]+)\}/g, function (_, name) {
var v = new(tree.Variable)('@' + name, that.index).eval(env);
return ('value' in v) ? v.value : v.toCSS();
});
return new(tree.Quoted)(this.quote + value + this.quote, value, this.escaped, this.index);
}
};
})(require('../tree'));

View File

@ -1,42 +0,0 @@
(function (tree) {
tree.Rule = function (name, value, important, index, inline) {
this.name = name;
this.value = (value instanceof tree.Value) ? value : new(tree.Value)([value]);
this.important = important ? ' ' + important.trim() : '';
this.index = index;
this.inline = inline || false;
if (name.charAt(0) === '@') {
this.variable = true;
} else { this.variable = false }
};
tree.Rule.prototype.toCSS = function (env) {
if (this.variable) { return "" }
else {
return this.name + (env.compress ? ':' : ': ') +
this.value.toCSS(env) +
this.important + (this.inline ? "" : ";");
}
};
tree.Rule.prototype.eval = function (context) {
return new(tree.Rule)(this.name,
this.value.eval(context),
this.important,
this.index, this.inline);
};
tree.Shorthand = function (a, b) {
this.a = a;
this.b = b;
};
tree.Shorthand.prototype = {
toCSS: function (env) {
return this.a.toCSS(env) + "/" + this.b.toCSS(env);
},
eval: function () { return this }
};
})(require('../tree'));

View File

@ -1,225 +0,0 @@
(function (tree) {
tree.Ruleset = function (selectors, rules, strictImports) {
this.selectors = selectors;
this.rules = rules;
this._lookups = {};
this.strictImports = strictImports;
};
tree.Ruleset.prototype = {
eval: function (env) {
var selectors = this.selectors && this.selectors.map(function (s) { return s.eval(env) });
var ruleset = new(tree.Ruleset)(selectors, this.rules.slice(0), this.strictImports);
ruleset.root = this.root;
ruleset.allowImports = this.allowImports;
// push the current ruleset to the frames stack
env.frames.unshift(ruleset);
// Evaluate imports
if (ruleset.root || ruleset.allowImports || !ruleset.strictImports) {
for (var i = 0; i < ruleset.rules.length; i++) {
if (ruleset.rules[i] instanceof tree.Import) {
Array.prototype.splice
.apply(ruleset.rules, [i, 1].concat(ruleset.rules[i].eval(env)));
}
}
}
// Store the frames around mixin definitions,
// so they can be evaluated like closures when the time comes.
for (var i = 0; i < ruleset.rules.length; i++) {
if (ruleset.rules[i] instanceof tree.mixin.Definition) {
ruleset.rules[i].frames = env.frames.slice(0);
}
}
// Evaluate mixin calls.
for (var i = 0; i < ruleset.rules.length; i++) {
if (ruleset.rules[i] instanceof tree.mixin.Call) {
Array.prototype.splice
.apply(ruleset.rules, [i, 1].concat(ruleset.rules[i].eval(env)));
}
}
// Evaluate everything else
for (var i = 0, rule; i < ruleset.rules.length; i++) {
rule = ruleset.rules[i];
if (! (rule instanceof tree.mixin.Definition)) {
ruleset.rules[i] = rule.eval ? rule.eval(env) : rule;
}
}
// Pop the stack
env.frames.shift();
return ruleset;
},
match: function (args) {
return !args || args.length === 0;
},
variables: function () {
if (this._variables) { return this._variables }
else {
return this._variables = this.rules.reduce(function (hash, r) {
if (r instanceof tree.Rule && r.variable === true) {
hash[r.name] = r;
}
return hash;
}, {});
}
},
variable: function (name) {
return this.variables()[name];
},
rulesets: function () {
if (this._rulesets) { return this._rulesets }
else {
return this._rulesets = this.rules.filter(function (r) {
return (r instanceof tree.Ruleset) || (r instanceof tree.mixin.Definition);
});
}
},
find: function (selector, self) {
self = self || this;
var rules = [], rule, match,
key = selector.toCSS();
if (key in this._lookups) { return this._lookups[key] }
this.rulesets().forEach(function (rule) {
if (rule !== self) {
for (var j = 0; j < rule.selectors.length; j++) {
if (match = selector.match(rule.selectors[j])) {
if (selector.elements.length > rule.selectors[j].elements.length) {
Array.prototype.push.apply(rules, rule.find(
new(tree.Selector)(selector.elements.slice(1)), self));
} else {
rules.push(rule);
}
break;
}
}
}
});
return this._lookups[key] = rules;
},
//
// Entry point for code generation
//
// `context` holds an array of arrays.
//
toCSS: function (context, env) {
var css = [], // The CSS output
rules = [], // node.Rule instances
_rules = [], //
rulesets = [], // node.Ruleset instances
paths = [], // Current selectors
selector, // The fully rendered selector
rule;
if (! this.root) {
if (context.length === 0) {
paths = this.selectors.map(function (s) { return [s] });
} else {
this.joinSelectors(paths, context, this.selectors);
}
}
// Compile rules and rulesets
for (var i = 0; i < this.rules.length; i++) {
rule = this.rules[i];
if (rule.rules || (rule instanceof tree.Directive) || (rule instanceof tree.Media)) {
rulesets.push(rule.toCSS(paths, env));
} else if (rule instanceof tree.Comment) {
if (!rule.silent) {
if (this.root) {
rulesets.push(rule.toCSS(env));
} else {
rules.push(rule.toCSS(env));
}
}
} else {
if (rule.toCSS && !rule.variable) {
rules.push(rule.toCSS(env));
} else if (rule.value && !rule.variable) {
rules.push(rule.value.toString());
}
}
}
rulesets = rulesets.join('');
// If this is the root node, we don't render
// a selector, or {}.
// Otherwise, only output if this ruleset has rules.
if (this.root) {
css.push(rules.join(env.compress ? '' : '\n'));
} else {
if (rules.length > 0) {
selector = paths.map(function (p) {
return p.map(function (s) {
return s.toCSS(env);
}).join('').trim();
}).join(env.compress ? ',' : ',\n');
// Remove duplicates
for (var i = rules.length - 1; i >= 0; i--) {
if (_rules.indexOf(rules[i]) === -1) {
_rules.unshift(rules[i]);
}
}
rules = _rules;
css.push(selector,
(env.compress ? '{' : ' {\n ') +
rules.join(env.compress ? '' : '\n ') +
(env.compress ? '}' : '\n}\n'));
}
}
css.push(rulesets);
return css.join('') + (env.compress ? '\n' : '');
},
joinSelectors: function (paths, context, selectors) {
for (var s = 0; s < selectors.length; s++) {
this.joinSelector(paths, context, selectors[s]);
}
},
joinSelector: function (paths, context, selector) {
var before = [], after = [], beforeElements = [],
afterElements = [], hasParentSelector = false, el;
for (var i = 0; i < selector.elements.length; i++) {
el = selector.elements[i];
if (el.combinator.value.charAt(0) === '&') {
hasParentSelector = true;
}
if (hasParentSelector) afterElements.push(el);
else beforeElements.push(el);
}
if (! hasParentSelector) {
afterElements = beforeElements;
beforeElements = [];
}
if (beforeElements.length > 0) {
before.push(new(tree.Selector)(beforeElements));
}
if (afterElements.length > 0) {
after.push(new(tree.Selector)(afterElements));
}
for (var c = 0; c < context.length; c++) {
paths.push(before.concat(context[c]).concat(after));
}
}
};
})(require('../tree'));

View File

@ -1,42 +0,0 @@
(function (tree) {
tree.Selector = function (elements) {
this.elements = elements;
if (this.elements[0].combinator.value === "") {
this.elements[0].combinator.value = ' ';
}
};
tree.Selector.prototype.match = function (other) {
var len = this.elements.length,
olen = other.elements.length,
max = Math.min(len, olen);
if (len < olen) {
return false;
} else {
for (var i = 0; i < max; i++) {
if (this.elements[i].value !== other.elements[i].value) {
return false;
}
}
}
return true;
};
tree.Selector.prototype.eval = function (env) {
return new(tree.Selector)(this.elements.map(function (e) {
return e.eval(env);
}));
};
tree.Selector.prototype.toCSS = function (env) {
if (this._css) { return this._css }
return this._css = this.elements.map(function (e) {
if (typeof(e) === 'string') {
return ' ' + e.trim();
} else {
return e.toCSS(env);
}
}).join('');
};
})(require('../tree'));

View File

@ -1,25 +0,0 @@
(function (tree) {
tree.URL = function (val, paths) {
if (val.data) {
this.attrs = val;
} else {
// Add the base path if the URL is relative and we are in the browser
if (typeof(window) !== 'undefined' && !/^(?:https?:\/\/|file:\/\/|data:|\/)/.test(val.value) && paths.length > 0) {
val.value = paths[0] + (val.value.charAt(0) === '/' ? val.value.slice(1) : val.value);
}
this.value = val;
this.paths = paths;
}
};
tree.URL.prototype = {
toCSS: function () {
return "url(" + (this.attrs ? 'data:' + this.attrs.mime + this.attrs.charset + this.attrs.base64 + this.attrs.data
: this.value.toCSS()) + ")";
},
eval: function (ctx) {
return this.attrs ? this : new(tree.URL)(this.value.eval(ctx), this.paths);
}
};
})(require('../tree'));

View File

@ -1,24 +0,0 @@
(function (tree) {
tree.Value = function (value) {
this.value = value;
this.is = 'value';
};
tree.Value.prototype = {
eval: function (env) {
if (this.value.length === 1) {
return this.value[0].eval(env);
} else {
return new(tree.Value)(this.value.map(function (v) {
return v.eval(env);
}));
}
},
toCSS: function (env) {
return this.value.map(function (e) {
return e.toCSS(env);
}).join(env.compress ? ',' : ', ');
}
};
})(require('../tree'));

View File

@ -1,26 +0,0 @@
(function (tree) {
tree.Variable = function (name, index, file) { this.name = name, this.index = index, this.file = file };
tree.Variable.prototype = {
eval: function (env) {
var variable, v, name = this.name;
if (name.indexOf('@@') == 0) {
name = '@' + new(tree.Variable)(name.slice(1)).eval(env).value;
}
if (variable = tree.find(env.frames, function (frame) {
if (v = frame.variable(name)) {
return v.value.eval(env);
}
})) { return variable }
else {
throw { type: 'Name',
message: "variable " + name + " is undefined",
filename: this.file,
index: this.index };
}
}
};
})(require('../tree'));

View File

@ -1,153 +0,0 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Horizon.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Horizon.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/Horizon"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Horizon"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."

View File

@ -1,416 +0,0 @@
/**
* Sphinx stylesheet -- basic theme
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
/* -- main layout ----------------------------------------------------------- */
div.clearer {
clear: both;
}
/* -- relbar ---------------------------------------------------------------- */
div.related {
width: 100%;
font-size: 90%;
}
div.related h3 {
display: none;
}
div.related ul {
margin: 0;
padding: 0 0 0 10px;
list-style: none;
}
div.related li {
display: inline;
}
div.related li.right {
float: right;
margin-right: 5px;
}
/* -- sidebar --------------------------------------------------------------- */
div.sphinxsidebarwrapper {
padding: 10px 5px 0 10px;
}
div.sphinxsidebar {
float: left;
width: 230px;
margin-left: -100%;
font-size: 90%;
}
div.sphinxsidebar ul {
list-style: none;
}
div.sphinxsidebar ul ul,
div.sphinxsidebar ul.want-points {
margin-left: 20px;
list-style: square;
}
div.sphinxsidebar ul ul {
margin-top: 0;
margin-bottom: 0;
}
div.sphinxsidebar form {
margin-top: 10px;
}
div.sphinxsidebar input {
border: 1px solid #98dbcc;
font-family: sans-serif;
font-size: 1em;
}
img {
border: 0;
}
/* -- search page ----------------------------------------------------------- */
ul.search {
margin: 10px 0 0 20px;
padding: 0;
}
ul.search li {
padding: 5px 0 5px 20px;
background-image: url(file.png);
background-repeat: no-repeat;
background-position: 0 7px;
}
ul.search li a {
font-weight: bold;
}
ul.search li div.context {
color: #888;
margin: 2px 0 0 30px;
text-align: left;
}
ul.keywordmatches li.goodmatch a {
font-weight: bold;
}
/* -- index page ------------------------------------------------------------ */
table.contentstable {
width: 90%;
}
table.contentstable p.biglink {
line-height: 150%;
}
a.biglink {
font-size: 1.3em;
}
span.linkdescr {
font-style: italic;
padding-top: 5px;
font-size: 90%;
}
/* -- general index --------------------------------------------------------- */
table.indextable td {
text-align: left;
vertical-align: top;
}
table.indextable dl, table.indextable dd {
margin-top: 0;
margin-bottom: 0;
}
table.indextable tr.pcap {
height: 10px;
}
table.indextable tr.cap {
margin-top: 10px;
background-color: #f2f2f2;
}
img.toggler {
margin-right: 3px;
margin-top: 3px;
cursor: pointer;
}
/* -- general body styles --------------------------------------------------- */
a.headerlink {
visibility: hidden;
}
h1:hover > a.headerlink,
h2:hover > a.headerlink,
h3:hover > a.headerlink,
h4:hover > a.headerlink,
h5:hover > a.headerlink,
h6:hover > a.headerlink,
dt:hover > a.headerlink {
visibility: visible;
}
div.body p.caption {
text-align: inherit;
}
div.body td {
text-align: left;
}
.field-list ul {
padding-left: 1em;
}
.first {
}
p.rubric {
margin-top: 30px;
font-weight: bold;
}
/* -- sidebars -------------------------------------------------------------- */
div.sidebar {
margin: 0 0 0.5em 1em;
border: 1px solid #ddb;
padding: 7px 7px 0 7px;
background-color: #ffe;
width: 40%;
float: right;
}
p.sidebar-title {
font-weight: bold;
}
/* -- topics ---------------------------------------------------------------- */
div.topic {
border: 1px solid #ccc;
padding: 7px 7px 0 7px;
margin: 10px 0 10px 0;
}
p.topic-title {
font-size: 1.1em;
font-weight: bold;
margin-top: 10px;
}
/* -- admonitions ----------------------------------------------------------- */
div.admonition {
margin-top: 10px;
margin-bottom: 10px;
padding: 7px;
}
div.admonition dt {
font-weight: bold;
}
div.admonition dl {
margin-bottom: 0;
}
p.admonition-title {
margin: 0px 10px 5px 0px;
font-weight: bold;
}
div.body p.centered {
text-align: center;
margin-top: 25px;
}
/* -- tables ---------------------------------------------------------------- */
table.docutils {
border: 0;
border-collapse: collapse;
}
table.docutils td, table.docutils th {
padding: 1px 8px 1px 0;
border-top: 0;
border-left: 0;
border-right: 0;
border-bottom: 1px solid #aaa;
}
table.field-list td, table.field-list th {
border: 0 !important;
}
table.footnote td, table.footnote th {
border: 0 !important;
}
th {
text-align: left;
padding-right: 5px;
}
/* -- other body styles ----------------------------------------------------- */
dl {
margin-bottom: 15px;
}
dd p {
margin-top: 0px;
}
dd ul, dd table {
margin-bottom: 10px;
}
dd {
margin-top: 3px;
margin-bottom: 10px;
margin-left: 30px;
}
dt:target, .highlight {
background-color: #fbe54e;
}
dl.glossary dt {
font-weight: bold;
font-size: 1.1em;
}
.field-list ul {
margin: 0;
padding-left: 1em;
}
.field-list p {
margin: 0;
}
.refcount {
color: #060;
}
.optional {
font-size: 1.3em;
}
.versionmodified {
font-style: italic;
}
.system-message {
background-color: #fda;
padding: 5px;
border: 3px solid red;
}
.footnote:target {
background-color: #ffa
}
.line-block {
display: block;
margin-top: 1em;
margin-bottom: 1em;
}
.line-block .line-block {
margin-top: 0;
margin-bottom: 0;
margin-left: 1.5em;
}
/* -- code displays --------------------------------------------------------- */
pre {
overflow: auto;
}
td.linenos pre {
padding: 5px 0px;
border: 0;
background-color: transparent;
color: #aaa;
}
table.highlighttable {
margin-left: 0.5em;
}
table.highlighttable td {
padding: 0 0.5em 0 0.5em;
}
tt.descname {
background-color: transparent;
font-weight: bold;
font-size: 1.2em;
}
tt.descclassname {
background-color: transparent;
}
tt.xref, a tt {
background-color: transparent;
font-weight: bold;
}
h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt {
background-color: transparent;
}
/* -- math display ---------------------------------------------------------- */
img.math {
vertical-align: middle;
}
div.body div.math p {
text-align: center;
}
span.eqno {
float: right;
}
/* -- printout stylesheet --------------------------------------------------- */
@media print {
div.document,
div.documentwrapper,
div.bodywrapper {
margin: 0 !important;
width: 100%;
}
div.sphinxsidebar,
div.related,
div.footer,
#top-link {
display: none;
}
}

View File

@ -1,230 +0,0 @@
/**
* Sphinx stylesheet -- default theme
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
@import url("basic.css");
/* -- page layout ----------------------------------------------------------- */
body {
font-family: sans-serif;
font-size: 100%;
background-color: #11303d;
color: #000;
margin: 0;
padding: 0;
}
div.document {
background-color: #1c4e63;
}
div.documentwrapper {
float: left;
width: 100%;
}
div.bodywrapper {
margin: 0 0 0 230px;
}
div.body {
background-color: #ffffff;
color: #000000;
padding: 0 20px 30px 20px;
}
div.footer {
color: #ffffff;
width: 100%;
padding: 9px 0 9px 0;
text-align: center;
font-size: 75%;
}
div.footer a {
color: #ffffff;
text-decoration: underline;
}
div.related {
background-color: #133f52;
line-height: 30px;
color: #ffffff;
}
div.related a {
color: #ffffff;
}
div.sphinxsidebar {
}
div.sphinxsidebar h3 {
font-family: 'Trebuchet MS', sans-serif;
color: #ffffff;
font-size: 1.4em;
font-weight: normal;
margin: 0;
padding: 0;
}
div.sphinxsidebar h3 a {
color: #ffffff;
}
div.sphinxsidebar h4 {
font-family: 'Trebuchet MS', sans-serif;
color: #ffffff;
font-size: 1.3em;
font-weight: normal;
margin: 5px 0 0 0;
padding: 0;
}
div.sphinxsidebar p {
color: #ffffff;
}
div.sphinxsidebar p.topless {
margin: 5px 10px 10px 10px;
}
div.sphinxsidebar ul {
margin: 10px;
padding: 0;
color: #ffffff;
}
div.sphinxsidebar a {
color: #98dbcc;
}
div.sphinxsidebar input {
border: 1px solid #98dbcc;
font-family: sans-serif;
font-size: 1em;
}
/* -- body styles ----------------------------------------------------------- */
a {
color: #355f7c;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
div.body p, div.body dd, div.body li {
text-align: left;
line-height: 130%;
}
div.body h1,
div.body h2,
div.body h3,
div.body h4,
div.body h5,
div.body h6 {
font-family: 'Trebuchet MS', sans-serif;
background-color: #f2f2f2;
font-weight: normal;
color: #20435c;
border-bottom: 1px solid #ccc;
margin: 20px -20px 10px -20px;
padding: 3px 0 3px 10px;
}
div.body h1 { margin-top: 0; font-size: 200%; }
div.body h2 { font-size: 160%; }
div.body h3 { font-size: 140%; }
div.body h4 { font-size: 120%; }
div.body h5 { font-size: 110%; }
div.body h6 { font-size: 100%; }
a.headerlink {
color: #c60f0f;
font-size: 0.8em;
padding: 0 4px 0 4px;
text-decoration: none;
}
a.headerlink:hover {
background-color: #c60f0f;
color: white;
}
div.body p, div.body dd, div.body li {
text-align: left;
line-height: 130%;
}
div.admonition p.admonition-title + p {
display: inline;
}
div.admonition p {
margin-bottom: 5px;
}
div.admonition pre {
margin-bottom: 5px;
}
div.admonition ul, div.admonition ol {
margin-bottom: 5px;
}
div.note {
background-color: #eee;
border: 1px solid #ccc;
}
div.seealso {
background-color: #ffc;
border: 1px solid #ff6;
}
div.topic {
background-color: #eee;
}
div.warning {
background-color: #ffe4e4;
border: 1px solid #f66;
}
p.admonition-title {
display: inline;
}
p.admonition-title:after {
content: ":";
}
pre {
padding: 5px;
background-color: #eeffcc;
color: #333333;
line-height: 120%;
border: 1px solid #ac9;
border-left: none;
border-right: none;
}
tt {
background-color: #ecf0f3;
padding: 0 1px 0 1px;
font-size: 0.95em;
}
.warning tt {
background: #efc2c2;
}
.note tt {
background: #d6d6d6;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -1,154 +0,0 @@
(function($) {
$.fn.tweet = function(o){
var s = {
username: ["seaofclouds"], // [string] required, unless you want to display our tweets. :) it can be an array, just do ["username1","username2","etc"]
list: null, //[string] optional name of list belonging to username
avatar_size: null, // [integer] height and width of avatar if displayed (48px max)
count: 3, // [integer] how many tweets to display?
intro_text: null, // [string] do you want text BEFORE your your tweets?
outro_text: null, // [string] do you want text AFTER your tweets?
join_text: null, // [string] optional text in between date and tweet, try setting to "auto"
auto_join_text_default: "i said,", // [string] auto text for non verb: "i said" bullocks
auto_join_text_ed: "i", // [string] auto text for past tense: "i" surfed
auto_join_text_ing: "i am", // [string] auto tense for present tense: "i was" surfing
auto_join_text_reply: "i replied to", // [string] auto tense for replies: "i replied to" @someone "with"
auto_join_text_url: "i was looking at", // [string] auto tense for urls: "i was looking at" http:...
loading_text: null, // [string] optional loading text, displayed while tweets load
query: null // [string] optional search query
};
if(o) $.extend(s, o);
$.fn.extend({
linkUrl: function() {
var returning = [];
var regexp = /((ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?)/gi;
this.each(function() {
returning.push(this.replace(regexp,"<a href=\"$1\">$1</a>"));
});
return $(returning);
},
linkUser: function() {
var returning = [];
var regexp = /[\@]+([A-Za-z0-9-_]+)/gi;
this.each(function() {
returning.push(this.replace(regexp,"<a href=\"http://twitter.com/$1\">@$1</a>"));
});
return $(returning);
},
linkHash: function() {
var returning = [];
var regexp = / [\#]+([A-Za-z0-9-_]+)/gi;
this.each(function() {
returning.push(this.replace(regexp, ' <a href="http://search.twitter.com/search?q=&tag=$1&lang=all&from='+s.username.join("%2BOR%2B")+'">#$1</a>'));
});
return $(returning);
},
capAwesome: function() {
var returning = [];
this.each(function() {
returning.push(this.replace(/\b(awesome)\b/gi, '<span class="awesome">$1</span>'));
});
return $(returning);
},
capEpic: function() {
var returning = [];
this.each(function() {
returning.push(this.replace(/\b(epic)\b/gi, '<span class="epic">$1</span>'));
});
return $(returning);
},
makeHeart: function() {
var returning = [];
this.each(function() {
returning.push(this.replace(/(&lt;)+[3]/gi, "<tt class='heart'>&#x2665;</tt>"));
});
return $(returning);
}
});
function relative_time(time_value) {
var parsed_date = Date.parse(time_value);
var relative_to = (arguments.length > 1) ? arguments[1] : new Date();
var delta = parseInt((relative_to.getTime() - parsed_date) / 1000);
var pluralize = function (singular, n) {
return '' + n + ' ' + singular + (n == 1 ? '' : 's');
};
if(delta < 60) {
return 'less than a minute ago';
} else if(delta < (45*60)) {
return 'about ' + pluralize("minute", parseInt(delta / 60)) + ' ago';
} else if(delta < (24*60*60)) {
return 'about ' + pluralize("hour", parseInt(delta / 3600)) + ' ago';
} else {
return 'about ' + pluralize("day", parseInt(delta / 86400)) + ' ago';
}
}
function build_url() {
var proto = ('https:' == document.location.protocol ? 'https:' : 'http:');
if (s.list) {
return proto+"//api.twitter.com/1/"+s.username[0]+"/lists/"+s.list+"/statuses.json?per_page="+s.count+"&callback=?";
} else if (s.query == null && s.username.length == 1) {
return proto+'//twitter.com/status/user_timeline/'+s.username[0]+'.json?count='+s.count+'&callback=?';
} else {
var query = (s.query || 'from:'+s.username.join('%20OR%20from:'));
return proto+'//search.twitter.com/search.json?&q='+query+'&rpp='+s.count+'&callback=?';
}
}
return this.each(function(){
var list = $('<ul class="tweet_list">').appendTo(this);
var intro = '<p class="tweet_intro">'+s.intro_text+'</p>';
var outro = '<p class="tweet_outro">'+s.outro_text+'</p>';
var loading = $('<p class="loading">'+s.loading_text+'</p>');
if(typeof(s.username) == "string"){
s.username = [s.username];
}
if (s.loading_text) $(this).append(loading);
$.getJSON(build_url(), function(data){
if (s.loading_text) loading.remove();
if (s.intro_text) list.before(intro);
$.each((data.results || data), function(i,item){
// auto join text based on verb tense and content
if (s.join_text == "auto") {
if (item.text.match(/^(@([A-Za-z0-9-_]+)) .*/i)) {
var join_text = s.auto_join_text_reply;
} else if (item.text.match(/(^\w+:\/\/[A-Za-z0-9-_]+\.[A-Za-z0-9-_:%&\?\/.=]+) .*/i)) {
var join_text = s.auto_join_text_url;
} else if (item.text.match(/^((\w+ed)|just) .*/im)) {
var join_text = s.auto_join_text_ed;
} else if (item.text.match(/^(\w*ing) .*/i)) {
var join_text = s.auto_join_text_ing;
} else {
var join_text = s.auto_join_text_default;
}
} else {
var join_text = s.join_text;
};
var from_user = item.from_user || item.user.screen_name;
var profile_image_url = item.profile_image_url || item.user.profile_image_url;
var join_template = '<span class="tweet_join"> '+join_text+' </span>';
var join = ((s.join_text) ? join_template : ' ');
var avatar_template = '<a class="tweet_avatar" href="http://twitter.com/'+from_user+'"><img src="'+profile_image_url+'" height="'+s.avatar_size+'" width="'+s.avatar_size+'" alt="'+from_user+'\'s avatar" title="'+from_user+'\'s avatar" border="0"/></a>';
var avatar = (s.avatar_size ? avatar_template : '');
var date = '<a href="http://twitter.com/'+from_user+'/statuses/'+item.id+'" title="view tweet on twitter">'+relative_time(item.created_at)+'</a>';
var text = '<span class="tweet_text">' +$([item.text]).linkUrl().linkUser().linkHash().makeHeart().capAwesome().capEpic()[0]+ '</span>';
// until we create a template option, arrange the items below to alter a tweet's display.
list.append('<li>' + avatar + date + join + text + '</li>');
list.children('li:first').addClass('tweet_first');
list.children('li:odd').addClass('tweet_even');
list.children('li:even').addClass('tweet_odd');
});
if (s.outro_text) list.after(outro);
});
});
};
})(jQuery);

View File

@ -1,245 +0,0 @@
/*
* nature.css_t
* ~~~~~~~~~~~~
*
* Sphinx stylesheet -- nature theme.
*
* :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
@import url("basic.css");
/* -- page layout ----------------------------------------------------------- */
body {
font-family: Arial, sans-serif;
font-size: 100%;
background-color: #111;
color: #555;
margin: 0;
padding: 0;
}
div.documentwrapper {
float: left;
width: 100%;
}
div.bodywrapper {
margin: 0 0 0 {{ theme_sidebarwidth|toint }}px;
}
hr {
border: 1px solid #B1B4B6;
}
div.document {
background-color: #eee;
}
div.body {
background-color: #ffffff;
color: #3E4349;
padding: 0 30px 30px 30px;
font-size: 0.9em;
}
div.footer {
color: #555;
width: 100%;
padding: 13px 0;
text-align: center;
font-size: 75%;
}
div.footer a {
color: #444;
text-decoration: underline;
}
div.related {
background-color: #6BA81E;
line-height: 32px;
color: #fff;
text-shadow: 0px 1px 0 #444;
font-size: 0.9em;
}
div.related a {
color: #E2F3CC;
}
div.sphinxsidebar {
font-size: 0.75em;
line-height: 1.5em;
}
div.sphinxsidebarwrapper{
padding: 20px 0;
}
div.sphinxsidebar h3,
div.sphinxsidebar h4 {
font-family: Arial, sans-serif;
color: #222;
font-size: 1.2em;
font-weight: normal;
margin: 0;
padding: 5px 10px;
background-color: #ddd;
text-shadow: 1px 1px 0 white
}
div.sphinxsidebar h4{
font-size: 1.1em;
}
div.sphinxsidebar h3 a {
color: #444;
}
div.sphinxsidebar p {
color: #888;
padding: 5px 20px;
}
div.sphinxsidebar p.topless {
}
div.sphinxsidebar ul {
margin: 10px 20px;
padding: 0;
color: #000;
}
div.sphinxsidebar a {
color: #444;
}
div.sphinxsidebar input {
border: 1px solid #ccc;
font-family: sans-serif;
font-size: 1em;
}
div.sphinxsidebar input[type=text]{
margin-left: 20px;
}
/* -- body styles ----------------------------------------------------------- */
a {
color: #005B81;
text-decoration: none;
}
a:hover {
color: #E32E00;
text-decoration: underline;
}
div.body h1,
div.body h2,
div.body h3,
div.body h4,
div.body h5,
div.body h6 {
font-family: Arial, sans-serif;
background-color: #BED4EB;
font-weight: normal;
color: #212224;
margin: 30px 0px 10px 0px;
padding: 5px 0 5px 10px;
text-shadow: 0px 1px 0 white
}
div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; }
div.body h2 { font-size: 150%; background-color: #C8D5E3; }
div.body h3 { font-size: 120%; background-color: #D8DEE3; }
div.body h4 { font-size: 110%; background-color: #D8DEE3; }
div.body h5 { font-size: 100%; background-color: #D8DEE3; }
div.body h6 { font-size: 100%; background-color: #D8DEE3; }
a.headerlink {
color: #c60f0f;
font-size: 0.8em;
padding: 0 4px 0 4px;
text-decoration: none;
}
a.headerlink:hover {
background-color: #c60f0f;
color: white;
}
div.body p, div.body dd, div.body li {
line-height: 1.5em;
}
div.admonition p.admonition-title + p {
display: inline;
}
div.highlight{
background-color: white;
}
div.note {
background-color: #eee;
border: 1px solid #ccc;
}
div.seealso {
background-color: #ffc;
border: 1px solid #ff6;
}
div.topic {
background-color: #eee;
}
div.warning {
background-color: #ffe4e4;
border: 1px solid #f66;
}
p.admonition-title {
display: inline;
}
p.admonition-title:after {
content: ":";
}
pre {
padding: 10px;
background-color: White;
color: #222;
line-height: 1.2em;
border: 1px solid #C6C9CB;
font-size: 1.1em;
margin: 1.5em 0 1.5em 0;
-webkit-box-shadow: 1px 1px 1px #d8d8d8;
-moz-box-shadow: 1px 1px 1px #d8d8d8;
}
tt {
background-color: #ecf0f3;
color: #222;
/* padding: 1px 2px; */
font-size: 1.1em;
font-family: monospace;
}
.viewcode-back {
font-family: Arial, sans-serif;
}
div.viewcode-block:target {
background-color: #f4debf;
border-top: 1px solid #ac9;
border-bottom: 1px solid #ac9;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -1,94 +0,0 @@
body {
background: #fff url(../_static/header_bg.jpg) top left no-repeat;
}
#header {
width: 950px;
margin: 0 auto;
height: 102px;
}
#header h1#logo {
background: url(../_static/openstack_logo.png) top left no-repeat;
display: block;
float: left;
text-indent: -9999px;
width: 175px;
height: 55px;
}
#navigation {
background: url(../_static/header-line.gif) repeat-x 0 bottom;
display: block;
float: left;
margin: 27px 0 0 25px;
padding: 0;
}
#navigation li{
float: left;
display: block;
margin-right: 25px;
}
#navigation li a {
display: block;
font-weight: normal;
text-decoration: none;
background-position: 50% 0;
padding: 20px 0 5px;
color: #353535;
font-size: 14px;
}
#navigation li a.current, #navigation li a.section {
border-bottom: 3px solid #cf2f19;
color: #cf2f19;
}
div.related {
background-color: #cde2f8;
border: 1px solid #b0d3f8;
}
div.related a {
color: #4078ba;
text-shadow: none;
}
div.sphinxsidebarwrapper {
padding-top: 0;
}
pre {
color: #555;
}
div.documentwrapper h1, div.documentwrapper h2, div.documentwrapper h3, div.documentwrapper h4, div.documentwrapper h5, div.documentwrapper h6 {
font-family: 'PT Sans', sans-serif !important;
color: #264D69;
border-bottom: 1px dotted #C5E2EA;
padding: 0;
background: none;
padding-bottom: 5px;
}
div.documentwrapper h3 {
color: #CF2F19;
}
a.headerlink {
color: #fff !important;
margin-left: 5px;
background: #CF2F19 !important;
}
div.body {
margin-top: -25px;
margin-left: 230px;
}
div.document {
width: 960px;
margin: 0 auto;
}

View File

@ -1,83 +0,0 @@
{% extends "basic/layout.html" %}
{% set css_files = css_files + ['_static/tweaks.css'] %}
{% set script_files = script_files + ['_static/jquery.tweet.js'] %}
{%- macro sidebar() %}
{%- if not embedded %}{% if not theme_nosidebar|tobool %}
<div class="sphinxsidebar">
<div class="sphinxsidebarwrapper">
{%- block sidebarlogo %}
{%- if logo %}
<p class="logo"><a href="{{ pathto(master_doc) }}">
<img class="logo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/>
</a></p>
{%- endif %}
{%- endblock %}
{%- block sidebartoc %}
{%- if display_toc %}
<h3><a href="{{ pathto(master_doc) }}">{{ _('Table Of Contents') }}</a></h3>
{{ toc }}
{%- endif %}
{%- endblock %}
{%- block sidebarrel %}
{%- if prev %}
<h4>{{ _('Previous topic') }}</h4>
<p class="topless"><a href="{{ prev.link|e }}"
title="{{ _('previous chapter') }}">{{ prev.title }}</a></p>
{%- endif %}
{%- if next %}
<h4>{{ _('Next topic') }}</h4>
<p class="topless"><a href="{{ next.link|e }}"
title="{{ _('next chapter') }}">{{ next.title }}</a></p>
{%- endif %}
{%- endblock %}
{%- block sidebarsourcelink %}
{%- if show_source and has_source and sourcename %}
<h3>{{ _('This Page') }}</h3>
<ul class="this-page-menu">
<li><a href="{{ pathto('_sources/' + sourcename, true)|e }}"
rel="nofollow">{{ _('Show Source') }}</a></li>
</ul>
{%- endif %}
{%- endblock %}
{%- if customsidebar %}
{% include customsidebar %}
{%- endif %}
{%- block sidebarsearch %}
{%- if pagename != "search" %}
<div id="searchbox" style="display: none">
<h3>{{ _('Quick search') }}</h3>
<form class="search" action="{{ pathto('search') }}" method="get">
<input type="text" name="q" size="18" />
<input type="submit" value="{{ _('Go') }}" />
<input type="hidden" name="check_keywords" value="yes" />
<input type="hidden" name="area" value="default" />
</form>
<p class="searchtip" style="font-size: 90%">
{{ _('Enter search terms or a module, class or function name.') }}
</p>
</div>
<script type="text/javascript">$('#searchbox').show(0);</script>
{%- endif %}
{%- endblock %}
</div>
</div>
{%- endif %}{% endif %}
{%- endmacro %}
{% block relbar1 %}{% endblock relbar1 %}
{% block header %}
<div id="header">
<h1 id="logo"><a href="http://www.openstack.org/">OpenStack</a></h1>
<ul id="navigation">
<li><a href="http://www.openstack.org/" title="Go to the Home page" class="link">Home</a></li>
<li><a href="http://www.openstack.org/projects/" title="Go to the OpenStack Projects page">Projects</a></li>
<li><a href="http://www.openstack.org/user-stories/" title="Go to the User Stories page" class="link">User Stories</a></li>
<li><a href="http://www.openstack.org/community/" title="Go to the Community page" class="link">Community</a></li>
<li><a href="http://www.openstack.org/blog/" title="Go to the OpenStack Blog">Blog</a></li>
<li><a href="http://wiki.openstack.org/" title="Go to the OpenStack Wiki">Wiki</a></li>
<li><a href="http://docs.openstack.org/" title="Go to OpenStack Documentation" class="current">Documentation</a></li>
</ul>
</div>
{% endblock %}

View File

@ -1,4 +0,0 @@
[theme]
inherit = basic
stylesheet = nature.css
pygments_style = tango

View File

@ -1,427 +0,0 @@
# -*- coding: utf-8 -*-
#
# Horizon documentation build configuration file, created by
# sphinx-quickstart on Thu Oct 27 11:38:59 2011.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys
import os
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.abspath(os.path.join(BASE_DIR, "..", ".."))
sys.path.insert(0, ROOT)
# This is required for ReadTheDocs.org, but isn't a bad idea anyway.
os.environ['DJANGO_SETTINGS_MODULE'] = 'openstack_dashboard.settings'
import horizon.version
def write_autodoc_index():
def find_autodoc_modules(module_name, sourcedir):
"""returns a list of modules in the SOURCE directory"""
modlist = []
os.chdir(os.path.join(sourcedir, module_name))
print "SEARCHING %s" % sourcedir
for root, dirs, files in os.walk("."):
for filename in files:
if filename.endswith(".py"):
# remove the pieces of the root
elements = root.split(os.path.sep)
# replace the leading "." with the module name
elements[0] = module_name
# and get the base module name
base, extension = os.path.splitext(filename)
if not (base == "__init__"):
elements.append(base)
result = ".".join(elements)
#print result
modlist.append(result)
return modlist
RSTDIR = os.path.abspath(os.path.join(BASE_DIR, "sourcecode"))
SRCS = {'horizon': ROOT,
'openstack_dashboard': ROOT}
EXCLUDED_MODULES = ('horizon.tests', 'openstack_dashboard.tests',)
CURRENT_SOURCES = {}
if not(os.path.exists(RSTDIR)):
os.mkdir(RSTDIR)
CURRENT_SOURCES[RSTDIR] = ['autoindex.rst']
INDEXOUT = open(os.path.join(RSTDIR, "autoindex.rst"), "w")
INDEXOUT.write("=================\n")
INDEXOUT.write("Source Code Index\n")
INDEXOUT.write("=================\n")
for modulename, path in SRCS.items():
sys.stdout.write("Generating source documentation for %s\n" %
modulename)
INDEXOUT.write("\n%s\n" % modulename.capitalize())
INDEXOUT.write("%s\n" % ("=" * len(modulename),))
INDEXOUT.write(".. toctree::\n")
INDEXOUT.write(" :maxdepth: 1\n")
INDEXOUT.write("\n")
MOD_DIR = os.path.join(RSTDIR, modulename)
CURRENT_SOURCES[MOD_DIR] = []
if not(os.path.exists(MOD_DIR)):
os.mkdir(MOD_DIR)
for module in find_autodoc_modules(modulename, path):
if any([module.startswith(exclude) for exclude \
in EXCLUDED_MODULES]):
print "Excluded module %s." % module
continue
mod_path = os.path.join(path, *module.split("."))
generated_file = os.path.join(MOD_DIR, "%s.rst" % module)
INDEXOUT.write(" %s/%s\n" % (modulename, module))
# Find the __init__.py module if this is a directory
if os.path.isdir(mod_path):
source_file = ".".join((os.path.join(mod_path, "__init__"),
"py",))
else:
source_file = ".".join((os.path.join(mod_path), "py"))
CURRENT_SOURCES[MOD_DIR].append("%s.rst" % module)
# Only generate a new file if the source has changed or we don't
# have a doc file to begin with.
if not os.access(generated_file, os.F_OK) or \
os.stat(generated_file).st_mtime < \
os.stat(source_file).st_mtime:
print "Module %s updated, generating new documentation." \
% module
FILEOUT = open(generated_file, "w")
header = "The :mod:`%s` Module" % module
FILEOUT.write("%s\n" % ("=" * len(header),))
FILEOUT.write("%s\n" % header)
FILEOUT.write("%s\n" % ("=" * len(header),))
FILEOUT.write(".. automodule:: %s\n" % module)
FILEOUT.write(" :members:\n")
FILEOUT.write(" :undoc-members:\n")
FILEOUT.write(" :show-inheritance:\n")
FILEOUT.write(" :noindex:\n")
FILEOUT.close()
INDEXOUT.close()
# Delete auto-generated .rst files for sources which no longer exist
for directory, subdirs, files in list(os.walk(RSTDIR)):
for old_file in files:
if old_file not in CURRENT_SOURCES.get(directory, []):
print "Removing outdated file for %s" % old_file
os.remove(os.path.join(directory, old_file))
write_autodoc_index()
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings.
# They can be extensions coming with Sphinx (named 'sphinx.ext.*')
# or your custom ones.
extensions = ['sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'sphinx.ext.todo',
'sphinx.ext.coverage',
'sphinx.ext.pngmath',
'sphinx.ext.viewcode']
# Add any paths that contain templates here, relative to this directory.
if os.getenv('HUDSON_PUBLISH_DOCS'):
templates_path = ['_ga', '_templates']
else:
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'Horizon'
copyright = u'2012, OpenStack, LLC'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = horizon.version.version_info.version_string()
# The full version, including alpha/beta/rc tags.
release = horizon.version.version_info.release_string()
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['**/#*', '**~', '**/#*#']
# The reST default role (used for this markup: `text`)
# to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
primary_domain = 'py'
nitpicky = False
# -- Options for HTML output --------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme_path = ['.']
html_theme = '_theme'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
html_theme_options = {
"nosidebar": "false"
}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
git_cmd = "git log --pretty=format:'%ad, commit %h' --date=local -n1"
html_last_updated_fmt = os.popen(git_cmd).read()
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'Horizondoc'
# -- Options for LaTeX output -------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass
# [howto/manual]).
latex_documents = [
('index', 'Horizon.tex', u'Horizon Documentation',
u'OpenStack, LLC', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output -------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'horizon', u'Horizon Documentation',
[u'OpenStack'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output -----------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'Horizon', u'Horizon Documentation', u'OpenStack',
'Horizon', 'One line description of project.', 'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# -- Options for Epub output --------------------------------------------------
# Bibliographic Dublin Core info.
epub_title = u'Horizon'
epub_author = u'OpenStack'
epub_publisher = u'OpenStack'
epub_copyright = u'2012, OpenStack'
# The language of the text. It defaults to the language option
# or en if the language is not set.
#epub_language = ''
# The scheme of the identifier. Typical schemes are ISBN or URL.
#epub_scheme = ''
# The unique identifier of the text. This can be an ISBN number
# or the project homepage.
#epub_identifier = ''
# A unique identification for the text.
#epub_uid = ''
# A tuple containing the cover image and cover page html template filenames.
#epub_cover = ()
# HTML files that should be inserted before the pages created by sphinx.
# The format is a list of tuples containing the path and title.
#epub_pre_files = []
# HTML files shat should be inserted after the pages created by sphinx.
# The format is a list of tuples containing the path and title.
#epub_post_files = []
# A list of files that should not be packed into the epub file.
#epub_exclude_files = []
# The depth of the table of contents in toc.ncx.
#epub_tocdepth = 3
# Allow duplicate toc entries.
#epub_tocdup = True
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'python': ('http://docs.python.org/', None),
'django':
('http://docs.djangoproject.com/en/dev/_objects/'),
'nova': ('http://nova.openstack.org', None),
'swift': ('http://swift.openstack.org', None),
'keystone': ('http://keystone.openstack.org', None),
'glance': ('http://glance.openstack.org', None)}

View File

@ -1,204 +0,0 @@
==================
Contributing Guide
==================
First and foremost, thank you for wanting to contribute! It's the only way
open source works!
Before you dive into writing patches, here are some of the basics:
* Project page: http://launchpad.net/horizon
* Bug tracker: https://bugs.launchpad.net/horizon
* Source code: https://github.com/openstack/horizon
* Code review: https://review.openstack.org/#q,status:open+project:openstack/horizon,n,z
* Jenkins build status: https://jenkins.openstack.org/view/Horizon/
* IRC Channel: #openstack-horizon on Freenode.
Making Contributions
====================
Getting Started
---------------
We'll start by assuming you've got a working checkout of the repository (if
not then please see the :doc:`quickstart`).
Second, you'll need to take care of a couple administrative tasks:
#. Create an account on Launchpad.
#. Sign the `OpenStack Contributor License Agreement`_ and follow the associated
instructions to verify your signature.
#. Request to join the `OpenStack Contributors`_ team on Launchpad.
#. Join the `Horizon Developers`_ team on Launchpad.
#. Follow the `instructions for setting up git-review`_ in your
development environment.
Whew! Got that all that? Okay! You're good to go.
Ways To Contribute
------------------
The easiest way to get started with Horizon's code is to pick a bug on
Launchpad that interests you, and start working on that. Alternatively, if
there's an OpenStack API feature you would like to see implemented in Horizon
feel free to try building it.
If those are too big, there are lots of great ways to get involved without
plunging in head-first:
* Report bugs, triage new tickets, and review old tickets on
the `bug tracker`_.
* Propose ideas for improvements via Launchpad Blueprints, via the
mailing list on the project page, or on IRC.
* Write documentation!
* Write unit tests for untested code!
.. _`bug tracker`: https://bugs.launchpad.net/horizon
Choosing Issues To Work On
--------------------------
In general, if you want to write code, there are three cases for issues
you might want to work on:
#. Confirmed bugs
#. Approved blueprints (features)
#. New bugs you've discovered
If you have an idea for a new feature that isn't in a blueprint yet, it's
a good idea to write the blueprint first so you don't end up writing a bunch
of code that may not go in the direction the community wants.
For bugs, open the bug first, but if you can reproduce the bug reliably and
identify its cause then it's usually safe to start working on it. However,
getting independent confirmation (and verifying that it's not a duplicate)
is always a good idea if you can be patient.
After You Write Your Patch
--------------------------
Once you've made your changes, there are a few things to do:
* Make sure the unit tests pass: ``./run_tests.sh``
* Make sure PEP8 is clean: ``./run_tests.sh --pep8``
* Make sure your code is up-to-date with the latest master: ``git pull --rebase``
* Finally, run ``git review`` to upload your changes to Gerrit for review.
The Horizon core developers will be notified of the new review and will examine
it in a timely fashion, either offering feedback or approving it to be merged.
If the review is approved, it is sent to Jenkins to verify the unit tests pass
and it can be merged cleanly. Once Jenkins approves it, the change will be
merged to the master repository and it's time to celebrate!
.. _`OpenStack Contributor License Agreement`: http://wiki.openstack.org/CLA
.. _`OpenStack Contributors`: https://launchpad.net/~openstack-cla
.. _`Horizon Developers`: https://launchpad.net/~horizon
.. _`instructions for setting up git-review`: http://wiki.openstack.org/GerritWorkflow
Etiquette
=========
The community's guidelines for etiquette are fairly simple:
* Treat everyone respectfully and professionally.
* If a bug is "in progress" in the bug tracker, don't start working on it
without contacting the author. Try on IRC, or via the launchpad email
contact link. If you don't get a response after a reasonable time, then go
ahead. Checking first avoids duplicate work and makes sure nobody's toes
get stepped on.
* If a blueprint is assigned, even if it hasn't been started, be sure you
contact the assignee before taking it on. These larger issues often have a
history of discussion or specific implementation details that the assignee
may be aware of that you are not.
* Please don't re-open tickets closed by a core developer. If you disagree with
the decision on the ticket, the appropriate solution is to take it up on
IRC or the mailing list.
* Give credit where credit is due; if someone helps you substantially with
a piece of code, it's polite (though not required) to thank them in your
commit message.
Code Style
==========
Python
------
We follow PEP8_ for all our Python code, and use ``pep8.py`` (available
via the shortcut ``./run_tests.sh --pep8``) to validate that our code
meets proper Python style guidelines.
.. _PEP8: http://www.python.org/dev/peps/pep-0008/
Django
------
Additionally, we follow `Django's style guide`_ for templates, views, and
other miscellany.
.. _Django's style guide: https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/
JavaScript
----------
As a project, Horizon adheres to code quality standards for our JavaScript
just as we do for our Python. To that end we recommend (but do not strictly
enforce) the use of JSLint_ to validate some general best practices.
The default options are mostly good, but the following accommodate some
allowances we make:
* Set ``Indentation`` to ``2``.
* Enable the ``Assume console, alert, ...`` option.
* Enable the ``Assume a browser`` option.
* Enable the ``Tolerate missing 'use strict' pragma`` option.
* Clear the ``Maximum number of errors`` field.
* Add ``horizon,$`` to the ``Predefined`` list.
.. _JSLint: http://jslint.com/
CSS
---
Style guidelines for CSS are currently quite minimal. Do your best to make the
code readable and well-organized. Two spaces are preferred for indentation
so as to match both the JavaScript and HTML files.
HTML
----
Again, readability is paramount; however be conscientous of how the browser
will handle whitespace when rendering the output. Two spaces is the preferred
indentation style to match all front-end code.
Documentation
-------------
Horizon's documentation is written in reStructuredText and uses Sphinx for
additional parsing and functionality, and should follow
standard practices for writing reST. This includes:
* Flow paragraphs such that lines wrap at 80 characters or less.
* Use proper grammar, spelling, capitalization and punctuation at all times.
* Make use of Sphinx's autodoc feature to document modules, classes
and functions. This keeps the docs close to the source.
* Where possible, use Sphinx's cross-reference syntax (e.g.
``:class:`~horizon.foo.Bar```) when referring to other Horizon components.
The better-linked our docs are, the easier they are to use.
Be sure to generate the documentation before submitting a patch for review.
Unexpected warnings often appear when building the documentation, and slight
reST syntax errors frequently cause links or cross-references not to work
correctly.
Conventions
-----------
Simply by convention, we have a few rules about naming:
* The term "project" is used in place of Keystone's "tenant" terminology
in all user-facing text. The term "tenant" is still used in API code to
make things more obvious for developers.
* The term "dashboard" refers to a top-level dashboard class, and "panel" to
the sub-items within a dashboard. Referring to a panel as a dashboard is
both confusing and incorrect.

View File

@ -1,37 +0,0 @@
==========================
Frequently Asked Questions
==========================
What is the relationship between ``Dashboards``, ``Panels``, and navigation?
The navigational structure is strongly encouraged to flow from
``Dashboard`` objects as top-level navigation items to ``Panel`` objects as
sub-navigation items as in the current implementation. Template tags
are provided to automatically generate this structure.
That said, you are not required to use the provided tools and can write
templates and URLconfs by hand to create any desired structure.
Does a panel have to be an app in ``INSTALLED_APPS``?
A panel can live in any Python module. It can be a standalone which ties
into an existing dashboard, or it can be contained alongside others within
a larger dashboard "app". There is no strict enforcement here. Python
is "a language for consenting adults." A module containing a Panel does
not need to be added to ``INSTALLED_APPS``, but this is a common and
convenient way to load a standalone panel.
Could I hook an external service into a panel using, for example, an iFrame?
Panels are just entry-points to hook views into the larger dashboard
navigational structure and enforce common attributes like RBAC. The
view and corresponding templates can contain anything you would like,
including iFrames.
What does this mean for visual design?
The ability to add an arbitrary number of top-level navigational items
(``Dashboard`` objects) poses a new design challenge. Horizon's lead
designer has taken on the challenge of providing a reference design
for Horizon which supports this possibility.

View File

@ -1,24 +0,0 @@
========
Glossary
========
Horizon
The OpenStack dashboard project. Also the name of the top-level
Python object which handles registration for the app.
Dashboard
A Python class representing a top-level navigation item (e.g. "syspanel")
which provides a consistent API for Horizon-compatible applications.
Panel
A Python class representing a sub-navigation item (e.g. "instances")
which contains all the necessary logic (views, forms, tests, etc.) for
that interface.
Project
Used in user-facing text in place of the term "Tenant" which is Keystone's
word.

View File

@ -1,126 +0,0 @@
..
Copyright 2012 OpenStack, LLC
All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain
a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
========================================
Horizon: The OpenStack Dashboard Project
========================================
Introduction
============
Horizon is the canonical implementation of `Openstack's Dashboard
<https://github.com/openstack/horizon>`_, which provides a web based user
interface to OpenStack services including Nova, Swift, Keystone, etc.
For a more in-depth look at Horizon and its architecture, see the
:doc:`Introduction to Horizon <intro>`.
To learn what you need to know to get going, see the :doc:`quickstart`.
Getting Started With Horizon
============================
How to use Horizon in your own projects.
.. toctree::
:maxdepth: 1
intro
quickstart
topics/tutorial
topics/deployment
topics/customizing
Developer Docs
==============
For those wishing to develop Horizon itself, or go in-depth with building
your own :class:`~horizon.Dashboard` or :class:`~horizon.Panel` classes,
the following documentation is provided.
General information
-------------------
Brief guides to areas of interest and importance when developing Horizon.
.. toctree::
:maxdepth: 1
contributing
testing
Topic Guides
------------
Information on how to work with specific areas of Horizon can be found in
the following topic guides.
.. toctree::
:maxdepth: 1
topics/tables
topics/testing
API Reference
-------------
In-depth documentation for Horizon and its APIs.
.. toctree::
:maxdepth: 1
ref/run_tests
ref/horizon
ref/workflows
ref/tables
ref/tabs
ref/forms
ref/middleware
ref/context_processors
ref/decorators
ref/exceptions
ref/test
Source Code Reference
---------------------
Auto-generated reference for the complete source code.
.. toctree::
:maxdepth: 1
sourcecode/autoindex
Release Notes
=============
.. toctree::
:glob:
:maxdepth: 1
releases/*
Information
===========
.. toctree::
:maxdepth: 1
faq
glossary
* :ref:`genindex`
* :ref:`modindex`

View File

@ -1,124 +0,0 @@
===================
Introducing Horizon
===================
.. contents:: Contents:
:local:
Values
======
"Think simple" as my old master used to say - meaning reduce
the whole of its parts into the simplest terms, getting back
to first principles.
-- Frank Lloyd Wright
Horizon holds several key values at the core of its design and architecture:
* Core Support: Out-of-the-box support for all core OpenStack projects.
* Extensible: Anyone can add a new component as a "first-class citizen".
* Manageable: The core codebase should be simple and easy-to-navigate.
* Consistent: Visual and interaction paradigms are maintained throughout.
* Stable: A reliable API with an emphasis on backwards-compatibility.
* Usable: Providing an *awesome* interface that people *want* to use.
The only way to attain and uphold those ideals is to make it *easy* for
developers to implement those values.
History
=======
Horizon started life as a single app to manage OpenStack's compute project.
As such, all it needed was a set of views, templates, and API calls.
From there it grew to support multiple OpenStack projects and APIs gradually,
arranged rigidly into "dash" and "syspanel" groupings.
During the "Diablo" release cycle an initial plugin system was added using
signals to hook in additional URL patterns and add links into the "dash"
and "syspanel" navigation.
This incremental growth served the goal of "Core Support" phenomenally, but
left "Extensible" and "Manageable" behind. And while the other key values took
shape of their own accord, it was time to re-architect for an extensible,
modular future.
The Current Architecture & How It Meets Our Values
==================================================
At its core, **Horizon should be a registration pattern for
applications to hook into**. Here's what that means and how it is
implemented in terms of our values:
Core Support
------------
Horizon ships with three central dashboards, a "User Dashboard", a
"System Dashboard", and a "Settings" dashboard. Between these three they
cover the core OpenStack applications and deliver on Core Support.
The Horizon application also ships with a set of API abstractions
for the core OpenStack projects in order to provide a consistent, stable set
of reusable methods for developers. Using these abstractions, developers
working on Horizon don't need to be intimately familiar with the APIs of
each OpenStack project.
Extensible
----------
A Horizon dashboard application is based around the :class:`~horizon.Dashboard`
class that provides a consistent API and set of capabilities for both
core OpenStack dashboard apps shipped with Horizon and equally for third-party
apps. The :class:`~horizon.Dashboard` class is treated as a top-level
navigation item.
Should a developer wish to provide functionality within an existing dashboard
(e.g. adding a monitoring panel to the user dashboard) the simple registration
pattern makes it possible to write an app which hooks into other dashboards
just as easily as creating a new dashboard. All you have to do is import the
dashboard you wish to modify.
Manageable
----------
Within the application, there is a simple method for registering a
:class:`~horizon.Panel` (sub-navigation items). Each panel contains the
necessary logic (views, forms, tests, etc.) for that interface. This granular
breakdown prevents files (such as ``api.py``) from becoming thousands of
lines long and makes code easy to find by correlating it directly to the
navigation.
Consistent
----------
By providing the necessary core classes to build from, as well as a
solid set of reusable templates and additional tools (base form classes,
base widget classes, template tags, and perhaps even class-based views)
we can maintain consistency across applications.
Stable
------
By architecting around these core classes and reusable components we
create an implicit contract that changes to these components will be
made in the most backwards-compatible ways whenever possible.
Usable
------
Ultimately that's up to each and every developer that touches the code,
but if we get all the other goals out of the way then we are free to focus
on the best possible experience.
.. seealso::
:doc:`Quickstart <quickstart>`
A short guide to getting started with using Horizon.
:doc:`Frequently Asked Questions <faq>`
Common questions and answers.
:doc:`Glossary <glossary>`
Common terms and their definitions.

View File

@ -1,207 +0,0 @@
==================
Horizon Quickstart
==================
Setup
=====
To setup an Horizon development environment simply clone the Horizon git
repository from http://github.com/openstack/horizon and execute the
``run_tests.sh`` script from the root folder (see :doc:`ref/run_tests`)::
> git clone https://github.com/openstack/horizon.git
> cd horizon
> ./run_tests.sh
Next you will need to setup your Django application config by copying ``openstack_dashboard/local/local_settings.py.example`` to ``openstack_dashboard/local_settings.py``. To do this quickly you can use the following command::
> cp openstack_dashboard/local/local_settings.py.example openstack_dashboard/local/local_settings.py
Horizon assumes a single end-point for OpenStack services which defaults to
the local host (127.0.0.1). If this is not the case change the
``OPENSTACK_HOST`` setting in the ``openstack_dashboard/local/local_settings.py`` file, to the actual IP address of the OpenStack end-point Horizon should use.
To start the Horizon development server use the Django ``manage.py`` utility
with the context of the virtual environment::
> tools/with_venv.sh ./manage.py runserver
Alternately specify the listen IP and port::
> tools/with_venv.sh ./manage.py runserver 0.0.0.0:8080
.. note::
If you would like to run commands without the prefix of ``tools/with_venv.sh`` you may source your environment directly. This will remain active as long as your shell session stays open::
> source .venv/bin/activate
Once the Horizon server is running point a web browser to http://localhost:8000
or to the IP and port the server is listening for.
.. note::
The ``DevStack`` project (http://devstack.org/) can be used to install
an OpenStack development environment from scratch.
.. note::
The minimum required set of OpenStack services running includes the
following:
* Nova (compute, api, scheduler, and network)
* Glance
* Keystone
Optional support is provided for Swift.
Horizon's Structure
===================
This project is a bit different from other OpenStack projects in that it has
two very distinct components underneath it: ``horizon``, and
``openstack_dashboard``.
The ``horizon`` directory holds the generic libraries and components that can
be used in any Django project.
The ``openstack_dashboard`` directory contains a reference Django project that
uses ``horizon``.
For development, both pieces share an environment which (by default) is
built with the ``tools/install_venv.py`` script. That script creates a
virtualenv and installs all the necessary packages.
If dependencies are added to either ``horizon`` or ``openstack_dashboard``,
they should be added to ``tools/pip-requires``.
.. important::
If you do anything which changes the environment (adding new dependencies
or renaming directories are both great examples) be sure to increment the
``environment_version`` counter in :doc:`run_tests.sh <ref/run_tests>`.
Project
=======
INSTALLED_APPS
--------------
At the project level you add Horizon and any desired dashboards to your
``settings.INSTALLED_APPS``::
INSTALLED_APPS = (
'django',
...
'horizon',
'horizon.dash',
'horizon.syspanel',
)
URLs
----
Then you add a single line to your project's ``urls.py``::
url(r'', include(horizon.urls)),
Those urls are automatically constructed based on the registered Horizon apps.
If a different URL structure is desired it can be constructed by hand.
Templates
---------
Pre-built template tags generate navigation. In your ``nav.html``
template you might have the following::
{% load horizon %}
<div class='nav'>
{% horizon_main_nav %}
</div>
And in your ``sidebar.html`` you might have::
{% load horizon %}
<div class='sidebar'>
{% horizon_dashboard_nav %}
</div>
These template tags are aware of the current "active" dashboard and panel
via template context variables and will render accordingly.
Application
===========
Structure
---------
An application would have the following structure (we'll use syspanel as
an example)::
syspanel/
|---__init__.py
|---dashboard.py <-----Registers the app with Horizon and sets dashboard properties
|---templates/
|---templatetags/
|---overview/
|---services/
|---images/
|---__init__.py
|---panel.py <-----Registers the panel in the app and defines panel properties
|---urls.py
|---views.py
|---forms.py
|---tests.py
|---api.py <-------Optional additional API methods for non-core services
|---templates/
...
...
Dashboard Classes
-----------------
Inside of ``dashboard.py`` you would have a class definition and the registration
process::
import horizon
class Syspanel(horizon.Dashboard):
name = "Syspanel" # Appears in navigation
slug = 'syspanel' # Appears in url
panels = ('overview', 'services', 'instances', 'flavors', 'images',
'tenants', 'users', 'quotas',)
default_panel = 'overview'
permissions = ('openstack.roles.admin',)
...
horizon.register(Syspanel)
Panel Classes
-------------
To connect a :class:`~horizon.Panel` with a :class:`~horizon.Dashboard` class
you register it in a ``panels.py`` file like so::
import horizon
from horizon.dashboard.syspanel import dashboard
class Images(horizon.Panel):
name = "Images"
slug = 'images'
permissions = ('openstack.roles.admin', 'my.other.permission',)
# You could also register your panel with another application's dashboard
dashboard.Syspanel.register(Images)
By default a :class:`~horizon.Panel` class looks for a ``urls.py`` file in the
same directory as ``panel.py`` to include in the rollup of url patterns from
panels to dashboards to Horizon, resulting in a wholly extensible, configurable
URL structure.

View File

@ -1,6 +0,0 @@
==========================
Horizon Context Processors
==========================
.. automodule:: horizon.context_processors
:members:

View File

@ -1,6 +0,0 @@
==================
Horizon Decorators
==================
.. automodule:: horizon.decorators
:members:

View File

@ -1,6 +0,0 @@
==================
Horizon Exceptions
==================
.. automodule:: horizon.exceptions
:members:

View File

@ -1,98 +0,0 @@
=============
Horizon Forms
=============
Horizon ships with some very useful base form classes, form fields,
class-based views, and javascript helpers which streamline most of the common
tasks related to form handling.
Form Classes
============
.. automodule:: horizon.forms.base
:members:
Form Fields
===========
.. automodule:: horizon.forms.fields
:members:
Form Views
==========
.. automodule:: horizon.forms.views
:members:
Forms Javascript
================
Switchable Fields
-----------------
By marking fields with the ``"switchable"`` and ``"switched"`` classes along
with defining a few data attributes you can programmatically hide, show,
and rename fields in a form.
The triggers are fields using a ``select`` input widget, marked with the
"switchable" class, and defining a "data-slug" attribute. When they are changed,
any input with the ``"switched"`` class and defining a ``"data-switch-on"``
attribute which matches the ``select`` input's ``"data-slug"`` attribute will be
evaluated for necessary changes. In simpler terms, if the ``"switched"`` target
input's ``"switch-on"`` matches the ``"slug"`` of the ``"switchable"`` trigger
input, it gets switched. Simple, right?
The ``"switched"`` inputs also need to define states. For each state in which
the input should be shown, it should define a data attribute like the
following: ``data-<slug>-<value>="<desired label>"``. When the switch event
happens the value of the ``"switchable"`` field will be compared to the
data attributes and the correct label will be applied to the field. If
a corresponding label for that value is *not* found, the field will
be hidden instead.
A simplified example is as follows::
source = forms.ChoiceField(
label=_('Source'),
choices=[
('cidr', _('CIDR')),
('sg', _('Security Group'))
],
widget=forms.Select(attrs={
'class': 'switchable',
'data-slug': 'source'
})
)
cidr = fields.IPField(
label=_("CIDR"),
required=False,
widget=forms.TextInput(attrs={
'class': 'switched',
'data-switch-on': 'source',
'data-source-cidr': _('CIDR')
})
)
security_group = forms.ChoiceField(
label=_('Security Group'),
required=False,
widget=forms.Select(attrs={
'class': 'switched',
'data-switch-on': 'source',
'data-source-sg': _('Security Group')
})
)
That code would create the ``"switchable"`` control field ``source``, and the
two ``"switched"`` fields ``cidr`` and ``security group`` which are hidden or
shown depending on the value of ``source``.
NOTE: A field can only safely define one slug in its ``"switch-on"`` attribute.
While switching on multiple fields is possible, the behavior is very hard to
predict due to the events being fired from the various switchable fields in
order. You generally end up just having it hidden most of the time by accident,
so it's not recommended. Instead just add a second field to the form and control
the two independently, then merge their results in the form's clean or handle
methods at the end.

View File

@ -1,45 +0,0 @@
======================
The ``horizon`` Module
======================
.. module:: horizon
Horizon ships with a single point of contact for hooking into your project if
you aren't developing your own :class:`~horizon.Dashboard` or
:class:`~horizon.Panel`::
import horizon
From there you can access all the key methods you need.
Horizon
=======
.. attribute:: urls
The auto-generated URLconf for Horizon. Usage::
url(r'', include(horizon.urls)),
.. autofunction:: register
.. autofunction:: unregister
.. autofunction:: get_absolute_url
.. autofunction:: get_user_home
.. autofunction:: get_dashboard
.. autofunction:: get_default_dashboard
.. autofunction:: get_dashboards
Dashboard
=========
.. autoclass:: Dashboard
:members:
Panel
=====
.. autoclass:: Panel
:members:
.. autoclass:: PanelGroup
:members:

View File

@ -1,6 +0,0 @@
==================
Horizon Middleware
==================
.. automodule:: horizon.middleware
:members:

View File

@ -1,224 +0,0 @@
===========================
The ``run_tests.sh`` Script
===========================
.. contents:: Contents:
:local:
Horizon ships with a script called ``run_tests.sh`` at the root of the
repository. This script provides many crucial functions for the project,
and also makes several otherwise complex tasks trivial for you as a
developer.
First Run
=========
If you start with a clean copy of the Horizon repository, the first thing
you should do is to run ``./run_tests.sh`` from the root of the repository.
This will do two things for you:
#. Set up a virtual environment for both the ``horizon`` module and
the ``openstack-dashboard`` project using
``openstack-dashboard/tools/install_venv.py``.
#. Run the tests for both ``horizon`` and ``openstack-dashboard`` using
their respective environments and verify that evreything is working.
Setting up the environment the first time can take several minutes, but only
needs to be done once. If dependencies are added in the future, updating the
environments will be necessary but not as time consuming.
I just want to run the tests!
=============================
Running the full set of unit tests quickly and easily is the main goal of this
script. All you need to do is::
./run_tests.sh
Yep, that's it. However, for a quicker test run you can skip the Selenium
tests by using the ``--skip-selenium`` flag::
./run_tests.sh --skip-selenium
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.
Running a subset of tests
-------------------------
Instead of running all tests, you can specify an individual directory, file,
class, or method that contains test code.
To run the tests in the ``horizon/test/tests/tables.py`` file::
./run_tests.sh horizon.test.tests.tables
To run the tests in the `WorkflowsTests` class in
``horizon/test/tests/workflows``::
./run_tests.sh horizon.test.tests.workflows:WorkflowsTests
To run just the `WorkflowsTests.test_workflow_view` test method::
./run_tests.sh horizon.test.tests.workflows:WorkflowsTests.test_workflow_view
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!
================
You can generate various reports and metrics using command line arguments
to ``run_tests.sh``.
Coverage
--------
To run coverage reports::
./run_tests.sh --coverage
The reports are saved to ``./reports/`` and ``./coverage.xml``.
PEP8
----
You can check for PEP8 violations as well::
./run_tests.sh --pep8
The results are saved to ``./pep8.txt``.
PyLint
------
For more detailed code analysis you can run::
./run_tests.sh --pylint
The output will be saved in ``./pylint.txt``.
Tab Characters
--------------
For those who dislike having a mix of tab characters and spaces for indentation
there's a command to check for that in Python, CSS, JavaScript and HTML files::
./run_tests.sh --tabs
This will output a total "tab count" and a list of the offending files.
Running the development server
==============================
As an added bonus, you can run Django's development server directly from
the root of the repository with ``run_tests.sh`` like so::
./run_tests.sh --runserver
This is effectively just an alias for::
./openstack-dashboard/tools/with_venv.sh ./openstack-dashboard/dashboard/manage.py runserver
Generating the documentation
============================
You can build Horizon's documentation automatically by running::
./run_tests.sh --docs
The output is stored in ``./doc/build/html/``.
Updating the translation files
==============================
You can update all of the translation files for both the ``horizon`` app and
``openstack_dashboard`` project with a single command:
./run_tests.sh --makemessages
or, more compactly:
./run_tests.sh --m
Starting clean
==============
If you ever want to start clean with a new environment for Horizon, you can
run::
./run_tests.sh --force
That will blow away the existing environments and create new ones for you.
Non-interactive Mode
====================
There is an optional flag which will run the script in a non-interactive
(and eventually less verbose) mode::
./run_tests.sh --quiet
This will automatically take the default action for actions which would
normally prompt for user input such as installing/updating the environment.
Environment Backups
===================
To speed up the process of doing clean checkouts, running continuous
integration tests, etc. there are options for backing up the current
environment and restoring from a backup.
./run_tests.sh --restore-environment
./run_tests.sh --backup-environment
The environment backup is stored in ``/tmp/.horizon_environment/``.
Environment Versioning
======================
Horizon keeps track of changes to the environment by incrementing an
``environment_version`` integer at the top of ``run_tests.sh``.
If you do anything which changes the environment (adding new dependencies
or renaming directories are both great examples) be sure to increment the
``environment_version`` counter as well.

View File

@ -1,82 +0,0 @@
==================
Horizon DataTables
==================
.. module:: horizon.tables
Horizon includes a componentized API for programmatically creating tables
in the UI. Why would you want this? It means that every table renders
correctly and consistently, table- and row-level actions all have a consistent
API and appearance, and generally you don't have to reinvent the wheel or
copy-and-paste every time you need a new table!
DataTable
=========
The core class which defines the high-level structure of the table being
represented. Example::
class MyTable(DataTable):
name = Column('name')
email = Column('email')
class Meta:
name = "my_table"
table_actions = (MyAction, MyOtherAction)
row_actions - (MyAction)
A full reference is included below:
.. autoclass:: DataTable
:members:
DataTable Options
=================
The following options can be defined in a ``Meta`` class inside a
:class:`.DataTable` class. Example::
class MyTable(DataTable):
class Meta:
name = "my_table"
verbose_name = "My Table"
.. autoclass:: horizon.tables.base.DataTableOptions
:members:
Table Components
================
.. autoclass:: Column
:members:
.. autoclass:: Row
:members:
Actions
=======
.. autoclass:: Action
:members:
.. autoclass:: LinkAction
:members:
.. autoclass:: FilterAction
:members:
.. autoclass:: BatchAction
:members:
.. autoclass:: DeleteAction
:members:
Class-Based Views
=================
Several class-based views are provided to make working with DataTables
easier in your UI.
.. autoclass:: DataTableView
.. autoclass:: MultiTableView

View File

@ -1,45 +0,0 @@
==========================
Horizon Tabs and TabGroups
==========================
.. module:: horizon.tabs
Horizon includes a set of reusable components for programmatically
building tabbed interfaces with fancy features like dynamic AJAX loading
and nearly effortless templating and styling.
Tab Groups
==========
For any tabbed interface, your fundamental element is the tab group which
contains all your tabs. This class provides a dead-simple API for building
tab groups and encapsulates all the necessary logic behind the scenes.
.. autoclass:: TabGroup
:members:
Tabs
====
The tab itself is the discrete unit for a tab group, representing one
view of data.
.. autoclass:: Tab
:members:
.. autoclass:: TableTab
:members:
TabView
=======
There is also a useful and simple generic class-based view for handling
the display of a :class:`~horizon.tabs.TabGroup` class.
.. autoclass:: TabView
:members:
.. autoclass:: TabbedTableView
:members:

View File

@ -1,25 +0,0 @@
========================
Horizon TestCase Classes
========================
.. module:: horizon.test.helpers
Horizon provides a base test case class which provides several useful
pre-prepared attributes for testing Horizon components.
.. autoclass:: TestCase
:members:
.. module :: openstack_dashboard.test.helpers
The OpenStack Dashboard also provides test case classes for greater
ease-of-use when testing APIs and OpenStack-specific auth scenarios.
.. autoclass:: TestCase
:members:
.. autoclass:: APITestCase
:members:
.. autoclass:: BaseAdminViewTests
:members:

View File

@ -1,33 +0,0 @@
=================
Horizon Workflows
=================
.. module:: horizon.workflows
One of the most challenging aspects of building a compelling user experience
is crafting complex multi-part workflows. Horizon's ``workflows`` module
aims to bring that capability within everyday reach.
Workflows
=========
.. autoclass:: Workflow
:members:
Steps
=====
.. autoclass:: Step
:members:
Actions
=======
.. autoclass:: Action
:members:
WorkflowView
============
.. autoclass:: WorkflowView
:members:

View File

@ -1,148 +0,0 @@
======================
Horizon 2012.1 "Essex"
======================
Release Overview
================
During the Essex release cycle, Horizon underwent a significant set of internal
changes to allow extensibility and customization while also adding a significant
number of new features and bringing much greater stability to every interaction
with the underlying components.
Highlights
==========
Extensibility
-------------
Making Horizon extensible for third-party developers was one of the core
goals for the Essex release cycle. Massive strides have been made to allow
for the addition of new "plug-in" components and customization of OpenStack
Dashboard deployments.
To support this extensability, all the components used to build on Horizon's
interface are now modular and reusable. Horizon's own dashboards use these
components, and they have all been built with third-party developers in mind.
Some of the main components are listed below.
Dashboards and Panels
~~~~~~~~~~~~~~~~~~~~~
Horizon's structure has been divided into logical groupings called dashboards
and panels. Horizon's classes representing these concepts handle all the
structural concerns associated with building a complete user interface
(navigation, access control, url structure, etc.).
Data Tables
~~~~~~~~~~~
One of the most common activities in a dashboard user interface is simply
displaying a list of resources or data and allowing the user to take actions on
that data. To this end, Horizon abstracted the commonalities of this task into a
reusable set of classes which allow developers to programmatically create
displays and interactions for their data with minimal effort and zero
boilerplate.
Tabs and TabGroups
~~~~~~~~~~~~~~~~~~
Another extremely common user-interface element is the use of "tabs" to break
down discrete groups of data into manageable chunks. Since these tabs often
encompasse vastly different data, may have completely different access
restrictions, and may sometimes be better-off being loaded dynamically rather
than with the initial page load, Horizon includes tab and tab group classes for
constructing these interfaces elegently and with no knowledge of the HTML, CSS
or JavaScript involved.
Nova Features
-------------
Support for Nova's features has been greatly improved in Essex:
* Support for Nova volumes, including:
* Volumes creation and management.
* Volume snapshots.
* Realtime AJAX updating for volumes in transition states.
* Improved Nova instance display and interactions, including:
* Launching instances from volumes.
* Pausing/suspending instances.
* Displaying instance power states.
* Realtime AJAX updating for instances in transition states.
* Support for managing Floating IP address pools.
* New instance and volume detail views.
Settings
--------
A new "Settings" area was added that offers several userful functions:
* EC2 credentials download.
* OpenStack RC file download.
* User language preference customization.
User Experience Improvements
----------------------------
* Support for batch actions on multiple resources (e.g. terminating multiple
instances at once).
* Modal interactions throughout the entire UI.
* AJAX form submission for in-place validation.
* Improved in-context help for forms (tooltips and validation messages).
Community
---------
* Creation and publication of a set of Human Interface Guidelines (HIG).
* Copious amounts of documentation for developers.
Under The Hood
--------------
* Internationalization fully enabled, with all strings marked for translation.
* Client library changes:
* Full migration to python-novaclient from the deprecated openstackx library.
* Migration to python-keystoneclient from the deprecated keystone portion
of the python-novaclient library.
* Client-side templating capabilities for more easily creating dynamic
interactions.
* Frontend overhaul to use the Bootstrap CSS/JS framework.
* Centralized error handling for vastly improved stability/reliability
across APIs/clients.
* Completely revamped test suite with comprehensive test data.
* Forward-compatibility with Django 1.4 and the option of cookie-based sessions.
Known Issues and Limitations
============================
Quantum
-------
Quantum support has been removed from Horizon for the Essex release. It will be
restored in Folsom in conjunction with Quantum's first release as a core
OpenStack project.
Keystone
--------
Due to the mechanisms by which Keystone determines "admin"-ness for a user, an
admin user interacting with the "Project" dashboard may see some inconsistent
behavior such as all resources being listed instead of only those belonging to
that project, or only being able to return to the "Admin" dashboard while
accessing certain projects.
Exceptions during customization
-------------------------------
Exceptions raised while overriding built-in Horizon behavior via the
"customization_module" setting may trigger a bug in the error handling
which will mask the original exception.
Backwards Compatibility
=======================
The Essex Horizon release is only partially backwards-compatible with Diablo
OpenStack components. While it is largely possible to log in and interact, many
functions in Nova, Glance and Keystone changed too substantially in Essex to
maintain full compatibliity.

View File

@ -1,159 +0,0 @@
=======================
Horizon 2012.2 "Folsom"
=======================
Release Overview
================
The Folsom release cycle brought several major advances to Horizon's user
experience while also reintroducing Quantum networking as a core piece
of the OpenStack Dashboard.
Highlights
==========
Networking (Quantum)
--------------------
With Quantum being a core project for the Folsom release, we worked closely
with the Quantum team to bring networking support back into Horizon. This
appears in two primary places: the Networks panel in both the Project and
Admin dashboards, and the Network tab in the Launch Instance workflow. Expect
further improvements in these areas as Quantum continues to mature and more
users adopt this model of virtual network management.
User Experience
---------------
Workflows
~~~~~~~~~
By far the biggest UI/UX change in the Folsom release is the introduction of
programmatic workflows. These components allow developers to create concise
interactions that combine discrete tasks spanning multiple services and
resources in a user-friendly way and with minimal boilerplate code. Within
a workflow, related objects can also be dynamically created so users don't lose
their place when they realize the item they wanted isn't currently available.
Look for examples of these workflows in Launch Instance, Associate Floating IP,
and Create/Edit Project.
Resource Browser
~~~~~~~~~~~~~~~~
Another cool new component is an interface designed for "browsing" resources
which are nested under a parent resource. The object store (Swift) is a prime
example of this. Now there is a consistent top-level navigation for containers
on the left-hand pane of the "browser" while the right-hand pane lets you
explore within those containers and sub-folders.
User Experience Improvements
----------------------------
* Timezone support is now enabled. You can select your preferred timezone
in the User Settings panel.
Community
---------
* Third-party developers who wish to build on Horizon can get started much
faster using the new dashboard and panel templates. See the docs on
`creating a dashboard`_ and `creating a panel`_ for more information.
* A `thorough set of documentation`_ for developers on how to go about
internationalizing, localizing and translating OpenStack projects
is now available.
.. _creating a dashboard: http://docs.openstack.org/developer/horizon/topics/tutorial.html#creating-a-dashboard
.. _creating a panel: http://docs.openstack.org/developer/horizon/topics/tutorial.html#creating-a-panel
.. _thorough set of documentation: http://wiki.openstack.org/Translations
Under The Hood
--------------
* The python-swiftclient library and python-cinderclient libraries are now
used under the hood instead of cloudfiles and python-novaclient respectively.
* Internationalization of client-side JavaScript is now possible in addition
to server-side Python code.
* Keystone authentication is now handled by a proper pluggable Django
authentication backend, offering significantly better and more reliable
security for Horizon.
Other Improvements and Fixes
----------------------------
Some of the general areas of improvement include:
* Images can now be added to Glance by providing a URL for Glance to download
the image data from.
* Quotas are now displayed dynamically throughout the Project dashboard.
* API endpoints are now displayed on the OpenStack RC File panel so they
can be organically discovered by an end-user.
* DataTables now support a summation row at the bottom of the table.
* Better cross-browser support (Safari and IE particularly).
* Fewer API calls to OpenStack endpoints (improves performance).
* Better validation of what actions are permitted when.
* Improved error handling and error messages.
Known Issues and Limitations
============================
Floating IPs and Quantum
------------------------
Due to the very late addition of floating IP support in Quantum, Nova's
integration there is lacking, so floating IP-related API calls to Nova will
fail when your OpenStack deployment uses Quantum for networking. This means
that Horizon actions such as "allocate" and "associate" floating IPs will
not work either since they rely on the underlying APIs.
Pagination
----------
A number of the "index" pages don't fully work with API pagination yet,
causing them to only display the first chunk of results returned by the API.
This number is often 1000 (as in the case of novaclient results), but does vary
somewhat.
Deleting large numbers of resources simultaneously
--------------------------------------------------
Using the "select all" checkbox to delete large numbers of resources via the
API can cause network timeouts (depending on configuration). This is
due to the APIs not supporting bulk-deletion natively, and consequently Horizon
has to send requests to delete each resource individually behind the scenes.
Backwards Compatibility
=======================
The Folsom Horizon release should be fully-compatible with both Folsom and
Essex versions of the rest of the OpenStack core projects (Nova, Swift, etc.).
While some features work significantly better with an all-Folsom stack due
to bugfixes, etc. in underlying services, there should not be any limitations
on what will or will not function. (Note: Quantum was not a core OpenStack
project in Essex, and thus this statement does not apply to network management.)
In terms of APIs provided for extending Horizon, there are a handful of
backwards-incompatible changes that were made:
* The ``can_haz`` and ``can_haz_list`` template filters have been renamed
to ``has_permissions`` and ``has_permissions_on_list`` respectively.
* The dashboard-specific ``base.html`` templates (e.g. ``nova/base.html``,
``syspanel/base.html``, etc.) have been removed in favor of a single
``base.html`` template.
* In conjunction with the previous item, the dashboard-specific template blocks
(e.g. ``nova_main``, ``syspanel_main``, etc.) have been removed in favor of
a single ``main`` template block.
Overall, though, great effort has been made to maintain compatibility for
third-party developers who may have built on Horizon so far.

View File

@ -1,41 +0,0 @@
=======================
Horizon's tests and you
=======================
How to run the tests
====================
Because Horizon is composed of both the ``horizon`` app and the
``openstack-dashboard`` reference project, there are in fact two sets of unit
tests. While they can be run individually without problem, there is an easier
way:
Included at the root of the repository is the ``run_tests.sh`` script
which invokes both sets of tests, and optionally generates analyses on both
components in the process. This script is what what Jenkins uses to verify the
stability of the project, so you should make sure you run it and it passes
before you submit any pull requests/patches.
To run the tests::
$ ./run_tests.sh
It's also possible to :doc:`run a subset of unit tests<ref/run_tests>`.
.. seealso::
:doc:`ref/run_tests`
Full reference for the ``run_tests.sh`` script.
Writing tests
=============
Horizon uses Django's unit test machinery (which extends Python's ``unittest2``
library) as the core of its test suite. As such, all tests for the Python code
should be written as unit tests. No doctests please.
In general new code without unit tests will not be accepted, and every bugfix
*must* include a regression test.
For a much more in-depth discussion of testing, see the :doc:`testing topic
guide </topics/testing>`.

View File

@ -1,139 +0,0 @@
===================
Customizing Horizon
===================
Changing the Site Title
=======================
The OpenStack Dashboard Site Title branding (i.e. "**OpenStack** Dashboard")
can be overwritten by adding the attribute ``SITE_BRANDING``
to ``local_settings.py`` with the value being the desired name.
The file ``local_settings.py`` can be found at the Horizon directory path of
``horizon/openstack-dashboard/local/local_settings.py``.
Changing the Logo
=================
The OpenStack Logo is pulled in through ``style.css``::
#splash .modal {
background: #fff url(../images/logo.png) no-repeat center 35px;
h1.brand a {
background: url(../images/logo.png) top left no-repeat;
To override the OpenStack Logo image, replace the image at the directory path
``horizon/openstack-dashboard/dashboard/static/dashboard/images/logo.png``.
The dimensions should be ``width: 108px, height: 121px``.
Modifying Existing Dashboards and Panels
========================================
If you wish to alter dashboards or panels which are not part of your codebase,
you can specify a custom python module which will be loaded after the entire
Horizon site has been initialized, but prior to the URLconf construction.
This allows for common site-customization requirements such as:
* Registering or unregistering panels from an existing dashboard.
* Changing the names of dashboards and panels.
* Re-ordering panels within a dashboard or panel group.
To specify the python module containing your modifications, add the key
``customization_module`` to your ``settings.HORIZON_CONFIG`` dictionary.
The value should be a string containing the path to your module in dotted
python path notation. Example::
HORIZON_CONFIG = {
"customization_module": "my_project.overrides"
}
You can do essentially anything you like in the customization module. For
example, you could change the name of a panel::
from django.utils.translation import ugettext_lazy as _
import horizon
# Rename "User Settings" to "User Options"
settings = horizon.get_dashboard("settings")
user_panel = settings.get_panel("user")
user_panel.name = _("User Options")
Or get the instances panel::
projects_dashboard = horizon.get_dashboard("project")
instances_panel = projects_dashboard.get_panel("instances")
And limit access to users with the Keystone Admin role::
permissions = list(getattr(instances_panel, 'permissions', []))
permissions.append('openstack.roles.admin')
instances_panel.permissions = tuple(permissions)
Or just remove it entirely::
projects_dashboard.unregister(instances_panel.__class__)
.. NOTE::
``my_project.overrides`` needs to be importable by the python process running
Horizon.
If your module is not installed as a system-wide python package,
you can either make it installable (e.g., with a setup.py)
or you can adjust the python path used by your WSGI server to include its location.
Probably the easiest way is to add a ``python-path`` argument to
the ``WSGIDaemonProcess`` line in Apache's Horizon config.
Assuming your ``my_project`` module lives in ``/opt/python/my_project``,
you'd make it look like the following::
WSGIDaemonProcess [... existing options ...] python-path=/opt/python
Button Icons
============
Horizon provides hooks for customizing the look and feel of each class of
button on the site. The following classes are used to identify each type of
button:
* Generic Classes
* btn-search
* btn-delete
* btn-upload
* btn-download
* btn-create
* btn-edit
* btn-list
* btn-copy
* btn-camera
* btn-stats
* btn-enable
* btn-disable
* Floating IP-specific Classes
* btn-allocate
* btn-release
* btn-associate
* btn-disassociate
* Instance-specific Classes
* btn-launch
* btn-terminate
* btn-reboot
* btn-pause
* btn-suspend
* btn-console
* btn-log
* Volume-specific classes
* btn-detach
Additionally, the site-wide default button classes can be configured by
setting ``ACTION_CSS_CLASSES`` to a tuple of the classes you wish to appear
on all action buttons in your ``local_settings.py`` file.

View File

@ -1,190 +0,0 @@
=================
Deploying Horizon
=================
This guide aims to cover some common questions, concerns and pitfalls you
may encounter when deploying Horizon in a production environment.
Logging
=======
Logging is an important concern for prouction deployments, and the intricacies
of good logging configuration go far beyond what can be covered here. However
there are a few points worth noting about the logging included with Horizon,
how to customize it, and where other components may take over:
* Horizon's logging uses Django's logging configuration mechanism, which
can be customized in your ``local_settings.py`` file through the
``LOGGING`` dictionary.
* Horizon's default logging example sets the log level to ``"INFO"``, which is
a reasonable choice for production deployments. For development, however,
you may want to change the log level to ``"DEBUG"``.
* Horizon also uses a number of 3rd-party clients which log separately. The
log level for these can still be controlled through Horizon's ``LOGGING``
config, however behaviors may vary beyond Horizon's control.
.. warning::
At this time there is `a known bug in python-keystoneclient`_ where it will
log the complete request body of any request sent to Keystone through it
(including logging passwords in plain text) when the log level is set to
``"DEBUG"``. If this behavior is not desired, make sure your log level is
``"INFO"`` or higher.
.. _a known bug in python-keystoneclient: https://bugs.launchpad.net/keystone/+bug/1004114
File Uploads
============
Horizon allows users to upload files via their web browser to other OpenStack
services such as Glance and Swift. Files uploaded through this mechanism are
first stored on the Horizon server before being forwarded on - files are not
uploaded directly or streamed as Horizon receives them. As Horizon itself does
not impose any restrictions on the size of file uploads, production deployments
will want to consider configuring their server hosting the Horizon application
to enforce such a limit to prevent large uploads exhausting system resources
and disrupting services. Deployments using Apache2 can use the
`LimitRequestBody directive`_ to achieve this.
Uploads to the Glance image store service tend to be particularly large - in
the order of hundreds of megabytes to multiple gigabytes. Deployments are able
to disable the ability to upload images through Horizon by setting
``HORIZON_IMAGES_ALLOW_UPLOAD`` to ``False`` in your ``local_settings.py``
file.
.. _LimitRequestBody directive: http://httpd.apache.org/docs/2.2/mod/core.html#limitrequestbody
Session Storage
===============
Horizon uses `Django's sessions framework`_ for handling user session data;
however that's not the end of the story. There are numerous session backends
available, which are controlled through the ``SESSION_ENGINE`` setting in
your ``local_settings.py`` file. What follows is a quick discussion of the
pros and cons of each of the common options as they pertain to deploying
Horizon specifically.
.. _Django's sessions framework: https://docs.djangoproject.com/en/dev/topics/http/sessions/
Local Memory Cache
------------------
Enabled by::
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
CACHES = {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'
}
Local memory storage is the quickest and easiest session backend to set up,
as it has no external dependencies whatsoever. However, it has two significant
drawbacks:
* No shared storage across processes or workers.
* No persistence after a process terminates.
The local memory backend is enabled as the default for Horizon solely because
it has no dependencies. It is not recommended for production use, or even for
serious development work. For better options, read on.
Memcached
---------
Enabled by::
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
CACHES = {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache'
'LOCATION': 'my_memcached_host:11211',
}
External caching using an application such as memcached offers persistence
and shared storage, and can be very useful for small-scale deployment and/or
development. However, for distributed and high-availability scenarios
memcached has inherent problems which are beyond the scope of this
documentation.
Memcached is an extremely fast and efficient cache backend for cases where it
fits the depooyment need. But it's not appropriate for all scenarios.
Requirements:
* Memcached service running and accessible.
* Python memcached module installed.
Database
--------
Enabled by::
SESSION_ENGINE = 'django.core.cache.backends.db.DatabaseCache'
DATABASES = {
'default': {
# Databe configuration here
}
}
Database-backed sessions are scalable (using an appropriate database strategy),
persistent, and can be made high-concurrency and highly-available.
The downside to this approach is that database-backed sessions are one of the
slower session storages, and incur a high overhead under heavy usage. Proper
configuration of your database deployment can also be a substantial
undertaking and is far beyond the scope of this documentation.
Cached Database
---------------
To mitigate the performance issues of database queries, you can also consider
using Django's ``cached_db`` session backend which utilizes both your database
and caching infrastructure to perform write-through caching and efficient
retrieval. You can enable this hybrid setting by configuring both your database
and cache as discussed above and then using::
SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
Cookies
-------
If you're using Django 1.4 or later, a new session backend is available to you
which avoids server load and scaling problems: the ``signed_cookies`` backend!
This backend stores session data in a cookie which is stored by the
user's browser. The backend uses a cryptographic signing technique to ensure
session data is not tampered with during transport (**this is not the same
as encryption, session data is still readable by an attacker**).
The pros of this session engine are that it doesn't require any additional
dependencies or infrastructure overhead, and it scales indefinitely as long
as the quantity of session data being stored fits into a normal cookie.
The biggest downside is that it places session data into storage on the user's
machine and transports it over the wire. It also limits the quantity of
session data which can be stored.
For a thorough discussion of the security implications of this session backend,
please read the `Django documentation on cookie-based sessions`_.
.. _Django documentation on cookie-based sessions: https://docs.djangoproject.com/en/dev/topics/http/sessions/#using-cookie-based-sessions
Secure Site Recommendations
---------------------------
When implementing Horizon for public usage, with the website served through
HTTPS, it is recommended that the following settings are applied.
To help protect the session cookies from `cross-site scripting`_, add the
following to ``local_settings.py`` :
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
Note that the CSRF_COOKIE_SECURE option is only available from Django 1.4. It
does no harm to have the setting in earlier versions, but it does not take effect.
You can also disable `browser autocompletion`_ for the authentication form by
changing the ``password_autocomplete`` attribute to ``off`` in ``horizon/conf/default.py``
.. _cross-site scripting: https://www.owasp.org/index.php/HttpOnly
.. _browser autocompletion: https://wiki.mozilla.org/The_autocomplete_attribute_and_web_documents_using_XHTML

View File

@ -1,129 +0,0 @@
======================
DataTables Topic Guide
======================
Horizon provides the :mod:`horizon.tables` module to provide
a convenient, reusable API for building data-driven displays and interfaces.
The core components of this API fall into three categories: ``DataTables``,
``Actions``, and ``Class-based Views``.
.. seealso::
For a detailed API information check out the :doc:`DataTables Reference
Guide </ref/tables>`.
Tables
======
The majority of interface in a dashboard-style interface ends up being
tabular displays of the various resources the dashboard interacts with.
The :class:`~horizon.tables.DataTable` class exists so you don't have to
reinvent the wheel each time.
Creating your own tables
------------------------
Creating a table is fairly simple:
#. Create a subclass of :class:`~horizon.tables.DataTable`.
#. Define columns on it using :class:`~horizon.tables.Column`.
#. Create an inner ``Meta`` class to contain the special options for
this table.
#. Define any actions for the table, and add them to
:attr:`~horizon.tables.DataTableOptions.table_actions` or
:attr:`~horizon.tables.DataTableOptions.row_actions`.
Examples of this can be found in any of the ``tables.py`` modules included
in the reference modules under ``horizon.dashboards``.
Connecting a table to a view
----------------------------
Once you've got your table set up the way you like it, the next step is to
wire it up to a view. To make this as easy as possible Horizon provides the
:class:`~horizon.tables.DataTableView` class-based view which can be subclassed
to display your table with just a couple lines of code. At it's simplest it
looks like this::
from horizon import tables
from .tables import MyTable
class MyTableView(tables.DataTableView):
table_class = MyTable
template_name = "my_app/my_table_view.html"
def get_data(self):
return my_api.objects.list()
In the template you would just need to include the following to render the
table::
{{ table.render }}
That's it! Easy, right?
Actions
=======
Actions comprise any manipulations that might happen on the data in the table
or the table itself. For example, this may be the standard object CRUD, linking
to related views based on the object's id, filtering the data in the table,
or fetching updated data when appropriate.
When actions get run
--------------------
There are two points in the request-response cycle in which actions can
take place; prior to data being loaded into the table, and after the data
is loaded. When you're using one of the pre-built class-based views for
working with your tables the pseudo-workflow looks like this:
#. The request enters view.
#. The table class is instantiated without data.
#. Any "preemptive" actions are checked to see if they should run.
#. Data is fetched and loaded into the table.
#. All other actions are checked to see if they should run.
#. If none of the actions have caused an early exit from the view,
the standard response from the view is returned (usually the
rendered table).
The benefit of the multi-step table instantiation is that you can use
preemptive actions which don't need access to the entire collection of data
to save yourself on processing overhead, API calls, etc.
Basic actions
-------------
At their simplest, there are three types of actions: actions which act on the
data in the table, actions which link to related resources, and actions that
alter which data is displayed. These correspond to
:class:`~horizon.tables.Action`, :class:`~horizon.tables.LinkAction`, and
:class:`~horizon.tables.FilterAction`.
Writing your own actions generally starts with subclassing one of those
action classes and customizing the designated attributes and methods.
Shortcut actions
----------------
There are several common tasks for which Horizon provides pre-built shortcut
classes. These include :class:`~horizon.tables.BatchAction`, and
:class:`~horizon.tables.DeleteAction`. Each of these abstracts away nearly
all of the boilerplate associated with writing these types of actions and
provides consistent error handling, logging, and user-facing interaction.
It is worth noting that ``BatchAction`` and ``DeleteAction`` are extensions
of the standard ``Action`` class.
Preemptive actions
------------------
Action classes which have their :attr:`~horizon.tables.Action.preempt`
attribute set to ``True`` will be evaluated before any data is loaded into
the table. As such, you must be careful not to rely on any table methods that
require data, such as :meth:`~horizon.tables.DataTable.get_object_display` or
:meth:`~horizon.tables.DataTable.get_object_by_id`. The advantage of preemptive
actions is that you can avoid having to do all the processing, API calls, etc.
associated with loading data into the table for actions which don't require
access to that information.

View File

@ -1,276 +0,0 @@
===================
Testing Topic Guide
===================
Having good tests in place is absolutely critical for ensuring a stable,
maintainable codebase. Hopefully that doesn't need any more explanation.
However, what defines a "good" test is not always obvious, and there are
a lot of common pitfalls that can easily shoot your test suite in the
foot.
If you already know everything about testing but are fed up with trying to
debug why a specific test failed, you can skip the intro and jump
stright to :ref:`debugging_unit_tests`.
An overview of testing
======================
There are three main types of tests, each with their associated pros and cons:
Unit tests
----------
These are isolated, stand-alone tests with no external dependencies. They are
written from the a perspective of "knowing the code", and test the assumptions
of the codebase and the developer.
Pros:
* Generally lightweight and fast.
* Can be run anywhere, anytime since they have no external dependencies.
Cons:
* Easy to be lax in writing them, or lazy in constructing them.
* Can't test interactions with live external services.
Functional tests
----------------
These are generally also isolated tests, though sometimes they may interact
with other services running locally. The key difference between functional
tests and unit tests, however, is that functional tests are written from the
perspective of the user (who knows nothing about the code) and only knows
what they put in and what they get back. Essentially this is a higher-level
testing of "does the result match the spec?".
Pros:
* Ensures that your code *always* meets the stated functional requirements.
* Verifies things from an "end user" perspective, which helps to ensure
a high-quality experience.
* Designing your code with a functional testing perspective in mind helps
keep a higher-level viewpoint in mind.
Cons:
* Requires an additional layer of thinking to define functional requirements
in terms of inputs and outputs.
* Often requires writing a separate set of tests and/or using a different
testing framework from your unit tests.
* Don't offer any insight into the quality or status of the underlying code,
only verifies that it works or it doesn't.
Integration Tests
-----------------
This layer of testing involves testing all of the components that your
codebase interacts with or relies on in conjunction. This is equivalent to
"live" testing, but in a repeatable manner.
Pros:
* Catches *many* bugs that unit and functional tests will not.
* Doesn't rely on assumptions about the inputs and outputs.
* Will warn you when changes in external components break your code.
Cons:
* Difficult and time-consuming to create a repeatable test environment.
* Did I mention that setting it up is a pain?
So what should I write?
-----------------------
A few simple guidelines:
#. Every bug fix should have a regression test. Period.
#. When writing a new feature, think about writing unit tests to verify
the behavior step-by-step as you write the feature. Every time you'd
go to run your code by hand and verify it manually, think "could I
write a test to do this instead?". That way when the feature is done
and you're ready to commit it you've already got a whole set of tests
that are more thorough than anything you'd write after the fact.
#. Write tests that hit every view in your application. Even if they
don't assert a single thing about the code, it tells you that your
users aren't getting fatal errors just by interacting with your code.
What makes a good unit test?
============================
Limiting our focus just to unit tests, there are a number of things you can
do to make your unit tests as useful, maintainable, and unburdensome as
possible.
Test data
---------
Use a single, consistent set of test data. Grow it over time, but do everything
you can not to fragment it. It quickly becomes unmaintainable and perniciously
out-of-sync with reality.
Make your test data as accurate to reality as possible. Supply *all* the
attributes of an object, provide objects in all the various states you may want
to test.
If you do the first suggestion above *first* it makes the second one far less
painful. Write once, use everywhere.
To make your life even easier, if your codebase doesn't have a built-in
ORM-like function to manage your test data you can consider buidling (or
borrowing) one yourself. Being able to do simple retrieval queries on your
test data is incredibly valuable.
Mocking
-------
Mocking is the practice of providing stand-ins for objects or pieces of code
you don't need to test. While convenient, they should be used with *extreme*
caution.
Why? Because overuse of mocks can rapidly land you in a situation where you're
not testing any real code. All you've done is verified that your mocking
framework returns what you tell it to. This problem can be very tricky to
recognize, since you may be mocking things in ``setUp`` methods, other modules,
etc.
A good rule of thumb is to mock as close to the source as possible. If you have
a function call that calls an external API in a view , mock out the external
API, not the whole function. If you mock the whole function you've suddenly
lost test coverage for an entire chunk of code *inside* your codebase. Cut the
ties cleanly right where your system ends and the external world begins.
Similarly, don't mock return values when you could construct a real return
value of the correct type with the correct attributes. You're just adding
another point of potential failure by exercising your mocking framework instead
of real code. Following the suggestions for testing above will make this a lot
less burdensome.
Assertions and verification
---------------------------
Think long and hard about what you really want to verify in your unit test. In
particular, think about what custom logic your code executes.
A common pitfall is to take a known test object, pass it through your code,
and then verify the properties of that object on the output. This is all well
and good, except if you're verifying properties that were untouched by your
code. What you want to check are the pieces that were *changed*, *added*, or
*removed*. Don't check the object's id attribute unless you have reason to
suspect it's not the object you started with. But if you added a new attribute
to it, be damn sure you verify that came out right.
It's also very common to avoid testing things you really care about because
it's more difficult. Verifying that the proper messages were displayed to the
user after an action, testing for form errors, making sure exception handling
is tested... these types of things aren't always easy, but they're extremely
necessary.
To that end, Horizon includes several custom assertions to make these tasks
easier. :meth:`~horizon.test.helpers.TestCase.assertNoFormErrors`,
:meth:`~horizon.test.helpers.TestCase.assertMessageCount`, and
:meth:`~horizon.test.helpers.TestCase.asertNoMessages` all exist for exactly
these purposes. Moreover, they provide useful output when things go wrong so
you're not left scratching your head wondering why your view test didn't
redirect as expected when you posted a form.
.. _debugging_unit_tests:
Debugging Unit Tests
====================
Tips and tricks
---------------
#. Use :meth:`~horizon.test.helpers.TestCase.assertNoFormErrors` immediately
after your ``client.post`` call for tests that handle form views. This will
immediately fail if your form POST failed due to a validation error and
tell you what the error was.
#. Use :meth:`~horizon.test.helpers.TestCase.assertMessageCount` and
:meth:`~horizon.test.helpers.TestCase.asertNoMessages` when a piece of code
is failing inexplicably. Since the core error handlers attach user-facing
error messages (and since the core logging is silenced during test runs)
these methods give you the dual benefit of verifying the output you expect
while clearly showing you the problematic error message if they fail.
#. Use Python's ``pdb`` module liberally. Many people don't realize it works
just as well in a test case as it does in a live view. Simply inserting
``import pdb; pdb.set_trace()`` anywhere in your codebase will drop the
interpreter into an interactive shell so you can explore your test
environment and see which of your assumptions about the code isn't,
in fact, flawlessly correct.
Common pitfalls
---------------
There are a number of typical (and non-obvious) ways to break the unit tests.
Some common things to look for:
#. Make sure you stub out the method exactly as it's called in the code
being tested. For example, if your real code calls
``api.keystone.tenant_get``, stubbing out ``api.tenant_get`` (available
for legacy reasons) will fail.
#. When defining the expected input to a stubbed call, make sure the
arguments are *identical*, this includes ``str`` vs. ``int`` differences.
#. Make sure your test data are completely in line with the expected inputs.
Again, ``str`` vs. ``int`` or missing properties on test objects will
kill your tests.
#. Make sure there's nothing amiss in your templates (particularly the
``{% url %}`` tag and its arguments). This often comes up when refactoring
views or renaming context variables. It can easily result in errors that
you might not stumble across while clicking around the development server.
#. Make sure you're not redirecting to views that no longer exist, e.g.
the ``index`` view for a panel that got combined (such as instances &
volumes).
#. Make sure your mock calls are in order before calling ``mox.ReplayAll``.
The order matters.
#. Make sure you repeat any stubbed out method calls that happen more than
once. They don't automatically repeat, you have to explicitly define them.
While this is a nuisance, it makes you acutely aware of how many API
calls are involved in a particular function.
Understanding the output from ``mox``
-------------------------------------
Horizon uses ``mox`` as its mocking framework of choice, and while it
offers many nice features, its output when a test fails can be quite
mysterious.
Unexpected Method Call
~~~~~~~~~~~~~~~~~~~~~~
This occurs when you stubbed out a piece of code, and it was subsequently
called in a way that you didn't specify it would be. There are two reasons
this tends to come up:
#. You defined the expected call, but a subtle difference crept in. This
may be a string versus integer difference, a string versus unicode
difference, a slightly off date/time, or passing a name instead of an id.
#. The method is actually being called *multiple times*. Since mox uses
a call stack internally, it simply pops off the expected method calls to
verify them. That means once a call is used once, it's gone. An easy way
to see if this is the case is simply to copy and paste your method call a
second time to see if the error changes. If it does, that means your method
is being called more times than you think it is.
Expected Method Never Called
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This one is the opposite of the unexpected method call. This one means you
tol mox to expect a call and it didn't happen. This is almost always the
result of an error in the conditions of the test. Using the
:meth:`~horizon.test.helpers.TestCase.assertNoFormErrors` and
:meth:`~horizon.test.helpers.TestCase.assertMessageCount` will make it readily
apparent what the problem is in the majority of cases. If not, then use ``pdb``
and start interrupting the code flow to see where things are getting off track.

View File

@ -1,545 +0,0 @@
===================
Building on Horizon
===================
This tutorial covers how to use the various components in Horizon to build
an example dashboard and panel with a data table and tabs.
As an example, we'll build on the Nova instances API to create a new and novel
"visualizations" dashboard with a "flocking" panel that presents the instance
data in a different manner.
You can find a reference implementation of the code being described here
on github at https://github.com/gabrielhurley/horizon_demo.
.. note::
There are a variety of other resources which may be helpful to read first,
since this is a more advanced tutorial. For example, you may want to start
with the :doc:`Horizon quickstart guide </quickstart>` or the
`Django tutorial`_.
.. _Django tutorial: https://docs.djangoproject.com/en/1.4/intro/tutorial01/
Creating a dashboard
====================
.. note::
It is perfectly valid to create a panel without a dashboard, and
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
---------
The recommended structure for a dashboard (or panel) follows suit with the
typical Django application layout. We'll name our dashboard "visualizations"::
visualizations
|--__init__.py
|--dashboard.py
|--templates/
|--static/
The ``dashboard.py`` module will contain our dashboard class for use by
Horizon; the ``templates`` and ``static`` directories give us homes for our
Django template files and static media respectively.
Within the ``static`` and ``templates`` directories it's generally good to
namespace your files like so::
templates/
|--visualizations/
static/
|--visualizations/
|--css/
|--js/
|--img/
With those files and directories in place, we can move on to writing our
dashboard class.
Defining a dashboard
--------------------
A dashboard class can be incredibly simple (about 3 lines at minimum),
defining nothing more than a name and a slug::
import horizon
class VizDash(horizon.Dashboard):
name = _("Visualizations")
slug = "visualizations"
In practice, a dashboard class will usually contain more information, such as a
list of panels, which panel is the default, and any permissions required to
access this dashboard::
class VizDash(horizon.Dashboard):
name = _("Visualizations")
slug = "visualizations"
panels = ('flocking',)
default_panel = 'flocking'
permissions = ('openstack.roles.admin',)
Building from that previous example we may also want to define a grouping of
panels which share a common theme and have a sub-heading in the navigation::
class InstanceVisualizations(horizon.PanelGroup):
slug = "instance_visualizations"
name = _("Instance Visualizations")
panels = ('flocking',)
class VizDash(horizon.Dashboard):
name = _("Visualizations")
slug = "visualizations"
panels = (InstanceVisualizations,)
default_panel = 'flocking'
permissions = ('openstack.roles.admin',)
The ``PanelGroup`` can be added to the dashboard class' ``panels`` list
just like the slug of the panel can.
Once our dashboard class is complete, all we need to do is register it::
horizon.register(VizDash)
The typical place for that would be the bottom of the ``dashboard.py`` file,
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. 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
---------
A panel is a relatively flat structure with the exception that templates
for a panel in a dashboard live in the dashboard's ``templates`` directory
rather than in the panel's ``templates`` directory. Continuing our
vizulaization/flocking example, let's see what the looks like::
# stand-alone panel structure
flocking/
|--__init__.py
|--panel.py
|--urls.py
|--views.py
|--templates/
|--flocking/
|--index.html
# panel-in-a-dashboard structure
visualizations/
|--__init__.py
|--dashboard.py
|--flocking/
|--__init__.py
|--panel.py
|--urls.py
|--views.py
|--templates/
|--visualizations/
|--flocking/
|--index.html
That follows standard Django namespacing conventions for apps and submodules
within apps. It also works cleanly with Django's automatic template discovery
in both cases.
Defining a panel
----------------
The ``panel.py`` file referenced above has a special meaning. Within a
dashboard, any module name listed in the ``panels`` attribute on the
dashboard class will be auto-discovered by looking for ``panel.py`` file
in a corresponding directory (the details are a bit magical, but have been
thoroughly vetted in Django's admin codebase).
Inside the ``panel.py`` module we define our ``Panel`` class::
class Flocking(horizon.Panel):
name = _("Flocking")
slug = 'flocking'
Simple, right? Once we've defined it, we register it with the dashboard::
from visualizations import dashboard
dashboard.VizDash.register(Flocking)
Easy! There are more options you can set to customize the ``Panel`` class, but
it makes some intelligent guesses about what the defaults should be.
URLs
----
One of the intelligent assumptions the ``Panel`` class makes is that it can
find a ``urls.py`` file in your panel directory which will define a view named
``index`` that handles the default view for that panel. This is what your
``urls.py`` file might look like::
from django.conf.urls.defaults import patterns, url
from .views import IndexView
urlpatterns = patterns('',
url(r'^$', IndexView.as_view(), name='index')
)
There's nothing there that isn't 100% standard Django code. This example
(and Horizon in general) uses the class-based views introduced in Django 1.3
to make code more reusable. Hence the view class is imported in the example
above, and the ``as_view()`` method is called in the URL pattern.
This, of course, presumes you have a view class, and takes us into the meat
of writing a ``Panel``.
Tables, Tabs, and Views
-----------------------
Now we get to the really exciting parts; everything before this was structural.
Starting with the high-level view, our end goal is to create a view (our
``IndexView`` class referenced above) which uses Horizon's ``DataTable``
class to display data and Horizon's ``TabGroup`` class to give us a
user-friendly tabbed interface in the browser.
We'll start with the table, combine that with the tabs, and then build our
view from the pieces.
Defining a table
~~~~~~~~~~~~~~~~
Horizon provides a :class:`~horizon.tables.DataTable` class which simplifies
the vast majority of displaying data to an end-user. We're just going to skim
the surface here, but it has a tremendous number of capabilities.
In this case, we're going to be presenting data about tables, so let's start
defining our table (and a ``tables.py`` module::
from horizon import tables
class FlockingInstancesTable(tables.DataTable):
host = tables.Column("OS-EXT-SRV-ATTR:host", verbose_name=_("Host"))
tenant = tables.Column('tenant_name', verbose_name=_("Tenant"))
user = tables.Column('user_name', verbose_name=_("user"))
vcpus = tables.Column('flavor_vcpus', verbose_name=_("VCPUs"))
memory = tables.Column('flavor_memory', verbose_name=_("Memory"))
age = tables.Column('age', verbose_name=_("Age"))
class Meta:
name = "instances"
verbose_name = _("Instances")
There are several things going on here... we created a table subclass,
and defined six columns on it. Each of those columns defines what attribute
it accesses on the instance object as the first argument, and since we like to
make everything translatable, we give each column a ``verbose_name`` that's
marked for translation.
Lastly, we added a ``Meta`` class which defines some properties about our
table, notably it's (translatable) verbose name, and a semi-unique "slug"-like
name to identify it.
.. note::
This is a slight simplification from the reality of how the instance
object is actually structured. In reality, accessing the flavor, tenant,
and user attributes on it requires an additional step. This code can be
seen in the example code available on github.
Defining tabs
~~~~~~~~~~~~~
So we have a table, ready to receive our data. We could go straight to a view
from here, but we can think bigger. In this case we're also going to use
Horizon's :class:`~horizon.tabs.TabGroup` class. This gives us a clean,
no-fuss tabbed interface to display both our visualization and, optionally,
our data table.
First off, let's make a tab for our visualization::
class VizTab(tabs.Tab):
name = _("Visualization")
slug = "viz"
template_name = "visualizations/flocking/_flocking.html"
def get_context_data(self, request):
return None
This is about as simple as you can get. Since our visualization will
ultiimately use AJAX to load it's data we don't need to pass any context
to the template, and all we need to define is the name and which template
it should use.
Now, we also need a tab for our data table::
from .tables import FlockingInstancesTable
class DataTab(tabs.TableTab):
name = _("Data")
slug = "data"
table_classes = (FlockingInstancesTable,)
template_name = "horizon/common/_detail_table.html"
preload = False
def get_instances_data(self):
try:
instances = utils.get_instances_data(self.tab_group.request)
except:
instances = []
exceptions.handle(self.tab_group.request,
_('Unable to retrieve instance list.'))
return instances
This tab gets a little more complicated. Foremost, it's a special type of
tab--one that handles data tables (and all their associated features)--and
it also uses the ``preload`` attribute to specify that this tab shouldn't
be loaded by default. It will instead be loaded via AJAX when someone clicks
on it, saving us on API calls in the vast majority of cases.
Lastly, this code introduces the concept of error handling in Horizon.
The :func:`horizon.exceptions.handle` function is a centralized error
handling mechanism that takes all the guess-work and inconsistency out of
dealing with exceptions from the API. Use it everywhere.
Tying it together in a view
~~~~~~~~~~~~~~~~~~~~~~~~~~~
There are lots of pre-built class-based views in Horizon. We try to provide
starting points for all the common combinations of components.
In this case we want a starting view type that works with both tabs and
tables... that'd be the :class:`~horizon.tabs.TabbedTableView` class. It takes
the best of the dynamic delayed-loading capabilities tab groups provide and
mixes in the actions and AJAX-updating that tables are capable of with almost
no work on the user's end. Let's see what the code would look like::
from .tables import FlockingInstancesTable
from .tabs import FlockingTabs
class IndexView(tabs.TabbedTableView):
tab_group_class = FlockingTabs
table_class = FlockingInstancesTable
template_name = 'visualizations/flocking/index.html'
That would get us 100% of the way to what we need if this particular
demo didn't involve an extra AJAX call to fetch back our visualization
data via AJAX. Because of that we need to override the class' ``get()``
method to return the right data for an AJAX call::
from .tables import FlockingInstancesTable
from .tabs import FlockingTabs
class IndexView(tabs.TabbedTableView):
tab_group_class = FlockingTabs
table_class = FlockingInstancesTable
template_name = 'visualizations/flocking/index.html'
def get(self, request, *args, **kwargs):
if self.request.is_ajax() and self.request.GET.get("json", False):
try:
instances = utils.get_instances_data(self.request)
except:
instances = []
exceptions.handle(request,
_('Unable to retrieve instance list.'))
data = json.dumps([i._apiresource._info for i in instances])
return http.HttpResponse(data)
else:
return super(IndexView, self).get(request, *args, **kwargs)
In this instance, we override the ``get()`` method such that if it's an
AJAX request and has the GET parameter we're looking for, it returns our
instance data in JSON format; otherwise it simply returns the view function
as per the usual.
The template
~~~~~~~~~~~~
We need three templates here: one for the view, and one for each of our two
tabs. The view template (in this case) can inherit from one of the other
dashboards::
{% extends 'syspanel/base.html' %}
{% load i18n %}
{% block title %}{% trans "Flocking" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Flocking") %}
{% endblock page_header %}
{% block syspanel_main %}
<div class="row-fluid">
<div class="span12">
{{ tab_group.render }}
</div>
</div>
{% endblock %}
This gives us a custom page title, a header, and render our tab group provided
by the view.
For the tabs, the one using the table is handled by a reusable template,
``"horizon/common/_detail_table.html"``. This is appropriate for any tab that
only displays a single table.
The second tab is a bit of secret sauce for the visualization, but it's still
quite simple and can be investigated in the github example.
The takeaway here is that each tab needs a template associated with it.
With all our code in place, the only thing left to do is to integrated it into
our OpenStack Dashboard site.
Setting up a project
====================
The vast majority of people will just customize the OpenStack Dashboard
example project that ships with Horizon. As such, this tutorial will
start from that and just illustrate the bits that can be customized.
Structure
---------
A site built on Horizon takes the form of a very typical Django project::
site/
|--__init__.py
|--manage.py
|--demo_dashboard/
|--__init__.py
|--models.py # required for Django even if unused
|--settings.py
|--templates/
|--static/
The key bits here are that ``demo_dashboard`` is on our python path, and that
the `settings.py`` file here will contain our customized Horizon config.
The settings file
-----------------
There are several key things you will generally want to customiz in your
site's settings file: specifying custom dashboards and panels, catching your
client's exception classes, and (possibly) specifying a file for advanced
overrides.
Specifying dashboards
~~~~~~~~~~~~~~~~~~~~~
The most basic thing to do is to add your own custom dashboard using the
``HORIZON_CONFIG`` dictionary in the settings file::
HORIZON_CONFIG = {
'dashboards': ('nova', 'syspanel', 'visualizations', 'settings',),
}
In this case, we've taken the default Horizon ``'dashboards'`` config and
added our ``visualizations`` dashboard to it. Note that the name here is the
name of the dashboard's module on the python path. It will find our
``dashboard.py`` file inside of it and load both the dashboard and its panels
automatically from there.
Error handling
~~~~~~~~~~~~~~
Adding custom error handler for your API client is quite easy. While it's not
necessary for this example, it would be done by customizing the
``'exceptions'`` value in the ``HORIZON_CONFIG`` dictionary::
import my_api.exceptions as my_api
'exceptions': {'recoverable': [my_api.Error,
my_api.ClientConnectionError],
'not_found': [my_api.NotFound],
'unauthorized': [my_api.NotAuthorized]},
.. _overrides:
Override file
~~~~~~~~~~~~~
The override file is the "god-mode" dashboard editor. The hook for this file
sits right between the automatic discovery mechanisms and the final setup
routines for the entire site. By specifying an override file you can alter
any behavior you like in existing code. This tutorial won't go in-depth,
but let's just say that with great power comes great responsibility.
To specify am override file, you set the ``'customization_module'`` value in
the ``HORIZON_CONFIG`` dictionary to the dotted python path of your
override module::
HORIZON_CONFIG = {
'customization_module': 'demo_dashboard.overrides'
}
This file is capable of adding dashboards, adding panels to existing
dashboards, renaming existing dashboards and panels (or altering other
attributes on them), removing panels from existing dashboards, and so on.
We could say more, but it only gets more dangerous...
Conclusion
==========
Sadly, the cake was a lie. The information in this "tutorial" was never
meant to leave you with a working dashboard. It's close. But there's
waaaaaay too much javascript involved in the visualization to cover it all
here, and it'd be irrelevant to Horizon anyway.
If you want to see the finished product, check out the github example
referenced at the beginning of this tutorial.
Clone the repository and simply run ``./run_tests.sh --runserver``. That'll
give you a 100% working dashboard that uses every technique in this tutorial.
What you've learned here, however, is the fundamentals of almost everything
you need to know to start writing interfaces for your own project based on the
components Horizon provides.
If you have questions, or feedback on how this tutorial could be improved,
please feel free to pass them along!

View File

@ -1,49 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
""" The Horizon interface.
Contains the core Horizon classes--:class:`~horizon.Dashboard` and
:class:`horizon.Panel`--the dynamic URLconf for Horizon, and common interface
methods like :func:`~horizon.register` and :func:`~horizon.unregister`.
"""
# Because this module is compiled by setup.py before Django may be installed
# in the environment we try importing Django and issue a warning but move on
# should that fail.
Horizon = None
try:
from horizon.base import Horizon, Dashboard, Panel, PanelGroup
except ImportError:
import warnings
def simple_warn(message, category, filename, lineno, file=None, line=None):
return '%s: %s' % (category.__name__, message)
msg = ("Could not import Horizon dependencies. "
"This is normal during installation.\n")
warnings.formatwarning = simple_warn
warnings.warn(msg, Warning)
if Horizon:
register = Horizon.register
unregister = Horizon.unregister
get_absolute_url = Horizon.get_absolute_url
get_user_home = Horizon.get_user_home
get_dashboard = Horizon.get_dashboard
get_default_dashboard = Horizon.get_default_dashboard
get_dashboards = Horizon.get_dashboards
urls = Horizon._lazy_urls

View File

@ -1,788 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Contains the core classes and functionality that makes Horizon what it is.
This module is considered internal, and should not be relied on directly.
Public APIs are made available through the :mod:`horizon` module and
the classes contained therein.
"""
import collections
import copy
import inspect
import logging
import os
from django.conf import settings
from django.conf.urls.defaults import patterns, url, include
from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse
from django.utils.datastructures import SortedDict
from django.utils.functional import SimpleLazyObject
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 import conf
from horizon.decorators import require_auth, require_perms, _current_component
LOG = logging.getLogger(__name__)
def _decorate_urlconf(urlpatterns, decorator, *args, **kwargs):
for pattern in urlpatterns:
if getattr(pattern, 'callback', None):
pattern._callback = decorator(pattern.callback, *args, **kwargs)
if getattr(pattern, 'url_patterns', []):
_decorate_urlconf(pattern.url_patterns, decorator, *args, **kwargs)
class NotRegistered(Exception):
pass
class HorizonComponent(object):
def __init__(self):
super(HorizonComponent, self).__init__()
if not self.slug:
raise ImproperlyConfigured('Every %s must have a slug.'
% self.__class__)
def __unicode__(self):
name = getattr(self, 'name', u"Unnamed %s" % self.__class__.__name__)
return unicode(name)
def _get_default_urlpatterns(self):
package_string = '.'.join(self.__module__.split('.')[:-1])
if getattr(self, 'urls', None):
try:
mod = import_module('.%s' % self.urls, package_string)
except ImportError:
mod = import_module(self.urls)
urlpatterns = mod.urlpatterns
else:
# Try importing a urls.py from the dashboard package
if module_has_submodule(import_module(package_string), 'urls'):
urls_mod = import_module('.urls', package_string)
urlpatterns = urls_mod.urlpatterns
else:
urlpatterns = patterns('')
return urlpatterns
class Registry(object):
def __init__(self):
self._registry = {}
if not getattr(self, '_registerable_class', None):
raise ImproperlyConfigured('Subclasses of Registry must set a '
'"_registerable_class" property.')
def _register(self, cls):
"""Registers the given class.
If the specified class is already registered then it is ignored.
"""
if not inspect.isclass(cls):
raise ValueError('Only classes may be registered.')
elif not issubclass(cls, self._registerable_class):
raise ValueError('Only %s classes or subclasses may be registered.'
% self._registerable_class.__name__)
if cls not in self._registry:
cls._registered_with = self
self._registry[cls] = cls()
return self._registry[cls]
def _unregister(self, cls):
"""Unregisters the given class.
If the specified class isn't registered, ``NotRegistered`` will
be raised.
"""
if not issubclass(cls, self._registerable_class):
raise ValueError('Only %s classes or subclasses may be '
'unregistered.' % self._registerable_class)
if cls not in self._registry.keys():
raise NotRegistered('%s is not registered' % cls)
del self._registry[cls]
return True
def _registered(self, cls):
if inspect.isclass(cls) and issubclass(cls, self._registerable_class):
found = self._registry.get(cls, None)
if found:
return found
else:
# Allow for fetching by slugs as well.
for registered in self._registry.values():
if registered.slug == cls:
return registered
class_name = self._registerable_class.__name__
if hasattr(self, "_registered_with"):
parent = self._registered_with._registerable_class.__name__
raise NotRegistered('%(type)s with slug "%(slug)s" is not '
'registered with %(parent)s "%(name)s".'
% {"type": class_name,
"slug": cls,
"parent": parent,
"name": self.slug})
else:
slug = getattr(cls, "slug", cls)
raise NotRegistered('%(type)s with slug "%(slug)s" is not '
'registered.' % {"type": class_name,
"slug": slug})
class Panel(HorizonComponent):
""" A base class for defining Horizon dashboard panels.
All Horizon dashboard panels should extend from this class. It provides
the appropriate hooks for automatically constructing URLconfs, and
providing permission-based access control.
.. attribute:: name
The name of the panel. This will be displayed in the
auto-generated navigation and various other places.
Default: ``''``.
.. attribute:: slug
A unique "short name" for the panel. The slug is used as
a component of the URL path for the panel. Default: ``''``.
.. attribute:: permissions
A list of permission names, all of which a user must possess in order
to access any view associated with this panel. This attribute
is combined cumulatively with any permissions required on the
``Dashboard`` class with which it is registered.
.. attribute:: urls
Path to a URLconf of views for this panel using dotted Python
notation. If no value is specified, a file called ``urls.py``
living in the same package as the ``panel.py`` file is used.
Default: ``None``.
.. attribute:: nav
.. method:: nav(context)
The ``nav`` attribute can be either boolean value or a callable
which accepts a ``RequestContext`` object as a single argument
to control whether or not this panel should appear in
automatically-generated navigation. Default: ``True``.
.. attribute:: index_url_name
The ``name`` argument for the URL pattern which corresponds to
the index view for this ``Panel``. This is the view that
:meth:`.Panel.get_absolute_url` will attempt to reverse.
"""
name = ''
slug = ''
urls = None
nav = True
index_url_name = "index"
def __repr__(self):
return "<Panel: %s>" % self.slug
def get_absolute_url(self):
""" Returns the default URL for this panel.
The default URL is defined as the URL pattern with ``name="index"`` in
the URLconf for this panel.
"""
try:
return reverse('horizon:%s:%s:%s' % (self._registered_with.slug,
self.slug,
self.index_url_name))
except Exception as exc:
# Logging here since this will often be called in a template
# where the exception would be hidden.
LOG.info("Error reversing absolute URL for %s: %s" % (self, exc))
raise
@property
def _decorated_urls(self):
urlpatterns = self._get_default_urlpatterns()
# Apply access controls to all views in the patterns
permissions = getattr(self, 'permissions', [])
_decorate_urlconf(urlpatterns, require_perms, permissions)
_decorate_urlconf(urlpatterns, _current_component, panel=self)
# Return the three arguments to django.conf.urls.defaults.include
return urlpatterns, self.slug, self.slug
class PanelGroup(object):
""" A container for a set of :class:`~horizon.Panel` classes.
When iterated, it will yield each of the ``Panel`` instances it
contains.
.. attribute:: slug
A unique string to identify this panel group. Required.
.. attribute:: name
A user-friendly name which will be used as the group heading in
places such as the navigation. Default: ``None``.
.. attribute:: panels
A list of panel module names which should be contained within this
grouping.
"""
def __init__(self, dashboard, slug=None, name=None, panels=None):
self.dashboard = dashboard
self.slug = slug or getattr(self, "slug", "default")
self.name = name or getattr(self, "name", None)
# Our panels must be mutable so it can be extended by others.
self.panels = list(panels or getattr(self, "panels", []))
def __repr__(self):
return "<%s: %s>" % (self.__class__.__name__, self.slug)
def __unicode__(self):
return self.name
def __iter__(self):
panel_instances = []
for name in self.panels:
try:
panel_instances.append(self.dashboard.get_panel(name))
except NotRegistered, e:
LOG.debug(e)
return iter(panel_instances)
class Dashboard(Registry, HorizonComponent):
""" A base class for defining Horizon dashboards.
All Horizon dashboards should extend from this base class. It provides the
appropriate hooks for automatic discovery of :class:`~horizon.Panel`
modules, automatically constructing URLconfs, and providing
permission-based access control.
.. attribute:: name
The name of the dashboard. This will be displayed in the
auto-generated navigation and various other places.
Default: ``''``.
.. attribute:: slug
A unique "short name" for the dashboard. The slug is used as
a component of the URL path for the dashboard. Default: ``''``.
.. attribute:: panels
The ``panels`` attribute can be either a flat list containing the name
of each panel **module** which should be loaded as part of this
dashboard, or a list of :class:`~horizon.PanelGroup` classes which
define groups of panels as in the following example::
class SystemPanels(horizon.PanelGroup):
slug = "syspanel"
name = _("System Panel")
panels = ('overview', 'instances', ...)
class Syspanel(horizon.Dashboard):
panels = (SystemPanels,)
Automatically generated navigation will use the order of the
modules in this attribute.
Default: ``[]``.
.. warning::
The values for this attribute should not correspond to the
:attr:`~.Panel.name` attributes of the ``Panel`` classes.
They should be the names of the Python modules in which the
``panel.py`` files live. This is used for the automatic
loading and registration of ``Panel`` classes much like
Django's ``ModelAdmin`` machinery.
Panel modules must be listed in ``panels`` in order to be
discovered by the automatic registration mechanism.
.. attribute:: default_panel
The name of the panel which should be treated as the default
panel for the dashboard, i.e. when you visit the root URL
for this dashboard, that's the panel that is displayed.
Default: ``None``.
.. attribute:: permissions
A list of permission names, all of which a user must possess in order
to access any panel registered with this dashboard. This attribute
is combined cumulatively with any permissions required on individual
:class:`~horizon.Panel` classes.
.. attribute:: urls
Optional path to a URLconf of additional views for this dashboard
which are not connected to specific panels. Default: ``None``.
.. attribute:: nav
Optional boolean to control whether or not this dashboard should
appear in automatically-generated navigation. Default: ``True``.
.. attribute:: supports_tenants
Optional boolean that indicates whether or not this dashboard includes
support for projects/tenants. If set to ``True`` this dashboard's
navigation will include a UI element that allows the user to select
project/tenant. Default: ``False``.
.. attribute:: public
Boolean value to determine whether this dashboard can be viewed
without being logged in. Defaults to ``False``.
"""
_registerable_class = Panel
name = ''
slug = ''
urls = None
panels = []
default_panel = None
nav = True
supports_tenants = False
public = False
def __repr__(self):
return "<Dashboard: %s>" % self.slug
def __init__(self, *args, **kwargs):
super(Dashboard, self).__init__(*args, **kwargs)
self._panel_groups = None
def get_panel(self, panel):
"""
Returns the specified :class:`~horizon.Panel` instance registered
with this dashboard.
"""
return self._registered(panel)
def get_panels(self):
"""
Returns the :class:`~horizon.Panel` instances registered with this
dashboard in order, without any panel groupings.
"""
all_panels = []
panel_groups = self.get_panel_groups()
for panel_group in panel_groups.values():
all_panels.extend(panel_group)
return all_panels
def get_panel_group(self, slug):
return self._panel_groups[slug]
def get_panel_groups(self):
registered = copy.copy(self._registry)
panel_groups = []
# Gather our known panels
for panel_group in self._panel_groups.values():
for panel in panel_group:
registered.pop(panel.__class__)
panel_groups.append((panel_group.slug, panel_group))
# Deal with leftovers (such as add-on registrations)
if len(registered):
slugs = [panel.slug for panel in registered.values()]
new_group = PanelGroup(self,
slug="other",
name=_("Other"),
panels=slugs)
panel_groups.append((new_group.slug, new_group))
return SortedDict(panel_groups)
def get_absolute_url(self):
""" Returns the default URL for this dashboard.
The default URL is defined as the URL pattern with ``name="index"``
in the URLconf for the :class:`~horizon.Panel` specified by
:attr:`~horizon.Dashboard.default_panel`.
"""
try:
return self._registered(self.default_panel).get_absolute_url()
except:
# Logging here since this will often be called in a template
# where the exception would be hidden.
LOG.exception("Error reversing absolute URL for %s." % self)
raise
@property
def _decorated_urls(self):
urlpatterns = self._get_default_urlpatterns()
default_panel = None
# Add in each panel's views except for the default view.
for panel in self._registry.values():
if panel.slug == self.default_panel:
default_panel = panel
continue
urlpatterns += patterns('',
url(r'^%s/' % panel.slug, include(panel._decorated_urls)))
# Now the default view, which should come last
if not default_panel:
raise NotRegistered('The default panel "%s" is not registered.'
% self.default_panel)
urlpatterns += patterns('',
url(r'', include(default_panel._decorated_urls)))
# Require login if not public.
if not self.public:
_decorate_urlconf(urlpatterns, require_auth)
# Apply access controls to all views in the patterns
permissions = getattr(self, 'permissions', [])
_decorate_urlconf(urlpatterns, require_perms, permissions)
_decorate_urlconf(urlpatterns, _current_component, dashboard=self)
# Return the three arguments to django.conf.urls.defaults.include
return urlpatterns, self.slug, self.slug
def _autodiscover(self):
""" Discovers panels to register from the current dashboard module. """
if getattr(self, "_autodiscover_complete", False):
return
panels_to_discover = []
panel_groups = []
# If we have a flat iterable of panel names, wrap it again so
# we have a consistent structure for the next step.
if all([isinstance(i, basestring) for i in self.panels]):
self.panels = [self.panels]
# Now iterate our panel sets.
for panel_set in self.panels:
# Instantiate PanelGroup classes.
if not isinstance(panel_set, collections.Iterable) and \
issubclass(panel_set, PanelGroup):
panel_group = panel_set(self)
# Check for nested tuples, and convert them to PanelGroups
elif not isinstance(panel_set, PanelGroup):
panel_group = PanelGroup(self, panels=panel_set)
# Put our results into their appropriate places
panels_to_discover.extend(panel_group.panels)
panel_groups.append((panel_group.slug, panel_group))
self._panel_groups = SortedDict(panel_groups)
# Do the actual discovery
package = '.'.join(self.__module__.split('.')[:-1])
mod = import_module(package)
for panel in panels_to_discover:
try:
before_import_registry = copy.copy(self._registry)
import_module('.%s.panel' % panel, package)
except:
self._registry = before_import_registry
if module_has_submodule(mod, panel):
raise
self._autodiscover_complete = True
@classmethod
def register(cls, panel):
""" Registers a :class:`~horizon.Panel` with this dashboard. """
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. """
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):
def __init__(*args, **kwargs):
raise NotImplementedError()
try:
from django.utils.functional import empty
except ImportError:
#Django 1.3 fallback
empty = None
class LazyURLPattern(SimpleLazyObject):
def __iter__(self):
if self._wrapped is empty:
self._setup()
return iter(self._wrapped)
def __reversed__(self):
if self._wrapped is empty:
self._setup()
return reversed(self._wrapped)
class Site(Registry, HorizonComponent):
""" The overarching class which encompasses all dashboards and panels. """
# Required for registry
_registerable_class = Dashboard
name = "Horizon"
namespace = 'horizon'
slug = 'horizon'
urls = 'horizon.site_urls'
def __repr__(self):
return u"<Site: %s>" % self.slug
@property
def _conf(self):
return conf.HORIZON_CONFIG
@property
def dashboards(self):
return self._conf['dashboards']
@property
def default_dashboard(self):
return self._conf['default_dashboard']
def register(self, dashboard):
""" Registers a :class:`~horizon.Dashboard` with Horizon."""
return self._register(dashboard)
def unregister(self, dashboard):
""" Unregisters a :class:`~horizon.Dashboard` from Horizon. """
return self._unregister(dashboard)
def registered(self, dashboard):
return self._registered(dashboard)
def register_panel(self, dashboard, panel):
dash_instance = self.registered(dashboard)
return dash_instance._register(panel)
def unregister_panel(self, dashboard, panel):
dash_instance = self.registered(dashboard)
if not dash_instance:
raise NotRegistered("The dashboard %s is not registered."
% dashboard)
return dash_instance._unregister(panel)
def get_dashboard(self, dashboard):
""" Returns the specified :class:`~horizon.Dashboard` instance. """
return self._registered(dashboard)
def get_dashboards(self):
""" Returns an ordered tuple of :class:`~horizon.Dashboard` modules.
Orders dashboards according to the ``"dashboards"`` key in
``HORIZON_CONFIG`` or else returns all registered dashboards
in alphabetical order.
Any remaining :class:`~horizon.Dashboard` classes registered with
Horizon but not listed in ``HORIZON_CONFIG['dashboards']``
will be appended to the end of the list alphabetically.
"""
if self.dashboards:
registered = copy.copy(self._registry)
dashboards = []
for item in self.dashboards:
dashboard = self._registered(item)
dashboards.append(dashboard)
registered.pop(dashboard.__class__)
if len(registered):
extra = registered.values()
extra.sort()
dashboards.extend(extra)
return dashboards
else:
dashboards = self._registry.values()
dashboards.sort()
return dashboards
def get_default_dashboard(self):
""" Returns the default :class:`~horizon.Dashboard` instance.
If ``"default_dashboard"`` is specified in ``HORIZON_CONFIG``
then that dashboard will be returned. If not, the first dashboard
returned by :func:`~horizon.get_dashboards` will be returned.
"""
if self.default_dashboard:
return self._registered(self.default_dashboard)
elif len(self._registry):
return self.get_dashboards()[0]
else:
raise NotRegistered("No dashboard modules have been registered.")
def get_user_home(self, user):
""" Returns the default URL for a particular user.
This method can be used to customize where a user is sent when
they log in, etc. By default it returns the value of
:meth:`get_absolute_url`.
An alternative function can be supplied to customize this behavior
by specifying a either a URL or a function which returns a URL via
the ``"user_home"`` key in ``HORIZON_CONFIG``. Each of these
would be valid::
{"user_home": "/home",} # A URL
{"user_home": "my_module.get_user_home",} # Path to a function
{"user_home": lambda user: "/" + user.name,} # A function
{"user_home": None,} # Will always return the default dashboard
This can be useful if the default dashboard may not be accessible
to all users. When user_home is missing from HORIZON_CONFIG,
it will default to the settings.LOGIN_REDIRECT_URL value.
"""
user_home = self._conf['user_home']
if user_home:
if callable(user_home):
return user_home(user)
elif isinstance(user_home, basestring):
# Assume we've got a URL if there's a slash in it
if user_home.find("/") != -1:
return user_home
else:
mod, func = user_home.rsplit(".", 1)
return getattr(import_module(mod), func)(user)
# If it's not callable and not a string, it's wrong.
raise ValueError('The user_home setting must be either a string '
'or a callable object (e.g. a function).')
else:
return self.get_absolute_url()
def get_absolute_url(self):
""" Returns the default URL for Horizon's URLconf.
The default URL is determined by calling
:meth:`~horizon.Dashboard.get_absolute_url`
on the :class:`~horizon.Dashboard` instance returned by
:meth:`~horizon.get_default_dashboard`.
"""
return self.get_default_dashboard().get_absolute_url()
@property
def _lazy_urls(self):
""" Lazy loading for URL patterns.
This method avoids problems associated with attempting to evaluate
the the URLconf before the settings module has been loaded.
"""
def url_patterns():
return self._urls()[0]
return LazyURLPattern(url_patterns), self.namespace, self.slug
def _urls(self):
""" Constructs the URLconf for Horizon from registered Dashboards. """
urlpatterns = self._get_default_urlpatterns()
self._autodiscover()
# Discover each dashboard's panels.
for dash in self._registry.values():
dash._autodiscover()
# Allow for override modules
if self._conf.get("customization_module", None):
customization_module = self._conf["customization_module"]
bits = customization_module.split('.')
mod_name = bits.pop()
package = '.'.join(bits)
mod = import_module(package)
try:
before_import_registry = copy.copy(self._registry)
import_module('%s.%s' % (package, mod_name))
except:
self._registry = before_import_registry
if module_has_submodule(mod, mod_name):
raise
# Compile the dynamic urlconf.
for dash in self._registry.values():
urlpatterns += patterns('',
url(r'^%s/' % dash.slug, include(dash._decorated_urls)))
# Return the three arguments to django.conf.urls.defaults.include
return urlpatterns, self.namespace, self.slug
def _autodiscover(self):
""" Discovers modules to register from ``settings.INSTALLED_APPS``.
This makes sure that the appropriate modules get imported to register
themselves with Horizon.
"""
if not getattr(self, '_registerable_class', None):
raise ImproperlyConfigured('You must set a '
'"_registerable_class" property '
'in order to use autodiscovery.')
# Discover both dashboards and panels, in that order
for mod_name in ('dashboard', 'panel'):
for app in settings.INSTALLED_APPS:
mod = import_module(app)
try:
before_import_registry = copy.copy(self._registry)
import_module('%s.%s' % (app, mod_name))
except:
self._registry = before_import_registry
if module_has_submodule(mod, mod_name):
raise
class HorizonSite(Site):
"""
A singleton implementation of Site such that all dealings with horizon
get the same instance no matter what. There can be only one.
"""
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super(Site, cls).__new__(cls, *args, **kwargs)
return cls._instance
# The one true Horizon
Horizon = HorizonSite()

View File

@ -1,18 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from .base import ResourceBrowser
from .views import ResourceBrowserView

View File

@ -1,150 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django import template
from django.utils.translation import ugettext_lazy as _
from horizon.tables import DataTable
from horizon.utils import html
from .breadcrumb import Breadcrumb
class ResourceBrowser(html.HTMLElement):
"""A class which defines a browser for displaying data.
.. attribute:: name
A short name or slug for the browser.
.. attribute:: verbose_name
A more verbose name for the browser meant for display purposes.
.. attribute:: navigation_table_class
This table displays data on the left side of the browser.
Set the ``navigation_table_class`` attribute with
the desired :class:`~horizon.tables.DataTable` class.
This table class must set browser_table attribute in Meta to
``"navigation"``.
.. attribute:: content_table_class
This table displays data on the right side of the browser.
Set the ``content_table_class`` attribute with
the desired :class:`~horizon.tables.DataTable` class.
This table class must set browser_table attribute in Meta to
``"content"``.
.. attribute:: navigation_kwarg_name
This attribute represents the key of the navigatable items in the
kwargs property of this browser's view.
Defaults to ``"navigation_kwarg"``.
.. attribute:: content_kwarg_name
This attribute represents the key of the content items in the
kwargs property of this browser's view.
Defaults to ``"content_kwarg"``.
.. attribute:: template
String containing the template which should be used to render
the browser. Defaults to ``"horizon/common/_resource_browser.html"``.
.. attribute:: context_var_name
The name of the context variable which will contain the browser when
it is rendered. Defaults to ``"browser"``.
.. attribute:: has_breadcrumb
Indicates if the content table of the browser would have breadcrumb.
Defaults to false.
.. attribute:: breadcrumb_template
This is a template used to render the breadcrumb.
Defaults to ``"horizon/common/_breadcrumb.html"``.
"""
name = None
verbose_name = None
navigation_table_class = None
content_table_class = None
navigation_kwarg_name = "navigation_kwarg"
content_kwarg_name = "content_kwarg"
navigable_item_name = _("Navigation Item")
template = "horizon/common/_resource_browser.html"
context_var_name = "browser"
has_breadcrumb = False
breadcrumb_template = "horizon/common/_breadcrumb.html"
breadcrumb_url = None
def __init__(self, request, tables_dict=None, attrs=None, **kwargs):
super(ResourceBrowser, self).__init__()
self.name = self.name or self.__class__.__name__
self.verbose_name = self.verbose_name or self.name.title()
self.request = request
self.kwargs = kwargs
self.has_breadcrumb = getattr(self, "has_breadcrumb")
if self.has_breadcrumb:
self.breadcrumb_template = getattr(self, "breadcrumb_template")
self.breadcrumb_url = getattr(self, "breadcrumb_url")
if not self.breadcrumb_url:
raise ValueError("You must specify a breadcrumb_url "
"if the has_breadcrumb is set to True.")
self.attrs.update(attrs or {})
self.check_table_class(self.content_table_class, "content_table_class")
self.check_table_class(self.navigation_table_class,
"navigation_table_class")
if tables_dict:
self.set_tables(tables_dict)
def check_table_class(self, cls, attr_name):
if not cls or not issubclass(cls, DataTable):
raise ValueError("You must specify a DataTable subclass for "
"the %s attribute on %s."
% (attr_name, self.__class__.__name__))
def set_tables(self, tables):
"""
Sets the table instances on the browser from a dictionary mapping table
names to table instances (as constructed by MultiTableView).
"""
self.navigation_table = tables[self.navigation_table_class._meta.name]
self.content_table = tables[self.content_table_class._meta.name]
navigation_item = self.kwargs.get(self.navigation_kwarg_name)
content_path = self.kwargs.get(self.content_kwarg_name)
# Tells the navigation table what is selected.
self.navigation_table.current_item_id = navigation_item
if self.has_breadcrumb:
self.prepare_breadcrumb(tables, navigation_item, content_path)
def prepare_breadcrumb(self, tables, navigation_item, content_path):
if self.has_breadcrumb and navigation_item and content_path:
for table in tables.values():
table.breadcrumb = Breadcrumb(self.request,
self.breadcrumb_template,
navigation_item,
content_path,
self.breadcrumb_url)
def render(self):
browser_template = template.loader.get_template(self.template)
extra_context = {self.context_var_name: self}
context = template.RequestContext(self.request, extra_context)
return browser_template.render(context)

View File

@ -1,48 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django import template
from horizon.utils import html
class Breadcrumb(html.HTMLElement):
def __init__(self, request, template, root,
subfolder_path, url, attr=None):
super(Breadcrumb, self).__init__()
self.template = template
self.request = request
self.root = root
self.subfolder_path = subfolder_path
self.url = url
self._subfolders = []
def get_subfolders(self):
if self.subfolder_path and not self._subfolders:
(parent, slash, folder) = self.subfolder_path.strip('/') \
.rpartition('/')
while folder:
path = "%s%s%s/" % (parent, slash, folder)
self._subfolders.insert(0, (folder, path))
(parent, slash, folder) = parent.rpartition('/')
return self._subfolders
def render(self):
""" Renders the table using the template from the table options. """
breadcrumb_template = template.loader.get_template(self.template)
extra_context = {"breadcrumb": self}
context = template.RequestContext(self.request, extra_context)
return breadcrumb_template.render(context)

View File

@ -1,49 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.utils.translation import ugettext_lazy as _
from horizon.tables import MultiTableView
class ResourceBrowserView(MultiTableView):
browser_class = None
def __init__(self, *args, **kwargs):
if not self.browser_class:
raise ValueError("You must specify a ResourceBrowser subclass "
"for the browser_class attribute on %s."
% self.__class__.__name__)
self.table_classes = (self.browser_class.navigation_table_class,
self.browser_class.content_table_class)
self.navigation_selection = False
super(ResourceBrowserView, self).__init__(*args, **kwargs)
def get_browser(self):
if not hasattr(self, "browser"):
self.browser = self.browser_class(self.request, **self.kwargs)
self.browser.set_tables(self.get_tables())
if not self.navigation_selection:
ct = self.browser.content_table
item = self.browser.navigable_item_name.lower()
ct._no_data_message = _("Select a %s to browse.") % item
return self.browser
def get_context_data(self, **kwargs):
context = super(ResourceBrowserView, self).get_context_data(**kwargs)
browser = self.get_browser()
context["%s_browser" % browser.name] = browser
return context

View File

@ -1,34 +0,0 @@
import copy
from django.utils.functional import LazyObject, empty
class LazySettings(LazyObject):
def _setup(self, name=None):
from django.conf import settings
from .default import HORIZON_CONFIG as DEFAULT_CONFIG
HORIZON_CONFIG = copy.copy(DEFAULT_CONFIG)
HORIZON_CONFIG.update(settings.HORIZON_CONFIG)
# Ensure we always have our exception configuration...
for exc_category in ['unauthorized', 'not_found', 'recoverable']:
if exc_category not in HORIZON_CONFIG['exceptions']:
default_exc_config = DEFAULT_CONFIG['exceptions'][exc_category]
HORIZON_CONFIG['exceptions'][exc_category] = default_exc_config
# Ensure our password validator always exists...
if 'regex' not in HORIZON_CONFIG['password_validator']:
default_pw_regex = DEFAULT_CONFIG['password_validator']['regex']
HORIZON_CONFIG['password_validator']['regex'] = default_pw_regex
if 'help_text' not in HORIZON_CONFIG['password_validator']:
default_pw_help = DEFAULT_CONFIG['password_validator']['help_text']
HORIZON_CONFIG['password_validator']['help_text'] = default_pw_help
self._wrapped = HORIZON_CONFIG
def __getitem__(self, name, fallback=None):
if self._wrapped is empty:
self._setup(name)
return self._wrapped.get(name, fallback)
HORIZON_CONFIG = LazySettings()

View File

@ -1,13 +0,0 @@
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

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

View File

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

View File

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

View File

@ -1,11 +0,0 @@
{% 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

@ -1,35 +0,0 @@
from django.conf import settings
from django.utils.translation import ugettext as _
# Default configuration dictionary. Do not mutate.
HORIZON_CONFIG = {
# Allow for ordering dashboards; list or tuple if provided.
'dashboards': None,
# Name of a default dashboard; defaults to first alphabetically if None
'default_dashboard': None,
# Default redirect url for users' home
'user_home': settings.LOGIN_REDIRECT_URL,
# AJAX settings for JavaScript
'ajax_queue_limit': 10,
'ajax_poll_interval': 2500,
# URL for additional help with this site.
'help_url': None,
# Exception configuration.
'exceptions': {'unauthorized': [],
'not_found': [],
'recoverable': []},
# Password configuration.
'password_validator': {'regex': '.*',
'help_text': _("Password is not accepted")},
'password_autocomplete': 'on',
# Enable or disable simplified floating IP address management.
'simple_ip_management': True
}

View File

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

View File

@ -1,13 +0,0 @@
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.{{ dash_name|title }}.register({{ panel_name|title }})

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