├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── make.bat └── source │ ├── api │ ├── index.rst │ ├── register.py │ ├── urls.py │ └── view.py │ ├── background │ ├── better_reporting_view.py │ ├── better_reporting_view_template.html │ ├── data_driven_site.png │ ├── index.rst │ ├── simple_reporting_view.py │ └── simple_reporting_view_template.html │ ├── chart_data │ └── index.rst │ ├── charts │ ├── bar_chart_example.png │ ├── bar_chart_example.py │ ├── column_chart_example.png │ ├── column_chart_example.py │ ├── index.rst │ ├── line_chart_example.png │ ├── line_chart_example.py │ ├── pie_chart_example.png │ ├── pie_chart_example.py │ ├── template_chart_example.py │ └── template_chart_example_template.html │ ├── conf.py │ ├── example.py │ ├── getting_started │ ├── index.rst │ ├── reports.py │ ├── template.html │ ├── template_example.png │ ├── views.py │ └── views_class.py │ ├── index.rst │ ├── logo.png │ └── renderers │ ├── basic_renderer_usage_example.py │ ├── chart_renderer_example.py │ ├── chart_renderer_usage_example.py │ ├── googlecharts │ ├── googlecharts_renderer_example.py │ └── index.rst │ ├── index.rst │ └── renderer_options_example.py ├── report_tools ├── __init__.py ├── api.py ├── chart_data.py ├── charts.py ├── models.py ├── renderers │ ├── __init__.py │ └── googlecharts │ │ ├── __init__.py │ │ └── gviz_api │ │ ├── COPYRIGHT │ │ ├── README │ │ ├── __init__.py │ │ ├── gviz_api.py │ │ └── gviz_api_test.py ├── reports.py ├── templates │ └── report_tools │ │ └── renderers │ │ └── googlecharts │ │ ├── barchart.html │ │ ├── chart.html │ │ ├── columnchart.html │ │ ├── linechart.html │ │ └── piechart.html ├── tests │ ├── __init__.py │ ├── reports.py │ ├── test_all.py │ └── views.py ├── urls.py └── views.py ├── setup.py ├── tests ├── __init__.py ├── manage.py ├── requirements.txt ├── runtests.py ├── runtests.sh └── testproj │ ├── __init__.py │ ├── core │ ├── __init__.py │ ├── models.py │ ├── reports.py │ ├── tests.py │ └── views.py │ ├── settings.py │ ├── templates │ ├── base.html │ └── core │ │ ├── index.html │ │ └── template_chart.html │ ├── urls.py │ └── wsgi.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | django_report_tools.egg-info 2 | .tox 3 | *.pyc 4 | .DS_Store 5 | _build 6 | build 7 | docs/build 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Evan Brumley 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE tests/* 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-report-tools 2 | =================== 3 | 4 | Django Report Tools aims to take the pain out of putting charts, graphs 5 | and tables into your Django projects. It provides a nice class-based 6 | framework to restore a little bit of elegance to your application's 7 | complex data views. 8 | 9 | 10 | Features 11 | -------- 12 | 13 | * Define your reports using the same syntax as Django forms and models 14 | * Use built-in 'renderers' to avoid the hassle of dealing with various 15 | charting technologies (currently only the Google Visualization Toolkit is supported) 16 | * Enter chart data in a standardised format 17 | * Build a simple API, allowing for the creation of chart exports or a 'save to dashboard' feature. 18 | 19 | 20 | Release Notes - 0.2.2 21 | --------------------- 22 | 23 | * Updated for Django 1.6 compatibility 24 | * Increased tox coverage 25 | 26 | 27 | Release Notes - 0.2.1 28 | --------------------- 29 | 30 | * Fixed a JSON encoding bug that broke Python 2.6 compatibility. 31 | * Added a tox.ini and setup.py test support so I can test and prevent these sorts of 32 | bug in the future. 33 | 34 | Release Notes - 0.2 35 | ------------------- 36 | 37 | * Updated for Django 1.5 compatibility 38 | * Fixed an XSS vector in the Google Charts renderer 39 | * Updated to the most recent GViz API 40 | 41 | 42 | A fully-functional example report 43 | ----------------- 44 | 45 | The following example implements a report with a simple pie chart, rendered 46 | using the Google Visualization Toolkit. 47 | 48 | :: 49 | 50 | from report_tools.reports import Report 51 | from report_tools.chart_data import ChartData 52 | from report_tools.renderers.googlecharts import GoogleChartsRenderer 53 | from report_tools import charts 54 | 55 | 56 | class MyReport(Report): 57 | renderer = GoogleChartsRenderer 58 | 59 | pie_chart = charts.PieChart( 60 | title="A nice, simple pie chart", 61 | width=400, 62 | height=300 63 | ) 64 | 65 | def get_data_for_pie_chart(self): 66 | data = ChartData() 67 | 68 | data.add_column("Pony Type") 69 | data.add_column("Population") 70 | 71 | data.add_row(["Blue", 20]) 72 | data.add_row(["Pink", 20]) 73 | data.add_row(["Magical", 1]) 74 | 75 | return data 76 | 77 | Read on in the documentation for a full explanation and lots more examples. 78 | 79 | 80 | Links 81 | ----- 82 | 83 | Project Home: http://github.com/evanbrumley/django-report-tools 84 | 85 | Documentation: http://django-report-tools.readthedocs.org 86 | 87 | 88 | Installation 89 | ------------ 90 | 91 | To install django-report-tools simply use: :: 92 | 93 | $ pip install django-report-tools 94 | 95 | Or alternatively: :: 96 | 97 | $ easy_install django-report-tools 98 | -------------------------------------------------------------------------------- /docs/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 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-report-tools.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-report-tools.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-report-tools" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-report-tools" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | if errorlevel 1 exit /b 1 46 | echo. 47 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 48 | goto end 49 | ) 50 | 51 | if "%1" == "dirhtml" ( 52 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 53 | if errorlevel 1 exit /b 1 54 | echo. 55 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 56 | goto end 57 | ) 58 | 59 | if "%1" == "singlehtml" ( 60 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 61 | if errorlevel 1 exit /b 1 62 | echo. 63 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 64 | goto end 65 | ) 66 | 67 | if "%1" == "pickle" ( 68 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 69 | if errorlevel 1 exit /b 1 70 | echo. 71 | echo.Build finished; now you can process the pickle files. 72 | goto end 73 | ) 74 | 75 | if "%1" == "json" ( 76 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished; now you can process the JSON files. 80 | goto end 81 | ) 82 | 83 | if "%1" == "htmlhelp" ( 84 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished; now you can run HTML Help Workshop with the ^ 88 | .hhp project file in %BUILDDIR%/htmlhelp. 89 | goto end 90 | ) 91 | 92 | if "%1" == "qthelp" ( 93 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 94 | if errorlevel 1 exit /b 1 95 | echo. 96 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 97 | .qhcp project file in %BUILDDIR%/qthelp, like this: 98 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-report-tools.qhcp 99 | echo.To view the help file: 100 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-report-tools.ghc 101 | goto end 102 | ) 103 | 104 | if "%1" == "devhelp" ( 105 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 106 | if errorlevel 1 exit /b 1 107 | echo. 108 | echo.Build finished. 109 | goto end 110 | ) 111 | 112 | if "%1" == "epub" ( 113 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 117 | goto end 118 | ) 119 | 120 | if "%1" == "latex" ( 121 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 122 | if errorlevel 1 exit /b 1 123 | echo. 124 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 125 | goto end 126 | ) 127 | 128 | if "%1" == "text" ( 129 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 130 | if errorlevel 1 exit /b 1 131 | echo. 132 | echo.Build finished. The text files are in %BUILDDIR%/text. 133 | goto end 134 | ) 135 | 136 | if "%1" == "man" ( 137 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 141 | goto end 142 | ) 143 | 144 | if "%1" == "changes" ( 145 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.The overview file is in %BUILDDIR%/changes. 149 | goto end 150 | ) 151 | 152 | if "%1" == "linkcheck" ( 153 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Link check complete; look for any errors in the above output ^ 157 | or in %BUILDDIR%/linkcheck/output.txt. 158 | goto end 159 | ) 160 | 161 | if "%1" == "doctest" ( 162 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 163 | if errorlevel 1 exit /b 1 164 | echo. 165 | echo.Testing of doctests in the sources finished, look at the ^ 166 | results in %BUILDDIR%/doctest/output.txt. 167 | goto end 168 | ) 169 | 170 | :end 171 | -------------------------------------------------------------------------------- /docs/source/api/index.rst: -------------------------------------------------------------------------------- 1 | Creating an API 2 | =============== 3 | 4 | Sometimes it's not enough just to have your charts accessible within the context 5 | of a larger report. Sometimes you need to pull them out, pass them around and so on. 6 | The following steps should provide a good way to start off a more complex data reporting 7 | system. 8 | 9 | 10 | Step 1 - Use the class-based view 11 | --------------------------------- 12 | 13 | The API relies on structured, class-based views to provide the hooks necessary to generate 14 | the reports and charts. The following is an example: 15 | 16 | .. literalinclude:: view.py 17 | :language: python 18 | 19 | This is a really simple class-based view. It behaves in the same way as Django's base `View` 20 | class, with the addition of a `get_report` method. This method provides the necessary hook 21 | for the API to extract the report without touching your other view code. 22 | 23 | 24 | Step 2 - Register the class-based view 25 | -------------------------------------- 26 | 27 | To register the view with the api, simply pass it into the ``register`` function in the ``api`` 28 | module, along with the key you wish to use to access the report later: 29 | 30 | .. literalinclude:: register.py 31 | :language: python 32 | 33 | 34 | Step 3 (optional) - Add the API endpoints to your urls.py 35 | --------------------------------------------------------- 36 | 37 | If you plan to make your chart HTML available externally, you can let the API handle your 38 | URLS for you by adding the following line to your `urls.py`. 39 | 40 | .. literalinclude:: urls.py 41 | :language: python 42 | 43 | 44 | Access a chart internally 45 | ------------------------- 46 | 47 | To access a chart from a registered report, simply use the ``report_tools.api.get_chart`` function. 48 | 49 | .. method:: report_tools.api.get_chart(request, api_key, chart_name, parameters=None, prefix=None) 50 | 51 | :param request: The current request 52 | :param api_key: The API key used to register the report view 53 | :param chart_name: The attribute name given to the required chart 54 | :param parameters: If provided, this dictionary will override the GET parameters 55 | in the provided request. 56 | :param prefix: If provided, this string will be prepended to the chart's id. Useful 57 | if you're displaying the same chart from the same report with different 58 | parameters. 59 | :returns: The requested chart object 60 | 61 | 62 | Access a chart externally 63 | ------------------------- 64 | 65 | If you've added the API to your urls.py (step 3), you should be able to access a simple JSON endpoint at 66 | ``api/report_api_key/chart_name/``. The endpoint will provide the chart HTML along with a dictionary of 67 | anything supplied in the chart's `attrs` parameter. 68 | -------------------------------------------------------------------------------- /docs/source/api/register.py: -------------------------------------------------------------------------------- 1 | from report_tools.views import ReportView 2 | from report_tools.api import register 3 | 4 | 5 | class MyReportView(ReportView): 6 | ... 7 | 8 | register(MyReportView, 'myreportview_api_key') 9 | -------------------------------------------------------------------------------- /docs/source/api/urls.py: -------------------------------------------------------------------------------- 1 | url(r'^api/', include('report_tools.urls')) 2 | -------------------------------------------------------------------------------- /docs/source/api/view.py: -------------------------------------------------------------------------------- 1 | # myapp/views.py 2 | 3 | from django.shortcuts import render 4 | from myapp.reports import MyReport 5 | from report_tools.views import ReportView 6 | 7 | 8 | class MyReportView(ReportView): 9 | def get_report(self, request): 10 | return MyReport() 11 | 12 | def get(self, request): 13 | template = 'myapp/my_report.html' 14 | context = { 15 | 'report': self.get_report(request), 16 | } 17 | 18 | return render(request, template, context) 19 | -------------------------------------------------------------------------------- /docs/source/background/better_reporting_view.py: -------------------------------------------------------------------------------- 1 | from report_tools import reports 2 | from report_tools import charts 3 | from report_tools.chart_data import ChartData 4 | 5 | 6 | def better_reporting_view(request): 7 | # Gather data 8 | my_objects = MyObject.objects.all() 9 | 10 | # Generate report 11 | report = MyReport(my_objects) 12 | 13 | context = { 14 | 'report': report 15 | } 16 | 17 | return render(request, 'mytemplate.html', context) 18 | 19 | 20 | class MyReport(reports.Report): 21 | renderer = MyRenderer 22 | 23 | chart1 = charts.PieChart(title="A nice, simple pie chart") 24 | chart2 = ... 25 | chart3 = ... 26 | 27 | def __init__(self, my_objects, *args, **kwargs): 28 | super(MyReport, self).__init__(*args, **kwargs) 29 | self.my_objects = my_objects 30 | 31 | # Here you could do any expensive calculations that 32 | # are needed for multiple charts 33 | 34 | def get_data_for_chart1(self): 35 | data = ChartData() 36 | 37 | # TODO: Fill data 38 | 39 | return data 40 | -------------------------------------------------------------------------------- /docs/source/background/better_reporting_view_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | {{ report.chart1 }} 5 |
6 | 7 |
8 | {{ report.chart2 }} 9 |
10 | 11 | ... 12 | -------------------------------------------------------------------------------- /docs/source/background/data_driven_site.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanbrumley/django-report-tools/3347705c6f7880a9766ac827ff7575e5a0b5bcb7/docs/source/background/data_driven_site.png -------------------------------------------------------------------------------- /docs/source/background/index.rst: -------------------------------------------------------------------------------- 1 | Background 2 | ========== 3 | 4 | When creating data driven web applications, the following is a relatively common site structure: 5 | 6 | .. image:: data_driven_site.png 7 | 8 | In this sort of layout, the data management pages provide all your basic CRUD, the results 9 | of which are passed into several reporting views. These views typically contain a series of charts 10 | and tables, giving an analysis of the data entered. The dashboard view then allows users to pick 11 | various charts and tables from the reporting views and keep them on a customised home page. 12 | 13 | The goal of the Report Tools package is to make the creation of the reporting views and dashboard 14 | as easy and clean as possible. Without such a package, a simple reporting view might look like this: 15 | 16 | .. literalinclude:: simple_reporting_view.py 17 | :language: python 18 | 19 | .. literalinclude:: simple_reporting_view_template.html 20 | :language: django 21 | 22 | This code has some problems: 23 | 24 | * Even after abstracting out a lot of the hard lifting, there's going to be a lot of code in your 25 | view, some of it quite repetitive. 26 | * There's no easy way to lift out individual charts for your dashboard - you'll have to repeat 27 | code somewhere else to get that to work. 28 | * Charts can't share calculations between each other in a clean way unless you de-abstract and make 29 | your view even longer. 30 | * The formats generated/required by ``gather_chart1_data`` and ``chart1_options`` are probably 31 | going to be heavily tied to the charting package you use. 32 | * ``generate_chart_html`` is going to be a massive can of time-eating worms. 33 | 34 | So, here's how you'd write a similar view with Report Tools: 35 | 36 | .. literalinclude:: better_reporting_view.py 37 | :language: python 38 | 39 | .. literalinclude:: better_reporting_view_template.html 40 | :language: django 41 | 42 | So you now have the following advantages: 43 | 44 | * Your view code is a lot shorter and more manageable 45 | * Options for your charts can be entered in a nice, declarative syntax 46 | * Calculation results can be shared by storing them as instance variables 47 | * Chart options and chart data are now entered in a standard format. 48 | * What used to be a monolithic ``generate_chart_html`` function is now implemented 49 | as a renderer class. Report Tools currently provides an inbuilt renderer for the 50 | Google Visualization Toolkit, and it's easy to write your own. 51 | * Because the entire report is stored in its own highly structured class, 52 | ripping an individual chart out for a dashboard is a lot easier. Report Tools 53 | even provides a class-based view expressly for this purpose. See the API generation 54 | documentation for examples. 55 | 56 | This gives you a basic overview of why Report Tools exists - to learn about how it works and how 57 | to use it properly, move on to the :doc:`Getting Started ` section. 58 | -------------------------------------------------------------------------------- /docs/source/background/simple_reporting_view.py: -------------------------------------------------------------------------------- 1 | def simple_reporting_view(request): 2 | # Gather data 3 | my_objects = MyObject.objects.all() 4 | 5 | # Generate chart 1 6 | chart1_data = gather_chart1_data(my_objects) 7 | chart1_options = {...} 8 | chart1_html = generate_chart_html(chart1_data, chart1_options) 9 | 10 | # Generate chart 2,3,4,5,6,7 11 | ... 12 | 13 | context = { 14 | 'chart1': chart1, 15 | 'chart2': chart2, 16 | ... 17 | } 18 | 19 | return render(request, 'mytemplate.html', context) 20 | -------------------------------------------------------------------------------- /docs/source/background/simple_reporting_view_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | {{ chart1 }} 5 |
6 | 7 |
8 | {{ chart2 }} 9 |
10 | 11 | ... 12 | -------------------------------------------------------------------------------- /docs/source/chart_data/index.rst: -------------------------------------------------------------------------------- 1 | Using ChartData to fill your charts 2 | =================================== 3 | 4 | .. class:: report_tools.chart_data.ChartData 5 | 6 | The ChartData class provides a consistent way to get data into charts. 7 | Each ChartData object represents a 2 dimensional array of cells, which 8 | can be annotated on a column, row or cell level. 9 | 10 | .. method:: report_tools.chart_data.ChartData.add_column(self, name, metadata=None) 11 | 12 | Adds a new column to the data table. 13 | 14 | :param name: The name of the column 15 | :param metadata: A dictionary of metadata describing the column 16 | 17 | .. method:: report_tools.chart_data.ChartData.add_columns(self, columns) 18 | 19 | Adds multiple columns to the data table 20 | 21 | :param columns: A list of column names. If you need to enter metadata with the columns, 22 | you can also pass in a list of name-metadata tuples. 23 | 24 | .. method:: report_tools.chart_data.add_row(self, data, metadata=None) 25 | 26 | Adds a new row to the datatable 27 | 28 | :param data: A list of data points that will form the row. The length of the list should 29 | match the number of columns added. 30 | :param metadata: A dictionary of metadata describing the row 31 | 32 | .. method:: report_tools.chart_data.add_rows(self, rows) 33 | 34 | Adds multiple rows to the data table 35 | 36 | :param rows: A list of rows. If you need to enter metadata with the rows, 37 | you can also pass in a list of row-metadata tuples. 38 | 39 | .. method:: get_columns(self) 40 | 41 | Returns a list of columns added so far 42 | 43 | ..method:: get_rows(self) 44 | 45 | Returns a list of rows added so far 46 | -------------------------------------------------------------------------------- /docs/source/charts/bar_chart_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanbrumley/django-report-tools/3347705c6f7880a9766ac827ff7575e5a0b5bcb7/docs/source/charts/bar_chart_example.png -------------------------------------------------------------------------------- /docs/source/charts/bar_chart_example.py: -------------------------------------------------------------------------------- 1 | class MyReport(Report): 2 | renderer = GoogleChartsRenderer 3 | 4 | bar_chart = charts.BarChart(title="Pony Populations", width="500") 5 | multiseries_bar_chart = charts.BarChart(title="Pony Populations by Country", width="500") 6 | 7 | def get_data_for_bar_chart(self): 8 | data = ChartData() 9 | 10 | data.add_column("Pony Type") 11 | data.add_column("Population") 12 | 13 | data.add_row(["Blue", 20]) 14 | data.add_row(["Pink", 20]) 15 | data.add_row(["Magical", 1]) 16 | 17 | return data 18 | 19 | def get_data_for_multiseries_bar_chart(self): 20 | data = ChartData() 21 | 22 | data.add_column("Pony Type") 23 | data.add_column("Australian Population") 24 | data.add_column("Switzerland Population") 25 | data.add_column("USA Population") 26 | 27 | data.add_row(["Blue", 5, 10, 5]) 28 | data.add_row(["Pink", 10, 2, 8]) 29 | data.add_row(["Magical", 1, 0, 0]) 30 | 31 | return data -------------------------------------------------------------------------------- /docs/source/charts/column_chart_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanbrumley/django-report-tools/3347705c6f7880a9766ac827ff7575e5a0b5bcb7/docs/source/charts/column_chart_example.png -------------------------------------------------------------------------------- /docs/source/charts/column_chart_example.py: -------------------------------------------------------------------------------- 1 | class MyReport(Report): 2 | renderer = GoogleChartsRenderer 3 | 4 | column_chart = charts.ColumnChart(title="Pony Populations", width="500") 5 | multiseries_column_chart = charts.ColumnChart(title="Pony Populations by Country", width="500") 6 | 7 | def get_data_for_column_chart(self): 8 | data = ChartData() 9 | 10 | data.add_column("Pony Type") 11 | data.add_column("Population") 12 | 13 | data.add_row(["Blue", 20]) 14 | data.add_row(["Pink", 20]) 15 | data.add_row(["Magical", 1]) 16 | 17 | return data 18 | 19 | def get_data_for_multiseries_column_chart(self): 20 | data = ChartData() 21 | 22 | data.add_column("Pony Type") 23 | data.add_column("Australian Population") 24 | data.add_column("Switzerland Population") 25 | data.add_column("USA Population") 26 | 27 | data.add_row(["Blue", 5, 10, 5]) 28 | data.add_row(["Pink", 10, 2, 8]) 29 | data.add_row(["Magical", 1, 0, 0]) 30 | 31 | return data -------------------------------------------------------------------------------- /docs/source/charts/index.rst: -------------------------------------------------------------------------------- 1 | Charts 2 | ====== 3 | 4 | .. py:class:: report_tools.charts.Chart(**kwargs) 5 | 6 | Charts represent a view of your data. Charts are usually a graph of some sort, 7 | but may also contain arbitrary HTML (see the TemplateChart class) 8 | 9 | Optional Chart Arguments 10 | ------------------------ 11 | 12 | title 13 | ~~~~~ 14 | 15 | .. attribute:: Chart.title 16 | 17 | A "human-friendly" title for your chart. Note that this field will typically 18 | not be rendered in the chart itself - it is provided for use in the surrounding 19 | HTML. For example: 20 | 21 | .. code-block:: django 22 | 23 |

{{ my_report.chart.title }}

24 |
{{ my_report.chart }}
25 | 26 | If you want to embed a title in the chart itself, you should refer to 27 | the :doc:`renderer documentation ` for your chosen rendering engine. 28 | 29 | 30 | renderer 31 | ~~~~~~~~ 32 | 33 | .. attribute:: Chart.renderer 34 | 35 | If you want the chart to use a different renderer to the one specified on the report, 36 | you can use this to pass in the appropriate renderer class. 37 | 38 | 39 | renderer_options 40 | ~~~~~~~~~~~~~~~~ 41 | 42 | .. attribute:: Chart.renderer_options 43 | 44 | Renderers will typically have a lot of specific customization options. This 45 | argument accepts a dictionary, which will be passed through to the renderer. 46 | For available options, check out the :doc:`renderer 47 | documentation `. 48 | 49 | 50 | attrs 51 | ~~~~~ 52 | 53 | .. attribute:: Chart.attrs 54 | 55 | If you want to store some extra information with the chart (i.e. what sort of 56 | container div should it use?), you can pass in a dictionary of attributes with 57 | this argument. 58 | 59 | 60 | Built-in Chart Classes 61 | ------------------------ 62 | 63 | PieChart 64 | ~~~~~~~~ 65 | 66 | .. py:class:: report_tools.charts.PieChart(width=None, height=None, **kwargs) 67 | 68 | A standard pie chart. The corresponding ``get_data_for_xxx`` method should 69 | provide a ``ChartData`` object with two columns. Column one should contain the data 70 | point labels, and column 2 should contain numerical values. 71 | 72 | Accepts two extra keyword arguments, *width* and *height*. These can be integers, 73 | floats or strings, depending on what your chosen rendering engine supports. 74 | 75 | Example: 76 | 77 | .. literalinclude:: pie_chart_example.py 78 | :language: python 79 | 80 | .. image:: pie_chart_example.png 81 | 82 | ColumnChart 83 | ~~~~~~~~~~~ 84 | 85 | .. py:class:: report_tools.charts.ColumnChart(width=None, height=None, **kwargs) 86 | 87 | A standard vertical column chart. The corresponding ``get_data_for_xxx`` method should 88 | provide a ChartData object with 1+n columns, where n is the number of data series 89 | to be displayed. Column one should contain the data point labels, and subsequent columns 90 | should contain numerical values. 91 | 92 | Accepts two extra keyword arguments, *width* and *height*. These can be integers, 93 | floats or strings, depending on what your chosen rendering engine supports. 94 | 95 | Example: 96 | 97 | .. literalinclude:: column_chart_example.py 98 | :language: python 99 | 100 | .. image:: column_chart_example.png 101 | 102 | BarChart 103 | ~~~~~~~~ 104 | 105 | .. py:class:: report_tools.charts.BarChart(width=None, height=None, **kwargs) 106 | 107 | A standard horizontal bar chart. The corresponding ``get_data_for_xxx`` method should 108 | provide a ChartData object with 1+n columns, where n is the number of data series 109 | to be displayed. Column one should contain the data point labels, and subsequent columns 110 | should contain numerical values. 111 | 112 | Accepts two extra keyword arguments, *width* and *height*. These can be integers, 113 | floats or strings, depending on what your chosen rendering engine supports. 114 | 115 | Example: 116 | 117 | .. literalinclude:: bar_chart_example.py 118 | :language: python 119 | 120 | .. image:: bar_chart_example.png 121 | 122 | LineChart 123 | ~~~~~~~~~ 124 | 125 | .. py:class:: report_tools.charts.LineChart(width=None, height=None, **kwargs) 126 | 127 | A standard line chart. The corresponding ``get_data_for_xxx`` method should 128 | provide a ChartData object with 1+n columns, where n is the number of data series 129 | to be displayed. Column one should contain the data point labels, and subsequent columns 130 | should contain numerical values. 131 | 132 | Example: 133 | 134 | .. literalinclude:: line_chart_example.py 135 | :language: python 136 | 137 | .. image:: line_chart_example.png 138 | 139 | TemplateChart 140 | ~~~~~~~~~~~~~ 141 | 142 | .. py:class:: report_tools.charts.TemplateChart(template, **kwargs) 143 | 144 | This chart simply renders a given template. The ``get_data_for_xxx`` method 145 | should return a dictionary context. An extra context variable 'chart_id' will 146 | be provided, which should be used if a unique identifier is required in the 147 | template. Note that the template chart does not require a renderer. 148 | 149 | Accepts one required argument, *template*. 150 | 151 | Example: 152 | 153 | .. literalinclude:: template_chart_example.py 154 | :language: python 155 | 156 | .. literalinclude:: template_chart_example_template.html 157 | :language: html 158 | -------------------------------------------------------------------------------- /docs/source/charts/line_chart_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanbrumley/django-report-tools/3347705c6f7880a9766ac827ff7575e5a0b5bcb7/docs/source/charts/line_chart_example.png -------------------------------------------------------------------------------- /docs/source/charts/line_chart_example.py: -------------------------------------------------------------------------------- 1 | class MyReport(Report): 2 | renderer = GoogleChartsRenderer 3 | 4 | line_chart = charts.LineChart(title="Blue Pony Population - 2009-2012", width="500") 5 | multiseries_line_chart = charts.LineChart(title="Pony Populations - 2009-2012", width="500") 6 | 7 | def get_data_for_line_chart(self): 8 | data = ChartData() 9 | 10 | data.add_column("Test Period") 11 | data.add_column("Blue Pony Population") 12 | 13 | data.add_row(["2009-10", 20]) 14 | data.add_row(["2010-11", 18]) 15 | data.add_row(["2011-12", 100]) 16 | 17 | return data 18 | 19 | def get_data_for_multiseries_line_chart(self): 20 | data = ChartData() 21 | 22 | data.add_column("Test Period") 23 | data.add_column("Blue Pony Population") 24 | data.add_column("Pink Pony Population") 25 | data.add_column("Magical Pony Population") 26 | 27 | data.add_row(["2009-10", 20, 10, 50]) 28 | data.add_row(["2010-11", 18, 8, 60]) 29 | data.add_row(["2011-12", 100, 120, 2]) 30 | 31 | return data -------------------------------------------------------------------------------- /docs/source/charts/pie_chart_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanbrumley/django-report-tools/3347705c6f7880a9766ac827ff7575e5a0b5bcb7/docs/source/charts/pie_chart_example.png -------------------------------------------------------------------------------- /docs/source/charts/pie_chart_example.py: -------------------------------------------------------------------------------- 1 | class MyReport(Report): 2 | renderer = GoogleChartsRenderer 3 | 4 | pie_chart = charts.PieChart(width=400, height=300) 5 | 6 | def get_data_for_pie_chart(self): 7 | data = ChartData() 8 | 9 | data.add_column("Pony Type") 10 | data.add_column("Population") 11 | 12 | data.add_row(["Blue", 20]) 13 | data.add_row(["Pink", 20]) 14 | data.add_row(["Magical", 1]) 15 | 16 | return data -------------------------------------------------------------------------------- /docs/source/charts/template_chart_example.py: -------------------------------------------------------------------------------- 1 | class MyReport(Report): 2 | template_chart = charts.TemplateChart(template="myapp/template_chart.html") 3 | 4 | def get_data_for_template_chart(self): 5 | pony_types = [ 6 | ('Blue', 'Equus Caeruleus'), 7 | ('Pink', 'Equus Roseus'), 8 | ('Magical', 'Equus Magica') 9 | ] 10 | 11 | template_context = { 12 | 'pony_types': pony_types 13 | } 14 | 15 | return template_context -------------------------------------------------------------------------------- /docs/source/charts/template_chart_example_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% for pony_type, latin_name in pony_types %} 10 | 11 | 12 | 13 | 14 | {% endfor %} 15 | 16 |
Pony TypeLatin Name
{{ pony_type }}{{ latin_name }}
-------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # django-report-tools documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Jan 12 14:58:42 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys 15 | import os 16 | 17 | # If extensions (or modules to document with autodoc) are in another directory, 18 | # add these directories to sys.path here. If the directory is relative to the 19 | # documentation root, use os.path.abspath to make it absolute, like shown here. 20 | #sys.path.insert(0, os.path.abspath('.')) 21 | 22 | # -- General configuration ----------------------------------------------------- 23 | 24 | # If your documentation needs a minimal Sphinx version, state it here. 25 | #needs_sphinx = '1.0' 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be extensions 28 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 29 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.viewcode'] 30 | 31 | # Add any paths that contain templates here, relative to this directory. 32 | templates_path = [] 33 | 34 | # The suffix of source filenames. 35 | source_suffix = '.rst' 36 | 37 | # The encoding of source files. 38 | #source_encoding = 'utf-8-sig' 39 | 40 | # The master toctree document. 41 | master_doc = 'index' 42 | 43 | # General information about the project. 44 | project = u'django-report-tools' 45 | copyright = u'' 46 | 47 | # The version info for the project you're documenting, acts as replacement for 48 | # |version| and |release|, also used in various other places throughout the 49 | # built documents. 50 | # 51 | # The short X.Y version. 52 | version = '0.2.1' 53 | # The full version, including alpha/beta/rc tags. 54 | release = '0.2.1' 55 | 56 | # The language for content autogenerated by Sphinx. Refer to documentation 57 | # for a list of supported languages. 58 | #language = None 59 | 60 | # There are two options for replacing |today|: either, you set today to some 61 | # non-false value, then it is used: 62 | #today = '' 63 | # Else, today_fmt is used as the format for a strftime call. 64 | #today_fmt = '%B %d, %Y' 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | exclude_patterns = [] 69 | 70 | # The reST default role (used for this markup: `text`) to use for all documents. 71 | #default_role = None 72 | 73 | # If true, '()' will be appended to :func: etc. cross-reference text. 74 | #add_function_parentheses = True 75 | 76 | # If true, the current module name will be prepended to all description 77 | # unit titles (such as .. function::). 78 | #add_module_names = True 79 | 80 | # If true, sectionauthor and moduleauthor directives will be shown in the 81 | # output. They are ignored by default. 82 | #show_authors = False 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | pygments_style = 'sphinx' 86 | 87 | # A list of ignored prefixes for module index sorting. 88 | #modindex_common_prefix = [] 89 | 90 | 91 | # -- Options for HTML output --------------------------------------------------- 92 | 93 | # The theme to use for HTML and HTML Help pages. See the documentation for 94 | # a list of builtin themes. 95 | html_theme = 'nature' 96 | 97 | # Theme options are theme-specific and customize the look and feel of a theme 98 | # further. For a list of options available for each theme, see the 99 | # documentation. 100 | #html_theme_options = {} 101 | 102 | # Add any paths that contain custom themes here, relative to this directory. 103 | html_theme_path = ['.'] 104 | 105 | # The name for this set of Sphinx documents. If None, it defaults to 106 | # " v documentation". 107 | #html_title = None 108 | 109 | # A shorter title for the navigation bar. Default is the same as html_title. 110 | #html_short_title = None 111 | 112 | # The name of an image file (relative to this directory) to place at the top 113 | # of the sidebar. 114 | html_logo = "logo.png" 115 | 116 | # The name of an image file (within the static path) to use as favicon of the 117 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 118 | # pixels large. 119 | #html_favicon = None 120 | 121 | # Add any paths that contain custom static files (such as style sheets) here, 122 | # relative to this directory. They are copied after the builtin static files, 123 | # so a file named "default.css" will overwrite the builtin "default.css". 124 | html_static_path = [] 125 | 126 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 127 | # using the given strftime format. 128 | #html_last_updated_fmt = '%b %d, %Y' 129 | 130 | # If true, SmartyPants will be used to convert quotes and dashes to 131 | # typographically correct entities. 132 | #html_use_smartypants = True 133 | 134 | # Custom sidebar templates, maps document names to template names. 135 | #html_sidebars = {} 136 | 137 | # Additional templates that should be rendered to pages, maps page names to 138 | # template names. 139 | #html_additional_pages = {} 140 | 141 | # If false, no module index is generated. 142 | #html_domain_indices = True 143 | 144 | # If false, no index is generated. 145 | #html_use_index = True 146 | 147 | # If true, the index is split into individual pages for each letter. 148 | #html_split_index = False 149 | 150 | # If true, links to the reST sources are added to the pages. 151 | #html_show_sourcelink = True 152 | 153 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 154 | #html_show_sphinx = True 155 | 156 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 157 | #html_show_copyright = True 158 | 159 | # If true, an OpenSearch description file will be output, and all pages will 160 | # contain a tag referring to it. The value of this option must be the 161 | # base URL from which the finished HTML is served. 162 | #html_use_opensearch = '' 163 | 164 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 165 | #html_file_suffix = None 166 | 167 | # Output file base name for HTML help builder. 168 | htmlhelp_basename = 'django-report-tools-doc' 169 | 170 | 171 | # -- Options for LaTeX output -------------------------------------------------- 172 | 173 | # The paper size ('letter' or 'a4'). 174 | #latex_paper_size = 'letter' 175 | 176 | # The font size ('10pt', '11pt' or '12pt'). 177 | #latex_font_size = '10pt' 178 | 179 | # Grouping the document tree into LaTeX files. List of tuples 180 | # (source start file, target name, title, author, documentclass [howto/manual]). 181 | latex_documents = [ 182 | ('index', 'django-report-tools.tex', u'django-report-tools Documentation', 183 | u'Evan Brumley', 'manual'), 184 | ] 185 | 186 | # The name of an image file (relative to this directory) to place at the top of 187 | # the title page. 188 | #latex_logo = None 189 | 190 | # For "manual" documents, if this is true, then toplevel headings are parts, 191 | # not chapters. 192 | #latex_use_parts = False 193 | 194 | # If true, show page references after internal links. 195 | #latex_show_pagerefs = False 196 | 197 | # If true, show URL addresses after external links. 198 | #latex_show_urls = False 199 | 200 | # Additional stuff for the LaTeX preamble. 201 | #latex_preamble = '' 202 | 203 | # Documents to append as an appendix to all manuals. 204 | #latex_appendices = [] 205 | 206 | # If false, no module index is generated. 207 | #latex_domain_indices = True 208 | 209 | 210 | # -- Options for manual page output -------------------------------------------- 211 | 212 | # One entry per manual page. List of tuples 213 | # (source start file, name, description, authors, manual section). 214 | man_pages = [ 215 | ('index', 'report-tools', u'report-tools Documentation', 216 | [u'Report Tools'], 1) 217 | ] 218 | -------------------------------------------------------------------------------- /docs/source/example.py: -------------------------------------------------------------------------------- 1 | class MyReport(Report): 2 | renderer = GoogleChartsRenderer 3 | 4 | pie_chart = charts.PieChart( 5 | title="A nice, simple pie chart", 6 | width=400, 7 | height=300 8 | ) 9 | 10 | def get_data_for_pie_chart(self): 11 | data = ChartData() 12 | 13 | data.add_column("Pony Type") 14 | data.add_column("Population") 15 | 16 | data.add_row(["Blue", 20]) 17 | data.add_row(["Pink", 20]) 18 | data.add_row(["Magical", 1]) 19 | 20 | return data 21 | -------------------------------------------------------------------------------- /docs/source/getting_started/index.rst: -------------------------------------------------------------------------------- 1 | Getting Started 2 | =============== 3 | 4 | Overview 5 | -------- 6 | 7 | This library deals with the following concepts: 8 | 9 | Report 10 | A class that maintains a collection of charts 11 | 12 | Chart 13 | A class that, given a set of options and a set of data, generates HTML to 14 | display that data in a nice format. Charts can include graphs, tables, 15 | and other arbitrary visualization types. 16 | 17 | ChartData 18 | A class that allows the collection of data in a structured format, which can 19 | then be passed into a Chart. 20 | 21 | ChartRenderer 22 | A class that implements the rendering of Chart objects. Changing the 23 | ChartRenderer class for a Report or Chart allows for easy changing of 24 | rendering technologies - i.e. google charts, FusionCharts, HighCharts etc. 25 | 26 | ReportView 27 | A class-based view to assist with the creation of a report API. More information 28 | in the :doc:`API documentation `. 29 | 30 | 31 | Report objects 32 | -------------- 33 | 34 | Consider the following, very simple report - this would usually be located in a 35 | ``reports.py`` file in your app directory: 36 | 37 | .. literalinclude:: reports.py 38 | :language: python 39 | 40 | A ``Report`` is composed of ``Chart`` objects. In this case, the report has a single 41 | chart ``pie_chart``. This chart has been given a title, width and height, but other than 42 | that it will use the default rendering options. 43 | 44 | For information on the available ``Chart`` classes, see the :doc:`Chart documentation `. 45 | 46 | Rendering for this report will be performed by the ``GoogleChartsRenderer`` class, 47 | which uses Google's visualization framework. For more information on the available 48 | rendering classes, see the :doc:`renderer documentation `. 49 | 50 | All charts on a report require a ``get_data_for_xxx`` method, where ``xxx`` is the 51 | attribute name given to the chart. In this case, you can see the ``get_data_for_pie_chart`` 52 | field has been created. Pie charts require a ``ChartData`` object as input, so 53 | the ``get_data_for_pie_chart`` method creates one, fills it with data and returns it. 54 | For detailed information on how to enter data into a ``ChartData`` object or other 55 | data storage formats, see the :doc:`ChartData documentation `. 56 | 57 | 58 | Using a report in your views 59 | ---------------------------- 60 | 61 | Using a report in your view is simple: 62 | 63 | .. literalinclude:: views.py 64 | :language: python 65 | 66 | 67 | You can also use the included class based view, which will help if you wish to 68 | use API features down the track: 69 | 70 | .. literalinclude:: views_class.py 71 | :language: python 72 | 73 | Note the use of the ``get_report`` method. This provides the hook required for the API 74 | to grab the report. For more information, check the :doc:`API documentation `. 75 | 76 | 77 | Using a report in your templates 78 | -------------------------------- 79 | 80 | Using a report in your template is also straightforward 81 | 82 | .. literalinclude:: template.html 83 | :language: html 84 | 85 | .. figure:: template_example.png -------------------------------------------------------------------------------- /docs/source/getting_started/reports.py: -------------------------------------------------------------------------------- 1 | # myapp/reports.py 2 | 3 | from report_tools.reports import Report 4 | from report_tools.chart_data import ChartData 5 | from report_tools.renderers.googlecharts import GoogleChartsRenderer 6 | from report_tools import charts 7 | 8 | 9 | class MyReport(Report): 10 | renderer = GoogleChartsRenderer 11 | 12 | pie_chart = charts.PieChart( 13 | title="A nice, simple pie chart", 14 | width=400, 15 | height=300 16 | ) 17 | 18 | def get_data_for_pie_chart(self): 19 | data = ChartData() 20 | 21 | data.add_column("Pony Type") 22 | data.add_column("Population") 23 | 24 | data.add_row(["Blue", 20]) 25 | data.add_row(["Pink", 20]) 26 | data.add_row(["Magical", 1]) 27 | 28 | return data 29 | -------------------------------------------------------------------------------- /docs/source/getting_started/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

{{ report.pie_chart.title }}

4 | 5 |
6 | {{ report.pie_chart }} 7 |
-------------------------------------------------------------------------------- /docs/source/getting_started/template_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanbrumley/django-report-tools/3347705c6f7880a9766ac827ff7575e5a0b5bcb7/docs/source/getting_started/template_example.png -------------------------------------------------------------------------------- /docs/source/getting_started/views.py: -------------------------------------------------------------------------------- 1 | # myapp/views.py 2 | 3 | from django.shortcuts import render 4 | from myapp.reports import MyReport 5 | 6 | 7 | def my_report(request): 8 | # Initialise the report 9 | template = "myapp/my_report.html" 10 | report = MyReport() 11 | context = {'report': report} 12 | 13 | return render(request, template, context) 14 | -------------------------------------------------------------------------------- /docs/source/getting_started/views_class.py: -------------------------------------------------------------------------------- 1 | # myapp/views.py 2 | 3 | from django.shortcuts import render 4 | from myapp.reports import MyReport 5 | from report_tools.views import ReportView 6 | 7 | 8 | class MyReportView(ReportView): 9 | def get_report(self, request): 10 | return MyReport() 11 | 12 | def get(self, request): 13 | template = 'myapp/my_report.html' 14 | context = { 15 | 'report': self.get_report(request), 16 | } 17 | 18 | return render(request, template, context) 19 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. wsp-reports documentation master file, created by 2 | sphinx-quickstart on Thu Jan 12 14:58:42 2012. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Django Report Tools 7 | =================== 8 | 9 | Report tools aims to take the pain out of putting charts, graphs and tables into 10 | your Django projects. It lets you do the following: 11 | 12 | * Define your reports using the same syntax as Django forms and models 13 | * Use built-in 'renderers' to avoid the hassle of dealing with various 14 | charting technologies (currently only the Google Visualization Toolkit is supported) 15 | * Enter chart data in a standardised format 16 | * Build a simple API, allowing for the creation of chart exports or a 'save to dashboard' 17 | feature. 18 | 19 | An example report: 20 | 21 | .. literalinclude:: example.py 22 | :language: python 23 | 24 | For an expanation of this code, read on to the :doc:`getting started ` section. 25 | 26 | Contents 27 | -------- 28 | 29 | .. toctree:: 30 | :maxdepth: 2 31 | 32 | getting_started/index 33 | charts/index 34 | renderers/index 35 | chart_data/index 36 | api/index 37 | 38 | Indices and tables 39 | ------------------ 40 | 41 | * :ref:`genindex` 42 | * :ref:`modindex` 43 | * :ref:`search` 44 | 45 | -------------------------------------------------------------------------------- /docs/source/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanbrumley/django-report-tools/3347705c6f7880a9766ac827ff7575e5a0b5bcb7/docs/source/logo.png -------------------------------------------------------------------------------- /docs/source/renderers/basic_renderer_usage_example.py: -------------------------------------------------------------------------------- 1 | class MyReport(Report): 2 | renderer = GoogleChartsRenderer 3 | 4 | column_chart = charts.ColumnChart(title="Pony Populations", width="500") 5 | 6 | def get_data_for_column_chart(self): 7 | ... 8 | -------------------------------------------------------------------------------- /docs/source/renderers/chart_renderer_example.py: -------------------------------------------------------------------------------- 1 | from report_tools.renderers import ChartRenderer 2 | 3 | 4 | class MyChartRenderer(ChartRenderer): 5 | @classmethod 6 | def render_piechart(cls, chart_id, options, data, renderer_options): 7 | return "
Pie Chart
" % chart_id 8 | 9 | @classmethod 10 | def render_columnchart(cls, chart_id, options, data, renderer_options): 11 | return "
Column Chart
" % chart_id 12 | 13 | @classmethod 14 | def render_barchart(cls, chart_id, options, data, renderer_options): 15 | return "
Bar Chart
" % chart_id 16 | 17 | @classmethod 18 | def render_linechart(cls, chart_id, options, data, renderer_options): 19 | return "
Line Chart
" % chart_id -------------------------------------------------------------------------------- /docs/source/renderers/chart_renderer_usage_example.py: -------------------------------------------------------------------------------- 1 | class MyReport(Report): 2 | renderer = GoogleChartsRenderer 3 | 4 | column_chart = charts.ColumnChart(title="Pony Populations", width="500") 5 | 6 | column_chart_other_renderer = charts.ColumnChart( 7 | title="Pony Populations", 8 | width="500", 9 | renderer=SomeOtherRenderer 10 | ) 11 | 12 | def get_data_for_column_chart(self): 13 | ... 14 | 15 | def get_data_for_column_chart_other_renderer(self): 16 | ... -------------------------------------------------------------------------------- /docs/source/renderers/googlecharts/googlecharts_renderer_example.py: -------------------------------------------------------------------------------- 1 | class MyReport(Report): 2 | renderer = GoogleChartsRenderer 3 | 4 | stacked_column_chart = charts.ColumnChart( 5 | title="Pony Populations", 6 | width="500", 7 | renderer_options={ 8 | 'isStacked': True, 9 | 'legend': { 10 | 'position': 'none', 11 | }, 12 | 'backgroundColor': '#f5f5f5', 13 | 'series': [ 14 | {'color': '#ff0000'}, 15 | {'color': '#0000ff'}, 16 | ], 17 | } 18 | 19 | ) 20 | 21 | def get_data_for_stacked_column_chart(self): 22 | ... 23 | -------------------------------------------------------------------------------- /docs/source/renderers/googlecharts/index.rst: -------------------------------------------------------------------------------- 1 | Google Charts 2 | ============= 3 | 4 | .. class:: report_tools.renderers.googlecharts.GoogleChartsRenderer 5 | 6 | The google charts renderer uses 7 | `Google Chart Tools `_ 8 | to render the built-in chart types. 9 | 10 | Chart Support 11 | ------------- 12 | 13 | The google chart renderer supports all the built-in chart types described 14 | in the :doc:`chart documentation `. This includes: 15 | 16 | * Pie Charts 17 | * Column Charts 18 | * Multi-series Column Charts 19 | * Bar Charts 20 | * Multi-series Bar Charts 21 | * Line Charts 22 | * Multi-series Line Charts 23 | 24 | Extra Charts 25 | ------------ 26 | 27 | There are currently no additional chart types included with the google charts 28 | renderer, although support for table charts and geo charts is planned. 29 | 30 | Prerequisites 31 | ------------- 32 | 33 | To use the google charts renderer, you must import the google javascript api by 34 | including the following html in your page: 35 | 36 | .. code-block:: html 37 | 38 | 39 | 40 | Renderer Options 41 | ---------------- 42 | 43 | The ``renderer_options`` dictionary for charts using the google charts renderer 44 | is simply JSON encoded and passed directly into the chart initialization javascript. 45 | You therefore have full control over any parameter defined in the Google Chart Tools 46 | documentation: 47 | 48 | * `Google Chart Tools Pie Chart Documentation `_ 49 | * `Google Chart Tools Column Chart Documentation `_ 50 | * `Google Chart Tools Bar Chart Documentation `_ 51 | * `Google Chart Tools Line Chart Documentation `_ 52 | 53 | For example, if you want to create a stacked column chart 54 | with no legend, a light grey background and red and blue columns, your chart definition 55 | might look something like the following: 56 | 57 | .. literalinclude:: googlecharts_renderer_example.py 58 | :language: python 59 | 60 | 61 | Tips and Tricks 62 | --------------- 63 | 64 | If you need to override the default html/javascript that the google renderer creates, 65 | you can override the default templates at: 66 | 67 | * ``report_tools/renderers/googlecharts/barchart.html`` 68 | * ``report_tools/renderers/googlecharts/columnchart.html`` 69 | * ``report_tools/renderers/googlecharts/linechart.html`` 70 | * ``report_tools/renderers/googlecharts/piechart.html`` 71 | -------------------------------------------------------------------------------- /docs/source/renderers/index.rst: -------------------------------------------------------------------------------- 1 | Chart Renderers 2 | =============== 3 | 4 | Chart renderers control the way reports display on your site. Currently, 5 | the only included renderer is for Google Charts, but more are on the way, 6 | and it's easy to write one for your own favourite charting package. 7 | 8 | Included Renderers 9 | ------------------ 10 | 11 | .. toctree:: 12 | :maxdepth: 1 13 | 14 | googlecharts/index 15 | 16 | Basic Usage 17 | ----------- 18 | 19 | Renderers are typically defined on your report objects. For example: 20 | 21 | .. literalinclude:: basic_renderer_usage_example.py 22 | :language: python 23 | :emphasize-lines: 2 24 | 25 | You can also select renderers on a chart-by-chart basis. For example: 26 | 27 | .. literalinclude:: chart_renderer_usage_example.py 28 | :language: python 29 | :emphasize-lines: 9 30 | 31 | Talking to Your Renderer 32 | ------------------------ 33 | 34 | Above and beyond the basic options described in the :doc:`chart documentation `, 35 | individual renderers usually provide a lot of unique customization 36 | options. You can set these by passing in a ``renderer_options`` dictionary to 37 | the chart. For example, for a red background using the Google Charts renderer: 38 | 39 | .. literalinclude:: renderer_options_example.py 40 | :language: python 41 | 42 | For information on the the various options available, refer to the documentation 43 | for your chosen renderer above. 44 | 45 | Writing Your Own 46 | ---------------- 47 | 48 | A very simple stub of a chart renderer looks something like the following: 49 | 50 | .. literalinclude:: chart_renderer_example.py 51 | :language: python 52 | 53 | 54 | When a chart is rendered, it goes to the selected chart renderer class and tries to call an 55 | appropriate class method. This method will typically be named ``render_xxx`` where ``xxx`` 56 | is a lower case representation of the chart's class name. All rendering methods take the 57 | same parameters: 58 | 59 | chart_id 60 | A unique identifier for the chart. Safe for use as an html element id. 61 | 62 | options 63 | If a chart accepts additional parameters, such as width, height or template, 64 | they will be loaded into this dictionary. 65 | 66 | data 67 | The data returned by the chart's ``get_data_for_xxx`` method. This typically 68 | comes in as a ChartData object, so you'll need to wrangle it into something 69 | your charting package can read. 70 | 71 | renderer_options 72 | The renderer options specified when the chart was defined on the report. 73 | -------------------------------------------------------------------------------- /docs/source/renderers/renderer_options_example.py: -------------------------------------------------------------------------------- 1 | class MyReport(Report): 2 | renderer = GoogleChartsRenderer 3 | 4 | column_chart = charts.ColumnChart( 5 | title="Pony Populations", 6 | width="500", 7 | renderer_options={ 8 | 'backgroundColor': "#ff0000" 9 | } 10 | ) 11 | 12 | def get_data_for_column_chart(self): 13 | ... 14 | -------------------------------------------------------------------------------- /report_tools/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django report handling 3 | """ 4 | 5 | import charts 6 | import reports 7 | import api 8 | import views 9 | -------------------------------------------------------------------------------- /report_tools/api.py: -------------------------------------------------------------------------------- 1 | from copy import copy 2 | 3 | from django.http import QueryDict 4 | 5 | 6 | 7 | OVERRIDE_PARAMS__CHART_HEIGHT = '_height' 8 | OVERRIDE_PARAMS__CHART_WIDTH = '_width' 9 | OVERRIDE_PARAMS__CHART_TITLE = '_title' 10 | 11 | 12 | class ReportAPIRegistry(object): 13 | def __init__(self): 14 | self.reports = {} 15 | 16 | @property 17 | def api_keys(self): 18 | return self.reports.keys() 19 | 20 | def register(self, report_view_class, api_key): 21 | if api_key not in self.reports: 22 | self.reports[api_key] = report_view_class 23 | 24 | def get_report_view_class(self, api_key): 25 | return self.reports.get(api_key, None) 26 | 27 | 28 | report_api_registry = ReportAPIRegistry() 29 | 30 | 31 | def register(report_view_class, api_key=None): 32 | if api_key: 33 | report_api_registry.register(report_view_class, api_key) 34 | else: 35 | report_api_registry.register(report_view_class, report_view_class.api_key) 36 | 37 | 38 | def get_chart(request, api_key, chart_name, parameters=None, prefix=None): 39 | request = copy(request) 40 | if parameters is not None: 41 | new_get = QueryDict('', mutable=True) 42 | new_get.update(parameters) 43 | request.GET = new_get 44 | 45 | report_view_class = report_api_registry.get_report_view_class(api_key) 46 | 47 | if not report_view_class: 48 | raise ReportNotFoundError("Report not found for api key '%s'. Available reports are '%s'." % 49 | (api_key, ', '.join(report_api_registry.api_keys))) 50 | 51 | report_view = report_view_class() 52 | 53 | return report_view.get_chart(request, chart_name, prefix) 54 | 55 | 56 | class ReportNotFoundError(Exception): 57 | pass 58 | 59 | 60 | class ChartNotFoundError(Exception): 61 | pass 62 | -------------------------------------------------------------------------------- /report_tools/chart_data.py: -------------------------------------------------------------------------------- 1 | from copy import copy 2 | 3 | 4 | 5 | class ChartDataError(Exception): 6 | pass 7 | 8 | 9 | class ChartDataColumn(object): 10 | def __init__(self, name, metadata=None): 11 | self.name = copy(name) 12 | 13 | if metadata is not None: 14 | self.metadata = copy(metadata) 15 | else: 16 | self.metadata = {} 17 | 18 | def get_metadata(self): 19 | return self.metadata 20 | 21 | def get_metadata_item(self, key): 22 | return self.metadata.get(key, None) 23 | 24 | 25 | class ChartDataRow(object): 26 | def __init__(self, data, metadata=None): 27 | self.cells = [] 28 | 29 | for datum in data: 30 | if type(datum) != ChartDataCell: 31 | if type(datum) in (list, tuple): 32 | datum = ChartDataCell(datum[0], datum[1]) 33 | else: 34 | datum = ChartDataCell(datum) 35 | 36 | self.cells.append(datum) 37 | 38 | if metadata is not None: 39 | self.metadata = copy(metadata) 40 | else: 41 | self.metadata = {} 42 | 43 | def __iter__(self): 44 | for cell in self.cells: 45 | yield cell 46 | 47 | def __getitem__(self, index): 48 | return self.cells[index] 49 | 50 | 51 | class ChartDataCell(object): 52 | def __init__(self, data, metadata=None): 53 | self.data = copy(data) 54 | 55 | if metadata is not None: 56 | self.metadata = copy(metadata) 57 | else: 58 | self.metadata = {} 59 | 60 | 61 | class ChartData(object): 62 | 63 | def __init__(self): 64 | self.columns = [] 65 | self.rows = [] 66 | 67 | def get_columns(self): 68 | return self.columns 69 | 70 | def get_rows(self): 71 | return self.rows 72 | 73 | def add_column(self, name, metadata=None): 74 | if self.rows: 75 | raise ChartDataError("Cannot add columns after data has been entered") 76 | 77 | column = ChartDataColumn(name, metadata) 78 | self.columns.append(column) 79 | 80 | def add_columns(self, columns): 81 | for column in columns: 82 | if type(column) in (list, tuple): 83 | name = column[0] 84 | metadata = column[1] 85 | else: 86 | name = column 87 | metadata = {} 88 | 89 | self.add_column(name, metadata) 90 | 91 | def add_row(self, data, metadata=None): 92 | if len(data) < len(self.columns): 93 | raise ChartDataError("Not enough data points (%s) for the given number of columns (%s)" % (len(data), len(self.columns))) 94 | 95 | if len(data) > len(self.columns): 96 | raise ChartDataError("Too many data points (%s) for the given number of columns (%s)" % (len(data), len(self.columns))) 97 | 98 | row = ChartDataRow(data, metadata) 99 | self.rows.append(row) 100 | 101 | def add_rows(self, rows): 102 | for row in rows: 103 | if type(row) in (list, tuple): 104 | data = row[0] 105 | metadata = row[1] 106 | else: 107 | data = row 108 | metadata = {} 109 | 110 | self.add_row(data, metadata) 111 | -------------------------------------------------------------------------------- /report_tools/charts.py: -------------------------------------------------------------------------------- 1 | from django.utils.encoding import smart_unicode 2 | from django.template.loader import render_to_string 3 | from django.utils.safestring import mark_safe 4 | 5 | 6 | 7 | class Chart(object): 8 | # Tracks each time a Chart instance is created. Used to retain order. 9 | creation_counter = 0 10 | name = None 11 | 12 | def __init__(self, title=None, renderer=None, renderer_options={}, attrs={}): 13 | if title is not None: 14 | title = smart_unicode(title) 15 | 16 | self.title = title 17 | 18 | self.renderer = renderer 19 | self.renderer_options = renderer_options 20 | self.attrs = attrs 21 | self.options = {} 22 | 23 | self.creation_counter = Chart.creation_counter 24 | Chart.creation_counter += 1 25 | 26 | def __unicode__(self): 27 | return self.name 28 | 29 | def render(self, chart_id, data, base_renderer=None): 30 | if not self.name: 31 | raise NotImplementedError 32 | 33 | if self.renderer: 34 | renderer = self.renderer 35 | else: 36 | renderer = base_renderer 37 | 38 | if renderer: 39 | render_method_name = 'render_' + self.name 40 | render_method = getattr(renderer, render_method_name, None) 41 | 42 | if render_method: 43 | return render_method(chart_id, self.options, data, self.renderer_options) 44 | else: 45 | raise NotImplementedError 46 | else: 47 | raise RendererRequiredError 48 | 49 | @classmethod 50 | def get_empty_data_object(cls, sort=None): 51 | raise NotImplementedError 52 | 53 | 54 | class RendererRequiredError(Exception): 55 | pass 56 | 57 | 58 | class DimensionedChart(Chart): 59 | def __init__(self, *args, **kwargs): 60 | width = kwargs.pop('width', None) 61 | height = kwargs.pop('height', None) 62 | 63 | super(DimensionedChart, self).__init__(*args, **kwargs) 64 | 65 | self.options['width'] = width 66 | self.options['height'] = height 67 | 68 | 69 | class PieChart(DimensionedChart): 70 | name = 'piechart' 71 | 72 | 73 | class BarChart(DimensionedChart): 74 | name = 'barchart' 75 | 76 | 77 | class ColumnChart(DimensionedChart): 78 | name = 'columnchart' 79 | 80 | 81 | class LineChart(DimensionedChart): 82 | name = 'linechart' 83 | 84 | 85 | class TemplateChart(Chart): 86 | name = 'templatechart' 87 | 88 | def __init__(self, template, *args, **kwargs): 89 | self.template = template 90 | super(TemplateChart, self).__init__(*args, **kwargs) 91 | 92 | def render(self, chart_id, data={}, base_renderer=None): 93 | if 'chart_id' not in data: 94 | data['chart_id'] = chart_id 95 | 96 | html = render_to_string(self.template, data) 97 | return mark_safe(html) 98 | 99 | 100 | class DummyChart(Chart): 101 | def render(self, chart_id, data, *args, **kwargs): 102 | return u'%s' % data 103 | -------------------------------------------------------------------------------- /report_tools/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanbrumley/django-report-tools/3347705c6f7880a9766ac827ff7575e5a0b5bcb7/report_tools/models.py -------------------------------------------------------------------------------- /report_tools/renderers/__init__.py: -------------------------------------------------------------------------------- 1 | class ChartRenderer(object): 2 | @classmethod 3 | def render_piechart(cls, chart_id, options, data, renderer_options): 4 | raise NotImplementedError 5 | 6 | @classmethod 7 | def render_columnchart(cls, chart_id, options, data, renderer_options): 8 | raise NotImplementedError 9 | 10 | @classmethod 11 | def render_barchart(cls, chart_id, options, data, renderer_options): 12 | raise NotImplementedError 13 | 14 | @classmethod 15 | def render_linechart(cls, chart_id, options, data, renderer_options): 16 | raise NotImplementedError 17 | 18 | 19 | class ChartRendererError(Exception): 20 | pass 21 | -------------------------------------------------------------------------------- /report_tools/renderers/googlecharts/__init__.py: -------------------------------------------------------------------------------- 1 | from report_tools.renderers import ChartRenderer, ChartRendererError 2 | from report_tools.renderers.googlecharts.gviz_api import gviz_api 3 | from django.template.loader import render_to_string 4 | from django.utils.safestring import mark_safe 5 | from django.utils.encoding import force_unicode 6 | 7 | try: 8 | import json 9 | except ImportError: 10 | import simplejson as json 11 | 12 | 13 | DEFAULT_WIDTH = 400 14 | DEFAULT_HEIGHT = 300 15 | 16 | 17 | class JSONEncoderForHTML(json.JSONEncoder): 18 | """ 19 | An encoder that produces JSON safe to embed in HTML. 20 | 21 | To embed JSON content in, say, a script tag on a web page, the 22 | characters &, < and > should be escaped. They cannot be escaped 23 | with the usual entities (e.g. &) because they are not expanded 24 | within 54 | """ 55 | @classmethod 56 | def render_piechart(cls, chart_id, options, data, renderer_options): 57 | template = 'report_tools/renderers/googlecharts/piechart.html' 58 | return cls._render(chart_id, options, data, renderer_options, template) 59 | 60 | @classmethod 61 | def render_columnchart(cls, chart_id, options, data, renderer_options): 62 | template = 'report_tools/renderers/googlecharts/columnchart.html' 63 | return cls._render(chart_id, options, data, renderer_options, template) 64 | 65 | @classmethod 66 | def render_barchart(cls, chart_id, options, data, renderer_options): 67 | template = 'report_tools/renderers/googlecharts/barchart.html' 68 | return cls._render(chart_id, options, data, renderer_options, template) 69 | 70 | @classmethod 71 | def render_linechart(cls, chart_id, options, data, renderer_options): 72 | template = 'report_tools/renderers/googlecharts/linechart.html' 73 | return cls._render(chart_id, options, data, renderer_options, template) 74 | 75 | @classmethod 76 | def _render(cls, chart_id, options, data, renderer_options, template): 77 | gchart_options = cls._process_base_options(options) 78 | gchart_options.update(renderer_options) 79 | data_json = mark_safe(GoogleChartsDataConverter.convert_to_datatable_json(data)) 80 | 81 | context = { 82 | 'chart_id': chart_id, 83 | 'data_json': data_json, 84 | 'options': mark_safe(json.dumps(gchart_options, cls=JSONEncoderForHTML)), 85 | } 86 | 87 | html = render_to_string(template, context) 88 | return mark_safe(html) 89 | 90 | @classmethod 91 | def _process_base_options(cls, options): 92 | gchart_options = { 93 | 'width': options.get('width', None), 94 | 'height': options.get('height', None), 95 | } 96 | 97 | return gchart_options 98 | 99 | 100 | class GoogleChartsDataConverter(object): 101 | @classmethod 102 | def convert_to_datatable_json(cls, data): 103 | # TODO: At some point we may need to expand this method to 104 | # account for non label-number chartdata types (i.e. geodata) 105 | return cls.convert_standard_chartdata_to_datatable_json(data) 106 | 107 | @classmethod 108 | def convert_standard_chartdata_to_datatable_json(cls, data): 109 | """ 110 | Converts a ChartData object to a datatable json blob assuming 111 | that the first column is a label and all subsequent columns are numbers 112 | """ 113 | cols = data.get_columns() 114 | 115 | label_col = cols[0] 116 | data_cols = cols[1:] 117 | 118 | if 'datatype' not in label_col.metadata: 119 | label_col.metadata['datatype'] = 'string' 120 | 121 | for col in data_cols: 122 | if 'datatype' not in col.metadata: 123 | col.metadata['datatype'] = 'number' 124 | 125 | return cls.convert_generic_to_datatable_json(data) 126 | 127 | @classmethod 128 | def convert_generic_to_datatable_json(cls, data): 129 | cols = data.get_columns() 130 | 131 | for col in cols: 132 | if 'datatype' not in col.metadata: 133 | col.metadata['datatype'] = 'string' 134 | 135 | description = {} 136 | index = 1 137 | columns_order = [] 138 | for col in cols: 139 | columns_order.append(str(index)) 140 | description[str(index)] = (col.metadata['datatype'], col.name) 141 | index += 1 142 | 143 | datatable_data = [] 144 | 145 | for row in data.get_rows(): 146 | datatable_data_row = {} 147 | index = 1 148 | for datum_cell in row: 149 | data_description = datum_cell.metadata.get('formatted_value', datum_cell.data) 150 | datatable_data_row[str(index)] = (datum_cell.data, force_unicode(data_description)) 151 | index += 1 152 | 153 | datatable_data.append(datatable_data_row) 154 | 155 | data_table = gviz_api.DataTable(description) 156 | data_table.LoadData(datatable_data) 157 | 158 | return data_table.ToJSon(columns_order=columns_order) 159 | -------------------------------------------------------------------------------- /report_tools/renderers/googlecharts/gviz_api/COPYRIGHT: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /report_tools/renderers/googlecharts/gviz_api/README: -------------------------------------------------------------------------------- 1 | Copyright (C) 2009 Google Inc. 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 | 15 | Installing the library: 16 | python ./setup.py install 17 | (You might need root privileges to do this) 18 | 19 | Testing the library: 20 | python ./setup.py test 21 | 22 | Dependencies: 23 | On Python <2.6 you will need to have simplejson[1] installed on your system. 24 | 25 | [1]: http://pypi.python.org/pypi/simplejson/ 26 | -------------------------------------------------------------------------------- /report_tools/renderers/googlecharts/gviz_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanbrumley/django-report-tools/3347705c6f7880a9766ac827ff7575e5a0b5bcb7/report_tools/renderers/googlecharts/gviz_api/__init__.py -------------------------------------------------------------------------------- /report_tools/renderers/googlecharts/gviz_api/gviz_api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Copyright (C) 2009 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """Converts Python data into data for Google Visualization API clients. 18 | 19 | This library can be used to create a google.visualization.DataTable usable by 20 | visualizations built on the Google Visualization API. Output formats are raw 21 | JSON, JSON response, JavaScript, CSV, and HTML table. 22 | 23 | See http://code.google.com/apis/visualization/ for documentation on the 24 | Google Visualization API. 25 | """ 26 | 27 | __author__ = "Amit Weinstein, Misha Seltzer, Jacob Baskin" 28 | 29 | import cgi 30 | import cStringIO 31 | import csv 32 | import datetime 33 | try: 34 | import json 35 | except ImportError: 36 | import simplejson as json 37 | import types 38 | 39 | 40 | 41 | class DataTableException(Exception): 42 | """The general exception object thrown by DataTable.""" 43 | pass 44 | 45 | 46 | class DataTableJSONEncoder(json.JSONEncoder): 47 | """JSON encoder that handles date/time/datetime objects correctly.""" 48 | 49 | def __init__(self): 50 | json.JSONEncoder.__init__(self, 51 | separators=(",", ":"), 52 | ensure_ascii=False) 53 | 54 | def encode(self, o): 55 | # Override JSONEncoder.encode because it has hacks for 56 | # performance that make things more complicated. 57 | chunks = self.iterencode(o) 58 | if self.ensure_ascii: 59 | return ''.join(chunks) 60 | else: 61 | return u''.join(chunks) 62 | 63 | # Added by Evan Brumley for django-report-tools 64 | # This code allows the datatable JSON to be safely rendered 65 | # into an html page without being screwed up by ampersands 66 | # and tags 67 | def iterencode(self, o): 68 | chunks = super(DataTableJSONEncoder, self).iterencode(o) 69 | for chunk in chunks: 70 | chunk = chunk.replace('&', '\\u0026') 71 | chunk = chunk.replace('<', '\\u003c') 72 | chunk = chunk.replace('>', '\\u003e') 73 | yield chunk 74 | 75 | def default(self, o): 76 | if isinstance(o, datetime.datetime): 77 | if o.microsecond == 0: 78 | # If the time doesn't have ms-resolution, leave it out to keep 79 | # things smaller. 80 | return "Date(%d,%d,%d,%d,%d,%d)" % ( 81 | o.year, o.month - 1, o.day, o.hour, o.minute, o.second) 82 | else: 83 | return "Date(%d,%d,%d,%d,%d,%d,%d)" % ( 84 | o.year, o.month - 1, o.day, o.hour, o.minute, o.second, 85 | o.microsecond / 1000) 86 | elif isinstance(o, datetime.date): 87 | return "Date(%d,%d,%d)" % (o.year, o.month - 1, o.day) 88 | elif isinstance(o, datetime.time): 89 | return [o.hour, o.minute, o.second] 90 | else: 91 | return super(DataTableJSONEncoder, self).default(o) 92 | 93 | 94 | class DataTable(object): 95 | """Wraps the data to convert to a Google Visualization API DataTable. 96 | 97 | Create this object, populate it with data, then call one of the ToJS... 98 | methods to return a string representation of the data in the format described. 99 | 100 | You can clear all data from the object to reuse it, but you cannot clear 101 | individual cells, rows, or columns. You also cannot modify the table schema 102 | specified in the class constructor. 103 | 104 | You can add new data one or more rows at a time. All data added to an 105 | instantiated DataTable must conform to the schema passed in to __init__(). 106 | 107 | You can reorder the columns in the output table, and also specify row sorting 108 | order by column. The default column order is according to the original 109 | table_description parameter. Default row sort order is ascending, by column 110 | 1 values. For a dictionary, we sort the keys for order. 111 | 112 | The data and the table_description are closely tied, as described here: 113 | 114 | The table schema is defined in the class constructor's table_description 115 | parameter. The user defines each column using a tuple of 116 | (id[, type[, label[, custom_properties]]]). The default value for type is 117 | string, label is the same as ID if not specified, and custom properties is 118 | an empty dictionary if not specified. 119 | 120 | table_description is a dictionary or list, containing one or more column 121 | descriptor tuples, nested dictionaries, and lists. Each dictionary key, list 122 | element, or dictionary element must eventually be defined as 123 | a column description tuple. Here's an example of a dictionary where the key 124 | is a tuple, and the value is a list of two tuples: 125 | {('a', 'number'): [('b', 'number'), ('c', 'string')]} 126 | 127 | This flexibility in data entry enables you to build and manipulate your data 128 | in a Python structure that makes sense for your program. 129 | 130 | Add data to the table using the same nested design as the table's 131 | table_description, replacing column descriptor tuples with cell data, and 132 | each row is an element in the top level collection. This will be a bit 133 | clearer after you look at the following examples showing the 134 | table_description, matching data, and the resulting table: 135 | 136 | Columns as list of tuples [col1, col2, col3] 137 | table_description: [('a', 'number'), ('b', 'string')] 138 | AppendData( [[1, 'z'], [2, 'w'], [4, 'o'], [5, 'k']] ) 139 | Table: 140 | a b <--- these are column ids/labels 141 | 1 z 142 | 2 w 143 | 4 o 144 | 5 k 145 | 146 | Dictionary of columns, where key is a column, and value is a list of 147 | columns {col1: [col2, col3]} 148 | table_description: {('a', 'number'): [('b', 'number'), ('c', 'string')]} 149 | AppendData( data: {1: [2, 'z'], 3: [4, 'w']} 150 | Table: 151 | a b c 152 | 1 2 z 153 | 3 4 w 154 | 155 | Dictionary where key is a column, and the value is itself a dictionary of 156 | columns {col1: {col2, col3}} 157 | table_description: {('a', 'number'): {'b': 'number', 'c': 'string'}} 158 | AppendData( data: {1: {'b': 2, 'c': 'z'}, 3: {'b': 4, 'c': 'w'}} 159 | Table: 160 | a b c 161 | 1 2 z 162 | 3 4 w 163 | """ 164 | 165 | def __init__(self, table_description, data=None, custom_properties=None): 166 | """Initialize the data table from a table schema and (optionally) data. 167 | 168 | See the class documentation for more information on table schema and data 169 | values. 170 | 171 | Args: 172 | table_description: A table schema, following one of the formats described 173 | in TableDescriptionParser(). Schemas describe the 174 | column names, data types, and labels. See 175 | TableDescriptionParser() for acceptable formats. 176 | data: Optional. If given, fills the table with the given data. The data 177 | structure must be consistent with schema in table_description. See 178 | the class documentation for more information on acceptable data. You 179 | can add data later by calling AppendData(). 180 | custom_properties: Optional. A dictionary from string to string that 181 | goes into the table's custom properties. This can be 182 | later changed by changing self.custom_properties. 183 | 184 | Raises: 185 | DataTableException: Raised if the data and the description did not match, 186 | or did not use the supported formats. 187 | """ 188 | self.__columns = self.TableDescriptionParser(table_description) 189 | self.__data = [] 190 | self.custom_properties = {} 191 | if custom_properties is not None: 192 | self.custom_properties = custom_properties 193 | if data: 194 | self.LoadData(data) 195 | 196 | @staticmethod 197 | def CoerceValue(value, value_type): 198 | """Coerces a single value into the type expected for its column. 199 | 200 | Internal helper method. 201 | 202 | Args: 203 | value: The value which should be converted 204 | value_type: One of "string", "number", "boolean", "date", "datetime" or 205 | "timeofday". 206 | 207 | Returns: 208 | An item of the Python type appropriate to the given value_type. Strings 209 | are also converted to Unicode using UTF-8 encoding if necessary. 210 | If a tuple is given, it should be in one of the following forms: 211 | - (value, formatted value) 212 | - (value, formatted value, custom properties) 213 | where the formatted value is a string, and custom properties is a 214 | dictionary of the custom properties for this cell. 215 | To specify custom properties without specifying formatted value, one can 216 | pass None as the formatted value. 217 | One can also have a null-valued cell with formatted value and/or custom 218 | properties by specifying None for the value. 219 | This method ignores the custom properties except for checking that it is a 220 | dictionary. The custom properties are handled in the ToJSon and ToJSCode 221 | methods. 222 | The real type of the given value is not strictly checked. For example, 223 | any type can be used for string - as we simply take its str( ) and for 224 | boolean value we just check "if value". 225 | Examples: 226 | CoerceValue(None, "string") returns None 227 | CoerceValue((5, "5$"), "number") returns (5, "5$") 228 | CoerceValue(100, "string") returns "100" 229 | CoerceValue(0, "boolean") returns False 230 | 231 | Raises: 232 | DataTableException: The value and type did not match in a not-recoverable 233 | way, for example given value 'abc' for type 'number'. 234 | """ 235 | if isinstance(value, tuple): 236 | # In case of a tuple, we run the same function on the value itself and 237 | # add the formatted value. 238 | if (len(value) not in [2, 3] or 239 | (len(value) == 3 and not isinstance(value[2], dict))): 240 | raise DataTableException("Wrong format for value and formatting - %s." % 241 | str(value)) 242 | if not isinstance(value[1], types.StringTypes + (types.NoneType,)): 243 | raise DataTableException("Formatted value is not string, given %s." % 244 | type(value[1])) 245 | js_value = DataTable.CoerceValue(value[0], value_type) 246 | return (js_value,) + value[1:] 247 | 248 | t_value = type(value) 249 | if value is None: 250 | return value 251 | if value_type == "boolean": 252 | return bool(value) 253 | 254 | elif value_type == "number": 255 | if isinstance(value, (int, long, float)): 256 | return value 257 | raise DataTableException("Wrong type %s when expected number" % t_value) 258 | 259 | elif value_type == "string": 260 | if isinstance(value, unicode): 261 | return value 262 | else: 263 | return str(value).decode("utf-8") 264 | 265 | elif value_type == "date": 266 | if isinstance(value, datetime.datetime): 267 | return datetime.date(value.year, value.month, value.day) 268 | elif isinstance(value, datetime.date): 269 | return value 270 | else: 271 | raise DataTableException("Wrong type %s when expected date" % t_value) 272 | 273 | elif value_type == "timeofday": 274 | if isinstance(value, datetime.datetime): 275 | return datetime.time(value.hour, value.minute, value.second) 276 | elif isinstance(value, datetime.time): 277 | return value 278 | else: 279 | raise DataTableException("Wrong type %s when expected time" % t_value) 280 | 281 | elif value_type == "datetime": 282 | if isinstance(value, datetime.datetime): 283 | return value 284 | else: 285 | raise DataTableException("Wrong type %s when expected datetime" % 286 | t_value) 287 | # If we got here, it means the given value_type was not one of the 288 | # supported types. 289 | raise DataTableException("Unsupported type %s" % value_type) 290 | 291 | @staticmethod 292 | def EscapeForJSCode(encoder, value): 293 | if value is None: 294 | return "null" 295 | elif isinstance(value, datetime.datetime): 296 | if value.microsecond == 0: 297 | # If it's not ms-resolution, leave that out to save space. 298 | return "new Date(%d,%d,%d,%d,%d,%d)" % (value.year, 299 | value.month - 1, # To match JS 300 | value.day, 301 | value.hour, 302 | value.minute, 303 | value.second) 304 | else: 305 | return "new Date(%d,%d,%d,%d,%d,%d,%d)" % (value.year, 306 | value.month - 1, # match JS 307 | value.day, 308 | value.hour, 309 | value.minute, 310 | value.second, 311 | value.microsecond / 1000) 312 | elif isinstance(value, datetime.date): 313 | return "new Date(%d,%d,%d)" % (value.year, value.month - 1, value.day) 314 | else: 315 | return encoder.encode(value) 316 | 317 | @staticmethod 318 | def ToString(value): 319 | if value is None: 320 | return "(empty)" 321 | elif isinstance(value, (datetime.datetime, 322 | datetime.date, 323 | datetime.time)): 324 | return str(value) 325 | elif isinstance(value, unicode): 326 | return value 327 | elif isinstance(value, bool): 328 | return str(value).lower() 329 | else: 330 | return str(value).decode("utf-8") 331 | 332 | @staticmethod 333 | def ColumnTypeParser(description): 334 | """Parses a single column description. Internal helper method. 335 | 336 | Args: 337 | description: a column description in the possible formats: 338 | 'id' 339 | ('id',) 340 | ('id', 'type') 341 | ('id', 'type', 'label') 342 | ('id', 'type', 'label', {'custom_prop1': 'custom_val1'}) 343 | Returns: 344 | Dictionary with the following keys: id, label, type, and 345 | custom_properties where: 346 | - If label not given, it equals the id. 347 | - If type not given, string is used by default. 348 | - If custom properties are not given, an empty dictionary is used by 349 | default. 350 | 351 | Raises: 352 | DataTableException: The column description did not match the RE, or 353 | unsupported type was passed. 354 | """ 355 | if not description: 356 | raise DataTableException("Description error: empty description given") 357 | 358 | if not isinstance(description, (types.StringTypes, tuple)): 359 | raise DataTableException("Description error: expected either string or " 360 | "tuple, got %s." % type(description)) 361 | 362 | if isinstance(description, types.StringTypes): 363 | description = (description,) 364 | 365 | # According to the tuple's length, we fill the keys 366 | # We verify everything is of type string 367 | for elem in description[:3]: 368 | if not isinstance(elem, types.StringTypes): 369 | raise DataTableException("Description error: expected tuple of " 370 | "strings, current element of type %s." % 371 | type(elem)) 372 | desc_dict = {"id": description[0], 373 | "label": description[0], 374 | "type": "string", 375 | "custom_properties": {}} 376 | if len(description) > 1: 377 | desc_dict["type"] = description[1].lower() 378 | if len(description) > 2: 379 | desc_dict["label"] = description[2] 380 | if len(description) > 3: 381 | if not isinstance(description[3], dict): 382 | raise DataTableException("Description error: expected custom " 383 | "properties of type dict, current element " 384 | "of type %s." % type(description[3])) 385 | desc_dict["custom_properties"] = description[3] 386 | if len(description) > 4: 387 | raise DataTableException("Description error: tuple of length > 4") 388 | if desc_dict["type"] not in ["string", "number", "boolean", 389 | "date", "datetime", "timeofday"]: 390 | raise DataTableException( 391 | "Description error: unsupported type '%s'" % desc_dict["type"]) 392 | return desc_dict 393 | 394 | @staticmethod 395 | def TableDescriptionParser(table_description, depth=0): 396 | """Parses the table_description object for internal use. 397 | 398 | Parses the user-submitted table description into an internal format used 399 | by the Python DataTable class. Returns the flat list of parsed columns. 400 | 401 | Args: 402 | table_description: A description of the table which should comply 403 | with one of the formats described below. 404 | depth: Optional. The depth of the first level in the current description. 405 | Used by recursive calls to this function. 406 | 407 | Returns: 408 | List of columns, where each column represented by a dictionary with the 409 | keys: id, label, type, depth, container which means the following: 410 | - id: the id of the column 411 | - name: The name of the column 412 | - type: The datatype of the elements in this column. Allowed types are 413 | described in ColumnTypeParser(). 414 | - depth: The depth of this column in the table description 415 | - container: 'dict', 'iter' or 'scalar' for parsing the format easily. 416 | - custom_properties: The custom properties for this column. 417 | The returned description is flattened regardless of how it was given. 418 | 419 | Raises: 420 | DataTableException: Error in a column description or in the description 421 | structure. 422 | 423 | Examples: 424 | A column description can be of the following forms: 425 | 'id' 426 | ('id',) 427 | ('id', 'type') 428 | ('id', 'type', 'label') 429 | ('id', 'type', 'label', {'custom_prop1': 'custom_val1'}) 430 | or as a dictionary: 431 | 'id': 'type' 432 | 'id': ('type',) 433 | 'id': ('type', 'label') 434 | 'id': ('type', 'label', {'custom_prop1': 'custom_val1'}) 435 | If the type is not specified, we treat it as string. 436 | If no specific label is given, the label is simply the id. 437 | If no custom properties are given, we use an empty dictionary. 438 | 439 | input: [('a', 'date'), ('b', 'timeofday', 'b', {'foo': 'bar'})] 440 | output: [{'id': 'a', 'label': 'a', 'type': 'date', 441 | 'depth': 0, 'container': 'iter', 'custom_properties': {}}, 442 | {'id': 'b', 'label': 'b', 'type': 'timeofday', 443 | 'depth': 0, 'container': 'iter', 444 | 'custom_properties': {'foo': 'bar'}}] 445 | 446 | input: {'a': [('b', 'number'), ('c', 'string', 'column c')]} 447 | output: [{'id': 'a', 'label': 'a', 'type': 'string', 448 | 'depth': 0, 'container': 'dict', 'custom_properties': {}}, 449 | {'id': 'b', 'label': 'b', 'type': 'number', 450 | 'depth': 1, 'container': 'iter', 'custom_properties': {}}, 451 | {'id': 'c', 'label': 'column c', 'type': 'string', 452 | 'depth': 1, 'container': 'iter', 'custom_properties': {}}] 453 | 454 | input: {('a', 'number', 'column a'): { 'b': 'number', 'c': 'string'}} 455 | output: [{'id': 'a', 'label': 'column a', 'type': 'number', 456 | 'depth': 0, 'container': 'dict', 'custom_properties': {}}, 457 | {'id': 'b', 'label': 'b', 'type': 'number', 458 | 'depth': 1, 'container': 'dict', 'custom_properties': {}}, 459 | {'id': 'c', 'label': 'c', 'type': 'string', 460 | 'depth': 1, 'container': 'dict', 'custom_properties': {}}] 461 | 462 | input: { ('w', 'string', 'word'): ('c', 'number', 'count') } 463 | output: [{'id': 'w', 'label': 'word', 'type': 'string', 464 | 'depth': 0, 'container': 'dict', 'custom_properties': {}}, 465 | {'id': 'c', 'label': 'count', 'type': 'number', 466 | 'depth': 1, 'container': 'scalar', 'custom_properties': {}}] 467 | 468 | input: {'a': ('number', 'column a'), 'b': ('string', 'column b')} 469 | output: [{'id': 'a', 'label': 'column a', 'type': 'number', 'depth': 0, 470 | 'container': 'dict', 'custom_properties': {}}, 471 | {'id': 'b', 'label': 'column b', 'type': 'string', 'depth': 0, 472 | 'container': 'dict', 'custom_properties': {}} 473 | 474 | NOTE: there might be ambiguity in the case of a dictionary representation 475 | of a single column. For example, the following description can be parsed 476 | in 2 different ways: {'a': ('b', 'c')} can be thought of a single column 477 | with the id 'a', of type 'b' and the label 'c', or as 2 columns: one named 478 | 'a', and the other named 'b' of type 'c'. We choose the first option by 479 | default, and in case the second option is the right one, it is possible to 480 | make the key into a tuple (i.e. {('a',): ('b', 'c')}) or add more info 481 | into the tuple, thus making it look like this: {'a': ('b', 'c', 'b', {})} 482 | -- second 'b' is the label, and {} is the custom properties field. 483 | """ 484 | # For the recursion step, we check for a scalar object (string or tuple) 485 | if isinstance(table_description, (types.StringTypes, tuple)): 486 | parsed_col = DataTable.ColumnTypeParser(table_description) 487 | parsed_col["depth"] = depth 488 | parsed_col["container"] = "scalar" 489 | return [parsed_col] 490 | 491 | # Since it is not scalar, table_description must be iterable. 492 | if not hasattr(table_description, "__iter__"): 493 | raise DataTableException("Expected an iterable object, got %s" % 494 | type(table_description)) 495 | if not isinstance(table_description, dict): 496 | # We expects a non-dictionary iterable item. 497 | columns = [] 498 | for desc in table_description: 499 | parsed_col = DataTable.ColumnTypeParser(desc) 500 | parsed_col["depth"] = depth 501 | parsed_col["container"] = "iter" 502 | columns.append(parsed_col) 503 | if not columns: 504 | raise DataTableException("Description iterable objects should not" 505 | " be empty.") 506 | return columns 507 | # The other case is a dictionary 508 | if not table_description: 509 | raise DataTableException("Empty dictionaries are not allowed inside" 510 | " description") 511 | 512 | # To differentiate between the two cases of more levels below or this is 513 | # the most inner dictionary, we consider the number of keys (more then one 514 | # key is indication for most inner dictionary) and the type of the key and 515 | # value in case of only 1 key (if the type of key is string and the type of 516 | # the value is a tuple of 0-3 items, we assume this is the most inner 517 | # dictionary). 518 | # NOTE: this way of differentiating might create ambiguity. See docs. 519 | if (len(table_description) != 1 or 520 | (isinstance(table_description.keys()[0], types.StringTypes) and 521 | isinstance(table_description.values()[0], tuple) and 522 | len(table_description.values()[0]) < 4)): 523 | # This is the most inner dictionary. Parsing types. 524 | columns = [] 525 | # We sort the items, equivalent to sort the keys since they are unique 526 | for key, value in sorted(table_description.items()): 527 | # We parse the column type as (key, type) or (key, type, label) using 528 | # ColumnTypeParser. 529 | if isinstance(value, tuple): 530 | parsed_col = DataTable.ColumnTypeParser((key,) + value) 531 | else: 532 | parsed_col = DataTable.ColumnTypeParser((key, value)) 533 | parsed_col["depth"] = depth 534 | parsed_col["container"] = "dict" 535 | columns.append(parsed_col) 536 | return columns 537 | # This is an outer dictionary, must have at most one key. 538 | parsed_col = DataTable.ColumnTypeParser(table_description.keys()[0]) 539 | parsed_col["depth"] = depth 540 | parsed_col["container"] = "dict" 541 | return ([parsed_col] + 542 | DataTable.TableDescriptionParser(table_description.values()[0], 543 | depth=depth + 1)) 544 | 545 | @property 546 | def columns(self): 547 | """Returns the parsed table description.""" 548 | return self.__columns 549 | 550 | def NumberOfRows(self): 551 | """Returns the number of rows in the current data stored in the table.""" 552 | return len(self.__data) 553 | 554 | def SetRowsCustomProperties(self, rows, custom_properties): 555 | """Sets the custom properties for given row(s). 556 | 557 | Can accept a single row or an iterable of rows. 558 | Sets the given custom properties for all specified rows. 559 | 560 | Args: 561 | rows: The row, or rows, to set the custom properties for. 562 | custom_properties: A string to string dictionary of custom properties to 563 | set for all rows. 564 | """ 565 | if not hasattr(rows, "__iter__"): 566 | rows = [rows] 567 | for row in rows: 568 | self.__data[row] = (self.__data[row][0], custom_properties) 569 | 570 | def LoadData(self, data, custom_properties=None): 571 | """Loads new rows to the data table, clearing existing rows. 572 | 573 | May also set the custom_properties for the added rows. The given custom 574 | properties dictionary specifies the dictionary that will be used for *all* 575 | given rows. 576 | 577 | Args: 578 | data: The rows that the table will contain. 579 | custom_properties: A dictionary of string to string to set as the custom 580 | properties for all rows. 581 | """ 582 | self.__data = [] 583 | self.AppendData(data, custom_properties) 584 | 585 | def AppendData(self, data, custom_properties=None): 586 | """Appends new data to the table. 587 | 588 | Data is appended in rows. Data must comply with 589 | the table schema passed in to __init__(). See CoerceValue() for a list 590 | of acceptable data types. See the class documentation for more information 591 | and examples of schema and data values. 592 | 593 | Args: 594 | data: The row to add to the table. The data must conform to the table 595 | description format. 596 | custom_properties: A dictionary of string to string, representing the 597 | custom properties to add to all the rows. 598 | 599 | Raises: 600 | DataTableException: The data structure does not match the description. 601 | """ 602 | # If the maximal depth is 0, we simply iterate over the data table 603 | # lines and insert them using _InnerAppendData. Otherwise, we simply 604 | # let the _InnerAppendData handle all the levels. 605 | if not self.__columns[-1]["depth"]: 606 | for row in data: 607 | self._InnerAppendData(({}, custom_properties), row, 0) 608 | else: 609 | self._InnerAppendData(({}, custom_properties), data, 0) 610 | 611 | def _InnerAppendData(self, prev_col_values, data, col_index): 612 | """Inner function to assist LoadData.""" 613 | # We first check that col_index has not exceeded the columns size 614 | if col_index >= len(self.__columns): 615 | raise DataTableException("The data does not match description, too deep") 616 | 617 | # Dealing with the scalar case, the data is the last value. 618 | if self.__columns[col_index]["container"] == "scalar": 619 | prev_col_values[0][self.__columns[col_index]["id"]] = data 620 | self.__data.append(prev_col_values) 621 | return 622 | 623 | if self.__columns[col_index]["container"] == "iter": 624 | if not hasattr(data, "__iter__") or isinstance(data, dict): 625 | raise DataTableException("Expected iterable object, got %s" % 626 | type(data)) 627 | # We only need to insert the rest of the columns 628 | # If there are less items than expected, we only add what there is. 629 | for value in data: 630 | if col_index >= len(self.__columns): 631 | raise DataTableException("Too many elements given in data") 632 | prev_col_values[0][self.__columns[col_index]["id"]] = value 633 | col_index += 1 634 | self.__data.append(prev_col_values) 635 | return 636 | 637 | # We know the current level is a dictionary, we verify the type. 638 | if not isinstance(data, dict): 639 | raise DataTableException("Expected dictionary at current level, got %s" % 640 | type(data)) 641 | # We check if this is the last level 642 | if self.__columns[col_index]["depth"] == self.__columns[-1]["depth"]: 643 | # We need to add the keys in the dictionary as they are 644 | for col in self.__columns[col_index:]: 645 | if col["id"] in data: 646 | prev_col_values[0][col["id"]] = data[col["id"]] 647 | self.__data.append(prev_col_values) 648 | return 649 | 650 | # We have a dictionary in an inner depth level. 651 | if not data.keys(): 652 | # In case this is an empty dictionary, we add a record with the columns 653 | # filled only until this point. 654 | self.__data.append(prev_col_values) 655 | else: 656 | for key in sorted(data): 657 | col_values = dict(prev_col_values[0]) 658 | col_values[self.__columns[col_index]["id"]] = key 659 | self._InnerAppendData((col_values, prev_col_values[1]), 660 | data[key], col_index + 1) 661 | 662 | def _PreparedData(self, order_by=()): 663 | """Prepares the data for enumeration - sorting it by order_by. 664 | 665 | Args: 666 | order_by: Optional. Specifies the name of the column(s) to sort by, and 667 | (optionally) which direction to sort in. Default sort direction 668 | is asc. Following formats are accepted: 669 | "string_col_name" -- For a single key in default (asc) order. 670 | ("string_col_name", "asc|desc") -- For a single key. 671 | [("col_1","asc|desc"), ("col_2","asc|desc")] -- For more than 672 | one column, an array of tuples of (col_name, "asc|desc"). 673 | 674 | Returns: 675 | The data sorted by the keys given. 676 | 677 | Raises: 678 | DataTableException: Sort direction not in 'asc' or 'desc' 679 | """ 680 | if not order_by: 681 | return self.__data 682 | 683 | proper_sort_keys = [] 684 | if isinstance(order_by, types.StringTypes) or ( 685 | isinstance(order_by, tuple) and len(order_by) == 2 and 686 | order_by[1].lower() in ["asc", "desc"]): 687 | order_by = (order_by,) 688 | for key in order_by: 689 | if isinstance(key, types.StringTypes): 690 | proper_sort_keys.append((key, 1)) 691 | elif (isinstance(key, (list, tuple)) and len(key) == 2 and 692 | key[1].lower() in ("asc", "desc")): 693 | proper_sort_keys.append((key[0], key[1].lower() == "asc" and 1 or -1)) 694 | else: 695 | raise DataTableException("Expected tuple with second value: " 696 | "'asc' or 'desc'") 697 | 698 | def SortCmpFunc(row1, row2): 699 | """cmp function for sorted. Compares by keys and 'asc'/'desc' keywords.""" 700 | for key, asc_mult in proper_sort_keys: 701 | cmp_result = asc_mult * cmp(row1[0].get(key), row2[0].get(key)) 702 | if cmp_result: 703 | return cmp_result 704 | return 0 705 | 706 | return sorted(self.__data, cmp=SortCmpFunc) 707 | 708 | def ToJSCode(self, name, columns_order=None, order_by=()): 709 | """Writes the data table as a JS code string. 710 | 711 | This method writes a string of JS code that can be run to 712 | generate a DataTable with the specified data. Typically used for debugging 713 | only. 714 | 715 | Args: 716 | name: The name of the table. The name would be used as the DataTable's 717 | variable name in the created JS code. 718 | columns_order: Optional. Specifies the order of columns in the 719 | output table. Specify a list of all column IDs in the order 720 | in which you want the table created. 721 | Note that you must list all column IDs in this parameter, 722 | if you use it. 723 | order_by: Optional. Specifies the name of the column(s) to sort by. 724 | Passed as is to _PreparedData. 725 | 726 | Returns: 727 | A string of JS code that, when run, generates a DataTable with the given 728 | name and the data stored in the DataTable object. 729 | Example result: 730 | "var tab1 = new google.visualization.DataTable(); 731 | tab1.addColumn("string", "a", "a"); 732 | tab1.addColumn("number", "b", "b"); 733 | tab1.addColumn("boolean", "c", "c"); 734 | tab1.addRows(10); 735 | tab1.setCell(0, 0, "a"); 736 | tab1.setCell(0, 1, 1, null, {"foo": "bar"}); 737 | tab1.setCell(0, 2, true); 738 | ... 739 | tab1.setCell(9, 0, "c"); 740 | tab1.setCell(9, 1, 3, "3$"); 741 | tab1.setCell(9, 2, false);" 742 | 743 | Raises: 744 | DataTableException: The data does not match the type. 745 | """ 746 | 747 | encoder = DataTableJSONEncoder() 748 | 749 | if columns_order is None: 750 | columns_order = [col["id"] for col in self.__columns] 751 | col_dict = dict([(col["id"], col) for col in self.__columns]) 752 | 753 | # We first create the table with the given name 754 | jscode = "var %s = new google.visualization.DataTable();\n" % name 755 | if self.custom_properties: 756 | jscode += "%s.setTableProperties(%s);\n" % ( 757 | name, encoder.encode(self.custom_properties)) 758 | 759 | # We add the columns to the table 760 | for i, col in enumerate(columns_order): 761 | jscode += "%s.addColumn(%s, %s, %s);\n" % ( 762 | name, 763 | encoder.encode(col_dict[col]["type"]), 764 | encoder.encode(col_dict[col]["label"]), 765 | encoder.encode(col_dict[col]["id"])) 766 | if col_dict[col]["custom_properties"]: 767 | jscode += "%s.setColumnProperties(%d, %s);\n" % ( 768 | name, i, encoder.encode(col_dict[col]["custom_properties"])) 769 | jscode += "%s.addRows(%d);\n" % (name, len(self.__data)) 770 | 771 | # We now go over the data and add each row 772 | for (i, (row, cp)) in enumerate(self._PreparedData(order_by)): 773 | # We add all the elements of this row by their order 774 | for (j, col) in enumerate(columns_order): 775 | if col not in row or row[col] is None: 776 | continue 777 | value = self.CoerceValue(row[col], col_dict[col]["type"]) 778 | if isinstance(value, tuple): 779 | cell_cp = "" 780 | if len(value) == 3: 781 | cell_cp = ", %s" % encoder.encode(row[col][2]) 782 | # We have a formatted value or custom property as well 783 | jscode += ("%s.setCell(%d, %d, %s, %s%s);\n" % 784 | (name, i, j, 785 | self.EscapeForJSCode(encoder, value[0]), 786 | self.EscapeForJSCode(encoder, value[1]), cell_cp)) 787 | else: 788 | jscode += "%s.setCell(%d, %d, %s);\n" % ( 789 | name, i, j, self.EscapeForJSCode(encoder, value)) 790 | if cp: 791 | jscode += "%s.setRowProperties(%d, %s);\n" % ( 792 | name, i, encoder.encode(cp)) 793 | return jscode 794 | 795 | def ToHtml(self, columns_order=None, order_by=()): 796 | """Writes the data table as an HTML table code string. 797 | 798 | Args: 799 | columns_order: Optional. Specifies the order of columns in the 800 | output table. Specify a list of all column IDs in the order 801 | in which you want the table created. 802 | Note that you must list all column IDs in this parameter, 803 | if you use it. 804 | order_by: Optional. Specifies the name of the column(s) to sort by. 805 | Passed as is to _PreparedData. 806 | 807 | Returns: 808 | An HTML table code string. 809 | Example result (the result is without the newlines): 810 | 811 | 812 | 813 | 814 | 815 | 816 |
abc
1"z"2
"3$""w"
817 | 818 | Raises: 819 | DataTableException: The data does not match the type. 820 | """ 821 | table_template = "%s
" 822 | columns_template = "%s" 823 | rows_template = "%s" 824 | row_template = "%s" 825 | header_cell_template = "%s" 826 | cell_template = "%s" 827 | 828 | if columns_order is None: 829 | columns_order = [col["id"] for col in self.__columns] 830 | col_dict = dict([(col["id"], col) for col in self.__columns]) 831 | 832 | columns_list = [] 833 | for col in columns_order: 834 | columns_list.append(header_cell_template % 835 | cgi.escape(col_dict[col]["label"])) 836 | columns_html = columns_template % "".join(columns_list) 837 | 838 | rows_list = [] 839 | # We now go over the data and add each row 840 | for row, unused_cp in self._PreparedData(order_by): 841 | cells_list = [] 842 | # We add all the elements of this row by their order 843 | for col in columns_order: 844 | # For empty string we want empty quotes (""). 845 | value = "" 846 | if col in row and row[col] is not None: 847 | value = self.CoerceValue(row[col], col_dict[col]["type"]) 848 | if isinstance(value, tuple): 849 | # We have a formatted value and we're going to use it 850 | cells_list.append(cell_template % cgi.escape(self.ToString(value[1]))) 851 | else: 852 | cells_list.append(cell_template % cgi.escape(self.ToString(value))) 853 | rows_list.append(row_template % "".join(cells_list)) 854 | rows_html = rows_template % "".join(rows_list) 855 | 856 | return table_template % (columns_html + rows_html) 857 | 858 | def ToCsv(self, columns_order=None, order_by=(), separator=","): 859 | """Writes the data table as a CSV string. 860 | 861 | Output is encoded in UTF-8 because the Python "csv" module can't handle 862 | Unicode properly according to its documentation. 863 | 864 | Args: 865 | columns_order: Optional. Specifies the order of columns in the 866 | output table. Specify a list of all column IDs in the order 867 | in which you want the table created. 868 | Note that you must list all column IDs in this parameter, 869 | if you use it. 870 | order_by: Optional. Specifies the name of the column(s) to sort by. 871 | Passed as is to _PreparedData. 872 | separator: Optional. The separator to use between the values. 873 | 874 | Returns: 875 | A CSV string representing the table. 876 | Example result: 877 | 'a','b','c' 878 | 1,'z',2 879 | 3,'w','' 880 | 881 | Raises: 882 | DataTableException: The data does not match the type. 883 | """ 884 | 885 | csv_buffer = cStringIO.StringIO() 886 | writer = csv.writer(csv_buffer, delimiter=separator) 887 | 888 | if columns_order is None: 889 | columns_order = [col["id"] for col in self.__columns] 890 | col_dict = dict([(col["id"], col) for col in self.__columns]) 891 | 892 | writer.writerow([col_dict[col]["label"].encode("utf-8") 893 | for col in columns_order]) 894 | 895 | # We now go over the data and add each row 896 | for row, unused_cp in self._PreparedData(order_by): 897 | cells_list = [] 898 | # We add all the elements of this row by their order 899 | for col in columns_order: 900 | value = "" 901 | if col in row and row[col] is not None: 902 | value = self.CoerceValue(row[col], col_dict[col]["type"]) 903 | if isinstance(value, tuple): 904 | # We have a formatted value. Using it only for date/time types. 905 | if col_dict[col]["type"] in ["date", "datetime", "timeofday"]: 906 | cells_list.append(self.ToString(value[1]).encode("utf-8")) 907 | else: 908 | cells_list.append(self.ToString(value[0]).encode("utf-8")) 909 | else: 910 | cells_list.append(self.ToString(value).encode("utf-8")) 911 | writer.writerow(cells_list) 912 | return csv_buffer.getvalue() 913 | 914 | def ToTsvExcel(self, columns_order=None, order_by=()): 915 | """Returns a file in tab-separated-format readable by MS Excel. 916 | 917 | Returns a file in UTF-16 little endian encoding, with tabs separating the 918 | values. 919 | 920 | Args: 921 | columns_order: Delegated to ToCsv. 922 | order_by: Delegated to ToCsv. 923 | 924 | Returns: 925 | A tab-separated little endian UTF16 file representing the table. 926 | """ 927 | return (self.ToCsv(columns_order, order_by, separator="\t") 928 | .decode("utf-8").encode("UTF-16LE")) 929 | 930 | def _ToJSonObj(self, columns_order=None, order_by=()): 931 | """Returns an object suitable to be converted to JSON. 932 | 933 | Args: 934 | columns_order: Optional. A list of all column IDs in the order in which 935 | you want them created in the output table. If specified, 936 | all column IDs must be present. 937 | order_by: Optional. Specifies the name of the column(s) to sort by. 938 | Passed as is to _PreparedData(). 939 | 940 | Returns: 941 | A dictionary object for use by ToJSon or ToJSonResponse. 942 | """ 943 | if columns_order is None: 944 | columns_order = [col["id"] for col in self.__columns] 945 | col_dict = dict([(col["id"], col) for col in self.__columns]) 946 | 947 | # Creating the column JSON objects 948 | col_objs = [] 949 | for col_id in columns_order: 950 | col_obj = {"id": col_dict[col_id]["id"], 951 | "label": col_dict[col_id]["label"], 952 | "type": col_dict[col_id]["type"]} 953 | if col_dict[col_id]["custom_properties"]: 954 | col_obj["p"] = col_dict[col_id]["custom_properties"] 955 | col_objs.append(col_obj) 956 | 957 | # Creating the rows jsons 958 | row_objs = [] 959 | for row, cp in self._PreparedData(order_by): 960 | cell_objs = [] 961 | for col in columns_order: 962 | value = self.CoerceValue(row.get(col, None), col_dict[col]["type"]) 963 | if value is None: 964 | cell_obj = None 965 | elif isinstance(value, tuple): 966 | cell_obj = {"v": value[0]} 967 | if len(value) > 1 and value[1] is not None: 968 | cell_obj["f"] = value[1] 969 | if len(value) == 3: 970 | cell_obj["p"] = value[2] 971 | else: 972 | cell_obj = {"v": value} 973 | cell_objs.append(cell_obj) 974 | row_obj = {"c": cell_objs} 975 | if cp: 976 | row_obj["p"] = cp 977 | row_objs.append(row_obj) 978 | 979 | json_obj = {"cols": col_objs, "rows": row_objs} 980 | if self.custom_properties: 981 | json_obj["p"] = self.custom_properties 982 | 983 | return json_obj 984 | 985 | def ToJSon(self, columns_order=None, order_by=()): 986 | """Returns a string that can be used in a JS DataTable constructor. 987 | 988 | This method writes a JSON string that can be passed directly into a Google 989 | Visualization API DataTable constructor. Use this output if you are 990 | hosting the visualization HTML on your site, and want to code the data 991 | table in Python. Pass this string into the 992 | google.visualization.DataTable constructor, e.g,: 993 | ... on my page that hosts my visualization ... 994 | google.setOnLoadCallback(drawTable); 995 | function drawTable() { 996 | var data = new google.visualization.DataTable(_my_JSon_string, 0.6); 997 | myTable.draw(data); 998 | } 999 | 1000 | Args: 1001 | columns_order: Optional. Specifies the order of columns in the 1002 | output table. Specify a list of all column IDs in the order 1003 | in which you want the table created. 1004 | Note that you must list all column IDs in this parameter, 1005 | if you use it. 1006 | order_by: Optional. Specifies the name of the column(s) to sort by. 1007 | Passed as is to _PreparedData(). 1008 | 1009 | Returns: 1010 | A JSon constructor string to generate a JS DataTable with the data 1011 | stored in the DataTable object. 1012 | Example result (the result is without the newlines): 1013 | {cols: [{id:"a",label:"a",type:"number"}, 1014 | {id:"b",label:"b",type:"string"}, 1015 | {id:"c",label:"c",type:"number"}], 1016 | rows: [{c:[{v:1},{v:"z"},{v:2}]}, c:{[{v:3,f:"3$"},{v:"w"},{v:null}]}], 1017 | p: {'foo': 'bar'}} 1018 | 1019 | Raises: 1020 | DataTableException: The data does not match the type. 1021 | """ 1022 | 1023 | encoder = DataTableJSONEncoder() 1024 | return encoder.encode( 1025 | self._ToJSonObj(columns_order, order_by)).encode("utf-8") 1026 | 1027 | def ToJSonResponse(self, columns_order=None, order_by=(), req_id=0, 1028 | response_handler="google.visualization.Query.setResponse"): 1029 | """Writes a table as a JSON response that can be returned as-is to a client. 1030 | 1031 | This method writes a JSON response to return to a client in response to a 1032 | Google Visualization API query. This string can be processed by the calling 1033 | page, and is used to deliver a data table to a visualization hosted on 1034 | a different page. 1035 | 1036 | Args: 1037 | columns_order: Optional. Passed straight to self.ToJSon(). 1038 | order_by: Optional. Passed straight to self.ToJSon(). 1039 | req_id: Optional. The response id, as retrieved by the request. 1040 | response_handler: Optional. The response handler, as retrieved by the 1041 | request. 1042 | 1043 | Returns: 1044 | A JSON response string to be received by JS the visualization Query 1045 | object. This response would be translated into a DataTable on the 1046 | client side. 1047 | Example result (newlines added for readability): 1048 | google.visualization.Query.setResponse({ 1049 | 'version':'0.6', 'reqId':'0', 'status':'OK', 1050 | 'table': {cols: [...], rows: [...]}}); 1051 | 1052 | Note: The URL returning this string can be used as a data source by Google 1053 | Visualization Gadgets or from JS code. 1054 | """ 1055 | 1056 | response_obj = { 1057 | "version": "0.6", 1058 | "reqId": str(req_id), 1059 | "table": self._ToJSonObj(columns_order, order_by), 1060 | "status": "ok" 1061 | } 1062 | encoder = DataTableJSONEncoder() 1063 | return "%s(%s);" % (response_handler, 1064 | encoder.encode(response_obj).encode("utf-8")) 1065 | 1066 | def ToResponse(self, columns_order=None, order_by=(), tqx=""): 1067 | """Writes the right response according to the request string passed in tqx. 1068 | 1069 | This method parses the tqx request string (format of which is defined in 1070 | the documentation for implementing a data source of Google Visualization), 1071 | and returns the right response according to the request. 1072 | It parses out the "out" parameter of tqx, calls the relevant response 1073 | (ToJSonResponse() for "json", ToCsv() for "csv", ToHtml() for "html", 1074 | ToTsvExcel() for "tsv-excel") and passes the response function the rest of 1075 | the relevant request keys. 1076 | 1077 | Args: 1078 | columns_order: Optional. Passed as is to the relevant response function. 1079 | order_by: Optional. Passed as is to the relevant response function. 1080 | tqx: Optional. The request string as received by HTTP GET. Should be in 1081 | the format "key1:value1;key2:value2...". All keys have a default 1082 | value, so an empty string will just do the default (which is calling 1083 | ToJSonResponse() with no extra parameters). 1084 | 1085 | Returns: 1086 | A response string, as returned by the relevant response function. 1087 | 1088 | Raises: 1089 | DataTableException: One of the parameters passed in tqx is not supported. 1090 | """ 1091 | tqx_dict = {} 1092 | if tqx: 1093 | tqx_dict = dict(opt.split(":") for opt in tqx.split(";")) 1094 | if tqx_dict.get("version", "0.6") != "0.6": 1095 | raise DataTableException( 1096 | "Version (%s) passed by request is not supported." 1097 | % tqx_dict["version"]) 1098 | 1099 | if tqx_dict.get("out", "json") == "json": 1100 | response_handler = tqx_dict.get("responseHandler", 1101 | "google.visualization.Query.setResponse") 1102 | return self.ToJSonResponse(columns_order, order_by, 1103 | req_id=tqx_dict.get("reqId", 0), 1104 | response_handler=response_handler) 1105 | elif tqx_dict["out"] == "html": 1106 | return self.ToHtml(columns_order, order_by) 1107 | elif tqx_dict["out"] == "csv": 1108 | return self.ToCsv(columns_order, order_by) 1109 | elif tqx_dict["out"] == "tsv-excel": 1110 | return self.ToTsvExcel(columns_order, order_by) 1111 | else: 1112 | raise DataTableException( 1113 | "'out' parameter: '%s' is not supported" % tqx_dict["out"]) 1114 | -------------------------------------------------------------------------------- /report_tools/renderers/googlecharts/gviz_api/gviz_api_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Copyright (C) 2009 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """Tests for the gviz_api module.""" 18 | 19 | __author__ = "Amit Weinstein" 20 | 21 | from datetime import date 22 | from datetime import datetime 23 | from datetime import time 24 | try: 25 | import json 26 | except ImportError: 27 | import simplejson as json 28 | import unittest 29 | 30 | from gviz_api import DataTable 31 | from gviz_api import DataTableException 32 | 33 | 34 | class DataTableTest(unittest.TestCase): 35 | 36 | def testCoerceValue(self): 37 | # We first check that given an unknown type it raises exception 38 | self.assertRaises(DataTableException, 39 | DataTable.CoerceValue, 1, "no_such_type") 40 | 41 | # If we give a type which does not match the value, we expect it to fail 42 | self.assertRaises(DataTableException, 43 | DataTable.CoerceValue, "a", "number") 44 | self.assertRaises(DataTableException, 45 | DataTable.CoerceValue, "b", "timeofday") 46 | self.assertRaises(DataTableException, 47 | DataTable.CoerceValue, 10, "date") 48 | 49 | # A tuple for value and formatted value should be of length 2 50 | self.assertRaises(DataTableException, 51 | DataTable.CoerceValue, (5, "5$", "6$"), "string") 52 | 53 | # Some good examples from all the different types 54 | self.assertEqual(True, DataTable.CoerceValue(True, "boolean")) 55 | self.assertEqual(False, DataTable.CoerceValue(False, "boolean")) 56 | self.assertEqual(True, DataTable.CoerceValue(1, "boolean")) 57 | self.assertEqual(None, DataTable.CoerceValue(None, "boolean")) 58 | self.assertEqual((False, u"a"), 59 | DataTable.CoerceValue((False, "a"), "boolean")) 60 | 61 | self.assertEqual(1, DataTable.CoerceValue(1, "number")) 62 | self.assertEqual(1., DataTable.CoerceValue(1., "number")) 63 | self.assertEqual(-5, DataTable.CoerceValue(-5, "number")) 64 | self.assertEqual(None, DataTable.CoerceValue(None, "number")) 65 | self.assertEqual((5, u"5$"), 66 | DataTable.CoerceValue((5, "5$"), "number")) 67 | 68 | self.assertEqual("-5", DataTable.CoerceValue(-5, "string")) 69 | self.assertEqual("abc", DataTable.CoerceValue("abc", "string")) 70 | self.assertEqual(None, DataTable.CoerceValue(None, "string")) 71 | 72 | self.assertEqual(date(2010, 1, 2), 73 | DataTable.CoerceValue(date(2010, 1, 2), "date")) 74 | self.assertEqual(date(2001, 2, 3), 75 | DataTable.CoerceValue(datetime(2001, 2, 3, 4, 5, 6), 76 | "date")) 77 | self.assertEqual(None, DataTable.CoerceValue(None, "date")) 78 | 79 | self.assertEqual(time(10, 11, 12), 80 | DataTable.CoerceValue(time(10, 11, 12), "timeofday")) 81 | self.assertEqual(time(3, 4, 5), 82 | DataTable.CoerceValue(datetime(2010, 1, 2, 3, 4, 5), 83 | "timeofday")) 84 | self.assertEqual(None, DataTable.CoerceValue(None, "timeofday")) 85 | 86 | self.assertEqual(datetime(2001, 2, 3, 4, 5, 6, 555000), 87 | DataTable.CoerceValue(datetime(2001, 2, 3, 4, 5, 6, 88 | 555000), 89 | "datetime")) 90 | self.assertEqual(None, DataTable.CoerceValue(None, "datetime")) 91 | self.assertEqual((None, "none"), 92 | DataTable.CoerceValue((None, "none"), "string")) 93 | 94 | def testDifferentStrings(self): 95 | # Checking escaping of strings in JSON output 96 | the_strings = ["new\nline", 97 | r"one\slash", 98 | r"two\\slash", 99 | u"unicode eng", 100 | u"unicode \u05e2\u05d1\u05e8\u05d9\u05ea", 101 | u"unicode \u05e2\u05d1\u05e8\u05d9\u05ea".encode("utf-8"), 102 | u'"\u05e2\u05d1\\"\u05e8\u05d9\u05ea"'] 103 | table = DataTable([("a", "string")], 104 | [[x] for x in the_strings]) 105 | 106 | json_obj = json.loads(table.ToJSon()) 107 | for i, row in enumerate(json_obj["rows"]): 108 | utf8_str = the_strings[i] 109 | if isinstance(utf8_str, unicode): 110 | utf8_str = utf8_str.encode("utf-8") 111 | 112 | out_str = row["c"][0]["v"] 113 | self.assertEqual(out_str.encode("utf-8"), utf8_str) 114 | 115 | def testColumnTypeParser(self): 116 | # Checking several wrong formats 117 | self.assertRaises(DataTableException, 118 | DataTable.ColumnTypeParser, 5) 119 | self.assertRaises(DataTableException, 120 | DataTable.ColumnTypeParser, ("a", 5, "c")) 121 | self.assertRaises(DataTableException, 122 | DataTable.ColumnTypeParser, ("a", "blah")) 123 | self.assertRaises(DataTableException, 124 | DataTable.ColumnTypeParser, ("a", "number", "c", "d")) 125 | 126 | # Checking several legal formats 127 | self.assertEqual({"id": "abc", "label": "abc", "type": "string", 128 | "custom_properties": {}}, 129 | DataTable.ColumnTypeParser("abc")) 130 | self.assertEqual({"id": "abc", "label": "abc", "type": "string", 131 | "custom_properties": {}}, 132 | DataTable.ColumnTypeParser(("abc",))) 133 | self.assertEqual({"id": "abc", "label": "bcd", "type": "string", 134 | "custom_properties": {}}, 135 | DataTable.ColumnTypeParser(("abc", "string", "bcd"))) 136 | self.assertEqual({"id": "a", "label": "b", "type": "number", 137 | "custom_properties": {}}, 138 | DataTable.ColumnTypeParser(("a", "number", "b"))) 139 | self.assertEqual({"id": "a", "label": "a", "type": "number", 140 | "custom_properties": {}}, 141 | DataTable.ColumnTypeParser(("a", "number"))) 142 | self.assertEqual({"id": "i", "label": "l", "type": "string", 143 | "custom_properties": {"key": "value"}}, 144 | DataTable.ColumnTypeParser(("i", "string", "l", 145 | {"key": "value"}))) 146 | 147 | def testTableDescriptionParser(self): 148 | # We expect it to fail with empty lists or dictionaries 149 | self.assertRaises(DataTableException, 150 | DataTable.TableDescriptionParser, {}) 151 | self.assertRaises(DataTableException, 152 | DataTable.TableDescriptionParser, []) 153 | self.assertRaises(DataTableException, 154 | DataTable.TableDescriptionParser, {"a": []}) 155 | self.assertRaises(DataTableException, 156 | DataTable.TableDescriptionParser, {"a": {"b": {}}}) 157 | 158 | # We expect it to fail if we give a non-string at the lowest level 159 | self.assertRaises(DataTableException, 160 | DataTable.TableDescriptionParser, {"a": 5}) 161 | self.assertRaises(DataTableException, 162 | DataTable.TableDescriptionParser, [("a", "number"), 6]) 163 | 164 | # Some valid examples which mixes both dictionaries and lists 165 | self.assertEqual( 166 | [{"id": "a", "label": "a", "type": "date", 167 | "depth": 0, "container": "iter", "custom_properties": {}}, 168 | {"id": "b", "label": "b", "type": "timeofday", 169 | "depth": 0, "container": "iter", "custom_properties": {}}], 170 | DataTable.TableDescriptionParser([("a", "date"), ("b", "timeofday")])) 171 | 172 | self.assertEqual( 173 | [{"id": "a", "label": "a", "type": "string", 174 | "depth": 0, "container": "dict", "custom_properties": {}}, 175 | {"id": "b", "label": "b", "type": "number", 176 | "depth": 1, "container": "iter", "custom_properties": {}}, 177 | {"id": "c", "label": "column c", "type": "string", 178 | "depth": 1, "container": "iter", "custom_properties": {}}], 179 | DataTable.TableDescriptionParser({"a": [("b", "number"), 180 | ("c", "string", "column c")]})) 181 | 182 | self.assertEqual( 183 | [{"id": "a", "label": "column a", "type": "number", "depth": 0, 184 | "container": "dict", "custom_properties": {}}, 185 | {"id": "b", "label": "column b", "type": "string", "depth": 0, 186 | "container": "dict", "custom_properties": {}}], 187 | DataTable.TableDescriptionParser({"a": ("number", "column a"), 188 | "b": ("string", "column b")})) 189 | 190 | self.assertEqual( 191 | [{"id": "a", "label": "column a", "type": "number", 192 | "depth": 0, "container": "dict", "custom_properties": {}}, 193 | {"id": "b", "label": "b", "type": "number", 194 | "depth": 1, "container": "dict", "custom_properties": {}}, 195 | {"id": "c", "label": "c", "type": "string", 196 | "depth": 1, "container": "dict", "custom_properties": {}}], 197 | DataTable.TableDescriptionParser({("a", "number", "column a"): 198 | {"b": "number", "c": "string"}})) 199 | 200 | self.assertEqual( 201 | [{"id": "a", "label": "column a", "type": "number", 202 | "depth": 0, "container": "dict", "custom_properties": {}}, 203 | {"id": "b", "label": "column b", "type": "string", 204 | "depth": 1, "container": "scalar", "custom_properties": {}}], 205 | DataTable.TableDescriptionParser({("a", "number", "column a"): 206 | ("b", "string", "column b")})) 207 | 208 | # Cases that might create ambiguity 209 | self.assertEqual( 210 | [{"id": "a", "label": "column a", "type": "number", "depth": 0, 211 | "container": "dict", "custom_properties": {}}], 212 | DataTable.TableDescriptionParser({"a": ("number", "column a")})) 213 | self.assertRaises(DataTableException, DataTable.TableDescriptionParser, 214 | {"a": ("b", "number")}) 215 | 216 | self.assertEqual( 217 | [{"id": "a", "label": "a", "type": "string", "depth": 0, 218 | "container": "dict", "custom_properties": {}}, 219 | {"id": "b", "label": "b", "type": "number", "depth": 1, 220 | "container": "scalar", "custom_properties": {}}], 221 | DataTable.TableDescriptionParser({"a": ("b", "number", "b", {})})) 222 | 223 | self.assertEqual( 224 | [{"id": "a", "label": "a", "type": "string", "depth": 0, 225 | "container": "dict", "custom_properties": {}}, 226 | {"id": "b", "label": "b", "type": "number", "depth": 1, 227 | "container": "scalar", "custom_properties": {}}], 228 | DataTable.TableDescriptionParser({("a",): ("b", "number")})) 229 | 230 | def testAppendData(self): 231 | # We check a few examples where the format of the data does not match the 232 | # description and hen a few valid examples. The test for the content itself 233 | # is done inside the ToJSCode and ToJSon functions. 234 | table = DataTable([("a", "number"), ("b", "string")]) 235 | self.assertEqual(0, table.NumberOfRows()) 236 | self.assertRaises(DataTableException, 237 | table.AppendData, [[1, "a", True]]) 238 | self.assertRaises(DataTableException, 239 | table.AppendData, {1: ["a"], 2: ["b"]}) 240 | self.assertEquals(None, table.AppendData([[1, "a"], [2, "b"]])) 241 | self.assertEqual(2, table.NumberOfRows()) 242 | self.assertEquals(None, table.AppendData([[3, "c"], [4]])) 243 | self.assertEqual(4, table.NumberOfRows()) 244 | 245 | table = DataTable({"a": "number", "b": "string"}) 246 | self.assertEqual(0, table.NumberOfRows()) 247 | self.assertRaises(DataTableException, 248 | table.AppendData, [[1, "a"]]) 249 | self.assertRaises(DataTableException, 250 | table.AppendData, {5: {"b": "z"}}) 251 | self.assertEquals(None, table.AppendData([{"a": 1, "b": "z"}])) 252 | self.assertEqual(1, table.NumberOfRows()) 253 | 254 | table = DataTable({("a", "number"): [("b", "string")]}) 255 | self.assertEqual(0, table.NumberOfRows()) 256 | self.assertRaises(DataTableException, 257 | table.AppendData, [[1, "a"]]) 258 | self.assertRaises(DataTableException, 259 | table.AppendData, {5: {"b": "z"}}) 260 | self.assertEquals(None, table.AppendData({5: ["z"], 6: ["w"]})) 261 | self.assertEqual(2, table.NumberOfRows()) 262 | 263 | table = DataTable({("a", "number"): {"b": "string", "c": "number"}}) 264 | self.assertEqual(0, table.NumberOfRows()) 265 | self.assertRaises(DataTableException, 266 | table.AppendData, [[1, "a"]]) 267 | self.assertRaises(DataTableException, 268 | table.AppendData, {1: ["a", 2]}) 269 | self.assertEquals(None, table.AppendData({5: {"b": "z", "c": 6}, 270 | 7: {"c": 8}, 271 | 9: {}})) 272 | self.assertEqual(3, table.NumberOfRows()) 273 | 274 | def testToJSCode(self): 275 | table = DataTable([("a", "number", "A'"), "b\"", ("c", "timeofday")], 276 | [[1], 277 | [None, "z", time(1, 2, 3)], 278 | [(2, "2$"), "w", time(2, 3, 4)]]) 279 | self.assertEqual(3, table.NumberOfRows()) 280 | self.assertEqual((u"var mytab = new google.visualization.DataTable();\n" 281 | u"mytab.addColumn(\"number\", \"A'\", \"a\");\n" 282 | u"mytab.addColumn(\"string\", \"b\\\"\", \"b\\\"\");\n" 283 | u"mytab.addColumn(\"timeofday\", \"c\", \"c\");\n" 284 | u"mytab.addRows(3);\n" 285 | u"mytab.setCell(0, 0, 1);\n" 286 | u"mytab.setCell(1, 1, \"z\");\n" 287 | u"mytab.setCell(1, 2, [1,2,3]);\n" 288 | u"mytab.setCell(2, 0, 2, \"2$\");\n" 289 | u"mytab.setCell(2, 1, \"w\");\n" 290 | u"mytab.setCell(2, 2, [2,3,4]);\n"), 291 | table.ToJSCode("mytab")) 292 | 293 | table = DataTable({("a", "number"): {"b": "date", "c": "datetime"}}, 294 | {1: {}, 295 | 2: {"b": date(1, 2, 3)}, 296 | 3: {"c": datetime(1, 2, 3, 4, 5, 6, 555000)}, 297 | 4: {"c": datetime(1, 2, 3, 4, 5, 6)}}) 298 | self.assertEqual(4, table.NumberOfRows()) 299 | self.assertEqual(("var mytab2 = new google.visualization.DataTable();\n" 300 | 'mytab2.addColumn("datetime", "c", "c");\n' 301 | 'mytab2.addColumn("date", "b", "b");\n' 302 | 'mytab2.addColumn("number", "a", "a");\n' 303 | 'mytab2.addRows(4);\n' 304 | 'mytab2.setCell(0, 2, 1);\n' 305 | 'mytab2.setCell(1, 1, new Date(1,1,3));\n' 306 | 'mytab2.setCell(1, 2, 2);\n' 307 | 'mytab2.setCell(2, 0, new Date(1,1,3,4,5,6,555));\n' 308 | 'mytab2.setCell(2, 2, 3);\n' 309 | 'mytab2.setCell(3, 0, new Date(1,1,3,4,5,6));\n' 310 | 'mytab2.setCell(3, 2, 4);\n'), 311 | table.ToJSCode("mytab2", columns_order=["c", "b", "a"])) 312 | 313 | def testToJSon(self): 314 | json_obj = {"cols": 315 | [{"id": "a", "label": "A", "type": "number"}, 316 | {"id": "b", "label": "b", "type": "string"}, 317 | {"id": "c", "label": "c", "type": "boolean"}], 318 | "rows": 319 | [{"c": [{"v": 1}, None, None]}, 320 | {"c": [None, {"v": "z"}, {"v": True}]}, 321 | {"c": [None, {"v": u"\u05d0"}, None]}, 322 | {"c": [None, {"v": u"\u05d1"}, None]}]} 323 | 324 | table = DataTable([("a", "number", "A"), "b", ("c", "boolean")], 325 | [[1], 326 | [None, "z", True], 327 | [None, u"\u05d0"], 328 | [None, u"\u05d1".encode("utf-8")]]) 329 | self.assertEqual(4, table.NumberOfRows()) 330 | self.assertEqual(json.dumps(json_obj, 331 | separators=(",", ":"), 332 | ensure_ascii=False).encode("utf-8"), 333 | table.ToJSon()) 334 | table.AppendData([[-1, "w", False]]) 335 | self.assertEqual(5, table.NumberOfRows()) 336 | json_obj["rows"].append({"c": [{"v": -1}, {"v": "w"}, {"v": False}]}) 337 | self.assertEqual(json.dumps(json_obj, 338 | separators=(",", ":"), 339 | ensure_ascii=False).encode("utf-8"), 340 | table.ToJSon()) 341 | 342 | json_obj = {"cols": 343 | [{"id": "t", "label": "T", "type": "timeofday"}, 344 | {"id": "d", "label": "d", "type": "date"}, 345 | {"id": "dt", "label": "dt", "type": "datetime"}], 346 | "rows": 347 | [{"c": [{"v": [1, 2, 3]}, {"v": "Date(1,1,3)"}, None]}]} 348 | table = DataTable({("d", "date"): [("t", "timeofday", "T"), 349 | ("dt", "datetime")]}) 350 | table.LoadData({date(1, 2, 3): [time(1, 2, 3)]}) 351 | self.assertEqual(1, table.NumberOfRows()) 352 | self.assertEqual(json.dumps(json_obj, separators=(",", ":")), 353 | table.ToJSon(columns_order=["t", "d", "dt"])) 354 | 355 | json_obj["rows"] = [ 356 | {"c": [{"v": [2, 3, 4], "f": "time 2 3 4"}, 357 | {"v": "Date(2,2,4)"}, 358 | {"v": "Date(1,1,3,4,5,6,555)"}]}, 359 | {"c": [None, {"v": "Date(3,3,5)"}, None]}] 360 | 361 | table.LoadData({date(2, 3, 4): [(time(2, 3, 4), "time 2 3 4"), 362 | datetime(1, 2, 3, 4, 5, 6, 555000)], 363 | date(3, 4, 5): []}) 364 | self.assertEqual(2, table.NumberOfRows()) 365 | 366 | self.assertEqual(json.dumps(json_obj, separators=(",", ":")), 367 | table.ToJSon(columns_order=["t", "d", "dt"])) 368 | 369 | json_obj = { 370 | "cols": [{"id": "a\"", "label": "a\"", "type": "string"}, 371 | {"id": "b", "label": "bb\"", "type": "number"}], 372 | "rows": [{"c": [{"v": "a1"}, {"v": 1}]}, 373 | {"c": [{"v": "a2"}, {"v": 2}]}, 374 | {"c": [{"v": "a3"}, {"v": 3}]}]} 375 | table = DataTable({"a\"": ("b", "number", "bb\"", {})}, 376 | {"a1": 1, "a2": 2, "a3": 3}) 377 | self.assertEqual(3, table.NumberOfRows()) 378 | self.assertEqual(json.dumps(json_obj, separators=(",", ":")), 379 | table.ToJSon()) 380 | 381 | def testCustomProperties(self): 382 | # The json of the initial data we load to the table. 383 | json_obj = {"cols": [{"id": "a", 384 | "label": "A", 385 | "type": "number", 386 | "p": {"col_cp": "col_v"}}, 387 | {"id": "b", "label": "b", "type": "string"}, 388 | {"id": "c", "label": "c", "type": "boolean"}], 389 | "rows": [{"c": [{"v": 1}, 390 | None, 391 | {"v": None, 392 | "p": {"null_cp": "null_v"}}], 393 | "p": {"row_cp": "row_v"}}, 394 | {"c": [None, 395 | {"v": "z", "p": {"cell_cp": "cell_v"}}, 396 | {"v": True}]}, 397 | {"c": [{"v": 3}, None, None], 398 | "p": {"row_cp2": "row_v2"}}], 399 | "p": {"global_cp": "global_v"}} 400 | jscode = ("var mytab = new google.visualization.DataTable();\n" 401 | "mytab.setTableProperties({\"global_cp\":\"global_v\"});\n" 402 | "mytab.addColumn(\"number\", \"A\", \"a\");\n" 403 | "mytab.setColumnProperties(0, {\"col_cp\":\"col_v\"});\n" 404 | "mytab.addColumn(\"string\", \"b\", \"b\");\n" 405 | "mytab.addColumn(\"boolean\", \"c\", \"c\");\n" 406 | "mytab.addRows(3);\n" 407 | "mytab.setCell(0, 0, 1);\n" 408 | "mytab.setCell(0, 2, null, null, {\"null_cp\":\"null_v\"});\n" 409 | "mytab.setRowProperties(0, {\"row_cp\":\"row_v\"});\n" 410 | "mytab.setCell(1, 1, \"z\", null, {\"cell_cp\":\"cell_v\"});\n" 411 | "mytab.setCell(1, 2, true);\n" 412 | "mytab.setCell(2, 0, 3);\n" 413 | "mytab.setRowProperties(2, {\"row_cp2\":\"row_v2\"});\n") 414 | 415 | table = DataTable([("a", "number", "A", {"col_cp": "col_v"}), "b", 416 | ("c", "boolean")], 417 | custom_properties={"global_cp": "global_v"}) 418 | table.AppendData([[1, None, (None, None, {"null_cp": "null_v"})]], 419 | custom_properties={"row_cp": "row_v"}) 420 | table.AppendData([[None, ("z", None, {"cell_cp": "cell_v"}), True], [3]]) 421 | table.SetRowsCustomProperties(2, {"row_cp2": "row_v2"}) 422 | self.assertEqual(json.dumps(json_obj, separators=(",", ":")), 423 | table.ToJSon()) 424 | self.assertEqual(jscode, table.ToJSCode("mytab")) 425 | 426 | def testToCsv(self): 427 | init_data_csv = "\r\n".join(["A,\"b\"\"\",c", 428 | "1,,", 429 | ",zz'top,true", 430 | ""]) 431 | table = DataTable([("a", "number", "A"), "b\"", ("c", "boolean")], 432 | [[(1, "$1")], [None, "zz'top", True]]) 433 | self.assertEqual(init_data_csv, table.ToCsv()) 434 | table.AppendData([[-1, "w", False]]) 435 | init_data_csv = "%s%s\r\n" % (init_data_csv, "-1,w,false") 436 | self.assertEquals(init_data_csv, table.ToCsv()) 437 | 438 | init_data_csv = "\r\n".join([ 439 | "T,d,dt", 440 | "01:02:03,1901-02-03,", 441 | "\"time \"\"2 3 4\"\"\",1902-03-04,1901-02-03 04:05:06", 442 | ",1903-04-05,", 443 | ""]) 444 | table = DataTable({("d", "date"): [("t", "timeofday", "T"), 445 | ("dt", "datetime")]}) 446 | table.LoadData({date(1901, 2, 3): [time(1, 2, 3)], 447 | date(1902, 3, 4): [(time(2, 3, 4), 'time "2 3 4"'), 448 | datetime(1901, 2, 3, 4, 5, 6)], 449 | date(1903, 4, 5): []}) 450 | self.assertEqual(init_data_csv, table.ToCsv(columns_order=["t", "d", "dt"])) 451 | 452 | def testToTsvExcel(self): 453 | table = DataTable({("d", "date"): [("t", "timeofday", "T"), 454 | ("dt", "datetime")]}) 455 | table.LoadData({date(1901, 2, 3): [time(1, 2, 3)], 456 | date(1902, 3, 4): [(time(2, 3, 4), 'time "2 3 4"'), 457 | datetime(1901, 2, 3, 4, 5, 6)], 458 | date(1903, 4, 5): []}) 459 | self.assertEqual(table.ToCsv().replace(",", "\t").encode("UTF-16LE"), 460 | table.ToTsvExcel()) 461 | 462 | def testToHtml(self): 463 | html_table_header = "" 464 | html_table_footer = "
" 465 | init_data_html = html_table_header + ( 466 | "" 467 | "A<b>c" 468 | "" 469 | "" 470 | "$1" 471 | "<z>true" 472 | "") + html_table_footer 473 | table = DataTable([("a", "number", "A<"), "b>", ("c", "boolean")], 474 | [[(1, "$1")], [None, "", True]]) 475 | self.assertEqual(init_data_html.replace("\n", ""), table.ToHtml()) 476 | 477 | init_data_html = html_table_header + ( 478 | "" 479 | "Tddt" 480 | "" 481 | "" 482 | "01:02:030001-02-03" 483 | "time 2 3 40002-03-04" 484 | "0001-02-03 04:05:06" 485 | "0003-04-05" 486 | "") + html_table_footer 487 | table = DataTable({("d", "date"): [("t", "timeofday", "T"), 488 | ("dt", "datetime")]}) 489 | table.LoadData({date(1, 2, 3): [time(1, 2, 3)], 490 | date(2, 3, 4): [(time(2, 3, 4), "time 2 3 4"), 491 | datetime(1, 2, 3, 4, 5, 6)], 492 | date(3, 4, 5): []}) 493 | self.assertEqual(init_data_html.replace("\n", ""), 494 | table.ToHtml(columns_order=["t", "d", "dt"])) 495 | 496 | def testOrderBy(self): 497 | data = [("b", 3), ("a", 3), ("a", 2), ("b", 1)] 498 | description = ["col1", ("col2", "number", "Second Column")] 499 | table = DataTable(description, data) 500 | 501 | table_num_sorted = DataTable(description, 502 | sorted(data, key=lambda x: (x[1], x[0]))) 503 | 504 | table_str_sorted = DataTable(description, 505 | sorted(data, key=lambda x: x[0])) 506 | 507 | table_diff_sorted = DataTable(description, 508 | sorted(sorted(data, key=lambda x: x[1]), 509 | key=lambda x: x[0], reverse=True)) 510 | 511 | self.assertEqual(table_num_sorted.ToJSon(), 512 | table.ToJSon(order_by=("col2", "col1"))) 513 | self.assertEqual(table_num_sorted.ToJSCode("mytab"), 514 | table.ToJSCode("mytab", order_by=("col2", "col1"))) 515 | 516 | self.assertEqual(table_str_sorted.ToJSon(), table.ToJSon(order_by="col1")) 517 | self.assertEqual(table_str_sorted.ToJSCode("mytab"), 518 | table.ToJSCode("mytab", order_by="col1")) 519 | 520 | self.assertEqual(table_diff_sorted.ToJSon(), 521 | table.ToJSon(order_by=[("col1", "desc"), "col2"])) 522 | self.assertEqual(table_diff_sorted.ToJSCode("mytab"), 523 | table.ToJSCode("mytab", 524 | order_by=[("col1", "desc"), "col2"])) 525 | 526 | def testToJSonResponse(self): 527 | description = ["col1", "col2", "col3"] 528 | data = [("1", "2", "3"), ("a", "b", "c"), ("One", "Two", "Three")] 529 | req_id = 4 530 | table = DataTable(description, data) 531 | 532 | start_str_default = r"google.visualization.Query.setResponse" 533 | start_str_handler = r"MyHandlerFunction" 534 | 535 | json_str = table.ToJSon().strip() 536 | 537 | json_response = table.ToJSonResponse(req_id=req_id) 538 | 539 | self.assertEquals(json_response.find(start_str_default + "("), 0) 540 | 541 | json_response_obj = json.loads(json_response[len(start_str_default) + 1:-2]) 542 | self.assertEquals(json_response_obj["table"], json.loads(json_str)) 543 | self.assertEquals(json_response_obj["version"], "0.6") 544 | self.assertEquals(json_response_obj["reqId"], str(req_id)) 545 | self.assertEquals(json_response_obj["status"], "ok") 546 | 547 | json_response = table.ToJSonResponse(req_id=req_id, 548 | response_handler=start_str_handler) 549 | 550 | self.assertEquals(json_response.find(start_str_handler + "("), 0) 551 | json_response_obj = json.loads(json_response[len(start_str_handler) + 1:-2]) 552 | self.assertEquals(json_response_obj["table"], json.loads(json_str)) 553 | 554 | def testToResponse(self): 555 | description = ["col1", "col2", "col3"] 556 | data = [("1", "2", "3"), ("a", "b", "c"), ("One", "Two", "Three")] 557 | table = DataTable(description, data) 558 | 559 | self.assertEquals(table.ToResponse(), table.ToJSonResponse()) 560 | self.assertEquals(table.ToResponse(tqx="out:csv"), table.ToCsv()) 561 | self.assertEquals(table.ToResponse(tqx="out:html"), table.ToHtml()) 562 | self.assertRaises(DataTableException, table.ToResponse, tqx="version:0.1") 563 | self.assertEquals(table.ToResponse(tqx="reqId:4;responseHandler:handle"), 564 | table.ToJSonResponse(req_id=4, response_handler="handle")) 565 | self.assertEquals(table.ToResponse(tqx="out:csv;reqId:4"), table.ToCsv()) 566 | self.assertEquals(table.ToResponse(order_by="col2"), 567 | table.ToJSonResponse(order_by="col2")) 568 | self.assertEquals(table.ToResponse(tqx="out:html", 569 | columns_order=("col3", "col2", "col1")), 570 | table.ToHtml(columns_order=("col3", "col2", "col1"))) 571 | self.assertRaises(ValueError, table.ToResponse, tqx="SomeWrongTqxFormat") 572 | self.assertRaises(DataTableException, table.ToResponse, tqx="out:bad") 573 | 574 | 575 | if __name__ == "__main__": 576 | unittest.main() 577 | -------------------------------------------------------------------------------- /report_tools/reports.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | from django.db import models 4 | from django.utils.datastructures import SortedDict 5 | from django.utils.encoding import StrAndUnicode 6 | from django.utils.safestring import mark_safe 7 | 8 | from .charts import Chart 9 | 10 | __all__ = ('BaseReport', 'Report') 11 | 12 | 13 | def pretty_name(name): 14 | """Converts 'first_name' to 'First name'""" 15 | if not name: 16 | return u'' 17 | return name.replace('_', ' ').capitalize() 18 | 19 | 20 | def get_declared_charts(bases, attrs, with_base_charts=True): 21 | """ 22 | Create a list of report chart instances from the passed in 'attrs', plus any 23 | similar charts on the base classes (in 'bases'). This is used by the Report 24 | metaclass. 25 | 26 | If 'with_base_charts' is True, all charts from the bases are used. 27 | Otherwise, only charts in the 'declared_charts' attribute on the bases are 28 | used. 29 | """ 30 | charts = [(chart_name, attrs.pop(chart_name)) for chart_name, obj in attrs.items() if isinstance(obj, Chart)] 31 | 32 | charts.sort(key=lambda x: x[1].creation_counter) 33 | 34 | # If this class is subclassing another Report, add that Report's charts. 35 | # Note that we loop over the bases in *reverse*. This is necessary in order 36 | # to preserver the correct order of charts. 37 | if with_base_charts: 38 | for base in bases[::-1]: 39 | if hasattr(base, 'base_charts'): 40 | charts = base.base_charts.items() + charts 41 | else: 42 | for base in bases[::-1]: 43 | if hasattr(base, 'declared_charts'): 44 | charts = base.declared_charts.items() + charts 45 | 46 | return SortedDict(charts) 47 | 48 | 49 | class DeclarativeChartsMetaclass(type): 50 | """ 51 | Metaclass that converts Chart attributes to a dictionary called 52 | 'base_charts', taking into account parent class 'base_charts' as well. 53 | """ 54 | def __new__(cls, name, bases, attrs): 55 | attrs['base_charts'] = get_declared_charts(bases, attrs) 56 | new_class = super(DeclarativeChartsMetaclass, 57 | cls).__new__(cls, name, bases, attrs) 58 | 59 | return new_class 60 | 61 | 62 | class BaseReport(StrAndUnicode): 63 | def __init__(self, data=None, prefix=None): 64 | self.data = data or {} 65 | self.prefix = prefix 66 | 67 | # The base_charts class attribute is the *class-wide* definition of 68 | # charts. Because a particular *instance* of the class might want to 69 | # alter self.charts, we create self.charts here by copying base_charts. 70 | # Instances should always modify self.charts; they should not modify 71 | # self.base_charts 72 | self.charts = deepcopy(self.base_charts) 73 | 74 | def __unicode__(self): 75 | return "WHOLE REPORT PRINTING NOT YET IMPLEMENTED" # TODO 76 | 77 | def __iter__(self): 78 | for name, chart in self.charts.items(): 79 | data = self._get_chart_data(name) 80 | yield BoundChart(self, chart, name, data) 81 | 82 | def __getitem__(self, name): 83 | "Returns a BoundChart with the given name" 84 | try: 85 | chart = self.charts[name] 86 | except KeyError: 87 | raise KeyError('Key %r not found in Report' % name) 88 | 89 | data = self._get_chart_data(name) 90 | return BoundChart(self, chart, name, data, self.prefix) 91 | 92 | def set_prefix(self, prefix): 93 | self.prefix = prefix 94 | 95 | def _get_chart_data(self, name): 96 | callback_name = 'get_data_for_%s' % name 97 | if name in self.data: 98 | data = self.data[name] 99 | elif hasattr(self, callback_name): 100 | data = getattr(self, callback_name)() 101 | self.data[name] = data 102 | else: 103 | data = None 104 | 105 | return data 106 | 107 | def setup(self, request): 108 | pass 109 | 110 | def api_setup(self, request): 111 | return self.setup(request) 112 | 113 | 114 | class Report(BaseReport): 115 | "A collection of charts, plus their associated data." 116 | # This is a separate class from BaseReport in order to abstract the way 117 | # self.charts is specified. This class (Report) is the one that does the 118 | # fancy metaclass stuff purely for the semantic sugar -- it allows one to 119 | # define a report using declarative syntax. 120 | # BaseReport itself has no way of designating self.charts 121 | __metaclass__ = DeclarativeChartsMetaclass 122 | 123 | 124 | class BoundChart(StrAndUnicode): 125 | "A chart plus data" 126 | def __init__(self, report, chart, name, data=None, prefix=None): 127 | self.report = report 128 | self.chart = chart 129 | self.name = name 130 | self.data = data 131 | self.prefix = prefix 132 | self.attrs = self.chart.attrs 133 | self.options = self.chart.options 134 | self.renderer_options = self.chart.renderer_options 135 | 136 | if self.chart.title is None: 137 | self.title = pretty_name(name) 138 | else: 139 | self.title = self.chart.title 140 | 141 | def __unicode__(self): 142 | """Renders this chart""" 143 | return self.render() 144 | 145 | @property 146 | def chart_id(self): 147 | if self.prefix: 148 | chart_id = 'chartid_%s_%s' % (self.prefix, self.name) 149 | else: 150 | chart_id = 'chartid_%s' % self.name 151 | 152 | return chart_id 153 | 154 | def render(self): 155 | base_renderer = getattr(self.report, 'renderer', None) 156 | 157 | return mark_safe(self.chart.render(self.chart_id, self.data, base_renderer=base_renderer)) 158 | -------------------------------------------------------------------------------- /report_tools/templates/report_tools/renderers/googlecharts/barchart.html: -------------------------------------------------------------------------------- 1 | {% extends "report_tools/renderers/googlecharts/chart.html" %} 2 | 3 | {% block chart_init %} 4 | var chart = new google.visualization.BarChart(document.getElementById('{{ chart_id }}')); 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /report_tools/templates/report_tools/renderers/googlecharts/chart.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 53 | -------------------------------------------------------------------------------- /report_tools/templates/report_tools/renderers/googlecharts/columnchart.html: -------------------------------------------------------------------------------- 1 | {% extends "report_tools/renderers/googlecharts/chart.html" %} 2 | 3 | {% block chart_init %} 4 | var chart = new google.visualization.ColumnChart(document.getElementById('{{ chart_id }}')); 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /report_tools/templates/report_tools/renderers/googlecharts/linechart.html: -------------------------------------------------------------------------------- 1 | {% extends "report_tools/renderers/googlecharts/chart.html" %} 2 | 3 | {% block chart_init %} 4 | var chart = new google.visualization.LineChart(document.getElementById('{{ chart_id }}')); 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /report_tools/templates/report_tools/renderers/googlecharts/piechart.html: -------------------------------------------------------------------------------- 1 | {% extends "report_tools/renderers/googlecharts/chart.html" %} 2 | 3 | {% block chart_init %} 4 | var chart = new google.visualization.PieChart(document.getElementById('{{ chart_id }}')); 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /report_tools/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Import tests here for django < 1.6 2 | from .test_all import * 3 | -------------------------------------------------------------------------------- /report_tools/tests/reports.py: -------------------------------------------------------------------------------- 1 | from report_tools.reports import Report 2 | from report_tools.chart_data import ChartData 3 | from report_tools.renderers.googlecharts import GoogleChartsRenderer 4 | from report_tools import charts 5 | 6 | 7 | 8 | class GenericReport(Report): 9 | template_chart = charts.TemplateChart(template="templates/examples/template_chart.html") 10 | 11 | def get_data_for_template_chart(self): 12 | template_context = { 13 | 'pony_types': ["Blue", "Pink", "Magical"] 14 | } 15 | 16 | return template_context 17 | 18 | 19 | class GoogleChartsReport(Report): 20 | renderer = GoogleChartsRenderer 21 | 22 | pie_chart = charts.PieChart(width="500") 23 | column_chart = charts.ColumnChart(width="500") 24 | line_chart = charts.LineChart(width="500") 25 | bar_chart = charts.BarChart(width="500") 26 | 27 | def get_single_series_data(self): 28 | data = ChartData() 29 | 30 | data.add_column("Pony Type") 31 | data.add_column("Population") 32 | 33 | data.add_row(["Blue", 20]) 34 | data.add_row(["Pink", 20]) 35 | data.add_row(["Magical", 1]) 36 | 37 | return data 38 | 39 | def get_multi_series_data(self): 40 | data = ChartData() 41 | 42 | data.add_column("Pony Type") 43 | data.add_column("Australian Population") 44 | data.add_column("Switzerland Population") 45 | data.add_column("USA Population") 46 | 47 | data.add_row(["Blue", (5, {'formatted_value': "Five"}), 10, 5]) 48 | data.add_row(["Pink", 10, 2, 8]) 49 | data.add_row(["Magical", 1, 0, 0]) 50 | 51 | return data 52 | 53 | def get_data_for_pie_chart(self): 54 | return self.get_single_series_data() 55 | 56 | def get_data_for_column_chart(self): 57 | return self.get_multi_series_data() 58 | 59 | def get_data_for_bar_chart(self): 60 | return self.get_multi_series_data() 61 | 62 | def get_data_for_line_chart(self): 63 | return self.get_multi_series_data() -------------------------------------------------------------------------------- /report_tools/tests/test_all.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.test.client import RequestFactory 3 | from report_tools.tests.reports import GoogleChartsReport 4 | from report_tools import api 5 | from report_tools.tests.views import GoogleChartsReportView 6 | 7 | 8 | 9 | class GoogleChartsTest(TestCase): 10 | def test_pie_chart(self): 11 | """ 12 | Test google charts rendering of a pie chart 13 | """ 14 | report = GoogleChartsReport() 15 | chart_html = u'%s' % report['pie_chart'] 16 | 17 | self.assertRegexpMatches(chart_html, r'google\.visualization\.PieChart') 18 | 19 | def test_column_chart(self): 20 | """ 21 | Test google chart rendering of a column chart 22 | """ 23 | report = GoogleChartsReport() 24 | chart_html = u'%s' % report['column_chart'] 25 | 26 | self.assertRegexpMatches(chart_html, r'google\.visualization\.ColumnChart') 27 | 28 | def test_line_chart(self): 29 | """ 30 | Test google chart rendering of a column chart 31 | """ 32 | report = GoogleChartsReport() 33 | chart_html = u'%s' % report['line_chart'] 34 | 35 | self.assertRegexpMatches(chart_html, r'google\.visualization\.LineChart') 36 | 37 | def test_bar_chart(self): 38 | """ 39 | Test google chart rendering of a bar chart 40 | """ 41 | report = GoogleChartsReport() 42 | chart_html = u'%s' % report['bar_chart'] 43 | 44 | self.assertRegexpMatches(chart_html, r'google\.visualization\.BarChart') 45 | 46 | 47 | class APITest(TestCase): 48 | def setUp(self): 49 | self.factory = RequestFactory() 50 | self.request = self.factory.get('/') 51 | 52 | api.register(GoogleChartsReportView) 53 | 54 | def test_internal_api_get(self): 55 | """ 56 | Test the internal get_chart function 57 | """ 58 | chart = api.get_chart(self.request, 'google_charts_report', 'pie_chart') 59 | chart_html = u'%s' % chart 60 | self.assertRegexpMatches(chart_html, r'google\.visualization\.PieChart') 61 | 62 | def test_report_not_found(self): 63 | """ 64 | Make sure asking for a non-existant report throws an appropriate error 65 | """ 66 | with self.assertRaises(api.ReportNotFoundError): 67 | chart = api.get_chart(self.request, 'doogle_charts_report', 'pie_chart') 68 | 69 | def test_chart_not_found(self): 70 | """ 71 | Make sure asking for a non-existant chart throws an appropriate error 72 | """ 73 | with self.assertRaises(api.ChartNotFoundError): 74 | chart = api.get_chart(self.request, 'google_charts_report', 'delicious_pie_chart') 75 | -------------------------------------------------------------------------------- /report_tools/tests/views.py: -------------------------------------------------------------------------------- 1 | from report_tools.views import ReportView 2 | from report_tools import api 3 | from report_tools.tests.reports import GoogleChartsReport 4 | 5 | 6 | 7 | class GoogleChartsReportView(ReportView): 8 | api_key = 'google_charts_report' 9 | 10 | def get_report(self, request): 11 | return GoogleChartsReport() 12 | -------------------------------------------------------------------------------- /report_tools/urls.py: -------------------------------------------------------------------------------- 1 | from views import ReportAPIDispatchView 2 | 3 | from django.conf.urls import * 4 | from django.conf import settings 5 | 6 | 7 | 8 | urlpatterns = patterns('', 9 | url(r'^(?P\w+)/(?P\w+)/$', ReportAPIDispatchView.as_view(), name="reports-api-chart"), 10 | ) 11 | -------------------------------------------------------------------------------- /report_tools/views.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | try: 4 | import json 5 | except ImportError: 6 | import simplejson as json 7 | 8 | from django.http import HttpResponse, Http404 9 | from django.views.generic import View 10 | from django.utils.decorators import classonlymethod 11 | from django.utils.safestring import mark_safe 12 | 13 | 14 | 15 | 16 | from report_tools.api import (ChartNotFoundError, report_api_registry, 17 | OVERRIDE_PARAMS__CHART_HEIGHT, OVERRIDE_PARAMS__CHART_WIDTH, 18 | OVERRIDE_PARAMS__CHART_TITLE) 19 | 20 | 21 | class ChartPermissionError(Exception): 22 | pass 23 | 24 | 25 | class ReportView(View): 26 | def security_check(self, request): 27 | return True 28 | 29 | def get_report(self, request, prefix=None): 30 | raise NotImplementedError 31 | 32 | def _get_report(self, request, prefix=None): 33 | report = self.get_report(request) 34 | 35 | if prefix: 36 | report.set_prefix(prefix) 37 | 38 | chart_height = request.GET.get(OVERRIDE_PARAMS__CHART_HEIGHT, None) 39 | chart_width = request.GET.get(OVERRIDE_PARAMS__CHART_WIDTH, None) 40 | chart_title = request.GET.get(OVERRIDE_PARAMS__CHART_TITLE, None) 41 | 42 | for chart_name, chart in report.charts.iteritems(): 43 | if chart_height is not None: 44 | chart.options['height'] = chart_height 45 | if chart_width is not None: 46 | chart.options['width'] = chart_width 47 | if chart_title is not None: 48 | chart.options['title'] = chart_title 49 | 50 | return report 51 | 52 | def get_chart(self, request, chart_name, prefix=None): 53 | if not self.security_check(request): 54 | raise ChartPermissionError("Chart access forbidden") 55 | 56 | report = self._get_report(request, prefix) 57 | 58 | try: 59 | chart = report[chart_name] 60 | except KeyError: 61 | raise ChartNotFoundError("Chart %s not found in this report" % chart_name) 62 | 63 | return chart 64 | 65 | def api_get(self, request, chart_name, prefix=None): 66 | chart = self.get_chart(request, chart_name, prefix) 67 | 68 | if chart: 69 | html = mark_safe(u'%s' % chart) 70 | attrs = chart.attrs 71 | else: 72 | html = mark_safe(self.security_failure_message) 73 | attrs = {} 74 | 75 | return_data = { 76 | 'html': html, 77 | 'attrs': attrs, 78 | } 79 | 80 | return HttpResponse(json.dumps(return_data), mimetype='application/javascript') 81 | 82 | @classonlymethod 83 | def as_api_view(cls, **initkwargs): 84 | """ 85 | Main entry point for an api request-response process. 86 | """ 87 | # sanitize keyword arguments 88 | for key in initkwargs: 89 | if key in cls.http_method_names: 90 | raise TypeError(u"You tried to pass in the %s method name as a " 91 | u"keyword argument to %s(). Don't do that." 92 | % (key, cls.__name__)) 93 | if not hasattr(cls, key): 94 | raise TypeError(u"%s() received an invalid keyword %r" % ( 95 | cls.__name__, key)) 96 | 97 | def view(request, *args, **kwargs): 98 | self = cls(**initkwargs) 99 | return self.api_dispatch(request, *args, **kwargs) 100 | 101 | # take name and docstring from class 102 | functools.update_wrapper(view, cls, updated=()) 103 | 104 | # and possible attributes set by decorators 105 | # like csrf_exempt from dispatch 106 | functools.update_wrapper(view, cls.dispatch, assigned=()) 107 | return view 108 | 109 | def dispatch(self, request, *args, **kwargs): 110 | # Try to dispatch to the right method; if a method doesn't exist, 111 | # defer to the error handler. Also defer to the error handler if the 112 | # request method isn't on the approved list. 113 | if request.method.lower() in self.http_method_names: 114 | if '_format' in request.GET: 115 | method_name = request.method.lower() + '_' + request.GET['_format'] + '_format' 116 | else: 117 | method_name = request.method.lower() 118 | handler = getattr(self, method_name, self.http_method_not_allowed) 119 | else: 120 | handler = self.http_method_not_allowed 121 | self.request = request 122 | self.args = args 123 | self.kwargs = kwargs 124 | return handler(request, *args, **kwargs) 125 | 126 | def api_dispatch(self, request, *args, **kwargs): 127 | # Try to dispatch to the right api method; if a method doesn't exist, 128 | # defer to the error handler. Also defer to the error handler if the 129 | # request method isn't on the approved list. 130 | if request.method.lower() in self.http_method_names: 131 | method_name = 'api_' + request.method.lower() 132 | handler = getattr(self, method_name, self.http_method_not_allowed) 133 | else: 134 | handler = self.http_method_not_allowed 135 | self.request = request 136 | self.args = args 137 | self.kwargs = kwargs 138 | return handler(request, *args, **kwargs) 139 | 140 | 141 | class ReportAPIDispatchView(View): 142 | def dispatch(self, request, report_api_key, chart_name): 143 | report_view_class = report_api_registry.get_report_view_class(report_api_key) 144 | 145 | if not report_view_class: 146 | raise Http404 147 | 148 | report_view = report_view_class() 149 | return report_view.api_dispatch(request, chart_name) 150 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup 3 | 4 | setup( 5 | name = 'django-report-tools', 6 | description = 'Class-based reports for elegant data views.', 7 | version = '0.2.2', 8 | author = 'Evan Brumley', 9 | author_email = 'evan.brumley@gmail.com', 10 | url = 'http://github.com/evanbrumley/django-report-tools', 11 | test_suite = "tests.runtests.runtests", 12 | packages=['report_tools', 'report_tools.tests', 'report_tools.renderers', 13 | 'report_tools.renderers.googlecharts', 14 | 'report_tools.renderers.googlecharts.gviz_api'], 15 | package_data={'report_tools': [ 16 | 'templates/report_tools/renderers/googlecharts/*.html', 17 | 'renderers/googlecharts/gviz_api/COPYRIGHT', 18 | 'renderers/googlecharts/gviz_api/README']}, 19 | classifiers = ['Development Status :: 4 - Beta', 20 | 'Environment :: Web Environment', 21 | 'Framework :: Django', 22 | 'Intended Audience :: Developers', 23 | 'License :: OSI Approved :: BSD License', 24 | 'Operating System :: OS Independent', 25 | 'Programming Language :: Python', 26 | 'Topic :: Utilities'], 27 | ) 28 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanbrumley/django-report-tools/3347705c6f7880a9766ac827ff7575e5a0b5bcb7/tests/__init__.py -------------------------------------------------------------------------------- /tests/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproj.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.5.1 2 | -------------------------------------------------------------------------------- /tests/runtests.py: -------------------------------------------------------------------------------- 1 | #This file mainly exists to allow python setup.py test to work. 2 | import os, sys 3 | os.environ['DJANGO_SETTINGS_MODULE'] = 'testproj.settings' 4 | test_dir = os.path.dirname(__file__) 5 | sys.path.insert(0, test_dir) 6 | 7 | from django.test.utils import get_runner 8 | from django.conf import settings 9 | 10 | def runtests(): 11 | TestRunner = get_runner(settings) 12 | test_runner = TestRunner(verbosity=1, interactive=True) 13 | failures = test_runner.run_tests(['report_tools']) 14 | sys.exit(bool(failures)) 15 | 16 | if __name__ == '__main__': 17 | runtests() 18 | -------------------------------------------------------------------------------- /tests/runtests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | python manage.py test report_tools 4 | -------------------------------------------------------------------------------- /tests/testproj/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanbrumley/django-report-tools/3347705c6f7880a9766ac827ff7575e5a0b5bcb7/tests/testproj/__init__.py -------------------------------------------------------------------------------- /tests/testproj/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanbrumley/django-report-tools/3347705c6f7880a9766ac827ff7575e5a0b5bcb7/tests/testproj/core/__init__.py -------------------------------------------------------------------------------- /tests/testproj/core/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /tests/testproj/core/reports.py: -------------------------------------------------------------------------------- 1 | from report_tools.reports import Report 2 | from report_tools.chart_data import ChartData 3 | from report_tools.renderers.googlecharts import GoogleChartsRenderer 4 | from report_tools import charts 5 | 6 | 7 | 8 | class MyReport(Report): 9 | renderer = GoogleChartsRenderer 10 | 11 | pie_chart = charts.PieChart(title="Pony Populations", width="500") 12 | template_chart = charts.TemplateChart(title="Pony Types", template="core/template_chart.html") 13 | column_chart = charts.ColumnChart(title="Pony Populations", width="500") 14 | multiseries_column_chart = charts.ColumnChart(title="Pony Populations by Country", width="500") 15 | bar_chart = charts.BarChart(title="Pony Populations", width="500") 16 | multiseries_bar_chart = charts.BarChart(title="Pony Populations by Country", width="500") 17 | line_chart = charts.LineChart(title="Blue Pony Population - 2009-2012", width="500") 18 | multiseries_line_chart = charts.LineChart(title="Pony Populations - 2009-2012", width="500") 19 | naughty_pie_chart = charts.PieChart(title="Pony Populations", width="500") 20 | 21 | def get_data_for_line_chart(self): 22 | data = ChartData() 23 | 24 | data.add_column("Test Period") 25 | data.add_column("Blue Pony Population") 26 | 27 | data.add_row(["2009-10", 20]) 28 | data.add_row(["2010-11", 18]) 29 | data.add_row(["2011-12", 100]) 30 | 31 | return data 32 | 33 | def get_data_for_multiseries_line_chart(self): 34 | data = ChartData() 35 | 36 | data.add_column("Test Period") 37 | data.add_column("Blue Pony Population") 38 | data.add_column("Pink Pony Population") 39 | data.add_column("Magical Pony Population") 40 | 41 | data.add_row(["2009-10", 20, 10, 50]) 42 | data.add_row(["2010-11", 18, 8, 60]) 43 | data.add_row(["2011-12", 100, 120, 2]) 44 | 45 | return data 46 | 47 | def get_data_for_bar_chart(self): 48 | data = ChartData() 49 | 50 | data.add_column("Pony Type") 51 | data.add_column("Population") 52 | 53 | data.add_row(["Blue", 20]) 54 | data.add_row(["Pink", 20]) 55 | data.add_row(["Magical", 1]) 56 | 57 | return data 58 | 59 | def get_data_for_multiseries_bar_chart(self): 60 | data = ChartData() 61 | 62 | data.add_column("Pony Type") 63 | data.add_column("Australian Population") 64 | data.add_column("Switzerland Population") 65 | data.add_column("USA Population") 66 | 67 | data.add_row(["Blue", 5, 10, 5]) 68 | data.add_row(["Pink", 10, 2, 8]) 69 | data.add_row(["Magical", 1, 0, 0]) 70 | 71 | return data 72 | 73 | def get_data_for_column_chart(self): 74 | data = ChartData() 75 | 76 | data.add_column("Pony Type") 77 | data.add_column("Population") 78 | 79 | data.add_row(["Blue", 20]) 80 | data.add_row(["Pink", 20]) 81 | data.add_row(["Magical", 1]) 82 | 83 | return data 84 | 85 | def get_data_for_multiseries_column_chart(self): 86 | data = ChartData() 87 | 88 | data.add_column("Pony Type") 89 | data.add_column("Australian Population") 90 | data.add_column("Switzerland Population") 91 | data.add_column("USA Population") 92 | 93 | data.add_row(["Blue", 5, 10, 5]) 94 | data.add_row(["Pink", 10, 2, 8]) 95 | data.add_row(["Magical", 1, 0, 0]) 96 | 97 | return data 98 | 99 | def get_data_for_pie_chart(self): 100 | data = ChartData() 101 | 102 | data.add_column("Pony Type") 103 | data.add_column("Population") 104 | 105 | data.add_row(["Blue", 20]) 106 | data.add_row(["Pink", 20]) 107 | data.add_row(["Magical", 1]) 108 | 109 | return data 110 | 111 | def get_data_for_naughty_pie_chart(self): 112 | data = ChartData() 113 | 114 | data.add_column("Pony &&&Type") 115 | data.add_column("Population") 116 | 117 | data.add_row(["Blue", 20]) 118 | data.add_row(["Pink&&&", 20]) 119 | data.add_row(["Magical", 1]) 120 | 121 | return data 122 | 123 | def get_data_for_template_chart(self): 124 | pony_types = [ 125 | ('Blue', 'Equus Caeruleus'), 126 | ('Pink', 'Equus Roseus'), 127 | ('Magical', 'Equus Magica') 128 | ] 129 | 130 | template_context = { 131 | 'pony_types': pony_types 132 | } 133 | 134 | return template_context 135 | -------------------------------------------------------------------------------- /tests/testproj/core/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates writing tests using the unittest module. These will pass 3 | when you run "manage.py test". 4 | 5 | Replace this with more appropriate tests for your application. 6 | """ 7 | 8 | from django.test import TestCase 9 | 10 | 11 | class SimpleTest(TestCase): 12 | def test_basic_addition(self): 13 | """ 14 | Tests that 1 + 1 always equals 2. 15 | """ 16 | self.assertEqual(1 + 1, 2) 17 | -------------------------------------------------------------------------------- /tests/testproj/core/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from report_tools.views import ReportView 3 | from report_tools import api 4 | from reports import MyReport 5 | 6 | 7 | 8 | class MyReportView(ReportView): 9 | api_key = 'my_report' 10 | 11 | def get_report(self, request): 12 | return MyReport() 13 | 14 | def get(self, request): 15 | template = "core/index.html" 16 | report = self.get_report(request) 17 | context = {'report': report} 18 | 19 | return render(request, template, context) 20 | 21 | api.register(MyReportView) 22 | -------------------------------------------------------------------------------- /tests/testproj/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for testproj project. 2 | import os 3 | import sys 4 | 5 | 6 | PROJECT_ROOT = os.path.dirname(__file__) 7 | sys.path.insert(0, os.path.join(PROJECT_ROOT, "../../")) 8 | 9 | DEBUG = True 10 | TEMPLATE_DEBUG = DEBUG 11 | 12 | ADMINS = ( 13 | # ('Your Name', 'your_email@example.com'), 14 | ) 15 | 16 | MANAGERS = ADMINS 17 | 18 | DATABASES = { 19 | 'default': { 20 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 21 | 'NAME': 'db.sqlite', # Or path to database file if using sqlite3. 22 | 'USER': '', # Not used with sqlite3. 23 | 'PASSWORD': '', # Not used with sqlite3. 24 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 25 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. 26 | } 27 | } 28 | 29 | # Local time zone for this installation. Choices can be found here: 30 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 31 | # although not all choices may be available on all operating systems. 32 | # On Unix systems, a value of None will cause Django to use the same 33 | # timezone as the operating system. 34 | # If running in a Windows environment this must be set to the same as your 35 | # system time zone. 36 | TIME_ZONE = 'America/Chicago' 37 | 38 | # Language code for this installation. All choices can be found here: 39 | # http://www.i18nguy.com/unicode/language-identifiers.html 40 | LANGUAGE_CODE = 'en-us' 41 | 42 | SITE_ID = 1 43 | 44 | # If you set this to False, Django will make some optimizations so as not 45 | # to load the internationalization machinery. 46 | USE_I18N = True 47 | 48 | # If you set this to False, Django will not format dates, numbers and 49 | # calendars according to the current locale. 50 | USE_L10N = True 51 | 52 | # If you set this to False, Django will not use timezone-aware datetimes. 53 | USE_TZ = True 54 | 55 | # Absolute filesystem path to the directory that will hold user-uploaded files. 56 | # Example: "/home/media/media.lawrence.com/media/" 57 | MEDIA_ROOT = '' 58 | 59 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 60 | # trailing slash. 61 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 62 | MEDIA_URL = '' 63 | 64 | # Absolute path to the directory static files should be collected to. 65 | # Don't put anything in this directory yourself; store your static files 66 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 67 | # Example: "/home/media/media.lawrence.com/static/" 68 | STATIC_ROOT = '' 69 | 70 | # URL prefix for static files. 71 | # Example: "http://media.lawrence.com/static/" 72 | STATIC_URL = '/static/' 73 | 74 | # Additional locations of static files 75 | STATICFILES_DIRS = ( 76 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 77 | # Always use forward slashes, even on Windows. 78 | # Don't forget to use absolute paths, not relative paths. 79 | ) 80 | 81 | # List of finder classes that know how to find static files in 82 | # various locations. 83 | STATICFILES_FINDERS = ( 84 | 'django.contrib.staticfiles.finders.FileSystemFinder', 85 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 86 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 87 | ) 88 | 89 | # Make this unique, and don't share it with anybody. 90 | SECRET_KEY = 'pe$b$-144ojvq9tdij+l_-9#_l^rk%-8b=niqw%=cc-iq==4ze' 91 | 92 | # List of callables that know how to import templates from various sources. 93 | TEMPLATE_LOADERS = ( 94 | 'django.template.loaders.filesystem.Loader', 95 | 'django.template.loaders.app_directories.Loader', 96 | # 'django.template.loaders.eggs.Loader', 97 | ) 98 | 99 | MIDDLEWARE_CLASSES = ( 100 | 'django.middleware.common.CommonMiddleware', 101 | 'django.contrib.sessions.middleware.SessionMiddleware', 102 | 'django.middleware.csrf.CsrfViewMiddleware', 103 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 104 | 'django.contrib.messages.middleware.MessageMiddleware', 105 | # Uncomment the next line for simple clickjacking protection: 106 | # 'django.middleware.clickjacking.XFrameOptionsMiddleware', 107 | ) 108 | 109 | ROOT_URLCONF = 'testproj.urls' 110 | 111 | # Python dotted path to the WSGI application used by Django's runserver. 112 | WSGI_APPLICATION = 'testproj.wsgi.application' 113 | 114 | TEMPLATE_DIRS = ( 115 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 116 | # Always use forward slashes, even on Windows. 117 | # Don't forget to use absolute paths, not relative paths. 118 | os.path.join(PROJECT_ROOT, "templates"), 119 | ) 120 | 121 | INSTALLED_APPS = ( 122 | 'django.contrib.auth', 123 | 'django.contrib.contenttypes', 124 | 'django.contrib.sessions', 125 | 'django.contrib.sites', 126 | 'django.contrib.messages', 127 | 'django.contrib.staticfiles', 128 | # Uncomment the next line to enable the admin: 129 | # 'django.contrib.admin', 130 | # Uncomment the next line to enable admin documentation: 131 | # 'django.contrib.admindocs', 132 | 'report_tools', 133 | 'testproj.core', 134 | ) 135 | 136 | # A sample logging configuration. The only tangible logging 137 | # performed by this configuration is to send an email to 138 | # the site admins on every HTTP 500 error when DEBUG=False. 139 | # See http://docs.djangoproject.com/en/dev/topics/logging for 140 | # more details on how to customize your logging configuration. 141 | LOGGING = { 142 | 'version': 1, 143 | 'disable_existing_loggers': False, 144 | 'filters': { 145 | 'require_debug_false': { 146 | '()': 'django.utils.log.RequireDebugFalse' 147 | } 148 | }, 149 | 'handlers': { 150 | 'mail_admins': { 151 | 'level': 'ERROR', 152 | 'filters': ['require_debug_false'], 153 | 'class': 'django.utils.log.AdminEmailHandler' 154 | } 155 | }, 156 | 'loggers': { 157 | 'django.request': { 158 | 'handlers': ['mail_admins'], 159 | 'level': 'ERROR', 160 | 'propagate': True, 161 | }, 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /tests/testproj/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Django Report Tools - Examples 6 | 7 | 8 | 9 | 10 | 11 | {% block content %} 12 | 13 | {% endblock %} 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/testproj/templates/core/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

Report Tools Example Charts

5 | 6 |

{{ report.pie_chart.title }}

7 | {{ report.pie_chart }} 8 | 9 |

{{ report.column_chart.title }}

10 | {{ report.column_chart }} 11 | 12 |

{{ report.multiseries_column_chart.title }}

13 | {{ report.multiseries_column_chart }} 14 | 15 |

{{ report.bar_chart.title }}

16 | {{ report.bar_chart }} 17 | 18 |

{{ report.multiseries_bar_chart.title }}

19 | {{ report.multiseries_bar_chart }} 20 | 21 |

{{ report.line_chart.title }}

22 | {{ report.line_chart }} 23 | 24 |

{{ report.multiseries_line_chart.title }}

25 | {{ report.multiseries_line_chart }} 26 | 27 |

{{ report.naughty_pie_chart.title }}

28 | {{ report.naughty_pie_chart }} 29 | 30 |

{{ report.template_chart.title }}

31 | {{ report.template_chart }} 32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /tests/testproj/templates/core/template_chart.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {% for pony_type, latin_name in pony_types %} 8 | 9 | 10 | 11 | 12 | {% endfor %} 13 | 14 |
Pony TypeLatin Name
{{ pony_type }}{{ latin_name }}
-------------------------------------------------------------------------------- /tests/testproj/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | from testproj.core.views import MyReportView 3 | 4 | # Uncomment the next two lines to enable the admin: 5 | # from django.contrib import admin 6 | # admin.autodiscover() 7 | 8 | urlpatterns = patterns('', 9 | # Examples: 10 | # url(r'^$', 'testproj.views.home', name='home'), 11 | # url(r'^testproj/', include('testproj.foo.urls')), 12 | 13 | # Uncomment the admin/doc line below to enable admin documentation: 14 | # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 15 | 16 | # Uncomment the next line to enable the admin: 17 | # url(r'^admin/', include(admin.site.urls)), 18 | 19 | url(r'^$', MyReportView.as_view(), name='index'), 20 | url(r'^api/', include('report_tools.urls')), 21 | ) 22 | -------------------------------------------------------------------------------- /tests/testproj/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for testproj project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | 18 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproj.settings") 19 | 20 | # This application object is used by any WSGI server configured to use this 21 | # file. This includes Django's development server, if the WSGI_APPLICATION 22 | # setting points here. 23 | from django.core.wsgi import get_wsgi_application 24 | application = get_wsgi_application() 25 | 26 | # Apply WSGI middleware here. 27 | # from helloworld.wsgi import HelloWorldApplication 28 | # application = HelloWorldApplication(application) 29 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py26, py27, django-1.5.5, django-1.4.10 8 | 9 | [testenv] 10 | commands = {envpython} setup.py test 11 | deps = 12 | django==1.6.1 13 | 14 | [testenv:django-1.5.5] 15 | commands = {envpython} setup.py test 16 | deps = 17 | django==1.5.5 18 | 19 | [testenv:django-1.4.10] 20 | commands = {envpython} setup.py test 21 | deps = 22 | django==1.4.10 23 | 24 | --------------------------------------------------------------------------------