Commit 5c03c9b8 authored by Joost Rijneveld's avatar Joost Rijneveld
Browse files

Merge branch 'feature/statistics' into 'master'

Add statistics page, refs #9 now feature equivalence with old website

Statistiekenpagina met pie charts die ongeveer dezelfde data laten zien als op de oude site. Resolvt #9 in ieder geval voor de feature equivalence milestone, maar zoals @lrijneveld al aangaf zijn er natuurlijk nog veel meer interessante statistieken toe te voegen aan deze pagina.

See merge request !46
parents b6a420f2 a93109a4
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-09-21 20:03+0200\n"
"PO-Revision-Date: 2016-09-21 20:04+0200\n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Last-Translator: \n"
"Language-Team: \n"
"X-Generator: Poedit 1.8.9\n"
#: members/static/members/js/statistics.js:50
msgid "Members per member type"
msgstr "Leden per lidtype"
#: members/static/members/js/statistics.js:59
msgid "Members"
msgstr "Leden"
#: members/static/members/js/statistics.js:62
msgid "Supporters"
msgstr "Begunstigers"
#: members/static/members/js/statistics.js:65
msgid "Honorary Members"
msgstr "Ereleden"
#: members/static/members/js/statistics.js:76
msgid "Total members per year"
msgstr "Aantal leden per jaar"
#: members/static/members/js/statistics.js:85
#: members/static/members/js/statistics.js:120
#: members/static/members/js/statistics.js:156
msgid "1st year"
msgstr "Eerstejaars"
#: members/static/members/js/statistics.js:88
#: members/static/members/js/statistics.js:123
#: members/static/members/js/statistics.js:159
msgid "2nd year"
msgstr "Tweedejaars"
#: members/static/members/js/statistics.js:91
#: members/static/members/js/statistics.js:126
#: members/static/members/js/statistics.js:162
msgid "3rd year"
msgstr "Derdejaars"
#: members/static/members/js/statistics.js:94
#: members/static/members/js/statistics.js:129
#: members/static/members/js/statistics.js:165
msgid "4th year"
msgstr "Vierdejaars"
#: members/static/members/js/statistics.js:97
#: members/static/members/js/statistics.js:132
#: members/static/members/js/statistics.js:168
msgid "5th year"
msgstr "Vijfdejaars"
#: members/static/members/js/statistics.js:100
#: members/static/members/js/statistics.js:135
#: members/static/members/js/statistics.js:171
msgid ">5th year"
msgstr "Ouderejaars"
#: members/static/members/js/statistics.js:111
msgid "Members per year"
msgstr "Leden per jaar"
#: members/static/members/js/statistics.js:147
msgid "Supporters per year"
msgstr "Begunstigers per jaar"
......@@ -5,7 +5,7 @@ from django.core import validators
from django.conf import settings
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from datetime import timedelta
from datetime import timedelta, date
import operator
from functools import reduce
......@@ -375,3 +375,45 @@ class BecomeAMemberDocument(models.Model):
def __str__(self):
return self.name
def gen_stats_member_type(member_types):
total = dict()
for member_type in member_types:
total[member_type] = (Member
.active_members
.filter(user__membership__type=member_type)
.count())
return total
def gen_stats_year(member_types):
"""
Generate list with 6 entries, where each entry represents the total amount
of Thalia members in a year. The sixth element contains all the multi-year
students.
"""
stats_year = []
current_year = date.today().year
for i in range(5):
new = dict()
for member_type in member_types:
new[member_type] = (Member
.active_members
.filter(starting_year=current_year - i)
.filter(user__membership__type=member_type)
.count())
stats_year.append(new)
# Add multi year members
new = dict()
for member_type in member_types:
new[member_type] = (Member
.active_members
.filter(starting_year__lt=current_year - 4)
.filter(user__membership__type=member_type)
.count())
stats_year.append(new)
return stats_year
This diff is collapsed.
/* Render statistics on statistics page */
var pieOptions = {
allowPointSlect: true,
cursor: 'pointer',
dataLabels : {
enabled: true,
formatter:function() { // Omit zero values
if(this.y != 0) {
return this.point.name + ": " + this.y; // TODO: add newline <br /> ?
}
},
distance: -50
}
};
$(function () {
Highcharts.theme = {
colors: ['#AE0046', '#E62272', '#E6478A', '#CC2482', '#8E1056', '#DC3472'],
title: {
style: {
color: '#000',
font: 'bold 16px "Trebuchet MS", Verdana, sans-serif'
}
},
subtitle: {
style: {
color: '#666666',
font: 'bold 12px "Trebuchet MS", Verdana, sans-serif'
}
},
credits: false, // Free for non-profit
legend: {
itemStyle: {
font: '9pt Trebuchet MS, Verdana, sans-serif',
color: 'black'
},
itemHoverStyle: {
color: 'gray'
}
}
};
// Apply the theme
Highcharts.setOptions(Highcharts.theme);
$('#membersTypeChart').highcharts({
chart: {
type: 'pie'
},
title: {
text: gettext('Members per member type'),
},
plotOptions: {
pie: pieOptions
},
series: [{
name: 'Thalianen',
colorByPoint: true,
data : [{
name: gettext('Members'),
y: total_stats_member_type.member
},{
name: gettext('Supporters'),
y: total_stats_member_type.supporter
},{
name: gettext('Honorary Members'),
y: total_stats_member_type.honorary
}]
}]
});
$('#totalYearChart').highcharts({
chart: {
type: 'pie'
},
title: {
text: gettext("Total members per year"),
},
plotOptions: {
pie: pieOptions
},
series: [{
name: 'Thalianen',
colorByPoint: true,
data : [{
name: gettext("1st year"),
y: total_stats_year[0].member + total_stats_year[0].supporter + total_stats_year[0].honorary
},{
name: gettext("2nd year"),
y: total_stats_year[1].member + total_stats_year[1].supporter + total_stats_year[1].honorary
},{
name: gettext("3rd year"),
y: total_stats_year[2].member + total_stats_year[2].supporter + total_stats_year[2].honorary
},{
name: gettext("4th year"),
y: total_stats_year[3].member + total_stats_year[3].supporter + total_stats_year[3].honorary
},{
name: gettext("5th year"),
y: total_stats_year[4].member + total_stats_year[4].supporter + total_stats_year[4].honorary
},{
name: gettext(">5th year"),
y: total_stats_year[5].member + total_stats_year[5].supporter + total_stats_year[5].honorary
}]
}]
});
$('#membersYearChart').highcharts({
chart: {
type: 'pie'
},
title: {
text: gettext("Members per year"),
},
plotOptions: {
pie: pieOptions
},
series: [{
name: 'Thalianen',
colorByPoint: true,
data : [{
name: gettext("1st year"),
y: total_stats_year[0].member
},{
name: gettext("2nd year"),
y: total_stats_year[1].member
},{
name: gettext("3rd year"),
y: total_stats_year[2].member
},{
name: gettext("4th year"),
y: total_stats_year[3].member
},{
name: gettext("5th year"),
y: total_stats_year[4].member
},{
name: gettext(">5th year"),
y: total_stats_year[5].member
}]
}]
});
$('#supportersYearChart').highcharts({
chart: {
type: 'pie'
},
title: {
text: gettext("Supporters per year"),
},
plotOptions: {
pie: pieOptions
},
series: [{
name: 'Thalianen',
colorByPoint: true,
data : [{
name: gettext("1st year"),
y: total_stats_year[0].supporter
},{
name: gettext("2nd year"),
y: total_stats_year[1].supporter
},{
name: gettext("3rd year"),
y: total_stats_year[2].supporter
},{
name: gettext("4th year"),
y: total_stats_year[3].supporter
},{
name: gettext("5th year"),
y: total_stats_year[4].supporter
},{
name: gettext(">5th year"),
y: total_stats_year[5].supporter
}]
}]
});
});
{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% block title %}{%trans "Statistics" %} - {{ block.super }}{% endblock %}
{% block body %}
<h1>{% trans "Statistics" %}</h1>
<h2>{% trans "Total amount of Thalia members" %}: {{ total_members }}</h2>
<div id="membersTypeChart" style="width:100%; height:400px;"></div>
<div id="totalYearChart" style="width:100%; height:400px;"></div>
<div id="membersYearChart" style="width:100%; height:400px;"></div>
<div id="supportersYearChart" style="width:100%; height:400px;"></div>
<br/><br/>
{% endblock %}
{% block js_head %}
{{ block.super }}
<script src="{% static 'members/js/highcharts.js' %}"></script>
{% endblock %}
{% block js_footer %}
{{ block.super }}
<script>
{% autoescape off %}
var total_stats_year = {{ total_stats_year }};
var total_stats_member_type = {{ total_stats_member_type }};
{% endautoescape %}
</script>
<script src="{% static 'members/js/statistics.js' %}"></script>
{% endblock %}
from datetime import datetime
from datetime import datetime, date, timedelta
from django.test import TestCase
from django.utils import timezone
from django.contrib.auth.models import User
from members.models import Member
from members.models import (Member, Membership,
gen_stats_year, gen_stats_member_type)
class MemberBirthdayTest(TestCase):
......@@ -54,3 +56,155 @@ class MemberBirthdayTest(TestCase):
def test_person_born_in_range_spanning_multiple_years(self):
self._assert_thom('1992-12-31', '1995-01-01')
class StatisticsTest(TestCase):
def setUp(self):
# Add 10 members with default membership
for i in range(10):
u = User(username=i)
u.save()
membership = Membership(user=u, type="member")
membership.save()
m = Member(user=u)
m.save()
def sum_members(self, members, type=None):
s = 0
for i in members:
if type is None:
for j in i.values():
s = s + j
else:
s = s + i[type]
return s
def sum_member_types(self, members):
s = 0
for i in members.values():
s = s + i
return s
def test_gen_stats_year_no_members(self):
member_types = ["member", "supporter", "honorary"]
result = gen_stats_year(member_types)
self.assertEqual(0, self.sum_members(result))
def test_gen_stats_active(self):
"""
Testing if active and non-active objects are counted correctly
"""
member_types = ["member", "supporter", "honorary"]
current_year = date.today().year
# Set start date to current year - 1:
for m in Member.objects.all():
m.starting_year = current_year - 1
m.save()
result = gen_stats_year(member_types)
self.assertEqual(10, self.sum_members(result))
self.assertEqual(10, self.sum_members(result, "member"))
result = gen_stats_member_type(member_types)
self.assertEqual(10, self.sum_member_types(result))
# Change one membership to supporter should decrease amount of members
m = Membership.objects.all()[0]
m.type = "supporter"
m.save()
result = gen_stats_year(member_types)
self.assertEqual(10, self.sum_members(result))
self.assertEqual(9, self.sum_members(result, "member"))
self.assertEqual(1, self.sum_members(result, "supporter"))
result = gen_stats_member_type(member_types)
self.assertEqual(10, self.sum_member_types(result))
self.assertEqual(9, result["member"])
self.assertEqual(1, result["supporter"])
# Same for honorary members
m = Membership.objects.all()[1]
m.type = "honorary"
m.save()
result = gen_stats_year(member_types)
self.assertEqual(10, self.sum_members(result))
self.assertEqual(8, self.sum_members(result, "member"))
self.assertEqual(1, self.sum_members(result, "supporter"))
self.assertEqual(1, self.sum_members(result, "honorary"))
result = gen_stats_member_type(member_types)
self.assertEqual(10, self.sum_member_types(result))
self.assertEqual(8, result["member"])
self.assertEqual(1, result["supporter"])
self.assertEqual(1, result["honorary"])
# Terminate one membership by setting end date to current_year -1,
# should decrease total amount and total members
m = Membership.objects.all()[2]
m.until = timezone.now() - timedelta(days=365)
m.save()
result = gen_stats_year(member_types)
self.assertEqual(9, self.sum_members(result))
self.assertEqual(7, self.sum_members(result, "member"))
self.assertEqual(1, self.sum_members(result, "supporter"))
self.assertEqual(1, self.sum_members(result, "honorary"))
result = gen_stats_member_type(member_types)
self.assertEqual(9, self.sum_member_types(result))
self.assertEqual(7, result["member"])
self.assertEqual(1, result["supporter"])
self.assertEqual(1, result["honorary"])
def test_gen_stats_different_years(self):
member_types = ["member", "supporter", "honorary"]
current_year = date.today().year
# one first year student
m = Member.objects.all()[0]
m.starting_year = current_year
m.save()
# one second year student
m = Member.objects.all()[1]
m.starting_year = current_year - 1
m.save()
# no third year students
# one fourth year student
m = Member.objects.all()[2]
m.starting_year = current_year - 3
m.save()
# no fifth year students
# one >5 year student
m = Member.objects.all()[3]
m.starting_year = current_year - 5
m.save()
# 4 active members
result = gen_stats_year(member_types)
self.assertEqual(4, self.sum_members(result))
self.assertEqual(4, self.sum_members(result, "member"))
# one first year student
self.assertEqual(1, result[0]['member'])
# one second year student
self.assertEqual(1, result[1]['member'])
# no third year students
self.assertEqual(0, result[2]['member'])
# one fourth year student
self.assertEqual(1, result[3]['member'])
# no fifth year students
self.assertEqual(0, result[4]['member'])
# one >5 year student
self.assertEqual(1, result[5]['member'])
import os
from datetime import date
from sendfile import sendfile
import json
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.shortcuts import get_object_or_404, render
from django.contrib.auth.decorators import login_required
from django.utils.text import slugify
from sendfile import sendfile
from .models import BecomeAMemberDocument
from .models import Member
from . import models
from .forms import MemberForm
......@@ -28,7 +28,7 @@ def index(request):
start_year += 1
year_range = reversed(range(start_year, date.today().year + 1))
members = Member.objects.all()
members = models.Member.objects.all()
if query_filter and query_filter.isdigit() and not (
query_filter == 'ex' or
query_filter == 'honor' or
......@@ -90,9 +90,9 @@ def index(request):
@login_required
def profile(request, pk=None):
if pk:
member = get_object_or_404(Member, pk=int(pk))
member = get_object_or_404(models.Member, pk=int(pk))
else:
member = get_object_or_404(Member, user=request.user)
member = get_object_or_404(models.Member, user=request.user)
# Group the memberships under the committees for easier template rendering
memberships = member.committeemembership_set.all()
......@@ -114,7 +114,8 @@ def profile(request, pk=None):
'chair': membership.chair
}]
}
achievements[name]['periods'].sort(key=lambda period: period['since'])
achievements[name]['periods'].sort(
key=lambda period: period['since'])
mentor_years = member.mentorship_set.all()
for mentor_year in mentor_years:
......@@ -135,7 +136,7 @@ def account(request):
@login_required
def edit_profile(request):
member = get_object_or_404(Member, user=request.user)
member = get_object_or_404(models.Member, user=request.user)
saved = False
if request.POST:
......@@ -151,12 +152,30 @@ def edit_profile(request):
def become_a_member(request):
context = {'documents': BecomeAMemberDocument.objects.all()}
context = {'documents': models.BecomeAMemberDocument.objects.all()}
return render(request, 'singlepages/become_a_member.html', context)
def get_become_a_member_document(request, pk):
document = get_object_or_404(BecomeAMemberDocument, pk=int(pk))
document = get_object_or_404(models.BecomeAMemberDocument, pk=int(pk))
ext = os.path.splitext(document.file.path)[1]
return sendfile(request, document.file.path, attachment=True,
return sendfile(request,
document.file.path,
attachment=True,
attachment_filename=slugify(document.name) + ext)
def statistics(request):
member_types = ("member", "supporter", "honorary")
# The numbers
total = models.Member.active_members.count()