30 | {% block object-tools %}{% endblock %}
31 |
32 | {% block description %}
33 | {% if report.description %}
34 |
{% trans "Description" %}
35 |
{{ report.description }}
36 | {% endif %}
37 | {% endblock %}
38 |
39 | {% block filters %}
40 |
{% trans "Filters" %}
41 |
49 | {% endblock %}
50 |
51 | {% block requested_reports %}
52 |
{% trans "Previously Requested Reports" %}
53 |
54 |
55 |
56 | | {% trans "Request Time" %} |
57 | {% trans "Completion Time" %} |
58 | {% trans "Params" %} |
59 | {% trans "Link" %} |
60 | {% trans "Status" %} |
61 |
62 |
63 |
64 | {% for report_request in requested_reports %}
65 |
66 | | {{report_request.request_made}} |
67 | {{report_request.completion_timestamp}} |
68 | {{report_request.params}} |
69 | {% trans "View Report" %} |
70 | {{report_request.task_status}} |
71 |
72 | {% endfor %}
73 |
74 |
75 | {% endblock %}
76 |
77 | {% endblock %}
78 |
--------------------------------------------------------------------------------
/reportengine/templates/reportengine/report.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/base_site.html" %}
2 | {% load adminmedia admin_list i18n %}
3 |
4 | {% block extrastyle %}
5 |
30 | {% block object-tools %}{% endblock %}
31 |
32 | {% block description %}
33 | {% if report.description %}
34 |
{% trans "Description" %}
35 |
{{ report.description }}
36 | {% endif %}
37 | {% endblock %}
38 |
39 | {% block filters %}
40 | {% if filter_form.fields %}
41 |
{% trans "Filters" %}
42 |
50 | {% endif %}
51 | {% endblock %}
52 |
53 | {% block alternate-formats %}
54 |
{% trans "Alternate Formats" %}
55 |
56 | {% for of in report.output_formats %}
57 | {% ifequal of output_format %}
58 | {{ of.verbose_name }} {% if not forloop.last %}|{% endif %}
59 | {% else %}
60 |
{{ of.verbose_name }} {% if not forloop.last %}|{% endif %}
61 | {% endifequal %}
62 | {% endfor %}
63 |
64 | {% endblock %}
65 |
66 |
Data
67 |
68 |
69 |
70 | {% for l in report.labels %}
71 | | {{ l }} |
72 | {% endfor %}
73 |
74 |
75 |
76 | {% for row in object_list %}
77 |
78 | {% for v in row %}
79 | | {{ v }} |
80 | {% endfor %}
81 |
82 | {% endfor %}
83 | {% for a in aggregates %}
84 |
85 | |
86 | {{ a.0 }} | {{ a.1 }} |
87 | {% endfor %}
88 |
89 |
90 |
91 | {% if cl %}
92 |
{% pagination cl %}
93 | {% endif %}
94 |
95 |
96 | {% endblock %}
97 |
--------------------------------------------------------------------------------
/example/example_reports/reports.py:
--------------------------------------------------------------------------------
1 | import reportengine
2 | from django.contrib.auth.models import User
3 | from reportengine.filtercontrols import StartsWithFilterControl
4 | from reportengine.outputformats import *
5 |
6 | class UserReport(reportengine.ModelReport):
7 | """An example of a model report"""
8 | verbose_name = "User Report"
9 | slug = "user-report"
10 | namespace = "system"
11 | description = "Listing of all users in the system"
12 | labels = ('username','is_active','email','first_name','last_name','date_joined')
13 | list_filter=['is_active','date_joined',StartsWithFilterControl('username'),'groups']
14 | date_field = "date_joined" # Allows auto filtering by this date
15 | model=User
16 | per_page = 500
17 |
18 | reportengine.register(UserReport)
19 |
20 | class ActiveUserReport(reportengine.QuerySetReport):
21 | """ An example of a queryset report. """
22 | verbose_name="Active User Report"
23 | slug = "active-user-report"
24 | namespace = "system"
25 | per_page=10
26 | labels = ('username','email','first_name','last_name','date_joined')
27 | queryset=User.objects.filter(is_active=True)
28 |
29 | reportengine.register(ActiveUserReport)
30 |
31 | class AppsReport(reportengine.Report):
32 | """An Example report that is pure python, just returning a list"""
33 | verbose_name="Installed Apps"
34 | namespace = "system"
35 | slug = "apps-report"
36 | labels = ('app_name',)
37 | per_page = 0
38 | output_formats = [AdminOutputFormat(),XMLOutputFormat(root_tag="apps",row_tag="app")]
39 |
40 | def get_rows(self,filters={},order_by=None):
41 | from django.conf import settings
42 | # maybe show off by pulling active content type models for each app?
43 | # from django.contrib.contenttypes.models import ContentType
44 | apps=[[a,] for a in settings.INSTALLED_APPS]
45 |
46 | if order_by:
47 | # TODO add sorting based on label?
48 | apps.sort()
49 | total=len(apps)
50 | return apps,(("total",total),)
51 |
52 | reportengine.register(AppsReport)
53 |
54 | class AdminActivityReport(reportengine.DateSQLReport):
55 | row_sql="""select username,user_id,count(*),min(action_time),max(action_time)
56 | from django_admin_log
57 | inner join auth_user on auth_user.id = django_admin_log.user_id
58 | where is_staff = 1
59 | and action_time >= '%(date__gte)s'
60 | and action_time < '%(date__lt)s'
61 | group by user_id;
62 | """
63 | aggregate_sql="""select avg(count) as average,max(count) as max,min(count) as min
64 | from (
65 | select count(user_id) as count
66 | from django_admin_log
67 | where action_time >= '%(date__gte)s'
68 | and action_time < '%(date__lt)s'
69 | group by user_id
70 | )"""
71 | # TODO adding parameters to the sql report is.. hard.
72 | #query_params = [("username","Username","char")]
73 | namespace="administration"
74 | labels = ('username','user id','actions','oldest','latest')
75 | verbose_name="Admin Activity Report"
76 | slug="admin-activity"
77 |
78 | reportengine.register(AdminActivityReport)
79 |
80 |
--------------------------------------------------------------------------------
/reportengine/management/commands/generate_report.py:
--------------------------------------------------------------------------------
1 | import reportengine
2 | import sys
3 | from django.core.management.base import BaseCommand, CommandError
4 | from optparse import make_option
5 | from reportengine.outputformats import CSVOutputFormat, XMLOutputFormat
6 | from urlparse import parse_qsl
7 |
8 | ## ASSUMPTIONS: We're running this from the command line, so we can ignore
9 | ## - AdminOutputFormat
10 | ## - pagination
11 |
12 | ## TODO: Be more DRY about how the report is generated, including
13 | ## outputformat selection and filters and context creation
14 |
15 | class Command(BaseCommand):
16 | help = 'Run a report'
17 | option_list = BaseCommand.option_list + (
18 | make_option('-n', '--namespace',
19 | dest='namespace',
20 | default=None,
21 | help='Report namespace'
22 | ),
23 | make_option('-r', '--report',
24 | dest='report',
25 | default=None,
26 | help='Name of report'
27 | ),
28 | make_option('-f', '--file',
29 | dest='file',
30 | default=None,
31 | help='Path to file (defaults to sys.stdout)'
32 | ),
33 | make_option('-o', '--format',
34 | dest='format',
35 | default='csv',
36 | help='Output format slug (csv, xml, etc)'
37 | ),
38 | make_option('-q', '--filter',
39 | dest='filter',
40 | default='',
41 | help='Filter args as a querystring (foo=bar&fizz=buzz)'
42 | ),
43 | make_option('-b', '--order-by',
44 | dest='order_by',
45 | default=None,
46 | help='Field to order the report by'
47 | ),
48 | )
49 |
50 | def handle(self, *args, **kwargs):
51 | if not kwargs['namespace'] or not kwargs['report']:
52 | raise CommandError('--namespace and --report are required')
53 |
54 | ## Try to open the file path if specified, default to sys.stdout if it wasn't
55 | if kwargs['file']:
56 | try:
57 | output = file(kwargs['file'], 'w')
58 | except Exception:
59 | raise CommandError('Could not open file path for writing')
60 | else:
61 | output = sys.stdout
62 |
63 | reportengine.autodiscover() ## Populate the reportengine registry
64 | try:
65 | report = reportengine.get_report(kwargs['namespace'], kwargs['report'])()
66 | except Exception, err:
67 | raise CommandError('Could not find report for (%(namespace)s, %(report)s)' % kwargs)
68 |
69 |
70 | ## Parse our filters
71 | request = dict(parse_qsl(kwargs['filter']))
72 | filter_form = report.get_filter_form(request)
73 | if filter_form.fields:
74 | if filter_form.is_valid():
75 | filters = filter_form.cleaned_data
76 | else:
77 | filters = {}
78 | else:
79 | if report.allow_unspecified_filters:
80 | filters = request
81 | else:
82 | filters = {}
83 |
84 | # Remove blank filters
85 | for k in filters.keys():
86 | if filters[k] == '':
87 | del filters[k]
88 |
89 | ## Update the mask and run the report!
90 | mask = report.get_default_mask()
91 | mask.update(filters)
92 | rows, aggregates = report.get_rows(mask, order_by=kwargs['order_by'])
93 |
94 | ## Get our output format, setting a default if one wasn't set or isn't valid for this report
95 | outputformat = None
96 | if output:
97 | for format in report.output_formats:
98 | if format.slug == kwargs['format']:
99 | outputformat = format
100 | if not outputformat:
101 | ## By default, [0] is AdminOutputFormat, so grab the last one instead
102 | outputformat = report.output_formats[-1]
103 |
104 | context = {
105 | 'report': report,
106 | 'title': report.verbose_name,
107 | 'rows': rows,
108 | 'filter_form': filter_form,
109 | 'aggregates': aggregates,
110 | 'paginator': None,
111 | 'cl': None,
112 | 'page': 0,
113 | 'urlparams': kwargs['filter']
114 | }
115 |
116 | outputformat.generate_output(context, output)
117 | output.close()
118 |
119 |
--------------------------------------------------------------------------------
/reportengine/outputformats.py:
--------------------------------------------------------------------------------
1 | from django.core.exceptions import ImproperlyConfigured
2 | from django.shortcuts import render_to_response
3 | from django.template.context import RequestContext
4 | from django.http import HttpResponse
5 | from django.utils.encoding import smart_unicode
6 | import csv
7 | from cStringIO import StringIO
8 | from xml.etree import ElementTree as ET
9 |
10 | ## Exporting to XLS requires the xlwt library
11 | ## http://www.python-excel.org/
12 | try:
13 | import xlwt
14 | XLS_AVAILABLE = True
15 | except ImportError:
16 | XLS_AVAILABLE = False
17 |
18 | class OutputFormat(object):
19 | verbose_name="Abstract Output Format"
20 | slug="output"
21 | no_paging=False
22 |
23 | def generate_output(self, context, output):
24 | ## output is expected to be a file-like object, be it Django Response,
25 | ## StringIO, file, or sys.stdout. Anything sith a .write method should do.
26 | raise NotImplemented("Use a subclass of OutputFormat.")
27 |
28 | def get_response(self,context,request):
29 | raise NotImplemented("Use a subclass of OutputFormat.")
30 |
31 | class AdminOutputFormat(OutputFormat):
32 | verbose_name="Admin Report"
33 | slug="admin"
34 |
35 | def generate_output(self, context, output):
36 | raise NotImplemented("Not necessary for this output format")
37 |
38 | def get_response(self,context,request):
39 | context.update({"output_format":self})
40 | return render_to_response('reportengine/report.html', context,
41 | context_instance=RequestContext(request))
42 |
43 | class CSVOutputFormat(OutputFormat):
44 | verbose_name="CSV (comma separated value)"
45 | slug="csv"
46 | no_paging=True
47 |
48 | # CONSIDER perhaps I could use **kwargs, but it is nice to see quickly what is available..
49 | def __init__(self,quotechar='"',quoting=csv.QUOTE_MINIMAL,delimiter=',',lineterminator='\n'):
50 | self.quotechar=quotechar
51 | self.quoting=quoting
52 | self.delimiter=delimiter
53 | self.lineterminator=lineterminator
54 |
55 | def generate_output(self, context, output):
56 | """
57 | :param context: should be a dictionary with keys 'aggregates' and 'rows' and 'report'
58 | :param output: should be a file-like object to which output can be written?
59 | :return: modified output object
60 | """
61 | w=csv.writer(output,
62 | delimiter=self.delimiter,
63 | quotechar=self.quotechar,
64 | quoting=self.quoting,
65 | lineterminator=self.lineterminator)
66 | for a in context["aggregates"]:
67 | w.writerow([smart_unicode(x).encode('utf8') for x in a])
68 | w.writerow( context["report"].labels)
69 | for r in context["rows"]:
70 | w.writerow([smart_unicode(x).encode('utf8') for x in r])
71 | return output
72 |
73 | def get_response(self,context,request):
74 | resp = HttpResponse(mimetype='text/csv')
75 | # CONSIDER maybe a "get_filename" from the report?
76 | resp['Content-Disposition'] = 'attachment; filename=%s.csv'%context['report'].slug
77 | self.generate_output(context, resp)
78 | return resp
79 |
80 |
81 | class XLSOutputFormat(OutputFormat):
82 | no_paging = True
83 | slug = 'xls'
84 | verbose_name = 'XLS (Microsoft Excel)'
85 |
86 | def generate_output(self, context, output):
87 | if not XLS_AVAILABLE:
88 | raise ImproperlyConfigured('Missing module xlwt.')
89 | ## Put all our data into a big list
90 | rows = []
91 | rows.extend(context['aggregates'])
92 | rows.append(context['report'].labels)
93 | rows.extend(context['rows'])
94 |
95 | ## Create the spreadsheet from our data
96 | workbook = xlwt.Workbook(encoding='utf8')
97 | worksheet = workbook.add_sheet('report')
98 | for row_index, row in enumerate(rows):
99 | for col_index, val in enumerate(row):
100 | if isinstance(val, basestring):
101 | val = smart_unicode(val).encode('utf8')
102 | worksheet.write(row_index, col_index, val)
103 | workbook.save(output)
104 |
105 | def get_response(self, context, request):
106 | resp = HttpResponse(mimetype='application/vnd.ms-excel')
107 | resp['Content-Disposition'] = 'attachment; filename=%s.xls' % context['report'].slug
108 | self.generate_output(context, resp)
109 | return resp
110 |
111 |
112 |
113 | class XMLOutputFormat(OutputFormat):
114 | verbose_name="XML"
115 | slug="xml"
116 | no_paging=True
117 |
118 | def __init__(self,root_tag="output",row_tag="entry",aggregate_tag="aggregate"):
119 | self.root_tag=root_tag
120 | self.row_tag=row_tag
121 | self.aggregate_tag=aggregate_tag
122 |
123 | def generate_output(self, context, output):
124 | root = ET.Element(self.root_tag) # CONSIDER maybe a nicer name or verbose name or something
125 | for a in context["aggregates"]:
126 | ae=ET.SubElement(root,self.aggregate_tag)
127 | ae.set("name",a[0])
128 | ae.text=smart_unicode(a[1])
129 | rows=context["rows"]
130 | labels=context["report"].labels
131 | for r in rows:
132 | e=ET.SubElement(root,self.row_tag)
133 | for l in range(len(labels)):
134 | e1=ET.SubElement(e,labels[l])
135 | e1.text = smart_unicode(r[l])
136 | tree=ET.ElementTree(root)
137 | tree.write(output)
138 |
139 | def get_response(self,context,request):
140 | resp = HttpResponse(mimetype='text/xml')
141 | # CONSIDER maybe a "get_filename" from the report?
142 | resp['Content-Disposition'] = 'attachment; filename=%s.xml'%context['report'].slug
143 | self.generate_output(context, resp)
144 | return resp
145 |
--------------------------------------------------------------------------------
/doc/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = _build
9 |
10 | # 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) .
14 | # the i18n builder cannot share the environment and doctrees with the others
15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
16 |
17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
18 |
19 | help:
20 | @echo "Please use \`make