Commits

Blue committed 074f905

Added a small stats app.

Comments (0)

Files changed (11)

example_project/exproj/settings.py

 
     'exproj.index',
     'spotnet',
+    'spotnet.stats',
 )
 if not django_version < (1, 3):
     INSTALLED_APPS += ('django.contrib.staticfiles',)

example_project/exproj/urls.py

 
 urlpatterns = patterns('',
     url(r'^$', 'exproj.index.views.index'),
+    url(r'^stats/', include('spotnet.stats.urls', namespace='spotnet-stats')),
     url(r'^spotnet/', include('spotnet.urls', namespace='spotnet')),
 
     # Uncomment the admin/doc line below to enable admin documentation:
         'spotnet.management',
         'spotnet.management.commands',
         'spotnet.tests',
+        'spotnet.stats',
     ],
     package_data={'spotnet': [
         'templates/spotnet/*.html',

spotnet/stats/__init__.py

Empty file added.

spotnet/stats/functions.py

+from datetime import date, timedelta
+from django.utils.translation import ugettext as _
+from spotnet.models import Post
+from spotnet.settings import CATEGORY_MAPPING
+
+
+def posts_per_day():
+    """Returns the number of posts for each day.
+
+    Returns an iterable of tuples (year, day-of-year, number-of-posts)
+    for each day the database has posts.
+    """
+
+    from django.db import connection, transaction
+    cursor = connection.cursor()
+
+    if cursor.db.vendor == 'sqlite':
+        query = """
+SELECT
+  django_extract('year',  "posted") AS year,
+  django_extract('month', "posted") AS month,
+  django_extract('day',   "posted") AS day,
+  COUNT(*) AS posts
+FROM %s
+GROUP BY year, month, day
+ORDER BY year, month, day""" % (Post._meta.db_table, )
+    elif cursor.db.vendor == 'mysql':
+        query = """
+SELECT
+  YEAR(posted)  AS year,
+  MONTH(posted) AS month,
+  DAY(posted)   AS day,
+  COUNT(*) AS posts
+FROM %s
+GROUP BY year, month, day
+ORDER BY year, month, day""" % (Post._meta.db_table, )
+    elif cursor.db.vendor == 'postgresql':
+        query = """
+SELECT
+  EXTRACT(YEAR  FROM posted) AS year,
+  EXTRACT(MONTH FROM posted) AS month,
+  EXTRACT(DAY   FROM posted) AS day,
+  COUNT(*) AS posts
+FROM %s
+GROUP BY year, month, day
+ORDER BY year, month, day""" % (Post._meta.db_table, )
+    else:
+        raise Exception("Unsupported database type")
+
+    cursor.execute(query)
+    data = cursor.fetchall()
+
+    date_start = date(*data[0][0:3])
+    date_end  = date(*data[-1][0:3])
+    dayspan = (date_end - date_start).days
+    one_day = timedelta(days=1)
+
+    postings = []
+    total_posts = 0
+    prev_date = date_start - one_day
+    for year, month, day, posts in data:
+        cur_date = date(year, month, day)
+        while (cur_date - prev_date).days > 1:
+            postings.append(0)
+            prev_date += one_day
+        postings.append(posts)
+        total_posts += posts
+        prev_date = cur_date
+
+    assert len(postings) == dayspan + 1
+
+    return postings, date_start, date_end, total_posts, dayspan
+
+
+def posts_per_category():
+    from django.db import connection, transaction
+    cursor = connection.cursor()
+
+    if cursor.db.vendor == 'postgresql':
+        query = """
+SELECT
+  category,
+  COUNT(*) AS posts,
+  SUM(size) AS size
+FROM %s
+GROUP BY category
+ORDER BY posts DESC""" % (Post._meta.db_table, )
+    else:
+        raise Exception("Unsupported database type")
+
+    cursor.execute(query)
+    data = cursor.fetchall()
+
+    results = []
+    unknown_category = (0, 0)
+    for cat, posts, size in data:
+        if cat is None:
+            assert unknown_category == (0, 0)
+            unknown_category = (posts, size)
+        else:
+            results.append((CATEGORY_MAPPING.get(cat, _('Invalid')), posts, size))
+
+    results.append((_('Unknown'), ) + unknown_category)
+
+    return results

spotnet/stats/models.py

Empty file added.

spotnet/stats/templates/spotnet_stats/daily_posts.html

+{% extends "spotnet_stats/index.html" %}
+{% load i18n %}
+
+
+{% block content %}
+  <style>
+
+    path {
+      fill: steelblue;
+    }
+
+    .axis text {
+      font: 10px sans-serif;
+      text-shadow: 0px 0px 3px white;
+    }
+
+    .axis path, .axis line {
+      fill: none;
+      stroke: #000;
+      shape-rendering: crispEdges;
+    }
+
+  </style>
+  <h2>{% trans "Daily posts" %}</h2>
+  <div class="gallery" id="chart" style="width: 100%;"></div>
+
+<!--  <script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/d3/2.10.0/d3.v2.min.js"></script> -->
+  <script type="text/javascript" src="http://d3js.org/d3.v2.js"></script>
+
+  <script>
+
+{% comment %}
+  var data = {
+{% for year, months in postings.iteritems %}
+      {{ year }}: {
+  {% for month, days in months.iteritems %}
+          {{ month }}: {
+    {% for day, posts in days.iteritems %}
+              {{ day}}: {{ posts }},
+    {% endfor %}
+          },
+  {% endfor %}
+      },
+{% endfor %}
+  };
+
+
+  var data = [
+{% for year, months in postings.iteritems %}
+  {% for month, days in months.iteritems %}
+    {% for day, posts in days.iteritems %}
+//      {"{{ year }} {{ month }} {{ day }}":
+      {{ posts }},
+    {% endfor %}
+  {% endfor %}
+{% endfor %}
+  ];
+
+{% endcomment %}
+
+
+var data = {{ postings_list }};
+
+// months minus one because js expects the range [0, 11]
+var date_start = new Date({{ date_start.year }}, {{ date_start.month }} - 1, {{ date_start.day }});
+var date_end = new Date({{ date_end.year }}, {{ date_end.month }} - 1, {{ date_end.day }});
+
+
+
+
+var n = 1, // number of layers
+    m = {{ dayspan }}, // number of samples per layer
+    stack = d3.layout.stack().offset("zero")(data_to_stream()),
+    margin = {top: 10, right: 10, bottom: 10, left: 10};
+
+margin.vert = margin.top  + margin.bottom;
+margin.horz = margin.left + margin.right;
+
+var width = document.getElementById('chart').offsetWidth,
+    height = width / 2.3,
+    mx = m - 1,
+    // the maximum value across all layers
+    my = d3.max(stack, function(d) {
+      return d3.max(d, function(d) {
+        return d.y0 + d.y;
+      });
+    });
+
+my = 1200; // To clip that one huge value
+
+var area = d3.svg.area()
+    // the x coord for a value
+    .x(function(d) { return d.x * (width - margin.horz) / mx + margin.left; })
+    // the y coord for the bottom of t
+    .y0(function(d) { return height - d.y0 * (height - margin.vert) / my - margin.bottom; })
+    .y1(function(d) { return height - (d.y + d.y0) * (height - margin.vert) / my - margin.bottom; });
+
+var vis = d3.select("#chart")
+  .append("svg")
+//    .attr("width",  width;
+    .attr("height", height);
+
+vis.selectAll("path")
+    .data(stack)
+  .enter().append("path")
+    .attr("d", area);
+
+
+
+
+function data_to_stream() {
+    var stream = []
+    for (n = 0; n < data.length; n++) {
+        stream[n] = {
+            x: n,
+            y: data[n], // < 2000 ? data[n] : 1500,
+        };
+    }
+    return [stream];
+}
+
+
+
+
+
+//format = d3.time.format("%b %Y");
+//date = format(date_str);
+
+
+
+var scale = d3.time.scale() //.utc()
+    .domain([date_start, date_end])
+    .range([margin.left, width - margin.right]);
+    //.nice(d3.time.days.utc)
+
+var xAxis = d3.svg.axis()
+    .scale(scale)
+    .orient("top")
+    .tickPadding(8);
+
+  vis.append("g")
+      .attr("class", "x axis")
+      .attr("transform", "translate(0," + (height - margin.bottom) + ")")
+      .call(xAxis);
+
+
+
+var yscale = d3.scale.linear()
+    .domain([my, 0])
+    .range([margin.bottom, height - margin.top]); // pixel height
+
+var yAxis = d3.svg.axis()
+    .scale(yscale)
+    .orient("right")
+    .tickPadding(8);
+
+  vis.append("g")
+      .attr("class", "y axis")
+      .attr("transform", "translate(" + margin.left + ")")
+      .call(yAxis);
+
+
+  </script>
+{% endblock %}

spotnet/stats/templates/spotnet_stats/general.html

+{% extends "spotnet_stats/index.html" %}
+{% load i18n %}
+
+
+{% block content %}
+  <h2>{% trans "General spotnet statistics" %}</h2>
+  <table>
+    <tr>
+      <th>{% trans "Total posts" %}</th>
+      <td>{{ total_posts }}</td>
+    </tr>
+    <tr>
+      <th>{% trans "Total downloadable" %}</th>
+      <td>{{ total_filesize|filesizeformat }}</td>
+    </tr>
+    <tr>
+      <th>{% trans "First post" %}</th>
+      <td>{{ first_post }}</td>
+    </tr>
+    <tr>
+      <th>{% trans "Last post" %}</th>
+      <td>{{ last_post }} ({{ last_post|timesince }} {% trans "ago" %})</td>
+    </tr>
+    <tr>
+      <th>{% trans "X" %}</th>
+      <td></td>
+    </tr>
+  </table>
+
+  <h2>{% trans "Posts per category" %}</h2>
+  <table>
+{% for cat, posts, filesize in category_posts %}
+    <tr>
+      <th>{{ cat|capfirst }}</th>
+      <td>{{ posts }}</td>
+      <td>{{ filesize|filesizeformat }}</td>
+    </tr>
+{% endfor %}
+  </table>
+{% endblock %}

spotnet/stats/templates/spotnet_stats/index.html

+<!doctype html>{% load i18n %}
+<html>
+<head>
+  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+  <title>Spotnet test project</title>
+</head>
+<body>
+  <h1>{% trans "Spotnet statistics" %}</h1>
+  <ul>
+    <li><a href="{% url spotnet-stats:general %}">General statistics</a></li>
+    <li><a href="{% url spotnet-stats:daily-posts %}">Posts per day</a></li>
+  </ul>
+{% block content %}
+{% endblock %}
+</body>
+</html>

spotnet/stats/urls.py

+try:
+    from django.conf.urls import patterns, include, url
+except ImportError as e:
+    # django 1.3 compatibility
+    try:
+        from django.conf.urls.defaults import patterns, include, url
+    except ImportError:
+        raise e
+
+
+urlpatterns = patterns('spotnet.stats.views',
+    url(r'^$', 'index', name='index'),
+    url(r'^general/$', 'general', name='general'),
+    url(r'^daily-posts/$', 'daily_posts', name='daily-posts'),
+)

spotnet/stats/views.py

+from django.utils.translation import ugettext as _
+try:
+    from django.shortcuts import render
+except ImportError:
+    # for django==1.2 compatibility
+    from django.shortcuts import render_to_response
+    from django.template import RequestContext
+    def render(request, template, context):
+        return render_to_response(
+            template,
+            context,
+            context_instance=RequestContext(request),
+        )
+
+from django.db.models import Sum
+from functions import posts_per_day, posts_per_category
+from spotnet.models import Post
+
+
+def index(request):
+    return render(
+        request,
+        'spotnet_stats/index.html', 
+        {},
+    )
+
+
+def general(request):
+    return render(
+        request,
+        'spotnet_stats/general.html',
+        dict(
+            total_posts = Post.objects.count(),
+            category_posts = posts_per_category(),
+            total_filesize = Post.objects.aggregate(s=Sum('size'))['s'],
+            first_post = Post.objects.order_by('posted').only('posted')[0].posted,
+            last_post = Post.objects.order_by('-posted').only('posted')[0].posted,
+        ),
+    )
+
+def daily_posts(request):
+    postings, date_start, date_end, total_posts, dayspan = posts_per_day()
+    return render(
+        request,
+        'spotnet_stats/daily_posts.html',
+        dict(
+            postings = postings,
+            postings_list = '[%s]' % ','.join(str(x) for x in postings),
+            total_posts = total_posts,
+
+            date_start = date_start,
+            date_end = date_end,
+            dayspan = dayspan,
+        ),
+    )