94 Commits

Author SHA1 Message Date
Mike Terzo
35486e8e49 Merge pull request #350 from mterzo/release_0_2_1
Prepare for release 0.2.1
2017-02-03 07:21:45 -05:00
Mike Terzo
1da673cea4 Prepare for release 0.2.1 2017-02-03 07:17:20 -05:00
Mike Terzo
a586e7c500 Merge pull request #351 from mterzo/pypuppet_db_docker_update
Update pypuppetdb in dockerfile
2017-02-03 07:09:36 -05:00
Mike Terzo
54f6e0c6da Update pypuppetdb in dockerfile 2017-02-03 07:05:37 -05:00
Mike Terzo
ba4d94a4dd Merge pull request #349 from redref/fix_dailychart
Fix dailychart in python3 + write a test for it
2017-02-03 06:52:18 -05:00
Mike Terzo
55756900c1 Validate dailychart json data 2017-02-03 12:13:46 +01:00
redref
2cdf5fea61 Fix python3 chart (_iter_dates) 2017-02-03 12:13:33 +01:00
Mike Terzo
2189577f02 Merge pull request #348 from mterzo/new_versions
Support for latest version
2017-02-02 08:46:29 -05:00
Mike Terzo
89e49d95a6 Fix Jinja2 template support for nested variables 2017-02-02 08:19:59 -05:00
Mike Terzo
596f0110f1 Support for new Flask errorhandling 2017-02-02 08:19:59 -05:00
Mike Terzo
58d613ba02 Fixing pep8 issues for the future 2017-02-02 08:19:59 -05:00
Mike Terzo
76c18d80d6 Unpinning to see how upgrading to latest works 2017-02-02 08:19:59 -05:00
Mike Terzo
36c913588a Merge pull request #331 from mterzo/all_envs
Support for PuppetDB 3.x on all environments
2017-02-02 08:04:40 -05:00
Mike Terzo
f243cc8584 Merge pull request #341 from mterzo/data_table_format
Update HTML and Javascript
2017-02-02 07:31:16 -05:00
Mike Terzo
04bc1452fb Merge pull request #1 from redref/mterzo_data_table_format
datatable and list behaviors
2017-01-31 16:33:31 -05:00
redref
1432cfeac2 Fix tablesort when no pie - +indent 2017-01-31 19:14:20 +01:00
redref
f6e04ca67f Remove non-needed columns option 2017-01-31 18:32:52 +01:00
redref
851797e4c6 filter-list handling reload 2017-01-31 17:11:41 +01:00
redref
17f902c18f Fix filter-list behavior with a class flag 2017-01-31 16:23:23 +01:00
redref
347749c0e1 Footer - grid padding in the same unit as text 2017-01-31 16:18:11 +01:00
Mike Terzo
b82a305952 When searching metrics, the search button doesn't disappear. 2017-01-31 03:50:11 -05:00
Mike Terzo
e28eb5027d Adding configuration option to specify the bar chart 2017-01-31 03:41:14 -05:00
Mike Terzo
1170577525 Fix broken sort and filter. Not everything has
been migrated to data tables and still needs to be
able to sort and filtered.
2017-01-31 03:41:14 -05:00
Mike Terzo
017dc7bf94 Update node reports table.
Disable search which is does not provide any
real functionality since you can't search
for unchanged, noop or changed, nor and can you filter
on <month> <date> since the data is in UTC.

Moved the page type to simple which only shows
<prev> and <next> buttons.  Having <numbers>
here makes the tab to wide causing overlay onto the
next column.
2017-01-31 03:04:48 -05:00
Tim Meusel
f016820d3a Merge pull request #342 from mterzo/pypuppetdb_upgrade
Update to latest pypuppetdb.
2017-01-30 22:18:58 +01:00
Mike Terzo
3e7119f63e Update to latest pypuppetdb. 2017-01-30 16:05:22 -05:00
Mike Terzo
89407d1718 Adding test cases for all environments for
both puppetdb 3.2.0 and 4.2.0
2017-01-27 07:40:08 -05:00
Mike Terzo
48ab6b615a Support for PuppetDB 3.x on all environments
Mbeans use type=default in puppetdb 3x, type was removed in 4x.
2017-01-27 07:40:08 -05:00
Tim Meusel
aadc2adf10 Merge pull request #337 from mterzo/node_js_fix
Node template js fix
2017-01-27 13:31:37 +01:00
Mike Terzo
3db2fff0b5 Fix nodes template to place report javascript in document
ready
2017-01-27 01:34:13 -05:00
Tim Meusel
7119098e8f Merge pull request #336 from mterzo/js_to_head
Moving java_script to head tag
2017-01-26 23:45:30 +01:00
Tim Meusel
39ed8fb4c4 Merge pull request #335 from mterzo/metrics_fix
Metrics can appear as paths, if these paths are passed
2017-01-26 23:44:15 +01:00
Mike Terzo
936814222d Refactor JSON to be in the html/head tag instead
of at the footer
2017-01-26 17:38:25 -05:00
Mike Terzo
5a12c08d2f Metrics can appear as paths, if these paths are passed
to Flask, it 500's
2017-01-26 17:32:24 -05:00
Mike Terzo
8b883b32f8 Adding new table settings to docker_settings 2017-01-26 13:53:29 -05:00
Mike Terzo
ebab9ccdbc Adding noop map from metrics 2017-01-26 13:52:55 -05:00
Mike Terzo
4f50811142 Adding first test case for report 2017-01-26 04:35:58 -05:00
Mike Terzo
86fe05f5f9 Adding default values to parameters 2017-01-26 04:25:32 -05:00
Mike Terzo
680ee0e217 Following the format of all the other app.route definitions 2017-01-26 04:24:53 -05:00
Mike Terzo
7943414691 Use the data that's provided by reports.metrics instead
of calling the API again
2017-01-26 01:25:34 -05:00
Mike Terzo
144f772141 Merge pull request #332 from voxpupuli/travis
add python36 to travis
2017-01-25 16:46:51 -05:00
Tim Meusel
e88ae16846 add python36 to travis 2017-01-25 22:41:25 +01:00
redref
103eaa8843 Paging - fix empty list 2017-01-25 22:38:12 +01:00
redref
c1b1badc96 Paging - Revamp tables with Jquery Datatables (Ajax) 2017-01-25 18:07:55 +01:00
Mike Terzo
7febd925e7 Merge pull request #328 from redref/zip_safe
Setup.py : zip_safe to permit static serve
2017-01-24 02:37:36 -05:00
Tim Meusel
38b1e9fe06 Merge pull request #326 from mterzo/better_testing
Flask testing.
2017-01-24 06:58:22 +01:00
Mike Terzo
caadaa0b35 Test index with division by zero
Signed-off-by: Mike Terzo <mike@terzo.org>
2017-01-23 19:54:00 -05:00
Mike Terzo
86488280c9 Test error conditions.
Fix 412 template to use standard styling that the other 400 templates use.
Update forbidden error to return status code 403 instead of 400.

Signed-off-by: Mike Terzo <mike@terzo.org>
2017-01-23 19:53:55 -05:00
Mike Terzo
2e4acc3e3f Adding radiator json testing
Signed-off-by: Mike Terzo <mike@terzo.org>
2017-01-23 19:53:52 -05:00
Mike Terzo
0570372d97 Testing pretty print produces good html
Signed-off-by: Mike Terzo <mike@terzo.org>
2017-01-23 19:53:48 -05:00
Mike Terzo
0d1fbcee88 Adding tests for node list
Signed-off-by: Mike Terzo <mike@terzo.org>
2017-01-23 19:53:45 -05:00
Mike Terzo
7cebe56fc4 Adding testing for all environments
Signed-off-by: Mike Terzo <mike@terzo.org>
2017-01-23 19:53:41 -05:00
Mike Terzo
e2c45648b9 Removing whitespace from classes in radiator view
Signed-off-by: Mike Terzo <mike@terzo.org>
2017-01-23 19:53:33 -05:00
Mike Terzo
fb6b8d2c0e Adding testing for radiator view.
Signed-off-by: Mike Terzo <mike@terzo.org>
2017-01-23 18:14:52 -05:00
Mike Terzo
c729b4d88d Adding testing for Puppetboard app using flask client.
Adding offline / online mode testing for validation.

