Closes #155
Signed-off-by: Richard Marko <rmarko(a)redhat.com>
---
configure.ac | 2 +
pyfaf/hub/Makefile.am | 2 +-
pyfaf/hub/media/style.css | 9 +++
pyfaf/hub/menu.py | 7 +++
pyfaf/hub/settings.py | 1 +
pyfaf/hub/stats/Makefile.am | 2 +
pyfaf/hub/stats/__init__.py | 1 +
pyfaf/hub/stats/urls.py | 42 +++++++++++++
pyfaf/hub/stats/views.py | 112 +++++++++++++++++++++++++++++++++
pyfaf/hub/templates/Makefile.am | 2 +-
pyfaf/hub/templates/stats/Makefile.am | 4 ++
pyfaf/hub/templates/stats/by_date.html | 71 +++++++++++++++++++++
pyfaf/hub/urls.py | 1 +
13 files changed, 254 insertions(+), 2 deletions(-)
create mode 100644 pyfaf/hub/stats/Makefile.am
create mode 100644 pyfaf/hub/stats/__init__.py
create mode 100644 pyfaf/hub/stats/urls.py
create mode 100644 pyfaf/hub/stats/views.py
create mode 100644 pyfaf/hub/templates/stats/Makefile.am
create mode 100644 pyfaf/hub/templates/stats/by_date.html
diff --git a/configure.ac b/configure.ac
index 9c619c0..b0117a9 100644
--- a/configure.ac
+++ b/configure.ac
@@ -57,6 +57,7 @@ AC_CONFIG_FILES([
pyfaf/hub/problems/Makefile
pyfaf/hub/reports/Makefile
pyfaf/hub/summary/Makefile
+ pyfaf/hub/stats/Makefile
pyfaf/hub/status/Makefile
pyfaf/hub/dumpdirs/Makefile
pyfaf/hub/templates/Makefile
@@ -64,6 +65,7 @@ AC_CONFIG_FILES([
pyfaf/hub/templates/problems/Makefile
pyfaf/hub/templates/reports/Makefile
pyfaf/hub/templates/summary/Makefile
+ pyfaf/hub/templates/stats/Makefile
pyfaf/hub/templates/status/Makefile
pyfaf/hub/templates/dumpdirs/Makefile
pyfaf/hub/templatetags/Makefile
diff --git a/pyfaf/hub/Makefile.am b/pyfaf/hub/Makefile.am
index 1f1fadb..f6fa88a 100644
--- a/pyfaf/hub/Makefile.am
+++ b/pyfaf/hub/Makefile.am
@@ -1,4 +1,4 @@
-SUBDIRS = templates xmlrpc summary problems media reports status common templatetags
services query dumpdirs
+SUBDIRS = templates xmlrpc summary problems media reports status common templatetags
services query dumpdirs stats
hub_PYTHON = hub.wsgi __init__.py menu.py settings.py urls.py
hubdir = $(pythondir)/pyfaf/hub
diff --git a/pyfaf/hub/media/style.css b/pyfaf/hub/media/style.css
index 381f522..c01a1fc 100644
--- a/pyfaf/hub/media/style.css
+++ b/pyfaf/hub/media/style.css
@@ -167,3 +167,12 @@ select, input[type="file"] {
#diff-choice select {
width: 100px;
}
+
+/* statistics */
+#date_stats table div.progress {
+ margin-bottom: 0px;
+}
+
+#date_stats table.comps th:nth-child(1) { width: auto; }
+#date_stats table.comps th:nth-child(2) { width: 50px; }
+#date_stats table.comps th:nth-child(3) { width: 130px; }
diff --git a/pyfaf/hub/menu.py b/pyfaf/hub/menu.py
index df5b72f..76263a2 100644
--- a/pyfaf/hub/menu.py
+++ b/pyfaf/hub/menu.py
@@ -232,6 +232,13 @@ menu = (
placeholder=True, url_args_pattern=[42, 42]),
StaffMenuItem("New", "pyfaf.hub.reports.views.new",
on_right=True),
)),
+ MenuItem("Statistics", "today_stats", menu=(
+ MenuItem("Today", "today_stats"),
+ MenuItem("Yesterday", "yesterday_stats"),
+ MenuItem("Last week", "last_week_stats"),
+ MenuItem("Last month", "last_month_stats"),
+ MenuItem("Last year", "last_year_stats"),
+ )),
StaffMenuItem("Status", "pyfaf.hub.status.views.index",
menu=(
MenuItem("Overview", "pyfaf.hub.status.views.index"),
MenuItem("Builds and Packages",
"pyfaf.hub.status.views.builds"),
diff --git a/pyfaf/hub/settings.py b/pyfaf/hub/settings.py
index 38e75fa..a7684d7 100644
--- a/pyfaf/hub/settings.py
+++ b/pyfaf/hub/settings.py
@@ -165,6 +165,7 @@ INSTALLED_APPS = (
'pyfaf.hub.reports',
'pyfaf.hub.problems',
'pyfaf.hub.services',
+ 'pyfaf.hub.stats',
)
AUTHENTICATION_BACKENDS = (
diff --git a/pyfaf/hub/stats/Makefile.am b/pyfaf/hub/stats/Makefile.am
new file mode 100644
index 0000000..640dcc2
--- /dev/null
+++ b/pyfaf/hub/stats/Makefile.am
@@ -0,0 +1,2 @@
+dumpdirs_PYTHON = __init__.py urls.py views.py
+dumpdirsdir = $(pythondir)/pyfaf/hub/stats
diff --git a/pyfaf/hub/stats/__init__.py b/pyfaf/hub/stats/__init__.py
new file mode 100644
index 0000000..ea30561
--- /dev/null
+++ b/pyfaf/hub/stats/__init__.py
@@ -0,0 +1 @@
+#empty
diff --git a/pyfaf/hub/stats/urls.py b/pyfaf/hub/stats/urls.py
new file mode 100644
index 0000000..93dbc7f
--- /dev/null
+++ b/pyfaf/hub/stats/urls.py
@@ -0,0 +1,42 @@
+import datetime
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns(
+ 'pyfaf.hub.stats.views',
+
+ url(r'^$', 'by_daterange'),
+
+ url(r'^today/$', 'by_daterange',
+ {'since': datetime.date.today(),
+ 'to': datetime.date.today() + datetime.timedelta(days=1)},
+ name='today_stats'),
+
+ url(r'^yesterday/$', 'by_daterange',
+ {'since': datetime.date.today() - datetime.timedelta(days=1),
+ 'to': datetime.date.today()},
+ name='yesterday_stats'),
+
+ url(r'^last_week/$', 'by_daterange',
+ {'since': datetime.date.today() - datetime.timedelta(days=7),
+ 'to': datetime.date.today()},
+ name='last_week_stats'),
+
+ url(r'^last_month/$', 'by_daterange',
+ {'since': datetime.date.today() - datetime.timedelta(days=30),
+ 'to': datetime.date.today()},
+ name='last_month_stats'),
+
+ url(r'^last_year/$', 'by_daterange',
+ {'since': datetime.date.today() - datetime.timedelta(days=365),
+ 'to': datetime.date.today()},
+ name='last_year_stats'),
+
+ url(r'^date/(?P<year>\d+)/(?P<month>\d+)/(?P<day>\d+)/$',
'by_date',
+ name='day_stats'),
+
+ url(r'^date/(?P<year>\d+)/(?P<month>\d+)/$', 'by_date',
+ name='month_stats'),
+
+ url(r'^date/(?P<year>\d+)/$', 'by_date',
+ name='year_stats'),
+)
diff --git a/pyfaf/hub/stats/views.py b/pyfaf/hub/stats/views.py
new file mode 100644
index 0000000..3d32710
--- /dev/null
+++ b/pyfaf/hub/stats/views.py
@@ -0,0 +1,112 @@
+import datetime
+
+import pyfaf
+from pyfaf import queries
+
+from django.template import RequestContext
+from django.shortcuts import render_to_response
+
+
+def by_daterange(request, since, to,
+ template_name='stats/by_date.html',
+ extra_context={}):
+ '''
+ Render date-based report statistics including reports `since` date
+ until `to` date.
+
+ View accepts `template_name` to be used and `extra_context` to pass
+ to it.
+ '''
+ db = pyfaf.storage.getDatabase()
+ since = min(since, to)
+ to = max(since, to)
+
+ history = 'daily'
+ day_count = (to - since).days
+ if day_count > 30:
+ history = 'weekly'
+ if day_count > 360:
+ history = 'monthly'
+
+ def date_filter(query):
+ return query.filter(hist_field >= since).filter(hist_field < to)
+
+ hist_table, hist_field = queries.get_history_target(history)
+ total_query = queries.query_history_sum(db, history=history)
+ total = date_filter(total_query).one()[0]
+
+ release_data = []
+
+ for release in queries.query_releases(db):
+ release_sum = queries.query_history_sum(
+ db, release.opsys.name, release.version, history=history)
+
+ release_sum = date_filter(release_sum).one()[0]
+ if not release_sum:
+ continue
+
+ percentage = int(release_sum * 100.0 / total)
+
+ comps = queries.query_report_count_per_component(
+ db, release.opsys.name, release.version, history=history)
+
+ comp_data = []
+ for comp, count in date_filter(comps).all():
+ comp_percentage = int(count * 100.0 / release_sum)
+ comp_data.append((comp, count, comp_percentage))
+
+ release_data.append({
+ 'release': release,
+ 'sum': release_sum,
+ 'comps': comp_data,
+ 'percentage': percentage,
+ })
+
+ data = {
+ 'since': since,
+ 'to': to,
+ 'total': total,
+ 'releases': sorted(release_data, key=lambda x: x['sum'],
reverse=True),
+ }
+ data.update(extra_context)
+
+ return render_to_response(template_name,
+ data,
+ context_instance=RequestContext(request))
+
+
+def by_date(request, year, month=None, day=None,
+ template_name='stats/by_date.html',
+ extra_context={}):
+ '''
+ Render date-based report statistics including reports for passed
+ `year`, optionaly narrowed to specific `month` and `day`.
+
+ View accepts `template_name` to be used and `extra_context` to pass
+ to it.
+ '''
+
+ year = int(year)
+ if month:
+ month = int(month)
+ if day:
+ day = int(day)
+
+ since = datetime.date(year, 1, 1)
+ add = 365
+
+ if month and month in range(1, 12):
+ since = since.replace(month=month)
+ add = 30
+
+ if day and day in range(1, 31):
+ try:
+ since = since.replace(day=day)
+ add = 1
+ except ValueError:
+ pass
+
+ to = since + datetime.timedelta(days=add)
+ return by_daterange(request, since=since, to=to,
+ template_name=template_name,
+ extra_context=extra_context)
diff --git a/pyfaf/hub/templates/Makefile.am b/pyfaf/hub/templates/Makefile.am
index 229962d..3e6c1b3 100644
--- a/pyfaf/hub/templates/Makefile.am
+++ b/pyfaf/hub/templates/Makefile.am
@@ -1,4 +1,4 @@
-SUBDIRS = openid problems reports summary status dumpdirs
+SUBDIRS = openid problems reports summary status dumpdirs stats
template_DATA = base.html report_graph.html filter_form.html paginator.html \
404.html external_links.html metric_table.html
diff --git a/pyfaf/hub/templates/stats/Makefile.am
b/pyfaf/hub/templates/stats/Makefile.am
new file mode 100644
index 0000000..d94f9ce
--- /dev/null
+++ b/pyfaf/hub/templates/stats/Makefile.am
@@ -0,0 +1,4 @@
+stats_DATA = by_date.html
+statsdir = $(pythondir)/pyfaf/hub/templates/stats
+
+EXTRA_DIST = $(stats_DATA)
diff --git a/pyfaf/hub/templates/stats/by_date.html
b/pyfaf/hub/templates/stats/by_date.html
new file mode 100644
index 0000000..c5f052b
--- /dev/null
+++ b/pyfaf/hub/templates/stats/by_date.html
@@ -0,0 +1,71 @@
+{% extends "base.html" %}
+
+{% block title %}Statistics{% endblock %}
+
+{% block js %}
+ <script type="text/javascript"
src="/js/bootstrap/bootstrap.js"></script>
+{% endblock %}
+
+{% block content %}
+<div class='row' id='date_stats'>
+ <div class='span3'>
+ <ul class='nav nav-tabs nav-stacked'>
+ {% for release in releases %}
+ <li>
+ <a href="#{{ release.release|slugify }}">
+ {{ release.release }}</a>
+ </li>
+ {% endfor %}
+ </ul>
+ </div>
+
+ <div class='span9'>
+ <h3>Summary</h3>
+ <p>
+ <dl class='dl-horizontal'>
+ <dt>Since date:</dt>
+ <dd> {{ since|date:"Y-m-d" }} </dd>
+ <dt>To date:</dt>
+ <dd> {{ to|date:"Y-m-d" }} </dd>
+ <dt>Reports:</dt>
+ <dd> {{ total|default_if_none:"No reports" }} </dd>
+ </dl>
+ </p>
+
+ {% for release in releases %}
+ <hr />
+ <div class='row' id='{{ release.release|slugify }}'>
+ <div class='span4'>
+ <h3>{{ release.release }}</h3>
+ <dl class='dl-horizontal'>
+ <dt>Reports:</dt>
+ <dd>{{ release.sum }} ({{ release.percentage }}%)</dd>
+ <dt>Components:</dt>
+ <dd>{{ release.comps|length }}</dd>
+ </dl>
+ </div>
+ <div class='span5'>
+ <table class='comps table table-striped table-bordered'>
+ <tr>
+ <th>Component</th>
+ <th>Count</th>
+ <th>Share</th>
+ </tr>
+ {% for comp, count, percentage in release.comps %}
+ <tr>
+ <td>{{ comp }}</td>
+ <td>{{ count }}</td>
+ <td>
+ <div class="progress progress-info progress-striped">
+ <div class="bar" style="width: {{ percentage
}}%"></div>
+ </div>
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+ </div>
+ {% endfor %}
+ </div>
+</div>
+{% endblock %}
diff --git a/pyfaf/hub/urls.py b/pyfaf/hub/urls.py
index 2503aa3..e26ad99 100644
--- a/pyfaf/hub/urls.py
+++ b/pyfaf/hub/urls.py
@@ -16,6 +16,7 @@ urlpatterns = patterns('',
# (r'^admin/doc/', include('django.contrib.admindocs.urls')),
url(r'^summary/', include('pyfaf.hub.summary.urls')),
+ url(r'^stats/', include('pyfaf.hub.stats.urls')),
url(r'^$', 'pyfaf.hub.summary.views.index'),
url(r'^problems/', include('pyfaf.hub.problems.urls')),
url(r'^query/', include('pyfaf.hub.query.urls')),
--
1.8.1.4