Removed old code
This commit is contained in:
parent
2677243357
commit
ffcaa95cb8
28
dashboard/.gitignore
vendored
28
dashboard/.gitignore
vendored
@ -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/
|
|
@ -1,4 +0,0 @@
|
|||||||
[gerrit]
|
|
||||||
host=review.openstack.org
|
|
||||||
port=29418
|
|
||||||
project=openstack/horizon.git
|
|
@ -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>
|
|
@ -1,4 +0,0 @@
|
|||||||
|
|
||||||
[pep8]
|
|
||||||
ignore = E121,E126,E127,E128,W602
|
|
||||||
exclude = vcsversion.py,panel_template,dash_template,local_settings.py
|
|
@ -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=_
|
|
@ -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
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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'));
|
|
@ -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;
|
|
@ -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'));
|
|
@ -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
@ -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));
|
|
@ -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'));
|
|
@ -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'));
|
|
@ -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'));
|
|
@ -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'));
|
|
@ -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'));
|
|
@ -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'));
|
|
@ -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'));
|
|
@ -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'));
|
|
@ -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'));
|
|
@ -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'));
|
|
@ -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'));
|
|
@ -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'));
|
|
@ -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'));
|
|
@ -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'));
|
|
||||||
|
|
@ -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'));
|
|
@ -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'));
|
|
@ -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'));
|
|
@ -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'));
|
|
@ -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'));
|
|
@ -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'));
|
|
@ -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'));
|
|
@ -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'));
|
|
@ -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'));
|
|
@ -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'));
|
|
@ -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'));
|
|
@ -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'));
|
|
@ -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."
|
|
0
dashboard/doc/source/_static/.gitignore
vendored
0
dashboard/doc/source/_static/.gitignore
vendored
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 |
@ -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(/(<)+[3]/gi, "<tt class='heart'>♥</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);
|
|
@ -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 |
@ -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;
|
|
||||||
}
|
|
@ -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 %}
|
|
@ -1,4 +0,0 @@
|
|||||||
[theme]
|
|
||||||
inherit = basic
|
|
||||||
stylesheet = nature.css
|
|
||||||
pygments_style = tango
|
|
@ -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)}
|
|
@ -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.
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
@ -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`
|
|
@ -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.
|
|
@ -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.
|
|
@ -1,6 +0,0 @@
|
|||||||
==========================
|
|
||||||
Horizon Context Processors
|
|
||||||
==========================
|
|
||||||
|
|
||||||
.. automodule:: horizon.context_processors
|
|
||||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||||||
==================
|
|
||||||
Horizon Decorators
|
|
||||||
==================
|
|
||||||
|
|
||||||
.. automodule:: horizon.decorators
|
|
||||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||||||
==================
|
|
||||||
Horizon Exceptions
|
|
||||||
==================
|
|
||||||
|
|
||||||
.. automodule:: horizon.exceptions
|
|
||||||
:members:
|
|
@ -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.
|
|
@ -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:
|
|
@ -1,6 +0,0 @@
|
|||||||
==================
|
|
||||||
Horizon Middleware
|
|
||||||
==================
|
|
||||||
|
|
||||||
.. automodule:: horizon.middleware
|
|
||||||
:members:
|
|
@ -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.
|
|
@ -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
|
|
@ -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:
|
|
@ -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:
|
|
@ -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:
|
|
@ -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.
|
|
@ -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.
|
|
@ -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>`.
|
|
@ -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.
|
|
@ -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
|
|
@ -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.
|
|
@ -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.
|
|
@ -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!
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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()
|
|
@ -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 }})
|
|
@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Stub file to work around django bug: https://code.djangoproject.com/ticket/7198
|
|
||||||
"""
|
|
@ -1 +0,0 @@
|
|||||||
/* Additional CSS for {{ dash_name }}. */
|
|
@ -1 +0,0 @@
|
|||||||
/* Additional JavaScript for {{ dash_name }}. */
|
|
@ -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 %}
|
|
@ -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
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Stub file to work around django bug: https://code.djangoproject.com/ticket/7198
|
|
||||||
"""
|
|
@ -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
Loading…
x
Reference in New Issue
Block a user