├── tipboard ├── tests │ ├── __init__.py │ ├── frontend │ │ └── lib │ │ │ └── jasmine-2.0.0 │ │ │ ├── jasmine_favicon.png │ │ │ ├── jasmine.css │ │ │ ├── console.js │ │ │ └── boot.js │ ├── test_console_commands.py │ ├── test_config_parser.py │ └── test_rest_api.py ├── tiles │ ├── empty.css │ ├── bar_chart.css │ ├── empty.html │ ├── listing.css │ ├── text.html │ ├── listing.html │ ├── bar_chart.html │ ├── cumulative_flow.css │ ├── line_chart.html │ ├── pie_chart.html │ ├── norm_chart.html │ ├── advanced_plot.html │ ├── cumulative_flow.html │ ├── just_value.html │ ├── fancy_listing.html │ ├── simple_percentage.css │ ├── simple_percentage.js │ ├── fancy_listing.css │ ├── big_value.css │ ├── text.js │ ├── just_value.css │ ├── listing.js │ ├── simple_percentage.html │ ├── just_value.js │ ├── big_value.js │ ├── advanced_plot.js │ ├── big_value.html │ ├── pie_chart.js │ ├── bar_chart.js │ ├── fancy_listing.js │ ├── cumulative_flow.js │ ├── norm_chart.js │ └── line_chart.js ├── static │ ├── fonts │ │ ├── miso-webfont.eot │ │ ├── miso-webfont.ttf │ │ ├── miso-webfont.woff │ │ ├── miso-light-webfont.eot │ │ ├── miso-light-webfont.ttf │ │ ├── miso-light-webfont.woff │ │ └── MISO_LICENSE │ ├── css │ │ ├── reset.css │ │ └── jquery.jqplot.css │ └── js │ │ ├── lib │ │ ├── jqplot │ │ │ └── plugins │ │ │ │ ├── jqplot.mobile.js │ │ │ │ └── jqplot.ciParser.js │ │ ├── html5shiv.js │ │ ├── simplify.js │ │ └── jquery.fullscreen.js │ │ └── flipboard.js ├── extras │ ├── README.rst │ ├── fabfile.py │ ├── client_code_example.py │ └── jira-ds.py ├── defaults │ ├── settings-local.py │ └── layout_config.yaml ├── __init__.py ├── templates │ ├── flipboard.html │ ├── base.html │ └── layout.html ├── redis_utils.py ├── parser.py ├── settings.py └── api.py ├── doc ├── changelog.rst ├── img │ ├── text.png │ ├── listing.png │ ├── bar-chart.png │ ├── big-value.png │ ├── just-value.png │ ├── line-chart.png │ ├── norm-chart.png │ ├── pie-chart.png │ ├── advanced-plot.png │ ├── fancy-listing.png │ ├── smaller │ │ ├── text.png │ │ ├── listing.png │ │ ├── bar-chart.png │ │ ├── big-value.png │ │ ├── just-value.png │ │ ├── line-chart.png │ │ ├── norm-chart.png │ │ ├── pie-chart.png │ │ ├── advanced-plot.png │ │ ├── fancy-listing.png │ │ ├── cumulative-flow.png │ │ └── simple-percentage.png │ ├── cumulative-flow.png │ └── simple-percentage.png ├── license.rst ├── index.rst ├── tile__listing.rst ├── overview.rst ├── tile__text.rst ├── tile__just_value.rst ├── tile__pie_chart.rst ├── tile__bar_chart.rst ├── tile__norm_chart.rst ├── extras.rst ├── tile__cumulative_flow.rst ├── tile__simple_percentage.rst ├── tile__fancy_listing.rst ├── tile__big_value.rst ├── installation.rst ├── tile__line_chart.rst ├── tiles.rst ├── api.rst ├── tile__advanced_plot.rst └── Makefile ├── .gitignore ├── requirements.txt ├── MANIFEST.in ├── .travis.yml ├── LICENSE.rst ├── setup.py ├── CHANGES.rst └── README.rst /tipboard/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGES.rst 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tipboard.egg-info/ 2 | /.idea/* 3 | *.pyc 4 | *.swp 5 | -------------------------------------------------------------------------------- /tipboard/tiles/empty.css: -------------------------------------------------------------------------------- 1 | .tile-empty h2 { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /tipboard/tiles/bar_chart.css: -------------------------------------------------------------------------------- 1 | .bar-chart .jqplot-point-label {color: #fff;} 2 | -------------------------------------------------------------------------------- /doc/img/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/doc/img/text.png -------------------------------------------------------------------------------- /doc/img/listing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/doc/img/listing.png -------------------------------------------------------------------------------- /doc/img/bar-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/doc/img/bar-chart.png -------------------------------------------------------------------------------- /doc/img/big-value.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/doc/img/big-value.png -------------------------------------------------------------------------------- /doc/img/just-value.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/doc/img/just-value.png -------------------------------------------------------------------------------- /doc/img/line-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/doc/img/line-chart.png -------------------------------------------------------------------------------- /doc/img/norm-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/doc/img/norm-chart.png -------------------------------------------------------------------------------- /doc/img/pie-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/doc/img/pie-chart.png -------------------------------------------------------------------------------- /doc/license.rst: -------------------------------------------------------------------------------- 1 | License 2 | _______ 3 | 4 | .. include:: ../LICENSE.rst 5 | :literal: 6 | -------------------------------------------------------------------------------- /doc/img/advanced-plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/doc/img/advanced-plot.png -------------------------------------------------------------------------------- /doc/img/fancy-listing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/doc/img/fancy-listing.png -------------------------------------------------------------------------------- /doc/img/smaller/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/doc/img/smaller/text.png -------------------------------------------------------------------------------- /doc/img/cumulative-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/doc/img/cumulative-flow.png -------------------------------------------------------------------------------- /doc/img/smaller/listing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/doc/img/smaller/listing.png -------------------------------------------------------------------------------- /doc/img/simple-percentage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/doc/img/simple-percentage.png -------------------------------------------------------------------------------- /doc/img/smaller/bar-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/doc/img/smaller/bar-chart.png -------------------------------------------------------------------------------- /doc/img/smaller/big-value.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/doc/img/smaller/big-value.png -------------------------------------------------------------------------------- /doc/img/smaller/just-value.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/doc/img/smaller/just-value.png -------------------------------------------------------------------------------- /doc/img/smaller/line-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/doc/img/smaller/line-chart.png -------------------------------------------------------------------------------- /doc/img/smaller/norm-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/doc/img/smaller/norm-chart.png -------------------------------------------------------------------------------- /doc/img/smaller/pie-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/doc/img/smaller/pie-chart.png -------------------------------------------------------------------------------- /doc/img/smaller/advanced-plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/doc/img/smaller/advanced-plot.png -------------------------------------------------------------------------------- /doc/img/smaller/fancy-listing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/doc/img/smaller/fancy-listing.png -------------------------------------------------------------------------------- /doc/img/smaller/cumulative-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/doc/img/smaller/cumulative-flow.png -------------------------------------------------------------------------------- /doc/img/smaller/simple-percentage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/doc/img/smaller/simple-percentage.png -------------------------------------------------------------------------------- /tipboard/static/fonts/miso-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/tipboard/static/fonts/miso-webfont.eot -------------------------------------------------------------------------------- /tipboard/static/fonts/miso-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/tipboard/static/fonts/miso-webfont.ttf -------------------------------------------------------------------------------- /tipboard/static/fonts/miso-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/tipboard/static/fonts/miso-webfont.woff -------------------------------------------------------------------------------- /tipboard/static/fonts/miso-light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/tipboard/static/fonts/miso-light-webfont.eot -------------------------------------------------------------------------------- /tipboard/static/fonts/miso-light-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/tipboard/static/fonts/miso-light-webfont.ttf -------------------------------------------------------------------------------- /tipboard/static/fonts/miso-light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/tipboard/static/fonts/miso-light-webfont.woff -------------------------------------------------------------------------------- /tipboard/extras/README.rst: -------------------------------------------------------------------------------- 1 | Extras 2 | ------ 3 | 4 | For the detailed description of the contents of this dir, see relevant section 5 | in our documentation. 6 | -------------------------------------------------------------------------------- /tipboard/tests/frontend/lib/jasmine-2.0.0/jasmine_favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/tipboard/HEAD/tipboard/tests/frontend/lib/jasmine-2.0.0/jasmine_favicon.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mock==1.0.1 2 | PyYAML==3.10 3 | redis==2.7.5 4 | requests==1.2.3 5 | six>=1.4.1 6 | tornado-redis==2.4.2 7 | tornado==3.0.1 8 | docopt==0.6.1 9 | raven==3.5.2 10 | Sphinx==1.2.2 11 | sphinxcontrib-httpdomain==1.3.0 12 | -------------------------------------------------------------------------------- /tipboard/defaults/settings-local.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import absolute_import 5 | from __future__ import division 6 | from __future__ import print_function 7 | from __future__ import unicode_literals 8 | 9 | -------------------------------------------------------------------------------- /tipboard/tiles/empty.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ title }}

4 |
5 |
6 |

This is an empty tile.

7 |
8 |
9 | -------------------------------------------------------------------------------- /tipboard/tiles/listing.css: -------------------------------------------------------------------------------- 1 | .listing { 2 | padding-top: 0.5em; 3 | padding-left: 0; 4 | } 5 | 6 | .listing li { 7 | font-size: 4rem; 8 | margin-bottom: 0.2em; 9 | } 10 | 11 | .listing li span { 12 | padding-left: 0.2em; 13 | } 14 | 15 | -------------------------------------------------------------------------------- /tipboard/tiles/text.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ title }}

4 |
5 |
6 | 7 |
8 |
9 | 10 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst CHANGES.rst LICENSE.rst requirements.txt 2 | recursive-include tipboard/static * 3 | recursive-include tipboard/templates * 4 | recursive-include tipboard/tiles * 5 | recursive-include tipboard/defaults * 6 | recursive-include tipboard/extras * 7 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Tipboard's documentation! 2 | ==================================== 3 | 4 | Contents: 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | overview 10 | installation 11 | configuration 12 | tiles 13 | api 14 | extras 15 | changelog 16 | license 17 | -------------------------------------------------------------------------------- /tipboard/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import absolute_import 5 | from __future__ import division 6 | from __future__ import print_function 7 | from __future__ import unicode_literals 8 | 9 | 10 | VERSION = (1, 4, 3) 11 | __version__ = '.'.join(str(num) for num in VERSION) 12 | -------------------------------------------------------------------------------- /tipboard/tiles/listing.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ title }}

4 |
5 |
6 | 11 |
12 |
13 | 14 | -------------------------------------------------------------------------------- /tipboard/tiles/bar_chart.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ title }}

4 |
5 |
6 |

7 |

8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /tipboard/tiles/cumulative_flow.css: -------------------------------------------------------------------------------- 1 | .tile > .tile-content > .cumulative-flow-chart { 2 | width: 70%; 3 | float: left; 4 | } 5 | 6 | .tile > .tile-content > .plot-labels { 7 | padding-left: 10px; 8 | margin-left: 70%; 9 | overflow: hidden; 10 | } 11 | 12 | .tile > .tile-content > .plot-labels h2 { 13 | font-size: 2.8rem; 14 | } 15 | 16 | .tile > .tile-content > .plot-labels .medium-result { 17 | font-size: 2rem; 18 | } 19 | -------------------------------------------------------------------------------- /tipboard/tiles/line_chart.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ title }}

4 |
5 |
6 |

7 |

8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /tipboard/tiles/pie_chart.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ title }}

4 |
5 |
6 |

7 |

8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /tipboard/tiles/norm_chart.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ title }}

4 |
5 |
6 |

7 |

8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /tipboard/templates/flipboard.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_title %}{{ page_title }}{% end %} 4 | 5 | {% block put_body %} 6 | 7 | 8 | 9 |
10 |

11 |
12 | 13 | 16 | 17 | 18 | 19 | {% end %} 20 | 21 | 22 | -------------------------------------------------------------------------------- /tipboard/tiles/advanced_plot.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ title }}

4 |
5 |
6 |

7 |
11 | 12 |
13 |
14 |
15 | -------------------------------------------------------------------------------- /tipboard/tiles/cumulative_flow.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ title }}

4 |
5 |
6 |
7 | 8 |
9 |

10 | 13 |
14 |
15 |
16 | 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors 5 | install: 6 | - pip install flake8 --use-mirrors 7 | - pip install . --use-mirrors 8 | before_script: 9 | - flake8 --statistics tipboard 10 | # command to run tests, e.g. python setup.py test 11 | script: 12 | - tipboard test test_config_parser 13 | - tipboard test test_console_commands 14 | - tipboard test test_rest_api.TestRestApi.test_01_api_key 15 | - tipboard test test_rest_api.TestRestApi.test_02_info_resource 16 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | Copyright 2013-2017 Allegro Group 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /tipboard/tiles/just_value.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ title }}

4 |
5 |
6 |

7 |

11 |
12 | 13 |
14 | 15 |
16 |
17 | -------------------------------------------------------------------------------- /tipboard/tiles/fancy_listing.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ title }}

4 |
5 |
6 |
7 |
8 |
9 | 10 |
11 |
12 |
13 |

14 |

15 |
16 |
17 |
18 |
19 | 20 | -------------------------------------------------------------------------------- /tipboard/tiles/simple_percentage.css: -------------------------------------------------------------------------------- 1 | .tile > .tile-content .highlight-footer { 2 | height: 30%; 3 | overflow: hidden; 4 | padding-top: 10%; 5 | position: relative; 6 | width: 100%; 7 | } 8 | 9 | .row_1_of_3 .tile > .tile-content .highlight-footer { 10 | height: 30%; 11 | overflow: hidden; 12 | padding-top: 2%; 13 | width: 100%; 14 | } 15 | 16 | .tile > .tile-content .result-row > .result-col { 17 | float: left; 18 | width: 40%; 19 | margin-left: 5%; 20 | margin-right: 0; 21 | } 22 | 23 | .tile > .tile-content .result-row > .with-right-border { 24 | } 25 | 26 | .tile.simple-percentage-tile .result-col * { 27 | white-space: nowrap; 28 | width: 100%; 29 | overflow: hidden; 30 | text-overflow: ellipsis; 31 | } 32 | -------------------------------------------------------------------------------- /tipboard/static/css/reset.css: -------------------------------------------------------------------------------- 1 | /* Eric Meyer's Reset CSS v2.0 - http://cssreset.com */ 2 | html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{border:0;font-size:100%;font:inherit;vertical-align:baseline;margin:0;padding:0}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:none}table{border-collapse:collapse;border-spacing:0} 3 | -------------------------------------------------------------------------------- /tipboard/tiles/simple_percentage.js: -------------------------------------------------------------------------------- 1 | /*jslint browser: true, devel: true*/ 2 | /*global $, WebSocket: false, Tipboard: false*/ 3 | 4 | "use strict"; 5 | 6 | function updateTileSimplePercentage(tileId, data, config) { 7 | Tipboard.Dashboard.setDataByKeys(tileId, data, 'all'); 8 | var highlighterNode = $('#' + tileId + '-big_value').parent(); 9 | Tipboard.DisplayUtils.applyHighlighterConfig( 10 | highlighterNode, config.big_value_color, config.fading_background 11 | ); 12 | var tile = Tipboard.Dashboard.id2node(tileId); 13 | // TODO: use rescaleTile instead 14 | Tipboard.TileDisplayDecorator.runAllDecorators(tile); 15 | Tipboard.DisplayUtils.expandLastChild($(tile).find('.tile-content')[0]); 16 | } 17 | 18 | Tipboard.Dashboard.registerUpdateFunction('simple_percentage', updateTileSimplePercentage); 19 | -------------------------------------------------------------------------------- /doc/tile__listing.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | ``listing`` 3 | =========== 4 | 5 | .. image:: img/smaller/listing.png 6 | 7 | **Description** 8 | 9 | Very simple tile for displaying list of entries (up to seven) of arbitrary 10 | content. For more sophisticated needs there is a ``fancy_listing`` tile. 11 | 12 | **Content** 13 | 14 | :: 15 | 16 | data = {"items": ["", "", ..., ""]} 17 | 18 | where: 19 | 20 | .. describe:: items 21 | 22 | List of items (entries) to display. 23 | 24 | Example:: 25 | 26 | curl http://localhost:7272/api/v0.1//push 27 | -X POST 28 | -d "tile=listing" 29 | -d "key=" 30 | -d "data={"items": ["Leader: 5", "Product Owner: 0", "Scrum Master: 3", "Developer: 0"]}" 31 | 32 | **Configuration** 33 | 34 | This tile does not offer any configuration options. 35 | -------------------------------------------------------------------------------- /tipboard/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block page_title %}Dashboard{% end %} 6 | 7 | {% for file in tipboard_css %} 8 | 9 | {% end %} 10 | {% for file in tipboard_js %} 11 | 12 | {% end %} 13 | 14 | 18 | 19 | {% block head %} {% end %} 20 | 21 | {% block put_body %} {% end %} 22 | 23 | -------------------------------------------------------------------------------- /doc/overview.rst: -------------------------------------------------------------------------------- 1 | Overview 2 | -------- 3 | 4 | Tipboard is a system for creating dashboards, written in JavaScript and Python. 5 | Its widgets ('tiles' in Tipboard's terminology) are completely separated from 6 | data sources, which provides great flexibility and relatively high degree of 7 | possible customizations. 8 | 9 | Because of its intended target (displaying various data and statistics in your 10 | office), it is optimized for larger screens. 11 | 12 | Similar projects: `Geckoboard `_, 13 | `Dashing `_. 14 | 15 | Project assumptions 16 | ~~~~~~~~~~~~~~~~~~~ 17 | 18 | #. Defining a dashboard layout (number of lines, columns, types of tiles etc.). 19 | #. Clear separation between tiles and data sources. 20 | #. Ability to create own tiles and scripts querying data sources (e.g. Jira, Bamboo, Confluence etc.). 21 | #. Feeding tiles via REST API. 22 | -------------------------------------------------------------------------------- /tipboard/tiles/fancy_listing.css: -------------------------------------------------------------------------------- 1 | .overflow-proof { 2 | /* 3 | testing class, it's a candidate to be used in each tile type 4 | */ 5 | overflow: hidden; 6 | } 7 | .tile.fancy-listing .fancy-listing-item { 8 | height: 10%; 9 | margin: 0 0 1%; 10 | overflow: hidden; 11 | width: 100%; 12 | } 13 | 14 | .tile.fancy-listing .fancy-listing-item:first-child { 15 | /* 16 | this element is cloned by js 17 | */ 18 | display: none; 19 | } 20 | 21 | .tile.fancy-listing .fancy-listing-item .float-wrapper{ 22 | float: left; 23 | height: 100%; 24 | overflow: hidden; 25 | width: 30%; 26 | } 27 | 28 | .tile.fancy-listing .fancy-listing-label { 29 | font-weight: bold; 30 | color: white; 31 | } 32 | 33 | .tile.fancy-listing .fancy-listing-label-inside { 34 | font-weight: 900; 35 | font-size: 1.5em; 36 | } 37 | 38 | .tile.fancy-listing .fancy-listing-def { 39 | float: right; 40 | width: 68%; 41 | } 42 | 43 | .tile.fancy-listing .fancy-listing-term { 44 | font-size: 1.5em; 45 | white-space: nowrap; 46 | } 47 | -------------------------------------------------------------------------------- /tipboard/tiles/big_value.css: -------------------------------------------------------------------------------- 1 | .tile.big_value_tile > .tile-content > .highlight-footer { 2 | clear: both; 3 | height: 30%; 4 | overflow: hidden; 5 | padding-top: 10%; 6 | width: 100%; 7 | } 8 | 9 | .row_1_of_3 .tile.big_value_tile > .tile-content .highlight-footer { 10 | height: 30%; 11 | overflow: hidden; 12 | padding-top: 3%; 13 | width: 100%; 14 | } 15 | 16 | .tile.big_value_tile > .tile-content > .result-row > .result-col { 17 | float: left; 18 | width: 45%; 19 | height: 15%; 20 | } 21 | 22 | .tile.big_value_tile > .tile-content > .result-row > .result-half-col { 23 | float: left; 24 | width: 45%; 25 | } 26 | 27 | .tile.big_value_tile > .tile-content > .result-row > .result-half-col { 28 | margin-left: 5%; 29 | overflow: hidden; 30 | white-space: nowrap; 31 | } 32 | 33 | .tile.big_value_tile > .tile-content > .result-row > .result-half-col > .result { 34 | font-size: 2.0rem; 35 | height: 15%; 36 | overflow: hidden; 37 | text-overflow: ellipsis; 38 | white-space: nowrap; 39 | width: 100%; 40 | } 41 | -------------------------------------------------------------------------------- /tipboard/tiles/text.js: -------------------------------------------------------------------------------- 1 | /*jslint browser: true, devel: true*/ 2 | /*global WebSocket: false, Tipboard: false*/ 3 | 4 | function updateTileText(id, data, meta, tipboard) { 5 | var tile = $("#" + id)[0]; 6 | var textSelector = 'span.text-container'; 7 | containers = $(tile).find(textSelector); 8 | if (containers.length != 1) { 9 | console.log('tile ' + tile + 'does not include ONE: ' + textSelector); 10 | } 11 | var nodeWithText = containers[0]; 12 | $(nodeWithText).html(data['text']); 13 | 14 | var textSelector = '#' + id + ' .text-container'; 15 | if (meta.font_size) { 16 | $(textSelector).css("font-size", meta.font_size); 17 | } 18 | if (meta.font_color) { 19 | $(textSelector).css( 20 | "color", Tipboard.DisplayUtils.replaceFromPalette(meta.font_color) 21 | ); 22 | } 23 | if (meta.font_weight) { 24 | $('.text-container').css("font-weight", meta.font_weight); 25 | } 26 | Tipboard.TileDisplayDecorator.runAllDecorators(tile); 27 | } 28 | Tipboard.Dashboard.updateFunctions['text'] = updateTileText; 29 | 30 | -------------------------------------------------------------------------------- /tipboard/tiles/just_value.css: -------------------------------------------------------------------------------- 1 | .tile.just-value-tile > .tile-content { 2 | overflow: hidden; 3 | white-space: nowrap; 4 | } 5 | .tile.just-value-tile > .tile-content > .just-value-div { 6 | height: 60%; 7 | width: 100%; 8 | 9 | /* Internet Explorer 10 */ 10 | display:-ms-flexbox; 11 | -ms-flex-pack:center; 12 | -ms-flex-align:center; 13 | /* Firefox */ 14 | display:-moz-box; 15 | -moz-box-pack:center; 16 | -moz-box-align:center; 17 | /* Safari, Opera, and Chrome */ 18 | display:-webkit-box; 19 | -webkit-box-pack:center; 20 | -webkit-box-align:center; 21 | /* W3C */ 22 | display:box; 23 | box-pack:center; 24 | box-align:center; 25 | } 26 | 27 | .tile.just-value-tile > .tile-content > .just-value-div > .just-value { 28 | font-size: 2.0em; 29 | overflow: hidden; 30 | text-align: center; 31 | white-space: nowrap; 32 | } 33 | 34 | .row_1_of_3 .tile.just-value-tile > .tile-content > .just-value-div > .just-value { 35 | font-size: 12.0rem; 36 | overflow: hidden; 37 | text-align: center; 38 | white-space: nowrap; 39 | } 40 | -------------------------------------------------------------------------------- /tipboard/static/fonts/MISO_LICENSE: -------------------------------------------------------------------------------- 1 | Mårten Nettelbladt - Miso License 2 | 3 | M M I SSS OOO 4 | MM MM I S S O O 5 | M M M M I S O O 6 | M M M I S O O 7 | M M I S O O 8 | M M I S S O O 9 | M M I SSS OOO 10 | ———————————————————- 11 | MISO is an architectural lettering font 12 | completed in 2006 by Mårten Nettelbladt. 13 | ———————————————————- 14 | MISO is available in three weights 15 | (Light, Regular, Bold) 16 | in TrueType format for Windows. 17 | ———————————————————- 18 | 19 | L I C E N S E I N F O R M A T I O N 20 | ———————————————————- 21 | You can use MISO for free. 22 | You can share MISO with your friends 23 | as long as you include this text file. 24 | You are NOT permitted to sell MISO. 25 | 26 | ———————————————————- 27 | If you have any comments about MISO 28 | please let me know! 29 | 30 | miso (a) omkrets.se 31 | ———————————————————- 32 | October 23rd 2006 33 | Mårten Nettelbladt 34 | Omkrets arkitektur 35 | 36 | June 24th 2007 37 | Some small adjustments 38 | 39 | November 27th 2008 40 | Open Type format added. 41 | Thanks to Torin Hill! 42 | -------------------------------------------------------------------------------- /tipboard/tiles/listing.js: -------------------------------------------------------------------------------- 1 | /*jslint browser: true, devel: true*/ 2 | /*global WebSocket: false, Tipboard: false*/ 3 | 4 | function updateTileListing(id, data, meta, tipboard) { 5 | var MAX_ITEMS = 7; 6 | var tile = $('#' + id)[0]; 7 | var container = $(tile).find('.listing')[0]; 8 | $(container).children().remove(); 9 | for (idx in data.items) { 10 | if (idx > MAX_ITEMS) { 11 | console.log([ 12 | 'ERROR: more then', 13 | MAX_ITEMS, 14 | 'items passed - RENDERING STOPPED' 15 | ].join(' ')) 16 | break; 17 | } 18 | var textContent = data.items[idx]; 19 | appendListingItem(container, textContent); 20 | } 21 | Tipboard.TileDisplayDecorator.runAllDecorators(tile); 22 | } 23 | 24 | Tipboard.Dashboard.registerUpdateFunction('listing', updateTileListing); 25 | 26 | 27 | function appendListingItem(container, itemText) { 28 | var htmlLabel = [ 29 | '
  • ', 30 | itemText, 31 | '
  • ' 32 | ].join('\n'); 33 | $(container).append(htmlLabel); 34 | } 35 | 36 | -------------------------------------------------------------------------------- /tipboard/tiles/simple_percentage.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |

    {{ title }}

    4 |
    5 |
    6 |

    7 |

    8 |
    9 | 10 |
    11 | 21 |
    22 |
    23 | -------------------------------------------------------------------------------- /tipboard/tiles/just_value.js: -------------------------------------------------------------------------------- 1 | /*jslint browser: true, devel: true*/ 2 | /*global WebSocket: false, Tipboard: false*/ 3 | 4 | function updateTileJustValue(tileId, data, config) { 5 | var tile = Tipboard.Dashboard.id2node(tileId); 6 | JustValue.setDataByKeys(tileId, data, 'all'); 7 | var highlighterNode = $('#' + tileId + '-just-value').parent(); 8 | Tipboard.DisplayUtils.applyHighlighterConfig( 9 | highlighterNode, config['just-value-color'], config.fading_background 10 | ); 11 | Tipboard.TileDisplayDecorator.runAllDecorators(tile); 12 | } 13 | 14 | Tipboard.Dashboard.registerUpdateFunction('just_value', updateTileJustValue); 15 | 16 | JustValue = { 17 | setDataByKeys: function(tileId, data, keys) { 18 | Tipboard.Dashboard.setDataByKeys(tileId, data, keys); 19 | }, 20 | setJustValueColor: function(tileId, meta) { 21 | // DEPRECATED function, Tipboard.DisplayUtils.applyHighlighterConfig 22 | var color = meta['just-value-color']; 23 | if (typeof(color) !== 'undefined') { 24 | color = Tipboard.DisplayUtils.replaceFromPalette(color); 25 | var dst = $('#' + tileId + '-just-value').parent(); 26 | $(dst).css('background-color', color); 27 | } 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | import os 4 | import sys 5 | from setuptools import setup, find_packages 6 | 7 | assert sys.version_info >= (2, 7), "Python 2.7+ required." 8 | 9 | current_dir = os.path.abspath(os.path.dirname(__file__)) 10 | with open(os.path.join(current_dir, 'README.rst')) as readme_file: 11 | with open(os.path.join(current_dir, 'CHANGES.rst')) as changes_file: 12 | long_description = readme_file.read() + '\n' + changes_file.read() 13 | 14 | from tipboard import __version__ 15 | 16 | with open('requirements.txt') as requirements: 17 | required = requirements.read().splitlines() 18 | 19 | setup( 20 | name='tipboard', 21 | version=__version__, 22 | description='Tipboard - a flexible solution for creating your dashboards.', 23 | long_description = long_description, 24 | url='http://tipboard.allegrogroup.com', 25 | license='Apache Software License v2.0', 26 | author='Allegro Group and Contributors', 27 | author_email='pylabs@allegro.pl', 28 | packages=find_packages(), 29 | include_package_data=True, 30 | zip_safe=False, 31 | platforms=['any'], 32 | install_requires=required, 33 | entry_points={ 34 | 'console_scripts': [ 35 | 'tipboard = tipboard.console:main', 36 | ], 37 | }, 38 | ) 39 | -------------------------------------------------------------------------------- /tipboard/tiles/big_value.js: -------------------------------------------------------------------------------- 1 | /*jslint browser: true, devel: true*/ 2 | /*global WebSocket: false, Tipboard: false*/ 3 | 4 | 5 | function updateTileBigValue(tileId, data, config) { 6 | var tile = Tipboard.Dashboard.id2node(tileId); 7 | BigValueTile.setDataByKeys(tileId, data, 'all'); 8 | var highlighterNode = $('#' + tileId + '-big-value').parent(); 9 | Tipboard.DisplayUtils.applyHighlighterConfig( 10 | highlighterNode, config.big_value_color, config.fading_background 11 | ); 12 | Tipboard.TileDisplayDecorator.runAllDecorators(tile); 13 | } 14 | 15 | Tipboard.Dashboard.registerUpdateFunction('big_value', updateTileBigValue); 16 | 17 | BigValueTile = { 18 | setDataByKeys: function(tileId, data, keys) { 19 | Tipboard.Dashboard.setDataByKeys(tileId, data, keys); 20 | }, 21 | setBigValueColor: function(tileId, meta) { 22 | // DEPRECATED function, Tipboard.DisplayUtils.applyHighlighterConfig 23 | var color = meta['big_value_color']; 24 | if (typeof(color) !== 'undefined') { 25 | color = Tipboard.DisplayUtils.replaceFromPalette(color) 26 | var tile = Tipboard.Dashboard.id2node(tileId); 27 | var bigValue = $(tile).find('#' + tileId + '-big-value')[0] 28 | var dst = $(bigValue).parent(); 29 | $(dst).css('background-color', color); 30 | } 31 | } 32 | }; 33 | 34 | -------------------------------------------------------------------------------- /tipboard/defaults/layout_config.yaml: -------------------------------------------------------------------------------- 1 | details: 2 | page_title: Empty Dashboard 3 | layout: 4 | - row_1_of_2: 5 | - col_1_of_4: 6 | - tile_template: empty 7 | tile_id: empty 8 | title: Empty Tile 9 | classes: 10 | 11 | - col_1_of_4: 12 | - tile_template: empty 13 | tile_id: empty 14 | title: Empty Tile 15 | classes: 16 | 17 | - col_1_of_4: 18 | - tile_template: empty 19 | tile_id: empty 20 | title: Empty Tile 21 | classes: 22 | 23 | - col_1_of_4: 24 | - tile_template: empty 25 | tile_id: empty 26 | title: Empty Tile 27 | classes: 28 | 29 | - row_1_of_2: 30 | - col_1_of_4: 31 | - tile_template: empty 32 | tile_id: empty 33 | title: Empty Tile 34 | classes: 35 | 36 | - col_1_of_4: 37 | - tile_template: empty 38 | tile_id: empty 39 | title: Empty Tile 40 | classes: 41 | 42 | - col_1_of_4: 43 | - tile_template: empty 44 | tile_id: empty 45 | title: Empty Tile 46 | classes: 47 | 48 | - col_1_of_4: 49 | - tile_template: empty 50 | tile_id: empty 51 | title: Empty Tile 52 | classes: 53 | -------------------------------------------------------------------------------- /tipboard/templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_title %}{{ details['page_title'] }}{% end %} 4 | 5 | {% block head %} 6 | {% for file in tiles_css %} 7 | 8 | {% end %} 9 | {% for file in tiles_js %} 10 | 11 | {% end %} 12 | {% end %} 13 | 14 | {% block put_body %} 15 | 16 | 17 | 18 | {% for rows in layout %} 19 | {% for row in rows.keys() %} 20 | {# ROWS #} 21 |
    22 | {% for item in rows[row] %} 23 | {# COLS #} 24 | {% set col=item.keys().pop() %} 25 |
    26 | {% for tile in item.values().pop() %} 27 | {# TILE #} 28 | {% module Template( 29 | tile_path(tile['tile_template']), 30 | tile_id=tile['tile_id'], 31 | title=tile['title'], 32 | timeout=tile.get('timeout', ''), 33 | ) %} 34 | 36 | 38 | {% end %} 39 |
    40 | {% end %} 41 |
    42 | {% end %} 43 | {% end %} 44 | 45 | 46 | 47 | {% end %} 48 | 49 | 50 | -------------------------------------------------------------------------------- /doc/tile__text.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | ``text`` 3 | ======== 4 | 5 | .. image:: img/smaller/text.png 6 | 7 | **Description** 8 | 9 | Simple text-tile designated to display... (surprise!) text. 10 | 11 | **Content** 12 | 13 | :: 14 | 15 | data = {"text": ""} 16 | 17 | where: 18 | 19 | .. describe:: text_content 20 | 21 | A textual content to be displayed. 22 | 23 | Example:: 24 | 25 | curl http://localhost:7272/api/v0.1//push 26 | -X POST 27 | -d "tile=text" 28 | -d "key=mytext" 29 | -d 'data={"text": "Hello world!"}' 30 | 31 | **Configuration** 32 | 33 | :: 34 | 35 | value = {"": ""} 36 | 37 | where: 38 | 39 | .. describe:: config_element 40 | 41 | One of three attributes of displayed text (i.e. ``font_size``, ``color`` and 42 | ``font_weight``). 43 | 44 | .. describe:: config_value 45 | 46 | Value matching above. 47 | 48 | Example:: 49 | 50 | curl http://localhost:7272/api/v0.1//tileconfig/mytext 51 | -X POST 52 | -d 'value={"font_color": "#00FF00"}' 53 | 54 | .. note:: 55 | 56 | Parameter ``font_size`` can be specified in two ways - as a number (e.g. 57 | ``"font_size": 10``) or as a string (e.g. ``"font_size": "10px"``) - both of 58 | them have the same effect. 59 | 60 | Keys ``font_size``, ``color``, ``font_weight`` with empty ``config_value`` 61 | are ignored (in such case, they will inherit those values from parent CSS). 62 | -------------------------------------------------------------------------------- /tipboard/tiles/advanced_plot.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 'use strict'; 3 | 4 | var AdvancedPlotTile; 5 | 6 | AdvancedPlotTile = { 7 | buildChart: function (tileId, plotData, config) { 8 | // DEPRECATED fn, use Tipboard.Dashboard.createGraph 9 | var containerId, plot; 10 | containerId = tileId + '-chart'; 11 | plot = $.jqplot(containerId, plotData, config); 12 | Tipboard.Dashboard.chartsIds[tileId] = plot; 13 | }, 14 | setDataByKeys: function (tileId, data, keys) { 15 | Tipboard.Dashboard.setDataByKeys(tileId, data, keys); 16 | }, 17 | rescaleContainers: function (tile) { 18 | Tipboard.DisplayUtils.expandLastChild(tile); 19 | Tipboard.DisplayUtils.expandLastChild($(tile).find('.tile-content')[0]); 20 | } 21 | }; 22 | 23 | function updateTileAdvancedPlot(tileId, data, meta) { 24 | var tile, newMeta; 25 | tile = Tipboard.Dashboard.id2node(tileId); 26 | AdvancedPlotTile.setDataByKeys(tileId, data, ['title', 'description']); 27 | newMeta = $.extend(true, {}, meta); 28 | var renderersSwapper = new RenderersSwapper(); 29 | renderersSwapper.swap(newMeta); 30 | AdvancedPlotTile.rescaleContainers(tile); 31 | AdvancedPlotTile.buildChart(tileId, data.plot_data, newMeta); 32 | } 33 | 34 | Tipboard.Dashboard.registerUpdateFunction('advanced_plot', updateTileAdvancedPlot); 35 | }($)); 36 | -------------------------------------------------------------------------------- /tipboard/extras/fabfile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from fabric.api import run 5 | 6 | from fabtools import require 7 | from fabtools.python import virtualenv 8 | 9 | 10 | _TIPBOARD_USER = 'tipboard' 11 | _SOURCE_CODE_URL = 'https://github.com/allegro/tipboard.git' 12 | 13 | 14 | def install(): 15 | configure_os() 16 | 17 | install_tipboard() 18 | 19 | run('chown -R %(user)s:%(user)s /home/%(user)s' % {'user': _TIPBOARD_USER}) 20 | 21 | 22 | def configure_os(): 23 | require.deb.packages([ 24 | 'python', 25 | 'python-dev', 26 | 'python-virtualenv', 27 | 'redis-server', 28 | 'libmysqlclient-dev', 29 | 'supervisor', 30 | 'git', 31 | ]) 32 | 33 | require.user(_TIPBOARD_USER, home='/home/' + _TIPBOARD_USER, 34 | shell='/bin/bash') 35 | 36 | 37 | def install_tipboard(): 38 | venv_path = '/home/%s/tipboard-current' % _TIPBOARD_USER 39 | 40 | require.python.virtualenv(venv_path) 41 | 42 | with virtualenv(venv_path): 43 | require.python.package('distribute==0.6.28') 44 | require.python.package('tipboard', 45 | url=_SOURCE_CODE_URL + '@master#egg=tipboard') 46 | 47 | require.supervisor.process('tipboard', 48 | command='%s/bin/tipboard runserver' % venv_path, 49 | directory='/home/%s' % _TIPBOARD_USER, 50 | user=_TIPBOARD_USER, 51 | stdout_logfile='/home/%s/out.log' % 52 | _TIPBOARD_USER) 53 | -------------------------------------------------------------------------------- /doc/tile__just_value.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | ``just_value`` 3 | ============== 4 | 5 | .. image:: img/smaller/just-value.png 6 | 7 | **Description** 8 | 9 | Tile for displaying single, short information with a possibility to change its 10 | background color. 11 | 12 | **Content** 13 | 14 | :: 15 | 16 | "data" = { 17 | "title": "", 18 | "description": "<description>", 19 | "just-value": "<value>" 20 | } 21 | 22 | where: 23 | 24 | .. describe:: title, description 25 | 26 | Title and description (subtitle) for the tile. 27 | 28 | .. describe:: just-value 29 | 30 | Value to be displayed on the tile, with optionally colored background. 31 | 32 | Example:: 33 | 34 | curl http://localhost:7272/api/v0.1/<api_key>/push 35 | -X POST 36 | -d "tile=just_value" 37 | -d "key=<tile_id>" 38 | -d 'data={"title": "Next release:", "description": "(days remaining)", "just-value": "23"}' 39 | 40 | **Configuration** 41 | 42 | :: 43 | 44 | value = { 45 | "just-value-color": "<color>", 46 | "fading_background": <BOOLEAN> 47 | } 48 | 49 | where: 50 | 51 | .. describe:: just-value-color 52 | 53 | Background color for ``just-value`` in a hexadecimal form or color name (e.g. 54 | ``#94C140`` or ``green``). 55 | 56 | .. describe:: fading_background 57 | 58 | Turns on/off background pulsation for ``just-value`` (may be useful for 59 | alerts etc.). 60 | 61 | .. versionadded:: 1.3.0 62 | 63 | Example:: 64 | 65 | curl http://localhost:7272/api/v0.1/<api_key>/tileconfig/<tile_id> 66 | -X POST 67 | -d 'value={"just-value-color": "green", "fading_background": true}' 68 | -------------------------------------------------------------------------------- /tipboard/tiles/big_value.html: -------------------------------------------------------------------------------- 1 | <div class="tile highlighter-tile big_value_tile" id="{{ tile_id }}"> 2 | <div class="tile-header"> 3 | <h3 >{{ title }}</h3> 4 | </div> 5 | <div class="tile-content"> 6 | <h2 id="{{ tile_id + '-title' }}" class="result big-result fixed-height"></h2> 7 | <h3 id="{{ tile_id + '-description' }}" class="result label fixed-height" 8 | ></h3> 9 | <div class="result big-value-container"> 10 | <span id="{{ tile_id + '-big-value' }}" class="big-value"></span> 11 | </div> 12 | <div class="result-row highlight-footer"> 13 | <div class="result-half-col"> 14 | <div class="result"> 15 | <span id="{{ tile_id + '-upper-left-label' }}" class="label"></span> 16 | <span id="{{ tile_id + '-upper-left-value' }}"></span> 17 | </div> 18 | <div class="result"> 19 | <span id="{{ tile_id + '-lower-left-label' }}" class="label"></span> 20 | <span id="{{ tile_id + '-lower-left-value' }}"></span> 21 | </div> 22 | </div> 23 | <div class="result-half-col"> 24 | <div class="result"> 25 | <span id="{{ tile_id + '-upper-right-label' }}" class="label"></span> 26 | <span id="{{ tile_id + '-upper-right-value' }}"></span> 27 | </div> 28 | <div class="result"> 29 | <span id="{{ tile_id + '-lower-right-label' }}" class="label"></span> 30 | <span id="{{ tile_id + '-lower-right-value' }}"></span> 31 | </div> 32 | </div> 33 | </div> 34 | </div> 35 | </div> 36 | -------------------------------------------------------------------------------- /doc/tile__pie_chart.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | ``pie_chart`` 3 | ============= 4 | 5 | .. image:: img/smaller/pie-chart.png 6 | 7 | **Description** 8 | 9 | "Pie-chart" style chart using `jqPlot <http://www.jqplot.com/>`_ library, with 10 | optional legend. 11 | 12 | **Content** 13 | 14 | :: 15 | 16 | data = { 17 | "title": "<optional_title>", 18 | "pie_data": [[identifier1, value1], [identifier2, value2], ...] 19 | } 20 | 21 | where: 22 | 23 | .. describe:: title 24 | 25 | Chart's title (optional). 26 | 27 | .. describe:: pie_data 28 | 29 | Data for pie-chart in a form of list of lists, where each sub-list is an 30 | identifier-value pair. Percentage of the whole chart shared by given part is 31 | calculated automatically by jqPlot - relatively to the sum of values of all 32 | parts. 33 | 34 | Example:: 35 | 36 | curl http//localhost:7272/api/v0.1/<api_key>/push 37 | -X POST 38 | -d "tile=pie_chart" 39 | -d "key=example_pie" 40 | -d 'data={"title": "My title", "pie_data": [["Pie 1", 25], ["Pie 2", 25], ["Pie 3", 50]]}' 41 | 42 | -- this will result in a pie-chart with title "My title", divided by three 43 | parts "Pie 1", "Pie 2" and "Pie 3". 44 | 45 | **Configuration** 46 | 47 | :: 48 | 49 | value = {<jqplot_config>} 50 | 51 | where: 52 | 53 | .. describe:: jqplot_config 54 | 55 | Configuration params in the form described by `jqPlot documentation 56 | <http://www.jqplot.com/tests/pie-donut-charts.php>`_. 57 | 58 | Example:: 59 | 60 | curl http://localhost:7272/api/v0.1/<api_key>/tileconfig/<tild_id> 61 | -X POST 62 | -d 'value={"title": true, "legend": {"show": true, "location": "s"}}' 63 | 64 | -- this will result in a pie-chart with legend turned on at the bottom of the tile (``s`` stands for "south") - its title will be turned on as well. 65 | -------------------------------------------------------------------------------- /doc/tile__bar_chart.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | ``bar_chart`` 3 | ============= 4 | 5 | .. image:: img/smaller/bar-chart.png 6 | 7 | **Description** 8 | 9 | Tile displaying data in the form of horizontal bar charts. Each cart may 10 | consist of one or more series of data - in the latter case, such bars are 11 | grouped respectively. There are no "hard" limits when it comes to the number of 12 | charts/series, although we suggest to keep them down to 3 charts of 2 series 13 | each. 14 | 15 | **Content** 16 | 17 | :: 18 | 19 | data = { 20 | "title": "<title>", 21 | "subtitle": "<subtitle>", 22 | "ticks": ["<label1>", "<label2>", "<label3>" ...], 23 | "series_list": [[<val1>, <val2>, <val3>, ...], 24 | [<val1>, <val2>, <val3>, ...]] 25 | } 26 | 27 | where: 28 | 29 | .. describe:: title, subtitle 30 | 31 | Title and subtitle displayed on the top of the tile. 32 | 33 | .. describe:: ticks 34 | 35 | Labels to be displayed on the left side of the charts. 36 | 37 | .. describe:: series_list 38 | 39 | List of series, as name suggests. ``[[1, 2]]`` will give one chart of two 40 | bars, ``[[3, 4, 5], [6, 7, 8]]`` will give two charts of three bars each. 41 | 42 | Example:: 43 | 44 | curl http://localhost:7272/api/v0.1/<api_key>/push 45 | -X POST 46 | -d "tile=bar_chart" 47 | -d "key=<tile_id>" 48 | -d 'data={"title": "The A-Team", 49 | "subtitle": "Velocity (Last tree sprints)", 50 | "ticks": ["n-2", "n-1", "Last (n)"], 51 | "series_list": [[49, 50, 35], [13, 45, 9]]}' 52 | 53 | **Configuration** 54 | 55 | This tile does not offer any configuration options. 56 | 57 | .. note:: 58 | 59 | In case of displaying more than one charts on the same tile, the number of 60 | values in ``series_list`` for every chart should be the same (and they 61 | should be equal to the number of ``ticks``. 62 | -------------------------------------------------------------------------------- /doc/tile__norm_chart.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | ``norm_chart`` 3 | ============== 4 | 5 | .. image:: img/smaller/norm-chart.png 6 | 7 | .. versionadded:: 1.3.0 8 | 9 | **Description** 10 | 11 | "Curve vs norm" style chart. Suitable for situations, when you want to 12 | compare some data with expected value ("norm") or put an emphasis on y-axis 13 | values. 14 | 15 | **Content** 16 | 17 | :: 18 | 19 | "data" = { 20 | "title": "<title>", 21 | "description": "<description>", 22 | "plot_data": [ [<series1>], [<series2>], [<series3>], ... ] 23 | } 24 | 25 | where: 26 | 27 | .. describe:: title, description 28 | 29 | Title and description (subtitle) for the tile. 30 | 31 | .. describe:: plot_data 32 | 33 | Data for charts in a form of list of series, where each series designates 34 | single chart; each element of a given series is a pair ``[x_axis_value, 35 | y_axis_value]``. 36 | 37 | Example:: 38 | 39 | curl http://localhost:7272/api/v0.1/<api_key>/push 40 | -X POST 41 | -d "tile=norm_chart" 42 | -d "key=<tile_id>" 43 | -d 'data={"title": "My title", 44 | "description": "Some description", 45 | "plot_data": [[[1, 2], [3, 5.12], [5, 13.1], [7, 33.6], [9, 85.9], [11, 219.9]], 46 | [[6, 2], [3, 5.12], [5, 13.1], [7, 33.6], [9, 85.9], [11, 219.9]]]}' 47 | 48 | **Configuration** 49 | 50 | :: 51 | 52 | value = { 53 | "easyNorms": [["<color>", <y-value>, <line_width>], ...] 54 | } 55 | 56 | where: 57 | 58 | .. describe:: easyNorms 59 | 60 | List of norms to be displayed. Each norm consists of three elements: 61 | 62 | .. describe:: color 63 | 64 | Color which given norm should use - in a hexadecimal form or color name 65 | (e.g. ``#94C140`` or ``green``). 66 | 67 | .. describe:: y-value 68 | 69 | Value for the norm. 70 | 71 | .. describe:: line_width 72 | 73 | Line thickness for the norm (in pixels). 74 | 75 | Example:: 76 | 77 | curl http://localhost:7272/api/v0.1/<api_key>/tileconfig/<tile_id> 78 | -X POST 79 | -d 'value={"easyNorms": [["yellow", 200, 2], ["green", 100, 2]]}' 80 | -------------------------------------------------------------------------------- /tipboard/tiles/pie_chart.js: -------------------------------------------------------------------------------- 1 | function updateTilePie(tileId, data, config, tipboard) { 2 | Tipboard.Dashboard.setDataByKeys(tileId, data, ['title', 'description']); 3 | renderersSwapper = new RenderersSwapper(); 4 | renderersSwapper.swap(config); 5 | var config = jQuery.extend(true, DEFAULT_PIE_CHART_CONFIG, { 6 | legend: { 7 | border: Tipboard.DisplayUtils.palette.tile_background, 8 | background: Tipboard.DisplayUtils.palette.tile_background 9 | }, 10 | grid: { 11 | background: Tipboard.DisplayUtils.palette.tile_background 12 | } 13 | }, config); 14 | // colors from palette 15 | if (config.seriesColors) { 16 | $.each(config.seriesColors, function (idx, color) { 17 | config.seriesColors[idx] = Tipboard.DisplayUtils.replaceFromPalette(color); 18 | }); 19 | } else { 20 | config.seriesColors = Tipboard.DisplayUtils.getPalette( 21 | tipboard.color 22 | ); 23 | } 24 | // autoscale required nodes and plot 25 | // TODO: use buildFittedChart 26 | var tile = Tipboard.Dashboard.id2node(tileId); 27 | Tipboard.DisplayUtils.expandLastChild(tile); 28 | Tipboard.DisplayUtils.expandLastChild($(tile).find('.tile-content')[0]); 29 | Tipboard.Dashboard.chartsIds[tileId] = $.jqplot( 30 | tileId + '-chart', [data.pie_data], config 31 | ); 32 | } 33 | Tipboard.Dashboard.registerUpdateFunction('pie_chart', updateTilePie); 34 | 35 | var DEFAULT_PIE_CHART_CONFIG = { 36 | title: false, 37 | legend: { 38 | textColor: 'white', 39 | renderer: $.jqplot.DonutLegendRenderer, 40 | location: 's', 41 | show: true, 42 | border: '0', 43 | //placement: "outside", 44 | rendererOptions: { 45 | numberColumns: 3 46 | } 47 | }, 48 | grid: { 49 | drawGridLines: false, 50 | borderWidth: 0, 51 | shadow: false 52 | }, 53 | seriesDefaults: { 54 | renderer: $.jqplot.DonutRenderer, 55 | rendererOptions: { 56 | padding: 0, 57 | shadowAlpha: 0, 58 | sliceMargin: 0, 59 | innerDiameter: 60, 60 | showDataLabels: true, 61 | startAngle: -90 62 | } 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /doc/extras.rst: -------------------------------------------------------------------------------- 1 | ====== 2 | Extras 3 | ====== 4 | 5 | Here you will find description of components which are not a part of the 6 | Tipboard project *per se*, although they may be useful to some of its users. 7 | Assuming standard installation, you can find them here:: 8 | 9 | <path_to_your_virtualenv>/lib/python2.7/site-packages/tipboard/extras 10 | 11 | .. note:: 12 | 13 | If you have developed something of similar nature and you are willing to 14 | share it, we encourage you to make a pull request to our repo. Thanks! 15 | 16 | ``jira-ds.py`` 17 | -------------- 18 | 19 | Script for fetching frequently used data from `JIRA 20 | <https://www.atlassian.com/software/jira>`_ issue tracker, in order to present 21 | it on your dashboards. Returns requested data to stdout in JSON format. For 22 | the list of available options see ``jira-ds.py --help``. 23 | 24 | This script is basically a wrapper around ``jira-ds.js``, so those two files 25 | shouldn't be separated. Requires `CasperJS <http://casperjs.org/>`_ and 26 | `PhantomJS <http://phantomjs.org/>`_ installed somewhere in your path (we 27 | suggest using `npm <http://nodejs.org/>`_ for that). 28 | 29 | Before you start using them, remember to fill in ``JIRA_CREDENTIALS`` and 30 | ``JIRA_BASE_URL`` (in ``jira-ds.py``) as well as ``url_jira`` and 31 | ``url_jira_login`` (in ``jira-ds.js``) with the your JIRA credentials, location 32 | of your JIRA instance and its login page. 33 | 34 | Tested with JIRA 6.1.x. 35 | 36 | 37 | ``client_code_example.py`` 38 | -------------------------- 39 | 40 | Simple Python script targeted to novice users serving as an example how to glue 41 | together three steps: fetching data, processing it and then sending it to the 42 | tile. See comments in the source code for further explanation. 43 | 44 | 45 | ``fabfile.py`` 46 | -------------- 47 | 48 | Script for quick, automated installations on remote machines. 49 | 50 | You need to have `fabric <https://github.com/ronnix/fabtools>`_ and 51 | `fabtools <http://fabtools.readthedocs.org>`_ to use remote install script. 52 | 53 | Run:: 54 | 55 | fab -H root@host install 56 | 57 | -- it will install all needed ``.deb`` packages, create virualenv and set up 58 | Tipboard service using master branch from our main repo. 59 | -------------------------------------------------------------------------------- /doc/tile__cumulative_flow.rst: -------------------------------------------------------------------------------- 1 | =================== 2 | ``cumulative_flow`` 3 | =================== 4 | 5 | .. image:: img/smaller/cumulative-flow.png 6 | 7 | **Description** 8 | 9 | Cumulative chart using `jqPlot <http://www.jqplot.com/>`_ library. Allows to 10 | display up to seven plots on a single chart. 11 | 12 | **Content** 13 | 14 | :: 15 | 16 | data = { 17 | "title": "<title>", 18 | "series_list": [{"label": "<label1>", "series": [<val1>, <val2>, ...]}, 19 | {"label": "<label2>", "series": [<val1>, <val2>, ...]}] 20 | } 21 | 22 | where: 23 | 24 | .. describe:: title 25 | 26 | Title to be displayed above the labels. 27 | 28 | .. describe:: series_list 29 | 30 | A container (i.e. list of objects) for the data; each such object 31 | corresponding to a single plot consists of two keys: ``label`` and 32 | ``series``, where the latter is a list of values constructing the plot. 33 | 34 | Example:: 35 | 36 | curl http://localhost:7272/api/v0.1/<api_key>/push 37 | -X POST 38 | -d "tile=cumulative_flow" 39 | -d "key=<tile_id>" 40 | -d 'data={"title": "My title:", 41 | "series_list": [{"label": "label 1", "series": [ 0, 0, 0, 0, 1, 1, 2, 2, 1, 1, 1, 0, 0, 2, 0 ]}, 42 | {"label": "label 2", "series": [ 0, 5, 0, 0, 1, 0, 0, 3, 0, 0, 0, 7, 8, 9, 1 ]}]}' 43 | 44 | **Configuration** 45 | 46 | :: 47 | 48 | value = {"ticks": [[<key>, "<value>"], [<key>, "<value>"], ... ]} 49 | 50 | where: 51 | 52 | .. describe:: ticks 53 | 54 | List of elements defining x-axis; each such element is a list of form ``[k, 55 | v]`` where ``k`` is an ordinal number designating position of such tick and 56 | ``v`` is a string which will be displayed in that place. 57 | 58 | Example:: 59 | 60 | curl http://localhost:7272/api/v0.1/<api_key>/tileconfig/<tile_id> 61 | -X POST 62 | -d 'value={"ticks": [[1, "mon"], [2, "tue"], [3, "wed"], [4, "thu"], [5, "fri"], [6, "sat"], [7, "sun"]]}' 63 | 64 | .. note:: 65 | 66 | If ``series_list`` contains more than one object (which is the case 99% of 67 | the time), each one of them should have ``series`` list of the same length - 68 | and this length should be equal to the number of ``ticks``. 69 | -------------------------------------------------------------------------------- /doc/tile__simple_percentage.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | ``simple_percentage`` 3 | ===================== 4 | 5 | .. image:: img/smaller/simple-percentage.png 6 | 7 | **Description** 8 | 9 | Tile displaying three arbitrary values (with labels) - one bigger ("main" 10 | value) and two smaller (at the bottom-left and bottom-right of the tile). It is 11 | possible to change background color for the main value. 12 | 13 | **Content** 14 | 15 | :: 16 | 17 | data = { 18 | "title": "<title>", 19 | "subtitle": "<subtitle>", 20 | "big_value": "<value1>", 21 | "left_value": "<value2>", 22 | "right_value": "<value3>" 23 | "left_label": "<label1>", 24 | "right_label": "<label2>" 25 | } 26 | 27 | where: 28 | 29 | .. describe:: title, subtitle 30 | 31 | They serve as a label for the ``big_value`` ("main" value). 32 | 33 | .. describe:: big_value 34 | 35 | Main value, which treated as a string, so it can contain symbols like ``%`` etc. 36 | 37 | .. describe:: left_value, right_value 38 | 39 | Smaller, bottom-left and bottom-right values. 40 | 41 | .. describe:: left_label, right_label 42 | 43 | Labels for above values. 44 | 45 | Example:: 46 | 47 | curl http://localhost:7272/api/v0.1/<api_key>/push 48 | -X POST 49 | -d "tile=simple_percentage" 50 | -d "key=<tile_id>" 51 | -d 'data={"title": "My title", "subtitle": "My subtitle", "big_value": "100%", 52 | "left_value": "50%", "left_label": "smaller label 1", 53 | "right_value": "25%", "right_label": "smaller label 2"}' 54 | 55 | **Configuration** 56 | 57 | :: 58 | 59 | value = { 60 | "big_value_color": "<color>", 61 | "fading_background": <BOOLEAN> 62 | } 63 | 64 | where: 65 | 66 | .. describe:: big_value_color 67 | 68 | Background color for ``big_value`` in a hexadecimal form or color name (e.g. 69 | ``#94C140`` or ``green``). 70 | 71 | .. describe:: fading_background 72 | 73 | Turns on/off background pulsation for ``big_value`` (may be useful for alerts etc.). 74 | 75 | .. versionadded:: 1.3.0 76 | 77 | Example:: 78 | 79 | curl http://localhost:7272/api/v0.1/<api_key>/tileconfig/<tile_id> 80 | -X POST 81 | -d 'value={"big_value_color": "green", "fading_background": true}' 82 | -------------------------------------------------------------------------------- /tipboard/static/js/lib/jqplot/plugins/jqplot.mobile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jqplot.jquerymobile plugin 3 | * jQuery Mobile virtual event support. 4 | * 5 | * Version: 1.0.8 6 | * Revision: 1250 7 | * 8 | * Copyright (c) 2011 Takashi Okamoto 9 | * jqPlot is currently available for use in all personal or commercial projects 10 | * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL 11 | * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can 12 | * choose the license that best suits your project and use it accordingly. 13 | * 14 | * Although not required, the author would appreciate an email letting him 15 | * know of any substantial use of jqPlot. You can reach the author at: 16 | * chris at jqplot dot com or see http://www.jqplot.com/info.php . 17 | * 18 | * If you are feeling kind and generous, consider supporting the project by 19 | * making a donation at: http://www.jqplot.com/donate.php . 20 | * 21 | */ 22 | (function($) { 23 | function postInit(target, data, options){ 24 | this.bindCustomEvents = function() { 25 | this.eventCanvas._elem.bind('vclick', {plot:this}, this.onClick); 26 | this.eventCanvas._elem.bind('dblclick', {plot:this}, this.onDblClick); 27 | this.eventCanvas._elem.bind('taphold', {plot:this}, this.onDblClick); 28 | this.eventCanvas._elem.bind('vmousedown', {plot:this}, this.onMouseDown); 29 | this.eventCanvas._elem.bind('vmousemove', {plot:this}, this.onMouseMove); 30 | this.eventCanvas._elem.bind('mouseenter', {plot:this}, this.onMouseEnter); 31 | this.eventCanvas._elem.bind('mouseleave', {plot:this}, this.onMouseLeave); 32 | if (this.captureRightClick) { 33 | this.eventCanvas._elem.bind('vmouseup', {plot:this}, this.onRightClick); 34 | this.eventCanvas._elem.get(0).oncontextmenu = function() { 35 | return false; 36 | }; 37 | } 38 | else { 39 | this.eventCanvas._elem.bind('vmouseup', {plot:this}, this.onMouseUp); 40 | } 41 | }; 42 | this.plugins.mobile = true; 43 | } 44 | $.jqplot.postInitHooks.push(postInit); 45 | })(jQuery); -------------------------------------------------------------------------------- /tipboard/tiles/bar_chart.js: -------------------------------------------------------------------------------- 1 | function updateTileBarChart(tileId, data, meta, tipboard) { 2 | Tipboard.Dashboard.setDataByKeys(tileId, data, ['title', 'subtitle']); 3 | renderersSwapper = new RenderersSwapper(); 4 | renderersSwapper.swap(meta); 5 | var config = jQuery.extend(true, {}, DEFAULT_BAR_CHART_CONFIG, { 6 | 'grid': { 7 | 'background': tipboard.color.tile_background, 8 | 'gridLineColor': tipboard.color.tile_background 9 | }, 10 | 'series': Tipboard.DisplayUtils.paletteAsSeriesColors() 11 | }, meta); 12 | if (Boolean(data.ticks)) { 13 | // ticks from *data* are more important than ticks from *config* 14 | config.axes.yaxis.ticks = data.ticks; 15 | } 16 | var tile = Tipboard.Dashboard.id2node(tileId); 17 | // TODO replace it with Tipboard.Dashboard.rescaleTile 18 | Tipboard.DisplayUtils.expandLastChild(tile); 19 | Tipboard.DisplayUtils.expandLastChild($(tile).find('.tile-content')[0]); 20 | Tipboard.Dashboard.chartsIds[tileId] = $.jqplot( 21 | tileId + '-chart', data.series_list, config 22 | ); 23 | } 24 | 25 | Tipboard.Dashboard.registerUpdateFunction('bar_chart', updateTileBarChart); 26 | 27 | var DEFAULT_BAR_CHART_CONFIG = { 28 | title: { 29 | show: false 30 | }, 31 | grid: { 32 | borderWidth: 0, 33 | shadow: false 34 | }, 35 | showMarker: false, 36 | axes: { 37 | xaxis: { 38 | tickOptions: { 39 | showLabel: false, 40 | showMark: false, 41 | shadow: false 42 | } 43 | }, 44 | yaxis: { 45 | renderer: $.jqplot.CategoryAxisRenderer, 46 | tickRenderer: $.jqplot.CanvasAxisTickRenderer, 47 | tickOptions: { 48 | shadow: false, 49 | textColor: '#a4a4a4', 50 | markSize: 0 51 | } 52 | } 53 | }, 54 | seriesDefaults: { 55 | pointLabels: { 56 | show: true 57 | }, 58 | renderer: $.jqplot.BarRenderer, 59 | rendererOptions: { 60 | barWidth: 25, 61 | barPadding: 3, 62 | shadowAlpha: 0, 63 | barDirection: 'horizontal' 64 | }, 65 | trendline: { 66 | show: false 67 | } 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /tipboard/static/js/lib/html5shiv.js: -------------------------------------------------------------------------------- 1 | /* 2 | HTML5 Shiv v3.6.2 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed 3 | */ 4 | (function(l,f){function m(){var a=e.elements;return"string"==typeof a?a.split(" "):a}function i(a){var b=n[a[o]];b||(b={},h++,a[o]=h,n[h]=b);return b}function p(a,b,c){b||(b=f);if(g)return b.createElement(a);c||(c=i(b));b=c.cache[a]?c.cache[a].cloneNode():r.test(a)?(c.cache[a]=c.createElem(a)).cloneNode():c.createElem(a);return b.canHaveChildren&&!s.test(a)?c.frag.appendChild(b):b}function t(a,b){if(!b.cache)b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag(); 5 | a.createElement=function(c){return!e.shivMethods?b.createElem(c):p(c,a,b)};a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+m().join().replace(/\w+/g,function(a){b.createElem(a);b.frag.createElement(a);return'c("'+a+'")'})+");return n}")(e,b.frag)}function q(a){a||(a=f);var b=i(a);if(e.shivCSS&&!j&&!b.hasCSS){var c,d=a;c=d.createElement("p");d=d.getElementsByTagName("head")[0]||d.documentElement;c.innerHTML="x<style>article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}</style>"; 6 | c=d.insertBefore(c.lastChild,d.firstChild);b.hasCSS=!!c}g||t(a,b);return a}var k=l.html5||{},s=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,r=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,j,o="_html5shiv",h=0,n={},g;(function(){try{var a=f.createElement("a");a.innerHTML="<xyz></xyz>";j="hidden"in a;var b;if(!(b=1==a.childNodes.length)){f.createElement("a");var c=f.createDocumentFragment();b="undefined"==typeof c.cloneNode|| 7 | "undefined"==typeof c.createDocumentFragment||"undefined"==typeof c.createElement}g=b}catch(d){g=j=!0}})();var e={elements:k.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup main mark meter nav output progress section summary time video",version:"3.6.2",shivCSS:!1!==k.shivCSS,supportsUnknownElements:g,shivMethods:!1!==k.shivMethods,type:"default",shivDocument:q,createElement:p,createDocumentFragment:function(a,b){a||(a=f);if(g)return a.createDocumentFragment(); 8 | for(var b=b||i(a),c=b.frag.cloneNode(),d=0,e=m(),h=e.length;d<h;d++)c.createElement(e[d]);return c}};l.html5=e;q(f)})(this,document); 9 | -------------------------------------------------------------------------------- /tipboard/static/js/flipboard.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | 'use strict'; 3 | 4 | var showError = function (msg) { 5 | $('.error-message').html(msg); 6 | $('iframe').hide(); 7 | $('.error-wrapper').show(); 8 | }; 9 | 10 | var Flipboard = { 11 | currentPathIdx: -1, 12 | dashboardsPaths: [], 13 | 14 | init: function (paths) { 15 | this.dashboardsPaths = paths; 16 | }, 17 | 18 | getNextDashboardPath: function () { 19 | this.currentPathIdx += 1; 20 | var lastIdx = this.dashboardsPaths.length - 1; 21 | if (this.currentPathIdx > lastIdx) { 22 | this.currentPathIdx = 0; 23 | } 24 | var path = this.dashboardsPaths[this.currentPathIdx]; 25 | return path; 26 | }, 27 | 28 | showNextDashboard: function () { 29 | var nextDashboardPath = this.getNextDashboardPath(); 30 | $('.error-wrapper').hide(); 31 | var activeIframe = $($('iframe')[0]); 32 | if (nextDashboardPath === $(activeIframe).attr('src')) { 33 | console.log('same dashboard - SKIPPING'); 34 | return false; 35 | } 36 | var clonedIframe = $(activeIframe.clone()); 37 | clonedIframe.attr('src', nextDashboardPath); 38 | $('body').append(clonedIframe); 39 | $(clonedIframe).on('load', function() { 40 | $(activeIframe).remove(); 41 | $(clonedIframe).addClass('fadeIn'); 42 | }); 43 | }, 44 | 45 | }; 46 | 47 | window.Flipboard = Flipboard; 48 | 49 | $(document).ready(function () { 50 | $.ajax({ 51 | method: 'post', 52 | url: "/flipboard/getDashboardsPaths", 53 | success: function(data) { 54 | Flipboard.init(data.paths); 55 | Flipboard.showNextDashboard(); 56 | var flipInterval = $('iframe').attr('data-fliptime-interval'); 57 | if (parseInt(flipInterval, 10) > 0) { 58 | setInterval(function () { 59 | Flipboard.showNextDashboard(); 60 | }, flipInterval * 1000); 61 | } 62 | }, 63 | error: function(request, textStatus, error) { 64 | console.log(request, textStatus, error); 65 | var msg = [ 66 | 'Error occured.', 67 | 'For more details check javascript logs.' 68 | ].join('<br>'); 69 | showError(msg); 70 | } 71 | }); 72 | }); 73 | }($)); 74 | -------------------------------------------------------------------------------- /doc/tile__fancy_listing.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | ``fancy_listing`` 3 | ================= 4 | 5 | .. image:: img/smaller/fancy-listing.png 6 | 7 | **Description** 8 | 9 | This tile is a more sophisticated version of ``listing`` tile offering colored 10 | labels and centering options. Each entry is an object containing three keys: 11 | ``label``, ``text`` and ``description``. Therefore, ``data`` (i.e. content) is 12 | just a list of such objects. 13 | 14 | **Content** 15 | 16 | :: 17 | 18 | "data" = [ 19 | {"label": "<label1>", "text": "<entry1>", "description": "<desc1>" }, 20 | {"label": "<label2>", "text": "<entry2>", "description": "<desc2>" } 21 | ] 22 | 23 | where: 24 | 25 | .. describe:: label 26 | 27 | Smaller label displayed on the left which can be colored. 28 | 29 | .. describe:: text 30 | 31 | A textual entry to be displayed next to the label. 32 | 33 | .. describe:: description 34 | 35 | Subtitle displayed below ``text`` element. 36 | 37 | Example:: 38 | 39 | curl http://localhost:7272/api/v0.1/<api_key>/push 40 | -X POST 41 | -d "tile=fancy_listing" 42 | -d "key=<tile_id>" 43 | -d 'data=[{"label": "My label 1", "text": "Lorem ipsum", "description": "such description" }, 44 | {"label": "My label 2", "text": "Dolor sit", "description": "yet another" }, 45 | {"label": "My label 3", "text": "Amet", "description": "" }]' 46 | 47 | **Configuration** 48 | 49 | :: 50 | 51 | value = { 52 | "vertical_center": <BOOLEAN>, 53 | "<position>": { 54 | "label_color": "<color>", 55 | "center": <BOOLEAN> 56 | }, 57 | } 58 | 59 | where: 60 | 61 | .. describe:: vertical_center 62 | 63 | Centers vertically all the entries (along with their labels). 64 | 65 | .. versionadded:: 1.3.0 66 | 67 | .. describe:: position 68 | 69 | Tells which entry (starting from 1) should be a subject to ``label_color`` 70 | and ``center`` (specified as subkeys of ``position``). 71 | 72 | .. describe:: label_color 73 | 74 | Sets the color of label for the entry given with ``position``. Color can 75 | be specified in a hexadecimal form (``#RRGGBB``) or by name (e.g. 76 | ``green``). 77 | 78 | .. describe:: center 79 | 80 | Centers horizontally entry's ``text`` and ``description`` (it does not 81 | affect label's position). 82 | 83 | .. versionadded:: 1.3.0 84 | 85 | Example:: 86 | 87 | curl http://localhost:7272/api/v0.1/<api_key>/tileconfig/<tile_id> 88 | -X POST 89 | -d 'value={"vertical_center": true, 90 | "1": {"label_color": "red", "center": true}, 91 | "3": {"label_color": "green", "center": true }}' 92 | -------------------------------------------------------------------------------- /doc/tile__big_value.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | ``big_value`` 3 | ============= 4 | 5 | .. image:: img/smaller/big-value.png 6 | 7 | **Description** 8 | 9 | This is a variation of ``simple_percentage`` tile. It has slightly different 10 | footer (four possible values instead of just two; values are displayed on the 11 | right side of the label instead of below), and the main value (``big_value``) 12 | is little bit bigger. 13 | 14 | **Content** 15 | 16 | :: 17 | 18 | data = { 19 | "title": "<title>", 20 | "description": "<description>", 21 | "big-value": "<value>", 22 | "upper-left-label": "<label>", 23 | "upper-left-value": "<value>", 24 | "lower-left-label": "<label>", 25 | "lower-left-value": "<value>", 26 | "upper-right-label": "<label>", 27 | "upper-right-value": "<value>", 28 | "lower-right-label": "<label>", 29 | "lower-right-value": "<value>" 30 | } 31 | 32 | where: 33 | 34 | .. describe:: title, description 35 | 36 | Title and description (subtitle) for the tile. 37 | 38 | .. describe:: big_value 39 | 40 | Main value, which treated as a string, so it can contain symbols like ``%`` etc. 41 | 42 | .. describe:: upper-left-value, lower-left-value, upper-right-value, lower-right-value 43 | 44 | Smaller, bottom-left and bottom-right values. 45 | 46 | .. describe:: upper-left-label, lower-left-label, upper-right-label, lower-right-label 47 | 48 | Labels for above values. 49 | 50 | 51 | Example:: 52 | 53 | curl http://localhost:7272/api/v0.1/<api_key>/push 54 | -X POST 55 | -d "tile=big_value" 56 | -d "key=<tile_id>" 57 | -d 'data={"title": "Tickets", 58 | "description": "number of blockers", 59 | "big-value": "314", 60 | "upper-left-label": "critical:", 61 | "upper-left-value": "1020", 62 | "lower-left-label": "major:", 63 | "lower-left-value": "8609", 64 | "upper-right-label": "minor:", 65 | "upper-right-value": "7532", 66 | "lower-right-label": "all:", 67 | "lower-right-value": "19 853"}' 68 | 69 | **Configuration** 70 | 71 | :: 72 | 73 | value = { 74 | "big_value_color": "<color>", 75 | "fading_background": <BOOLEAN> 76 | } 77 | 78 | where: 79 | 80 | .. describe:: big_value_color 81 | 82 | Background color for ``big_value`` in a hexadecimal form or color name (e.g. 83 | ``#94C140`` or ``green``). 84 | 85 | .. describe:: fading_background 86 | 87 | Turns on/off background pulsation for ``big_value`` (may be useful for 88 | alerts etc.). 89 | 90 | .. versionadded:: 1.3.0 91 | 92 | 93 | Example:: 94 | 95 | curl http://localhost:7272/api/v0.1/<api_key>/tileconfig/<tile_id> 96 | -X POST 97 | -d 'value={"big_value_color": "green", "fading_background": true}' 98 | -------------------------------------------------------------------------------- /tipboard/tiles/fancy_listing.js: -------------------------------------------------------------------------------- 1 | /*jslint browser: true, devel: true*/ 2 | /*global WebSocket: false, Tipboard: false*/ 3 | 4 | function updateTileFancyListing(tileId, data, config, tipboard) { 5 | var tile = Tipboard.Dashboard.id2node(tileId); 6 | var nodeToClone = FancyListing.initContainer(tile); 7 | if (typeof(nodeToClone) === 'undefined') { 8 | return false; 9 | } 10 | FancyListing.populateItems(tile, nodeToClone, data); 11 | FancyListing.applyConfig(tile, config); 12 | Tipboard.TileDisplayDecorator.runAllDecorators(tile); 13 | if (config['vertical_center'] === true) { 14 | FancyListing.verticalCenter(tile); 15 | } 16 | } 17 | 18 | Tipboard.Dashboard.registerUpdateFunction('fancy_listing', updateTileFancyListing); 19 | 20 | FancyListing = { 21 | initContainer: function(tile) { 22 | var nodeToClone = $(tile).find('.fancy-listing-item')[0]; 23 | if (typeof(nodeToClone) === 'undefined') { 24 | console.log('ABORTING - no node to clone'); 25 | return false; 26 | } 27 | $(tile).find('.fancy-listing-item').slice(1).remove(); 28 | return nodeToClone; 29 | }, 30 | appendCloned: function(tile, nodeToClone) { 31 | var container = $(tile).find('.tile-content')[0]; 32 | $(nodeToClone).clone().appendTo(container); 33 | }, 34 | populateItems: function(tile, clonedNode, data) { 35 | $.each(data, function(idx, tileData) { 36 | FancyListing.appendCloned(tile, clonedNode); 37 | FancyListing.replaceData(tile, tileData); 38 | }); 39 | }, 40 | applyConfig: function(tile, config) { 41 | $.each(config, function(idx, tileConfig) { 42 | if (/\d+/.test(idx)) { 43 | var item = $(tile).find('.fancy-listing-item')[idx]; 44 | // set color 45 | var color = Tipboard.DisplayUtils.replaceFromPalette( 46 | tileConfig['label_color'] 47 | ); 48 | $(item).find('.fancy-listing-label').css('background-color', color); 49 | // set centering 50 | if (tileConfig['center'] === true) { 51 | $(item).find('.fancy-listing-def').css( 52 | 'text-align', 'center' 53 | ); 54 | } 55 | } 56 | }); 57 | }, 58 | verticalCenter: function(tile) { 59 | // TODO: replace it with css class and toggle the class 60 | containerHeight = $(tile).find('.tile-content').height(); 61 | children = $(tile).find('.tile-content').children().slice(1); 62 | var childrensHeight = 0; 63 | $.each(children, function(idx, child) { 64 | childrensHeight += $(child).outerHeight(true); 65 | }); 66 | positionToSet = (containerHeight - childrensHeight) / 2; 67 | if (positionToSet > 0) { 68 | $(children[0]).css('padding-top', positionToSet); 69 | } 70 | }, 71 | replaceData: function(tile, tileData) { 72 | var lastItem = $(tile).find('.fancy-listing-item:last-child')[0]; 73 | $(lastItem).find('.fancy-listing-label-inside').html(tileData['label']); 74 | $(lastItem).find('.fancy-listing-term').html(tileData['text']); 75 | $(lastItem).find('.fancy-listing-desc').html(tileData['description']); 76 | } 77 | }; 78 | 79 | -------------------------------------------------------------------------------- /doc/installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | ============== 4 | How to install 5 | ============== 6 | 7 | You can install Tipboard on a variety of sensible operating systems. This guide 8 | assumes **Ubuntu Server 12.04 LTS** and presents shell command examples 9 | accordingly. 10 | 11 | Prerequisites 12 | ------------- 13 | 14 | Tipboard requires Python 2.7 which can be installed with this command:: 15 | 16 | $ sudo apt-get install python-dev python-virtualenv 17 | 18 | Another dependency which needs to be satisfied before proceeding further is 19 | `Redis <http://redis.io/>`_ server:: 20 | 21 | $ sudo apt-get install redis-server 22 | 23 | Optional yet recommended packages 24 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 25 | 26 | One of such packages is ``supervisor`` - it facilitates program administration 27 | (e.g. its reboot), especially if there are a few instances launched on the 28 | machine. 29 | 30 | Based on the Tornado framework, Tipboard has a built-in server available, but 31 | a typical use case assumes communication with the world via reverse proxy (e.g. 32 | using ``nginx`` or ``apache``). 33 | 34 | .. note:: 35 | 36 | Although configuration of reverse proxy is out of scope of this manual, we 37 | would like to emphasize that Tipboard uses Web Sockets – a relatively new 38 | mechanism – and thus you should ensure a server in a version that will 39 | support it (e.g. ``nginx`` >= 1.3.13 or ``apache2`` >= 2.4.6). By default 40 | Ubuntu 12.04 offers older versions – you may then use backports. 41 | 42 | .. note:: 43 | 44 | It will be useful to have an updated version of ``pip`` (i.e. >= 1.4) and 45 | ``virtualenv`` (i.e. >= 1.10). 46 | 47 | Preparing environment for installation 48 | -------------------------------------- 49 | 50 | Start by creating a user, the privileges of whom will be used by the 51 | application (for the needs of this manual, let's create the user "pylabs"):: 52 | 53 | $ sudo adduser pylabs --home /home/pylabs --shell /bin/bash 54 | $ sudo su - pylabs 55 | 56 | Virtual environment 57 | ~~~~~~~~~~~~~~~~~~~ 58 | 59 | Continue by creating a virtual environment that will help you conveniently 60 | separate your instance from what you already have installed in the system 61 | (let's say we name it "tb-env"):: 62 | 63 | $ cd /home/pylabs 64 | $ virtualenv tb-env 65 | 66 | Activate the created virtual environment with the following command:: 67 | 68 | $ source /home/pylabs/tb-env/bin/activate 69 | 70 | .. note:: 71 | 72 | It is worth saving the above line in the ``~/.profile`` file. As a result, 73 | the virtual environment will be activated automatically whenever you log in 74 | on the machine. 75 | 76 | .. note:: 77 | 78 | Further setup assumes an activated virtual environment, which is denoted by 79 | ``(tb-env)`` prefix in your shell prompt. 80 | 81 | Installing with pip 82 | ------------------- 83 | 84 | After creating and activating virtualenv, install the latest (current) version 85 | of Tipboard package available on pypi ("Python Package Index") with the 86 | following command:: 87 | 88 | (tb-env)$ pip install tipboard 89 | 90 | Verification 91 | ------------ 92 | 93 | To verify if installation has been successful, launch this command:: 94 | 95 | (tb-env)$ tipboard runserver 96 | 97 | If you see the message "Listening on port..." instead of errors, it means that 98 | installation was successful and you may proceed to the next section. 99 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Change Log 2 | ---------- 3 | 4 | 1.4.1 5 | ~~~~~ 6 | 7 | Released on November 16, 2016. 8 | 9 | * Fixes for tiles: 'advanced_plot', 'simple_percentage' and 'text-tile'. 10 | 11 | * Fix for tile keys cache. 12 | 13 | * Added support for RequireJS. 14 | 15 | * Make use of simplify.js to make charts (in e.g. 'line_chart' tile) more readable. 16 | 17 | * Fix for 'Target dimension not set' error when layout 'row_1_of_1' is used. 18 | 19 | * Minor fixes, improvements, cleanups etc. 20 | 21 | 22 | 1.4.0 23 | ~~~~~ 24 | 25 | Released on August 28, 2014. 26 | 27 | * Tipboard got open-sourced! 28 | 29 | 30 | 1.3.1 31 | ~~~~~ 32 | 33 | Released on July 23, 2014. 34 | 35 | * Added extensive documentation. 36 | 37 | * Numerous fixes in 'jira-ds' script (e.g added timeouts). 38 | 39 | * Fixed definitions of colors available for tiles. 40 | 41 | * Fixed checking for expired data (+ made it timezone aware). 42 | 43 | * Added integration with Travis. 44 | 45 | * Changed default size of the log files. 46 | 47 | 48 | 1.3.0 49 | ~~~~~ 50 | 51 | Released on February 17, 2014. 52 | 53 | New features: 54 | 55 | * Fading highlighter (for just_value, big_value and simple_percentage tiles). 56 | 57 | * Fancy centering options for fancy_listing tile. 58 | 59 | * Notifications on data expiration. 60 | 61 | * New tile: norm_chart. 62 | 63 | * Possibility to define more than one dashboard per application instance. 64 | 65 | 66 | Bug fixes: 67 | 68 | * Tiles no longer vanish when flipping is enabled. 69 | 70 | * Characters like '.' or '-' (and some others) in tiles' ids are no longer 71 | causing problems. 72 | 73 | * Renderer names (like OHLCRenderer, MarkerRenderer, ShadowRenderer and 74 | ShapeRenderer) can now safely be passed to tiles' configs. 75 | 76 | 77 | Others: 78 | 79 | * Error messages displayed on tiles got more emphasis. 80 | 81 | * Renderer names (in tiles' configs) are now case insensitive. 82 | 83 | * Added frontend tests and selector for tests. 84 | 85 | 86 | 1.2.0 87 | ~~~~~ 88 | 89 | Released on December 19, 2013. 90 | 91 | This release brings new features and some minor bugfixes. 92 | 93 | * New tiles: big_value, just_value, advanced_plot. 94 | 95 | * Rewritten 'jira-ds' script with some new options (e.g. 'maxResults' for JQL). 96 | 97 | * Completely new graphic theme - with new colors, fonts etc. 98 | 99 | * Fixed existing tests and some new added. 100 | 101 | * Exceptions raised by JavaScript are now displayed on the tiles. 102 | 103 | * Improved config handling for bar_chart, pie_chart and line_chart. 104 | 105 | * Added possibility to specify specialized renderers for almost all plots 106 | (except cumulative_flow). 107 | 108 | 109 | 1.1.0 110 | ~~~~~ 111 | 112 | Released on November 20, 2013. 113 | 114 | This release contains multiple improvements and bugfixes: 115 | 116 | * Tiles are no longer packages (i.e. folders). 117 | 118 | * Reorganized files/folders structure. 119 | 120 | * Massively reduced app's settings. 121 | 122 | * Simplified layout config (no more classes, only one keyword needed to get 123 | tile flips working). 124 | 125 | * New tiles: bar_chart, fancy_listing. 126 | 127 | * Improved scaling of tiles + some cosmetic changes. 128 | 129 | * Unique API key is generated automatically for every project. 130 | 131 | * Fabric script for administrative installs 132 | 133 | 134 | 1.0.0 135 | ~~~~~ 136 | 137 | Released on November 06, 2013. 138 | 139 | This is the first release of Tipboard. 140 | 141 | * initial release 142 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Tipboard 3 | ======== 4 | 5 | 6 | Project update 7 | ------------- 8 | 9 | *This project is no longer actively maintained* 10 | 11 | The source code remains accessible for anyone to explore, modify, and adapt as desired. However, we won't be able to merge any patches or address issues. 12 | 13 | Feel free to fork, enhance, and enjoy the Open Source spirit. 14 | 15 | Thank you for the community's interest and contributions! 16 | 17 | 18 | |docs| 19 | 20 | Introduction 21 | ------------ 22 | 23 | Tipboard is a system for creating dashboards, written in JavaScript and Python. 24 | Its widgets ('tiles' in Tipboard's terminology) are completely separated from 25 | data sources, which provides great flexibility and relatively high degree of 26 | possible customizations. 27 | 28 | Because of its intended target (displaying various data and statistics in your 29 | office), it is optimized for larger screens. 30 | 31 | Similar projects: `Geckoboard <http://www.geckoboard.com/>`_, 32 | `Dashing <http://shopify.github.io/dashing/>`_. 33 | 34 | A detailed, technical documentation for Tipboard can be found 35 | `here <http://tipboard.readthedocs.org/en/latest/>`_. 36 | 37 | 38 | Quick start 39 | ----------- 40 | 41 | Requirements 42 | ~~~~~~~~~~~~ 43 | 44 | Assuming Ubuntu or similar Linux distribution, some required packages need 45 | to be installed first:: 46 | 47 | $ sudo apt-get install python-dev python-virtualenv redis-server 48 | 49 | Virtual environment 50 | ~~~~~~~~~~~~~~~~~~~ 51 | 52 | Continue by creating a virtual environment that will help you conveniently 53 | separate your instance from what you already have installed in the system 54 | (let's say we will name it "tb-env"):: 55 | 56 | $ virtualenv tb-env 57 | 58 | Activate the created virtual environment with the following command:: 59 | 60 | $ source tb-env/bin/activate 61 | 62 | Installation with pip 63 | ~~~~~~~~~~~~~~~~~~~~~ 64 | 65 | After creating and activating virtualenv, install the latest (current) version 66 | of Tipboard package available on `pypi <https://pypi.python.org/pypi>`_ 67 | ("Python Package Index") with the following command:: 68 | 69 | (tb-env)$ pip install tipboard 70 | 71 | Next, you need to create a configuration template for your dashboard - let's 72 | say we will call it 'my_test_dashboard':: 73 | 74 | (tb-env)$ tipboard create_project my_test_dashboard 75 | 76 | This command will create ``.tipboard`` directory in your home dir and will 77 | fill it with default settings for your dashboard. 78 | 79 | Verification 80 | ~~~~~~~~~~~~ 81 | 82 | To verify your installation, launch this command:: 83 | 84 | (tb-env)$ tipboard runserver 85 | 86 | If you see the message ``Listening on port...`` instead of any errors, it means 87 | that installation was successful and you may now 88 | `configure <http://tipboard.readthedocs.org/en/latest/configuration.html>`_ 89 | your newly installed Tipboard instance. You may also point your favorite 90 | web browser to ``http://localhost:7272`` to see the current state of your 91 | dashboard. 92 | 93 | 94 | License 95 | ------- 96 | 97 | Tipboard is licensed under the `Apache License, v2.0 <http://tipboard.readthedocs.org/en/latest/license.html>`_. 98 | 99 | Copyright (c) 2013-2017 `Allegro Group <http://allegrogroup.com>`_. 100 | 101 | .. |docs| image:: https://readthedocs.org/projects/tipboard/badge/?version=latest 102 | :alt: Documentation Status 103 | :scale: 100% 104 | :target: https://readthedocs.org/projects/tipboard/ 105 | -------------------------------------------------------------------------------- /doc/tile__line_chart.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | ``line_chart`` 3 | ============== 4 | 5 | .. image:: img/smaller/line-chart.png 6 | 7 | **Description** 8 | 9 | Line-chart using `jqPlot <http://www.jqplot.com/>`_ library. Allows to display 10 | arbitrary number of plots on single chart, with automatic generation of trend 11 | lines for them (which is turned on by default). 12 | 13 | **Content** 14 | 15 | :: 16 | 17 | data = { 18 | "subtitle": "<subtitle_text">, 19 | "description": "<description_text>", 20 | "series_list": [[<series1>], [<series2>], [<series3>], ...] 21 | } 22 | 23 | where: 24 | 25 | .. describe:: subtitle, description 26 | 27 | Additional text fields for charts descriptions (optional - you can pass 28 | empty strings here). 29 | 30 | .. describe:: series_list 31 | 32 | Data for line-charts in a form of list of series, where each series 33 | designates single chart; each element of a given series is a pair 34 | ``[x_axis_value, y_axis_value]``. 35 | 36 | Example:: 37 | 38 | curl http://localhost:7272/api/v0.1/<api_key>/push 39 | -X POST 40 | -d "tile=line_chart" 41 | -d "key=example_line" 42 | -d 'data={"subtitle": "averages from last week", 43 | "description": "Sales in our dept", 44 | "series_list": [[["23.09", 8326], ["24.09", 260630], ["25.09", 240933], ["26.09", 229639], ["27.09", 190240], ["28.09", 125272], ["29.09", 3685]], 45 | [["23.09", 3685], ["24.09", 125272], ["25.09", 190240], ["26.09", 229639], ["27.09", 240933], ["28.09", 260630], ["29.09", 108326]]]}' 46 | 47 | -- this will give two plots on a single chart (on x-axis there will be "23.09", 48 | "24.09", "25.09" and so on) with heading "Sales in our dept" and subtitle 49 | "averages from last week". 50 | 51 | **Configuration** 52 | 53 | :: 54 | 55 | value = {<jqplot_config>, simplify: <simplify_config>} 56 | 57 | where: 58 | 59 | .. describe:: jqplot_config 60 | 61 | Configuration params in the form described by `jqPlot documentation 62 | <http://www.jqplot.com/tests/line-charts.php>`_. 63 | 64 | Example:: 65 | 66 | curl http://localhost:7272/api/v0.1/<api_key>/tileconfig/example_line 67 | -X POST 68 | -d 'value={"grid": {"drawGridLines": true, 69 | "gridLineColor": "#FFFFFF", 70 | "shadow": false, 71 | "background": "#000000", 72 | "borderWidth": 0}}' 73 | 74 | -- this will set up the grid (in white color), black background and will turn 75 | off shadow effects as well as borders. 76 | 77 | simplify_config 78 | 79 | :: 80 | 81 | simplify_config = { 82 | tolerancy: 10, 83 | data_points_limit: 50, // we will TRY to achieve lower number of data points than this 84 | max_simplifying_steps: 5, 85 | simplify_step_multiplicator: 1.5 86 | }; 87 | 88 | Each option is self-describing. This feature tries to optimize dataset to achieve points count lower than `data_points_limit`. If simplify_config is not set, there won't be any simplify process at all (you will just have your raw data displayed). 89 | 90 | :: 91 | 92 | curl -X POST http://127.0.0.1:7272/api/v0.1/dev_key/tileconfig/test_line 93 | -d 'value={"simplify": {"tolerancy": 2}}' 94 | 95 | .. note:: 96 | 97 | In case of displaying multiple plots on a single chart (e.g. for more than 98 | one data series) you have to keep in mind that the ``x_axis_value`` values 99 | should be the same for all of those plots. 100 | -------------------------------------------------------------------------------- /tipboard/redis_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import absolute_import 5 | from __future__ import division 6 | from __future__ import print_function 7 | from __future__ import unicode_literals 8 | 9 | from datetime import datetime 10 | import json 11 | import logging 12 | import time 13 | 14 | import redis 15 | 16 | from tipboard import settings 17 | 18 | log = logging.getLogger(__name__) 19 | 20 | db_events_path = lambda: '{}:events'.format(settings.PROJECT_NAME) 21 | 22 | 23 | class KeyNotFound(Exception): 24 | def __init__(self, key): 25 | self.key = key 26 | self.message = "Key '{}' was not found in db".format(self.key) 27 | 28 | 29 | def redis_actions(method, tile_id, value=None, tile=None): 30 | key = '%s:tile:%s' % (settings.PROJECT_NAME, tile_id) 31 | try: 32 | redis_db = redis.Redis(**settings.REDIS_SYNC) 33 | except Exception as e: 34 | raise TypeError('Redis: %s' % e) 35 | else: 36 | if method.lower() == 'post': 37 | try: 38 | last_tile_data = get_redis_value(tile_id) 39 | except KeyNotFound as e: 40 | meta = {} 41 | else: 42 | meta = last_tile_data.get('meta') 43 | localtime = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") 44 | tz = '{0:+06.2f}'.format(-float(time.altzone) / 3600) 45 | iso_time = localtime + tz.replace('.', ':') # ISO-8601 46 | required_structure = dict( 47 | id=tile_id, 48 | tile_template=tile, 49 | data=json.loads(value), 50 | meta=meta, 51 | modified=iso_time, 52 | ) 53 | dumped_value = json.dumps(required_structure) 54 | redis_db.set(key, dumped_value) 55 | log.debug( 56 | 'db key: {} set to value: {}'.format( 57 | repr(key), repr(dumped_value) 58 | ) 59 | ) 60 | redis_db.publish(db_events_path(), key) 61 | return 'Push success.' 62 | 63 | elif method.lower() == 'get': 64 | db_value = redis_db.get(key) 65 | log.debug('got db value: {}'.format(db_value)) 66 | return db_value 67 | elif method.lower() == 'delete': 68 | redis_db.delete(key) 69 | log.debug('deleted db key: {} '.format(key)) 70 | return 'Deleted.' 71 | else: 72 | return 'Unkown method.' 73 | 74 | 75 | def get_redis_value(tile_id): 76 | key = '%s:tile:%s' % (settings.PROJECT_NAME, tile_id) 77 | try: 78 | redis_db = redis.Redis(**settings.REDIS_SYNC) 79 | except Exception as e: 80 | raise TypeError('Redis: %s' % e) 81 | else: 82 | jsoned = redis_db.get(key) 83 | if not jsoned: 84 | raise KeyNotFound(key) 85 | db_data = json.loads(jsoned) 86 | return db_data 87 | 88 | 89 | def set_redis_value(tile_id, data): 90 | key = '%s:tile:%s' % (settings.PROJECT_NAME, tile_id) 91 | try: 92 | redis_db = redis.Redis(**settings.REDIS) 93 | except Exception as e: 94 | raise TypeError('Redis: %s' % e) 95 | else: 96 | dumped_value = json.dumps(data) 97 | redis_db.set(key, dumped_value) 98 | redis_db.publish(db_events_path(), key) 99 | 100 | 101 | def key_exist(key_id): 102 | key = '%s:tile:%s' % (settings.PROJECT_NAME, key_id) 103 | redis_db = redis.Redis(**settings.REDIS) 104 | return True if redis_db.get(key) else False 105 | -------------------------------------------------------------------------------- /tipboard/tiles/cumulative_flow.js: -------------------------------------------------------------------------------- 1 | /*jslint browser: true, devel: true*/ 2 | /*global WebSocket: false, Tipboard: false*/ 3 | 4 | function updateTileCumulativeFlow(id, data, meta, tipboard) { 5 | if (data.series_list.length > 7) { 6 | console.log('ERROR: more then 7 series passed - RENDERING STOPPED') 7 | return; 8 | } 9 | var tile = $('#' + id)[0]; 10 | $('#' + id + '-title').html(data['title']); 11 | var config = jQuery.extend(true, {}, DEFAULT_CUMULATIVE_FLOW_CHART_CONFIG); 12 | config['axes']['xaxis']['ticks'] = Boolean(meta.ticks) ? meta.ticks : []; 13 | config['grid']['background'] = tipboard.color.tile_background; 14 | config['grid']['gridLineColor'] = tipboard.color.tile_background; 15 | var palette = Tipboard.DisplayUtils.getPalette(tipboard.color); 16 | var seriesForJqplot = []; 17 | // clear html labels before inserting 18 | $($(tile).find('.plot-labels')[0]).find('div').remove() 19 | for (idx in data.series_list) { 20 | // unpack series for jqplot 21 | var seriesData = data.series_list[idx]; 22 | var series = seriesData['series']; 23 | seriesForJqplot.push(series); 24 | // update plots labels and colors 25 | config['series'][idx] = { 26 | 'label': data.series_list[idx]['label'], 27 | 'color': palette[idx] 28 | }; 29 | // insert labels to html 30 | var lastValue = series[series.length - 1]; 31 | appendHtmlLabels( 32 | $(tile).find('.plot-labels')[0], 33 | config['series'][idx]['label'], 34 | lastValue, 35 | config['series'][idx]['color'] 36 | ); 37 | } 38 | var plot = $.jqplot(id + '-chart', seriesForJqplot, config); 39 | Tipboard.Dashboard.chartsIds[id] = plot; 40 | Tipboard.TileDisplayDecorator.runAllDecorators($("#" + id)[0]); 41 | } 42 | 43 | Tipboard.Dashboard.registerUpdateFunction('cumulative_flow', updateTileCumulativeFlow); 44 | 45 | var DEFAULT_CUMULATIVE_FLOW_CHART_CONFIG = { 46 | grid: { 47 | drawGridLines: false, 48 | background: '#232526', 49 | gridLineColor: '#232526', 50 | borderWidth: 0, 51 | shadow: false 52 | }, 53 | stackSeries: true, 54 | seriesDefaults: { 55 | fill: true, 56 | pointLabels: { 57 | show: false 58 | }, 59 | trendline: { 60 | show: false 61 | }, 62 | showMarker: false 63 | }, 64 | series: [], 65 | legend: { 66 | show: false 67 | }, 68 | axes: { 69 | xaxis: { 70 | ticks: [], 71 | tickRenderer: $.jqplot.CanvasAxisTickRenderer, 72 | tickOptions: { 73 | angle: - 90 74 | }, 75 | drawMajorGridlines: false 76 | }, 77 | yaxis: { 78 | min: 0, 79 | tickOptions: { 80 | showLabel: true, 81 | showMark: false, 82 | shadow: false 83 | } 84 | } 85 | } 86 | } 87 | 88 | function appendHtmlLabels(container, label, value, color) { 89 | var htmlLabel = [ 90 | '<div class="result bugs-count-result medium-result">', 91 | '<span style="float:left" class="issue-stats-bugs-number">', 92 | label, 93 | '</span>', 94 | '<span style="float:right" class="issue-stats-bugs-number float-right">', 95 | value, 96 | '</span>', 97 | '<div style="clear:both"></div>', 98 | '</div>' 99 | ].join('\n'); 100 | $(container).append(htmlLabel); 101 | $(container).find('span').last().css('color', color); 102 | } 103 | 104 | -------------------------------------------------------------------------------- /tipboard/static/js/lib/simplify.js: -------------------------------------------------------------------------------- 1 | /* 2 | (c) 2013, Vladimir Agafonkin 3 | Simplify.js, a high-performance JS polyline simplification library 4 | mourner.github.io/simplify-js 5 | */ 6 | 7 | (function () { 'use strict'; 8 | 9 | // to suit your point format, run search/replace for '.x' and '.y'; 10 | // for 3D version, see 3d branch (configurability would draw significant performance overhead) 11 | 12 | // square distance between 2 points 13 | function getSqDist(p1, p2) { 14 | 15 | var dx = p1.x - p2.x, 16 | dy = p1.y - p2.y; 17 | 18 | return dx * dx + dy * dy; 19 | } 20 | 21 | // square distance from a point to a segment 22 | function getSqSegDist(p, p1, p2) { 23 | 24 | var x = p1.x, 25 | y = p1.y, 26 | dx = p2.x - x, 27 | dy = p2.y - y; 28 | 29 | if (dx !== 0 || dy !== 0) { 30 | 31 | var t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy); 32 | 33 | if (t > 1) { 34 | x = p2.x; 35 | y = p2.y; 36 | 37 | } else if (t > 0) { 38 | x += dx * t; 39 | y += dy * t; 40 | } 41 | } 42 | 43 | dx = p.x - x; 44 | dy = p.y - y; 45 | 46 | return dx * dx + dy * dy; 47 | } 48 | // rest of the code doesn't care about point format 49 | 50 | // basic distance-based simplification 51 | function simplifyRadialDist(points, sqTolerance) { 52 | 53 | var prevPoint = points[0], 54 | newPoints = [prevPoint], 55 | point; 56 | 57 | for (var i = 1, len = points.length; i < len; i++) { 58 | point = points[i]; 59 | 60 | if (getSqDist(point, prevPoint) > sqTolerance) { 61 | newPoints.push(point); 62 | prevPoint = point; 63 | } 64 | } 65 | 66 | if (prevPoint !== point) newPoints.push(point); 67 | 68 | return newPoints; 69 | } 70 | 71 | function simplifyDPStep(points, first, last, sqTolerance, simplified) { 72 | var maxSqDist = 0, 73 | index; 74 | 75 | for (var i = first + 1; i < last; i++) { 76 | var sqDist = getSqSegDist(points[i], points[first], points[last]); 77 | 78 | if (sqDist > maxSqDist) { 79 | index = i; 80 | maxSqDist = sqDist; 81 | } 82 | } 83 | 84 | if (maxSqDist > sqTolerance) { 85 | if (index - first > 1) simplifyDPStep(points, first, index, sqTolerance, simplified); 86 | simplified.push(points[index]); 87 | if (last - index > 1) simplifyDPStep(points, index, last, sqTolerance, simplified); 88 | } 89 | } 90 | 91 | // simplification using Ramer-Douglas-Peucker algorithm 92 | function simplifyDouglasPeucker(points, sqTolerance) { 93 | var last = points.length - 1; 94 | 95 | var simplified = [points[0]]; 96 | simplifyDPStep(points, 0, last, sqTolerance, simplified); 97 | simplified.push(points[last]); 98 | 99 | return simplified; 100 | } 101 | 102 | // both algorithms combined for awesome performance 103 | function simplify(points, tolerance, highestQuality) { 104 | 105 | if (points.length <= 2) return points; 106 | 107 | var sqTolerance = tolerance !== undefined ? tolerance * tolerance : 1; 108 | 109 | points = highestQuality ? points : simplifyRadialDist(points, sqTolerance); 110 | points = simplifyDouglasPeucker(points, sqTolerance); 111 | 112 | return points; 113 | } 114 | 115 | // export as AMD module / Node module / browser or worker variable 116 | if (typeof define === 'function' && define.amd) define(function() { return simplify; }); 117 | else if (typeof module !== 'undefined') module.exports = simplify; 118 | else if (typeof self !== 'undefined') self.simplify = simplify; 119 | else window.simplify = simplify; 120 | 121 | })(); 122 | -------------------------------------------------------------------------------- /doc/tiles.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Tiles 3 | ===== 4 | 5 | Every tile consists of an obligatory ``.html`` file and two optional ``.css`` 6 | and ``.js`` files. All three files belonging to a tile should have the same 7 | name that corresponds with the tile name – e.g. with the ``pie_chart`` tile 8 | these are ``pie_chart.html``, ``pie_chart.css`` and ``pie_chart.js`` files 9 | respectively. 10 | 11 | Customizing tiles 12 | ----------------- 13 | 14 | If you want to modify a tile (e.g. change a CSS attribute, which obviously 15 | cannot be done via API), copy a desired file in the folder of tiles delivered 16 | with the application (i.e. 17 | ``<path_to_your_virtualenv>/lib/python2.7/site-packages/tipboard/tiles``), 18 | paste it in your tile folder (i.e. ``~/.tipboard/custom_tiles``) and edit 19 | according to your needs. 20 | 21 | Files in your ``custom_tiles`` folder take precedence over those shipped by 22 | default with the application and thus you can easily replace desired elements 23 | (e.g. if you want to change the text color, just copy and edit the ``.css`` 24 | file – without touching ``.html`` and ``.js`` files). We plan to introduce a 25 | command simplifying this process in the future. 26 | 27 | Color palette 28 | ------------- 29 | 30 | Color palette used by Tipboard's tiles is defined as shown in the table below. 31 | To retain consistency, we strongly suggest sticking to them while customizing 32 | tiles. 33 | 34 | +-------------+---------------------+ 35 | | Value | Name | 36 | +=============+=====================+ 37 | | ``#000000`` | ``black`` | 38 | +-------------+---------------------+ 39 | | ``#FFFFFF`` | ``white`` | 40 | +-------------+---------------------+ 41 | | ``#25282D`` | ``tile_background`` | 42 | +-------------+---------------------+ 43 | | ``#DC5945`` | ``red`` | 44 | +-------------+---------------------+ 45 | | ``#FF9618`` | ``yellow`` | 46 | +-------------+---------------------+ 47 | | ``#94C140`` | ``green`` | 48 | +-------------+---------------------+ 49 | | ``#12B0C5`` | ``blue`` | 50 | +-------------+---------------------+ 51 | | ``#9C4274`` | ``violet`` | 52 | +-------------+---------------------+ 53 | | ``#EC663C`` | ``orange`` | 54 | +-------------+---------------------+ 55 | | ``#54C5C0`` | ``naval`` | 56 | +-------------+---------------------+ 57 | 58 | Common elements 59 | --------------- 60 | 61 | * tile's content (``data`` key) and its configuration (``value`` key) should be 62 | send as two separate requests - once you have established desired 63 | configuration it does not make much sense to send it over and over again; 64 | * in order to reset tile's config, you just send an empty ``value`` key (e.g. 65 | ``value = {}``). 66 | 67 | .. _tiles_library: 68 | 69 | Library of available tiles 70 | -------------------------- 71 | 72 | In the following pages we present a "library" of available tiles (i.e. those 73 | bundled with the application), which should serve as a specification how to 74 | send data to them and how to set up its configuration options. 75 | 76 | .. toctree:: 77 | :maxdepth: 2 78 | 79 | tile__text 80 | tile__pie_chart 81 | tile__line_chart 82 | tile__cumulative_flow 83 | tile__simple_percentage 84 | tile__listing 85 | tile__bar_chart 86 | tile__fancy_listing 87 | tile__big_value 88 | tile__just_value 89 | tile__advanced_plot 90 | tile__norm_chart 91 | 92 | .. note:: 93 | 94 | In order to keep brevity, all examples presented in specifications above do 95 | not include any escape characters. Therefore, it's up to you to insert them 96 | where necessary. 97 | 98 | And also, remember to set all the elements in angle brackets (e.g. 99 | ``<api_key>``, ``<tile_id>`` etc.) to reflect your configuration. 100 | -------------------------------------------------------------------------------- /tipboard/parser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import absolute_import 5 | from __future__ import division 6 | from __future__ import print_function 7 | from __future__ import unicode_literals 8 | 9 | import glob 10 | import os 11 | import yaml 12 | 13 | from tipboard import settings 14 | 15 | 16 | class WrongSumOfRows(Exception): 17 | pass 18 | 19 | 20 | def _get_tiles_dict(col): 21 | return col.values().pop() 22 | 23 | 24 | def _get_cols(rows): 25 | #TODO: validation col_1_of_4 26 | cols = [] 27 | for col in rows.values()[0]: 28 | cols.append(col) 29 | return cols 30 | 31 | 32 | def _get_rows(layout): 33 | """Validates and returns number of rows.""" 34 | rows_data = [] 35 | rows_class = [] 36 | for row in layout: 37 | rows_data.append(row) 38 | rows_class.append(row.keys().pop()) 39 | rows_count = 0 40 | sum_of_rows = [] 41 | for row_class in rows_class: 42 | splited_class = row_class.split('_') # ex: row_1_of_2 43 | row = splited_class[1] 44 | of_rows = int(splited_class[3]) 45 | if rows_count == 0: 46 | rows_count = int(of_rows) 47 | sum_of_rows.append(int(row)) 48 | elif not rows_count == of_rows: 49 | raise WrongSumOfRows('The sum of the lines is incorrect.') 50 | else: 51 | sum_of_rows.append(int(row)) 52 | if not sum(sum_of_rows) == rows_count: 53 | raise WrongSumOfRows('The sum of the lines is incorrect.') 54 | return rows_data 55 | 56 | 57 | def _find_tiles_names(layout): 58 | name_list, key_list = [], [] 59 | for row in _get_rows(layout): 60 | for col in _get_cols(row): 61 | for tile_dict in _get_tiles_dict(col): 62 | name = tile_dict['tile_template'] 63 | key = tile_dict['tile_id'] 64 | if key not in key_list: 65 | key_list.append(key) 66 | if name not in name_list: 67 | name_list.append(name) 68 | return name_list, key_list 69 | 70 | 71 | def get_config_files_names(): 72 | """ 73 | Return all configs files' names (without '.yaml' ext.) from user space 74 | (~/.tipboard/) 75 | """ 76 | configs_names = [] 77 | configs_dir = os.path.join(settings._user_config_dir, '*.yaml') 78 | for config_path in glob.glob(configs_dir): 79 | filename = os.path.basename(config_path) 80 | head, ext = os.path.splitext(filename) 81 | configs_names.append(head) 82 | return configs_names 83 | 84 | 85 | def config_file_name2path(config_name): 86 | """ 87 | Return file path to *config_name* (eg. 'layout_config') 88 | """ 89 | path = os.path.join( 90 | settings._user_config_dir, '.'.join([config_name, 'yaml']) 91 | ) 92 | return path 93 | 94 | 95 | def get_tiles_configs(): 96 | """ 97 | Return dict with both tiles' keys and ids from all available configs 98 | """ 99 | tiles_configs = { 100 | 'tiles_keys': set(), 101 | 'tiles_names': set() 102 | } 103 | configs_names = get_config_files_names() 104 | for config_name in configs_names: 105 | parsed_config = process_layout_config(config_name) 106 | tiles_configs['tiles_keys'].update(set(parsed_config['tiles_keys'])) 107 | tiles_configs['tiles_names'].update(set(parsed_config['tiles_names'])) 108 | return tiles_configs 109 | 110 | 111 | def process_layout_config(layout_name): 112 | config_path = config_file_name2path(layout_name) 113 | with open(config_path, 'r') as layout_config: 114 | config = yaml.load(layout_config) 115 | layout = config['layout'] 116 | config['tiles_names'], config['tiles_keys'] = _find_tiles_names(layout) 117 | return config 118 | -------------------------------------------------------------------------------- /tipboard/extras/client_code_example.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This is an example 'client_code' script which demonstrates how to 'glue' 4 | # together a tile (or collection of tiles) with an external script fetching 5 | # json'ed data from data source. 6 | # 7 | # Script like this one is meant to be launched periodically from crontab. 8 | # 9 | # The main idea here is as follows: you're querying the data source(s) you want, 10 | # transform resulting data according to your needs, and finally you're pushing 11 | # it to your tile, along with optional config data. Really simple, isn't it? 12 | 13 | import json 14 | import requests 15 | 16 | from subprocess import check_output 17 | 18 | # Get your API_KEY from your settings file ('~/.tipboard/settings-local.py'). 19 | API_KEY = '' 20 | # Change '127.0.0.1:7272' to the address of your Tipboard instance. 21 | API_URL = 'http://127.0.0.1:7272/api/v0.1/{}'.format(API_KEY) 22 | API_URL_PUSH = '/'.join((API_URL, 'push')) 23 | API_URL_TILECONFIG = '/'.join((API_URL, 'tileconfig')) 24 | 25 | 26 | #### Helper functions for data transformations (data sources --> tiles). 27 | 28 | # We have only one tile here, hence 'prepare_for_pie_chart' is all we need, 29 | # but in real-life scenario you may want some additional functions like 30 | # 'prepare_for_line_chart', 'prepare_for_velocity_chart' and so on. 31 | 32 | def prepare_for_pie_chart(data): 33 | # Pie chart needs data as a list of lists (whose elements are pairs 34 | # component-percentage), so we have to prepare it. 35 | data_prepared = [] 36 | for k, v in data.items(): 37 | data_prepared.append([k, v[0]]) 38 | data_prepared = {'title': 'my title', 'pie_data': data_prepared} 39 | return data_prepared 40 | 41 | 42 | #### Putting it all together. 43 | 44 | def main(): 45 | # Tile 'pie001' (pie chart) 46 | # (let's say we want to show issues count for project 'Tipboard' grouped by 47 | # issue status i.e. 'Resolved', 'In Progress', 'Open', 'Closed' etc.) 48 | TILE_NAME = 'pie_chart' 49 | TILE_KEY = 'pie001' 50 | # Now we are launching our script asking data source for the stuff we want: 51 | # (see its documentation for available options/switches) 52 | ds_output = check_output(["python", "jira-ds.py", "--project=Tipboard", 53 | "--issues-counts"]) 54 | data = json.loads(ds_output) 55 | data_selected = data.get('status_summary') 56 | data_prepared = prepare_for_pie_chart(data_selected) 57 | data_jsoned = json.dumps(data_prepared) 58 | data_to_push = { 59 | 'tile': TILE_NAME, 60 | 'key': TILE_KEY, 61 | 'data': data_jsoned, 62 | } 63 | resp = requests.post(API_URL_PUSH, data=data_to_push) 64 | if resp.status_code != 200: 65 | print(resp.text) 66 | return 67 | 68 | # (optional) config for our pie chart, which is pushed in a separate request 69 | tile_config = { 70 | 'legend': { 71 | 'show': True, 72 | 'location': 's', 73 | } 74 | } 75 | tile_config_jsoned = json.dumps(tile_config) 76 | data_to_push = { 77 | 'value': tile_config_jsoned, 78 | } 79 | resp = requests.post( 80 | '/'.join((API_URL_TILECONFIG, TILE_KEY)), 81 | data=data_to_push, 82 | ) 83 | if resp.status_code != 200: 84 | print(resp.text) 85 | return 86 | 87 | 88 | # Tile 'pie002' 89 | # TILE_NAME = 'pie_chart' 90 | # TILE_KEY = 'pie002' 91 | # output = check_output(... 92 | # ...and the rest in the same manner as in above example, i.e.: 93 | # - query the data source for data 94 | # - select what you want from that data 95 | # - transform selected data according to tile's specs (if needed) 96 | # - push transformed data to API_URL_PUSH 97 | # - push config data for your tile to API_URL_CONFIG (let's say you want 98 | # to override default behavior and show pie chart's legend at 99 | # the bottom of the tile, or change some colors etc.) 100 | # That's all! 101 | 102 | 103 | if __name__ == '__main__': 104 | main() 105 | -------------------------------------------------------------------------------- /tipboard/tests/frontend/lib/jasmine-2.0.0/jasmine.css: -------------------------------------------------------------------------------- 1 | body { background-color: #eeeeee; padding: 0; margin: 5px; overflow-y: scroll; } 2 | 3 | .html-reporter { font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333333; } 4 | .html-reporter a { text-decoration: none; } 5 | .html-reporter a:hover { text-decoration: underline; } 6 | .html-reporter p, .html-reporter h1, .html-reporter h2, .html-reporter h3, .html-reporter h4, .html-reporter h5, .html-reporter h6 { margin: 0; line-height: 14px; } 7 | .html-reporter .banner, .html-reporter .symbol-summary, .html-reporter .summary, .html-reporter .result-message, .html-reporter .spec .description, .html-reporter .spec-detail .description, .html-reporter .alert .bar, .html-reporter .stack-trace { padding-left: 9px; padding-right: 9px; } 8 | .html-reporter .banner .version { margin-left: 14px; } 9 | .html-reporter #jasmine_content { position: fixed; right: 100%; } 10 | .html-reporter .version { color: #aaaaaa; } 11 | .html-reporter .banner { margin-top: 14px; } 12 | .html-reporter .duration { color: #aaaaaa; float: right; } 13 | .html-reporter .symbol-summary { overflow: hidden; *zoom: 1; margin: 14px 0; } 14 | .html-reporter .symbol-summary li { display: inline-block; height: 8px; width: 14px; font-size: 16px; } 15 | .html-reporter .symbol-summary li.passed { font-size: 14px; } 16 | .html-reporter .symbol-summary li.passed:before { color: #5e7d00; content: "\02022"; } 17 | .html-reporter .symbol-summary li.failed { line-height: 9px; } 18 | .html-reporter .symbol-summary li.failed:before { color: #b03911; content: "x"; font-weight: bold; margin-left: -1px; } 19 | .html-reporter .symbol-summary li.disabled { font-size: 14px; } 20 | .html-reporter .symbol-summary li.disabled:before { color: #bababa; content: "\02022"; } 21 | .html-reporter .symbol-summary li.pending { line-height: 17px; } 22 | .html-reporter .symbol-summary li.pending:before { color: #ba9d37; content: "*"; } 23 | .html-reporter .exceptions { color: #fff; float: right; margin-top: 5px; margin-right: 5px; } 24 | .html-reporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; } 25 | .html-reporter .bar.failed { background-color: #b03911; } 26 | .html-reporter .bar.passed { background-color: #a6b779; } 27 | .html-reporter .bar.skipped { background-color: #bababa; } 28 | .html-reporter .bar.menu { background-color: #fff; color: #aaaaaa; } 29 | .html-reporter .bar.menu a { color: #333333; } 30 | .html-reporter .bar a { color: white; } 31 | .html-reporter.spec-list .bar.menu.failure-list, .html-reporter.spec-list .results .failures { display: none; } 32 | .html-reporter.failure-list .bar.menu.spec-list, .html-reporter.failure-list .summary { display: none; } 33 | .html-reporter .running-alert { background-color: #666666; } 34 | .html-reporter .results { margin-top: 14px; } 35 | .html-reporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; } 36 | .html-reporter.showDetails .summaryMenuItem:hover { text-decoration: underline; } 37 | .html-reporter.showDetails .detailsMenuItem { font-weight: bold; text-decoration: underline; } 38 | .html-reporter.showDetails .summary { display: none; } 39 | .html-reporter.showDetails #details { display: block; } 40 | .html-reporter .summaryMenuItem { font-weight: bold; text-decoration: underline; } 41 | .html-reporter .summary { margin-top: 14px; } 42 | .html-reporter .summary ul { list-style-type: none; margin-left: 14px; padding-top: 0; padding-left: 0; } 43 | .html-reporter .summary ul.suite { margin-top: 7px; margin-bottom: 7px; } 44 | .html-reporter .summary li.passed a { color: #5e7d00; } 45 | .html-reporter .summary li.failed a { color: #b03911; } 46 | .html-reporter .summary li.pending a { color: #ba9d37; } 47 | .html-reporter .description + .suite { margin-top: 0; } 48 | .html-reporter .suite { margin-top: 14px; } 49 | .html-reporter .suite a { color: #333333; } 50 | .html-reporter .failures .spec-detail { margin-bottom: 28px; } 51 | .html-reporter .failures .spec-detail .description { background-color: #b03911; } 52 | .html-reporter .failures .spec-detail .description a { color: white; } 53 | .html-reporter .result-message { padding-top: 14px; color: #333333; white-space: pre; } 54 | .html-reporter .result-message span.result { display: block; } 55 | .html-reporter .stack-trace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666666; border: 1px solid #ddd; background: white; white-space: pre; } 56 | -------------------------------------------------------------------------------- /tipboard/static/js/lib/jqplot/plugins/jqplot.ciParser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jqPlot 3 | * Pure JavaScript plotting plugin using jQuery 4 | * 5 | * Version: 1.0.8 6 | * Revision: 1250 7 | * 8 | * Copyright (c) 2009-2013 Chris Leonello 9 | * jqPlot is currently available for use in all personal or commercial projects 10 | * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL 11 | * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can 12 | * choose the license that best suits your project and use it accordingly. 13 | * 14 | * Although not required, the author would appreciate an email letting him 15 | * know of any substantial use of jqPlot. You can reach the author at: 16 | * chris at jqplot dot com or see http://www.jqplot.com/info.php . 17 | * 18 | * If you are feeling kind and generous, consider supporting the project by 19 | * making a donation at: http://www.jqplot.com/donate.php . 20 | * 21 | * sprintf functions contained in jqplot.sprintf.js by Ash Searle: 22 | * 23 | * version 2007.04.27 24 | * author Ash Searle 25 | * http://hexmen.com/blog/2007/03/printf-sprintf/ 26 | * http://hexmen.com/js/sprintf.js 27 | * The author (Ash Searle) has placed this code in the public domain: 28 | * "This code is unrestricted: you are free to use it however you like." 29 | * 30 | */ 31 | (function($) { 32 | /** 33 | * Class: $.jqplot.ciParser 34 | * Data Renderer function which converts a custom JSON data object into jqPlot data format. 35 | * Set this as a callable on the jqplot dataRenderer plot option: 36 | * 37 | * > plot = $.jqplot('mychart', [data], { dataRenderer: $.jqplot.ciParser, ... }); 38 | * 39 | * Where data is an object in JSON format or a JSON encoded string conforming to the 40 | * City Index API spec. 41 | * 42 | * Note that calling the renderer function is handled internally by jqPlot. The 43 | * user does not have to call the function. The parameters described below will 44 | * automatically be passed to the ciParser function. 45 | * 46 | * Parameters: 47 | * data - JSON encoded string or object. 48 | * plot - reference to jqPlot Plot object. 49 | * 50 | * Returns: 51 | * data array in jqPlot format. 52 | * 53 | */ 54 | $.jqplot.ciParser = function (data, plot) { 55 | var ret = [], 56 | line, 57 | temp, 58 | i, j, k, kk; 59 | 60 | if (typeof(data) == "string") { 61 | data = $.jqplot.JSON.parse(data, handleStrings); 62 | } 63 | 64 | else if (typeof(data) == "object") { 65 | for (k in data) { 66 | for (i=0; i<data[k].length; i++) { 67 | for (kk in data[k][i]) { 68 | data[k][i][kk] = handleStrings(kk, data[k][i][kk]); 69 | } 70 | } 71 | } 72 | } 73 | 74 | else { 75 | return null; 76 | } 77 | 78 | // function handleStrings 79 | // Checks any JSON encoded strings to see if they are 80 | // encoded dates. If so, pull out the timestamp. 81 | // Expects dates to be represented by js timestamps. 82 | 83 | function handleStrings(key, value) { 84 | var a; 85 | if (value != null) { 86 | if (value.toString().indexOf('Date') >= 0) { 87 | //here we will try to extract the ticks from the Date string in the "value" fields of JSON returned data 88 | a = /^\/Date\((-?[0-9]+)\)\/$/.exec(value); 89 | if (a) { 90 | return parseInt(a[1], 10); 91 | } 92 | } 93 | return value; 94 | } 95 | } 96 | 97 | for (var prop in data) { 98 | line = []; 99 | temp = data[prop]; 100 | switch (prop) { 101 | case "PriceTicks": 102 | for (i=0; i<temp.length; i++) { 103 | line.push([temp[i]['TickDate'], temp[i]['Price']]); 104 | } 105 | break; 106 | case "PriceBars": 107 | for (i=0; i<temp.length; i++) { 108 | line.push([temp[i]['BarDate'], temp[i]['Open'], temp[i]['High'], temp[i]['Low'], temp[i]['Close']]); 109 | } 110 | break; 111 | } 112 | ret.push(line); 113 | } 114 | return ret; 115 | }; 116 | })(jQuery); -------------------------------------------------------------------------------- /tipboard/tests/frontend/lib/jasmine-2.0.0/console.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2008-2013 Pivotal Labs 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | */ 23 | function getJasmineRequireObj() { 24 | if (typeof module !== "undefined" && module.exports) { 25 | return exports; 26 | } else { 27 | window.jasmineRequire = window.jasmineRequire || {}; 28 | return window.jasmineRequire; 29 | } 30 | } 31 | 32 | getJasmineRequireObj().console = function(jRequire, j$) { 33 | j$.ConsoleReporter = jRequire.ConsoleReporter(); 34 | }; 35 | 36 | getJasmineRequireObj().ConsoleReporter = function() { 37 | 38 | var noopTimer = { 39 | start: function(){}, 40 | elapsed: function(){ return 0; } 41 | }; 42 | 43 | function ConsoleReporter(options) { 44 | var print = options.print, 45 | showColors = options.showColors || false, 46 | onComplete = options.onComplete || function() {}, 47 | timer = options.timer || noopTimer, 48 | specCount, 49 | failureCount, 50 | failedSpecs = [], 51 | pendingCount, 52 | ansi = { 53 | green: '\x1B[32m', 54 | red: '\x1B[31m', 55 | yellow: '\x1B[33m', 56 | none: '\x1B[0m' 57 | }; 58 | 59 | this.jasmineStarted = function() { 60 | specCount = 0; 61 | failureCount = 0; 62 | pendingCount = 0; 63 | print("Started"); 64 | printNewline(); 65 | timer.start(); 66 | }; 67 | 68 | this.jasmineDone = function() { 69 | printNewline(); 70 | for (var i = 0; i < failedSpecs.length; i++) { 71 | specFailureDetails(failedSpecs[i]); 72 | } 73 | 74 | printNewline(); 75 | var specCounts = specCount + " " + plural("spec", specCount) + ", " + 76 | failureCount + " " + plural("failure", failureCount); 77 | 78 | if (pendingCount) { 79 | specCounts += ", " + pendingCount + " pending " + plural("spec", pendingCount); 80 | } 81 | 82 | print(specCounts); 83 | 84 | printNewline(); 85 | var seconds = timer.elapsed() / 1000; 86 | print("Finished in " + seconds + " " + plural("second", seconds)); 87 | 88 | printNewline(); 89 | 90 | onComplete(failureCount === 0); 91 | }; 92 | 93 | this.specDone = function(result) { 94 | specCount++; 95 | 96 | if (result.status == "pending") { 97 | pendingCount++; 98 | print(colored("yellow", "*")); 99 | return; 100 | } 101 | 102 | if (result.status == "passed") { 103 | print(colored("green", '.')); 104 | return; 105 | } 106 | 107 | if (result.status == "failed") { 108 | failureCount++; 109 | failedSpecs.push(result); 110 | print(colored("red", 'F')); 111 | } 112 | }; 113 | 114 | return this; 115 | 116 | function printNewline() { 117 | print("\n"); 118 | } 119 | 120 | function colored(color, str) { 121 | return showColors ? (ansi[color] + str + ansi.none) : str; 122 | } 123 | 124 | function plural(str, count) { 125 | return count == 1 ? str : str + "s"; 126 | } 127 | 128 | function repeat(thing, times) { 129 | var arr = []; 130 | for (var i = 0; i < times; i++) { 131 | arr.push(thing); 132 | } 133 | return arr; 134 | } 135 | 136 | function indent(str, spaces) { 137 | var lines = (str || '').split("\n"); 138 | var newArr = []; 139 | for (var i = 0; i < lines.length; i++) { 140 | newArr.push(repeat(" ", spaces).join("") + lines[i]); 141 | } 142 | return newArr.join("\n"); 143 | } 144 | 145 | function specFailureDetails(result) { 146 | printNewline(); 147 | print(result.fullName); 148 | 149 | for (var i = 0; i < result.failedExpectations.length; i++) { 150 | var failedExpectation = result.failedExpectations[i]; 151 | printNewline(); 152 | print(indent(failedExpectation.stack, 2)); 153 | } 154 | 155 | printNewline(); 156 | } 157 | } 158 | 159 | return ConsoleReporter; 160 | }; 161 | -------------------------------------------------------------------------------- /tipboard/tiles/norm_chart.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | 'use strict'; 3 | 4 | var NormChartTile; 5 | 6 | NormChartTile = { 7 | defaultConfig: { 8 | legend: { 9 | location: 's', 10 | renderer: $.jqplot.EnhancedLegendRenderer, 11 | show: true, 12 | rendererOptions: { 13 | numberColumns: 3 14 | } 15 | }, 16 | grid: { 17 | gridLineWidth: 0, 18 | gridLineColor: '#4a4d52', 19 | borderWidth: 0, 20 | shadow: false 21 | }, 22 | axes: { 23 | yaxis: { 24 | tickOptions: { 25 | textColor: 'white', 26 | showMark: false 27 | } 28 | }, 29 | xaxis: { 30 | drawMajorGridlines: false, 31 | show: false, 32 | autoscale: true, 33 | tickOptions: { 34 | showLabel: false, 35 | showMark: false, 36 | shadow: false 37 | } 38 | } 39 | }, 40 | seriesDefaults: { 41 | trendline: { 42 | show: false 43 | }, 44 | pointLabels: { 45 | show: false 46 | }, 47 | rendererOptions: { 48 | smooth: true 49 | }, 50 | showMarker: false 51 | }, 52 | }, 53 | 54 | isRawNormsSet: function (config) { 55 | var isSet; 56 | try { 57 | isSet = config.canvasOverlay.objects; 58 | isSet = typeof isSet === "object" ? true : false; 59 | } catch (e) { 60 | if (e.name === "TypeError") { 61 | isSet = false; 62 | } else { 63 | throw e; 64 | } 65 | } 66 | return isSet; 67 | }, 68 | 69 | hasEasyNorms: function (config) { 70 | var hasIt = typeof config.easyNorms === 'object' ? true : false; 71 | return hasIt; 72 | }, 73 | 74 | useEasyNorms: function (config) { 75 | // config.easyNorms is [[color, y, width], [..]] 76 | config.canvasOverlay = { 77 | show: true, 78 | objects: [] 79 | }; 80 | 81 | $.each(config.easyNorms, function (idx, val) { 82 | var color = ( 83 | Tipboard.DisplayUtils.replaceFromPalette(val[0]) || 84 | Tipboard.DisplayUtils.paletteAsList()[ 85 | // wrap colors when out of list 86 | idx % Tipboard.DisplayUtils.paletteAsList.length 87 | ]); 88 | config.canvasOverlay.objects.push({ 89 | horizontalLine: { 90 | color: color, 91 | y: val[1] || 0, 92 | lineWidth: val[2] || 2, 93 | shadow: false 94 | //lineCap: 'butt', 95 | //xOffset: 0 96 | } 97 | }); 98 | }); 99 | delete config.easyNorms; 100 | }, 101 | 102 | setNorms: function (config) { 103 | if (NormChartTile.isRawNormsSet(config)) { 104 | delete config.easyNorms; 105 | } else { 106 | if (NormChartTile.hasEasyNorms(config)) { 107 | NormChartTile.useEasyNorms(config); 108 | } 109 | } 110 | }, 111 | 112 | updateTile: function (tileId, data, config) { 113 | Tipboard.Dashboard.setDataByKeys( 114 | tileId, data, ['title', 'description'] 115 | ); 116 | var newConfig = $.extend(true, { 117 | // XXX: it could be set in defaultConfig, but initially palette 118 | // is undefined 119 | legend: { 120 | border: Tipboard.DisplayUtils.palette.tile_background, 121 | background: Tipboard.DisplayUtils.palette.tile_background 122 | }, 123 | grid: { 124 | background: Tipboard.DisplayUtils.palette.tile_background 125 | } 126 | }, NormChartTile.defaultConfig, config); 127 | NormChartTile.setNorms(newConfig); 128 | renderersSwapper.swap(newConfig); 129 | var tile = Tipboard.Dashboard.id2node(tileId); 130 | Tipboard.DisplayUtils.rescaleTile(tile); 131 | Tipboard.DisplayUtils.createGraph( 132 | tileId, data.plot_data, newConfig 133 | ); 134 | } 135 | }; 136 | 137 | window.NormChartTile = NormChartTile; 138 | Tipboard.Dashboard.registerUpdateFunction( 139 | 'norm_chart', NormChartTile.updateTile 140 | ); 141 | 142 | }($)); 143 | -------------------------------------------------------------------------------- /tipboard/tests/test_console_commands.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from __future__ import absolute_import 5 | from __future__ import division 6 | from __future__ import print_function 7 | from __future__ import unicode_literals 8 | 9 | import os 10 | import random 11 | import re 12 | import shutil 13 | import string 14 | import sys 15 | import time 16 | import unittest 17 | 18 | from mock import patch 19 | 20 | from tipboard import console, settings 21 | 22 | 23 | #TODO: mute command's output that goes to stdout 24 | class TestConfigFiles(unittest.TestCase): 25 | 26 | def setUp(self): 27 | _temp = __import__('tipboard', fromlist=[str('settings')]) 28 | self.settings = _temp.settings 29 | # we need to make sure that already existing '~/.tipboard' dir won't be 30 | # affected by tests, so we back it up (and restore it in tearDown) 31 | if os.path.exists(self.settings._user_config_dir): 32 | self.orig_conf_dir = self.settings._user_config_dir 33 | rnd_str = ''.join( 34 | random.choice(string.ascii_uppercase + string.digits) 35 | for x in range(8) 36 | ) 37 | self.bkp_conf_dir = '_'.join((self.orig_conf_dir, rnd_str)) 38 | os.rename(self.orig_conf_dir, self.bkp_conf_dir) 39 | 40 | @patch.object(sys, 'argv', [None, 'create_project', 'test_project']) 41 | def test_all_files_created(self): 42 | """ 43 | check if command 'create_project test_project' creates required 44 | project structure. 45 | 46 | project structure contains: 47 | ~/.tipboard 48 | custom_tiles 49 | layout_config.yaml 50 | settings-local.py 51 | """ 52 | console.main() 53 | config_paths = self.project_config_paths() 54 | for path_to_check in config_paths: 55 | self.assertTrue(os.path.exists(path_to_check)) 56 | 57 | @patch.object(sys, 'argv', [None, 'create_project', 'test_project']) 58 | def test_recreate_project(self): 59 | """ 60 | check if command 'create_project test_project' called twice 61 | doesn't affect already existing '~/.tipboard' dir 62 | """ 63 | console.main() 64 | config_paths = self.project_config_paths() 65 | first_times, second_times = [], [] 66 | for path in config_paths: 67 | mod_time = time.ctime(os.path.getmtime(path)) 68 | first_times.append(mod_time) 69 | time.sleep(0.1) 70 | console.main() 71 | for path in config_paths: 72 | mod_time = time.ctime(os.path.getmtime(path)) 73 | second_times.append(mod_time) 74 | for first_time, second_time in zip(first_times, second_times): 75 | item_idx = first_times.index(first_time) 76 | msg = u"file has changed: {}".format(config_paths[item_idx]) 77 | self.assertEqual(first_time, second_time, msg) 78 | 79 | @patch.object(sys, 'argv', [None, 'create_project', 'test_project']) 80 | def test_config_contains_project_name(self): 81 | """check if 'settings-local.py' contains project's name""" 82 | console.main() 83 | project_name_line = "PROJECT_NAME = 'test_project'" 84 | _settings_local = os.path.join( 85 | settings._user_config_dir, 'settings-local.py' 86 | ) 87 | msg = 'settings-local.py does not include {}'.format( 88 | repr(project_name_line) 89 | ) 90 | with open(_settings_local) as settings_file: 91 | self.assertIn(project_name_line, settings_file.read(), msg) 92 | 93 | @patch.object(sys, 'argv', [None, 'create_project', 'test_project']) 94 | def test_config_contains_api_key(self): 95 | """check if 'settings-local.py' contains API key""" 96 | console.main() 97 | api_key_line = re.compile('API_KEY = \'[0-9a-z]{32}\'') 98 | _settings_local = os.path.join( 99 | settings._user_config_dir, 'settings-local.py' 100 | ) 101 | msg = 'settings-local.py does not include API_KEY string' 102 | with open(_settings_local) as settings_file: 103 | self.assertRegexpMatches(settings_file.read(), api_key_line, msg) 104 | 105 | def tearDown(self): 106 | try: 107 | # delete '~/.tipboard' dir that we created for tests 108 | shutil.rmtree(self.settings._user_config_dir) 109 | except (OSError): 110 | pass 111 | try: 112 | if os.path.exists(self.bkp_conf_dir): 113 | # restore conf dir backed up by setUp 114 | os.rename(self.bkp_conf_dir, self.orig_conf_dir) 115 | except AttributeError: 116 | pass 117 | self.reset_settings() 118 | 119 | def reset_settings(self): 120 | reload(self.settings) 121 | 122 | def project_config_paths(self): 123 | config_paths = [ 124 | self.settings._user_config_dir, 125 | os.path.join(self.settings._user_config_dir, 'custom_tiles'), 126 | os.path.join(self.settings._user_config_dir, 'layout_config.yaml'), 127 | os.path.join(self.settings._user_config_dir, 'settings-local.py'), 128 | ] 129 | return config_paths 130 | -------------------------------------------------------------------------------- /tipboard/settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import absolute_import 5 | from __future__ import division 6 | from __future__ import print_function 7 | from __future__ import unicode_literals 8 | 9 | import os 10 | 11 | ############################################################################## 12 | # Stuff that can be / should be overridden in local settings 13 | 14 | # Redis server configuration 15 | REDIS_HOST = 'localhost' 16 | REDIS_PORT = 6379 17 | REDIS_PASSWORD = None 18 | REDIS_DB = 4 19 | 20 | DEBUG = False 21 | API_KEY = 'default api key' 22 | HOST = 'localhost' 23 | PORT = 7272 24 | PROJECT_NAME = 'example' 25 | 26 | # Javascript log level ('1' for 'standard', '2' for 'debug') 27 | JS_LOG_LEVEL = 1 28 | 29 | # Our default color palette 30 | COLORS = { 31 | 'black': '#000000', 32 | 'white': '#FFFFFF', 33 | 'tile_background': '#25282d', 34 | 'red': '#DC5945', 35 | 'yellow': '#FF9618', 36 | 'green': '#94C140', 37 | 'blue': '#12B0C5', 38 | 'violet': '#9C4274', 39 | 'orange': '#EC663C', 40 | 'naval': '#54C5C0', 41 | } 42 | 43 | # how many seconds dashboard is displayed before is flipped 44 | FLIPBOARD_INTERVAL = 0 45 | # file name(s) of EXISTING layouts without extension, eg. ['layout_config'] 46 | FLIPBOARD_SEQUENCE = [] 47 | 48 | # We are using Sentry for catching/aggregating errors 49 | SENTRY_DSN = '' 50 | 51 | ############################################################################## 52 | # Settings below should not be changed directly by user 53 | 54 | # Load local settings (~/.tipboard/local_settings.py) 55 | try: 56 | execfile( 57 | os.path.join(os.path.expanduser("~"), '.tipboard/settings-local.py') 58 | ) 59 | except IOError: 60 | pass 61 | 62 | # Redis config that should be processed after local settings 63 | REDIS = dict( 64 | host=REDIS_HOST, 65 | port=REDIS_PORT, 66 | password=REDIS_PASSWORD, 67 | db=REDIS_DB, 68 | ) 69 | REDIS_ASYNC = dict( 70 | host=REDIS_HOST, 71 | port=REDIS_PORT, 72 | password=REDIS_PASSWORD, 73 | selected_db=REDIS_DB, 74 | ) 75 | REDIS_SYNC = dict( 76 | host=REDIS_HOST, 77 | port=REDIS_PORT, 78 | password=REDIS_PASSWORD, 79 | db=REDIS_DB, 80 | ) 81 | 82 | # Location of Tipboard sources 83 | TIPBOARD_PATH = os.path.dirname(__file__) 84 | 85 | # Tiles' paths which should be examined in given order (i.e. user's --> app's) 86 | TILES_PATHS = [ 87 | os.path.join(os.path.expanduser("~"), '.tipboard/custom_tiles'), 88 | os.path.join(TIPBOARD_PATH, 'tiles'), 89 | ] 90 | 91 | # Determine which layout config should be used 92 | _user_config_dir = os.path.join(os.path.expanduser("~"), '.tipboard') 93 | _user_layout_config = os.path.join(_user_config_dir, 'layout_config.yaml') 94 | _fallback_layout_config = os.path.join( 95 | TIPBOARD_PATH, 'defaults/layout_config.yaml' 96 | ) 97 | if not os.path.exists(_user_layout_config): 98 | LAYOUT_CONFIG = _fallback_layout_config 99 | else: 100 | LAYOUT_CONFIG = _user_layout_config 101 | 102 | # CSS/JS files required by Tipboard 103 | # TODO: do we really need to put this stuff in settings..?? 104 | TIPBOARD_CSS_STYLES = [ 105 | 'css/reset.css', 106 | 'css/jquery.jqplot.css', 107 | 'css/layout.css', 108 | ] 109 | TIPBOARD_JAVASCRIPTS = [ 110 | 'js/lib/jquery.js', 111 | 'js/lib/simplify.js', 112 | 'js/lib/jquery.fullscreen.js', 113 | 'js/lib/jqplot/jquery.jqplot.js', 114 | 'js/lib/jqplot/plugins/jqplot.trendline.js', 115 | 'js/lib/jqplot/plugins/jqplot.canvasAxisTickRenderer.js', 116 | 'js/lib/jqplot/plugins/jqplot.canvasTextRenderer.js', 117 | 'js/lib/jqplot/plugins/jqplot.categoryAxisRenderer.js', 118 | 'js/lib/jqplot/plugins/jqplot.barRenderer.js', 119 | 'js/lib/jqplot/plugins/jqplot.pointLabels.js', 120 | 'js/lib/jqplot/plugins/jqplot.highlighter.js', 121 | 'js/lib/jqplot/plugins/jqplot.dateAxisRenderer.js', 122 | 'js/lib/jqplot/plugins/jqplot.pieRenderer.js', 123 | # XXX: importing .. 124 | #'js/lib/jqplot/plugins/jqplot.BezierCurveRenderer.js', 125 | # .. spoils rendering. Try: first plot from: 126 | # http://www.jqplot.com/deploy/dist/examples/pieTest4.html 127 | 'js/lib/jqplot/plugins/jqplot.blockRenderer.js', 128 | 'js/lib/jqplot/plugins/jqplot.bubbleRenderer.js', 129 | 'js/lib/jqplot/plugins/jqplot.canvasAxisLabelRenderer.js', 130 | 'js/lib/jqplot/plugins/jqplot.canvasOverlay.js', 131 | 'js/lib/jqplot/plugins/jqplot.ciParser.js', 132 | 'js/lib/jqplot/plugins/jqplot.cursor.js', 133 | 'js/lib/jqplot/plugins/jqplot.donutRenderer.js', 134 | 'js/lib/jqplot/plugins/jqplot.dragable.js', 135 | 'js/lib/jqplot/plugins/jqplot.enhancedLegendRenderer.js', 136 | 'js/lib/jqplot/plugins/jqplot.funnelRenderer.js', 137 | 'js/lib/jqplot/plugins/jqplot.json2.js', 138 | 'js/lib/jqplot/plugins/jqplot.logAxisRenderer.js', 139 | 'js/lib/jqplot/plugins/jqplot.mekkoAxisRenderer.js', 140 | 'js/lib/jqplot/plugins/jqplot.mekkoRenderer.js', 141 | 'js/lib/jqplot/plugins/jqplot.meterGaugeRenderer.js', 142 | 'js/lib/jqplot/plugins/jqplot.mobile.js', 143 | 'js/lib/jqplot/plugins/jqplot.ohlcRenderer.js', 144 | 'js/lib/jqplot/plugins/jqplot.pyramidAxisRenderer.js', 145 | 'js/lib/jqplot/plugins/jqplot.pyramidGridRenderer.js', 146 | 'js/lib/jqplot/plugins/jqplot.pyramidRenderer.js', 147 | 'js/tipboard.js', 148 | ] 149 | -------------------------------------------------------------------------------- /tipboard/tiles/line_chart.js: -------------------------------------------------------------------------------- 1 | /*jslint browser: true, devel: true*/ 2 | /*global WebSocket: false, Tipboard: false*/ 3 | 4 | function simplifyLineData(series_data, user_config) { 5 | var config = { 6 | tolerancy: 10, 7 | data_points_limit: 50, // we will TRY to achieve lower number of data points than this 8 | max_simplifying_steps: 5, 9 | simplify_step_multiplicator: 1.5 10 | }; 11 | 12 | $.extend(config, user_config); 13 | 14 | var simplify_data = new Array(); 15 | var return_data = new Array(); 16 | 17 | for(var series = 0; series < series_data.length; series++) { 18 | simplify_data[series] = new Array(); 19 | return_data[series] = new Array(); 20 | 21 | // converting data to format acceptable by simplify.js library 22 | if(typeof series_data[series][0] === typeof []) { 23 | for(var tick = 0; tick < series_data[series].length; tick++) { 24 | simplify_data[series].push({ 25 | x: tick, 26 | y: series_data[series][tick][1], 27 | key: series_data[series][tick][0] 28 | }); 29 | } 30 | } else { 31 | for(var tick = 0; tick < series_data[series].length; tick++) { 32 | simplify_data[series].push({ 33 | x: tick, 34 | y: series_data[series][tick], 35 | key: null 36 | }); 37 | } 38 | } 39 | 40 | var current_tolerance = config.tolerancy; 41 | for(var i = 0; i < config.max_simplifying_steps; i++) { 42 | simplify_data[series] = simplify(simplify_data[series], current_tolerance); 43 | if(simplify_data[series].length < config.data_points_limit) break; 44 | current_tolerance = Math.floor(current_tolerance * config.simplify_step_multiplicator); 45 | } 46 | 47 | // prepare in data format understandable by jqplot 48 | for(var tick = 0; tick < simplify_data[series].length; tick++) { 49 | if(simplify_data[series][tick].key != null) 50 | return_data[series][tick] = [ 51 | simplify_data[series][tick].key, 52 | simplify_data[series][tick].y 53 | ]; 54 | else 55 | return_data[series][simplify_data[series][tick].x] = simplify_data[series][tick].y; 56 | } 57 | 58 | // fill all created gaps with null for jqplot 59 | for(var i = 0; i < return_data[series].length; i++) { 60 | if(!return_data[series][i]) 61 | return_data[series][i] = null; 62 | } 63 | } 64 | 65 | return return_data; 66 | } 67 | 68 | function updateTileLine(tileId, data, meta, tipboard) { 69 | var tile = Tipboard.Dashboard.id2node(tileId); 70 | Tipboard.Dashboard.setDataByKeys(tileId, data, ['subtitle', 'description']); 71 | // config creation 72 | renderersSwapper = new RenderersSwapper(); 73 | renderersSwapper.swap(meta); 74 | var graphColors = { 75 | grid: { 76 | background: tipboard.color.tile_background, 77 | gridLineColor: tipboard.color.tile_background 78 | }, 79 | series: Tipboard.DisplayUtils.paletteAsSeriesColors() 80 | }; 81 | var config = $.extend(true, {}, DEFAULT_LINE_CHART_CONFIG, graphColors, meta); 82 | 83 | // autoscale required nodes and plot 84 | // TODO use Tipboard.Dashboard.buildChart 85 | Tipboard.DisplayUtils.expandLastChild(tile); 86 | Tipboard.DisplayUtils.expandLastChild($(tile).find('.tile-content')[0]); 87 | 88 | if(config.simplify) data.series_list = simplifyLineData(data.series_list, config.simplify); 89 | 90 | Tipboard.Dashboard.chartsIds[tileId] = $.jqplot( 91 | tileId + '-chart', data.series_list, config 92 | ); 93 | } 94 | 95 | Tipboard.Dashboard.registerUpdateFunction('line_chart', updateTileLine); 96 | 97 | var DEFAULT_LINE_CHART_CONFIG = { 98 | legend: { 99 | show: false, 100 | location: 's', 101 | border: '#232526', 102 | background: '#232526' 103 | }, 104 | title: { 105 | show: false 106 | }, 107 | grid: { 108 | drawGridLines: false, 109 | borderWidth: 0, 110 | shadow: false 111 | }, 112 | axes: { 113 | xaxis: { 114 | renderer: $.jqplot.CategoryAxisRenderer, 115 | show: true, 116 | tickOptions: { 117 | showLabel: true, 118 | showMark: false, 119 | shadow: false 120 | } 121 | }, 122 | yaxis: { 123 | show: true, 124 | autoscale: true, 125 | tickOptions: { 126 | showLabel: false, 127 | showMark: false, 128 | shadow: false 129 | } 130 | } 131 | }, 132 | seriesDefaults: { 133 | showMarker: true, 134 | lineWidth: 3, 135 | shadow: false, 136 | trendline: { 137 | lineWidth: 3 138 | }, 139 | pointLabels: { 140 | color: '#ffffff', 141 | xpadding: 10, 142 | ypadding: 10, 143 | location: 'sw' 144 | }, 145 | markerRenderer: $.jqplot.MarkerRenderer, 146 | markerOptions: { 147 | show: true, 148 | style: 'filledCircle', 149 | lineWidth: 2, 150 | size: 9, 151 | shadow: true, 152 | shadowAngle: 45, 153 | shadowOffset: 1, 154 | shadowDepth: 3, 155 | shadowAlpha: 0.07 156 | } 157 | } 158 | }; 159 | -------------------------------------------------------------------------------- /tipboard/api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import absolute_import 5 | from __future__ import division 6 | from __future__ import print_function 7 | from __future__ import unicode_literals 8 | 9 | import logging 10 | import json 11 | import urlparse 12 | 13 | import tornado.escape 14 | import tornado.ioloop 15 | import tornado.web 16 | 17 | import tipboard 18 | 19 | from tipboard import redis_utils, settings 20 | from tipboard.parser import ( 21 | get_config_files_names, 22 | get_tiles_configs, 23 | process_layout_config, 24 | ) 25 | 26 | logger = logging.getLogger(__name__) 27 | 28 | api_version = 'v0.1' 29 | 30 | 31 | class ValidatorMixin(object): 32 | """ 33 | Validators returning None if everything is OK - otherwise suitable HTTP 34 | status and message is set. 35 | """ 36 | def validate_post_request(self, post_field, allowed_fields): 37 | error_msg = None 38 | dict_diff = list(set(post_field) - set(allowed_fields)) 39 | if dict_diff: 40 | self.set_status(400) 41 | error_msg = 'There are fields not allowed in your request.' 42 | for field in allowed_fields: 43 | if field not in post_field.keys(): 44 | self.set_status(400) 45 | error_msg = "There is no '{}' field in your request.".format( 46 | field, 47 | ) 48 | break 49 | return error_msg 50 | 51 | def validate_with_config_file(self, post, parsed_config): 52 | error_msg = None 53 | if post['tile'][0] not in parsed_config['tiles_names']: 54 | self.set_status(404) 55 | error_msg = "Tile's name not found in the configuration file.\n" 56 | elif post['key'][0] not in parsed_config['tiles_keys']: 57 | self.set_status(404) 58 | error_msg = "Tile's key/id not found in the configuration file.\n" 59 | return error_msg 60 | 61 | def validate_with_config_files(self, post): 62 | tiles_configs = get_tiles_configs() 63 | error_msg = self.validate_with_config_file(post, tiles_configs) 64 | return error_msg 65 | 66 | def validate_with_json(self, jsoned_data): 67 | error_msg = None 68 | try: 69 | json.loads(jsoned_data) 70 | except ValueError: 71 | self.set_status(400) 72 | logger.debug('invalid json data: {}'.format(repr(jsoned_data))) 73 | error_msg = 'Invalid JSON data.\n' 74 | return error_msg 75 | 76 | 77 | class ProjectInfo(tornado.web.RequestHandler): 78 | """Handles project info requests (for debugging/diagnostics).""" 79 | def get(self): 80 | # TODO: add info regarding custom/default tiles being used 81 | response = { 82 | 'tipboard_version': tipboard.__version__, 83 | 'project_name': settings.PROJECT_NAME, 84 | 'project_layout_config': settings.LAYOUT_CONFIG, 85 | 'redis_db': settings.REDIS, 86 | } 87 | self.write(response) 88 | 89 | 90 | class Push(tornado.web.RequestHandler, ValidatorMixin): 91 | """ 92 | Handles pushing tile's data. 93 | For pushing tile's config see MetaProperty class. 94 | """ 95 | def post(self): 96 | validation_error = None 97 | post_field = urlparse.parse_qs(self.request.body) 98 | validation_error = self.validate_post_request( 99 | post_field, 100 | ['tile', 'key', 'data'], 101 | ) 102 | if validation_error: 103 | self.write(validation_error) 104 | return 105 | validation_error = self.validate_with_config_files(post_field) 106 | if validation_error: 107 | self.write(validation_error) 108 | return 109 | validation_error = self.validate_with_json(post_field['data'][0]) 110 | if validation_error: 111 | self.write(validation_error) 112 | return 113 | try: 114 | redis_utils.redis_actions( 115 | method='post', 116 | tile_id=post_field['key'][0], 117 | value=post_field['data'][0], 118 | tile=post_field['tile'][0], 119 | ) 120 | except Exception as e: 121 | self.set_status(500) 122 | self.write(e.message) 123 | else: 124 | self.write("Tile's data pushed successfully.\n") 125 | 126 | 127 | class TileData(tornado.web.RequestHandler): 128 | """Handles reading and deleting of tile's data.""" 129 | # TODO: "it's better to ask forgiveness than permission" ;) 130 | def get(self, tile_key): 131 | if redis_utils.key_exist(tile_key): 132 | self.write(redis_utils.redis_actions('get', tile_key)) 133 | else: 134 | self.set_status(404) 135 | self.write('%s key does not exist.\n' % tile_key) 136 | 137 | def delete(self, tile_key): 138 | if redis_utils.key_exist(tile_key): 139 | redis_utils.redis_actions('delete', tile_key) 140 | self.write("Tile's data deleted.\n") 141 | else: 142 | self.set_status(404) 143 | self.write('%s key does not exist.\n' % tile_key) 144 | 145 | 146 | class MetaProperty(tornado.web.RequestHandler, ValidatorMixin): 147 | """Handles requests related to tile config changes.""" 148 | def post(self, tile_key): 149 | post_field = urlparse.parse_qs(self.request.body) 150 | validation_error = self.validate_post_request( 151 | post_field, 152 | ['value'], 153 | ) 154 | if validation_error: 155 | self.write(validation_error) 156 | return 157 | try: 158 | tile_data = redis_utils.get_redis_value(tile_key) 159 | except Exception: 160 | self.set_status(404) 161 | self.write("Can't find key %s.\n" % tile_key) 162 | return 163 | validation_error = self.validate_with_json(post_field['value'][0]) 164 | if validation_error: 165 | self.write(validation_error) 166 | return 167 | tile_data['meta'] = json.loads(post_field['value'][0]) 168 | redis_utils.set_redis_value(tile_key, tile_data) 169 | self.write("Tile's config updated.\n") 170 | 171 | def delete(self, tile_key): 172 | try: 173 | tile_data = redis_utils.get_redis_value(tile_key) 174 | except Exception: 175 | self.set_status(404) 176 | self.write('Cant find %s.\n' % tile_key) 177 | else: 178 | tile_data['meta'] = {} 179 | redis_utils.set_redis_value(tile_key, tile_data) 180 | self.write("Tile's config deleted.\n") 181 | -------------------------------------------------------------------------------- /doc/api.rst: -------------------------------------------------------------------------------- 1 | === 2 | API 3 | === 4 | 5 | One of the advantages of Tipboard is flexibility in feeding tiles with data. We 6 | achieve that by providing a simple, REST API - that way, your feeding scripts 7 | may be written in any language (Python, Ruby, Bash, Perl, PHP - you name it). 8 | The only limitation is the format of input data accepted by a given tile type 9 | (see :ref:`tiles_library` for the details). 10 | 11 | To experiment with resources specified below you can use tools like `Advanced 12 | REST Client <http://chromerestclient.appspot.com/>`_ (Chrome extension), or 13 | `cURL <http://curl.haxx.se/>`_, if you prefer working from command line. For 14 | Python programmers, there's an excellent `Requests 15 | <http://docs.python-requests.org/en/latest/>`_ library, which we strongly 16 | recommend. 17 | 18 | .. _api_key: 19 | 20 | API key 21 | ------- 22 | 23 | To send anything to your tiles, first you have to get your API key. This unique 24 | key is generated for you automatically during Tipboard's installation and may 25 | be read in the ``~/.tipboard/settings-local.py`` file - it is a sequence of 26 | characters starting with ``API_KEY``, e.g.:: 27 | 28 | API_KEY = 'e2c3275d0e1a4bc0da360dd225d74a43' 29 | 30 | If you can't see any such string, just add the key manually (it doesn't have 31 | to be as long and hard to memorize as the one above, though). 32 | 33 | .. note:: 34 | 35 | Every change in ``settings-local.py`` file requires restart of the 36 | application. 37 | 38 | Available resources 39 | ------------------- 40 | 41 | Current API version: **v0.1** 42 | 43 | .. note:: 44 | 45 | In 99% of cases, probably only ``push`` and ``tileconfig`` will be of 46 | interest to you (and maybe ``tiledata`` too). 47 | 48 | 49 | .. http:post:: /api/(api_version)/(api_key)/push 50 | 51 | Feeds tiles with data. Input data should be provided in the format that 52 | complies with the one used in a desired tile. **Note:** a tile to which data 53 | will be sent is defined by the key included in the data sent rather than by 54 | `tile_id` as in cases below. 55 | 56 | :param api_version: version of API to be used 57 | :param api_key: your API key 58 | 59 | **Example request**: 60 | 61 | .. sourcecode:: http 62 | 63 | POST /api/v0.1/my_key/push 64 | Host: localhost:7272 65 | POST data: tile=text key=id_1 data={"text": "Hello world!"} 66 | 67 | **Example response**: 68 | 69 | .. sourcecode:: http 70 | 71 | HTTP/1.1 200 OK 72 | Content-Type: text/html; charset=UTF-8 73 | 74 | Tile's data pushed successfully. 75 | 76 | .. http:post:: /api/(api_version)/(api_key)/tileconfig/(tile_id) 77 | 78 | Configures tile specified by `tile_id`. The configuration should comply with 79 | the specification of a given tile type. 80 | 81 | :param api_version: version of API to be used 82 | :param api_key: your API key 83 | :param tile_id: unique tile's ID from your ``layout_config.yaml`` file 84 | 85 | **Example request**: 86 | 87 | .. sourcecode:: http 88 | 89 | GET /api/v0.1/my_key/tileconfig/id_1 90 | Host: localhost:7272 91 | POST data: value={"font_color": "#00FF00"} 92 | 93 | **Example response**: 94 | 95 | .. sourcecode:: http 96 | 97 | HTTP/1.1 200 OK 98 | Content-Type: text/html; charset=UTF-8 99 | 100 | Tile's config updated. 101 | 102 | 103 | .. http:delete:: /api/(api_version)/(api_key)/tileconfig/(tile_id) 104 | 105 | Resets configuration of the tile specified by `tile_id`. 106 | 107 | :param api_version: version of API to be used 108 | :param api_key: your API key 109 | :param tile_id: unique tile's ID from your ``layout_config.yaml`` file 110 | 111 | **Example request**: 112 | 113 | .. sourcecode:: http 114 | 115 | DELETE /api/v0.1/my_key/tileconfig/id_1 116 | Host: localhost:7272 117 | 118 | **Example response**: 119 | 120 | .. sourcecode:: http 121 | 122 | HTTP/1.1 200 OK 123 | Content-Type: text/html; charset=UTF-8 124 | 125 | Tile's config deleted. 126 | 127 | .. http:get:: /api/(api_version)/(api_key)/tiledata/(tile_id) 128 | 129 | Retrieves data belonging to the tile specified by `tile_id`. May be useful 130 | in cases when you need to re-fetch some parts of your data (e.g. when 131 | updating your team's stats) or just for diagnostics. 132 | 133 | :param api_version: version of API to be used 134 | :param api_key: your API key 135 | :param tile_id: unique tile's ID from your ``layout_config.yaml`` file 136 | 137 | **Example request**: 138 | 139 | .. sourcecode:: http 140 | 141 | GET /api/v0.1/my_key/tiledata/id_1 142 | Host: localhost:7272 143 | 144 | **Example response**: 145 | 146 | .. sourcecode:: http 147 | 148 | HTTP/1.1 200 OK 149 | Content-Type: application/json; charset=UTF-8 150 | 151 | { 152 | "tile_template": "text", 153 | "meta": { 154 | "font_color": "#ff9618", 155 | "font_size": "45px" 156 | }, 157 | "data": { 158 | "text": "Lorem ipsum." 159 | }, 160 | "id": "id_1" 161 | } 162 | 163 | .. http:delete:: /api/(api_version)/(api_key)/tiledata/(tile_id) 164 | 165 | Removes everything belonging to the tile given by `tile_id` from Redis. 166 | 167 | :param api_version: version of API to be used 168 | :param api_key: your API key 169 | :param tile_id: unique tile's ID from your ``layout_config.yaml`` file 170 | 171 | **Example request**: 172 | 173 | .. sourcecode:: http 174 | 175 | DELETE /api/v0.1/my_key/tiledata/id_1 176 | Host: localhost:7272 177 | 178 | **Example response**: 179 | 180 | .. sourcecode:: http 181 | 182 | HTTP/1.1 200 OK 183 | Content-Type: text/html; charset=UTF-8 184 | 185 | Tile's data deleted. 186 | 187 | .. http:get:: /api/(api_version)/(api_key)/info 188 | 189 | Provides information on project and user configuration. This resource has 190 | been created for debugging purposes. 191 | 192 | :param api_version: version of API to be used 193 | :param api_key: your API key 194 | 195 | **Example request**: 196 | 197 | .. sourcecode:: http 198 | 199 | GET /api/v0.1/my_key/info 200 | Host: localhost:7272 201 | 202 | **Example response**: 203 | 204 | .. sourcecode:: http 205 | 206 | HTTP/1.1 200 OK 207 | Content-Type: application/json; charset=UTF-8 208 | 209 | { 210 | "tipboard_version": "1.3.0", 211 | "project_layout_config": "/home/pylabs/.tipboard/layout_config.yaml", 212 | "redis_db": { 213 | "host": "localhost", 214 | "db": 4, 215 | "port": 6379 216 | }, 217 | "project_name": "pylabs" 218 | } 219 | -------------------------------------------------------------------------------- /tipboard/static/js/lib/jquery.fullscreen.js: -------------------------------------------------------------------------------- 1 | (function($, window, documentElement, height, width) { 2 | 3 | // browser detection code courtesy of quirksmode, http://www.quirksmode.org/js/detect.html 4 | // slightly simplified, as well as minor changes for readability purposes 5 | 6 | var BrowserDetect = { 7 | init: function () { 8 | this.browser = this.searchString(this.dataBrowser) || "An unknown browser"; 9 | this.version = this.searchVersion(navigator.userAgent) 10 | || this.searchVersion(navigator.appVersion) 11 | || "an unknown version"; 12 | this.OS = this.searchString(this.dataOS) || "an unknown OS"; 13 | }, 14 | 15 | searchString: function (data) { 16 | for (var i=0;i<data.length;i++) { 17 | var dataString = data[i].string; 18 | var dataProp = data[i].prop; 19 | this.versionSearchString = data[i].versionSearch || data[i].identity; 20 | if (dataString) { 21 | if (dataString.indexOf(data[i].subString) != -1) 22 | return data[i].identity; 23 | } 24 | else if (dataProp) 25 | return data[i].identity; 26 | } 27 | }, 28 | 29 | searchVersion: function (dataString) { 30 | var index = dataString.indexOf(this.versionSearchString); 31 | if (index == -1) return; 32 | return parseFloat(dataString.substring(index+this.versionSearchString.length+1)); 33 | }, 34 | 35 | dataBrowser: [ 36 | { string: navigator.userAgent, subString: "Chrome", identity: "Chrome" }, 37 | { string: navigator.vendor, subString: "Apple", identity: "Safari", versionSearch: "Version" }, 38 | { prop: window.opera, identity: "Opera", versionSearch: "Version" }, 39 | { string: navigator.userAgent, subString: "Firefox", identity: "Firefox" }, 40 | { string: navigator.userAgent, subString: "MSIE", identity: "Explorer", versionSearch: "MSIE" } 41 | ], 42 | 43 | dataOS : [ 44 | { string: navigator.platform, subString: "Win", identity: "Windows" }, 45 | { string: navigator.platform, subString: "Mac", identity: "Mac" }, 46 | { string: navigator.platform, subString: "Linux", identity: "Linux" } 47 | ] 48 | 49 | }; 50 | 51 | BrowserDetect.init(); 52 | // Browser name: BrowserDetect.browser 53 | // Browser version: BrowserDetect.version 54 | // OS name: BrowserDetect.OS 55 | 56 | // here are major browsers' keyboard mapping for triggering fullscreen on/off 57 | var keys = { 58 | "MSIE": { 59 | "Windows": { ctrlKey: false, altKey: false, metaKey: false, shiftKey: false, which: 122, string: "F11", alt: "F11" } 60 | }, 61 | "Firefox": { 62 | "Windows": { ctrlKey: false, altKey: false, metaKey: false, shiftKey: false, which: 122, string: "F11", alt: "F11" }, 63 | "Linux": { ctrlKey: false, altKey: false, metaKey: false, shiftKey: false, which: 122, string: "F11", alt: "F11" }, 64 | "Mac": { ctrlKey: false, altKey: false, metaKey: true, shiftKey: true, which: 70, string: "⇧⌘F", alt: "Shift+Command+F" } 65 | }, 66 | "Chrome": { 67 | "Windows": { ctrlKey: false, altKey: false, metaKey: false, shiftKey: false, which: 122, string: "F11", alt: "F11" }, 68 | "Linux": { ctrlKey: false, altKey: false, metaKey: false, shiftKey: false, which: 122, string: "F11", alt: "F11" }, 69 | "Mac": { ctrlKey: false, altKey: false, metaKey: true, shiftKey: true, which: 70, string: "⇧⌘F", alt: "Shift+Command+F" } 70 | }, 71 | "Safari": { // still missing Safari on Windows... help! 72 | "Mac": { ctrlKey: true, altKey: false, metaKey: false, shiftKey: true, which: 70, string: "^⌘F", alt: "Control+Command+F" } 73 | }, 74 | "Opera": { 75 | "Windows": { ctrlKey: false, altKey: false, metaKey: false, shiftKey: false, which: 122, string: "F11", alt: "F11" }, 76 | "Linux": { ctrlKey: false, altKey: false, metaKey: false, shiftKey: false, which: 122, string: "F11", alt: "F11" }, 77 | "Mac": { ctrlKey: false, altKey: false, metaKey: true, shiftKey: true, which: 70, string: "⇧⌘F", alt: "Shift+Command+F" } 78 | }, 79 | 80 | }; 81 | 82 | var 83 | isFullScreen = function() { 84 | return (documentElement.clientHeight == height && documentElement.clientWidth == width) || 85 | window.fullScreen || 86 | (window.outerHeight == height && window.outerWidth == width) || 87 | (BrowserDetect.browser == "Safari" && window.outerHeight == (height - 40) && window.outerWidth == width) 88 | ; 89 | } 90 | ,$window = $(window) 91 | ; 92 | 93 | var thisKeys = keys[BrowserDetect.browser][BrowserDetect.OS]; 94 | var shortcut = { shortcut: thisKeys.string, longform: thisKeys.alt }; 95 | 96 | $window 97 | .data('fullscreen-state', isFullScreen()) 98 | .data('fullscreen-key', shortcut) 99 | .resize(function() { 100 | var fullscreenState = isFullScreen(); 101 | 102 | if ($window.data('fullscreen-state') && !fullscreenState) { 103 | $window 104 | .data('fullscreen-state', fullscreenState) 105 | .trigger('fullscreen-toggle', [false]) 106 | .trigger('fullscreen-off') 107 | ; 108 | } 109 | else if (!$window.data('fullscreen-state') && fullscreenState) { 110 | $window 111 | .data('fullscreen-state', fullscreenState) 112 | .trigger('fullscreen-toggle', [true]) 113 | .trigger('fullscreen-on') 114 | ; 115 | } 116 | }) 117 | .keydown(function(e) { 118 | if (thisKeys && e.ctrlKey == thisKeys.ctrlKey && e.altKey == thisKeys.altKey && e.metaKey == thisKeys.metaKey && e.shiftKey == thisKeys.shiftKey && e.which == thisKeys.which) 119 | $window.trigger('fullscreen-key', [thisKeys.string, thisKeys.alt]); 120 | }) 121 | ; 122 | 123 | })(jQuery, this, document.documentElement, screen.height, screen.width); 124 | -------------------------------------------------------------------------------- /doc/tile__advanced_plot.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | ``advanced_plot`` 3 | ================= 4 | 5 | .. image:: img/smaller/advanced-plot.png 6 | 7 | **Description** 8 | 9 | This tile is for more demanding users. It basically allows to display arbitrary 10 | type of chart/plot from the `jqPlot <http://www.jqplot.com/>`_ library, along 11 | with the title and description (both are optional). 12 | 13 | Before you start experimenting with jqPlot library, we suggest to familiarize 14 | yourself with `this manual 15 | <http://www.jqplot.com/docs/files/usage-txt.html#jqPlot_Usage>`_. After that 16 | you should check out `options tutorial 17 | <http://www.jqplot.com/docs/files/optionsTutorial-txt.html#Options_Tutorial>`_ 18 | and `options summary <http://www.jqplot.com/docs/files/jqplot-core-js.html>`_. 19 | 20 | Here you will find `some examples <http://www.jqplot.com/deploy/dist/examples/>`_. 21 | 22 | **Content** 23 | 24 | :: 25 | 26 | "data" = { 27 | "title": "<tile>", 28 | "description": "<description>", 29 | "plot_data": "<data>" 30 | } 31 | 32 | where: 33 | 34 | .. describe:: title, description 35 | 36 | Title and description (subtitle) for the tile. 37 | 38 | .. describe:: plot_data 39 | 40 | Data that will be fed directly to your plot. Its form depends on the 41 | specific type of plot that you are going to use - see jqPlot's documentation 42 | for the details. 43 | 44 | Example (using horizontal `Bar Chart 45 | <http://www.jqplot.com/deploy/dist/examples/barTest.html>`_ - third example 46 | from the top):: 47 | 48 | curl http://localhost:7272/api/v0.1/<api_key>/push 49 | -X POST 50 | -d "tile=advanced_plot" 51 | -d "key=<tile_id>" 52 | -d 'data={"title": "Metric Tons per Year", "description": "", 53 | "plot_data": [[[2,1], [4,2], [6,3], [3,4]], 54 | [[5,1], [1,2], [3,3], [4,4]], 55 | [[4,1], [7,2], [1,3], [2,4]]]}' 56 | 57 | .. note:: 58 | 59 | Keep in mind that ``advanced_plot`` can display arbitrary charts from jqPlot 60 | library, and more than often they are quite different when it comes to the 61 | parameters required etc. 62 | 63 | **Configuration** 64 | 65 | :: 66 | 67 | value = { 68 | "value": "<jqplot_config>" 69 | } 70 | 71 | where: 72 | 73 | .. describe:: value 74 | 75 | Raw configuration that will be passed directly to jqPlot and which should 76 | obey the rules defined by the jqPlot library. Internally, this config will 77 | be passed as ``$.jqplot(some-container, some-data, our-config)``. 78 | 79 | If such configuration contains one of jqPlot's renderers, its name should be 80 | passed as a string, according to the table below: 81 | 82 | +--------------------------------------+-------------------------------+ 83 | | jqPlot's renderer | string to send | 84 | +======================================+===============================+ 85 | | ``$.jqplot.BarRenderer`` | ``"BarRenderer"`` | 86 | +--------------------------------------+-------------------------------+ 87 | | ``$.jqplot.BlockRenderer`` | ``"BlockRenderer"`` | 88 | +--------------------------------------+-------------------------------+ 89 | | ``$.jqplot.BubbleRenderer`` | ``"BubbleRenderer"`` | 90 | +--------------------------------------+-------------------------------+ 91 | | ``$.jqplot.CanvasAxisLabelRenderer`` | ``"CanvasAxisLabelRenderer"`` | 92 | +--------------------------------------+-------------------------------+ 93 | | ``$.jqplot.CanvasAxisTickRenderer`` | ``"CanvasAxisTickRenderer"`` | 94 | +--------------------------------------+-------------------------------+ 95 | | ``$.jqplot.CanvasTextRenderer`` | ``"CanvasTextRenderer"`` | 96 | +--------------------------------------+-------------------------------+ 97 | | ``$.jqplot.CategoryAxisRenderer`` | ``"CategoryAxisRenderer"`` | 98 | +--------------------------------------+-------------------------------+ 99 | | ``$.jqplot.DateAxisRenderer`` | ``"DateAxisRenderer"`` | 100 | +--------------------------------------+-------------------------------+ 101 | | ``$.jqplot.DonutRenderer`` | ``"DonutRenderer"`` | 102 | +--------------------------------------+-------------------------------+ 103 | | ``$.jqplot.EnhancedLegendRenderer`` | ``"EnhancedLegendRenderer"`` | 104 | +--------------------------------------+-------------------------------+ 105 | | ``$.jqplot.FunnelRenderer`` | ``"FunnelRenderer"`` | 106 | +--------------------------------------+-------------------------------+ 107 | | ``$.jqplot.LogAxisRenderer`` | ``"LogAxisRenderer"`` | 108 | +--------------------------------------+-------------------------------+ 109 | | ``$.jqplot.MekkoAxisRenderer`` | ``"MekkoAxisRenderer"`` | 110 | +--------------------------------------+-------------------------------+ 111 | | ``$.jqplot.MekkoRenderer`` | ``"MekkoRenderer"`` | 112 | +--------------------------------------+-------------------------------+ 113 | | ``$.jqplot.MeterGaugeRenderer`` | ``"MeterGaugeRenderer"`` | 114 | +--------------------------------------+-------------------------------+ 115 | | ``$.jqplot.OhlcRenderer`` | ``"OhlcRenderer"`` | 116 | +--------------------------------------+-------------------------------+ 117 | | ``$.jqplot.PieRenderer`` | ``"PieRenderer"`` | 118 | +--------------------------------------+-------------------------------+ 119 | | ``$.jqplot.PyramidAxisRenderer`` | ``"PyramidAxisRenderer"`` | 120 | +--------------------------------------+-------------------------------+ 121 | | ``$.jqplot.PyramidGridRenderer`` | ``"PyramidGridRenderer"`` | 122 | +--------------------------------------+-------------------------------+ 123 | | ``$.jqplot.PyramidRenderer`` | ``"PyramidRenderer"`` | 124 | +--------------------------------------+-------------------------------+ 125 | 126 | Example (using horizontal `Bar Chart 127 | <http://www.jqplot.com/deploy/dist/examples/barTest.html>`_ - third example 128 | from the top):: 129 | 130 | curl http://localhost:7272/api/v0.1/<api_key>/tileconfig/<tile_id> 131 | -X POST 132 | -d 'value={ 133 | "seriesDefaults": { 134 | "trendline": {"show": false}, 135 | "renderer":"BarRenderer", 136 | "pointLabels": {"show": true, "location": "e", "edgeTolerance": -15}, 137 | "shadowAngle": 135, 138 | "rendererOptions": {"barDirection": "horizontal"} 139 | }, 140 | "axes": {"yaxis": { "renderer": "CategoryAxisRenderer"}}}' 141 | -------------------------------------------------------------------------------- /tipboard/static/css/jquery.jqplot.css: -------------------------------------------------------------------------------- 1 | /*rules for the plot target div. These will be cascaded down to all plot elements according to css rules*/ 2 | .jqplot-target { 3 | position: relative; 4 | color: #666666; 5 | font-family: "Trebuchet MS", Arial, Helvetica, sans-serif; 6 | font-size: 1em; 7 | /* height: 300px; 8 | width: 400px;*/ 9 | } 10 | 11 | /*rules applied to all axes*/ 12 | .jqplot-axis { 13 | font-size: 0.75em; 14 | } 15 | 16 | .jqplot-xaxis { 17 | margin-top: 10px; 18 | } 19 | 20 | .jqplot-x2axis { 21 | margin-bottom: 10px; 22 | } 23 | 24 | .jqplot-yaxis { 25 | margin-right: 10px; 26 | } 27 | 28 | .jqplot-y2axis, .jqplot-y3axis, .jqplot-y4axis, .jqplot-y5axis, .jqplot-y6axis, .jqplot-y7axis, .jqplot-y8axis, .jqplot-y9axis, .jqplot-yMidAxis { 29 | margin-left: 10px; 30 | margin-right: 10px; 31 | } 32 | 33 | /*rules applied to all axis tick divs*/ 34 | .jqplot-axis-tick, .jqplot-xaxis-tick, .jqplot-yaxis-tick, .jqplot-x2axis-tick, .jqplot-y2axis-tick, .jqplot-y3axis-tick, .jqplot-y4axis-tick, .jqplot-y5axis-tick, .jqplot-y6axis-tick, .jqplot-y7axis-tick, .jqplot-y8axis-tick, .jqplot-y9axis-tick, .jqplot-yMidAxis-tick { 35 | position: absolute; 36 | white-space: pre; 37 | } 38 | 39 | 40 | .jqplot-xaxis-tick { 41 | top: 0px; 42 | /* initial position untill tick is drawn in proper place */ 43 | left: 15px; 44 | /* padding-top: 10px;*/ 45 | vertical-align: top; 46 | } 47 | 48 | .jqplot-x2axis-tick { 49 | bottom: 0px; 50 | /* initial position untill tick is drawn in proper place */ 51 | left: 15px; 52 | /* padding-bottom: 10px;*/ 53 | vertical-align: bottom; 54 | } 55 | 56 | .jqplot-yaxis-tick { 57 | right: 0px; 58 | /* initial position untill tick is drawn in proper place */ 59 | top: 15px; 60 | /* padding-right: 10px;*/ 61 | text-align: right; 62 | } 63 | 64 | .jqplot-yaxis-tick.jqplot-breakTick { 65 | right: -20px; 66 | margin-right: 0px; 67 | padding:1px 5px 1px 5px; 68 | /*background-color: white;*/ 69 | z-index: 2; 70 | font-size: 1.5em; 71 | } 72 | 73 | .jqplot-y2axis-tick, .jqplot-y3axis-tick, .jqplot-y4axis-tick, .jqplot-y5axis-tick, .jqplot-y6axis-tick, .jqplot-y7axis-tick, .jqplot-y8axis-tick, .jqplot-y9axis-tick { 74 | left: 0px; 75 | /* initial position untill tick is drawn in proper place */ 76 | top: 15px; 77 | /* padding-left: 10px;*/ 78 | /* padding-right: 15px;*/ 79 | text-align: left; 80 | } 81 | 82 | .jqplot-yMidAxis-tick { 83 | text-align: center; 84 | white-space: nowrap; 85 | } 86 | 87 | .jqplot-xaxis-label { 88 | margin-top: 10px; 89 | font-size: 11pt; 90 | position: absolute; 91 | } 92 | 93 | .jqplot-x2axis-label { 94 | margin-bottom: 10px; 95 | font-size: 11pt; 96 | position: absolute; 97 | } 98 | 99 | .jqplot-yaxis-label { 100 | margin-right: 10px; 101 | /* text-align: center;*/ 102 | font-size: 11pt; 103 | position: absolute; 104 | } 105 | 106 | .jqplot-yMidAxis-label { 107 | font-size: 11pt; 108 | position: absolute; 109 | } 110 | 111 | .jqplot-y2axis-label, .jqplot-y3axis-label, .jqplot-y4axis-label, .jqplot-y5axis-label, .jqplot-y6axis-label, .jqplot-y7axis-label, .jqplot-y8axis-label, .jqplot-y9axis-label { 112 | /* text-align: center;*/ 113 | font-size: 11pt; 114 | margin-left: 10px; 115 | position: absolute; 116 | } 117 | 118 | .jqplot-meterGauge-tick { 119 | font-size: 0.75em; 120 | color: #999999; 121 | } 122 | 123 | .jqplot-meterGauge-label { 124 | font-size: 1em; 125 | color: #999999; 126 | } 127 | 128 | table.jqplot-table-legend { 129 | margin-top: 12px; 130 | margin-bottom: 12px; 131 | margin-left: 12px; 132 | margin-right: 12px; 133 | } 134 | 135 | table.jqplot-table-legend, table.jqplot-cursor-legend { 136 | background-color: rgba(255,255,255,0.6); 137 | border: 1px solid #cccccc; 138 | position: absolute; 139 | font-size: 0.75em; 140 | } 141 | 142 | td.jqplot-table-legend { 143 | vertical-align:middle; 144 | } 145 | 146 | /* 147 | These rules could be used instead of assigning 148 | element styles and relying on js object properties. 149 | */ 150 | 151 | /* 152 | td.jqplot-table-legend-swatch { 153 | padding-top: 0.5em; 154 | text-align: center; 155 | } 156 | 157 | tr.jqplot-table-legend:first td.jqplot-table-legend-swatch { 158 | padding-top: 0px; 159 | } 160 | */ 161 | 162 | td.jqplot-seriesToggle:hover, td.jqplot-seriesToggle:active { 163 | cursor: pointer; 164 | } 165 | 166 | .jqplot-table-legend .jqplot-series-hidden { 167 | text-decoration: line-through; 168 | } 169 | 170 | div.jqplot-table-legend-swatch-outline { 171 | border: 1px solid #cccccc; 172 | padding:1px; 173 | } 174 | 175 | div.jqplot-table-legend-swatch { 176 | width:0px; 177 | height:0px; 178 | border-top-width: 5px; 179 | border-bottom-width: 5px; 180 | border-left-width: 6px; 181 | border-right-width: 6px; 182 | border-top-style: solid; 183 | border-bottom-style: solid; 184 | border-left-style: solid; 185 | border-right-style: solid; 186 | } 187 | 188 | .jqplot-title { 189 | top: 0px; 190 | left: 0px; 191 | padding-bottom: 0.5em; 192 | font-size: 1.2em; 193 | } 194 | 195 | table.jqplot-cursor-tooltip { 196 | border: 1px solid #cccccc; 197 | font-size: 0.75em; 198 | } 199 | 200 | 201 | .jqplot-cursor-tooltip { 202 | border: 1px solid #cccccc; 203 | font-size: 0.75em; 204 | white-space: nowrap; 205 | background: rgba(208,208,208,0.5); 206 | padding: 1px; 207 | } 208 | 209 | .jqplot-highlighter-tooltip, .jqplot-canvasOverlay-tooltip { 210 | border: 1px solid #cccccc; 211 | font-size: 0.75em; 212 | white-space: nowrap; 213 | background: rgba(208,208,208,0.5); 214 | padding: 1px; 215 | } 216 | 217 | .jqplot-point-label { 218 | font-size: 0.75em; 219 | z-index: 2; 220 | } 221 | 222 | td.jqplot-cursor-legend-swatch { 223 | vertical-align: middle; 224 | text-align: center; 225 | } 226 | 227 | div.jqplot-cursor-legend-swatch { 228 | width: 1.2em; 229 | height: 0.7em; 230 | } 231 | 232 | .jqplot-error { 233 | /* Styles added to the plot target container when there is an error go here.*/ 234 | text-align: center; 235 | } 236 | 237 | .jqplot-error-message { 238 | /* Styling of the custom error message div goes here.*/ 239 | position: relative; 240 | top: 46%; 241 | display: inline-block; 242 | } 243 | 244 | div.jqplot-bubble-label { 245 | font-size: 0.8em; 246 | /* background: rgba(90%, 90%, 90%, 0.15);*/ 247 | padding-left: 2px; 248 | padding-right: 2px; 249 | color: rgb(20%, 20%, 20%); 250 | } 251 | 252 | div.jqplot-bubble-label.jqplot-bubble-label-highlight { 253 | background: rgba(90%, 90%, 90%, 0.7); 254 | } 255 | 256 | div.jqplot-noData-container { 257 | text-align: center; 258 | background-color: rgba(96%, 96%, 96%, 0.3); 259 | } 260 | -------------------------------------------------------------------------------- /tipboard/extras/jira-ds.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Jira-ds - a command line utility for fetching various data from Jira. 5 | 6 | Usage: 7 | jira-ds.py --project=NAME (--summary|--cvsr|--issues-counts) 8 | jira-ds.py --board=NAME (--control-chart|--velocity) 9 | jira-ds.py --jql=QUERY [-m <limit>] 10 | jira-ds.py -h | --help 11 | 12 | Options: 13 | --project=NAME Project name (case sensitive). 14 | --board=NAME Board name (case sensitive). 15 | --jql=QUERY Return raw data using JQL (Jira Query Language) query. 16 | Beware that some queries may be quite heavy on resources! 17 | -m <limit> Set 'maxResults' for '--jql' option [default: 1000]. 18 | (hint: setting it to '0' gives you total number of results 19 | without fetching the data) 20 | --summary Get issues counts from 'summary' tab. 21 | --cvsr Get data table from 'created vs resolved' report 22 | (past month). 23 | --issues-counts Get issues counts grouped by priority, status, type etc. 24 | --control-chart Get data table from 'control chart' report (past month). 25 | --velocity Get data table from 'velocity chart' report. 26 | -h --help Show this screen. 27 | 28 | """ 29 | 30 | import json 31 | import os 32 | import requests 33 | import subprocess 34 | 35 | from docopt import docopt 36 | 37 | JIRA_BASE_URL = '' # needs to be filled in 38 | JIRA_API_URL = JIRA_BASE_URL + '/rest/api/2/' 39 | GREENHOPPER_API_URL = JIRA_BASE_URL + '/rest/greenhopper/1.0/' 40 | JIRA_CREDENTIALS = { # needs to be filled in 41 | 'user': '', 42 | 'password': '', 43 | } 44 | SUBP_ENV = os.environ.copy() 45 | SUBP_ENV['JIRA_USER'] = JIRA_CREDENTIALS.get('user') 46 | SUBP_ENV['JIRA_PASSWORD'] = JIRA_CREDENTIALS.get('password') 47 | SUBP_PARAMS = ["casperjs", "jira-ds.js"] 48 | REQUESTS_TIMEOUT = 10 # in seconds 49 | 50 | 51 | ##### 'Internal' stuff. 52 | 53 | def _get_identifiers(name, url): 54 | """ 55 | Gets project_id and project_key (when 'url' points to project) or board_id 56 | (when 'url' points to board). 57 | """ 58 | session = requests.Session() 59 | resp = session.get(url, timeout=REQUESTS_TIMEOUT, params={ 60 | 'os_authType': 'basic', 61 | 'os_username': JIRA_CREDENTIALS.get('user'), 62 | 'os_password': JIRA_CREDENTIALS.get('password'), 63 | }) 64 | resp.raise_for_status() 65 | rest_data = resp.json() 66 | try: 67 | data = rest_data.get('views') # for boards 68 | except AttributeError: 69 | data = rest_data # for projects 70 | for d in data: 71 | if d.get('name') == name: 72 | return (d.get('id'), d.get('key')) 73 | return (None, None) # board/project doesn't exist 74 | 75 | 76 | def _get_data_from_subprocess(subp_params): 77 | try: 78 | data = subprocess.check_output(subp_params, env=SUBP_ENV) 79 | except subprocess.CalledProcessError as e: 80 | print("Error in jira-ds.js script: '{}'.".format(e.output.strip())) 81 | return 82 | else: 83 | return json.loads(data) 84 | 85 | 86 | ##### Command-line options. 87 | 88 | def get_summary(project_key): 89 | subp_params = SUBP_PARAMS + ["--project-key=" + project_key, 90 | "--step=summary"] 91 | return _get_data_from_subprocess(subp_params) 92 | 93 | 94 | def get_cvsr(project_id): 95 | subp_params = SUBP_PARAMS + ["--project-id=" + project_id, "--step=cvsr"] 96 | return _get_data_from_subprocess(subp_params) 97 | 98 | 99 | def get_issues_counts(project_key): 100 | subp_params = SUBP_PARAMS + ["--project-key=" + project_key, 101 | "--step=issues-counts"] 102 | return _get_data_from_subprocess(subp_params) 103 | 104 | 105 | def get_control_chart(board_id): 106 | subp_params = SUBP_PARAMS + ["--board-id=" + str(board_id), 107 | "--step=control-chart"] 108 | return _get_data_from_subprocess(subp_params) 109 | 110 | 111 | def get_velocity(board_id): 112 | subp_params = SUBP_PARAMS + ["--board-id=" + str(board_id), 113 | "--step=velocity"] 114 | return _get_data_from_subprocess(subp_params) 115 | 116 | 117 | def get_jql(query, max_results): 118 | session = requests.Session() 119 | query_url = JIRA_API_URL + 'search?' 120 | resp = session.get(query_url, timeout=REQUESTS_TIMEOUT, params={ 121 | 'os_authType': 'basic', 122 | 'os_username': JIRA_CREDENTIALS.get('user'), 123 | 'os_password': JIRA_CREDENTIALS.get('password'), 124 | 'start-index': 0, 125 | 'maxResults': max_results, 126 | 'jql': query, 127 | }) 128 | resp.raise_for_status() 129 | rest_data = resp.json() 130 | return(rest_data) 131 | 132 | 133 | ##### Putting it all together. 134 | 135 | def main(): 136 | args = docopt(__doc__) 137 | project_name = args.get('--project') 138 | board_name = args.get('--board') 139 | jql = args.get('--jql') 140 | if project_name: 141 | url = JIRA_API_URL + 'project' 142 | try: 143 | project_id, project_key = _get_identifiers(project_name, url) 144 | except requests.exceptions.RequestException as e: 145 | print("Your query has raised an error: '{}'.".format(e.message)) 146 | return 147 | else: 148 | if not project_id: 149 | print("Project '{}' does not exist.").format(project_name) 150 | return 151 | if args.get('--summary'): 152 | result = get_summary(project_key) 153 | elif args.get('--cvsr'): 154 | result = get_cvsr(project_id) 155 | elif args.get('--issues-counts'): 156 | result = get_issues_counts(project_key) 157 | elif board_name: 158 | url = GREENHOPPER_API_URL + 'rapidview' 159 | try: 160 | board_id, _ = _get_identifiers(board_name, url) 161 | except requests.exceptions.RequestException as e: 162 | print("Your query has raised an error: '{}'.".format(e.message)) 163 | return 164 | else: 165 | if not board_id: 166 | print("Board '{}' does not exist.").format(board_name) 167 | return 168 | if args.get('--control-chart'): 169 | result = get_control_chart(board_id) 170 | elif args.get('--velocity'): 171 | result = get_velocity(board_id) 172 | elif jql: 173 | max_results = args.get('-m') 174 | try: 175 | result = get_jql(jql, max_results) 176 | except requests.exceptions.RequestException as e: 177 | print("Your query has raised an error: '{}'.".format(e.message)) 178 | return 179 | print(json.dumps(result, ensure_ascii=False)) 180 | 181 | if __name__ == '__main__': 182 | main() 183 | -------------------------------------------------------------------------------- /tipboard/tests/frontend/lib/jasmine-2.0.0/boot.js: -------------------------------------------------------------------------------- 1 | /** 2 | Starting with version 2.0, this file "boots" Jasmine, performing all of the necessary initialization before executing the loaded environment and all of a project's specs. This file should be loaded after `jasmine.js`, but before any project source files or spec files are loaded. Thus this file can also be used to customize Jasmine for a project. 3 | 4 | If a project is using Jasmine via the standalone distribution, this file can be customized directly. If a project is using Jasmine via the [Ruby gem][jasmine-gem], this file can be copied into the support directory via `jasmine copy_boot_js`. Other environments (e.g., Python) will have different mechanisms. 5 | 6 | The location of `boot.js` can be specified and/or overridden in `jasmine.yml`. 7 | 8 | [jasmine-gem]: http://github.com/pivotal/jasmine-gem 9 | */ 10 | 11 | (function() { 12 | 13 | /** 14 | * ## Require & Instantiate 15 | * 16 | * Require Jasmine's core files. Specifically, this requires and attaches all of Jasmine's code to the `jasmine` reference. 17 | */ 18 | window.jasmine = jasmineRequire.core(jasmineRequire); 19 | 20 | /** 21 | * Since this is being run in a browser and the results should populate to an HTML page, require the HTML-specific Jasmine code, injecting the same reference. 22 | */ 23 | jasmineRequire.html(jasmine); 24 | 25 | /** 26 | * Create the Jasmine environment. This is used to run all specs in a project. 27 | */ 28 | var env = jasmine.getEnv(); 29 | 30 | /** 31 | * ## The Global Interface 32 | * 33 | * Build up the functions that will be exposed as the Jasmine public interface. A project can customize, rename or alias any of these functions as desired, provided the implementation remains unchanged. 34 | */ 35 | var jasmineInterface = { 36 | describe: function(description, specDefinitions) { 37 | return env.describe(description, specDefinitions); 38 | }, 39 | 40 | xdescribe: function(description, specDefinitions) { 41 | return env.xdescribe(description, specDefinitions); 42 | }, 43 | 44 | it: function(desc, func) { 45 | return env.it(desc, func); 46 | }, 47 | 48 | xit: function(desc, func) { 49 | return env.xit(desc, func); 50 | }, 51 | 52 | beforeEach: function(beforeEachFunction) { 53 | return env.beforeEach(beforeEachFunction); 54 | }, 55 | 56 | afterEach: function(afterEachFunction) { 57 | return env.afterEach(afterEachFunction); 58 | }, 59 | 60 | expect: function(actual) { 61 | return env.expect(actual); 62 | }, 63 | 64 | pending: function() { 65 | return env.pending(); 66 | }, 67 | 68 | spyOn: function(obj, methodName) { 69 | return env.spyOn(obj, methodName); 70 | }, 71 | 72 | jsApiReporter: new jasmine.JsApiReporter({ 73 | timer: new jasmine.Timer() 74 | }) 75 | }; 76 | 77 | /** 78 | * Add all of the Jasmine global/public interface to the proper global, so a project can use the public interface directly. For example, calling `describe` in specs instead of `jasmine.getEnv().describe`. 79 | */ 80 | if (typeof window == "undefined" && typeof exports == "object") { 81 | extend(exports, jasmineInterface); 82 | } else { 83 | extend(window, jasmineInterface); 84 | } 85 | 86 | /** 87 | * Expose the interface for adding custom equality testers. 88 | */ 89 | jasmine.addCustomEqualityTester = function(tester) { 90 | env.addCustomEqualityTester(tester); 91 | }; 92 | 93 | /** 94 | * Expose the interface for adding custom expectation matchers 95 | */ 96 | jasmine.addMatchers = function(matchers) { 97 | return env.addMatchers(matchers); 98 | }; 99 | 100 | /** 101 | * Expose the mock interface for the JavaScript timeout functions 102 | */ 103 | jasmine.clock = function() { 104 | return env.clock; 105 | }; 106 | 107 | /** 108 | * ## Runner Parameters 109 | * 110 | * More browser specific code - wrap the query string in an object and to allow for getting/setting parameters from the runner user interface. 111 | */ 112 | 113 | var queryString = new jasmine.QueryString({ 114 | getWindowLocation: function() { return window.location; } 115 | }); 116 | 117 | var catchingExceptions = queryString.getParam("catch"); 118 | env.catchExceptions(typeof catchingExceptions === "undefined" ? true : catchingExceptions); 119 | 120 | /** 121 | * ## Reporters 122 | * The `HtmlReporter` builds all of the HTML UI for the runner page. This reporter paints the dots, stars, and x's for specs, as well as all spec names and all failures (if any). 123 | */ 124 | var htmlReporter = new jasmine.HtmlReporter({ 125 | env: env, 126 | onRaiseExceptionsClick: function() { queryString.setParam("catch", !env.catchingExceptions()); }, 127 | getContainer: function() { return document.body; }, 128 | createElement: function() { return document.createElement.apply(document, arguments); }, 129 | createTextNode: function() { return document.createTextNode.apply(document, arguments); }, 130 | timer: new jasmine.Timer() 131 | }); 132 | 133 | /** 134 | * The `jsApiReporter` also receives spec results, and is used by any environment that needs to extract the results from JavaScript. 135 | */ 136 | env.addReporter(jasmineInterface.jsApiReporter); 137 | env.addReporter(htmlReporter); 138 | 139 | /** 140 | * Filter which specs will be run by matching the start of the full name against the `spec` query param. 141 | */ 142 | var specFilter = new jasmine.HtmlSpecFilter({ 143 | filterString: function() { return queryString.getParam("spec"); } 144 | }); 145 | 146 | env.specFilter = function(spec) { 147 | return specFilter.matches(spec.getFullName()); 148 | }; 149 | 150 | /** 151 | * Setting up timing functions to be able to be overridden. Certain browsers (Safari, IE 8, phantomjs) require this hack. 152 | */ 153 | window.setTimeout = window.setTimeout; 154 | window.setInterval = window.setInterval; 155 | window.clearTimeout = window.clearTimeout; 156 | window.clearInterval = window.clearInterval; 157 | 158 | /** 159 | * ## Execution 160 | * 161 | * Replace the browser window's `onload`, ensure it's called, and then run all of the loaded specs. This includes initializing the `HtmlReporter` instance and then executing the loaded Jasmine environment. All of this will happen after all of the specs are loaded. 162 | */ 163 | var currentWindowOnload = window.onload; 164 | 165 | window.onload = function() { 166 | if (currentWindowOnload) { 167 | currentWindowOnload(); 168 | } 169 | htmlReporter.initialize(); 170 | env.execute(); 171 | }; 172 | 173 | /** 174 | * Helper function for readability above. 175 | */ 176 | function extend(destination, source) { 177 | for (var property in source) destination[property] = source[property]; 178 | return destination; 179 | } 180 | 181 | }()); 182 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make <target>' where <target> is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Tipboard.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Tipboard.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Tipboard" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Tipboard" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /tipboard/tests/test_config_parser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from __future__ import absolute_import 5 | from __future__ import division 6 | from __future__ import print_function 7 | from __future__ import unicode_literals 8 | 9 | import os 10 | import random 11 | import shutil 12 | import string 13 | import sys 14 | import unittest 15 | 16 | from mock import patch 17 | 18 | from tipboard import console, parser, settings 19 | 20 | DEFAULT_CONFIG_NAME = 'layout_config' 21 | SECOND_CONFIG_NAME = '2nd-' + DEFAULT_CONFIG_NAME 22 | 23 | 24 | @patch.object(sys, 'argv', [None, 'create_project', 'test_project']) 25 | def _create_test_project(): 26 | console.main() 27 | 28 | 29 | class TestConfigParser(unittest.TestCase): 30 | 31 | def setUp(self): 32 | # backup userspace .tipboard 33 | _temp = __import__('tipboard', fromlist=[str('settings')]) 34 | self.settings = _temp.settings 35 | # we need to make sure that already existing '~/.tipboard' dir won't be 36 | # affected by tests, so we back it up (and restore it in tearDown) 37 | if os.path.exists(self.settings._user_config_dir): 38 | self.orig_conf_dir = self.settings._user_config_dir 39 | rnd_str = ''.join( 40 | random.choice(string.ascii_uppercase + string.digits) 41 | for x in range(8) 42 | ) 43 | self.bkp_conf_dir = '_'.join((self.orig_conf_dir, rnd_str)) 44 | os.rename(self.orig_conf_dir, self.bkp_conf_dir) 45 | _create_test_project() 46 | 47 | def test_get_rows(self): 48 | """simple call of get_rows method and check rows count""" 49 | EXPECTED = 2 50 | config_file = parser.process_layout_config(DEFAULT_CONFIG_NAME) 51 | found_rows = parser._get_rows(config_file['layout']) 52 | rows_count = len(found_rows) 53 | msg = u'Found {} rows instead of expected {}'.format( 54 | rows_count, EXPECTED 55 | ) 56 | self.assertEqual(rows_count, EXPECTED, msg) 57 | 58 | def test_get_rows_cols_validator(self): 59 | """ 60 | negative test: checks if parser's get_rows method find wrong rows count 61 | """ 62 | config_file = parser.process_layout_config(DEFAULT_CONFIG_NAME) 63 | broken_layout = config_file['layout'][:] 64 | popped_row = broken_layout.pop() 65 | row_class, cols_data = popped_row.items()[0] 66 | broken_key = row_class.replace('1', '5') 67 | broken_row = { 68 | broken_key: cols_data, 69 | } 70 | broken_layout.append(broken_row) 71 | try: 72 | parser._get_rows(broken_layout) 73 | except parser.WrongSumOfRows: 74 | # test passed 75 | pass 76 | else: 77 | raise Exception("Parser's get_rows method skipped layout error") 78 | 79 | def tearDown(self): 80 | try: 81 | # delete '~/.tipboard' dir that we created for tests 82 | shutil.rmtree(self.settings._user_config_dir) 83 | except (OSError): 84 | pass 85 | try: 86 | if os.path.exists(self.bkp_conf_dir): 87 | # restore conf dir backed up by setUp 88 | os.rename(self.bkp_conf_dir, self.orig_conf_dir) 89 | except AttributeError: 90 | pass 91 | reload(self.settings) 92 | 93 | 94 | class TestConfigFiles(unittest.TestCase): 95 | 96 | def setUp(self): 97 | # backup userspace .tipboard 98 | _temp = __import__('tipboard', fromlist=[str('settings')]) 99 | self.settings = _temp.settings 100 | # we need to make sure that already existing '~/.tipboard' dir won't be 101 | # affected by tests, so we back it up (and restore it in tearDown) 102 | if os.path.exists(self.settings._user_config_dir): 103 | self.orig_conf_dir = self.settings._user_config_dir 104 | rnd_str = ''.join( 105 | random.choice(string.ascii_uppercase + string.digits) 106 | for x in range(8) 107 | ) 108 | self.bkp_conf_dir = '_'.join((self.orig_conf_dir, rnd_str)) 109 | os.rename(self.orig_conf_dir, self.bkp_conf_dir) 110 | _create_test_project() 111 | self._create_second_config() 112 | 113 | def test_finding_configs_files(self): 114 | """ 115 | Check if function get_config_files_names finds all configs files in 116 | user space 117 | """ 118 | expected_configs_names = [DEFAULT_CONFIG_NAME, SECOND_CONFIG_NAME] 119 | found_configs_names = parser.get_config_files_names() 120 | self.assertEqual( 121 | len(expected_configs_names), len(found_configs_names), 122 | "Expected config number: {}, found: {}".format( 123 | len(expected_configs_names), len(found_configs_names) 124 | ) 125 | ) 126 | for config_name in found_configs_names: 127 | config_file_path = parser.config_file_name2path(config_name) 128 | self.assertTrue( 129 | os.path.exists(config_file_path), 130 | 'File does not exists: {}'.format(config_file_path), 131 | ) 132 | 133 | def test_collecting_tiles_data(self): 134 | """ 135 | Checks if function get_tiles_configs returns: 136 | - correct number of tile ids 137 | - correct number of tile types 138 | - all required tiles_types and tiles_keys 139 | """ 140 | tile_types_number = 2 # from data_to_check 141 | tile_ids_number = 3 # from data_to_check 142 | data_to_check = ( 143 | ('empty', 'empty'), 144 | ('text', 'id1'), 145 | ('text', 'id2'), 146 | ) 147 | read_data = parser.get_tiles_configs() 148 | 149 | self.assertEqual( 150 | len(read_data['tiles_names']), tile_types_number, 151 | 'Expected tile_types number: {} found {}'.format( 152 | tile_types_number, len(read_data['tiles_names']) 153 | ) 154 | ) 155 | self.assertEqual( 156 | len(read_data['tiles_keys']), tile_ids_number, 157 | 'Expected tile_ids number: {} found {}'.format( 158 | tile_types_number, len(read_data['tiles_names']) 159 | ) 160 | ) 161 | for tile_name, tile_key in data_to_check: 162 | self.assertIn(tile_name, read_data['tiles_names']) 163 | self.assertIn(tile_key, read_data['tiles_keys']) 164 | 165 | def _create_second_config(self): 166 | file_content = """ 167 | details: 168 | page_title: Empty Dashboard 169 | layout: 170 | - row_1_of_2: 171 | - col_1_of_1: 172 | - tile_template: text 173 | tile_id: id1 174 | title: Empty Tile 175 | classes: 176 | 177 | - row_1_of_2: 178 | - col_1_of_1: 179 | - tile_template: text 180 | tile_id: id2 181 | title: Empty Tile 182 | classes: """ 183 | dst = os.path.join( 184 | settings._user_config_dir, '.'.join([SECOND_CONFIG_NAME, 'yaml']) 185 | ) 186 | with open(dst, 'wb') as config_file: 187 | config_file.write(str(file_content)) 188 | 189 | def tearDown(self): 190 | try: 191 | # delete '~/.tipboard' dir that we created for tests 192 | shutil.rmtree(self.settings._user_config_dir) 193 | except (OSError): 194 | pass 195 | try: 196 | if os.path.exists(self.bkp_conf_dir): 197 | # restore conf dir backed up by setUp 198 | os.rename(self.bkp_conf_dir, self.orig_conf_dir) 199 | except AttributeError: 200 | pass 201 | reload(self.settings) 202 | -------------------------------------------------------------------------------- /tipboard/tests/test_rest_api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from __future__ import absolute_import 5 | from __future__ import division 6 | from __future__ import print_function 7 | from __future__ import unicode_literals 8 | 9 | from datetime import datetime, timedelta 10 | import binascii 11 | import json 12 | import os 13 | import urllib 14 | 15 | from mock import patch 16 | from tornado.testing import AsyncHTTPTestCase 17 | 18 | from tipboard import settings, __version__ 19 | from tipboard.api import api_version 20 | from tipboard.app import app 21 | 22 | TILE_ID = '_'.join(('test_tile', binascii.b2a_hex(os.urandom(5)))) 23 | 24 | 25 | # TODO: mock fake project id 26 | class TestRestApi(AsyncHTTPTestCase): 27 | """ 28 | Basic tests for API resources provided by Tipboard, i.e.: 29 | - push (POST) 30 | - tileconfig (POST, DELETE) 31 | - tiledata (GET, DELETE) 32 | - info (GET) 33 | """ 34 | 35 | def shortDescription(self): 36 | # suppress printing tests' docstrings when verbosity level is >= 2 37 | return None 38 | 39 | def get_app(self): 40 | return app 41 | 42 | # in case of strange timeouts uncommenting this method may help 43 | # (see: https://github.com/facebook/tornado/issues/663) 44 | #def get_new_ioloop(self): 45 | # return tornado.ioloop.IOLoop.instance() 46 | 47 | # I know that tests should be independent, but in this case it is 48 | # much better to run them in particular order. 49 | def test_01_api_key(self): 50 | """Test API key validation.""" 51 | valid_key = settings.API_KEY 52 | invalid_key = 'some_invalid_key' 53 | url = '/'.join(('', 'api', api_version, valid_key, 'info')) 54 | self.http_client.fetch(self.get_url(url), self.stop) 55 | response = self.wait() 56 | self.assertEqual(response.code, 200) 57 | url = '/'.join(('', 'api', api_version, invalid_key, 'info')) 58 | self.http_client.fetch(self.get_url(url), self.stop) 59 | response = self.wait() 60 | self.assertEqual(response.code, 404) 61 | 62 | def test_02_info_resource(self): 63 | """Test if API resource 'info' works.""" 64 | url = '/'.join(('', 'api', api_version, settings.API_KEY, 'info')) 65 | self.http_client.fetch(self.get_url(url), self.stop) 66 | response = self.wait() 67 | self.assertEqual(response.code, 200) 68 | info_expected = { 69 | 'tipboard_version': __version__, 70 | 'project_name': settings.PROJECT_NAME, 71 | 'project_layout_config': settings.LAYOUT_CONFIG, 72 | 'redis_db': settings.REDIS, 73 | } 74 | info_expected = json.dumps(info_expected) 75 | self.assertEqual(response.body, info_expected) 76 | 77 | def test_03_push_resource(self): 78 | """Test if posting tile's data works.""" 79 | url = '/'.join(('', 'api', api_version, settings.API_KEY, 'push')) 80 | data_to_push = { 81 | 'tile': 'text', 82 | 'key': TILE_ID, 83 | 'data': json.dumps('test string'), 84 | } 85 | body = urllib.urlencode(data_to_push) 86 | fake_config = { 87 | 'tiles_keys': [TILE_ID], 88 | 'tiles_names': ['text'], 89 | } 90 | with patch('tipboard.api.get_tiles_configs') as m: 91 | m.return_value = fake_config 92 | self.http_client.fetch( 93 | self.get_url(url), self.stop, method='POST', body=body 94 | ) 95 | response = self.wait() 96 | self.assertEqual(response.code, 200) 97 | 98 | def test_04_tileconfig_resource_post(self): 99 | """Test if posting tile's config works.""" 100 | url = '/'.join(('', 'api', api_version, settings.API_KEY, 'tileconfig', 101 | TILE_ID)) 102 | config_to_push = { 103 | 'value': json.dumps("{'font_color': '#00FF00'}"), 104 | } 105 | body = urllib.urlencode(config_to_push) 106 | fake_config = { 107 | 'tiles_keys': [TILE_ID], 108 | 'tiles_names': ['text'], 109 | } 110 | with patch('tipboard.api.process_layout_config') as m: 111 | m.return_value = fake_config 112 | self.http_client.fetch( 113 | self.get_url(url), self.stop, method='POST', body=body 114 | ) 115 | response = self.wait() 116 | self.assertEqual(response.code, 200) 117 | self.assertEqual(response.body, "Tile's config updated.\n") 118 | 119 | def test_05_tiledata_resource_get(self): 120 | """Test if we can get back tile's data and config.""" 121 | url = '/'.join(('', 'api', api_version, settings.API_KEY, 'tiledata', 122 | TILE_ID)) 123 | fake_config = { 124 | 'tiles_keys': [TILE_ID], 125 | 'tiles_names': ['text'], 126 | } 127 | with patch('tipboard.api.process_layout_config') as m: 128 | m.return_value = fake_config 129 | self.http_client.fetch(self.get_url(url), self.stop) 130 | mod_time_expected = datetime.now() 131 | response = self.wait() 132 | self.assertEqual(response.code, 200) 133 | data_received = json.loads(response.body) 134 | mod_time_received = datetime.strptime(data_received.pop('modified'), 135 | '%Y-%m-%d %H:%M:%S') 136 | data_expected = { 137 | 'tile_template': 'text', 138 | 'meta': "{'font_color': '#00FF00'}", 139 | 'data': 'test string', 140 | 'id': TILE_ID, 141 | } 142 | self.assertEqual(data_received, data_expected) 143 | # 'sanity_diff' is just a safety margin 144 | sanity_diff = timedelta(seconds=5) 145 | self.assertLessEqual(mod_time_expected - mod_time_received, 146 | sanity_diff) 147 | 148 | def test_06_tileconfig_resource_delete(self): 149 | """Test if we can delete tile's config.""" 150 | url = '/'.join(('', 'api', api_version, settings.API_KEY, 'tileconfig', 151 | TILE_ID)) 152 | fake_config = { 153 | 'tiles_keys': [TILE_ID], 154 | 'tiles_names': ['text'], 155 | } 156 | with patch('tipboard.api.process_layout_config') as m: 157 | m.return_value = fake_config 158 | self.http_client.fetch(self.get_url(url), self.stop, 159 | method='DELETE') 160 | response = self.wait() 161 | self.assertEqual(response.code, 200) 162 | # now let's check if 'meta' field is empty (it should be) 163 | url = '/'.join(('', 'api', api_version, settings.API_KEY, 'tiledata', 164 | TILE_ID)) 165 | with patch('tipboard.api.process_layout_config') as m: 166 | m.return_value = fake_config 167 | self.http_client.fetch(self.get_url(url), self.stop) 168 | response = self.wait() 169 | self.assertEqual(response.code, 200) 170 | data_received = json.loads(response.body) 171 | self.assertEqual(data_received['meta'], {}) 172 | 173 | def test_07_tiledata_resource_delete(self): 174 | """Test if deleting tile's key works.""" 175 | url = '/'.join(('', 'api', api_version, settings.API_KEY, 'tiledata', 176 | TILE_ID)) 177 | fake_config = { 178 | 'tiles_keys': [TILE_ID], 179 | 'tiles_names': ['text'], 180 | } 181 | with patch('tipboard.api.process_layout_config') as m: 182 | m.return_value = fake_config 183 | self.http_client.fetch(self.get_url(url), self.stop, 184 | method='DELETE') 185 | response = self.wait() 186 | self.assertEqual(response.code, 200) 187 | self.assertEqual(response.body, "Tile's data deleted.\n") 188 | --------------------------------------------------------------------------------