This is the start of adding a ton of tests with the start
to mocking for pypupppetdb
2017-01-23 18:06:53 -05:00
Mike Terzo
0e712da71f Closing html tags for links properly 2017-01-23 18:06:53 -05:00
Mike Terzo
ff409c5f6d Adding coverage for invalid log setting 2017-01-23 18:06:52 -05:00
Mike Terzo
7302dbecec Convert Unit tests to use py.test format 2017-01-23 18:06:49 -05:00
Robert Fletcher
333347d113 Radiator JSON output (#329)
Json output from radiator when Accept header is application/json
2017-01-23 16:15:18 -05:00
Joris Dedieu
9fe0f091f3 catch a division by zero in radiator while environment has no nodes (#325)
* catch a division by zero in radiator while environment has no nodes
2017-01-05 15:51:38 -05:00
Mike Terzo
4938644593 Merge pull request #327 from raphink/docker_puppetdb_default
Use puppetdb as PUPPETDB_HOST on Docker
2017-01-05 15:31:31 -05:00
Raphaël Pinson
df91efff33 Use puppetdb as PUPPETDB_HOST on Docker
This allows to plug and play a PuppetDB container (or use extra host)
2017-01-05 12:54:31 +01:00
sacres
fdc6b00525 Merge pull request #310 from mterzo/docker_env
Default settings use environment variables
2017-01-03 00:42:44 -05:00
redref
5ef7c66377 Setup.py : zip_safe to permit static serve 2017-01-01 21:26:37 +01:00
Mike Terzo
803178053b Merge pull request #324 from mterzo/doc_update
Updating documentation for secret_key
2016-12-19 20:42:35 -05:00
Mike Terzo
65d9abc749 Updating documentation for secret_key 2016-12-19 16:28:28 -05:00
Mike Terzo
b96e76ff10 Use alpine python and gunicorn
Adding docker_settings.py which reads environment variables
to allows for environment variable to be passed to the
container
2016-12-19 15:54:32 -05:00
Mike Terzo
72a194c82e Update default_settings to use environment variables.
Easier environment configuration when using docker to
run puppetboard.
2016-12-19 13:48:34 -05:00
Mike Terzo
1966c1d31d Merge pull request #318 from ts-mini/fixing-width-radiator
radiator column width to percent
2016-12-19 00:18:09 -05:00
Mike Terzo
7c889d5b2e Merge pull request #311 from manuq/dailycharts
Dailycharts
2016-12-18 23:40:02 -05:00
Mike Terzo
0e3b4d230e Merge pull request #309 from alchemyx/patch-1
Update README.rst
2016-12-18 23:30:44 -05:00
Peter Souter
654af73914 Merge pull request #320 from roidelapluie/bandit
[Security] Implement bandit
2016-12-14 16:05:31 +00:00
Julien Pivotto
6fa0a4a796 [Security] Implement bandit
Bandit (https://github.com/openstack/bandit) is a python
security linter.

Signed-off-by: Julien Pivotto <roidelapluie@inuits.eu>
2016-12-07 10:05:34 +01:00
Tyler Horvath
4d744b902f radiator column width to percent
>1000 nodes causing clipping with 1.75em width
2016-12-02 13:57:34 -07:00
Manuel Quiñones
08e214ec15 Overview, Node pages: Add bar chart of daily runs
The Overview will display a bar chart of daily runs, categorized by
report status (changed, unchanged, failed).

The chart data is loaded asynchronously from JSON so it doesn't provoke
a delay in the page load. The data is JSON enconded.

This feature was in the original Puppet Dashboard.  The change was
proposed and discussed in issue #308 .

Application changes:

- app.py: New view daily_reports_chart to serve the chart data as JSON.

- dailychart.py: Submodule to query and format the chart data.

Template changes:

- layout.html: New block to add more elements to the HTML header.

- index.html, node.html: Add C3 CSS in header block, add DIV placeholder
  for the chart in content block, add dailychart.js (and dependencies)
  in script block.

Settings:

- DAILY_REPORTS_CHART_ENABLED: New setting to turn off the charts. By
  default is on.

- DAILY_REPORTS_CHART_DAYS: Changes the range of days to display in the
  charts.

Javascript changes:

- dailychart.js: New script that loads the JSON data for the chart and
  calls C3 to generate a bar chart.

CSS changes:

- puppetboard.css: Set fixed height to the chart container to avoid a
  page resize after the chart is loaded.
2016-10-26 16:39:03 -03:00
Manuel Quiñones
68ef8ac0da Upgrade c3.js to 0.4.11 and add corresponding CSS
The bar charts in next commit look wrong without CSS for c3.js .
2016-10-25 13:46:41 -03:00
Michał Margula
1897a4393f Update README.rst
There is no random in os module, changed to urandom (same as in default_setttings), also fixed path in example apache config.
2016-10-22 10:51:52 +02:00
Mike Terzo
3fbd182453 Adding unittests (#300)
* Create a custom class to handle aborting 204 properly.  If this isn't
covered the server will send a 500 due to a python exception

* Moved py.test configuration under tool:pytest, this was causing a
warning.  This is new to 3.0.1 which is now the pinned version

* Unittest for puppetboard.utils
2016-09-11 21:01:13 -04:00
Corey Hammerton
dffd42af1d puppetboard/templates/metrics.html: Added searchability to the Metrics list (#298)
This resolves #297

Adding a search bar to the Metrics page to allow the user to filter the list
items to only show what the user wants.
2016-09-03 17:11:40 -04:00
Corey Hammerton
df3d4a5eaa puppetboard/app.py: Adding an environment filter for displaying Fact names (#295)
This resolves #276

Current behaviour of the Facts page would query the fact_names endpoint
regardless of environment. This update would query the Facts endpoint,
extracting each unique fact name known to the environment.
2016-09-03 17:11:28 -04:00
Alejandro Figueroa
c585251862 Change hostname to certname where applicable (#294)
Certain column headers were referring to a node's certname as its
hostname. This commit corrects that by renaming the column headers.
2016-09-03 17:11:03 -04:00
Corey Hammerton
43526279e0 puppetboard/templates/layout.html: Displaying the current active environment as the dropdown label (#291)
This fixed #290

To help make it more obvious for users to see what the current environment
is replacing the original text of the Environments dropdown menu from
'Environments' to the current environment.

The other suggestion was to make the active item stand out more but that
would require custom CSS that may conflict with the Sementic UI.
2016-09-03 17:10:46 -04:00
Corey Hammerton
5048662861 puppetboard/app.py: Simplifying the Inventory Code (#289)
* puppetboard/app.py: Simplifying the code generating and rendering the Inventory

This resolves #275

This update eliminates one iteration over the resulting inventory facts
that generates a multidimensional dictionary keyed by the node's certname
to another dictionary of key-value pairs of the fact name and fact value.

* puppetboard/templates/inventory.html: Wrapping the fact values in links to the Node page

This comes as a request from #280
2016-09-03 17:10:33 -04:00
Corey Hammerton
0c0a15bdf2 puppetboard/app.py: Enhancing queries for Node and Report states (#271)
* puppetboard/app.py: Enhancing queries for Node and Report states

This resolves #264

On the Nodes and Reports tabs when the user adds a status query string
argument additional query clauses are generated based on its value.
Can be one of failed, changed, unchanged, noop or unreported (for Nodes
only)

No query clause is generated for noop on the Nodes tab. The query field
latest_report_noop was added in PuppetDB 4.1 and we do not want to break
compatability between minor or bug-fix versions.

* puppetboard/app.py: Simplifying the query logic in nodes()

The new logic starts with a blank `AndOperator()` object then proceeds
to build the query based on environment and status values. After all
after all checking if there are no operations declare the object as
None.

* puppetboard/app.py: Simplifying the query logic for reports()

Similar to the work done for nodes()

* puppetboard/app.py: Fixing pep8 formatting in nodes()

* Add pagination to reports/<node>
2016-08-18 20:39:31 -04:00
Nikolai Røed Kristiansen
feac4441c9 Travis: run tests under python 3.5 (#284) 2016-07-27 19:59:44 -04:00
Thomas Hager
294e2d6559 Fixed a typo in Puppetboard's catalog view (#282)
Replaced oder_by_str with order_by_str in get_or_abort() to fix app crash.
2016-07-27 19:59:20 -04:00
Tim Meusel
12b0d09f9b Merge pull request #278 from petems/fix_badges
Fix rst syntax for badges
2016-07-15 10:50:16 +02:00
Peter Souter
510bdccbb5 Fix rst syntax for badges
* Require links to be on newline
2016-07-15 09:39:03 +01:00
Peter Souter
ba7dd9f264 Merge pull request #274 from mterzo/add_coveralls_badge
Add coveralls.io badge
2016-07-15 09:16:53 +01:00
Mike Terzo
a8ca234a3b Add coveralls.io badge 2016-07-14 21:38:49 -04:00
Peter Souter
c1fb6fbdc2 Add Travis Badge (#273) 2016-07-14 19:43:09 -04:00
Mike Terzo
1afe120a12 Update radiator view (#272)
* Fixing 404 on jquery.js

* Remove symlink for jquery.min.map
2016-07-14 19:40:06 -04:00
Mike Terzo
faac5fa1bc Travis ci integration (#267)
* Initial travis-ci integration

* Format code base to PEP8
2016-07-13 20:59:07 -04:00
Corey Hammerton
0ac64530bf README.rst: Fixing code-block strings. (#265)
Github wasn't showing the new readme blocks because they were encoded
code_block instead of the correct code-block.
2016-06-29 20:44:25 -04:00
58 changed files with 214096 additions and 905 deletions

3
.coveragerc Normal file
View File

@@ -0,0 +1,3 @@
[report]
exclude_lines =
pragma: notest

7
.gitignore vendored
View File

@@ -1,5 +1,8 @@
*.py[cod]
# Editor tmp files
.*.sw*
# C extensions
*.so
@@ -22,6 +25,7 @@ lib64
pip-log.txt
# Unit test / coverage reports
.cache
.coverage
.tox
nosetests.xml
@@ -36,3 +40,6 @@ nosetests.xml
# PuppetDB Settings
/settings.py
# Virtual Environment
venv

33
.travis.yml Normal file
View File

@@ -0,0 +1,33 @@
language: python
python:
- "2.6"
- "2.7"
- "3.5"
- "3.6"
env:
- PINNED=TRUE
- PINNED=FALSE
matrix:
allow_failures:
- python: 2.6
env: PINNED=FALSE
- python: 2.7
env: PINNED=FALSE
- python: 3.5
env: PINNED=FALSE
- python: 3.6
env: PINNED=FALSE
install:
- if [ "${PINNED}" == "FALSE" ]; then python scripts/unpin.py; fi
- pip install -r requirements.txt
- pip install -U -r requirements-test.txt
- pip install -q coverage coveralls --use-wheel
script:
- py.test --cov=puppetboard --pep8 -v
- ./bandit.sh
after_success:
- coveralls

View File

@@ -4,6 +4,32 @@ Changelog
This is the changelog for Puppetboard.
0.2.1
=====
* Daily Charts
* Fixed missing javascript files on radiator view.
* TravisCI and Coveralls integration.
* Fixed app crash in catalog view.
* Upgrade pypuppetdb to 0.3.2
* Enhanced queries for Node and Report (#271)
* Optimize Inventory Code.
* Use certname instead of hostname to identify nodes when applicable.
* Add environment filter for facts.
* Update cs.js to 0.4.11
* Fix radiator column alignment
* Security checks with bandit
* Dockerfile now uses gunicorn and environment variables for
configuration.
* Handle division by zero errors.
* Implement new Jquery Datatables.
* JSON output for radiator. * Move javascript to head tag.
* Optimize reports and node page queries.
* Fix all environments for PuppetDB 3.2
* Fact graph chart now configurable.
* Support for Flask 0.12 and Jinja2 2.9
* Fix misreporting noops as changes.
0.2.0
=====

View File

@@ -1,3 +1,12 @@
FROM grahamdumpleton/mod-wsgi-docker:python-2.7-onbuild
CMD [ "wsgi.py" ]
FROM python:2.7-alpine
ENV PUPPETBOARD_PORT 80
ENV PUPPETBOARD_SETTINGS docker_settings.py
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
COPY requirements-docker.txt /usr/src/app/
RUN pip install --no-cache-dir -r requirements-docker.txt
COPY . /usr/src/app
CMD gunicorn -b 0.0.0.0:${PUPPETBOARD_PORT} --access-logfile=/dev/stdout puppetboard.app:app

View File

@@ -2,4 +2,4 @@ include README.rst
include CHANGELOG.rst
include LICENSE
recursive-include puppetboard/static *.css *.js *icons.* Open_Sans.woff
recursive-include puppetboard/templates *.html
recursive-include puppetboard/templates *.html *.json.tpl

View File

@@ -2,6 +2,12 @@
Puppetboard
###########
.. image:: https://travis-ci.org/voxpupuli/puppetboard.svg?branch=master
:target: https://travis-ci.org/voxpupuli/puppetboard
.. image:: https://coveralls.io/repos/github/voxpupuli/puppetboard/badge.svg?branch=master
:target: https://coveralls.io/github/voxpupuli/puppetboard?branch=master
Puppetboard is a web interface to `PuppetDB`_ aiming to replace the reporting
functionality of `Puppet Dashboard`_.
@@ -14,6 +20,7 @@ As of version 0.1.0 and higher, Puppetboard **requires** PuppetDB 3.
.. _PuppetDB: http://docs.puppetlabs.com/puppetdb/latest/index.html
.. _Puppet Dashboard: http://docs.puppetlabs.com/dashboard/
.. _Flask: http://flask.pocoo.org
.. _FlaskSession: http://flask.pocoo.org/docs/0.11/quickstart/#sessions
At the current time of writing, Puppetboard supports the following Python versions:
* Python 2.6
@@ -122,6 +129,20 @@ image is planned for the 0.2.x series.
.. _Dockerfile: https://github.com/voxpupuli/puppetboard/blob/master/Dockerfile
Usage:
.. code-block:: bash
$ docker build -t puppetboard .
$ docker run -it -p 9080:80 -v /etc/puppetlabs/puppet/ssl:/etc/puppetlabs/puppet/ssl \
-e PUPPETDB_HOST=<hostname> \
-e PUPPETDB_PORT=8081 \
-e PUPPETDB_SSL_VERIFY=/etc/puppetlabs/puppetdb/ssl/ca.pem \
-e PUPPETDB_KEY=/etc/puppetlabs/puppetdb/ssl/private.pem \
-e PUPPETDB_CERT=/etc/puppetlabs/puppetdb/ssl/public.pem \
-e INVENTORY_FACTS='Hostname,fqdn, IP Address,ipaddress' \
-e ENABLE_CATALOG=true \
-e GRAPH_FACTS='architecture,puppetversion,osfamily' \
puppetboard
Development
-----------
@@ -198,6 +219,9 @@ Other settings that might be interesting in no particular order:
* ``ENABLE_QUERY``: Defaults to ``True`` causing a Query tab to show up in the
web interface allowing users to write and execute arbitrary queries against
a set of endpoints in PuppetDB. Change this to ``False`` to disable this.
* ``GRAPH_TYPE```: Specify the type of graph to display. Default is
pie, other good option is donut. Other choices can be found here:
`_C3JS_documentation`
* ``GRAPH_FACTS``: A list of fact names to tell PuppetBoard to generate a
pie-chart on the fact page. With some fact values being unique per node,
like ipaddress, uuid, and serial number, as well as structured facts it was
@@ -215,11 +239,12 @@ Other settings that might be interesting in no particular order:
* ``REPORTS_COUNT``: Defaults to ``10`` the limit of the number of reports
to load on the node or any reports page.
* ``OFFLINE_MODE``: If set to ``True`` load static assets (jquery,
semantic-ui, tablesorter, etc) from the local web server instead of a CDN.
semantic-ui, etc) from the local web server instead of a CDN.
Defaults to ``False``.
.. _pypuppetdb documentation: http://pypuppetdb.readthedocs.org/en/v0.1.0/quickstart.html#ssl
.. _Flask documentation: http://flask.pocoo.org/docs/0.10/quickstart/#sessions
.. _C3JS_documentation: http://c3js.org/examples.html#chart
Puppet Enterprise
-----------------
@@ -294,21 +319,23 @@ puppetboard directory:
Make sure this file is readable by the user the webserver runs as.
Flask requires a static secret_key in order to protect itself from CSRF exploits.
The default secret_key in ``default_settings.py`` generates a random 24 character
string, however this string is re-generated on each request under httpd >= 2.4.
Flask requires a static secret_key, see `FlaskSession`_, in order to protect
itself from CSRF exploits. The default secret_key in ``default_settings.py``
generates a random 24 character string, however this string is re-generated
on each request under httpd >= 2.4.
To generate your own secret_key create a python script with the following content
and run it once:
.. code_block:: python
.. code-block:: python
import os
print os.random(24)
os.urandom(24)
'\xfd{H\xe5<\x95\xf9\xe3\x96.5\xd1\x01O<!\xd5\xa2\xa0\x9fR"\xa1\xa8'
Copy the output and add the following to your ``wsgi.py`` file:
.. code_block:: python
.. code-block:: python
application.secret_key = '<your secret key>'
@@ -326,7 +353,7 @@ Here is a sample configuration for Debian and Ubuntu:
CustomLog /var/log/apache2/puppetboard.access.log combined
Alias /static /usr/local/lib/pythonX.Y/dist-packages/puppetboard/static
<Directory /usr/lib/python2.X/dist-packages/puppetboard/static>
<Directory /usr/local/lib/pythonX.X/dist-packages/puppetboard/static>
Satisfy Any
Allow from all
</Directory>

12
bandit.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/bash -xe
# Runs bandit tests
pyver="$(python -V 2>&1)"
if [[ $pyver =~ Python\ 2\.6 ]]
then
echo 'Bandit does not support python 2.6'
else
bandit -r puppetboard
bandit -r tests
fi

2
conftest.py Normal file
View File

@@ -0,0 +1,2 @@
import puppetboard
import test

10
dev.py
View File

@@ -3,11 +3,7 @@ from __future__ import absolute_import
import os
import subprocess
if 'PUPPETBOARD_SETTINGS' not in os.environ:
os.environ['PUPPETBOARD_SETTINGS'] = os.path.join(
os.getcwd(), 'settings.py'
)
# Set PUPPETBOARD_SETTINGS to your settings.py
from puppetboard.app import app
if __name__ == '__main__':
@@ -20,12 +16,12 @@ if __name__ == '__main__':
app.config['DEV_COFFEE_LOCATION'], '-w', '-c',
'-o', 'puppetboard/static/js',
'puppetboard/static/coffeescript'
])
])
except OSError:
app.logger.error(
'The coffee executable was not found, disabling automatic '
'CoffeeScript compilation'
)
)
# Start the Flask development server
app.debug = True

View File

@@ -7,24 +7,38 @@ try:
from urllib import unquote
except ImportError:
from urllib.parse import unquote
from datetime import datetime
from datetime import datetime, timedelta
from itertools import tee
from flask import (
Flask, render_template, abort, url_for,
Response, stream_with_context, redirect,
request, session
)
request, session, jsonify
)
from pypuppetdb import connect
from pypuppetdb.errors import EmptyResponseError
from pypuppetdb.QueryBuilder import *
from puppetboard.forms import (CatalogForm, QueryForm)
from puppetboard.utils import (
get_or_abort, yield_or_stop,
jsonprint, prettyprint, Pagination
)
get_or_abort, yield_or_stop, get_db_version,
jsonprint, prettyprint
)
from puppetboard.dailychart import get_daily_reports_chart
import werkzeug.exceptions as ex
REPORTS_COLUMNS = [
{'attr': 'end', 'filter': 'end_time',
'name': 'End time', 'type': 'datetime'},
{'attr': 'status', 'name': 'Status', 'type': 'status'},
{'attr': 'certname', 'name': 'Certname', 'type': 'node'},
{'attr': 'version', 'filter': 'configuration_version',
'name': 'Configuration version'},
{'attr': 'agent_version', 'filter': 'puppet_version',
'name': 'Agent version'},
]
app = Flask(__name__)
@@ -59,12 +73,14 @@ def stream_template(template_name, **context):
rv.enable_buffering(5)
return rv
def url_for_field(field, value):
args = request.view_args.copy()
args.update(request.args.copy())
args[field] = value
return url_for(request.endpoint, **args)
def environments():
envs = get_or_abort(puppetdb.environments)
x = []
@@ -74,12 +90,14 @@ def environments():
return x
def check_env(env, envs):
if env != '*' and env not in envs:
abort(404)
app.jinja_env.globals['url_for_field'] = url_for_field
@app.context_processor
def utility_processor():
def now(format='%m/%d/%Y %H:%M:%S'):
@@ -88,6 +106,26 @@ def utility_processor():
return dict(now=now)
#
# 204 doesn't have a mapping in werkzeug, we need to define a custom
# class and then set it to the mappings.
#
class NoContent(ex.HTTPException):
code = 204
description = '<p>No content</p'
abort.mapping[204] = NoContent
try:
@app.errorhandler(204)
def no_content(e):
return '', 204
except KeyError:
@app.errorhandler(EmptyResponseError)
def no_content(e):
return '', 204
@app.errorhandler(400)
def bad_request(e):
envs = environments()
@@ -97,7 +135,7 @@ def bad_request(e):
@app.errorhandler(403)
def forbidden(e):
envs = environments()
return render_template('403.html', envs=envs), 400
return render_template('403.html', envs=envs), 403
@app.errorhandler(404)
@@ -140,15 +178,23 @@ def index(env):
query = app.config['OVERVIEW_FILTER']
prefix = 'puppetlabs.puppetdb.population'
query_type = ''
# Puppet DB version changed the query format from 3.2.0
# to 4.0 when querying mbeans
if get_db_version(puppetdb) < (4, 0, 0):
query_type = 'type=default,'
num_nodes = get_or_abort(
puppetdb.metric,
"{0}{1}".format(prefix, ':name=num-nodes'))
"{0}{1}".format(prefix, ':%sname=num-nodes' % query_type))
num_resources = get_or_abort(
puppetdb.metric,
"{0}{1}".format(prefix, ':name=num-resources'))
"{0}{1}".format(prefix, ':%sname=num-resources' % query_type))
avg_resources_node = get_or_abort(
puppetdb.metric,
"{0}{1}".format(prefix, ':name=avg-resources-per-node'))
"{0}{1}".format(prefix,
':%sname=avg-resources-per-node' % query_type))
metrics['num_nodes'] = num_nodes['Value']
metrics['num_resources'] = num_resources['Value']
metrics['avg_resources_node'] = "{0:10.0f}".format(
@@ -162,8 +208,8 @@ def index(env):
num_nodes_query.add_field(FunctionOperator('count'))
num_nodes_query.add_query(query)
if app.config['OVERVIEW_FILTER'] != None:
query.add(app.config['OVERVIEW_FILTER'])
if app.config['OVERVIEW_FILTER'] is not None:
query.add(app.config['OVERVIEW_FILTER'])
num_resources_query = ExtractOperator()
num_resources_query.add_field(FunctionOperator('count'))
@@ -186,9 +232,9 @@ def index(env):
metrics['avg_resources_node'] = 0
nodes = get_or_abort(puppetdb.nodes,
query=query,
unreported=app.config['UNRESPONSIVE_HOURS'],
with_status=True)
query=query,
unreported=app.config['UNRESPONSIVE_HOURS'],
with_status=True)
nodes_overview = []
stats = {
@@ -197,7 +243,7 @@ def index(env):
'failed': 0,
'unreported': 0,
'noop': 0
}
}
for node in nodes:
if node.status == 'unreported':
@@ -221,7 +267,7 @@ def index(env):
stats=stats,
envs=envs,
current_env=env
)
)
@app.route('/nodes', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@@ -240,16 +286,32 @@ def nodes(env):
:type env: :obj:`string`
"""
envs = environments()
status_arg = request.args.get('status', '')
check_env(env, envs)
if env == '*':
query = None
else:
query = AndOperator()
query = AndOperator()
if env != '*':
query.add(EqualsOperator("catalog_environment", env))
query.add(EqualsOperator("facts_environment", env))
status_arg = request.args.get('status', '')
if status_arg in ['failed', 'changed', 'unchanged']:
query.add(EqualsOperator('latest_report_status', status_arg))
elif status_arg == 'unreported':
unreported = datetime.datetime.utcnow()
unreported = (unreported -
timedelta(hours=app.config['UNRESPONSIVE_HOURS']))
unreported = unreported.replace(microsecond=0).isoformat()
unrep_query = OrOperator()
unrep_query.add(NullOperator('report_timestamp', True))
unrep_query.add(LessEqualOperator('report_timestamp', unreported))
query.add(unrep_query)
if len(query.operations) == 0:
query = None
nodelist = puppetdb.nodes(
query=query,
unreported=app.config['UNRESPONSIVE_HOURS'],
@@ -263,9 +325,9 @@ def nodes(env):
nodes.append(node)
return Response(stream_with_context(
stream_template('nodes.html',
nodes=nodes,
envs=envs,
current_env=env)))
nodes=nodes,
envs=envs,
current_env=env)))
@app.route('/inventory', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@@ -286,29 +348,26 @@ def inventory(env):
envs = environments()
check_env(env, envs)
fact_desc = [] # a list of fact descriptions to go
# in the table header
headers = [] # a list of fact descriptions to go
# in the table header
fact_names = [] # a list of inventory fact names
factvalues = {} # values of the facts for all the nodes
# indexed by node name and fact name
nodedata = {} # a dictionary containing list of inventoried
# facts indexed by node name
nodelist = set() # a set of node names
fact_data = {} # a multidimensional dict for node and
# fact data
# load the list of items/facts we want in our inventory
try:
inv_facts = app.config['INVENTORY_FACTS']
except KeyError:
inv_facts = [ ('Hostname' ,'fqdn' ),
('IP Address' ,'ipaddress' ),
('OS' ,'lsbdistdescription'),
('Architecture' ,'hardwaremodel' ),
('Kernel Version','kernelrelease' ) ]
inv_facts = [('Hostname', 'fqdn'),
('IP Address', 'ipaddress'),
('OS', 'lsbdistdescription'),
('Architecture', 'hardwaremodel'),
('Kernel Version', 'kernelrelease')]
# generate a list of descriptions and a list of fact names
# from the list of tuples inv_facts.
for description,name in inv_facts:
fact_desc.append(description)
for desc, name in inv_facts:
headers.append(desc)
fact_names.append(name)
query = AndOperator()
@@ -323,30 +382,26 @@ def inventory(env):
# get all the facts from PuppetDB
facts = puppetdb.facts(query=query)
# convert the json in easy to access data structure
for fact in facts:
factvalues[fact.node,fact.name] = fact.value
nodelist.add(fact.node)
if fact.node not in fact_data:
fact_data[fact.node] = {}
# generate the per-host data
for node in nodelist:
nodedata[node] = []
for fact_name in fact_names:
try:
nodedata[node].append(factvalues[node,fact_name])
except KeyError:
nodedata[node].append("undef")
fact_data[fact.node][fact.name] = fact.value
return Response(stream_with_context(
stream_template('inventory.html',
nodedata=nodedata,
fact_desc=fact_desc,
stream_template(
'inventory.html',
headers=headers,
fact_names=fact_names,
fact_data=fact_data,
envs=envs,
current_env=env)))
current_env=env
)))
@app.route('/node/<node_name>', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/node/<node_name>')
@app.route('/node/<node_name>/',
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/node/<node_name>/')
def node(env, node_name):
"""Display a dashboard for a node showing as much data as we have on that
node. This includes facts and reports but not Resources as that is too
@@ -366,206 +421,161 @@ def node(env, node_name):
node = get_or_abort(puppetdb.node, node_name)
facts = node.facts()
reports = get_or_abort(puppetdb.reports,
query=query,
limit=app.config['REPORTS_COUNT'],
order_by='[{"field": "start_time", "order": "desc"}]')
reports, reports_events = tee(reports)
report_event_counts = {}
for report in reports_events:
report_event_counts[report.hash_] = {}
for event in report.events():
if event.status == 'success':
try:
report_event_counts[report.hash_]['successes'] += 1
except KeyError:
report_event_counts[report.hash_]['successes'] = 1
elif event.status == 'failure':
try:
report_event_counts[report.hash_]['failures'] += 1
except KeyError:
report_event_counts[report.hash_]['failures'] = 1
elif event.status == 'noop':
try:
report_event_counts[report.hash_]['noops'] += 1
except KeyError:
report_event_counts[report.hash_]['noops'] = 1
elif event.status == 'skipped':
try:
report_event_counts[report.hash_]['skips'] += 1
except KeyError:
report_event_counts[report.hash_]['skips'] = 1
return render_template(
'node.html',
node=node,
facts=yield_or_stop(facts),
reports=yield_or_stop(reports),
reports_count=app.config['REPORTS_COUNT'],
report_event_counts=report_event_counts,
envs=envs,
current_env=env)
current_env=env,
columns=REPORTS_COLUMNS[:2])
@app.route('/reports/', defaults={'env': app.config['DEFAULT_ENVIRONMENT'], 'page': 1})
@app.route('/<env>/reports/', defaults={'page': 1})
@app.route('/<env>/reports/page/<int:page>')
def reports(env, page):
"""Displays a list of reports and status from all nodes, retreived using the
reports endpoint, sorted by start_time.
@app.route('/reports/',
defaults={'env': app.config['DEFAULT_ENVIRONMENT'],
'node_name': None})
@app.route('/<env>/reports/', defaults={'node_name': None})
@app.route('/reports/<node_name>/',
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/reports/<node_name>')
def reports(env, node_name):
"""Query and Return JSON data to reports Jquery datatable
:param env: Search for all reports in this environment
:type env: :obj:`string`
:param page: Calculates the offset of the query based on the report count
and this value
:type page: :obj:`int`
"""
envs = environments()
check_env(env, envs)
limit = request.args.get('limit', app.config['REPORTS_COUNT'])
reports_query = None
total_query = ExtractOperator()
total_query.add_field(FunctionOperator("count"))
if env != '*':
reports_query = EqualsOperator("environment", env)
total_query.add_query(reports_query)
try:
paging_args = {'limit': int(limit)}
paging_args['offset'] = int((page-1) * paging_args['limit'])
except ValueError:
paging_args = {}
reports = get_or_abort(puppetdb.reports,
query=reports_query,
order_by='[{"field": "start_time", "order": "desc"}]',
**paging_args)
total = get_or_abort(puppetdb._query,
'reports',
query=total_query)
total = total[0]['count']
reports, reports_events = tee(reports)
report_event_counts = {}
if total == 0 and page != 1:
abort(404)
for report in reports_events:
report_event_counts[report.hash_] = {}
for event in report.events():
if event.status == 'success':
try:
report_event_counts[report.hash_]['successes'] += 1
except KeyError:
report_event_counts[report.hash_]['successes'] = 1
elif event.status == 'failure':
try:
report_event_counts[report.hash_]['failures'] += 1
except KeyError:
report_event_counts[report.hash_]['failures'] = 1
elif event.status == 'noop':
try:
report_event_counts[report.hash_]['noops'] += 1
except KeyError:
report_event_counts[report.hash_]['noops'] = 1
elif event.status == 'skipped':
try:
report_event_counts[report.hash_]['skips'] += 1
except KeyError:
report_event_counts[report.hash_]['skips'] = 1
return Response(stream_with_context(stream_template(
'reports.html',
reports=yield_or_stop(reports),
reports_count=app.config['REPORTS_COUNT'],
report_event_counts=report_event_counts,
pagination=Pagination(page, paging_args.get('limit', total), total),
envs=envs,
current_env=env,
limit=paging_args.get('limit', total))))
@app.route('/reports/<node_name>/', defaults={'env': app.config['DEFAULT_ENVIRONMENT'], 'page': 1})
@app.route('/<env>/reports/<node_name>', defaults={'page': 1})
@app.route('/<env>/reports/<node_name>/page/<int:page>')
def reports_node(env, node_name, page):
"""Fetches all reports for a node and processes them eventually rendering
a table displaying those reports.
:param env: Search for reports in this environment
:type env: :obj:`string`
:param node_name: Find the reports whose certname match this value
:type node_name: :obj:`string`
:param page: Calculates the offset of the query based on the report count
and this value
:type page: :obj:`int`
"""
envs = environments()
check_env(env, envs)
query = AndOperator()
total_query = ExtractOperator()
total_query.add_field(FunctionOperator("count"))
if env != '*':
query.add(EqualsOperator("environment", env))
query.add(EqualsOperator("certname", node_name))
total_query.add_query(query)
reports = get_or_abort(puppetdb.reports,
query=query,
limit=app.config['REPORTS_COUNT'],
offset=(page-1) * app.config['REPORTS_COUNT'],
order_by='[{"field": "start_time", "order": "desc"}]')
total = get_or_abort(puppetdb._query,
'reports',
query=total_query)
total = total[0]['count']
reports, reports_events = tee(reports)
report_event_counts = {}
if total == 0 and page != 1:
abort(404)
for report in reports_events:
report_event_counts[report.hash_] = {}
for event in report.events():
if event.status == 'success':
try:
report_event_counts[report.hash_]['successes'] += 1
except KeyError:
report_event_counts[report.hash_]['successes'] = 1
elif event.status == 'failure':
try:
report_event_counts[report.hash_]['failures'] += 1
except KeyError:
report_event_counts[report.hash_]['failures'] = 1
elif event.status == 'noop':
try:
report_event_counts[report.hash_]['noops'] += 1
except KeyError:
report_event_counts[report.hash_]['noops'] = 1
elif event.status == 'skipped':
try:
report_event_counts[report.hash_]['skips'] += 1
except KeyError:
report_event_counts[report.hash_]['skips'] = 1
return render_template(
'reports.html',
reports=reports,
reports_count=app.config['REPORTS_COUNT'],
report_event_counts=report_event_counts,
pagination=Pagination(page, app.config['REPORTS_COUNT'], total),
envs=envs,
current_env=env)
current_env=env,
node_name=node_name,
columns=REPORTS_COLUMNS)
@app.route('/report/<node_name>/<report_id>', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/reports/json',
defaults={'env': app.config['DEFAULT_ENVIRONMENT'],
'node_name': None})
@app.route('/<env>/reports/json', defaults={'node_name': None})
@app.route('/reports/<node_name>/json',
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/reports/<node_name>/json')
def reports_ajax(env, node_name):
"""Query and Return JSON data to reports Jquery datatable
:param env: Search for all reports in this environment
:type env: :obj:`string`
"""
draw = int(request.args.get('draw', 0))
start = int(request.args.get('start', 0))
length = int(request.args.get('length', app.config['NORMAL_TABLE_COUNT']))
paging_args = {'limit': length, 'offset': start}
search_arg = request.args.get('search[value]')
order_column = int(request.args.get('order[0][column]', 0))
order_filter = REPORTS_COLUMNS[order_column].get(
'filter', REPORTS_COLUMNS[order_column]['attr'])
order_dir = request.args.get('order[0][dir]')
order_args = '[{"field": "%s", "order": "%s"}]' % (order_filter, order_dir)
status_args = request.args.get('columns[1][search][value]', '').split('|')
max_col = len(REPORTS_COLUMNS)
for i in range(len(REPORTS_COLUMNS)):
if request.args.get("columns[%s][data]" % i, None):
max_col = i + 1
envs = environments()
check_env(env, envs)
reports_query = AndOperator()
if env != '*':
reports_query.add(EqualsOperator("environment", env))
if node_name:
reports_query.add(EqualsOperator("certname", node_name))
if search_arg:
search_query = OrOperator()
search_query.add(RegexOperator("certname", r"%s" % search_arg))
search_query.add(RegexOperator("puppet_version", r"%s" % search_arg))
search_query.add(RegexOperator(
"configuration_version", r"%s" % search_arg))
reports_query.add(search_query)
status_query = OrOperator()
for status_arg in status_args:
if status_arg in ['failed', 'changed', 'unchanged']:
arg_query = AndOperator()
arg_query.add(EqualsOperator('status', status_arg))
arg_query.add(EqualsOperator('noop', False))
status_query.add(arg_query)
if status_arg == 'unchanged':
arg_query = AndOperator()
arg_query.add(EqualsOperator('noop', True))
arg_query.add(EqualsOperator('noop_pending', False))
status_query.add(arg_query)
elif status_arg == 'noop':
arg_query = AndOperator()
arg_query.add(EqualsOperator('noop', True))
arg_query.add(EqualsOperator('noop_pending', True))
status_query.add(arg_query)
if len(status_query.operations) == 0:
if len(reports_query.operations) == 0:
reports_query = None
else:
reports_query.add(status_query)
if status_args[0] != 'none':
reports = get_or_abort(
puppetdb.reports,
query=reports_query,
order_by=order_args,
include_total=True,
**paging_args)
reports, reports_events = tee(reports)
total = None
else:
reports = []
reports_events = []
total = 0
report_event_counts = {}
# Create a map from the metrics data to what the templates
# use to express the data.
report_map = {
'success': 'successes',
'failure': 'failures',
'skipped': 'skips',
'noops': 'noop'
}
for report in reports_events:
if total is None:
total = puppetdb.total
report_counts = {'successes': 0, 'failures': 0, 'skips': 0}
for metrics in report.metrics:
if 'name' in metrics and metrics['name'] in report_map:
key_name = report_map[metrics['name']]
report_counts[key_name] = metrics['value']
report_event_counts[report.hash_] = report_counts
if total is None:
total = 0
return render_template(
'reports.json.tpl',
draw=draw,
total=total,
total_filtered=total,
reports=reports,
report_event_counts=report_event_counts,
envs=envs,
current_env=env,
columns=REPORTS_COLUMNS[:max_col])
@app.route('/report/<node_name>/<report_id>',
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/report/<node_name>/<report_id>')
def report(env, node_name, report_id):
"""Displays a single report including all the events associated with that
@@ -626,9 +636,24 @@ def facts(env):
"""
envs = environments()
check_env(env, envs)
facts = []
order_by = '[{"field": "name", "order": "asc"}]'
if env == '*':
facts = get_or_abort(puppetdb.fact_names)
else:
query = ExtractOperator()
query.add_field(str('name'))
query.add_query(EqualsOperator("environment", env))
query.add_group_by(str("name"))
for names in get_or_abort(puppetdb._query,
'facts',
query=query,
order_by=order_by):
facts.append(names['name'])
facts_dict = collections.defaultdict(list)
facts = get_or_abort(puppetdb.fact_names)
for fact in facts:
letter = fact[0].upper()
letter_list = facts_dict[letter]
@@ -637,10 +662,11 @@ def facts(env):
sorted_facts_dict = sorted(facts_dict.items())
return render_template('facts.html',
facts_dict=sorted_facts_dict,
facts_len=sum(map(len,facts_dict.values())) + len(facts_dict)*5,
envs=envs,
current_env=env)
facts_dict=sorted_facts_dict,
facts_len=(sum(map(len, facts_dict.values())) +
len(facts_dict) * 5),
envs=envs,
current_env=env)
@app.route('/fact/<fact>', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@@ -679,7 +705,8 @@ def fact(env, fact):
current_env=env)))
@app.route('/fact/<fact>/<value>', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/fact/<fact>/<value>',
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/fact/<fact>/<value>')
def fact_value(env, fact, value):
"""On asking for fact/value get all nodes with that fact.
@@ -700,9 +727,9 @@ def fact_value(env, fact, value):
query = EqualsOperator("environment", env)
facts = get_or_abort(puppetdb.facts,
name=fact,
value=value,
query=query)
name=fact,
value=value,
query=query)
localfacts = [f for f in yield_or_stop(facts)]
return render_template(
'fact.html',
@@ -713,7 +740,8 @@ def fact_value(env, fact, value):
current_env=env)
@app.route('/query', methods=('GET', 'POST'), defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/query', methods=('GET', 'POST'),
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/query', methods=('GET', 'POST'))
def query(env):
"""Allows to execute raw, user created querries against PuppetDB. This is
@@ -745,14 +773,14 @@ def query(env):
form.endpoints.data,
query=query)
return render_template('query.html',
form=form,
result=result,
envs=envs,
current_env=env)
form=form,
result=result,
envs=envs,
current_env=env)
return render_template('query.html',
form=form,
envs=envs,
current_env=env)
form=form,
envs=envs,
current_env=env)
else:
log.warn('Access to query interface disabled by administrator..')
abort(403)
@@ -772,13 +800,14 @@ def metrics(env):
metrics = get_or_abort(puppetdb._query, 'mbean')
return render_template('metrics.html',
metrics=sorted(metrics.keys()),
envs=envs,
current_env=env)
metrics=sorted(metrics.keys()),
envs=envs,
current_env=env)
@app.route('/metric/<metric>', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/metric/<metric>')
@app.route('/metric/<path:metric>',
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/metric/<path:metric>')
def metric(env, metric):
"""Lists all information about the metric of the given name.
@@ -790,7 +819,7 @@ def metric(env, metric):
check_env(env, envs)
name = unquote(metric)
metric = puppetdb.metric(metric)
metric = get_or_abort(puppetdb.metric, metric)
return render_template(
'metric.html',
name=name,
@@ -798,6 +827,7 @@ def metric(env, metric):
envs=envs,
current_env=env)
@app.route('/catalogs', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/catalogs')
def catalogs(env):
@@ -819,10 +849,11 @@ def catalogs(env):
query.add(NullOperator("catalog_timestamp", False))
order_by_str = '[{"field": "certname", "order": "asc"}]'
nodes = get_or_abort(puppetdb.nodes,
query=query,
with_status=False,
order_by='[{"field": "certname", "order": "asc"}]')
query=query,
with_status=False,
order_by=order_by_str)
nodes, temp = tee(nodes)
for node in temp:
@@ -855,7 +886,9 @@ def catalogs(env):
log.warn('Access to catalog interface disabled by administrator')
abort(403)
@app.route('/catalog/<node_name>', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/catalog/<node_name>',
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/catalog/<node_name>')
def catalog_node(env, node_name):
"""Fetches from PuppetDB the compiled catalog of a given node.
@@ -868,16 +901,18 @@ def catalog_node(env, node_name):
if app.config['ENABLE_CATALOG']:
catalog = get_or_abort(puppetdb.catalog,
node=node_name)
node=node_name)
return render_template('catalog.html',
catalog=catalog,
envs=envs,
current_env=env)
catalog=catalog,
envs=envs,
current_env=env)
else:
log.warn('Access to catalog interface disabled by administrator')
abort(403)
@app.route('/catalog/submit', methods=['POST'], defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/catalog/submit', methods=['POST'],
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/catalog/submit', methods=['POST'])
def catalog_submit(env):
"""Receives the submitted form data from the catalogs page and directs
@@ -901,15 +936,17 @@ def catalog_submit(env):
against = form.against.data
return redirect(
url_for('catalog_compare',
env=env,
compare=compare,
against=against))
env=env,
compare=compare,
against=against))
return redirect(url_for('catalogs', env=env))
else:
log.warn('Access to catalog interface disabled by administrator')
abort(403)
@app.route('/catalogs/compare/<compare>...<against>', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/catalogs/compare/<compare>...<against>',
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/catalogs/compare/<compare>...<against>')
def catalog_compare(env, compare, against):
"""Compares the catalog of one node, parameter compare, with that of
@@ -923,19 +960,20 @@ def catalog_compare(env, compare, against):
if app.config['ENABLE_CATALOG']:
compare_cat = get_or_abort(puppetdb.catalog,
node=compare)
node=compare)
against_cat = get_or_abort(puppetdb.catalog,
node=against)
node=against)
return render_template('catalog_compare.html',
compare=compare_cat,
against=against_cat,
envs=envs,
current_env=env)
compare=compare_cat,
against=against_cat,
envs=envs,
current_env=env)
else:
log.warn('Access to catalog interface disabled by administrator')
abort(403)
@app.route('/radiator', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/radiator')
def radiator(env):
@@ -946,10 +984,13 @@ def radiator(env):
check_env(env, envs)
if env == '*':
query_type = ''
if get_db_version(puppetdb) < (4, 0, 0):
query_type = 'type=default,'
query = None
metrics = get_or_abort(
puppetdb.metric,
'puppetlabs.puppetdb.population:name=num-nodes')
'puppetlabs.puppetdb.population:%sname=num-nodes' % query_type)
num_nodes = metrics['Value']
else:
query = AndOperator()
@@ -966,13 +1007,11 @@ def radiator(env):
query=metric_query)
num_nodes = metrics[0]['count']
nodes = puppetdb.nodes(
query=query,
unreported=app.config['UNRESPONSIVE_HOURS'],
with_status=True
)
)
stats = {
'changed_percent': 0,
@@ -989,8 +1028,6 @@ def radiator(env):
'unreported': 0,
}
for node in nodes:
if node.status == 'unreported':
stats['unreported'] += 1
@@ -1001,21 +1038,55 @@ def radiator(env):
elif node.status == 'noop':
stats['noop'] += 1
elif node.status == 'skipped':
stats['skipped'] +=1
stats['skipped'] += 1
else:
stats['unchanged'] += 1
try:
stats['changed_percent'] = int(100 * (stats['changed'] /
float(num_nodes)))
stats['failed_percent'] = int(100 * stats['failed'] / float(num_nodes))
stats['noop_percent'] = int(100 * stats['noop'] / float(num_nodes))
stats['skipped_percent'] = int(100 * (stats['skipped'] /
float(num_nodes)))
stats['unchanged_percent'] = int(100 * (stats['unchanged'] /
float(num_nodes)))
stats['unreported_percent'] = int(100 * (stats['unreported'] /
float(num_nodes)))
except ZeroDivisionError:
stats['changed_percent'] = 0
stats['failed_percent'] = 0
stats['noop_percent'] = 0
stats['skipped_percent'] = 0
stats['unchanged_percent'] = 0
stats['unreported_percent'] = 0
stats['changed_percent'] = int(100 * stats['changed'] / float(num_nodes))
stats['failed_percent'] = int(100 * stats['failed'] / float(num_nodes))
stats['noop_percent'] = int(100 * stats['noop'] / float(num_nodes))
stats['skipped_percent'] = int(100 * stats['skipped'] / float(num_nodes))
stats['unchanged_percent'] = int(100 * stats['unchanged'] / float(num_nodes))
stats['unreported_percent'] = int(100 * stats['unreported'] / float(num_nodes))
if ('Accept' in request.headers and
request.headers["Accept"] == 'application/json'):
return jsonify(**stats)
return render_template(
'radiator.html',
stats=stats,
total=num_nodes
)
@app.route('/daily_reports_chart.json',
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/daily_reports_chart.json')
def daily_reports_chart(env):
"""Return JSON data to generate a bar chart of daily runs.
If certname is passed as GET argument, the data will target that
node only.
"""
certname = request.args.get('certname')
result = get_or_abort(
get_daily_reports_chart,
db=puppetdb,
env=env,
days_number=app.config['DAILY_REPORTS_CHART_DAYS'],
certname=certname,
)
return jsonify(result=result)

81
puppetboard/dailychart.py Normal file
View File

@@ -0,0 +1,81 @@
from datetime import datetime, timedelta
from pypuppetdb.utils import UTC
from pypuppetdb.QueryBuilder import (
ExtractOperator, FunctionOperator, AndOperator,
GreaterEqualOperator, LessOperator, EqualsOperator,
)
DATE_FORMAT = "%Y-%m-%d"
DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
def _iter_dates(days_number, reverse=False):
"""Return a list of datetime pairs AB, BC, CD, ... that represent the
24hs time ranges of today (until this midnight) and the
previous days.
"""
one_day = timedelta(days=1)
today = datetime.utcnow().replace(hour=0, minute=0, second=0,
microsecond=0, tzinfo=UTC())
days_list = list(today + one_day * (1 - i) for i in range(days_number + 1))
if reverse:
days_list.reverse()
return zip(days_list, days_list[1:])
return zip(days_list[1:], days_list)
def _build_query(env, start, end, certname=None):
"""Build a extract query with optional certname and environment."""
query = ExtractOperator()
query.add_field(FunctionOperator('count'))
query.add_field('status')
subquery = AndOperator()
subquery.add(GreaterEqualOperator('start_time', start))
subquery.add(LessOperator('start_time', end))
if certname is not None:
subquery.add(EqualsOperator('certname', certname))
if env != '*':
subquery.add(EqualsOperator('environment', env))
query.add_query(subquery)
query.add_group_by("status")
return query
def _format_report_data(day, query_output):
"""Format the output of the query to a simpler dict."""
result = {'day': day, 'changed': 0, 'unchanged': 0, 'failed': 0}
for out in query_output:
if out['status'] == 'changed':
result['changed'] = out['count']
elif out['status'] == 'unchanged':
result['unchanged'] = out['count']
elif out['status'] == 'failed':
result['failed'] = out['count']
return result
def get_daily_reports_chart(db, env, days_number, certname=None):
"""Return the sum of each report status (changed, unchanged, failed)
per day, for today and the previous N days.
This information is used to present a chart.
:param db: The puppetdb.
:param env: Sum up the reports in this environment.
:param days_number: How many days to sum, including today.
:param certname: If certname is passed, only the reports of that
certname will be added. If certname is not passed, all reports in
the database will be considered.
"""
result = []
for start, end in _iter_dates(days_number, reverse=True):
query = _build_query(
env=env,
start=start.strftime(DATETIME_FORMAT),
end=end.strftime(DATETIME_FORMAT),
certname=certname,
)
day = start.strftime(DATE_FORMAT)
output = db._query('reports', query=query)
result.append(_format_report_data(day, output))
return result

View File

@@ -15,10 +15,13 @@ UNRESPONSIVE_HOURS = 2
ENABLE_QUERY = True
LOCALISE_TIMESTAMP = True
LOGLEVEL = 'info'
REPORTS_COUNT = 10
NORMAL_TABLE_COUNT = 100
LITTLE_TABLE_COUNT = 10
TABLE_COUNT_SELECTOR = [10, 20, 50, 100, 500]
OFFLINE_MODE = False
ENABLE_CATALOG = False
OVERVIEW_FILTER = None
GRAPH_TYPE = 'pie'
GRAPH_FACTS = ['architecture',
'clientversion',
'domain',
@@ -31,10 +34,12 @@ GRAPH_FACTS = ['architecture',
'osfamily',
'puppetversion',
'processorcount']
INVENTORY_FACTS = [ ('Hostname', 'fqdn' ),
('IP Address', 'ipaddress' ),
('OS', 'lsbdistdescription'),
('Architecture', 'hardwaremodel' ),
('Kernel Version', 'kernelrelease' ),
('Puppet Version', 'puppetversion' ), ]
INVENTORY_FACTS = [('Hostname', 'fqdn'),
('IP Address', 'ipaddress'),
('OS', 'lsbdistdescription'),
('Architecture', 'hardwaremodel'),
('Kernel Version', 'kernelrelease'),
('Puppet Version', 'puppetversion'), ]
REFRESH_RATE = 30
DAILY_REPORTS_CHART_ENABLED = True
DAILY_REPORTS_CHART_DAYS = 8

View File

@@ -0,0 +1,76 @@
import os
PUPPETDB_HOST = os.getenv('PUPPETDB_HOST', 'puppetdb')
PUPPETDB_PORT = int(os.getenv('PUPPETDB_PORT', '8080'))
# Since this is an env it will alwas be a string, we need
# to conver that string to a bool
SSL_VERIFY = os.getenv('PUPPETDB_SSL_VERIFY', 'True')
if SSL_VERIFY.upper() == 'TRUE':
PUPPETDB_SSL_VERIFY = True
elif SSL_VERIFY.upper() == 'FALSE':
PUPPETDB_SSL_VERIFY = False
else:
PUPPETDB_SSL_VERIFY = SSL_VERIFY
PUPPETDB_KEY = os.getenv('PUPPETDB_KEY', None)
PUPPETDB_CERT = os.getenv('PUPPETDB_CERT', None)
PUPPETDB_TIMEOUT = int(os.getenv('PUPPETDB_TIMEOUT', '20'))
DEFAULT_ENVIRONMENT = os.getenv('DEFAULT_ENVIRONMENT', 'production')
SECRET_KEY = os.getenv('SECRET_KEY', os.urandom(24))
DEV_LISTEN_HOST = os.getenv('DEV_LISTEN_HOST', '127.0.0.1')
DEV_LISTEN_PORT = int(os.getenv('DEV_LISTEN_PORT', '5000'))
DEV_COFFEE_LOCATION = os.getenv('DEV_COFFEE_LOCATION', 'coffee')
UNRESPONSIVE_HOURS = int(os.getenv('UNRESPONSIVE_HOURS', '2'))
ENABLE_QUERY = os.getenv('ENABLE_QUERY', 'True')
LOCALISE_TIMESTAMP = bool(os.getenv('LOCALISE_TIMESTAMP',
'True').upper() == 'TRUE')
LOGLEVEL = os.getenv('LOGLEVEL', 'info')
NORMAL_TABLE_COUNT = int(os.getenv('REPORTS_COUNT', '100'))
LITTLE_TABLE_COUNT = int(os.getenv('LITTLE_TABLE_COUNT', '10'))
TABLE_COUNT_DEF = "10,20,50,100,500"
TABLE_COUNT_SELECTOR = [int(x) for x in os.getenv('TABLE_COUNT_SELECTOR',
TABLE_COUNT_DEF).split(',')]
OFFLINE_MODE = bool(os.getenv('OFFLINE_MODE', 'False').upper() == 'TRUE')
ENABLE_CATALOG = bool(os.getenv('ENABLE_CATALOG', 'False').upper() == 'TRUE')
OVERVIEW_FILTER = os.getenv('OVERVIEW_FILTER', None)
GRAPH_FACTS_DEFAULT = ','.join(['architecture', 'clientversion', 'domain',
'lsbcodename', 'lsbdistcodename', 'lsbdistid',
'lsbdistrelease', 'lsbmajdistrelease',
'netmask', 'osfamily', 'puppetversion',
'processorcount'])
GRAPH_FACTS = [x.strip() for x in os.getenv('GRAPH_FACTS',
GRAPH_FACTS_DEFAULT).split(',')]
GRAPH_TYPE = os.getenv('GRAPH_TYPE', 'pie')
# Tuples are hard to express as an environment variable, so here
# the tupple can be listed as a list of items
# export INVENTORY_FACTS="Hostname, fqdn, IP Address, ipaddress,.. etc"
# Define default array of of strings, this code is a bit neater than having
# a large string
INVENTORY_FACTS_DEFAULT = ','.join(['Hostname', 'fqdn',
'IP Address', 'ipaddress',
'OS', 'lsbdistdescription',
'Architecture', 'hardwaremodel',
'Kernel Version', 'kernelrelease',
'Puppet Version', 'puppetversion'])
# take either input as a list Key, Value, Key, Value, and conver it to an
# array: ['Key', 'Value']
INV_STR = os.getenv('INVENTORY_FACTS', INVENTORY_FACTS_DEFAULT).split(',')
# Take the Array and convert it to a tuple
INVENTORY_FACTS = [(INV_STR[i].strip(),
INV_STR[i + 1].strip()) for i in range(0, len(INV_STR), 2)]
REFRESH_RATE = int(os.getenv('REFRESH_RATE', '30'))
DAILY_REPORTS_CHART_ENABLED = bool(os.getenv('DAILY_REPORTS_CHART_ENABLED',
'True').upper() == 'TRUE')
DAILY_REPORTS_CHART_DAYS = int(os.getenv('DAILY_REPORTS_CHART_DAYS', '8'))

View File

@@ -26,9 +26,10 @@ class QueryForm(Form):
('edges', 'Edges'),
('environments', 'Environments'),
('pql', 'PQL'),
])
])
rawjson = BooleanField('Raw JSON')
class CatalogForm(Form):
"""The form used to compare the catalogs of different nodes."""
compare = HiddenField('compare')

View File

@@ -1,21 +1,28 @@
$ = jQuery
$ ->
$('input.filter-list').parent('div').removeClass('hide')
$("input.filter-list").on "keyup", (e) ->
rex = new RegExp($(this).val(), "i")
filter_list = (val) ->
rex = new RegExp(val, "i")
$(".searchable li").hide()
$(".searchable li").parent().parent().hide()
$(".searchable li").parent().parent('.list_hide_segment').hide()
$(".searchable li").filter( ->
rex.test $(this).text()
).show()
$(".searchable li").filter( ->
rex.test $(this).text()
).parent().parent().show()
$("input.filter-list").on "keyup", (e) ->
# If key is escape, reset value
if e.keyCode is 27
$(e.currentTarget).val ""
ev = $.Event("keyup")
ev.keyCode = 13
$(e.currentTarget).trigger(ev)
e.currentTarget.blur()
else
filter_list($(this).val())
$("input.filter-list").ready ->
elem = $("input.filter-list")
elem.focus()
val = elem.val()
filter_list(val)
# Force cursor at the end
elem.val('').val(val)

View File

@@ -1,24 +0,0 @@
$ = jQuery
$ ->
if $('th.default-sort').data()
$('table.sortable').tablesort().data('tablesort').sort($("th.default-sort"),"desc")
$('thead th.date').data 'sortBy', (th, td, tablesort) ->
return moment.utc(td.text()).unix()
$('input.filter-table').parent('div').removeClass('hide')
$("input.filter-table").on "keyup", (e) ->
rex = new RegExp($(this).val(), "i")
$(".searchable tr").hide()
$(".searchable tr").filter( ->
rex.test $(this).text()
).show()
if e.keyCode is 27
$(e.currentTarget).val ""
ev = $.Event("keyup")
ev.keyCode = 13
$(e.currentTarget).trigger(ev)
e.currentTarget.blur()

1
puppetboard/static/css/c3.min.css vendored Normal file
View File

@@ -0,0 +1 @@
.c3 svg{font:10px sans-serif;-webkit-tap-highlight-color:transparent}.c3 line,.c3 path{fill:none;stroke:#000}.c3 text{-webkit-user-select:none;-moz-user-select:none;user-select:none}.c3-bars path,.c3-event-rect,.c3-legend-item-tile,.c3-xgrid-focus,.c3-ygrid{shape-rendering:crispEdges}.c3-chart-arc path{stroke:#fff}.c3-chart-arc text{fill:#fff;font-size:13px}.c3-grid line{stroke:#aaa}.c3-grid text{fill:#aaa}.c3-xgrid,.c3-ygrid{stroke-dasharray:3 3}.c3-text.c3-empty{fill:gray;font-size:2em}.c3-line{stroke-width:1px}.c3-circle._expanded_{stroke-width:1px;stroke:#fff}.c3-selected-circle{fill:#fff;stroke-width:2px}.c3-bar{stroke-width:0}.c3-bar._expanded_{fill-opacity:.75}.c3-target.c3-focused{opacity:1}.c3-target.c3-focused path.c3-line,.c3-target.c3-focused path.c3-step{stroke-width:2px}.c3-target.c3-defocused{opacity:.3!important}.c3-region{fill:#4682b4;fill-opacity:.1}.c3-brush .extent{fill-opacity:.1}.c3-legend-item{font-size:12px}.c3-legend-item-hidden{opacity:.15}.c3-legend-background{opacity:.75;fill:#fff;stroke:#d3d3d3;stroke-width:1}.c3-title{font:14px sans-serif}.c3-tooltip-container{z-index:10}.c3-tooltip{border-collapse:collapse;border-spacing:0;background-color:#fff;empty-cells:show;-webkit-box-shadow:7px 7px 12px -9px #777;-moz-box-shadow:7px 7px 12px -9px #777;box-shadow:7px 7px 12px -9px #777;opacity:.9}.c3-tooltip tr{border:1px solid #CCC}.c3-tooltip th{background-color:#aaa;font-size:14px;padding:2px 5px;text-align:left;color:#FFF}.c3-tooltip td{font-size:13px;padding:3px 6px;background-color:#fff;border-left:1px dotted #999}.c3-tooltip td>span{display:inline-block;width:10px;height:10px;margin-right:6px}.c3-tooltip td.value{text-align:right}.c3-area{stroke-width:0;opacity:.2}.c3-chart-arcs-title{dominant-baseline:middle;font-size:1.3em}.c3-chart-arcs .c3-chart-arcs-background{fill:#e0e0e0;stroke:none}.c3-chart-arcs .c3-chart-arcs-gauge-unit{fill:#000;font-size:16px}.c3-chart-arcs .c3-chart-arcs-gauge-max,.c3-chart-arcs .c3-chart-arcs-gauge-min{fill:#777}.c3-chart-arc .c3-gauge-value{fill:#000}

View File

@@ -13,22 +13,8 @@ h1.ui.header.no-margin-bottom {
margin-bottom: 0;
}
.tablesorter-header-inner {
float: left;
}
th.tablesorter-headerAsc::after {
content: '\25b4' !important;
float: right;
}
th.tablesorter-headerDesc::after {
content: '\25be' !important;
float: right;
}
.ui.grid.padding-bottom {
padding-bottom: 40px !important;
padding-bottom: 4em !important;
}
.status {
@@ -194,3 +180,7 @@ th.tablesorter-headerDesc::after {
margin-right: -50%;
transform: translate(-50%, -50%)
}
#dailyReportsChartContainer {
height: 160px;
}

View File

@@ -39,4 +39,4 @@ body.radiator_controller table.node_summary tr td .percent {color:#000;position:
body.radiator_controller table.node_summary tr td .percent span {margin-left:0.1em;}
body.radiator_controller table.node_summary tr td .label {position:relative;height:100%;}
body.radiator_controller table.node_summary tr td .label span {margin-left:0.1em;}
body.radiator_controller table.node_summary tr td .count {text-align:right;width:1.75em;display:inline-block;font-weight:bold;margin-top:-0.12em;}
body.radiator_controller table.node_summary tr td .count {text-align:right;width:100%;display:inline-block;font-weight:bold;margin-top:-0.12em;}

View File

@@ -0,0 +1 @@
table.dataTable.table{margin:0}table.dataTable.table thead th,table.dataTable.table thead td{position:relative}table.dataTable.table thead th.sorting,table.dataTable.table thead th.sorting_asc,table.dataTable.table thead th.sorting_desc,table.dataTable.table thead td.sorting,table.dataTable.table thead td.sorting_asc,table.dataTable.table thead td.sorting_desc{padding-right:20px}table.dataTable.table thead th.sorting:after,table.dataTable.table thead th.sorting_asc:after,table.dataTable.table thead th.sorting_desc:after,table.dataTable.table thead td.sorting:after,table.dataTable.table thead td.sorting_asc:after,table.dataTable.table thead td.sorting_desc:after{position:absolute;top:12px;right:8px;display:block;font-family:Icons}table.dataTable.table thead th.sorting:after,table.dataTable.table thead td.sorting:after{content:"\f0dc";color:#ddd;font-size:0.8em}table.dataTable.table thead th.sorting_asc:after,table.dataTable.table thead td.sorting_asc:after{content:"\f0de"}table.dataTable.table thead th.sorting_desc:after,table.dataTable.table thead td.sorting_desc:after{content:"\f0dd"}table.dataTable.table td,table.dataTable.table th{-webkit-box-sizing:content-box;box-sizing:content-box}table.dataTable.table td.dataTables_empty,table.dataTable.table th.dataTables_empty{text-align:center}table.dataTable.table.nowrap th,table.dataTable.table.nowrap td{white-space:nowrap}div.dataTables_wrapper div.dataTables_length select{vertical-align:middle;min-height:2.7142em}div.dataTables_wrapper div.dataTables_length .ui.selection.dropdown{min-width:0}div.dataTables_wrapper div.dataTables_filter input{margin-left:0.5em}div.dataTables_wrapper div.dataTables_info{padding-top:13px;white-space:nowrap}div.dataTables_wrapper div.dataTables_processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;text-align:center}div.dataTables_wrapper div.row.dt-table{padding:0}div.dataTables_wrapper div.dataTables_scrollHead table.dataTable{border-bottom-right-radius:0;border-bottom-left-radius:0;border-bottom:none}div.dataTables_wrapper div.dataTables_scrollBody thead .sorting:after,div.dataTables_wrapper div.dataTables_scrollBody thead .sorting_asc:after,div.dataTables_wrapper div.dataTables_scrollBody thead .sorting_desc:after{display:none}div.dataTables_wrapper div.dataTables_scrollBody table.dataTable{border-radius:0;border-top:none;border-bottom-width:0}div.dataTables_wrapper div.dataTables_scrollBody table.dataTable.no-footer{border-bottom-width:1px}div.dataTables_wrapper div.dataTables_scrollFoot table.dataTable{border-top-right-radius:0;border-top-left-radius:0;border-top:none}

View File

@@ -0,0 +1,9 @@
/*!
DataTables Bootstrap 3 integration
©2011-2015 SpryMedia Ltd - datatables.net/license
*/
(function(b){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(a){return b(a,window,document)}):"object"===typeof exports?module.exports=function(a,d){a||(a=window);if(!d||!d.fn.dataTable)d=require("datatables.net")(a,d).$;return b(d,a,a.document)}:b(jQuery,window,document)})(function(b,a,d,m){var e=b.fn.dataTable;b.extend(!0,e.defaults,{dom:"<'ui grid'<'row'<'eight wide column'l><'right aligned eight wide column'f>><'row dt-table'<'sixteen wide column'tr>><'row'<'seven wide column'i><'right aligned nine wide column'p>>>",
renderer:"semanticUI"});b.extend(e.ext.classes,{sWrapper:"dataTables_wrapper dt-semanticUI",sFilter:"dataTables_filter ui input",sProcessing:"dataTables_processing ui segment",sPageButton:"paginate_button item"});e.ext.renderer.pageButton.semanticUI=function(h,a,r,s,j,n){var o=new e.Api(h),t=h.oClasses,k=h.oLanguage.oPaginate,u=h.oLanguage.oAria.paginate||{},f,g,p=0,q=function(a,d){var e,i,l,c,m=function(a){a.preventDefault();!b(a.currentTarget).hasClass("disabled")&&o.page()!=a.data.action&&o.page(a.data.action).draw("page")};
e=0;for(i=d.length;e<i;e++)if(c=d[e],b.isArray(c))q(a,c);else{g=f="";switch(c){case "ellipsis":f="&#x2026;";g="disabled";break;case "first":f=k.sFirst;g=c+(0<j?"":" disabled");break;case "previous":f=k.sPrevious;g=c+(0<j?"":" disabled");break;case "next":f=k.sNext;g=c+(j<n-1?"":" disabled");break;case "last":f=k.sLast;g=c+(j<n-1?"":" disabled");break;default:f=c+1,g=j===c?"active":""}l=-1===g.indexOf("disabled")?"a":"div";f&&(l=b("<"+l+">",{"class":t.sPageButton+" "+g,id:0===r&&"string"===typeof c?
h.sTableId+"_"+c:null,href:"#","aria-controls":h.sTableId,"aria-label":u[c],"data-dt-idx":p,tabindex:h.iTabIndex}).html(f).appendTo(a),h.oApi._fnBindAction(l,{action:c},m),p++)}},i;try{i=b(a).find(d.activeElement).data("dt-idx")}catch(v){}q(b(a).empty().html('<div class="ui pagination menu"/>').children(),s);i!==m&&b(a).find("[data-dt-idx="+i+"]").focus()};b(d).on("init.dt",function(a,d){if("dt"===a.namespace&&b.fn.dropdown){var e=new b.fn.dataTable.Api(d);b("div.dataTables_length select",e.table().container()).dropdown()}});
return e});

View File

@@ -0,0 +1,167 @@
/*!
DataTables 1.10.13
©2008-2016 SpryMedia Ltd - datatables.net/license
*/
(function(h){"function"===typeof define&&define.amd?define(["jquery"],function(E){return h(E,window,document)}):"object"===typeof exports?module.exports=function(E,H){E||(E=window);H||(H="undefined"!==typeof window?require("jquery"):require("jquery")(E));return h(H,E,E.document)}:h(jQuery,window,document)})(function(h,E,H,k){function Y(a){var b,c,d={};h.each(a,function(e){if((b=e.match(/^([^A-Z]+?)([A-Z])/))&&-1!=="a aa ai ao as b fn i m o s ".indexOf(b[1]+" "))c=e.replace(b[0],b[2].toLowerCase()),
d[c]=e,"o"===b[1]&&Y(a[e])});a._hungarianMap=d}function J(a,b,c){a._hungarianMap||Y(a);var d;h.each(b,function(e){d=a._hungarianMap[e];if(d!==k&&(c||b[d]===k))"o"===d.charAt(0)?(b[d]||(b[d]={}),h.extend(!0,b[d],b[e]),J(a[d],b[d],c)):b[d]=b[e]})}function Fa(a){var b=m.defaults.oLanguage,c=a.sZeroRecords;!a.sEmptyTable&&(c&&"No data available in table"===b.sEmptyTable)&&F(a,a,"sZeroRecords","sEmptyTable");!a.sLoadingRecords&&(c&&"Loading..."===b.sLoadingRecords)&&F(a,a,"sZeroRecords","sLoadingRecords");
a.sInfoThousands&&(a.sThousands=a.sInfoThousands);(a=a.sDecimal)&&fb(a)}function gb(a){A(a,"ordering","bSort");A(a,"orderMulti","bSortMulti");A(a,"orderClasses","bSortClasses");A(a,"orderCellsTop","bSortCellsTop");A(a,"order","aaSorting");A(a,"orderFixed","aaSortingFixed");A(a,"paging","bPaginate");A(a,"pagingType","sPaginationType");A(a,"pageLength","iDisplayLength");A(a,"searching","bFilter");"boolean"===typeof a.sScrollX&&(a.sScrollX=a.sScrollX?"100%":"");"boolean"===typeof a.scrollX&&(a.scrollX=
a.scrollX?"100%":"");if(a=a.aoSearchCols)for(var b=0,c=a.length;b<c;b++)a[b]&&J(m.models.oSearch,a[b])}function hb(a){A(a,"orderable","bSortable");A(a,"orderData","aDataSort");A(a,"orderSequence","asSorting");A(a,"orderDataType","sortDataType");var b=a.aDataSort;b&&!h.isArray(b)&&(a.aDataSort=[b])}function ib(a){if(!m.__browser){var b={};m.__browser=b;var c=h("<div/>").css({position:"fixed",top:0,left:-1*h(E).scrollLeft(),height:1,width:1,overflow:"hidden"}).append(h("<div/>").css({position:"absolute",
top:1,left:1,width:100,overflow:"scroll"}).append(h("<div/>").css({width:"100%",height:10}))).appendTo("body"),d=c.children(),e=d.children();b.barWidth=d[0].offsetWidth-d[0].clientWidth;b.bScrollOversize=100===e[0].offsetWidth&&100!==d[0].clientWidth;b.bScrollbarLeft=1!==Math.round(e.offset().left);b.bBounding=c[0].getBoundingClientRect().width?!0:!1;c.remove()}h.extend(a.oBrowser,m.__browser);a.oScroll.iBarWidth=m.__browser.barWidth}function jb(a,b,c,d,e,f){var g,j=!1;c!==k&&(g=c,j=!0);for(;d!==
e;)a.hasOwnProperty(d)&&(g=j?b(g,a[d],d,a):a[d],j=!0,d+=f);return g}function Ga(a,b){var c=m.defaults.column,d=a.aoColumns.length,c=h.extend({},m.models.oColumn,c,{nTh:b?b:H.createElement("th"),sTitle:c.sTitle?c.sTitle:b?b.innerHTML:"",aDataSort:c.aDataSort?c.aDataSort:[d],mData:c.mData?c.mData:d,idx:d});a.aoColumns.push(c);c=a.aoPreSearchCols;c[d]=h.extend({},m.models.oSearch,c[d]);la(a,d,h(b).data())}function la(a,b,c){var b=a.aoColumns[b],d=a.oClasses,e=h(b.nTh);if(!b.sWidthOrig){b.sWidthOrig=
e.attr("width")||null;var f=(e.attr("style")||"").match(/width:\s*(\d+[pxem%]+)/);f&&(b.sWidthOrig=f[1])}c!==k&&null!==c&&(hb(c),J(m.defaults.column,c),c.mDataProp!==k&&!c.mData&&(c.mData=c.mDataProp),c.sType&&(b._sManualType=c.sType),c.className&&!c.sClass&&(c.sClass=c.className),h.extend(b,c),F(b,c,"sWidth","sWidthOrig"),c.iDataSort!==k&&(b.aDataSort=[c.iDataSort]),F(b,c,"aDataSort"));var g=b.mData,j=R(g),i=b.mRender?R(b.mRender):null,c=function(a){return"string"===typeof a&&-1!==a.indexOf("@")};
b._bAttrSrc=h.isPlainObject(g)&&(c(g.sort)||c(g.type)||c(g.filter));b._setter=null;b.fnGetData=function(a,b,c){var d=j(a,b,k,c);return i&&b?i(d,b,a,c):d};b.fnSetData=function(a,b,c){return S(g)(a,b,c)};"number"!==typeof g&&(a._rowReadObject=!0);a.oFeatures.bSort||(b.bSortable=!1,e.addClass(d.sSortableNone));a=-1!==h.inArray("asc",b.asSorting);c=-1!==h.inArray("desc",b.asSorting);!b.bSortable||!a&&!c?(b.sSortingClass=d.sSortableNone,b.sSortingClassJUI=""):a&&!c?(b.sSortingClass=d.sSortableAsc,b.sSortingClassJUI=
d.sSortJUIAscAllowed):!a&&c?(b.sSortingClass=d.sSortableDesc,b.sSortingClassJUI=d.sSortJUIDescAllowed):(b.sSortingClass=d.sSortable,b.sSortingClassJUI=d.sSortJUI)}function Z(a){if(!1!==a.oFeatures.bAutoWidth){var b=a.aoColumns;Ha(a);for(var c=0,d=b.length;c<d;c++)b[c].nTh.style.width=b[c].sWidth}b=a.oScroll;(""!==b.sY||""!==b.sX)&&ma(a);s(a,null,"column-sizing",[a])}function $(a,b){var c=na(a,"bVisible");return"number"===typeof c[b]?c[b]:null}function aa(a,b){var c=na(a,"bVisible"),c=h.inArray(b,
c);return-1!==c?c:null}function ba(a){var b=0;h.each(a.aoColumns,function(a,d){d.bVisible&&"none"!==h(d.nTh).css("display")&&b++});return b}function na(a,b){var c=[];h.map(a.aoColumns,function(a,e){a[b]&&c.push(e)});return c}function Ia(a){var b=a.aoColumns,c=a.aoData,d=m.ext.type.detect,e,f,g,j,i,h,l,q,r;e=0;for(f=b.length;e<f;e++)if(l=b[e],r=[],!l.sType&&l._sManualType)l.sType=l._sManualType;else if(!l.sType){g=0;for(j=d.length;g<j;g++){i=0;for(h=c.length;i<h;i++){r[i]===k&&(r[i]=B(a,i,e,"type"));
q=d[g](r[i],a);if(!q&&g!==d.length-1)break;if("html"===q)break}if(q){l.sType=q;break}}l.sType||(l.sType="string")}}function kb(a,b,c,d){var e,f,g,j,i,n,l=a.aoColumns;if(b)for(e=b.length-1;0<=e;e--){n=b[e];var q=n.targets!==k?n.targets:n.aTargets;h.isArray(q)||(q=[q]);f=0;for(g=q.length;f<g;f++)if("number"===typeof q[f]&&0<=q[f]){for(;l.length<=q[f];)Ga(a);d(q[f],n)}else if("number"===typeof q[f]&&0>q[f])d(l.length+q[f],n);else if("string"===typeof q[f]){j=0;for(i=l.length;j<i;j++)("_all"==q[f]||h(l[j].nTh).hasClass(q[f]))&&
d(j,n)}}if(c){e=0;for(a=c.length;e<a;e++)d(e,c[e])}}function N(a,b,c,d){var e=a.aoData.length,f=h.extend(!0,{},m.models.oRow,{src:c?"dom":"data",idx:e});f._aData=b;a.aoData.push(f);for(var g=a.aoColumns,j=0,i=g.length;j<i;j++)g[j].sType=null;a.aiDisplayMaster.push(e);b=a.rowIdFn(b);b!==k&&(a.aIds[b]=f);(c||!a.oFeatures.bDeferRender)&&Ja(a,e,c,d);return e}function oa(a,b){var c;b instanceof h||(b=h(b));return b.map(function(b,e){c=Ka(a,e);return N(a,c.data,e,c.cells)})}function B(a,b,c,d){var e=a.iDraw,
f=a.aoColumns[c],g=a.aoData[b]._aData,j=f.sDefaultContent,i=f.fnGetData(g,d,{settings:a,row:b,col:c});if(i===k)return a.iDrawError!=e&&null===j&&(K(a,0,"Requested unknown parameter "+("function"==typeof f.mData?"{function}":"'"+f.mData+"'")+" for row "+b+", column "+c,4),a.iDrawError=e),j;if((i===g||null===i)&&null!==j&&d!==k)i=j;else if("function"===typeof i)return i.call(g);return null===i&&"display"==d?"":i}function lb(a,b,c,d){a.aoColumns[c].fnSetData(a.aoData[b]._aData,d,{settings:a,row:b,col:c})}
function La(a){return h.map(a.match(/(\\.|[^\.])+/g)||[""],function(a){return a.replace(/\\\./g,".")})}function R(a){if(h.isPlainObject(a)){var b={};h.each(a,function(a,c){c&&(b[a]=R(c))});return function(a,c,f,g){var j=b[c]||b._;return j!==k?j(a,c,f,g):a}}if(null===a)return function(a){return a};if("function"===typeof a)return function(b,c,f,g){return a(b,c,f,g)};if("string"===typeof a&&(-1!==a.indexOf(".")||-1!==a.indexOf("[")||-1!==a.indexOf("("))){var c=function(a,b,f){var g,j;if(""!==f){j=La(f);
for(var i=0,n=j.length;i<n;i++){f=j[i].match(ca);g=j[i].match(V);if(f){j[i]=j[i].replace(ca,"");""!==j[i]&&(a=a[j[i]]);g=[];j.splice(0,i+1);j=j.join(".");if(h.isArray(a)){i=0;for(n=a.length;i<n;i++)g.push(c(a[i],b,j))}a=f[0].substring(1,f[0].length-1);a=""===a?g:g.join(a);break}else if(g){j[i]=j[i].replace(V,"");a=a[j[i]]();continue}if(null===a||a[j[i]]===k)return k;a=a[j[i]]}}return a};return function(b,e){return c(b,e,a)}}return function(b){return b[a]}}function S(a){if(h.isPlainObject(a))return S(a._);
if(null===a)return function(){};if("function"===typeof a)return function(b,d,e){a(b,"set",d,e)};if("string"===typeof a&&(-1!==a.indexOf(".")||-1!==a.indexOf("[")||-1!==a.indexOf("("))){var b=function(a,d,e){var e=La(e),f;f=e[e.length-1];for(var g,j,i=0,n=e.length-1;i<n;i++){g=e[i].match(ca);j=e[i].match(V);if(g){e[i]=e[i].replace(ca,"");a[e[i]]=[];f=e.slice();f.splice(0,i+1);g=f.join(".");if(h.isArray(d)){j=0;for(n=d.length;j<n;j++)f={},b(f,d[j],g),a[e[i]].push(f)}else a[e[i]]=d;return}j&&(e[i]=e[i].replace(V,
""),a=a[e[i]](d));if(null===a[e[i]]||a[e[i]]===k)a[e[i]]={};a=a[e[i]]}if(f.match(V))a[f.replace(V,"")](d);else a[f.replace(ca,"")]=d};return function(c,d){return b(c,d,a)}}return function(b,d){b[a]=d}}function Ma(a){return D(a.aoData,"_aData")}function pa(a){a.aoData.length=0;a.aiDisplayMaster.length=0;a.aiDisplay.length=0;a.aIds={}}function qa(a,b,c){for(var d=-1,e=0,f=a.length;e<f;e++)a[e]==b?d=e:a[e]>b&&a[e]--; -1!=d&&c===k&&a.splice(d,1)}function da(a,b,c,d){var e=a.aoData[b],f,g=function(c,d){for(;c.childNodes.length;)c.removeChild(c.firstChild);
c.innerHTML=B(a,b,d,"display")};if("dom"===c||(!c||"auto"===c)&&"dom"===e.src)e._aData=Ka(a,e,d,d===k?k:e._aData).data;else{var j=e.anCells;if(j)if(d!==k)g(j[d],d);else{c=0;for(f=j.length;c<f;c++)g(j[c],c)}}e._aSortData=null;e._aFilterData=null;g=a.aoColumns;if(d!==k)g[d].sType=null;else{c=0;for(f=g.length;c<f;c++)g[c].sType=null;Na(a,e)}}function Ka(a,b,c,d){var e=[],f=b.firstChild,g,j,i=0,n,l=a.aoColumns,q=a._rowReadObject,d=d!==k?d:q?{}:[],r=function(a,b){if("string"===typeof a){var c=a.indexOf("@");
-1!==c&&(c=a.substring(c+1),S(a)(d,b.getAttribute(c)))}},m=function(a){if(c===k||c===i)j=l[i],n=h.trim(a.innerHTML),j&&j._bAttrSrc?(S(j.mData._)(d,n),r(j.mData.sort,a),r(j.mData.type,a),r(j.mData.filter,a)):q?(j._setter||(j._setter=S(j.mData)),j._setter(d,n)):d[i]=n;i++};if(f)for(;f;){g=f.nodeName.toUpperCase();if("TD"==g||"TH"==g)m(f),e.push(f);f=f.nextSibling}else{e=b.anCells;f=0;for(g=e.length;f<g;f++)m(e[f])}if(b=b.firstChild?b:b.nTr)(b=b.getAttribute("id"))&&S(a.rowId)(d,b);return{data:d,cells:e}}
function Ja(a,b,c,d){var e=a.aoData[b],f=e._aData,g=[],j,i,n,l,q;if(null===e.nTr){j=c||H.createElement("tr");e.nTr=j;e.anCells=g;j._DT_RowIndex=b;Na(a,e);l=0;for(q=a.aoColumns.length;l<q;l++){n=a.aoColumns[l];i=c?d[l]:H.createElement(n.sCellType);i._DT_CellIndex={row:b,column:l};g.push(i);if((!c||n.mRender||n.mData!==l)&&(!h.isPlainObject(n.mData)||n.mData._!==l+".display"))i.innerHTML=B(a,b,l,"display");n.sClass&&(i.className+=" "+n.sClass);n.bVisible&&!c?j.appendChild(i):!n.bVisible&&c&&i.parentNode.removeChild(i);
n.fnCreatedCell&&n.fnCreatedCell.call(a.oInstance,i,B(a,b,l),f,b,l)}s(a,"aoRowCreatedCallback",null,[j,f,b])}e.nTr.setAttribute("role","row")}function Na(a,b){var c=b.nTr,d=b._aData;if(c){var e=a.rowIdFn(d);e&&(c.id=e);d.DT_RowClass&&(e=d.DT_RowClass.split(" "),b.__rowc=b.__rowc?sa(b.__rowc.concat(e)):e,h(c).removeClass(b.__rowc.join(" ")).addClass(d.DT_RowClass));d.DT_RowAttr&&h(c).attr(d.DT_RowAttr);d.DT_RowData&&h(c).data(d.DT_RowData)}}function mb(a){var b,c,d,e,f,g=a.nTHead,j=a.nTFoot,i=0===
h("th, td",g).length,n=a.oClasses,l=a.aoColumns;i&&(e=h("<tr/>").appendTo(g));b=0;for(c=l.length;b<c;b++)f=l[b],d=h(f.nTh).addClass(f.sClass),i&&d.appendTo(e),a.oFeatures.bSort&&(d.addClass(f.sSortingClass),!1!==f.bSortable&&(d.attr("tabindex",a.iTabIndex).attr("aria-controls",a.sTableId),Oa(a,f.nTh,b))),f.sTitle!=d[0].innerHTML&&d.html(f.sTitle),Pa(a,"header")(a,d,f,n);i&&ea(a.aoHeader,g);h(g).find(">tr").attr("role","row");h(g).find(">tr>th, >tr>td").addClass(n.sHeaderTH);h(j).find(">tr>th, >tr>td").addClass(n.sFooterTH);
if(null!==j){a=a.aoFooter[0];b=0;for(c=a.length;b<c;b++)f=l[b],f.nTf=a[b].cell,f.sClass&&h(f.nTf).addClass(f.sClass)}}function fa(a,b,c){var d,e,f,g=[],j=[],i=a.aoColumns.length,n;if(b){c===k&&(c=!1);d=0;for(e=b.length;d<e;d++){g[d]=b[d].slice();g[d].nTr=b[d].nTr;for(f=i-1;0<=f;f--)!a.aoColumns[f].bVisible&&!c&&g[d].splice(f,1);j.push([])}d=0;for(e=g.length;d<e;d++){if(a=g[d].nTr)for(;f=a.firstChild;)a.removeChild(f);f=0;for(b=g[d].length;f<b;f++)if(n=i=1,j[d][f]===k){a.appendChild(g[d][f].cell);
for(j[d][f]=1;g[d+i]!==k&&g[d][f].cell==g[d+i][f].cell;)j[d+i][f]=1,i++;for(;g[d][f+n]!==k&&g[d][f].cell==g[d][f+n].cell;){for(c=0;c<i;c++)j[d+c][f+n]=1;n++}h(g[d][f].cell).attr("rowspan",i).attr("colspan",n)}}}}function O(a){var b=s(a,"aoPreDrawCallback","preDraw",[a]);if(-1!==h.inArray(!1,b))C(a,!1);else{var b=[],c=0,d=a.asStripeClasses,e=d.length,f=a.oLanguage,g=a.iInitDisplayStart,j="ssp"==y(a),i=a.aiDisplay;a.bDrawing=!0;g!==k&&-1!==g&&(a._iDisplayStart=j?g:g>=a.fnRecordsDisplay()?0:g,a.iInitDisplayStart=
-1);var g=a._iDisplayStart,n=a.fnDisplayEnd();if(a.bDeferLoading)a.bDeferLoading=!1,a.iDraw++,C(a,!1);else if(j){if(!a.bDestroying&&!nb(a))return}else a.iDraw++;if(0!==i.length){f=j?a.aoData.length:n;for(j=j?0:g;j<f;j++){var l=i[j],q=a.aoData[l];null===q.nTr&&Ja(a,l);l=q.nTr;if(0!==e){var r=d[c%e];q._sRowStripe!=r&&(h(l).removeClass(q._sRowStripe).addClass(r),q._sRowStripe=r)}s(a,"aoRowCallback",null,[l,q._aData,c,j]);b.push(l);c++}}else c=f.sZeroRecords,1==a.iDraw&&"ajax"==y(a)?c=f.sLoadingRecords:
f.sEmptyTable&&0===a.fnRecordsTotal()&&(c=f.sEmptyTable),b[0]=h("<tr/>",{"class":e?d[0]:""}).append(h("<td />",{valign:"top",colSpan:ba(a),"class":a.oClasses.sRowEmpty}).html(c))[0];s(a,"aoHeaderCallback","header",[h(a.nTHead).children("tr")[0],Ma(a),g,n,i]);s(a,"aoFooterCallback","footer",[h(a.nTFoot).children("tr")[0],Ma(a),g,n,i]);d=h(a.nTBody);d.children().detach();d.append(h(b));s(a,"aoDrawCallback","draw",[a]);a.bSorted=!1;a.bFiltered=!1;a.bDrawing=!1}}function T(a,b){var c=a.oFeatures,d=c.bFilter;
c.bSort&&ob(a);d?ga(a,a.oPreviousSearch):a.aiDisplay=a.aiDisplayMaster.slice();!0!==b&&(a._iDisplayStart=0);a._drawHold=b;O(a);a._drawHold=!1}function pb(a){var b=a.oClasses,c=h(a.nTable),c=h("<div/>").insertBefore(c),d=a.oFeatures,e=h("<div/>",{id:a.sTableId+"_wrapper","class":b.sWrapper+(a.nTFoot?"":" "+b.sNoFooter)});a.nHolding=c[0];a.nTableWrapper=e[0];a.nTableReinsertBefore=a.nTable.nextSibling;for(var f=a.sDom.split(""),g,j,i,n,l,q,k=0;k<f.length;k++){g=null;j=f[k];if("<"==j){i=h("<div/>")[0];
n=f[k+1];if("'"==n||'"'==n){l="";for(q=2;f[k+q]!=n;)l+=f[k+q],q++;"H"==l?l=b.sJUIHeader:"F"==l&&(l=b.sJUIFooter);-1!=l.indexOf(".")?(n=l.split("."),i.id=n[0].substr(1,n[0].length-1),i.className=n[1]):"#"==l.charAt(0)?i.id=l.substr(1,l.length-1):i.className=l;k+=q}e.append(i);e=h(i)}else if(">"==j)e=e.parent();else if("l"==j&&d.bPaginate&&d.bLengthChange)g=qb(a);else if("f"==j&&d.bFilter)g=rb(a);else if("r"==j&&d.bProcessing)g=sb(a);else if("t"==j)g=tb(a);else if("i"==j&&d.bInfo)g=ub(a);else if("p"==
j&&d.bPaginate)g=vb(a);else if(0!==m.ext.feature.length){i=m.ext.feature;q=0;for(n=i.length;q<n;q++)if(j==i[q].cFeature){g=i[q].fnInit(a);break}}g&&(i=a.aanFeatures,i[j]||(i[j]=[]),i[j].push(g),e.append(g))}c.replaceWith(e);a.nHolding=null}function ea(a,b){var c=h(b).children("tr"),d,e,f,g,j,i,n,l,q,k;a.splice(0,a.length);f=0;for(i=c.length;f<i;f++)a.push([]);f=0;for(i=c.length;f<i;f++){d=c[f];for(e=d.firstChild;e;){if("TD"==e.nodeName.toUpperCase()||"TH"==e.nodeName.toUpperCase()){l=1*e.getAttribute("colspan");
q=1*e.getAttribute("rowspan");l=!l||0===l||1===l?1:l;q=!q||0===q||1===q?1:q;g=0;for(j=a[f];j[g];)g++;n=g;k=1===l?!0:!1;for(j=0;j<l;j++)for(g=0;g<q;g++)a[f+g][n+j]={cell:e,unique:k},a[f+g].nTr=d}e=e.nextSibling}}}function ta(a,b,c){var d=[];c||(c=a.aoHeader,b&&(c=[],ea(c,b)));for(var b=0,e=c.length;b<e;b++)for(var f=0,g=c[b].length;f<g;f++)if(c[b][f].unique&&(!d[f]||!a.bSortCellsTop))d[f]=c[b][f].cell;return d}function ua(a,b,c){s(a,"aoServerParams","serverParams",[b]);if(b&&h.isArray(b)){var d={},
e=/(.*?)\[\]$/;h.each(b,function(a,b){var c=b.name.match(e);c?(c=c[0],d[c]||(d[c]=[]),d[c].push(b.value)):d[b.name]=b.value});b=d}var f,g=a.ajax,j=a.oInstance,i=function(b){s(a,null,"xhr",[a,b,a.jqXHR]);c(b)};if(h.isPlainObject(g)&&g.data){f=g.data;var n=h.isFunction(f)?f(b,a):f,b=h.isFunction(f)&&n?n:h.extend(!0,b,n);delete g.data}n={data:b,success:function(b){var c=b.error||b.sError;c&&K(a,0,c);a.json=b;i(b)},dataType:"json",cache:!1,type:a.sServerMethod,error:function(b,c){var d=s(a,null,"xhr",
[a,null,a.jqXHR]);-1===h.inArray(!0,d)&&("parsererror"==c?K(a,0,"Invalid JSON response",1):4===b.readyState&&K(a,0,"Ajax error",7));C(a,!1)}};a.oAjaxData=b;s(a,null,"preXhr",[a,b]);a.fnServerData?a.fnServerData.call(j,a.sAjaxSource,h.map(b,function(a,b){return{name:b,value:a}}),i,a):a.sAjaxSource||"string"===typeof g?a.jqXHR=h.ajax(h.extend(n,{url:g||a.sAjaxSource})):h.isFunction(g)?a.jqXHR=g.call(j,b,i,a):(a.jqXHR=h.ajax(h.extend(n,g)),g.data=f)}function nb(a){return a.bAjaxDataGet?(a.iDraw++,C(a,
!0),ua(a,wb(a),function(b){xb(a,b)}),!1):!0}function wb(a){var b=a.aoColumns,c=b.length,d=a.oFeatures,e=a.oPreviousSearch,f=a.aoPreSearchCols,g,j=[],i,n,l,k=W(a);g=a._iDisplayStart;i=!1!==d.bPaginate?a._iDisplayLength:-1;var r=function(a,b){j.push({name:a,value:b})};r("sEcho",a.iDraw);r("iColumns",c);r("sColumns",D(b,"sName").join(","));r("iDisplayStart",g);r("iDisplayLength",i);var ra={draw:a.iDraw,columns:[],order:[],start:g,length:i,search:{value:e.sSearch,regex:e.bRegex}};for(g=0;g<c;g++)n=b[g],
l=f[g],i="function"==typeof n.mData?"function":n.mData,ra.columns.push({data:i,name:n.sName,searchable:n.bSearchable,orderable:n.bSortable,search:{value:l.sSearch,regex:l.bRegex}}),r("mDataProp_"+g,i),d.bFilter&&(r("sSearch_"+g,l.sSearch),r("bRegex_"+g,l.bRegex),r("bSearchable_"+g,n.bSearchable)),d.bSort&&r("bSortable_"+g,n.bSortable);d.bFilter&&(r("sSearch",e.sSearch),r("bRegex",e.bRegex));d.bSort&&(h.each(k,function(a,b){ra.order.push({column:b.col,dir:b.dir});r("iSortCol_"+a,b.col);r("sSortDir_"+
a,b.dir)}),r("iSortingCols",k.length));b=m.ext.legacy.ajax;return null===b?a.sAjaxSource?j:ra:b?j:ra}function xb(a,b){var c=va(a,b),d=b.sEcho!==k?b.sEcho:b.draw,e=b.iTotalRecords!==k?b.iTotalRecords:b.recordsTotal,f=b.iTotalDisplayRecords!==k?b.iTotalDisplayRecords:b.recordsFiltered;if(d){if(1*d<a.iDraw)return;a.iDraw=1*d}pa(a);a._iRecordsTotal=parseInt(e,10);a._iRecordsDisplay=parseInt(f,10);d=0;for(e=c.length;d<e;d++)N(a,c[d]);a.aiDisplay=a.aiDisplayMaster.slice();a.bAjaxDataGet=!1;O(a);a._bInitComplete||
wa(a,b);a.bAjaxDataGet=!0;C(a,!1)}function va(a,b){var c=h.isPlainObject(a.ajax)&&a.ajax.dataSrc!==k?a.ajax.dataSrc:a.sAjaxDataProp;return"data"===c?b.aaData||b[c]:""!==c?R(c)(b):b}function rb(a){var b=a.oClasses,c=a.sTableId,d=a.oLanguage,e=a.oPreviousSearch,f=a.aanFeatures,g='<input type="search" class="'+b.sFilterInput+'"/>',j=d.sSearch,j=j.match(/_INPUT_/)?j.replace("_INPUT_",g):j+g,b=h("<div/>",{id:!f.f?c+"_filter":null,"class":b.sFilter}).append(h("<label/>").append(j)),f=function(){var b=!this.value?
"":this.value;b!=e.sSearch&&(ga(a,{sSearch:b,bRegex:e.bRegex,bSmart:e.bSmart,bCaseInsensitive:e.bCaseInsensitive}),a._iDisplayStart=0,O(a))},g=null!==a.searchDelay?a.searchDelay:"ssp"===y(a)?400:0,i=h("input",b).val(e.sSearch).attr("placeholder",d.sSearchPlaceholder).on("keyup.DT search.DT input.DT paste.DT cut.DT",g?Qa(f,g):f).on("keypress.DT",function(a){if(13==a.keyCode)return!1}).attr("aria-controls",c);h(a.nTable).on("search.dt.DT",function(b,c){if(a===c)try{i[0]!==H.activeElement&&i.val(e.sSearch)}catch(d){}});
return b[0]}function ga(a,b,c){var d=a.oPreviousSearch,e=a.aoPreSearchCols,f=function(a){d.sSearch=a.sSearch;d.bRegex=a.bRegex;d.bSmart=a.bSmart;d.bCaseInsensitive=a.bCaseInsensitive};Ia(a);if("ssp"!=y(a)){yb(a,b.sSearch,c,b.bEscapeRegex!==k?!b.bEscapeRegex:b.bRegex,b.bSmart,b.bCaseInsensitive);f(b);for(b=0;b<e.length;b++)zb(a,e[b].sSearch,b,e[b].bEscapeRegex!==k?!e[b].bEscapeRegex:e[b].bRegex,e[b].bSmart,e[b].bCaseInsensitive);Ab(a)}else f(b);a.bFiltered=!0;s(a,null,"search",[a])}function Ab(a){for(var b=
m.ext.search,c=a.aiDisplay,d,e,f=0,g=b.length;f<g;f++){for(var j=[],i=0,n=c.length;i<n;i++)e=c[i],d=a.aoData[e],b[f](a,d._aFilterData,e,d._aData,i)&&j.push(e);c.length=0;h.merge(c,j)}}function zb(a,b,c,d,e,f){if(""!==b){for(var g=[],j=a.aiDisplay,d=Ra(b,d,e,f),e=0;e<j.length;e++)b=a.aoData[j[e]]._aFilterData[c],d.test(b)&&g.push(j[e]);a.aiDisplay=g}}function yb(a,b,c,d,e,f){var d=Ra(b,d,e,f),f=a.oPreviousSearch.sSearch,g=a.aiDisplayMaster,j,e=[];0!==m.ext.search.length&&(c=!0);j=Bb(a);if(0>=b.length)a.aiDisplay=
g.slice();else{if(j||c||f.length>b.length||0!==b.indexOf(f)||a.bSorted)a.aiDisplay=g.slice();b=a.aiDisplay;for(c=0;c<b.length;c++)d.test(a.aoData[b[c]]._sFilterRow)&&e.push(b[c]);a.aiDisplay=e}}function Ra(a,b,c,d){a=b?a:Sa(a);c&&(a="^(?=.*?"+h.map(a.match(/"[^"]+"|[^ ]+/g)||[""],function(a){if('"'===a.charAt(0))var b=a.match(/^"(.*)"$/),a=b?b[1]:a;return a.replace('"',"")}).join(")(?=.*?")+").*$");return RegExp(a,d?"i":"")}function Bb(a){var b=a.aoColumns,c,d,e,f,g,j,i,h,l=m.ext.type.search;c=!1;
d=0;for(f=a.aoData.length;d<f;d++)if(h=a.aoData[d],!h._aFilterData){j=[];e=0;for(g=b.length;e<g;e++)c=b[e],c.bSearchable?(i=B(a,d,e,"filter"),l[c.sType]&&(i=l[c.sType](i)),null===i&&(i=""),"string"!==typeof i&&i.toString&&(i=i.toString())):i="",i.indexOf&&-1!==i.indexOf("&")&&(xa.innerHTML=i,i=$b?xa.textContent:xa.innerText),i.replace&&(i=i.replace(/[\r\n]/g,"")),j.push(i);h._aFilterData=j;h._sFilterRow=j.join(" ");c=!0}return c}function Cb(a){return{search:a.sSearch,smart:a.bSmart,regex:a.bRegex,
caseInsensitive:a.bCaseInsensitive}}function Db(a){return{sSearch:a.search,bSmart:a.smart,bRegex:a.regex,bCaseInsensitive:a.caseInsensitive}}function ub(a){var b=a.sTableId,c=a.aanFeatures.i,d=h("<div/>",{"class":a.oClasses.sInfo,id:!c?b+"_info":null});c||(a.aoDrawCallback.push({fn:Eb,sName:"information"}),d.attr("role","status").attr("aria-live","polite"),h(a.nTable).attr("aria-describedby",b+"_info"));return d[0]}function Eb(a){var b=a.aanFeatures.i;if(0!==b.length){var c=a.oLanguage,d=a._iDisplayStart+
1,e=a.fnDisplayEnd(),f=a.fnRecordsTotal(),g=a.fnRecordsDisplay(),j=g?c.sInfo:c.sInfoEmpty;g!==f&&(j+=" "+c.sInfoFiltered);j+=c.sInfoPostFix;j=Fb(a,j);c=c.fnInfoCallback;null!==c&&(j=c.call(a.oInstance,a,d,e,f,g,j));h(b).html(j)}}function Fb(a,b){var c=a.fnFormatNumber,d=a._iDisplayStart+1,e=a._iDisplayLength,f=a.fnRecordsDisplay(),g=-1===e;return b.replace(/_START_/g,c.call(a,d)).replace(/_END_/g,c.call(a,a.fnDisplayEnd())).replace(/_MAX_/g,c.call(a,a.fnRecordsTotal())).replace(/_TOTAL_/g,c.call(a,
f)).replace(/_PAGE_/g,c.call(a,g?1:Math.ceil(d/e))).replace(/_PAGES_/g,c.call(a,g?1:Math.ceil(f/e)))}function ha(a){var b,c,d=a.iInitDisplayStart,e=a.aoColumns,f;c=a.oFeatures;var g=a.bDeferLoading;if(a.bInitialised){pb(a);mb(a);fa(a,a.aoHeader);fa(a,a.aoFooter);C(a,!0);c.bAutoWidth&&Ha(a);b=0;for(c=e.length;b<c;b++)f=e[b],f.sWidth&&(f.nTh.style.width=v(f.sWidth));s(a,null,"preInit",[a]);T(a);e=y(a);if("ssp"!=e||g)"ajax"==e?ua(a,[],function(c){var f=va(a,c);for(b=0;b<f.length;b++)N(a,f[b]);a.iInitDisplayStart=
d;T(a);C(a,!1);wa(a,c)},a):(C(a,!1),wa(a))}else setTimeout(function(){ha(a)},200)}function wa(a,b){a._bInitComplete=!0;(b||a.oInit.aaData)&&Z(a);s(a,null,"plugin-init",[a,b]);s(a,"aoInitComplete","init",[a,b])}function Ta(a,b){var c=parseInt(b,10);a._iDisplayLength=c;Ua(a);s(a,null,"length",[a,c])}function qb(a){for(var b=a.oClasses,c=a.sTableId,d=a.aLengthMenu,e=h.isArray(d[0]),f=e?d[0]:d,d=e?d[1]:d,e=h("<select/>",{name:c+"_length","aria-controls":c,"class":b.sLengthSelect}),g=0,j=f.length;g<j;g++)e[0][g]=
new Option(d[g],f[g]);var i=h("<div><label/></div>").addClass(b.sLength);a.aanFeatures.l||(i[0].id=c+"_length");i.children().append(a.oLanguage.sLengthMenu.replace("_MENU_",e[0].outerHTML));h("select",i).val(a._iDisplayLength).on("change.DT",function(){Ta(a,h(this).val());O(a)});h(a.nTable).on("length.dt.DT",function(b,c,d){a===c&&h("select",i).val(d)});return i[0]}function vb(a){var b=a.sPaginationType,c=m.ext.pager[b],d="function"===typeof c,e=function(a){O(a)},b=h("<div/>").addClass(a.oClasses.sPaging+
b)[0],f=a.aanFeatures;d||c.fnInit(a,b,e);f.p||(b.id=a.sTableId+"_paginate",a.aoDrawCallback.push({fn:function(a){if(d){var b=a._iDisplayStart,i=a._iDisplayLength,h=a.fnRecordsDisplay(),l=-1===i,b=l?0:Math.ceil(b/i),i=l?1:Math.ceil(h/i),h=c(b,i),k,l=0;for(k=f.p.length;l<k;l++)Pa(a,"pageButton")(a,f.p[l],l,h,b,i)}else c.fnUpdate(a,e)},sName:"pagination"}));return b}function Va(a,b,c){var d=a._iDisplayStart,e=a._iDisplayLength,f=a.fnRecordsDisplay();0===f||-1===e?d=0:"number"===typeof b?(d=b*e,d>f&&
(d=0)):"first"==b?d=0:"previous"==b?(d=0<=e?d-e:0,0>d&&(d=0)):"next"==b?d+e<f&&(d+=e):"last"==b?d=Math.floor((f-1)/e)*e:K(a,0,"Unknown paging action: "+b,5);b=a._iDisplayStart!==d;a._iDisplayStart=d;b&&(s(a,null,"page",[a]),c&&O(a));return b}function sb(a){return h("<div/>",{id:!a.aanFeatures.r?a.sTableId+"_processing":null,"class":a.oClasses.sProcessing}).html(a.oLanguage.sProcessing).insertBefore(a.nTable)[0]}function C(a,b){a.oFeatures.bProcessing&&h(a.aanFeatures.r).css("display",b?"block":"none");
s(a,null,"processing",[a,b])}function tb(a){var b=h(a.nTable);b.attr("role","grid");var c=a.oScroll;if(""===c.sX&&""===c.sY)return a.nTable;var d=c.sX,e=c.sY,f=a.oClasses,g=b.children("caption"),j=g.length?g[0]._captionSide:null,i=h(b[0].cloneNode(!1)),n=h(b[0].cloneNode(!1)),l=b.children("tfoot");l.length||(l=null);i=h("<div/>",{"class":f.sScrollWrapper}).append(h("<div/>",{"class":f.sScrollHead}).css({overflow:"hidden",position:"relative",border:0,width:d?!d?null:v(d):"100%"}).append(h("<div/>",
{"class":f.sScrollHeadInner}).css({"box-sizing":"content-box",width:c.sXInner||"100%"}).append(i.removeAttr("id").css("margin-left",0).append("top"===j?g:null).append(b.children("thead"))))).append(h("<div/>",{"class":f.sScrollBody}).css({position:"relative",overflow:"auto",width:!d?null:v(d)}).append(b));l&&i.append(h("<div/>",{"class":f.sScrollFoot}).css({overflow:"hidden",border:0,width:d?!d?null:v(d):"100%"}).append(h("<div/>",{"class":f.sScrollFootInner}).append(n.removeAttr("id").css("margin-left",
0).append("bottom"===j?g:null).append(b.children("tfoot")))));var b=i.children(),k=b[0],f=b[1],r=l?b[2]:null;if(d)h(f).on("scroll.DT",function(){var a=this.scrollLeft;k.scrollLeft=a;l&&(r.scrollLeft=a)});h(f).css(e&&c.bCollapse?"max-height":"height",e);a.nScrollHead=k;a.nScrollBody=f;a.nScrollFoot=r;a.aoDrawCallback.push({fn:ma,sName:"scrolling"});return i[0]}function ma(a){var b=a.oScroll,c=b.sX,d=b.sXInner,e=b.sY,b=b.iBarWidth,f=h(a.nScrollHead),g=f[0].style,j=f.children("div"),i=j[0].style,n=j.children("table"),
j=a.nScrollBody,l=h(j),q=j.style,r=h(a.nScrollFoot).children("div"),m=r.children("table"),p=h(a.nTHead),o=h(a.nTable),u=o[0],s=u.style,t=a.nTFoot?h(a.nTFoot):null,x=a.oBrowser,U=x.bScrollOversize,ac=D(a.aoColumns,"nTh"),P,L,Q,w,Wa=[],y=[],z=[],A=[],B,C=function(a){a=a.style;a.paddingTop="0";a.paddingBottom="0";a.borderTopWidth="0";a.borderBottomWidth="0";a.height=0};L=j.scrollHeight>j.clientHeight;if(a.scrollBarVis!==L&&a.scrollBarVis!==k)a.scrollBarVis=L,Z(a);else{a.scrollBarVis=L;o.children("thead, tfoot").remove();
t&&(Q=t.clone().prependTo(o),P=t.find("tr"),Q=Q.find("tr"));w=p.clone().prependTo(o);p=p.find("tr");L=w.find("tr");w.find("th, td").removeAttr("tabindex");c||(q.width="100%",f[0].style.width="100%");h.each(ta(a,w),function(b,c){B=$(a,b);c.style.width=a.aoColumns[B].sWidth});t&&I(function(a){a.style.width=""},Q);f=o.outerWidth();if(""===c){s.width="100%";if(U&&(o.find("tbody").height()>j.offsetHeight||"scroll"==l.css("overflow-y")))s.width=v(o.outerWidth()-b);f=o.outerWidth()}else""!==d&&(s.width=
v(d),f=o.outerWidth());I(C,L);I(function(a){z.push(a.innerHTML);Wa.push(v(h(a).css("width")))},L);I(function(a,b){if(h.inArray(a,ac)!==-1)a.style.width=Wa[b]},p);h(L).height(0);t&&(I(C,Q),I(function(a){A.push(a.innerHTML);y.push(v(h(a).css("width")))},Q),I(function(a,b){a.style.width=y[b]},P),h(Q).height(0));I(function(a,b){a.innerHTML='<div class="dataTables_sizing" style="height:0;overflow:hidden;">'+z[b]+"</div>";a.style.width=Wa[b]},L);t&&I(function(a,b){a.innerHTML='<div class="dataTables_sizing" style="height:0;overflow:hidden;">'+
A[b]+"</div>";a.style.width=y[b]},Q);if(o.outerWidth()<f){P=j.scrollHeight>j.offsetHeight||"scroll"==l.css("overflow-y")?f+b:f;if(U&&(j.scrollHeight>j.offsetHeight||"scroll"==l.css("overflow-y")))s.width=v(P-b);(""===c||""!==d)&&K(a,1,"Possible column misalignment",6)}else P="100%";q.width=v(P);g.width=v(P);t&&(a.nScrollFoot.style.width=v(P));!e&&U&&(q.height=v(u.offsetHeight+b));c=o.outerWidth();n[0].style.width=v(c);i.width=v(c);d=o.height()>j.clientHeight||"scroll"==l.css("overflow-y");e="padding"+
(x.bScrollbarLeft?"Left":"Right");i[e]=d?b+"px":"0px";t&&(m[0].style.width=v(c),r[0].style.width=v(c),r[0].style[e]=d?b+"px":"0px");o.children("colgroup").insertBefore(o.children("thead"));l.scroll();if((a.bSorted||a.bFiltered)&&!a._drawHold)j.scrollTop=0}}function I(a,b,c){for(var d=0,e=0,f=b.length,g,j;e<f;){g=b[e].firstChild;for(j=c?c[e].firstChild:null;g;)1===g.nodeType&&(c?a(g,j,d):a(g,d),d++),g=g.nextSibling,j=c?j.nextSibling:null;e++}}function Ha(a){var b=a.nTable,c=a.aoColumns,d=a.oScroll,
e=d.sY,f=d.sX,g=d.sXInner,j=c.length,i=na(a,"bVisible"),n=h("th",a.nTHead),l=b.getAttribute("width"),k=b.parentNode,r=!1,m,p,o=a.oBrowser,d=o.bScrollOversize;(m=b.style.width)&&-1!==m.indexOf("%")&&(l=m);for(m=0;m<i.length;m++)p=c[i[m]],null!==p.sWidth&&(p.sWidth=Gb(p.sWidthOrig,k),r=!0);if(d||!r&&!f&&!e&&j==ba(a)&&j==n.length)for(m=0;m<j;m++)i=$(a,m),null!==i&&(c[i].sWidth=v(n.eq(m).width()));else{j=h(b).clone().css("visibility","hidden").removeAttr("id");j.find("tbody tr").remove();var u=h("<tr/>").appendTo(j.find("tbody"));
j.find("thead, tfoot").remove();j.append(h(a.nTHead).clone()).append(h(a.nTFoot).clone());j.find("tfoot th, tfoot td").css("width","");n=ta(a,j.find("thead")[0]);for(m=0;m<i.length;m++)p=c[i[m]],n[m].style.width=null!==p.sWidthOrig&&""!==p.sWidthOrig?v(p.sWidthOrig):"",p.sWidthOrig&&f&&h(n[m]).append(h("<div/>").css({width:p.sWidthOrig,margin:0,padding:0,border:0,height:1}));if(a.aoData.length)for(m=0;m<i.length;m++)r=i[m],p=c[r],h(Hb(a,r)).clone(!1).append(p.sContentPadding).appendTo(u);h("[name]",
j).removeAttr("name");p=h("<div/>").css(f||e?{position:"absolute",top:0,left:0,height:1,right:0,overflow:"hidden"}:{}).append(j).appendTo(k);f&&g?j.width(g):f?(j.css("width","auto"),j.removeAttr("width"),j.width()<k.clientWidth&&l&&j.width(k.clientWidth)):e?j.width(k.clientWidth):l&&j.width(l);for(m=e=0;m<i.length;m++)k=h(n[m]),g=k.outerWidth()-k.width(),k=o.bBounding?Math.ceil(n[m].getBoundingClientRect().width):k.outerWidth(),e+=k,c[i[m]].sWidth=v(k-g);b.style.width=v(e);p.remove()}l&&(b.style.width=
v(l));if((l||f)&&!a._reszEvt)b=function(){h(E).on("resize.DT-"+a.sInstance,Qa(function(){Z(a)}))},d?setTimeout(b,1E3):b(),a._reszEvt=!0}function Gb(a,b){if(!a)return 0;var c=h("<div/>").css("width",v(a)).appendTo(b||H.body),d=c[0].offsetWidth;c.remove();return d}function Hb(a,b){var c=Ib(a,b);if(0>c)return null;var d=a.aoData[c];return!d.nTr?h("<td/>").html(B(a,c,b,"display"))[0]:d.anCells[b]}function Ib(a,b){for(var c,d=-1,e=-1,f=0,g=a.aoData.length;f<g;f++)c=B(a,f,b,"display")+"",c=c.replace(bc,
""),c=c.replace(/&nbsp;/g," "),c.length>d&&(d=c.length,e=f);return e}function v(a){return null===a?"0px":"number"==typeof a?0>a?"0px":a+"px":a.match(/\d$/)?a+"px":a}function W(a){var b,c,d=[],e=a.aoColumns,f,g,j,i;b=a.aaSortingFixed;c=h.isPlainObject(b);var n=[];f=function(a){a.length&&!h.isArray(a[0])?n.push(a):h.merge(n,a)};h.isArray(b)&&f(b);c&&b.pre&&f(b.pre);f(a.aaSorting);c&&b.post&&f(b.post);for(a=0;a<n.length;a++){i=n[a][0];f=e[i].aDataSort;b=0;for(c=f.length;b<c;b++)g=f[b],j=e[g].sType||
"string",n[a]._idx===k&&(n[a]._idx=h.inArray(n[a][1],e[g].asSorting)),d.push({src:i,col:g,dir:n[a][1],index:n[a]._idx,type:j,formatter:m.ext.type.order[j+"-pre"]})}return d}function ob(a){var b,c,d=[],e=m.ext.type.order,f=a.aoData,g=0,j,i=a.aiDisplayMaster,h;Ia(a);h=W(a);b=0;for(c=h.length;b<c;b++)j=h[b],j.formatter&&g++,Jb(a,j.col);if("ssp"!=y(a)&&0!==h.length){b=0;for(c=i.length;b<c;b++)d[i[b]]=b;g===h.length?i.sort(function(a,b){var c,e,g,j,i=h.length,k=f[a]._aSortData,m=f[b]._aSortData;for(g=
0;g<i;g++)if(j=h[g],c=k[j.col],e=m[j.col],c=c<e?-1:c>e?1:0,0!==c)return"asc"===j.dir?c:-c;c=d[a];e=d[b];return c<e?-1:c>e?1:0}):i.sort(function(a,b){var c,g,j,i,k=h.length,m=f[a]._aSortData,p=f[b]._aSortData;for(j=0;j<k;j++)if(i=h[j],c=m[i.col],g=p[i.col],i=e[i.type+"-"+i.dir]||e["string-"+i.dir],c=i(c,g),0!==c)return c;c=d[a];g=d[b];return c<g?-1:c>g?1:0})}a.bSorted=!0}function Kb(a){for(var b,c,d=a.aoColumns,e=W(a),a=a.oLanguage.oAria,f=0,g=d.length;f<g;f++){c=d[f];var j=c.asSorting;b=c.sTitle.replace(/<.*?>/g,
"");var i=c.nTh;i.removeAttribute("aria-sort");c.bSortable&&(0<e.length&&e[0].col==f?(i.setAttribute("aria-sort","asc"==e[0].dir?"ascending":"descending"),c=j[e[0].index+1]||j[0]):c=j[0],b+="asc"===c?a.sSortAscending:a.sSortDescending);i.setAttribute("aria-label",b)}}function Xa(a,b,c,d){var e=a.aaSorting,f=a.aoColumns[b].asSorting,g=function(a,b){var c=a._idx;c===k&&(c=h.inArray(a[1],f));return c+1<f.length?c+1:b?null:0};"number"===typeof e[0]&&(e=a.aaSorting=[e]);c&&a.oFeatures.bSortMulti?(c=h.inArray(b,
D(e,"0")),-1!==c?(b=g(e[c],!0),null===b&&1===e.length&&(b=0),null===b?e.splice(c,1):(e[c][1]=f[b],e[c]._idx=b)):(e.push([b,f[0],0]),e[e.length-1]._idx=0)):e.length&&e[0][0]==b?(b=g(e[0]),e.length=1,e[0][1]=f[b],e[0]._idx=b):(e.length=0,e.push([b,f[0]]),e[0]._idx=0);T(a);"function"==typeof d&&d(a)}function Oa(a,b,c,d){var e=a.aoColumns[c];Ya(b,{},function(b){!1!==e.bSortable&&(a.oFeatures.bProcessing?(C(a,!0),setTimeout(function(){Xa(a,c,b.shiftKey,d);"ssp"!==y(a)&&C(a,!1)},0)):Xa(a,c,b.shiftKey,d))})}
function ya(a){var b=a.aLastSort,c=a.oClasses.sSortColumn,d=W(a),e=a.oFeatures,f,g;if(e.bSort&&e.bSortClasses){e=0;for(f=b.length;e<f;e++)g=b[e].src,h(D(a.aoData,"anCells",g)).removeClass(c+(2>e?e+1:3));e=0;for(f=d.length;e<f;e++)g=d[e].src,h(D(a.aoData,"anCells",g)).addClass(c+(2>e?e+1:3))}a.aLastSort=d}function Jb(a,b){var c=a.aoColumns[b],d=m.ext.order[c.sSortDataType],e;d&&(e=d.call(a.oInstance,a,b,aa(a,b)));for(var f,g=m.ext.type.order[c.sType+"-pre"],j=0,i=a.aoData.length;j<i;j++)if(c=a.aoData[j],
c._aSortData||(c._aSortData=[]),!c._aSortData[b]||d)f=d?e[j]:B(a,j,b,"sort"),c._aSortData[b]=g?g(f):f}function za(a){if(a.oFeatures.bStateSave&&!a.bDestroying){var b={time:+new Date,start:a._iDisplayStart,length:a._iDisplayLength,order:h.extend(!0,[],a.aaSorting),search:Cb(a.oPreviousSearch),columns:h.map(a.aoColumns,function(b,d){return{visible:b.bVisible,search:Cb(a.aoPreSearchCols[d])}})};s(a,"aoStateSaveParams","stateSaveParams",[a,b]);a.oSavedState=b;a.fnStateSaveCallback.call(a.oInstance,a,
b)}}function Lb(a,b,c){var d,e,f=a.aoColumns,b=function(b){if(b&&b.time){var i=s(a,"aoStateLoadParams","stateLoadParams",[a,g]);if(-1===h.inArray(!1,i)&&(i=a.iStateDuration,!(0<i&&b.time<+new Date-1E3*i)&&!(b.columns&&f.length!==b.columns.length))){a.oLoadedState=h.extend(!0,{},g);b.start!==k&&(a._iDisplayStart=b.start,a.iInitDisplayStart=b.start);b.length!==k&&(a._iDisplayLength=b.length);b.order!==k&&(a.aaSorting=[],h.each(b.order,function(b,c){a.aaSorting.push(c[0]>=f.length?[0,c[1]]:c)}));b.search!==
k&&h.extend(a.oPreviousSearch,Db(b.search));if(b.columns){d=0;for(e=b.columns.length;d<e;d++)i=b.columns[d],i.visible!==k&&(f[d].bVisible=i.visible),i.search!==k&&h.extend(a.aoPreSearchCols[d],Db(i.search))}s(a,"aoStateLoaded","stateLoaded",[a,g])}}c()};if(a.oFeatures.bStateSave){var g=a.fnStateLoadCallback.call(a.oInstance,a,b);g!==k&&b(g)}else c()}function Aa(a){var b=m.settings,a=h.inArray(a,D(b,"nTable"));return-1!==a?b[a]:null}function K(a,b,c,d){c="DataTables warning: "+(a?"table id="+a.sTableId+
" - ":"")+c;d&&(c+=". For more information about this error, please see http://datatables.net/tn/"+d);if(b)E.console&&console.log&&console.log(c);else if(b=m.ext,b=b.sErrMode||b.errMode,a&&s(a,null,"error",[a,d,c]),"alert"==b)alert(c);else{if("throw"==b)throw Error(c);"function"==typeof b&&b(a,d,c)}}function F(a,b,c,d){h.isArray(c)?h.each(c,function(c,d){h.isArray(d)?F(a,b,d[0],d[1]):F(a,b,d)}):(d===k&&(d=c),b[c]!==k&&(a[d]=b[c]))}function Mb(a,b,c){var d,e;for(e in b)b.hasOwnProperty(e)&&(d=b[e],
h.isPlainObject(d)?(h.isPlainObject(a[e])||(a[e]={}),h.extend(!0,a[e],d)):a[e]=c&&"data"!==e&&"aaData"!==e&&h.isArray(d)?d.slice():d);return a}function Ya(a,b,c){h(a).on("click.DT",b,function(b){a.blur();c(b)}).on("keypress.DT",b,function(a){13===a.which&&(a.preventDefault(),c(a))}).on("selectstart.DT",function(){return!1})}function z(a,b,c,d){c&&a[b].push({fn:c,sName:d})}function s(a,b,c,d){var e=[];b&&(e=h.map(a[b].slice().reverse(),function(b){return b.fn.apply(a.oInstance,d)}));null!==c&&(b=h.Event(c+
".dt"),h(a.nTable).trigger(b,d),e.push(b.result));return e}function Ua(a){var b=a._iDisplayStart,c=a.fnDisplayEnd(),d=a._iDisplayLength;b>=c&&(b=c-d);b-=b%d;if(-1===d||0>b)b=0;a._iDisplayStart=b}function Pa(a,b){var c=a.renderer,d=m.ext.renderer[b];return h.isPlainObject(c)&&c[b]?d[c[b]]||d._:"string"===typeof c?d[c]||d._:d._}function y(a){return a.oFeatures.bServerSide?"ssp":a.ajax||a.sAjaxSource?"ajax":"dom"}function ia(a,b){var c=[],c=Nb.numbers_length,d=Math.floor(c/2);b<=c?c=X(0,b):a<=d?(c=X(0,
c-2),c.push("ellipsis"),c.push(b-1)):(a>=b-1-d?c=X(b-(c-2),b):(c=X(a-d+2,a+d-1),c.push("ellipsis"),c.push(b-1)),c.splice(0,0,"ellipsis"),c.splice(0,0,0));c.DT_el="span";return c}function fb(a){h.each({num:function(b){return Ba(b,a)},"num-fmt":function(b){return Ba(b,a,Za)},"html-num":function(b){return Ba(b,a,Ca)},"html-num-fmt":function(b){return Ba(b,a,Ca,Za)}},function(b,c){x.type.order[b+a+"-pre"]=c;b.match(/^html\-/)&&(x.type.search[b+a]=x.type.search.html)})}function Ob(a){return function(){var b=
[Aa(this[m.ext.iApiIndex])].concat(Array.prototype.slice.call(arguments));return m.ext.internal[a].apply(this,b)}}var m=function(a){this.$=function(a,b){return this.api(!0).$(a,b)};this._=function(a,b){return this.api(!0).rows(a,b).data()};this.api=function(a){return a?new u(Aa(this[x.iApiIndex])):new u(this)};this.fnAddData=function(a,b){var c=this.api(!0),d=h.isArray(a)&&(h.isArray(a[0])||h.isPlainObject(a[0]))?c.rows.add(a):c.row.add(a);(b===k||b)&&c.draw();return d.flatten().toArray()};this.fnAdjustColumnSizing=
function(a){var b=this.api(!0).columns.adjust(),c=b.settings()[0],d=c.oScroll;a===k||a?b.draw(!1):(""!==d.sX||""!==d.sY)&&ma(c)};this.fnClearTable=function(a){var b=this.api(!0).clear();(a===k||a)&&b.draw()};this.fnClose=function(a){this.api(!0).row(a).child.hide()};this.fnDeleteRow=function(a,b,c){var d=this.api(!0),a=d.rows(a),e=a.settings()[0],h=e.aoData[a[0][0]];a.remove();b&&b.call(this,e,h);(c===k||c)&&d.draw();return h};this.fnDestroy=function(a){this.api(!0).destroy(a)};this.fnDraw=function(a){this.api(!0).draw(a)};
this.fnFilter=function(a,b,c,d,e,h){e=this.api(!0);null===b||b===k?e.search(a,c,d,h):e.column(b).search(a,c,d,h);e.draw()};this.fnGetData=function(a,b){var c=this.api(!0);if(a!==k){var d=a.nodeName?a.nodeName.toLowerCase():"";return b!==k||"td"==d||"th"==d?c.cell(a,b).data():c.row(a).data()||null}return c.data().toArray()};this.fnGetNodes=function(a){var b=this.api(!0);return a!==k?b.row(a).node():b.rows().nodes().flatten().toArray()};this.fnGetPosition=function(a){var b=this.api(!0),c=a.nodeName.toUpperCase();
return"TR"==c?b.row(a).index():"TD"==c||"TH"==c?(a=b.cell(a).index(),[a.row,a.columnVisible,a.column]):null};this.fnIsOpen=function(a){return this.api(!0).row(a).child.isShown()};this.fnOpen=function(a,b,c){return this.api(!0).row(a).child(b,c).show().child()[0]};this.fnPageChange=function(a,b){var c=this.api(!0).page(a);(b===k||b)&&c.draw(!1)};this.fnSetColumnVis=function(a,b,c){a=this.api(!0).column(a).visible(b);(c===k||c)&&a.columns.adjust().draw()};this.fnSettings=function(){return Aa(this[x.iApiIndex])};
this.fnSort=function(a){this.api(!0).order(a).draw()};this.fnSortListener=function(a,b,c){this.api(!0).order.listener(a,b,c)};this.fnUpdate=function(a,b,c,d,e){var h=this.api(!0);c===k||null===c?h.row(b).data(a):h.cell(b,c).data(a);(e===k||e)&&h.columns.adjust();(d===k||d)&&h.draw();return 0};this.fnVersionCheck=x.fnVersionCheck;var b=this,c=a===k,d=this.length;c&&(a={});this.oApi=this.internal=x.internal;for(var e in m.ext.internal)e&&(this[e]=Ob(e));this.each(function(){var e={},g=1<d?Mb(e,a,!0):
a,j=0,i,e=this.getAttribute("id"),n=!1,l=m.defaults,q=h(this);if("table"!=this.nodeName.toLowerCase())K(null,0,"Non-table node initialisation ("+this.nodeName+")",2);else{gb(l);hb(l.column);J(l,l,!0);J(l.column,l.column,!0);J(l,h.extend(g,q.data()));var r=m.settings,j=0;for(i=r.length;j<i;j++){var p=r[j];if(p.nTable==this||p.nTHead.parentNode==this||p.nTFoot&&p.nTFoot.parentNode==this){var u=g.bRetrieve!==k?g.bRetrieve:l.bRetrieve;if(c||u)return p.oInstance;if(g.bDestroy!==k?g.bDestroy:l.bDestroy){p.oInstance.fnDestroy();
break}else{K(p,0,"Cannot reinitialise DataTable",3);return}}if(p.sTableId==this.id){r.splice(j,1);break}}if(null===e||""===e)this.id=e="DataTables_Table_"+m.ext._unique++;var o=h.extend(!0,{},m.models.oSettings,{sDestroyWidth:q[0].style.width,sInstance:e,sTableId:e});o.nTable=this;o.oApi=b.internal;o.oInit=g;r.push(o);o.oInstance=1===b.length?b:q.dataTable();gb(g);g.oLanguage&&Fa(g.oLanguage);g.aLengthMenu&&!g.iDisplayLength&&(g.iDisplayLength=h.isArray(g.aLengthMenu[0])?g.aLengthMenu[0][0]:g.aLengthMenu[0]);
g=Mb(h.extend(!0,{},l),g);F(o.oFeatures,g,"bPaginate bLengthChange bFilter bSort bSortMulti bInfo bProcessing bAutoWidth bSortClasses bServerSide bDeferRender".split(" "));F(o,g,["asStripeClasses","ajax","fnServerData","fnFormatNumber","sServerMethod","aaSorting","aaSortingFixed","aLengthMenu","sPaginationType","sAjaxSource","sAjaxDataProp","iStateDuration","sDom","bSortCellsTop","iTabIndex","fnStateLoadCallback","fnStateSaveCallback","renderer","searchDelay","rowId",["iCookieDuration","iStateDuration"],
["oSearch","oPreviousSearch"],["aoSearchCols","aoPreSearchCols"],["iDisplayLength","_iDisplayLength"],["bJQueryUI","bJUI"]]);F(o.oScroll,g,[["sScrollX","sX"],["sScrollXInner","sXInner"],["sScrollY","sY"],["bScrollCollapse","bCollapse"]]);F(o.oLanguage,g,"fnInfoCallback");z(o,"aoDrawCallback",g.fnDrawCallback,"user");z(o,"aoServerParams",g.fnServerParams,"user");z(o,"aoStateSaveParams",g.fnStateSaveParams,"user");z(o,"aoStateLoadParams",g.fnStateLoadParams,"user");z(o,"aoStateLoaded",g.fnStateLoaded,
"user");z(o,"aoRowCallback",g.fnRowCallback,"user");z(o,"aoRowCreatedCallback",g.fnCreatedRow,"user");z(o,"aoHeaderCallback",g.fnHeaderCallback,"user");z(o,"aoFooterCallback",g.fnFooterCallback,"user");z(o,"aoInitComplete",g.fnInitComplete,"user");z(o,"aoPreDrawCallback",g.fnPreDrawCallback,"user");o.rowIdFn=R(g.rowId);ib(o);var t=o.oClasses;g.bJQueryUI?(h.extend(t,m.ext.oJUIClasses,g.oClasses),g.sDom===l.sDom&&"lfrtip"===l.sDom&&(o.sDom='<"H"lfr>t<"F"ip>'),o.renderer)?h.isPlainObject(o.renderer)&&
!o.renderer.header&&(o.renderer.header="jqueryui"):o.renderer="jqueryui":h.extend(t,m.ext.classes,g.oClasses);q.addClass(t.sTable);o.iInitDisplayStart===k&&(o.iInitDisplayStart=g.iDisplayStart,o._iDisplayStart=g.iDisplayStart);null!==g.iDeferLoading&&(o.bDeferLoading=!0,e=h.isArray(g.iDeferLoading),o._iRecordsDisplay=e?g.iDeferLoading[0]:g.iDeferLoading,o._iRecordsTotal=e?g.iDeferLoading[1]:g.iDeferLoading);var v=o.oLanguage;h.extend(!0,v,g.oLanguage);v.sUrl&&(h.ajax({dataType:"json",url:v.sUrl,success:function(a){Fa(a);
J(l.oLanguage,a);h.extend(true,v,a);ha(o)},error:function(){ha(o)}}),n=!0);null===g.asStripeClasses&&(o.asStripeClasses=[t.sStripeOdd,t.sStripeEven]);var e=o.asStripeClasses,x=q.children("tbody").find("tr").eq(0);-1!==h.inArray(!0,h.map(e,function(a){return x.hasClass(a)}))&&(h("tbody tr",this).removeClass(e.join(" ")),o.asDestroyStripes=e.slice());e=[];r=this.getElementsByTagName("thead");0!==r.length&&(ea(o.aoHeader,r[0]),e=ta(o));if(null===g.aoColumns){r=[];j=0;for(i=e.length;j<i;j++)r.push(null)}else r=
g.aoColumns;j=0;for(i=r.length;j<i;j++)Ga(o,e?e[j]:null);kb(o,g.aoColumnDefs,r,function(a,b){la(o,a,b)});if(x.length){var w=function(a,b){return a.getAttribute("data-"+b)!==null?b:null};h(x[0]).children("th, td").each(function(a,b){var c=o.aoColumns[a];if(c.mData===a){var d=w(b,"sort")||w(b,"order"),e=w(b,"filter")||w(b,"search");if(d!==null||e!==null){c.mData={_:a+".display",sort:d!==null?a+".@data-"+d:k,type:d!==null?a+".@data-"+d:k,filter:e!==null?a+".@data-"+e:k};la(o,a)}}})}var U=o.oFeatures,
e=function(){if(g.aaSorting===k){var a=o.aaSorting;j=0;for(i=a.length;j<i;j++)a[j][1]=o.aoColumns[j].asSorting[0]}ya(o);U.bSort&&z(o,"aoDrawCallback",function(){if(o.bSorted){var a=W(o),b={};h.each(a,function(a,c){b[c.src]=c.dir});s(o,null,"order",[o,a,b]);Kb(o)}});z(o,"aoDrawCallback",function(){(o.bSorted||y(o)==="ssp"||U.bDeferRender)&&ya(o)},"sc");var a=q.children("caption").each(function(){this._captionSide=h(this).css("caption-side")}),b=q.children("thead");b.length===0&&(b=h("<thead/>").appendTo(q));
o.nTHead=b[0];b=q.children("tbody");b.length===0&&(b=h("<tbody/>").appendTo(q));o.nTBody=b[0];b=q.children("tfoot");if(b.length===0&&a.length>0&&(o.oScroll.sX!==""||o.oScroll.sY!==""))b=h("<tfoot/>").appendTo(q);if(b.length===0||b.children().length===0)q.addClass(t.sNoFooter);else if(b.length>0){o.nTFoot=b[0];ea(o.aoFooter,o.nTFoot)}if(g.aaData)for(j=0;j<g.aaData.length;j++)N(o,g.aaData[j]);else(o.bDeferLoading||y(o)=="dom")&&oa(o,h(o.nTBody).children("tr"));o.aiDisplay=o.aiDisplayMaster.slice();
o.bInitialised=true;n===false&&ha(o)};g.bStateSave?(U.bStateSave=!0,z(o,"aoDrawCallback",za,"state_save"),Lb(o,g,e)):e()}});b=null;return this},x,u,p,t,$a={},Pb=/[\r\n]/g,Ca=/<.*?>/g,cc=/^\d{2,4}[\.\/\-]\d{1,2}[\.\/\-]\d{1,2}([T ]{1}\d{1,2}[:\.]\d{2}([\.:]\d{2})?)?$/,dc=RegExp("(\\/|\\.|\\*|\\+|\\?|\\||\\(|\\)|\\[|\\]|\\{|\\}|\\\\|\\$|\\^|\\-)","g"),Za=/[',$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfk]/gi,M=function(a){return!a||!0===a||"-"===a?!0:!1},Qb=function(a){var b=parseInt(a,10);return!isNaN(b)&&
isFinite(a)?b:null},Rb=function(a,b){$a[b]||($a[b]=RegExp(Sa(b),"g"));return"string"===typeof a&&"."!==b?a.replace(/\./g,"").replace($a[b],"."):a},ab=function(a,b,c){var d="string"===typeof a;if(M(a))return!0;b&&d&&(a=Rb(a,b));c&&d&&(a=a.replace(Za,""));return!isNaN(parseFloat(a))&&isFinite(a)},Sb=function(a,b,c){return M(a)?!0:!(M(a)||"string"===typeof a)?null:ab(a.replace(Ca,""),b,c)?!0:null},D=function(a,b,c){var d=[],e=0,f=a.length;if(c!==k)for(;e<f;e++)a[e]&&a[e][b]&&d.push(a[e][b][c]);else for(;e<
f;e++)a[e]&&d.push(a[e][b]);return d},ja=function(a,b,c,d){var e=[],f=0,g=b.length;if(d!==k)for(;f<g;f++)a[b[f]][c]&&e.push(a[b[f]][c][d]);else for(;f<g;f++)e.push(a[b[f]][c]);return e},X=function(a,b){var c=[],d;b===k?(b=0,d=a):(d=b,b=a);for(var e=b;e<d;e++)c.push(e);return c},Tb=function(a){for(var b=[],c=0,d=a.length;c<d;c++)a[c]&&b.push(a[c]);return b},sa=function(a){var b=[],c,d,e=a.length,f,g=0;d=0;a:for(;d<e;d++){c=a[d];for(f=0;f<g;f++)if(b[f]===c)continue a;b.push(c);g++}return b};m.util=
{throttle:function(a,b){var c=b!==k?b:200,d,e;return function(){var b=this,g=+new Date,h=arguments;d&&g<d+c?(clearTimeout(e),e=setTimeout(function(){d=k;a.apply(b,h)},c)):(d=g,a.apply(b,h))}},escapeRegex:function(a){return a.replace(dc,"\\$1")}};var A=function(a,b,c){a[b]!==k&&(a[c]=a[b])},ca=/\[.*?\]$/,V=/\(\)$/,Sa=m.util.escapeRegex,xa=h("<div>")[0],$b=xa.textContent!==k,bc=/<.*?>/g,Qa=m.util.throttle,Ub=[],w=Array.prototype,ec=function(a){var b,c,d=m.settings,e=h.map(d,function(a){return a.nTable});
if(a){if(a.nTable&&a.oApi)return[a];if(a.nodeName&&"table"===a.nodeName.toLowerCase())return b=h.inArray(a,e),-1!==b?[d[b]]:null;if(a&&"function"===typeof a.settings)return a.settings().toArray();"string"===typeof a?c=h(a):a instanceof h&&(c=a)}else return[];if(c)return c.map(function(){b=h.inArray(this,e);return-1!==b?d[b]:null}).toArray()};u=function(a,b){if(!(this instanceof u))return new u(a,b);var c=[],d=function(a){(a=ec(a))&&(c=c.concat(a))};if(h.isArray(a))for(var e=0,f=a.length;e<f;e++)d(a[e]);
else d(a);this.context=sa(c);b&&h.merge(this,b);this.selector={rows:null,cols:null,opts:null};u.extend(this,this,Ub)};m.Api=u;h.extend(u.prototype,{any:function(){return 0!==this.count()},concat:w.concat,context:[],count:function(){return this.flatten().length},each:function(a){for(var b=0,c=this.length;b<c;b++)a.call(this,this[b],b,this);return this},eq:function(a){var b=this.context;return b.length>a?new u(b[a],this[a]):null},filter:function(a){var b=[];if(w.filter)b=w.filter.call(this,a,this);
else for(var c=0,d=this.length;c<d;c++)a.call(this,this[c],c,this)&&b.push(this[c]);return new u(this.context,b)},flatten:function(){var a=[];return new u(this.context,a.concat.apply(a,this.toArray()))},join:w.join,indexOf:w.indexOf||function(a,b){for(var c=b||0,d=this.length;c<d;c++)if(this[c]===a)return c;return-1},iterator:function(a,b,c,d){var e=[],f,g,h,i,n,l=this.context,m,p,t=this.selector;"string"===typeof a&&(d=c,c=b,b=a,a=!1);g=0;for(h=l.length;g<h;g++){var s=new u(l[g]);if("table"===b)f=
c.call(s,l[g],g),f!==k&&e.push(f);else if("columns"===b||"rows"===b)f=c.call(s,l[g],this[g],g),f!==k&&e.push(f);else if("column"===b||"column-rows"===b||"row"===b||"cell"===b){p=this[g];"column-rows"===b&&(m=Da(l[g],t.opts));i=0;for(n=p.length;i<n;i++)f=p[i],f="cell"===b?c.call(s,l[g],f.row,f.column,g,i):c.call(s,l[g],f,g,i,m),f!==k&&e.push(f)}}return e.length||d?(a=new u(l,a?e.concat.apply([],e):e),b=a.selector,b.rows=t.rows,b.cols=t.cols,b.opts=t.opts,a):this},lastIndexOf:w.lastIndexOf||function(a,
b){return this.indexOf.apply(this.toArray.reverse(),arguments)},length:0,map:function(a){var b=[];if(w.map)b=w.map.call(this,a,this);else for(var c=0,d=this.length;c<d;c++)b.push(a.call(this,this[c],c));return new u(this.context,b)},pluck:function(a){return this.map(function(b){return b[a]})},pop:w.pop,push:w.push,reduce:w.reduce||function(a,b){return jb(this,a,b,0,this.length,1)},reduceRight:w.reduceRight||function(a,b){return jb(this,a,b,this.length-1,-1,-1)},reverse:w.reverse,selector:null,shift:w.shift,
sort:w.sort,splice:w.splice,toArray:function(){return w.slice.call(this)},to$:function(){return h(this)},toJQuery:function(){return h(this)},unique:function(){return new u(this.context,sa(this))},unshift:w.unshift});u.extend=function(a,b,c){if(c.length&&b&&(b instanceof u||b.__dt_wrapper)){var d,e,f,g=function(a,b,c){return function(){var d=b.apply(a,arguments);u.extend(d,d,c.methodExt);return d}};d=0;for(e=c.length;d<e;d++)f=c[d],b[f.name]="function"===typeof f.val?g(a,f.val,f):h.isPlainObject(f.val)?
{}:f.val,b[f.name].__dt_wrapper=!0,u.extend(a,b[f.name],f.propExt)}};u.register=p=function(a,b){if(h.isArray(a))for(var c=0,d=a.length;c<d;c++)u.register(a[c],b);else for(var e=a.split("."),f=Ub,g,j,c=0,d=e.length;c<d;c++){g=(j=-1!==e[c].indexOf("()"))?e[c].replace("()",""):e[c];var i;a:{i=0;for(var n=f.length;i<n;i++)if(f[i].name===g){i=f[i];break a}i=null}i||(i={name:g,val:{},methodExt:[],propExt:[]},f.push(i));c===d-1?i.val=b:f=j?i.methodExt:i.propExt}};u.registerPlural=t=function(a,b,c){u.register(a,
c);u.register(b,function(){var a=c.apply(this,arguments);return a===this?this:a instanceof u?a.length?h.isArray(a[0])?new u(a.context,a[0]):a[0]:k:a})};p("tables()",function(a){var b;if(a){b=u;var c=this.context;if("number"===typeof a)a=[c[a]];else var d=h.map(c,function(a){return a.nTable}),a=h(d).filter(a).map(function(){var a=h.inArray(this,d);return c[a]}).toArray();b=new b(a)}else b=this;return b});p("table()",function(a){var a=this.tables(a),b=a.context;return b.length?new u(b[0]):a});t("tables().nodes()",
"table().node()",function(){return this.iterator("table",function(a){return a.nTable},1)});t("tables().body()","table().body()",function(){return this.iterator("table",function(a){return a.nTBody},1)});t("tables().header()","table().header()",function(){return this.iterator("table",function(a){return a.nTHead},1)});t("tables().footer()","table().footer()",function(){return this.iterator("table",function(a){return a.nTFoot},1)});t("tables().containers()","table().container()",function(){return this.iterator("table",
function(a){return a.nTableWrapper},1)});p("draw()",function(a){return this.iterator("table",function(b){"page"===a?O(b):("string"===typeof a&&(a="full-hold"===a?!1:!0),T(b,!1===a))})});p("page()",function(a){return a===k?this.page.info().page:this.iterator("table",function(b){Va(b,a)})});p("page.info()",function(){if(0===this.context.length)return k;var a=this.context[0],b=a._iDisplayStart,c=a.oFeatures.bPaginate?a._iDisplayLength:-1,d=a.fnRecordsDisplay(),e=-1===c;return{page:e?0:Math.floor(b/c),
pages:e?1:Math.ceil(d/c),start:b,end:a.fnDisplayEnd(),length:c,recordsTotal:a.fnRecordsTotal(),recordsDisplay:d,serverSide:"ssp"===y(a)}});p("page.len()",function(a){return a===k?0!==this.context.length?this.context[0]._iDisplayLength:k:this.iterator("table",function(b){Ta(b,a)})});var Vb=function(a,b,c){if(c){var d=new u(a);d.one("draw",function(){c(d.ajax.json())})}if("ssp"==y(a))T(a,b);else{C(a,!0);var e=a.jqXHR;e&&4!==e.readyState&&e.abort();ua(a,[],function(c){pa(a);for(var c=va(a,c),d=0,e=c.length;d<
e;d++)N(a,c[d]);T(a,b);C(a,!1)})}};p("ajax.json()",function(){var a=this.context;if(0<a.length)return a[0].json});p("ajax.params()",function(){var a=this.context;if(0<a.length)return a[0].oAjaxData});p("ajax.reload()",function(a,b){return this.iterator("table",function(c){Vb(c,!1===b,a)})});p("ajax.url()",function(a){var b=this.context;if(a===k){if(0===b.length)return k;b=b[0];return b.ajax?h.isPlainObject(b.ajax)?b.ajax.url:b.ajax:b.sAjaxSource}return this.iterator("table",function(b){h.isPlainObject(b.ajax)?
b.ajax.url=a:b.ajax=a})});p("ajax.url().load()",function(a,b){return this.iterator("table",function(c){Vb(c,!1===b,a)})});var bb=function(a,b,c,d,e){var f=[],g,j,i,n,l,m;i=typeof b;if(!b||"string"===i||"function"===i||b.length===k)b=[b];i=0;for(n=b.length;i<n;i++){j=b[i]&&b[i].split&&!b[i].match(/[\[\(:]/)?b[i].split(","):[b[i]];l=0;for(m=j.length;l<m;l++)(g=c("string"===typeof j[l]?h.trim(j[l]):j[l]))&&g.length&&(f=f.concat(g))}a=x.selector[a];if(a.length){i=0;for(n=a.length;i<n;i++)f=a[i](d,e,f)}return sa(f)},
cb=function(a){a||(a={});a.filter&&a.search===k&&(a.search=a.filter);return h.extend({search:"none",order:"current",page:"all"},a)},db=function(a){for(var b=0,c=a.length;b<c;b++)if(0<a[b].length)return a[0]=a[b],a[0].length=1,a.length=1,a.context=[a.context[b]],a;a.length=0;return a},Da=function(a,b){var c,d,e,f=[],g=a.aiDisplay;c=a.aiDisplayMaster;var j=b.search;d=b.order;e=b.page;if("ssp"==y(a))return"removed"===j?[]:X(0,c.length);if("current"==e){c=a._iDisplayStart;for(d=a.fnDisplayEnd();c<d;c++)f.push(g[c])}else if("current"==
d||"applied"==d)f="none"==j?c.slice():"applied"==j?g.slice():h.map(c,function(a){return-1===h.inArray(a,g)?a:null});else if("index"==d||"original"==d){c=0;for(d=a.aoData.length;c<d;c++)"none"==j?f.push(c):(e=h.inArray(c,g),(-1===e&&"removed"==j||0<=e&&"applied"==j)&&f.push(c))}return f};p("rows()",function(a,b){a===k?a="":h.isPlainObject(a)&&(b=a,a="");var b=cb(b),c=this.iterator("table",function(c){var e=b,f;return bb("row",a,function(a){var b=Qb(a);if(b!==null&&!e)return[b];f||(f=Da(c,e));if(b!==
null&&h.inArray(b,f)!==-1)return[b];if(a===null||a===k||a==="")return f;if(typeof a==="function")return h.map(f,function(b){var e=c.aoData[b];return a(b,e._aData,e.nTr)?b:null});b=Tb(ja(c.aoData,f,"nTr"));if(a.nodeName){if(a._DT_RowIndex!==k)return[a._DT_RowIndex];if(a._DT_CellIndex)return[a._DT_CellIndex.row];b=h(a).closest("*[data-dt-row]");return b.length?[b.data("dt-row")]:[]}if(typeof a==="string"&&a.charAt(0)==="#"){var i=c.aIds[a.replace(/^#/,"")];if(i!==k)return[i.idx]}return h(b).filter(a).map(function(){return this._DT_RowIndex}).toArray()},
c,e)},1);c.selector.rows=a;c.selector.opts=b;return c});p("rows().nodes()",function(){return this.iterator("row",function(a,b){return a.aoData[b].nTr||k},1)});p("rows().data()",function(){return this.iterator(!0,"rows",function(a,b){return ja(a.aoData,b,"_aData")},1)});t("rows().cache()","row().cache()",function(a){return this.iterator("row",function(b,c){var d=b.aoData[c];return"search"===a?d._aFilterData:d._aSortData},1)});t("rows().invalidate()","row().invalidate()",function(a){return this.iterator("row",
function(b,c){da(b,c,a)})});t("rows().indexes()","row().index()",function(){return this.iterator("row",function(a,b){return b},1)});t("rows().ids()","row().id()",function(a){for(var b=[],c=this.context,d=0,e=c.length;d<e;d++)for(var f=0,g=this[d].length;f<g;f++){var h=c[d].rowIdFn(c[d].aoData[this[d][f]]._aData);b.push((!0===a?"#":"")+h)}return new u(c,b)});t("rows().remove()","row().remove()",function(){var a=this;this.iterator("row",function(b,c,d){var e=b.aoData,f=e[c],g,h,i,n,l;e.splice(c,1);
g=0;for(h=e.length;g<h;g++)if(i=e[g],l=i.anCells,null!==i.nTr&&(i.nTr._DT_RowIndex=g),null!==l){i=0;for(n=l.length;i<n;i++)l[i]._DT_CellIndex.row=g}qa(b.aiDisplayMaster,c);qa(b.aiDisplay,c);qa(a[d],c,!1);Ua(b);c=b.rowIdFn(f._aData);c!==k&&delete b.aIds[c]});this.iterator("table",function(a){for(var c=0,d=a.aoData.length;c<d;c++)a.aoData[c].idx=c});return this});p("rows.add()",function(a){var b=this.iterator("table",function(b){var c,f,g,h=[];f=0;for(g=a.length;f<g;f++)c=a[f],c.nodeName&&"TR"===c.nodeName.toUpperCase()?
h.push(oa(b,c)[0]):h.push(N(b,c));return h},1),c=this.rows(-1);c.pop();h.merge(c,b);return c});p("row()",function(a,b){return db(this.rows(a,b))});p("row().data()",function(a){var b=this.context;if(a===k)return b.length&&this.length?b[0].aoData[this[0]]._aData:k;b[0].aoData[this[0]]._aData=a;da(b[0],this[0],"data");return this});p("row().node()",function(){var a=this.context;return a.length&&this.length?a[0].aoData[this[0]].nTr||null:null});p("row.add()",function(a){a instanceof h&&a.length&&(a=a[0]);
var b=this.iterator("table",function(b){return a.nodeName&&"TR"===a.nodeName.toUpperCase()?oa(b,a)[0]:N(b,a)});return this.row(b[0])});var eb=function(a,b){var c=a.context;if(c.length&&(c=c[0].aoData[b!==k?b:a[0]])&&c._details)c._details.remove(),c._detailsShow=k,c._details=k},Wb=function(a,b){var c=a.context;if(c.length&&a.length){var d=c[0].aoData[a[0]];if(d._details){(d._detailsShow=b)?d._details.insertAfter(d.nTr):d._details.detach();var e=c[0],f=new u(e),g=e.aoData;f.off("draw.dt.DT_details column-visibility.dt.DT_details destroy.dt.DT_details");
0<D(g,"_details").length&&(f.on("draw.dt.DT_details",function(a,b){e===b&&f.rows({page:"current"}).eq(0).each(function(a){a=g[a];a._detailsShow&&a._details.insertAfter(a.nTr)})}),f.on("column-visibility.dt.DT_details",function(a,b){if(e===b)for(var c,d=ba(b),f=0,h=g.length;f<h;f++)c=g[f],c._details&&c._details.children("td[colspan]").attr("colspan",d)}),f.on("destroy.dt.DT_details",function(a,b){if(e===b)for(var c=0,d=g.length;c<d;c++)g[c]._details&&eb(f,c)}))}}};p("row().child()",function(a,b){var c=
this.context;if(a===k)return c.length&&this.length?c[0].aoData[this[0]]._details:k;if(!0===a)this.child.show();else if(!1===a)eb(this);else if(c.length&&this.length){var d=c[0],c=c[0].aoData[this[0]],e=[],f=function(a,b){if(h.isArray(a)||a instanceof h)for(var c=0,k=a.length;c<k;c++)f(a[c],b);else a.nodeName&&"tr"===a.nodeName.toLowerCase()?e.push(a):(c=h("<tr><td/></tr>").addClass(b),h("td",c).addClass(b).html(a)[0].colSpan=ba(d),e.push(c[0]))};f(a,b);c._details&&c._details.detach();c._details=h(e);
c._detailsShow&&c._details.insertAfter(c.nTr)}return this});p(["row().child.show()","row().child().show()"],function(){Wb(this,!0);return this});p(["row().child.hide()","row().child().hide()"],function(){Wb(this,!1);return this});p(["row().child.remove()","row().child().remove()"],function(){eb(this);return this});p("row().child.isShown()",function(){var a=this.context;return a.length&&this.length?a[0].aoData[this[0]]._detailsShow||!1:!1});var fc=/^([^:]+):(name|visIdx|visible)$/,Xb=function(a,b,
c,d,e){for(var c=[],d=0,f=e.length;d<f;d++)c.push(B(a,e[d],b));return c};p("columns()",function(a,b){a===k?a="":h.isPlainObject(a)&&(b=a,a="");var b=cb(b),c=this.iterator("table",function(c){var e=a,f=b,g=c.aoColumns,j=D(g,"sName"),i=D(g,"nTh");return bb("column",e,function(a){var b=Qb(a);if(a==="")return X(g.length);if(b!==null)return[b>=0?b:g.length+b];if(typeof a==="function"){var e=Da(c,f);return h.map(g,function(b,f){return a(f,Xb(c,f,0,0,e),i[f])?f:null})}var k=typeof a==="string"?a.match(fc):
"";if(k)switch(k[2]){case "visIdx":case "visible":b=parseInt(k[1],10);if(b<0){var m=h.map(g,function(a,b){return a.bVisible?b:null});return[m[m.length+b]]}return[$(c,b)];case "name":return h.map(j,function(a,b){return a===k[1]?b:null});default:return[]}if(a.nodeName&&a._DT_CellIndex)return[a._DT_CellIndex.column];b=h(i).filter(a).map(function(){return h.inArray(this,i)}).toArray();if(b.length||!a.nodeName)return b;b=h(a).closest("*[data-dt-column]");return b.length?[b.data("dt-column")]:[]},c,f)},
1);c.selector.cols=a;c.selector.opts=b;return c});t("columns().header()","column().header()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTh},1)});t("columns().footer()","column().footer()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTf},1)});t("columns().data()","column().data()",function(){return this.iterator("column-rows",Xb,1)});t("columns().dataSrc()","column().dataSrc()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].mData},
1)});t("columns().cache()","column().cache()",function(a){return this.iterator("column-rows",function(b,c,d,e,f){return ja(b.aoData,f,"search"===a?"_aFilterData":"_aSortData",c)},1)});t("columns().nodes()","column().nodes()",function(){return this.iterator("column-rows",function(a,b,c,d,e){return ja(a.aoData,e,"anCells",b)},1)});t("columns().visible()","column().visible()",function(a,b){var c=this.iterator("column",function(b,c){if(a===k)return b.aoColumns[c].bVisible;var f=b.aoColumns,g=f[c],j=b.aoData,
i,n,l;if(a!==k&&g.bVisible!==a){if(a){var m=h.inArray(!0,D(f,"bVisible"),c+1);i=0;for(n=j.length;i<n;i++)l=j[i].nTr,f=j[i].anCells,l&&l.insertBefore(f[c],f[m]||null)}else h(D(b.aoData,"anCells",c)).detach();g.bVisible=a;fa(b,b.aoHeader);fa(b,b.aoFooter);za(b)}});a!==k&&(this.iterator("column",function(c,e){s(c,null,"column-visibility",[c,e,a,b])}),(b===k||b)&&this.columns.adjust());return c});t("columns().indexes()","column().index()",function(a){return this.iterator("column",function(b,c){return"visible"===
a?aa(b,c):c},1)});p("columns.adjust()",function(){return this.iterator("table",function(a){Z(a)},1)});p("column.index()",function(a,b){if(0!==this.context.length){var c=this.context[0];if("fromVisible"===a||"toData"===a)return $(c,b);if("fromData"===a||"toVisible"===a)return aa(c,b)}});p("column()",function(a,b){return db(this.columns(a,b))});p("cells()",function(a,b,c){h.isPlainObject(a)&&(a.row===k?(c=a,a=null):(c=b,b=null));h.isPlainObject(b)&&(c=b,b=null);if(null===b||b===k)return this.iterator("table",
function(b){var d=a,e=cb(c),f=b.aoData,g=Da(b,e),i=Tb(ja(f,g,"anCells")),j=h([].concat.apply([],i)),l,n=b.aoColumns.length,m,p,t,u,s,v;return bb("cell",d,function(a){var c=typeof a==="function";if(a===null||a===k||c){m=[];p=0;for(t=g.length;p<t;p++){l=g[p];for(u=0;u<n;u++){s={row:l,column:u};if(c){v=f[l];a(s,B(b,l,u),v.anCells?v.anCells[u]:null)&&m.push(s)}else m.push(s)}}return m}if(h.isPlainObject(a))return[a];c=j.filter(a).map(function(a,b){return{row:b._DT_CellIndex.row,column:b._DT_CellIndex.column}}).toArray();
if(c.length||!a.nodeName)return c;v=h(a).closest("*[data-dt-row]");return v.length?[{row:v.data("dt-row"),column:v.data("dt-column")}]:[]},b,e)});var d=this.columns(b,c),e=this.rows(a,c),f,g,j,i,n,l=this.iterator("table",function(a,b){f=[];g=0;for(j=e[b].length;g<j;g++){i=0;for(n=d[b].length;i<n;i++)f.push({row:e[b][g],column:d[b][i]})}return f},1);h.extend(l.selector,{cols:b,rows:a,opts:c});return l});t("cells().nodes()","cell().node()",function(){return this.iterator("cell",function(a,b,c){return(a=
a.aoData[b])&&a.anCells?a.anCells[c]:k},1)});p("cells().data()",function(){return this.iterator("cell",function(a,b,c){return B(a,b,c)},1)});t("cells().cache()","cell().cache()",function(a){a="search"===a?"_aFilterData":"_aSortData";return this.iterator("cell",function(b,c,d){return b.aoData[c][a][d]},1)});t("cells().render()","cell().render()",function(a){return this.iterator("cell",function(b,c,d){return B(b,c,d,a)},1)});t("cells().indexes()","cell().index()",function(){return this.iterator("cell",
function(a,b,c){return{row:b,column:c,columnVisible:aa(a,c)}},1)});t("cells().invalidate()","cell().invalidate()",function(a){return this.iterator("cell",function(b,c,d){da(b,c,a,d)})});p("cell()",function(a,b,c){return db(this.cells(a,b,c))});p("cell().data()",function(a){var b=this.context,c=this[0];if(a===k)return b.length&&c.length?B(b[0],c[0].row,c[0].column):k;lb(b[0],c[0].row,c[0].column,a);da(b[0],c[0].row,"data",c[0].column);return this});p("order()",function(a,b){var c=this.context;if(a===
k)return 0!==c.length?c[0].aaSorting:k;"number"===typeof a?a=[[a,b]]:a.length&&!h.isArray(a[0])&&(a=Array.prototype.slice.call(arguments));return this.iterator("table",function(b){b.aaSorting=a.slice()})});p("order.listener()",function(a,b,c){return this.iterator("table",function(d){Oa(d,a,b,c)})});p("order.fixed()",function(a){if(!a){var b=this.context,b=b.length?b[0].aaSortingFixed:k;return h.isArray(b)?{pre:b}:b}return this.iterator("table",function(b){b.aaSortingFixed=h.extend(!0,{},a)})});p(["columns().order()",
"column().order()"],function(a){var b=this;return this.iterator("table",function(c,d){var e=[];h.each(b[d],function(b,c){e.push([c,a])});c.aaSorting=e})});p("search()",function(a,b,c,d){var e=this.context;return a===k?0!==e.length?e[0].oPreviousSearch.sSearch:k:this.iterator("table",function(e){e.oFeatures.bFilter&&ga(e,h.extend({},e.oPreviousSearch,{sSearch:a+"",bRegex:null===b?!1:b,bSmart:null===c?!0:c,bCaseInsensitive:null===d?!0:d}),1)})});t("columns().search()","column().search()",function(a,
b,c,d){return this.iterator("column",function(e,f){var g=e.aoPreSearchCols;if(a===k)return g[f].sSearch;e.oFeatures.bFilter&&(h.extend(g[f],{sSearch:a+"",bRegex:null===b?!1:b,bSmart:null===c?!0:c,bCaseInsensitive:null===d?!0:d}),ga(e,e.oPreviousSearch,1))})});p("state()",function(){return this.context.length?this.context[0].oSavedState:null});p("state.clear()",function(){return this.iterator("table",function(a){a.fnStateSaveCallback.call(a.oInstance,a,{})})});p("state.loaded()",function(){return this.context.length?
this.context[0].oLoadedState:null});p("state.save()",function(){return this.iterator("table",function(a){za(a)})});m.versionCheck=m.fnVersionCheck=function(a){for(var b=m.version.split("."),a=a.split("."),c,d,e=0,f=a.length;e<f;e++)if(c=parseInt(b[e],10)||0,d=parseInt(a[e],10)||0,c!==d)return c>d;return!0};m.isDataTable=m.fnIsDataTable=function(a){var b=h(a).get(0),c=!1;if(a instanceof m.Api)return!0;h.each(m.settings,function(a,e){var f=e.nScrollHead?h("table",e.nScrollHead)[0]:null,g=e.nScrollFoot?
h("table",e.nScrollFoot)[0]:null;if(e.nTable===b||f===b||g===b)c=!0});return c};m.tables=m.fnTables=function(a){var b=!1;h.isPlainObject(a)&&(b=a.api,a=a.visible);var c=h.map(m.settings,function(b){if(!a||a&&h(b.nTable).is(":visible"))return b.nTable});return b?new u(c):c};m.camelToHungarian=J;p("$()",function(a,b){var c=this.rows(b).nodes(),c=h(c);return h([].concat(c.filter(a).toArray(),c.find(a).toArray()))});h.each(["on","one","off"],function(a,b){p(b+"()",function(){var a=Array.prototype.slice.call(arguments);
a[0]=h.map(a[0].split(/\s/),function(a){return!a.match(/\.dt\b/)?a+".dt":a}).join(" ");var d=h(this.tables().nodes());d[b].apply(d,a);return this})});p("clear()",function(){return this.iterator("table",function(a){pa(a)})});p("settings()",function(){return new u(this.context,this.context)});p("init()",function(){var a=this.context;return a.length?a[0].oInit:null});p("data()",function(){return this.iterator("table",function(a){return D(a.aoData,"_aData")}).flatten()});p("destroy()",function(a){a=a||
!1;return this.iterator("table",function(b){var c=b.nTableWrapper.parentNode,d=b.oClasses,e=b.nTable,f=b.nTBody,g=b.nTHead,j=b.nTFoot,i=h(e),f=h(f),k=h(b.nTableWrapper),l=h.map(b.aoData,function(a){return a.nTr}),p;b.bDestroying=!0;s(b,"aoDestroyCallback","destroy",[b]);a||(new u(b)).columns().visible(!0);k.off(".DT").find(":not(tbody *)").off(".DT");h(E).off(".DT-"+b.sInstance);e!=g.parentNode&&(i.children("thead").detach(),i.append(g));j&&e!=j.parentNode&&(i.children("tfoot").detach(),i.append(j));
b.aaSorting=[];b.aaSortingFixed=[];ya(b);h(l).removeClass(b.asStripeClasses.join(" "));h("th, td",g).removeClass(d.sSortable+" "+d.sSortableAsc+" "+d.sSortableDesc+" "+d.sSortableNone);b.bJUI&&(h("th span."+d.sSortIcon+", td span."+d.sSortIcon,g).detach(),h("th, td",g).each(function(){var a=h("div."+d.sSortJUIWrapper,this);h(this).append(a.contents());a.detach()}));f.children().detach();f.append(l);g=a?"remove":"detach";i[g]();k[g]();!a&&c&&(c.insertBefore(e,b.nTableReinsertBefore),i.css("width",
b.sDestroyWidth).removeClass(d.sTable),(p=b.asDestroyStripes.length)&&f.children().each(function(a){h(this).addClass(b.asDestroyStripes[a%p])}));c=h.inArray(b,m.settings);-1!==c&&m.settings.splice(c,1)})});h.each(["column","row","cell"],function(a,b){p(b+"s().every()",function(a){var d=this.selector.opts,e=this;return this.iterator(b,function(f,g,h,i,m){a.call(e[b](g,"cell"===b?h:d,"cell"===b?d:k),g,h,i,m)})})});p("i18n()",function(a,b,c){var d=this.context[0],a=R(a)(d.oLanguage);a===k&&(a=b);c!==
k&&h.isPlainObject(a)&&(a=a[c]!==k?a[c]:a._);return a.replace("%d",c)});m.version="1.10.13";m.settings=[];m.models={};m.models.oSearch={bCaseInsensitive:!0,sSearch:"",bRegex:!1,bSmart:!0};m.models.oRow={nTr:null,anCells:null,_aData:[],_aSortData:null,_aFilterData:null,_sFilterRow:null,_sRowStripe:"",src:null,idx:-1};m.models.oColumn={idx:null,aDataSort:null,asSorting:null,bSearchable:null,bSortable:null,bVisible:null,_sManualType:null,_bAttrSrc:!1,fnCreatedCell:null,fnGetData:null,fnSetData:null,
mData:null,mRender:null,nTh:null,nTf:null,sClass:null,sContentPadding:null,sDefaultContent:null,sName:null,sSortDataType:"std",sSortingClass:null,sSortingClassJUI:null,sTitle:null,sType:null,sWidth:null,sWidthOrig:null};m.defaults={aaData:null,aaSorting:[[0,"asc"]],aaSortingFixed:[],ajax:null,aLengthMenu:[10,25,50,100],aoColumns:null,aoColumnDefs:null,aoSearchCols:[],asStripeClasses:null,bAutoWidth:!0,bDeferRender:!1,bDestroy:!1,bFilter:!0,bInfo:!0,bJQueryUI:!1,bLengthChange:!0,bPaginate:!0,bProcessing:!1,
bRetrieve:!1,bScrollCollapse:!1,bServerSide:!1,bSort:!0,bSortMulti:!0,bSortCellsTop:!1,bSortClasses:!0,bStateSave:!1,fnCreatedRow:null,fnDrawCallback:null,fnFooterCallback:null,fnFormatNumber:function(a){return a.toString().replace(/\B(?=(\d{3})+(?!\d))/g,this.oLanguage.sThousands)},fnHeaderCallback:null,fnInfoCallback:null,fnInitComplete:null,fnPreDrawCallback:null,fnRowCallback:null,fnServerData:null,fnServerParams:null,fnStateLoadCallback:function(a){try{return JSON.parse((-1===a.iStateDuration?
sessionStorage:localStorage).getItem("DataTables_"+a.sInstance+"_"+location.pathname))}catch(b){}},fnStateLoadParams:null,fnStateLoaded:null,fnStateSaveCallback:function(a,b){try{(-1===a.iStateDuration?sessionStorage:localStorage).setItem("DataTables_"+a.sInstance+"_"+location.pathname,JSON.stringify(b))}catch(c){}},fnStateSaveParams:null,iStateDuration:7200,iDeferLoading:null,iDisplayLength:10,iDisplayStart:0,iTabIndex:0,oClasses:{},oLanguage:{oAria:{sSortAscending:": activate to sort column ascending",
sSortDescending:": activate to sort column descending"},oPaginate:{sFirst:"First",sLast:"Last",sNext:"Next",sPrevious:"Previous"},sEmptyTable:"No data available in table",sInfo:"Showing _START_ to _END_ of _TOTAL_ entries",sInfoEmpty:"Showing 0 to 0 of 0 entries",sInfoFiltered:"(filtered from _MAX_ total entries)",sInfoPostFix:"",sDecimal:"",sThousands:",",sLengthMenu:"Show _MENU_ entries",sLoadingRecords:"Loading...",sProcessing:"Processing...",sSearch:"Search:",sSearchPlaceholder:"",sUrl:"",sZeroRecords:"No matching records found"},
oSearch:h.extend({},m.models.oSearch),sAjaxDataProp:"data",sAjaxSource:null,sDom:"lfrtip",searchDelay:null,sPaginationType:"simple_numbers",sScrollX:"",sScrollXInner:"",sScrollY:"",sServerMethod:"GET",renderer:null,rowId:"DT_RowId"};Y(m.defaults);m.defaults.column={aDataSort:null,iDataSort:-1,asSorting:["asc","desc"],bSearchable:!0,bSortable:!0,bVisible:!0,fnCreatedCell:null,mData:null,mRender:null,sCellType:"td",sClass:"",sContentPadding:"",sDefaultContent:null,sName:"",sSortDataType:"std",sTitle:null,
sType:null,sWidth:null};Y(m.defaults.column);m.models.oSettings={oFeatures:{bAutoWidth:null,bDeferRender:null,bFilter:null,bInfo:null,bLengthChange:null,bPaginate:null,bProcessing:null,bServerSide:null,bSort:null,bSortMulti:null,bSortClasses:null,bStateSave:null},oScroll:{bCollapse:null,iBarWidth:0,sX:null,sXInner:null,sY:null},oLanguage:{fnInfoCallback:null},oBrowser:{bScrollOversize:!1,bScrollbarLeft:!1,bBounding:!1,barWidth:0},ajax:null,aanFeatures:[],aoData:[],aiDisplay:[],aiDisplayMaster:[],
aIds:{},aoColumns:[],aoHeader:[],aoFooter:[],oPreviousSearch:{},aoPreSearchCols:[],aaSorting:null,aaSortingFixed:[],asStripeClasses:null,asDestroyStripes:[],sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],aoFooterCallback:[],aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bDeferLoading:!1,bInitialised:!1,aoOpenRows:[],sDom:null,
searchDelay:null,sPaginationType:"two_button",iStateDuration:0,aoStateSave:[],aoStateLoad:[],oSavedState:null,oLoadedState:null,sAjaxSource:null,sAjaxDataProp:null,bAjaxDataGet:!0,jqXHR:null,json:k,oAjaxData:k,fnServerData:null,aoServerParams:[],sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,iDraw:0,bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iRecordsTotal:0,_iRecordsDisplay:0,bJUI:null,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],
fnRecordsTotal:function(){return"ssp"==y(this)?1*this._iRecordsTotal:this.aiDisplayMaster.length},fnRecordsDisplay:function(){return"ssp"==y(this)?1*this._iRecordsDisplay:this.aiDisplay.length},fnDisplayEnd:function(){var a=this._iDisplayLength,b=this._iDisplayStart,c=b+a,d=this.aiDisplay.length,e=this.oFeatures,f=e.bPaginate;return e.bServerSide?!1===f||-1===a?b+d:Math.min(b+a,this._iRecordsDisplay):!f||c>d||-1===a?d:c},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null,
aLastSort:[],oPlugins:{},rowIdFn:null,rowId:null};m.ext=x={buttons:{},classes:{},builder:"-source-",errMode:"alert",feature:[],search:[],selector:{cell:[],column:[],row:[]},internal:{},legacy:{ajax:null},pager:{},renderer:{pageButton:{},header:{}},order:{},type:{detect:[],search:{},order:{}},_unique:0,fnVersionCheck:m.fnVersionCheck,iApiIndex:0,oJUIClasses:{},sVersion:m.version};h.extend(x,{afnFiltering:x.search,aTypes:x.type.detect,ofnSearch:x.type.search,oSort:x.type.order,afnSortData:x.order,aoFeatures:x.feature,
oApi:x.internal,oStdClasses:x.classes,oPagination:x.pager});h.extend(m.ext.classes,{sTable:"dataTable",sNoFooter:"no-footer",sPageButton:"paginate_button",sPageButtonActive:"current",sPageButtonDisabled:"disabled",sStripeOdd:"odd",sStripeEven:"even",sRowEmpty:"dataTables_empty",sWrapper:"dataTables_wrapper",sFilter:"dataTables_filter",sInfo:"dataTables_info",sPaging:"dataTables_paginate paging_",sLength:"dataTables_length",sProcessing:"dataTables_processing",sSortAsc:"sorting_asc",sSortDesc:"sorting_desc",
sSortable:"sorting",sSortableAsc:"sorting_asc_disabled",sSortableDesc:"sorting_desc_disabled",sSortableNone:"sorting_disabled",sSortColumn:"sorting_",sFilterInput:"",sLengthSelect:"",sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead",sScrollHeadInner:"dataTables_scrollHeadInner",sScrollBody:"dataTables_scrollBody",sScrollFoot:"dataTables_scrollFoot",sScrollFootInner:"dataTables_scrollFootInner",sHeaderTH:"",sFooterTH:"",sSortJUIAsc:"",sSortJUIDesc:"",sSortJUI:"",sSortJUIAscAllowed:"",
sSortJUIDescAllowed:"",sSortJUIWrapper:"",sSortIcon:"",sJUIHeader:"",sJUIFooter:""});var Ea="",Ea="",G=Ea+"ui-state-default",ka=Ea+"css_right ui-icon ui-icon-",Yb=Ea+"fg-toolbar ui-toolbar ui-widget-header ui-helper-clearfix";h.extend(m.ext.oJUIClasses,m.ext.classes,{sPageButton:"fg-button ui-button "+G,sPageButtonActive:"ui-state-disabled",sPageButtonDisabled:"ui-state-disabled",sPaging:"dataTables_paginate fg-buttonset ui-buttonset fg-buttonset-multi ui-buttonset-multi paging_",sSortAsc:G+" sorting_asc",
sSortDesc:G+" sorting_desc",sSortable:G+" sorting",sSortableAsc:G+" sorting_asc_disabled",sSortableDesc:G+" sorting_desc_disabled",sSortableNone:G+" sorting_disabled",sSortJUIAsc:ka+"triangle-1-n",sSortJUIDesc:ka+"triangle-1-s",sSortJUI:ka+"carat-2-n-s",sSortJUIAscAllowed:ka+"carat-1-n",sSortJUIDescAllowed:ka+"carat-1-s",sSortJUIWrapper:"DataTables_sort_wrapper",sSortIcon:"DataTables_sort_icon",sScrollHead:"dataTables_scrollHead "+G,sScrollFoot:"dataTables_scrollFoot "+G,sHeaderTH:G,sFooterTH:G,sJUIHeader:Yb+
" ui-corner-tl ui-corner-tr",sJUIFooter:Yb+" ui-corner-bl ui-corner-br"});var Nb=m.ext.pager;h.extend(Nb,{simple:function(){return["previous","next"]},full:function(){return["first","previous","next","last"]},numbers:function(a,b){return[ia(a,b)]},simple_numbers:function(a,b){return["previous",ia(a,b),"next"]},full_numbers:function(a,b){return["first","previous",ia(a,b),"next","last"]},first_last_numbers:function(a,b){return["first",ia(a,b),"last"]},_numbers:ia,numbers_length:7});h.extend(!0,m.ext.renderer,
{pageButton:{_:function(a,b,c,d,e,f){var g=a.oClasses,j=a.oLanguage.oPaginate,i=a.oLanguage.oAria.paginate||{},m,l,p=0,r=function(b,d){var k,t,u,s,v=function(b){Va(a,b.data.action,true)};k=0;for(t=d.length;k<t;k++){s=d[k];if(h.isArray(s)){u=h("<"+(s.DT_el||"div")+"/>").appendTo(b);r(u,s)}else{m=null;l="";switch(s){case "ellipsis":b.append('<span class="ellipsis">&#x2026;</span>');break;case "first":m=j.sFirst;l=s+(e>0?"":" "+g.sPageButtonDisabled);break;case "previous":m=j.sPrevious;l=s+(e>0?"":" "+
g.sPageButtonDisabled);break;case "next":m=j.sNext;l=s+(e<f-1?"":" "+g.sPageButtonDisabled);break;case "last":m=j.sLast;l=s+(e<f-1?"":" "+g.sPageButtonDisabled);break;default:m=s+1;l=e===s?g.sPageButtonActive:""}if(m!==null){u=h("<a>",{"class":g.sPageButton+" "+l,"aria-controls":a.sTableId,"aria-label":i[s],"data-dt-idx":p,tabindex:a.iTabIndex,id:c===0&&typeof s==="string"?a.sTableId+"_"+s:null}).html(m).appendTo(b);Ya(u,{action:s},v);p++}}}},t;try{t=h(b).find(H.activeElement).data("dt-idx")}catch(u){}r(h(b).empty(),
d);t!==k&&h(b).find("[data-dt-idx="+t+"]").focus()}}});h.extend(m.ext.type.detect,[function(a,b){var c=b.oLanguage.sDecimal;return ab(a,c)?"num"+c:null},function(a){if(a&&!(a instanceof Date)&&!cc.test(a))return null;var b=Date.parse(a);return null!==b&&!isNaN(b)||M(a)?"date":null},function(a,b){var c=b.oLanguage.sDecimal;return ab(a,c,!0)?"num-fmt"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Sb(a,c)?"html-num"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Sb(a,c,!0)?"html-num-fmt"+
c:null},function(a){return M(a)||"string"===typeof a&&-1!==a.indexOf("<")?"html":null}]);h.extend(m.ext.type.search,{html:function(a){return M(a)?a:"string"===typeof a?a.replace(Pb," ").replace(Ca,""):""},string:function(a){return M(a)?a:"string"===typeof a?a.replace(Pb," "):a}});var Ba=function(a,b,c,d){if(0!==a&&(!a||"-"===a))return-Infinity;b&&(a=Rb(a,b));a.replace&&(c&&(a=a.replace(c,"")),d&&(a=a.replace(d,"")));return 1*a};h.extend(x.type.order,{"date-pre":function(a){return Date.parse(a)||-Infinity},
"html-pre":function(a){return M(a)?"":a.replace?a.replace(/<.*?>/g,"").toLowerCase():a+""},"string-pre":function(a){return M(a)?"":"string"===typeof a?a.toLowerCase():!a.toString?"":a.toString()},"string-asc":function(a,b){return a<b?-1:a>b?1:0},"string-desc":function(a,b){return a<b?1:a>b?-1:0}});fb("");h.extend(!0,m.ext.renderer,{header:{_:function(a,b,c,d){h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(c.sSortingClass+" "+d.sSortAsc+" "+d.sSortDesc).addClass(h[e]==
"asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc:c.sSortingClass)}})},jqueryui:function(a,b,c,d){h("<div/>").addClass(d.sSortJUIWrapper).append(b.contents()).append(h("<span/>").addClass(d.sSortIcon+" "+c.sSortingClassJUI)).appendTo(b);h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(d.sSortAsc+" "+d.sSortDesc).addClass(h[e]=="asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc:c.sSortingClass);b.find("span."+d.sSortIcon).removeClass(d.sSortJUIAsc+" "+d.sSortJUIDesc+" "+d.sSortJUI+" "+
d.sSortJUIAscAllowed+" "+d.sSortJUIDescAllowed).addClass(h[e]=="asc"?d.sSortJUIAsc:h[e]=="desc"?d.sSortJUIDesc:c.sSortingClassJUI)}})}}});var Zb=function(a){return"string"===typeof a?a.replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"):a};m.render={number:function(a,b,c,d,e){return{display:function(f){if("number"!==typeof f&&"string"!==typeof f)return f;var g=0>f?"-":"",h=parseFloat(f);if(isNaN(h))return Zb(f);h=h.toFixed(c);f=Math.abs(h);h=parseInt(f,10);f=c?b+(f-h).toFixed(c).substring(2):
"";return g+(d||"")+h.toString().replace(/\B(?=(\d{3})+(?!\d))/g,a)+f+(e||"")}}},text:function(){return{display:Zb}}};h.extend(m.ext.internal,{_fnExternApiFunc:Ob,_fnBuildAjax:ua,_fnAjaxUpdate:nb,_fnAjaxParameters:wb,_fnAjaxUpdateDraw:xb,_fnAjaxDataSrc:va,_fnAddColumn:Ga,_fnColumnOptions:la,_fnAdjustColumnSizing:Z,_fnVisibleToColumnIndex:$,_fnColumnIndexToVisible:aa,_fnVisbleColumns:ba,_fnGetColumns:na,_fnColumnTypes:Ia,_fnApplyColumnDefs:kb,_fnHungarianMap:Y,_fnCamelToHungarian:J,_fnLanguageCompat:Fa,
_fnBrowserDetect:ib,_fnAddData:N,_fnAddTr:oa,_fnNodeToDataIndex:function(a,b){return b._DT_RowIndex!==k?b._DT_RowIndex:null},_fnNodeToColumnIndex:function(a,b,c){return h.inArray(c,a.aoData[b].anCells)},_fnGetCellData:B,_fnSetCellData:lb,_fnSplitObjNotation:La,_fnGetObjectDataFn:R,_fnSetObjectDataFn:S,_fnGetDataMaster:Ma,_fnClearTable:pa,_fnDeleteIndex:qa,_fnInvalidate:da,_fnGetRowElements:Ka,_fnCreateTr:Ja,_fnBuildHead:mb,_fnDrawHead:fa,_fnDraw:O,_fnReDraw:T,_fnAddOptionsHtml:pb,_fnDetectHeader:ea,
_fnGetUniqueThs:ta,_fnFeatureHtmlFilter:rb,_fnFilterComplete:ga,_fnFilterCustom:Ab,_fnFilterColumn:zb,_fnFilter:yb,_fnFilterCreateSearch:Ra,_fnEscapeRegex:Sa,_fnFilterData:Bb,_fnFeatureHtmlInfo:ub,_fnUpdateInfo:Eb,_fnInfoMacros:Fb,_fnInitialise:ha,_fnInitComplete:wa,_fnLengthChange:Ta,_fnFeatureHtmlLength:qb,_fnFeatureHtmlPaginate:vb,_fnPageChange:Va,_fnFeatureHtmlProcessing:sb,_fnProcessingDisplay:C,_fnFeatureHtmlTable:tb,_fnScrollDraw:ma,_fnApplyToChildren:I,_fnCalculateColumnWidths:Ha,_fnThrottle:Qa,
_fnConvertToWidth:Gb,_fnGetWidestNode:Hb,_fnGetMaxLenString:Ib,_fnStringToCss:v,_fnSortFlatten:W,_fnSort:ob,_fnSortAria:Kb,_fnSortListener:Xa,_fnSortAttachListener:Oa,_fnSortingClasses:ya,_fnSortData:Jb,_fnSaveState:za,_fnLoadState:Lb,_fnSettingsFromNode:Aa,_fnLog:K,_fnMap:F,_fnBindAction:Ya,_fnCallbackReg:z,_fnCallbackFire:s,_fnLengthOverflow:Ua,_fnRenderer:Pa,_fnDataSource:y,_fnRowAttributes:Na,_fnCalculateEnd:function(){}});h.fn.dataTable=m;m.$=h;h.fn.dataTableSettings=m.settings;h.fn.dataTableExt=
m.ext;h.fn.DataTable=function(a){return h(this).dataTable(a).api()};h.each(m,function(a,b){h.fn.DataTable[a]=b});return h.fn.dataTable});

View File

@@ -1,215 +0,0 @@
A tiny & dead-simple jQuery plugin for sortable tables.
Here's a basic [demo](http://dl.dropbox.com/u/780754/tablesort/index.html).
Install
---
Just add jQuery & the tablesort plugin to your page:
<script src="http://code.jquery.com/jquery-latest.min.js"></script>
<script src="jquery.tablesort.js"></script>
(The plugin is also compatible with [Zepto.js](https://github.com/madrobby/zepto)).
Basic use
---
Call the appropriate method on the table you want to make sortable:
$('table').tablesort();
The table will be sorted when the column headers are clicked.
To prevent a column from being sortable, just add the `no-sort` class:
<th class="no-sort">Photo</th>
Your table should follow this general format:
> Note: If you have access to the table markup, it's better to wrap your table rows
in `<thead>` and `<tbody>` elements (see below), resulting in a slightly faster sort.
>
> If you can't use `<thead>`, the plugin will fall back by sorting all `<tr>` rows
that contain a `<td>` element using jQuery's `.has()` method (ie, the header row,
containing `<th>` elements, will remain at the top where it belongs).
<table>
<thead>
<tr>
<th></th>
...
</tr>
</thead>
<tbody>
<tr>
<td></td>
...
</tr>
</tbody>
</table>
If you want some imageless arrows to indicate the sort, just add this to your CSS:
th.sorted.ascending:after {
content: " \2191";
}
th.sorted.descending:after {
content: " \2193";
}
How cells are sorted
---
At the moment cells are naively sorted using string comparison. By default, the `<td>`'s text is used, but you can easily override that by adding a `data-sort-value` attribute to the cell. For example to sort by a date while keeping the cell contents human-friendly, just add the timestamp as the `data-sort-value`:
<td data-sort-value="1331110651437">March 7, 2012</td>
This allows you to sort your cells using your own criteria without having to write a custom sort function. It also keeps the plugin lightweight by not having to guess & parse dates.
Defining custom sort functions
---
If you have special requirements (or don't want to clutter your markup like the above example) you can easily hook in your own function that determines the sort value for a given cell.
Custom sort functions are attached to `<th>` elements using `data()` and are used to determine the sort value for all cells in that column:
// Sort by dates in YYYY-MM-DD format
$('thead th.date').data('sortBy', function(th, td, tablesort) {
return new Date(td.text());
});
// Sort hex values, ie: "FF0066":
$('thead th.hex').data('sortBy', function(th, td, tablesort) {
return parseInt(td.text(), 16);
});
// Sort by an arbitrary object, ie: a Backbone model:
$('thead th.personID').data('sortBy', function(th, td, tablesort) {
return App.People.get(td.text());
});
Sort functions are passed three parameters:
* the `<th>` being sorted on
* the `<td>` for which the current sort value is required
* the `tablesort` instance
Events
---
The following events are triggered on the `<table>` element being sorted, `'tablesort:start'` and `'tablesort:complete'`. The `event` and `tablesort` instance are passed as parameters:
$('table').on('tablesort:start', function(event, tablesort) {
console.log("Starting the sort...");
});
$('table').on('tablesort:complete', function(event, tablesort) {
console.log("Sort finished!");
});
tablesort instances
---
A table's tablesort instance can be retrieved by querying the data object:
$('table').tablesort(); // Make the table sortable.
var tablesort = $('table').data('tablesort'); // Get a reference to it's tablesort instance
Properties:
tablesort.$table // The <table> being sorted.
tablesort.$th // The <th> currently sorted by (null if unsorted).
tablesort.index // The column index of tablesort.$th (or null).
tablesort.direction // The direction of the current sort, either 'asc' or 'desc' (or null if unsorted).
tablesort.settings // Settings for this instance (see below).
Methods:
// Sorts by the specified column and, optionally, direction ('asc' or 'desc').
// If direction is omitted, the reverse of the current direction is used.
tablesort.sort(th, direction);
tablesort.destroy();
Default Sorting
---
It's possible to apply a default sort on page load using the `.sort()` method described above. Simply grab the tablesort instance and call `.sort()`, padding in the `<th>` element you want to sort by.
Assuming your markup is `<table class="sortable">` and the column to sort by default is `<th class="default-sort">` you would write:
```javascript
$(function() {
$('table.sortable').tablesort().data('tablesort').sort($("th.default-sort"));
});
```
Settings
---
Here are the supported options and their default values:
$.tablesort.defaults = {
debug: $.tablesort.DEBUG, // Outputs some basic debug info when true.
asc: 'sorted ascending', // CSS classes added to `<th>` elements on sort.
desc: 'sorted descending'
};
You can also change the global debug value which overrides the instance's settings:
$.tablesort.DEBUG = false;
Alternatives
---
I don't use this plugin much any more — most of the fixes & improvements are provided by contributors.
If this plugin isn't meeting your needs and you don't want to submit a pull-request, here are some alternative table-sorting plugins.
* [Stupid jQuery Table Sort](https://github.com/joequery/Stupid-Table-Plugin)
_(Feel free to suggest more by [opening a new issue](https://github.com/kylefox/jquery-tablesort/issues/new))_
Contributing
---
As always, all suggestions, bug reports/fixes, and improvements are welcome.
Minify JavaScript with [Closure Compiler](http://closure-compiler.appspot.com/home) (default options)
Help with any of the following is particularly appreciated:
* Performance improvements
* Making the code as concise/efficient as possible
* Browser compatibility
Please fork and send pull requests, or [report an issue.](https://github.com/kylefox/jquery-tablesort/issues)
# License
jQuery tablesort is distributed under the MIT License.
Learn more at http://opensource.org/licenses/mit-license.php
Copyright (c) 2012 Kyle Fox
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,39 @@
jQuery(function ($) {
function generateChart(el) {
var url = "/daily_reports_chart.json";
var certname = $(el).attr('data-certname');
if (typeof certname !== typeof undefined && certname !== false) {
url = url + "?certname=" + certname;
}
d3.json(url, function(data) {
var chart = c3.generate({
bindto: '#dailyReportsChart',
data: {
type: 'bar',
json: data['result'],
keys: {
x: 'day',
value: ['failed', 'changed', 'unchanged'],
},
groups: [
['failed', 'changed', 'unchanged']
],
colors: { // Must match CSS colors
'failed':'#AA4643',
'changed':'#4572A7',
'unchanged':'#89A54E',
}
},
size: {
height: 160
},
axis: {
x: {
type: 'category'
}
}
});
});
}
generateChart($("#dailyReportsChart"));
});

View File

@@ -1 +0,0 @@
../jquery-2.1.1/jquery.min.map

View File

@@ -1,31 +1,42 @@
// Generated by CoffeeScript 1.4.0
// Generated by CoffeeScript 1.9.3
(function() {
var $;
var $, filter_list;
$ = jQuery;
$(function() {});
$('input.filter-list').parent('div').removeClass('hide');
$("input.filter-list").on("keyup", function(e) {
var ev, rex;
rex = new RegExp($(this).val(), "i");
filter_list = function(val) {
var rex;
rex = new RegExp(val, "i");
$(".searchable li").hide();
$(".searchable li").parent().parent().hide();
$(".searchable li").parent().parent('.list_hide_segment').hide();
$(".searchable li").filter(function() {
return rex.test($(this).text());
}).show();
$(".searchable li").filter(function() {
return $(".searchable li").filter(function() {
return rex.test($(this).text());
}).parent().parent().show();
};
$("input.filter-list").on("keyup", function(e) {
var ev;
if (e.keyCode === 27) {
$(e.currentTarget).val("");
ev = $.Event("keyup");
ev.keyCode = 13;
$(e.currentTarget).trigger(ev);
return e.currentTarget.blur();
} else {
return filter_list($(this).val());
}
});
$("input.filter-list").ready(function() {
var elem, val;
elem = $("input.filter-list");
elem.focus();
val = elem.val();
filter_list(val);
return elem.val('').val(val);
});
}).call(this);

View File

@@ -1,23 +1,19 @@
jQuery(function ($) {
var localise_timestamp = function(timestamp){
if (timestamp === "None"){
return '';
};
d = moment.utc(timestamp);
d.local();
return d;
};
$.fn.extend({
localise_timestamp: function (){
var tstring = $(this).text().trim();
if (tstring === "None"){
$(this).text('Unknown');
} else {
var result = moment(tstring).utc();
result.local();
$(this).text(result.format('MMM DD YYYY - HH:mm:ss'));
}
}
})
jQuery(function ($) {
$("[rel=utctimestamp]").each(
function(index, timestamp){
var tstamp = $(timestamp);
var tstring = tstamp.text().trim();
var result = localise_timestamp(tstring);
if (result == '') {
tstamp.text('Unknown');
} else {
tstamp.text(localise_timestamp(tstring).format('MMM DD YYYY - HH:mm:ss'));
};
$(this).localise_timestamp();
});
});

View File

@@ -1,11 +1,5 @@
{% extends 'layout.html' %}
{% block row_fluid %}
<div class="container" style="margin-bottom:55px;">
<div class="row">
<div class="span12">
<h2>Feature unavailable</h2>
<p>You've configured Puppetboard with an API version that does not support this feature.</p>
</div>
</div>
</div>
{% block content %}
<h1>Feature unavailable</h1>
<p>You've configured Puppetboard with an API version that does not support this feature.</p>
{% endblock %}

View File

@@ -45,126 +45,9 @@
</tbody>
</table>
{%- endmacro %}
{% macro facts_graph(facts, autofocus=False, condensed=False, show_node=False, margin_top=20, margin_bottom=20) -%}
<script src="{{url_for('static', filename='js/d3.min.js')}}"></script>
<script src="{{url_for('static', filename='js/c3.min.js')}}"></script>
<div id="factChart" width="300" height="300"></div>
<script type="text/javascript">
var data = [
{% for fact in facts|groupby('value') %}
{
label: '{{ fact.grouper.replace("\n", " ") }}',
value: {{ fact.list|length }}
},
{% endfor %}
{
value: 0,
}
]
var fact_values = data.map(function(item) { return [item.label, item.value]; }).filter(function(item){return item[0];}).sort(function(a,b){return b[1] - a[1];});
var realdata = fact_values.slice(0, 15);
var otherdata = fact_values.slice(15);
if (otherdata.length > 0) {
realdata.push(["other", otherdata.reduce(function(a,b){return a + b[1];},0)]);
}
var chart = c3.generate({
bindto: '#factChart',
data: {
columns: realdata,
type : 'pie',
}
});
</script>
{%- endmacro %}
{% macro reports_table(reports, reports_count, report_event_counts, current_env, condensed=False, hash_truncate=False, show_conf_col=True, show_agent_col=True, show_host_col=True, show_run_col=False, show_full_col=False, show_search_bar=False, searchable=False) -%}
{% if show_search_bar %}
<div class="ui fluid icon input hide" style="margin-bottom:20px">
<input autofocus="autofocus" class="filter-table" placeholder="Type here to filter...">
</div>
{% endif %}
<div class="ui info message">
Only showing {{reports_count}} reports sorted by Start Time.
</div>
<table class='ui very basic {% if condensed %}very compact{% endif %} table stackable sortable table'>
<thead>
<tr>
<th class="default-sort">Start time</th>
<th>Status</th>
{% if show_host_col %}
<th>Hostname</th>
{% endif %}
{% if show_run_col %}
<th>Run time</th>
{% endif %}
{% if show_full_col %}
<th>Full report</th>
{% endif %}
{% if show_conf_col %}
<th>Configuration version</th>
{% endif %}
{% if show_agent_col %}
<th>Agent version</th>
{% endif %}
<tr>
</thead>
<tbody {% if searchable %}class="searchable" {% endif %}>
{% for report in reports %}
{% if hash_truncate %}
{% set rep_hash = "%s&hellip;"|format(report.hash_[0:10])|safe %}
{% else %}
{% set rep_hash = report.hash_ %}
{% endif %}
{% if report.failed %}
<tr class="error">
{% else %}
<tr>
{% endif %}
<td rel="utctimestamp">{{report.start}}</td>
<td>
{% call status_counts(status=report.status, node_name=report.node, events=report_event_counts[report.hash_], report_hash=report.hash_, current_env=current_env) %}{% endcall %}
</td>
{% if show_host_col %}
<td><a href="{{url_for('node', env=current_env, node_name=report.node)}}">{{ report.node }}</a></td>
{% endif %}
{% if show_run_col %}
<td>{{report.run_time}}</td>
{% endif %}
{% if show_full_col %}
<td><a href="{{url_for('report', env=current_env, node_name=report.node, report_id=report.hash_)}}">{{rep_hash}}</a></td>
{% endif %}
{% if show_conf_col %}
<td>{{report.version}}</td>
{% endif %}
{% if show_agent_col %}
<td>{{report.agent_version}}</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
{%- endmacro %}
{% macro status_counts(caller, status, node_name, events, current_env, unreported_time=False, report_hash=False) -%}
<a class="ui
{% if status == 'failed' -%}
failed
{% elif status == 'changed' -%}
changed
{% elif status == 'unreported' -%}
unreported
{% elif status == 'noop' -%}
noop
{% elif status == 'unchanged' -%}
unchanged
{% endif -%}
label status" href="
{{url_for('report', env=current_env, node_name=node_name, report_id=report_hash)}}
">
{{ status|upper }}
</a>
<a class="ui {{status}} label status" href="{{url_for('report', env=current_env, node_name=node_name, report_id=report_hash)}}">{{ status|upper }}</a>
{% if status == 'unreported' %}
<span class="ui label status"> {{ unreported_time|upper }} </span>
{% else %}
@@ -173,26 +56,56 @@
{% if events['skips'] %}<span class="ui small count label skipped">{{events['skips']}}</span>{% else %}<span class="ui small count label">0</span>{% endif%}
{% endif %}
{%- endmacro %}
{% macro render_pagination(pagination) -%}
<div class="ui pagination menu">
{% if pagination.has_prev %}
<a class="item" href="{{url_for_field('page', 1)}}">&laquo; First</a>
<a class="item" href="{{url_for_field('page', pagination.page - 1)}}">Prev</a>
{% endif %}
{% for page in pagination.iter_pages() %}
{% if page %}
{% if page != pagination.page %}
<a class="item" href="{{url_for_field('page', page)}}">{{page}}</a>
{% else %}
<a class="active item">{{page}}</a>
{% endif %}
{% else %}
<a class="disabled item">...</a>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<a class="item" href="{{url_for_field('page', pagination.page + 1)}}">Next</a>
<a class="item" href="{{url_for_field('page', pagination.pages)}}">Last &raquo;</a>
{% endif %}
</div>
{% endmacro %}
{% macro datatable_init(table_html_id, ajax_url, default_length, length_selector, extra_options=None) -%}
// Init datatable
$.fn.dataTable.ext.errMode = 'throw';
var table = $('#{{ table_html_id }}').DataTable({
// Permit flow auto-readjust (responsive)
"autoWidth": false,
// Activate "processing" message
"processing": true,
// Activate Ajax mode
"serverSide": true,
// Responsive
"responsive": true,
// Defer rendering out of screen lines (JIT)
"deferRender": true,
// Data loading URL
"ajax": "{{ ajax_url }}",
// Paging options
"lengthMenu": {{ length_selector }},
"pageLength": {{ default_length }},
// Default sort
"order": [[ 0, "desc" ]],
// Custom options
{% if extra_options %}{% call extra_options() %}Callback to parent defined options{% endcall %}{% endif %}
});
table.on('draw.dt', function(){
$('#{{ table_html_id }} [rel=utctimestamp]').each(
function(index, timestamp){
$(this).localise_timestamp();
});
});
// Override Datatables search box events to delay Ajax call while writing
var searchWait = 0;
var searchWaitInterval;
$('.dataTables_filter input')
.unbind()
.bind('input', function(e){
var item = $(this);
searchWait = 0;
if(!searchWaitInterval) searchWaitInterval = setInterval(function(){
if(searchWait>=3){
clearInterval(searchWaitInterval);
searchWaitInterval = '';
searchTerm = $(item).val();
table.search(searchTerm).draw();
searchWait = 0;
}
searchWait++;
},80);
});
{%- endmacro %}

View File

@@ -7,7 +7,7 @@
<table class='ui very basic very compact table'>
<thead>
<tr>
<th>Hostname</th>
<th>Certname</th>
<th>Version</th>
<th>Transaction UUID</th>
<th>Code ID</th>

View File

@@ -8,7 +8,7 @@
<thead>
<tr>
<th></th>
<th>Hostname</th>
<th>Certname</th>
<th>Compile Time</th>
<th>Compare With</th>
</tr>

View File

@@ -1,10 +1,47 @@
{% extends 'layout.html' %}
{% import '_macros.html' as macros %}
{% block content %}
<h1>{{name}}{% if value %}/{{value}}{% endif %} ({{facts|length}})</h1>
{% block javascript %}
{% if render_graph %}
{{macros.facts_graph(facts, autofocus=True, show_node=True, margin_bottom=10)}}
var chart = null;
var data = [
{% for fact in facts|groupby('value') %}
{
label: '{{ fact.grouper.replace("\n", " ") }}',
value: {{ fact.list|length }}
},
{% endfor %}
{
value: 0,
}
]
var fact_values = data.map(function(item) { return [item.label, item.value]; }).filter(function(item){return item[0];}).sort(function(a,b){return b[1] - a[1];});
var realdata = fact_values.slice(0, 15);
var otherdata = fact_values.slice(15);
if (otherdata.length > 0) {
realdata.push(["other", otherdata.reduce(function(a,b){return a + b[1];},0)]);
}
{% endif %}
{% endblock javascript %}
{% block onload_script %}
$('table').tablesort();
{% if render_graph %}
chart = c3.generate({
bindto: '#factChart',
data: {
columns: realdata,
type : '{{config.GRAPH_TYPE|default('pie')}}',
}
});
{% endif %}
{% endblock onload_script %}
{% block content %}
<div id="factChart" width="300" height="300"></div>
<h1>{{name}}{% if value %}/{{value}}{% endif %} ({{facts|length}})</h1>
{% if value %}
{{macros.facts_table(facts, current_env=current_env, autofocus=True, show_node=True, show_value=False, margin_bottom=10)}}
{% else %}

View File

@@ -1,29 +1,29 @@
{% extends 'layout.html' %}
{% block content %}
<div class="ui fluid icon input hide" style="margin-bottom:20px">
<div class="ui fluid icon input" style="margin-bottom:20px">
<input autofocus="autofocus" class="filter-list" placeholder="Type here to filter...">
</div>
<div class="ui searchable stackable doubling four column grid factlist">
<div class="column">
{%- set facts_count = 0 -%}
{%- set break = facts_len//4 + 1 -%}
{%- for key,facts_list in facts_dict %}
<div class="ui segment">
<a class="ui darkblue ribbon label">{{key}}</a>
<ul>
{%- for fact in facts_list %}
<li><a href="{{url_for('fact', env=current_env, fact=fact)}}">{{fact}}</a></li>
{%- endfor %}
</ul>
</div>
{%- set facts_count = facts_count + facts_list|length -%}
{%- if facts_count >= break -%}
</div>
<div class="column">
{%- set break = facts_len//4 + 1 + break -%}
{%- endif -%}
{%- set facts_count = facts_count + 5 -%}
{% endfor %}
</div>
<div class="column">
{%- set facts_count = 0 -%}
{%- set break = facts_len//4 + 1 -%}
{%- for key,facts_list in facts_dict %}
<div class="ui list_hide_segment segment">
<a class="ui darkblue ribbon label">{{key}}</a>
<ul>
{%- for fact in facts_list %}
<li><a href="{{url_for('fact', env=current_env, fact=fact)}}">{{fact}}</a></li>
{%- endfor %}
</ul>
</div>
{%- set facts_count = facts_count + facts_list|length -%}
{%- if facts_count >= break -%}
</div>
<div class="column">
{%- set break = facts_len//4 + 1 + break -%}
{%- endif -%}
{%- set facts_count = facts_count + 5 -%}
{% endfor %}
</div>
</div>
{% endblock content %}

View File

@@ -1,5 +1,18 @@
{% extends 'layout.html' %}
{% import '_macros.html' as macros %}
{% block head %}
{% if config.DAILY_REPORTS_CHART_ENABLED %}
<link href="{{ url_for('static', filename='css/c3.min.css') }}" rel="stylesheet">
{% endif %}
{% if config.DAILY_REPORTS_CHART_ENABLED %}
{% block script %}
<script src="{{url_for('static', filename='js/d3.min.js')}}"></script>
<script src="{{url_for('static', filename='js/c3.min.js')}}"></script>
<script src="{{url_for('static', filename='js/dailychart.js')}}"></script>
{% endblock script %}
{% endif %}
{% endblock head %}
{% block content %}
{% if config.REFRESH_RATE > 0 %}
<meta http-equiv="refresh" content="{{config.REFRESH_RATE}}">
@@ -59,6 +72,11 @@
<span>Avg. resources/node</span>
</div>
</div>
{% if config.DAILY_REPORTS_CHART_ENABLED %}
<div id="dailyReportsChartContainer" class="one column row">
<div id="dailyReportsChart"></div>
</div>
{% endif %}
<div class="ui divider"></div>
<div class="one column row">
<div class="column">
@@ -68,7 +86,7 @@
<thead>
<tr>
<th class="five wide">Status</th>
<th class="five wide">Hostname</th>
<th class="five wide">Certname</th>
<th class="five wide date default-sort">Report</th>
<th class="one wide"></th>
</tr>
@@ -96,7 +114,7 @@
</td>
<td>
{% if node.report_timestamp %}
<a title='Reports' href="{{url_for('reports_node', env=current_env, node_name=node.name)}}"><i class='large darkblue book icon'></i></a>
<a title='Reports' href="{{url_for('reports', env=current_env, node_name=node.name)}}"><i class='large darkblue book icon'></i></a>
{% endif %}
</td>
</tr>

View File

@@ -6,16 +6,16 @@
<table class='ui compact very basic sortable table'>
<thead>
<tr>
{% for description in fact_desc %}
<th>{{description}}</th>
{% for head in headers %}
<th{% if loop.index == 1 %} class="default-sort"{% endif %}>{{head}}</th>
{% endfor %}
</tr>
</thead>
<tbody class="searchable">
{% for nodename in nodedata %}
{% for node, facts in fact_data.iteritems() %}
<tr>
{% for item in nodedata[nodename] %}
<td>{{item}}</td>
{% for name in fact_names %}
<td><a href="{{url_for('node', env=current_env, node_name=node)}}">{{facts.get(name, 'undef')}}</a></td>
{% endfor %}
</tr>
{% endfor %}

View File

@@ -12,11 +12,50 @@
src: local('Open Sans'), local('OpenSans'), url({{ url_for('static', filename='fonts/Open_Sans.woff') }}) format('woff');
}
</style>
<link href='{{ url_for('static', filename='jquery-datatables-1.10.13/dataTables.semanticui.min.css') }}' rel='stylesheet' type='text/css'>
{% else %}
<link href='//fonts.googleapis.com/css?family=Open+Sans' rel='stylesheet' type='text/css'>
<link href='//fonts.googleapis.com/css?family=Open+Sans' rel='stylesheet' type='text/css' />
<link href='//cdnjs.cloudflare.com/ajax/libs/datatables/1.10.13/css/dataTables.semanticui.min.css' rel='stylesheet' type='text/css'>
{% endif %}
<link href="{{ url_for('static', filename='Semantic-UI-2.1.8/semantic.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/puppetboard.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='Semantic-UI-2.1.8/semantic.min.css') }}" rel="stylesheet" />
<link href="{{ url_for('static', filename='css/puppetboard.css') }}" rel="stylesheet" />
{% if config.OFFLINE_MODE %}
<script src="{{ url_for('static', filename='jquery-2.1.1/jquery.min.js') }}"></script>
<script src="{{ url_for('static', filename='jquery-datatables-1.10.13/jquery.dataTables.min.js') }}"></script>
<script src="{{ url_for('static', filename='jquery-datatables-1.10.13/dataTables.semanticui.min.js') }}"></script>
{% if config.LOCALISE_TIMESTAMP %}
<script src="{{ url_for('static', filename='moment.js-2.7.0/moment.min.js') }}"></script>
{% endif %}
{% else %}
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/datatables/1.10.13/js/jquery.dataTables.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/datatables/1.10.13/js/dataTables.semanticui.min.js"></script>
{% if config.LOCALISE_TIMESTAMP %}
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.7.0/moment.min.js"></script>
{% endif %}
{% endif %}
{% if config.LOCALISE_TIMESTAMP %}
<script src="{{ url_for('static', filename='js/timestamps.js')}}"></script>
{% endif %}
<script src="{{ url_for('static', filename='Semantic-UI-2.1.8/semantic.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/lists.js') }}"></script>
<script src="{{ url_for('static', filename='js/scroll.top.js') }}"></script>
<script src="{{url_for('static', filename='js/d3.min.js')}}"></script>
<script src="{{url_for('static', filename='js/c3.min.js')}}"></script>
<script src="{{url_for('static',
filename='jquery-tablesort-v.0.0.7/jquery.tablesort.min.js')}}"></script>
{% block script %} {% endblock script %}
<script type="text/javascript">
{% block javascript %} {% endblock javascript %}
$(document).ready(function(){
$(".ui.dropdown").dropdown();
$.getScript('{{url_for('static', filename='js/lists.js')}}')
$.getScript('{{url_for('static', filename='js/tables.js')}}')
{% block onload_script %} {% endblock onload_script %}
})
</script>
{% block head %} {% endblock head %}
</head>
<body>
@@ -39,7 +78,7 @@
href="{{ url_for(endpoint, env=current_env) }}">{{ caption }}</a>
{%- endfor %}
<div class="ui dropdown item">
Environments
{{current_env}}
<i class="dropdown icon"></i>
<div class="menu">
<a class="{% if '*' == current_env %}active {% endif %}item" href="{{url_for_field('env', '*')}}">All environments</a>
@@ -48,7 +87,7 @@
{% endfor %}
</div>
</div>
<div class="item right"><a href="https://github.com/voxpupuli/puppetboard" target="_blank">v0.2.0</a></div>
<div class="item right"><a href="https://github.com/voxpupuli/puppetboard" target="_blank">v0.2.1</a></div>
</div>
<div class="ui grid padding-bottom">
<div class="one wide column"></div>
@@ -67,31 +106,5 @@
Copyright &copy; 2013-{{ now('%Y') }} <a href="https://github.com/voxpupuli" target="_blank">Puppet Community</a>. <span style="float:right">Live from PuppetDB.</span>
</div>
</footer>
{% if config.OFFLINE_MODE %}
<script src="{{ url_for('static', filename='jquery-2.1.1/jquery.min.js') }}"></script>
{% if config.LOCALISE_TIMESTAMP %}
<script src="{{ url_for('static', filename='moment.js-2.7.0/moment.min.js') }}"></script>
{% endif %}
{% else %}
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
{% if config.LOCALISE_TIMESTAMP %}
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.7.0/moment.min.js"></script>
{% endif %}
{% endif %}
{% if config.LOCALISE_TIMESTAMP %}
<script src="{{ url_for('static', filename='js/timestamps.js')}}"></script>
{% endif %}
<script src="{{ url_for('static', filename='Semantic-UI-2.1.8/semantic.min.js') }}"></script>
<script src="{{ url_for('static', filename='jquery-tablesort-v.0.0.7/jquery.tablesort.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/lists.js') }}"></script>
<script src="{{ url_for('static', filename='js/tables.js') }}"></script>
<script src="{{ url_for('static', filename='js/scroll.top.js') }}"></script>
<script type="text/javascript">
$(".ui.dropdown").dropdown();
$('table').tablesort();
</script>
{% block script %} {% endblock script %}
</body>
</html>

View File

@@ -1,9 +1,14 @@
{% extends 'layout.html' %}
{% block content %}
<h1>Metrics</h1>
<ul>
<div class="ui fluid icon input" style="margin-bottom:20px">
<input autofocus="autofocus" class="filter-list" placeholder="Type here to filter...">
</div>
<ul class="ui list searchable">
{% for metric in metrics %}
<li><a href="{{url_for('metric', env=current_env, metric=metric)}}">{{metric}}</li>
<li>
<a href="{{url_for('metric', env=current_env, metric=metric)}}">{{metric}}</a>
</li>
{% endfor %}
</ul>
{% endblock content %}

View File

@@ -1,5 +1,25 @@
{% extends 'layout.html' %}
{% import '_macros.html' as macros %}
{% block head %}
{% if config.DAILY_REPORTS_CHART_ENABLED %}
<link href="{{ url_for('static', filename='css/c3.min.css') }}" rel="stylesheet" />
{% endif %}
{% block script %}
{% if config.DAILY_REPORTS_CHART_ENABLED %}
<script src="{{url_for('static', filename='js/d3.min.js')}}"></script>
<script src="{{url_for('static', filename='js/c3.min.js')}}"></script>
<script src="{{url_for('static', filename='js/dailychart.js')}}"></script>
{% endif %}
{% endblock script %}
{% endblock head %}
{% block onload_script %}
{% macro extra_options(caller) %}
'pagingType': 'simple',
"bFilter": false,
{% endmacro %}
{{ macros.datatable_init(table_html_id="reports_table", ajax_url=url_for('reports_ajax', env=current_env, node_name=node.name), default_length=config.LITTLE_TABLE_COUNT, length_selector=config.TABLE_COUNT_SELECTOR, extra_options=extra_options) }}
{% endblock onload_script %}
{% block content %}
<div class='ui two column grid'>
<div class='column'>
@@ -8,7 +28,7 @@
<table class="ui very basic very compact table">
<tbody>
<tr>
<th>Hostname</th>
<th>Certname</th>
<td style="word-wrap:break-word"><b>{{node.name}}</b></td>
</tr>
<tr>
@@ -28,8 +48,23 @@
</div>
<div class='row'>
<h1>Reports</h1>
{{ macros.reports_table(reports, reports_count, report_event_counts, condensed=True, hash_truncate=True, show_conf_col=False, show_agent_col=False, show_host_col=False, current_env=current_env)}}
<a href="{{url_for('reports_node', node_name=node.name)}}">Show All</a>
{% if config.DAILY_REPORTS_CHART_ENABLED %}
<div id="dailyReportsChartContainer" class="one column row">
<div id="dailyReportsChart" data-certname="{{node.name}}"></div>
</div>
{% endif %}
<table id="reports_table" class='ui very basic very condensed table stackable'>
<thead>
<tr>
{% for column in columns %}
<th>{{ column.name }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
</tbody>
</table>
<a href="{{url_for('reports', env=current_env, node_name=node.name)}}">Show All</a>
</div>
</div>
<div class='column'>

View File

@@ -8,7 +8,7 @@
<thead>
<tr>
<th>Status</th>
<th class="default">Hostname</th>
<th class="default">Certname</th>
<th class="date default-sort">Catalog</th>
<th class="date">Report</th>
<th>&nbsp;</th>
@@ -35,11 +35,22 @@
</td>
<td>
{% if node.report_timestamp %}
<a title='Reports' href="{{url_for('reports_node', env=current_env, node_name=node.name, page=1)}}"><i class='large darkblue book icon'></i></a>
<a title='Reports' href="{{url_for('reports', env=current_env, node_name=node.name)}}"><i class='large darkblue book icon'></i></a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="ui dropdown" style="float:right;">
<div class="text">Filter By</div>
<i class="dropdown icon"></i>
<div class="menu">
<a class="item" href="{{url_for_field('status', 'failed')}}">Failed</a>
<a class="item" href="{{url_for_field('status', 'changed')}}">Changed</a>
<a class="item" href="{{url_for_field('status', 'unchanged')}}">Unchanged</a>
<a class="item" href="{{url_for_field('status', 'noop')}}">Noop</a>
<a class="item" href="{{url_for_field('status', 'unreported')}}">Unreported</a>
</div>
</div>
{% endblock content %}

View File

@@ -6,12 +6,16 @@
<meta http-equiv='refresh' content="{{config.REFRESH_RATE}}"/>
{% endif %}
<link href="{{ url_for('static', filename='css/radiator.css')}}" media="screen" rel="stylesheet" type="text/css" />
<script src="{{ url_for('static', filename='js/jquery.min.js')}}" type="text/javascript"></script>
{% if config.OFFLINE_MODE %}
<script src="{{ url_for('static', filename='jquery-2.1.1/jquery.min.js') }}"></script>
{% else %}
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
{% endif %}
<script src="{{ url_for('static', filename='js/radiator.js')}}" type="text/javascript"></script>
</head>
<body class='radiator_controller radiator_index_action no-sidebar'>
<table class='node_summary'>
<tr class='failed '>
<tr class='failed'>
<td class='count_column'>
<span class='count'>{{stats['failed']}}</span>
</td>
@@ -26,7 +30,7 @@
</div>
</td>
</tr>
<tr class='unreported '>
<tr class='unreported'>
<td class='count_column'>
<span class='count'>{{stats['unreported']}}</span>
</td>
@@ -41,7 +45,7 @@
</div>
</td>
</tr>
<tr class='noop '>
<tr class='noop'>
<td class='count_column'>
<span class='count'>{{stats['noop']}}</span>
</td>
@@ -56,7 +60,7 @@
</div>
</td>
</tr>
<tr class='changed '>
<tr class='changed'>
<td class='count_column'>
<span class='count'>{{stats['changed']}}</span>
</td>
@@ -71,7 +75,7 @@
</div>
</td>
</tr>
<tr class='unchanged '>
<tr class='unchanged'>
<td class='count_column'>
<span class='count'>{{stats['unchanged']}}</span>
</td>
@@ -86,7 +90,7 @@
</div>
</td>
</tr>
<tr class='total '>
<tr class='total'>
<td class='count_column'>
<span class='count'>{{total}}</span>
</td>

View File

@@ -4,7 +4,7 @@
<table class='ui basic table'>
<thead>
<tr>
<th>Hostname</th>
<th>Certname</th>
<th>Configuration version</th>
<th>Start time</th>
<th>End time</th>

View File

@@ -1,17 +1,58 @@
{% extends 'layout.html' %}
{% import '_macros.html' as macros %}
{% block content %}
{{ macros.reports_table(reports, reports_count, report_event_counts, condensed=False, hash_truncate=False, show_conf_col=True, show_agent_col=True, show_host_col=True, show_search_bar=True, searchable=True, current_env=current_env)}}
{{ macros.render_pagination(pagination)}}
<div class="ui dropdown" style="float:right;">
<div class="text">Limit</div>
<i class="dropdown icon"></i>
<div class="menu">
<a class="{% if limit == config.REPORTS_COUNT %}active {% endif %}item" href="{{url_for_field('limit', config.REPORTS_COUNT)}}">{{config.REPORTS_COUNT}}</a>
<a class="{% if limit == 25 %}active {% endif %}item" href="{{url_for_field('limit', 25)}}">25</a>
<a class="{% if limit == 50 %}active {% endif %}item" href="{{url_for_field('limit', 50)}}">50</a>
<a class="{% if limit == 100 %}active {% endif %}item" href="{{url_for_field('limit', 100)}}">100</a>
<a class="{% if limit == '*' %}active {% endif %}item" href="{{url_for_field('limit', '*')}}">All</a>
<div class="ui wide grid">
<div class="wide row">
<div class="three wide column">
<div class="ui">Status</div>
</div>
{% for status in ['failed', 'changed', 'unchanged', 'noop'] %}
<div class="three wide column">
<div class="ui checked checkbox">
<input id="{{ status }}" checked="" type="checkbox">
<label>{{ status|capitalize }}</label>
</div>
</div>
{% endfor %}
</div>
</div>
<table id="reports_table" class='ui very basic table stackable'>
<thead>
<tr>
{% for column in columns %}
<th>{{ column.name }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
</tbody>
</table>
{% endblock content %}
{% block onload_script %}
{% macro extra_options(caller) %}
// No initial loading
"deferLoading": true,
{% endmacro %}
{{ macros.datatable_init(table_html_id="reports_table", ajax_url=url_for('reports_ajax', env=current_env, node_name=node_name), default_length=config.NORMAL_TABLE_COUNT, length_selector=config.TABLE_COUNT_SELECTOR, extra_options=extra_options) }}
// Event listener for status filters
function status_filter_change(){
var sum = '';
var failed = $('#failed').prop('checked');
var changed = $('#changed').prop('checked');
var unchanged = $('#unchanged').prop('checked');
var noop = $('#noop').prop('checked');
if ( failed && changed && unchanged && noop) { sum = '*'; }
else if (!(failed || changed || unchanged || noop)) { sum = 'none'; }
else {
if (failed) { sum += 'failed|'; }
if (changed) { sum += 'changed|'; }
if (unchanged) { sum += 'unchanged|'; }
if (noop) { sum += 'noop|'; }
}
table.column(1).search(sum).draw();
}
$('#failed, #changed, #unchanged, #noop').change(status_filter_change);
// Call at init - fix page reload behavior
status_filter_change();
{% endblock onload_script %}

View File

@@ -0,0 +1,29 @@
{%- import '_macros.html' as macros -%}
{
"draw": {{draw}},
"recordsTotal": {{total}},
"recordsFiltered": {{total_filtered}},
"data": [
{%- set report_flag = false -%}
{% for report in reports -%}
{%- if not loop.first %},{%- endif -%}
[
{%- set column_flag = false -%}
{%- for column in columns -%}
{%- if not loop.first %},{%- endif -%}
{%- if column.type == 'datetime' -%}
"<span rel=\"utctimestamp\">{{ report[column.attr] }}</span>"
{%- elif column.type == 'status' -%}
{% filter jsonprint -%}
{{ macros.status_counts(status=report.status, node_name=report.node, events=report_event_counts[report.hash_], report_hash=report.hash_, current_env=current_env) }}
{%- endfilter %}
{%- elif column.type == 'node' -%}
{% filter jsonprint %}<a href="{{url_for('node', env=current_env, node_name=report.node)}}">{{ report.node }}</a>{% endfilter %}
{%- else -%}
{{ report[column.attr] | jsonprint }}
{%- endif -%}
{%- endfor -%}
]
{% endfor %}
]
}

View File

@@ -19,40 +19,67 @@ except NameError:
log = logging.getLogger(__name__)
def jsonprint(value):
return json.dumps(value, indent=2, separators=(',', ': '))
def get_db_version(puppetdb):
'''
Get the version of puppetdb. Version form 3.2 query
interface is slightly different on mbeans
'''
ver = ()
try:
version = puppetdb.current_version()
(major, minor, build) = [int(x) for x in version.split('.')]
ver = (major, minor, build)
log.info("PuppetDB Version %d.%d.%d" % (major, minor, build))
except ValueError as e:
log.error("Unable to determine version from string: '%s'" % version)
ver = (4, 2, 0)
except HTTPError as e:
log.error(str(e))
except ConnectionError as e:
log.error(str(e))
except EmptyResponseError as e:
log.error(str(e))
return ver
def formatvalue(value):
if isinstance(value, str):
return value
return value
elif isinstance(value, list):
return ", ".join(value)
return ", ".join(value)
elif isinstance(value, dict):
ret = ""
for k in value:
ret += k+" => "+formatvalue(value[k])+",<br/>"
return ret
ret = ""
for k in value:
ret += k + " => " + formatvalue(value[k]) + ",<br/>"
return ret
else:
return str(value)
return str(value)
def prettyprint(value):
html = '<table class="ui basic fixed sortable table"><thead><tr>'
# Get keys
for k in value[0]:
html += "<th>"+k+"</th>"
html += "<th>" + k + "</th>"
html += "</tr></thead><tbody>"
for e in value:
html += "<tr>"
for k in e:
html += "<td>"+formatvalue(e[k])+"</td>"
html += "<td>" + formatvalue(e[k]) + "</td>"
html += "</tr>"
html += "</tbody></table>"
return(html)
def get_or_abort(func, *args, **kwargs):
"""Execute the function with its arguments and handle the possible
errors that might occur.
@@ -86,35 +113,3 @@ def yield_or_stop(generator):
raise
except (EmptyResponseError, ConnectionError, HTTPError):
raise StopIteration
class Pagination(object):
def __init__(self, page, per_page, total_count):
self.page = page
self.per_page = per_page
self.total_count = total_count
@property
def pages(self):
return int(ceil(self.total_count / float(self.per_page)))
@property
def has_prev(self):
return self.page > 1
@property
def has_next(self):
return self.page < self.pages
def iter_pages(self, left_edge=2, left_current=2,
right_current=5, right_edge=2):
last = 0
for num in xrange(1, self.pages + 1):
if num <= left_edge or \
(num > self.page - left_current - 1 and \
num < self.page + right_current) or \
num > self.pages - right_edge:
if last + 1 != num:
yield None
yield num
last = num

10
requirements-docker.txt Normal file
View File

@@ -0,0 +1,10 @@
gunicorn==19.6.0
Flask==0.10.1
Flask-WTF==0.12
Jinja2==2.7.2
MarkupSafe==0.19
WTForms==2.1
Werkzeug==0.11.10
itsdangerous==0.23
pypuppetdb==0.3.2
requests==2.6.0

11
requirements-test.txt Normal file
View File

@@ -0,0 +1,11 @@
pep8==1.6.2
coverage==4.0
mock==1.3.0
pytest==3.0.1
pytest-pep8==1.0.5
pytest-cov==2.2.1
pytest-mock==1.5.0
cov-core==1.15.0
unittest2==1.1.0; python_version < '2.7'
bandit
beautifulsoup4==4.5.3

View File

@@ -5,5 +5,5 @@ MarkupSafe==0.19
WTForms==2.1
Werkzeug==0.11.10
itsdangerous==0.23
pypuppetdb==0.3.1
pypuppetdb==0.3.2
requests==2.6.0

19
scripts/unpin.py Normal file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env python
import glob
import re
try:
import future.utils
except:
pass
for req_file in glob.glob('requirements*.txt'):
new_data = []
with open(req_file, 'r') as fp:
data = fp.readlines()
for line in data:
new_data.append(re.sub(r'==\d+(\.\d+){0,3}\s+$', '\n', line))
with open(req_file, 'w') as fp:
fp.writelines(new_data)

View File

@@ -6,3 +6,20 @@ build_requires = python-setuptools
requires = python-flask
python-flask-wtf
python-pypuppetdb
[pep8]
max-line-length=100
exclude=venv,dist,build
ignore=E402
[nosetests]
with-coverage = 1
with-xunit = 1
cover-package = puppetboard
[flake8]
exclude=venv
[tool:pytest]
addopts = --cov=puppetboard --cov-report=term-missing
norecursedirs = docs .tox venv
pep8ignore = E402

View File

@@ -9,7 +9,7 @@ if sys.argv[-1] == 'publish':
os.system('python setup.py sdist upload')
sys.exit()
VERSION = "0.2.0"
VERSION = "0.2.1"
with codecs.open('README.rst', encoding='utf-8') as f:
README = f.read()
@@ -28,12 +28,13 @@ setup(
description='Web frontend for PuppetDB',
include_package_data=True,
long_description='\n'.join((README, CHANGELOG)),
zip_safe=False,
install_requires=[
"Flask >= 0.10.1",
"Flask-WTF >= 0.12, <= 0.13",
"WTForms >= 2.0, < 3.0",
"pypuppetdb >= 0.3.0, < 0.4.0",
],
],
keywords="puppet puppetdb puppetboard",
classifiers=[
'Development Status :: 3 - Alpha',
@@ -49,5 +50,5 @@ setup(
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
],
],
)

0
test/__init__.py Normal file
View File

211719
test/data/test_json_report_ok Normal file

File diff suppressed because it is too large Load Diff

560
test/test_app.py Normal file
View File

@@ -0,0 +1,560 @@
import pytest
import json
import os
from datetime import datetime
from puppetboard import app
from pypuppetdb.types import Node, Report
from puppetboard import default_settings
from bs4 import BeautifulSoup
class MockDbQuery(object):
def __init__(self, responses):
self.responses = responses
def get(self, method, **kws):
resp = None
if method in self.responses:
resp = self.responses[method].pop(0)
if 'validate' in resp:
checks = resp['validate']['checks']
resp = resp['validate']['data']
for check in checks:
assert check in kws
expected_value = checks[check]
assert expected_value == kws[check]
return resp
@pytest.fixture
def mock_puppetdb_environments(mocker):
environemnts = [
{'name': 'production'},
{'name': 'staging'}
]
return mocker.patch.object(app.puppetdb, 'environments',
return_value=environemnts)
@pytest.fixture
def mock_puppetdb_default_nodes(mocker):
node_list = [
Node('_', 'node-unreported',
report_timestamp='2013-08-01T09:57:00.000Z',
latest_report_hash='1234567',
catalog_timestamp='2013-08-01T09:57:00.000Z',
facts_timestamp='2013-08-01T09:57:00.000Z',
status_report='unreported'),
Node('_', 'node-changed',
report_timestamp='2013-08-01T09:57:00.000Z',
latest_report_hash='1234567',
catalog_timestamp='2013-08-01T09:57:00.000Z',
facts_timestamp='2013-08-01T09:57:00.000Z',
status_report='changed'),
Node('_', 'node-failed',
report_timestamp='2013-08-01T09:57:00.000Z',
latest_report_hash='1234567',
catalog_timestamp='2013-08-01T09:57:00.000Z',
facts_timestamp='2013-08-01T09:57:00.000Z',
status_report='failed'),
Node('_', 'node-noop',
report_timestamp='2013-08-01T09:57:00.000Z',
latest_report_hash='1234567',
catalog_timestamp='2013-08-01T09:57:00.000Z',
facts_timestamp='2013-08-01T09:57:00.000Z',
status_report='noop'),
Node('_', 'node-unchanged',
report_timestamp='2013-08-01T09:57:00.000Z',
latest_report_hash='1234567',
catalog_timestamp='2013-08-01T09:57:00.000Z',
facts_timestamp='2013-08-01T09:57:00.000Z',
status_report='unchanged'),
Node('_', 'node-skipped',
report_timestamp='2013-08-01T09:57:00.000Z',
latest_report_hash='1234567',
catalog_timestamp='2013-08-01T09:57:00.000Z',
facts_timestamp='2013-08-01T09:57:00.000Z',
status_report='skipped')
]
return mocker.patch.object(app.puppetdb, 'nodes',
return_value=iter(node_list))
@pytest.fixture
def input_data(request):
data_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
'data')
data = None
with open('%s/%s' % (data_path, request.function.__name__), "r") as fp:
data = fp.read()
return data
@pytest.fixture
def client():
client = app.app.test_client()
return client
def test_first_test():
assert app is not None, ("%s" % reg.app)
def test_no_env(client, mock_puppetdb_environments):
rv = client.get('/nonexsistenv/')
assert rv.status_code == 404
def test_get_index(client, mocker,
mock_puppetdb_environments,
mock_puppetdb_default_nodes):
query_data = {
'nodes': [[{'count': 10}]],
'resources': [[{'count': 40}]],
}
dbquery = MockDbQuery(query_data)
mocker.patch.object(app.puppetdb, '_query', side_effect=dbquery.get)
rv = client.get('/')
soup = BeautifulSoup(rv.data, 'html.parser')
assert soup.title.contents[0] == 'Puppetboard'
assert rv.status_code == 200
def test_index_all(client, mocker,
mock_puppetdb_environments,
mock_puppetdb_default_nodes):
base_str = 'puppetlabs.puppetdb.population:'
query_data = {
'version': [{'version': '4.2.0'}],
'mbean': [
{
'validate': {
'data': {'Value': '50'},
'checks': {
'path': '%sname=num-nodes' % base_str
}
}
},
{
'validate': {
'data': {'Value': '60'},
'checks': {
'path': '%sname=num-resources' % base_str
}
}
},
{
'validate': {
'data': {'Value': 60.3},
'checks': {
'path': '%sname=avg-resources-per-node' % base_str
}
}
}
]
}
dbquery = MockDbQuery(query_data)
mocker.patch.object(app.puppetdb, '_query', side_effect=dbquery.get)
rv = client.get('/%2A/')
soup = BeautifulSoup(rv.data, 'html.parser')
assert soup.title.contents[0] == 'Puppetboard'
vals = soup.find_all('h1',
{"class": "ui header darkblue no-margin-bottom"})
assert len(vals) == 3
assert vals[0].string == '50'
assert vals[1].string == '60'
assert vals[2].string == ' 60'
assert rv.status_code == 200
def test_index_all_older_puppetdb(client, mocker,
mock_puppetdb_environments,
mock_puppetdb_default_nodes):
base_str = 'puppetlabs.puppetdb.population:type=default,'
query_data = {
'version': [{'version': '3.2.0'}],
'mbean': [
{
'validate': {
'data': {'Value': '50'},
'checks': {
'path': '%sname=num-nodes' % base_str
}
}
},
{
'validate': {
'data': {'Value': '60'},
'checks': {
'path': '%sname=num-resources' % base_str
}
}
},
{
'validate': {
'data': {'Value': 60.3},
'checks': {
'path': '%sname=avg-resources-per-node' % base_str
}
}
}
]
}
dbquery = MockDbQuery(query_data)
mocker.patch.object(app.puppetdb, '_query', side_effect=dbquery.get)
rv = client.get('/%2A/')
soup = BeautifulSoup(rv.data, 'html.parser')
assert soup.title.contents[0] == 'Puppetboard'
vals = soup.find_all('h1',
{"class": "ui header darkblue no-margin-bottom"})
assert len(vals) == 3
assert vals[0].string == '50'
assert vals[1].string == '60'
assert vals[2].string == ' 60'
assert rv.status_code == 200
def test_index_division_by_zero(client, mocker):
mock_puppetdb_environments(mocker)
mock_puppetdb_default_nodes(mocker)
query_data = {
'nodes': [[{'count': 0}]],
'resources': [[{'count': 40}]],
}
dbquery = MockDbQuery(query_data)
mocker.patch.object(app.puppetdb, '_query', side_effect=dbquery.get)
rv = client.get('/')
assert rv.status_code == 200
soup = BeautifulSoup(rv.data, 'html.parser')
assert soup.title.contents[0] == 'Puppetboard'
vals = soup.find_all('h1',
{"class": "ui header darkblue no-margin-bottom"})
assert len(vals) == 3
assert vals[2].string == '0'
def test_offline_mode(client, mocker):
app.app.config['OFFLINE_MODE'] = True
mock_puppetdb_environments(mocker)
mock_puppetdb_default_nodes(mocker)
query_data = {
'nodes': [[{'count': 10}]],
'resources': [[{'count': 40}]],
}
dbquery = MockDbQuery(query_data)
mocker.patch.object(app.puppetdb, '_query', side_effect=dbquery.get)
rv = client.get('/')
soup = BeautifulSoup(rv.data, 'html.parser')
assert soup.title.contents[0] == 'Puppetboard'
for link in soup.find_all('link'):
assert "//" not in link['href']
for script in soup.find_all('script'):
if "src" in script.attrs:
assert "//" not in script['src']
assert rv.status_code == 200
def test_default_node_view(client, mocker,
mock_puppetdb_environments,
mock_puppetdb_default_nodes):
rv = client.get('/nodes')
soup = BeautifulSoup(rv.data, 'html.parser')
assert soup.title.contents[0] == 'Puppetboard'
for label in ['failed', 'changed', 'unreported', 'noop']:
vals = soup.find_all('a',
{"class": "ui %s label status" % label})
assert len(vals) == 1
assert 'node-%s' % (label) in vals[0].attrs['href']
assert rv.status_code == 200
def test_radiator_view(client, mocker,
mock_puppetdb_environments,
mock_puppetdb_default_nodes):
query_data = {
'nodes': [[{'count': 10}]],
'resources': [[{'count': 40}]],
}
dbquery = MockDbQuery(query_data)
mocker.patch.object(app.puppetdb, '_query', side_effect=dbquery.get)
rv = client.get('/radiator')
assert rv.status_code == 200
soup = BeautifulSoup(rv.data, 'html.parser')
assert soup.title.contents[0] == 'Puppetboard'
assert soup.h1 != 'Not Found'
total = soup.find(class_='total')
assert '10' in total.text
def test_radiator_view_all(client, mocker,
mock_puppetdb_environments,
mock_puppetdb_default_nodes):
base_str = 'puppetlabs.puppetdb.population:'
query_data = {
'version': [{'version': '4.2.0'}],
'mbean': [
{
'validate': {
'data': {'Value': '50'},
'checks': {
'path': '%sname=num-nodes' % base_str
}
}
},
{
'validate': {
'data': {'Value': '60'},
'checks': {
'path': '%sname=num-resources' % base_str
}
}
},
{
'validate': {
'data': {'Value': 60.3},
'checks': {
'path': '%sname=avg-resources-per-node' % base_str
}
}
}
]
}
dbquery = MockDbQuery(query_data)
mocker.patch.object(app.puppetdb, '_query', side_effect=dbquery.get)
rv = client.get('/%2A/radiator')
assert rv.status_code == 200
soup = BeautifulSoup(rv.data, 'html.parser')
assert soup.title.contents[0] == 'Puppetboard'
assert soup.h1 != 'Not Found'
total = soup.find(class_='total')
assert '50' in total.text
def test_radiator_view_all_old_version(client, mocker,
mock_puppetdb_environments,
mock_puppetdb_default_nodes):
base_str = 'puppetlabs.puppetdb.population:type=default,'
query_data = {
'version': [{'version': '3.2.0'}],
'mbean': [
{
'validate': {
'data': {'Value': '50'},
'checks': {
'path': '%sname=num-nodes' % base_str
}
}
},
{
'validate': {
'data': {'Value': '60'},
'checks': {
'path': '%sname=num-resources' % base_str
}
}
},
{
'validate': {
'data': {'Value': 60.3},
'checks': {
'path': '%sname=avg-resources-per-node' % base_str
}
}
}
]
}
dbquery = MockDbQuery(query_data)
mocker.patch.object(app.puppetdb, '_query', side_effect=dbquery.get)
rv = client.get('/%2A/radiator')
assert rv.status_code == 200
soup = BeautifulSoup(rv.data, 'html.parser')
assert soup.title.contents[0] == 'Puppetboard'
assert soup.h1 != 'Not Found'
total = soup.find(class_='total')
assert '50' in total.text
def test_radiator_view_json(client, mocker,
mock_puppetdb_environments,
mock_puppetdb_default_nodes):
query_data = {
'nodes': [[{'count': 10}]],
'resources': [[{'count': 40}]],
}
dbquery = MockDbQuery(query_data)
mocker.patch.object(app.puppetdb, '_query', side_effect=dbquery.get)
rv = client.get('/radiator', headers={'Accept': 'application/json'})
assert rv.status_code == 200
json_data = json.loads(rv.data.decode('utf-8'))
assert json_data['unreported'] == 1
assert json_data['noop'] == 1
assert json_data['failed'] == 1
assert json_data['changed'] == 1
assert json_data['skipped'] == 1
assert json_data['unchanged'] == 1
def test_radiator_view_bad_env(client, mocker):
mock_puppetdb_environments(mocker)
mock_puppetdb_default_nodes(mocker)
query_data = {
'nodes': [[{'count': 10}]],
'resources': [[{'count': 40}]],
}
dbquery = MockDbQuery(query_data)
mocker.patch.object(app.puppetdb, '_query', side_effect=dbquery.get)
rv = client.get('/nothere/radiator')
assert rv.status_code == 404
soup = BeautifulSoup(rv.data, 'html.parser')
assert soup.title.contents[0] == 'Puppetboard'
assert soup.h1.text == 'Not Found'
def test_radiator_view_division_by_zero(client, mocker):
mock_puppetdb_environments(mocker)
mock_puppetdb_default_nodes(mocker)
query_data = {
'nodes': [[{'count': 0}]],
'resources': [[{'count': 40}]],
}
dbquery = MockDbQuery(query_data)
mocker.patch.object(app.puppetdb, '_query', side_effect=dbquery.get)
rv = client.get('/radiator')
assert rv.status_code == 200
soup = BeautifulSoup(rv.data, 'html.parser')
assert soup.title.contents[0] == 'Puppetboard'
total = soup.find(class_='total')
assert '0' in total.text
def test_json_report_ok(client, mocker, input_data):
mock_puppetdb_environments(mocker)
mock_puppetdb_default_nodes(mocker)
query_response = json.loads(input_data)
query_data = {
'reports': [
{
'validate': {
'data': query_response[:100],
'checks': {
'limit': 100,
'offset': 0
}
}
}
]
}
dbquery = MockDbQuery(query_data)
mocker.patch.object(app.puppetdb, '_query', side_effect=dbquery.get)
app.puppetdb.last_total = 499
rv = client.get('/reports/json')
assert rv.status_code == 200
result_json = json.loads(rv.data.decode('utf-8'))
assert 'data' in result_json
assert len(result_json['data']) == 100
def test_json_daily_reports_chart_ok(client, mocker):
mock_puppetdb_environments(mocker)
mock_puppetdb_default_nodes(mocker)
query_data = {
'reports': [
[{'status': 'changed', 'count': 1}]
for i in range(app.app.config['DAILY_REPORTS_CHART_DAYS'])
]
}
import logging
logging.error(query_data)
dbquery = MockDbQuery(query_data)
mocker.patch.object(app.puppetdb, '_query', side_effect=dbquery.get)
rv = client.get('/daily_reports_chart.json')
result_json = json.loads(rv.data.decode('utf-8'))
assert 'result' in result_json
assert (len(result_json['result']) ==
app.app.config['DAILY_REPORTS_CHART_DAYS'])
day_format = '%Y-%m-%d'
cur_day = datetime.strptime(result_json['result'][0]['day'], day_format)
for day in result_json['result'][1:]:
next_day = datetime.strptime(day['day'], day_format)
assert cur_day < next_day
cur_day = next_day
assert rv.status_code == 200

73
test/test_app_error.py Normal file
View File

@@ -0,0 +1,73 @@
import pytest
from flask import Flask, current_app
from puppetboard import app
from bs4 import BeautifulSoup
@pytest.fixture
def mock_puppetdb_environments(mocker):
environemnts = [
{'name': 'production'},
{'name': 'staging'}
]
return mocker.patch.object(app.puppetdb, 'environments',
return_value=environemnts)
def test_error_no_content():
result = app.no_content(None)
assert result[0] == ''
assert result[1] == 204
def test_error_bad_request(mock_puppetdb_environments):
with app.app.test_request_context():
(output, error_code) = app.bad_request(None)
soup = BeautifulSoup(output, 'html.parser')
assert 'The request sent to PuppetDB was invalid' in soup.p.text
assert error_code == 400
def test_error_forbidden(mock_puppetdb_environments):
with app.app.test_request_context():
(output, error_code) = app.forbidden(None)
soup = BeautifulSoup(output, 'html.parser')
long_string = "%s %s" % ('What you were looking for has',
'been disabled by the administrator')
assert long_string in soup.p.text
assert error_code == 403
def test_error_not_found(mock_puppetdb_environments):
with app.app.test_request_context():
(output, error_code) = app.not_found(None)
soup = BeautifulSoup(output, 'html.parser')
long_string = "%s %s" % ('What you were looking for could not',
'be found in PuppetDB.')
assert long_string in soup.p.text
assert error_code == 404
def test_error_precond(mock_puppetdb_environments):
with app.app.test_request_context():
(output, error_code) = app.precond_failed(None)
soup = BeautifulSoup(output, 'html.parser')
long_string = "%s %s" % ('You\'ve configured Puppetboard with an API',
'version that does not support this feature.')
assert long_string in soup.p.text
assert error_code == 412
def test_error_server(mock_puppetdb_environments):
with app.app.test_request_context():
(output, error_code) = app.server_error(None)
soup = BeautifulSoup(output, 'html.parser')
assert 'Internal Server Error' in soup.h2.text
assert error_code == 500

View File

@@ -0,0 +1,118 @@
import pytest
import os
from puppetboard import docker_settings
from puppetboard import app
try:
import future.utils
except:
pass
try:
from imp import reload as reload
except:
pass
@pytest.fixture(scope='function')
def cleanUpEnv(request):
for env_var in dir(docker_settings):
if (env_var.startswith('__') or env_var.startswith('_') or
env_var.islower()):
continue
if env_var in os.environ:
del os.environ[env_var]
reload(docker_settings)
return
def test_default_host_port(cleanUpEnv):
assert docker_settings.PUPPETDB_HOST == 'puppetdb'
assert docker_settings.PUPPETDB_PORT == 8080
def test_set_host_port(cleanUpEnv):
os.environ['PUPPETDB_HOST'] = 'puppetdb2'
os.environ['PUPPETDB_PORT'] = '9081'
reload(docker_settings)
assert docker_settings.PUPPETDB_HOST == 'puppetdb2'
assert docker_settings.PUPPETDB_PORT == 9081
def test_cert_true_test(cleanUpEnv):
os.environ['PUPPETDB_SSL_VERIFY'] = 'True'
reload(docker_settings)
assert docker_settings.PUPPETDB_SSL_VERIFY is True
os.environ['PUPPETDB_SSL_VERIFY'] = 'true'
reload(docker_settings)
assert docker_settings.PUPPETDB_SSL_VERIFY is True
def test_cert_false_test(cleanUpEnv):
os.environ['PUPPETDB_SSL_VERIFY'] = 'False'
reload(docker_settings)
assert docker_settings.PUPPETDB_SSL_VERIFY is False
os.environ['PUPPETDB_SSL_VERIFY'] = 'false'
reload(docker_settings)
assert docker_settings.PUPPETDB_SSL_VERIFY is False
def test_cert_path(cleanUpEnv):
ca_file = '/usr/ssl/path/ca.pem'
os.environ['PUPPETDB_SSL_VERIFY'] = ca_file
reload(docker_settings)
assert docker_settings.PUPPETDB_SSL_VERIFY == ca_file
def validate_facts(facts):
assert isinstance(facts, list)
assert len(facts) > 0
for map in facts:
assert isinstance(map, tuple)
assert len(map) == 2
def test_inventory_facts_default(cleanUpEnv):
validate_facts(docker_settings.INVENTORY_FACTS)
def test_invtory_facts_custom(cleanUpEnv):
os.environ['INVENTORY_FACTS'] = "A, B, C, D"
reload(docker_settings)
validate_facts(docker_settings.INVENTORY_FACTS)
def test_graph_facts_defautl(cleanUpEnv):
facts = docker_settings.GRAPH_FACTS
assert isinstance(facts, list)
assert 'puppetversion' in facts
def test_graph_facts_custom(cleanUpEnv):
os.environ['GRAPH_FACTS'] = "architecture, puppetversion, extra"
reload(docker_settings)
facts = docker_settings.GRAPH_FACTS
assert isinstance(facts, list)
assert len(facts) == 3
assert 'puppetversion' in facts
assert 'architecture' in facts
assert 'extra' in facts
def test_bad_log_value(cleanUpEnv):
os.environ['LOGLEVEL'] = 'g'
os.environ['PUPPETBOARD_SETTINGS'] = '../puppetboard/docker_settings.py'
reload(docker_settings)
with pytest.raises(ValueError) as error:
reload(app)
def test_default_table_selctor(cleanUpEnv):
assert [10, 20, 50, 100, 500] == docker_settings.TABLE_COUNT_SELECTOR
def test_env_table_selector(cleanUpEnv):
os.environ['TABLE_COUNT_SELECTOR'] = '5,15,25'
reload(docker_settings)
assert [5, 15, 25] == docker_settings.TABLE_COUNT_SELECTOR

235
test/test_utils.py Normal file
View File

@@ -0,0 +1,235 @@
import pytest
import sys
import json
import mock
from types import GeneratorType
from requests.exceptions import HTTPError, ConnectionError
from pypuppetdb.errors import EmptyResponseError
from requests import Response
from werkzeug.exceptions import NotFound, InternalServerError
from puppetboard import utils
from puppetboard import app
from puppetboard.app import NoContent
from bs4 import BeautifulSoup
import logging
def test_json_format():
demo = [{'foo': 'bar'}, {'bar': 'foo'}]
sample = json.dumps(demo, indent=2, separators=(',', ': '))
assert sample == utils.jsonprint(demo), "Json formatting has changed"
def test_format_val_str():
x = "some string"
assert x == utils.formatvalue(x), "Should return same value"
def test_format_val_array():
x = ['a', 'b', 'c']
assert "a, b, c" == utils.formatvalue(x)
def test_format_val_dict_one_layer():
x = {'a': 'b'}
assert "a => b,<br/>" == utils.formatvalue(x)
def test_format_val_tuple():
x = ('a', 'b')
assert str(x) == utils.formatvalue(x)
def test_get():
x = "hello world"
def test_get_or_abort():
return x
assert x == utils.get_or_abort(test_get_or_abort)
def test_pretty_print():
test_data = [{'hello': 'world'}]
html = utils.prettyprint(test_data)
soup = BeautifulSoup(html, 'html.parser')
assert soup.th.text == 'hello'
@pytest.fixture
def mock_log(mocker):
return mocker.patch('logging.log')
@pytest.fixture
def mock_info_log(mocker):
logger = logging.getLogger('puppetboard.utils')
return mocker.patch.object(logger, 'info')
@pytest.fixture
def mock_err_log(mocker):
logger = logging.getLogger('puppetboard.utils')
return mocker.patch.object(logger, 'error')
def test_http_error(mock_log):
err = "NotFound"
def raise_http_error():
x = Response()
x.status_code = 404
x.reason = err
raise HTTPError(err, response=x)
with pytest.raises(NotFound):
utils.get_or_abort(raise_http_error)
mock_log.error.assert_called_once_with(err)
def test_http_connection_error(mock_log):
err = "ConnectionError"
def connection_error():
x = Response()
x.status_code = 500
x.reason = err
raise ConnectionError(err, response=x)
with pytest.raises(InternalServerError):
utils.get_or_abort(connection_error)
mock_log.error.assert_called_with(err)
def test_http_empty(mock_log, mocker):
err = "Empty Response"
def connection_error():
raise EmptyResponseError(err)
flask_abort = mocker.patch('flask.abort')
with pytest.raises(NoContent):
utils.get_or_abort(connection_error)
mock_log.error.assert_called_with(err)
flask_abort.assert_called_with('204')
def test_db_version_good(mocker, mock_info_log):
mocker.patch.object(app.puppetdb, 'current_version', return_value='4.2.0')
err = 'PuppetDB Version %d.%d.%d' % (4, 2, 0)
result = utils.get_db_version(app.puppetdb)
mock_info_log.assert_called_with(err)
assert (4, 0, 0) < result
assert (4, 2, 0) == result
assert (3, 2, 0) < result
assert (4, 3, 0) > result
assert (5, 0, 0) > result
assert (4, 2, 1) > result
def test_db_invalid_version(mocker, mock_err_log):
mocker.patch.object(app.puppetdb, 'current_version', return_value='4')
err = u"Unable to determine version from string: '%s'" % (4)
result = utils.get_db_version(app.puppetdb)
mock_err_log.assert_called_with(err)
assert (4, 0, 0) < result
assert (4, 2, 0) == result
def test_db_http_error(mocker, mock_err_log):
err = "NotFound"
def raise_http_error():
x = Response()
x.status_code = 404
x.reason = err
raise HTTPError(err, response=x)
mocker.patch.object(app.puppetdb, 'current_version',
side_effect=raise_http_error)
result = utils.get_db_version(app.puppetdb)
mock_err_log.assert_called_with(err)
assert result == ()
def test_db_connection_error(mocker, mock_err_log):
err = "ConnectionError"
def connection_error():
x = Response()
x.status_code = 500
x.reason = err
raise ConnectionError(err, response=x)
mocker.patch.object(app.puppetdb, 'current_version',
side_effect=connection_error)
result = utils.get_db_version(app.puppetdb)
mock_err_log.assert_called_with(err)
assert result == ()
def test_db_empty_response(mocker, mock_err_log):
err = "Empty Response"
def connection_error():
raise EmptyResponseError(err)
mocker.patch.object(app.puppetdb, 'current_version',
side_effect=connection_error)
result = utils.get_db_version(app.puppetdb)
mock_err_log.assert_called_with(err)
assert result == ()
def test_iter():
test_list = (0, 1, 2, 3)
def my_generator():
for i in test_list:
yield i
gen = utils.yield_or_stop(my_generator())
assert isinstance(gen, GeneratorType)
i = 0
for val in gen:
assert i == val
i = i + 1
def test_stop_empty():
def my_generator():
yield 1
raise EmptyResponseError
yield 2
gen = utils.yield_or_stop(my_generator())
for val in gen:
assert 1 == val
def test_stop_conn_error():
def my_generator():
yield 1
raise ConnectionError
yield 2
gen = utils.yield_or_stop(my_generator())
for val in gen:
assert 1 == val
def test_stop_http_error():
def my_generator():
yield 1
raise HTTPError
yield 2
gen = utils.yield_or_stop(my_generator())
for val in gen:
assert 1 == val