Commit da49820e authored by Thijs de Jong's avatar Thijs de Jong

Merge branch 'change-stats-graphs' into 'master'

Change graph types on statistics page

See merge request !1356
parents 551443e1 f07ae389
......@@ -56,6 +56,14 @@ activemembers.models module
:undoc-members:
:show-inheritance:
activemembers.services module
-----------------------------
.. automodule:: activemembers.services
:members:
:undoc-members:
:show-inheritance:
activemembers.sitemaps module
-----------------------------
......
from django.db.models import Count, Q
from django.utils import timezone
from activemembers.models import Committee
def generate_statistics():
"""
Generate statistics about number of members in each committee
:return: Dict with key, value being resp. name, member count of committees.
"""
committees = Committee.active_objects.annotate(member_count=(
Count('members', filter=(
Q(membergroupmembership__until=None) |
Q(membergroupmembership__until__gte=timezone.now())))))
data = {}
for committee in committees:
data.update({
committee.name: committee.member_count
})
return data
from collections import OrderedDict
from django.utils import timezone
from django.utils.datetime_safe import date
from django.utils.translation import ugettext_lazy as _, get_language
from events import emails
from events.exceptions import RegistrationError
from events.models import Registration, RegistrationInformationField
from events.models import Registration, RegistrationInformationField, Event
from payments.models import Payment
from utils.snippets import datetime_to_lectureyear
def is_user_registered(member, event):
......@@ -286,3 +288,24 @@ def update_registration_by_organiser(registration, member, data):
registration.present = data['present']
registration.save()
def generate_category_statistics():
"""
Generate statistics about events, number of events per category
:return: Dict with key, value resp. being category, event count.
"""
year = datetime_to_lectureyear(timezone.now())
data = {}
for i in range(5):
year_start = date(year=year - i, month=9, day=1)
year_end = date(year=year - i + 1, month=9, day=1)
data[str(year - i)] = {
str(display): Event.objects.filter(category=key,
start__gte=year_start,
end__lte=year_end).count()
for key, display in Event.EVENT_CATEGORIES
}
return data
"""Services defined in the members package"""
from datetime import date
from typing import Callable, List, Dict, Union, Any
from typing import Callable, List, Dict, Any
from django.db.models import Q, Count
from django.utils import timezone
......@@ -12,7 +12,7 @@ from utils.snippets import datetime_to_lectureyear
def _member_group_memberships(
member: Member, condition: Callable[[Membership], bool]
member: Member, condition: Callable[[Membership], bool]
) -> Dict[str, Any]:
"""
Determines the group membership of a user based on a condition
......@@ -85,60 +85,58 @@ def member_societies(member) -> List:
return sorted(societies.values(), key=lambda x: x['earliest'])
def gen_stats_member_type(member_types) -> Dict[str, int]:
def gen_stats_member_type() -> Dict[str, int]:
"""
Generate a dictionary where every key is a member type with
the value being the number of current members of that type
"""
total = dict()
for member_type in member_types:
total[member_type] = (Membership
data = {}
for key, display in Membership.MEMBERSHIP_TYPES:
data[str(display)] = (Membership
.objects
.filter(since__lte=date.today())
.filter(Q(until__isnull=True) |
Q(until__gt=date.today()))
.filter(type=member_type)
.filter(type=key)
.count())
return total
return data
def gen_stats_year(
member_types) -> List[Dict[Union[str, Any], Union[int, Any]]]:
def gen_stats_year() -> Dict[str, Dict[str, int]]:
"""
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 = []
stats_year = {}
current_year = datetime_to_lectureyear(date.today())
for i in range(5):
new = dict()
new['cohort'] = current_year - i
for member_type in member_types:
new[member_type] = (
new = {}
for key, _ in Membership.MEMBERSHIP_TYPES:
new[key] = (
Membership.objects
.filter(user__profile__starting_year=current_year - i)
.filter(since__lte=date.today())
.filter(Q(until__isnull=True) |
Q(until__gt=date.today()))
.filter(type=member_type)
.filter(type=key)
.count())
stats_year.append(new)
stats_year[str(current_year - i)] = new
# Add multi year members
new = dict()
new['cohort'] = gettext('Older')
for member_type in member_types:
new[member_type] = (
new = {}
for key, _ in Membership.MEMBERSHIP_TYPES:
new[key] = (
Membership.objects
.filter(user__profile__starting_year__lt=current_year - 4)
.filter(since__lte=date.today())
.filter(Q(until__isnull=True) |
Q(until__gt=date.today()))
.filter(type=member_type)
.filter(type=key)
.count())
stats_year.append(new)
stats_year[str(gettext('Older'))] = new
return stats_year
......
......@@ -12,24 +12,28 @@
<h2 class="text-center mb-4">{% trans "Total amount of Thalia members" %}: {{ total_members }}</h2>
<div class="row">
<div class="col-sm-6 col-lg-4">
<div class="col-12 col-md-6">
<canvas id="members-type-chart"></canvas>
</div>
<div class="col-sm-6 col-lg-4">
<div class="col-12 col-md-6">
<canvas id="total-year-chart"></canvas>
</div>
<div class="col-sm-6 col-lg-4">
<canvas id="members-year-chart"></canvas>
<div class="col-12 my-5">
<canvas id="committees-members-chart"></canvas>
</div>
<div class="col-sm-6 col-lg-4">
<canvas id="benefactors-year-chart"></canvas>
<div class="col-12 mb-5">
<canvas id="event-category-chart"></canvas>
</div>
<div class="col-sm-6 col-lg-4">
<div class="col-12 col-md-6">
<canvas id="pizza-total-type-chart"></canvas>
</div>
{% if current_pizza_orders != 'null' %} {# None is json-serialized to 'null' #}
<div class="col-sm-6 col-lg-4">
<div class="col-12 col-md-6">
<canvas id="pizza-current-type-chart"></canvas>
</div>
{% endif %}
......
......@@ -12,6 +12,7 @@ from members.services import gen_stats_year, gen_stats_member_type
from utils.snippets import datetime_to_lectureyear
@freeze_time('2020-01-01')
class StatisticsTest(TestCase):
@classmethod
......@@ -27,34 +28,32 @@ class StatisticsTest(TestCase):
def sum_members(self, members, type=None):
if type is None:
return sum(sum(list(i.values())[1:]) for i in members)
return sum(sum(i.values()) for i in members.values())
else:
return sum(map(lambda x: x[type], members))
return sum(i[type] for i in members.values())
def sum_member_types(self, members):
return sum(members.values())
def test_gen_stats_year_no_members(self):
member_types = [t[0] for t in Membership.MEMBERSHIP_TYPES]
result = gen_stats_year(member_types)
result = gen_stats_year()
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 = [t[0] for t in Membership.MEMBERSHIP_TYPES]
current_year = datetime_to_lectureyear(date.today())
# Set start date to current year - 1:
for m in Member.objects.all():
m.profile.starting_year = current_year - 1
m.profile.save()
result = gen_stats_year(member_types)
result = gen_stats_year()
self.assertEqual(10, self.sum_members(result))
self.assertEqual(10, self.sum_members(result, Membership.MEMBER))
result = gen_stats_member_type(member_types)
result = {k: v for k, v in gen_stats_member_type().items()}
self.assertEqual(10, self.sum_member_types(result))
# Change one membership to benefactor should decrease amount of members
......@@ -62,52 +61,51 @@ class StatisticsTest(TestCase):
m.type = Membership.BENEFACTOR
m.save()
result = gen_stats_year(member_types)
result = gen_stats_year()
self.assertEqual(10, self.sum_members(result))
self.assertEqual(9, self.sum_members(result, Membership.MEMBER))
self.assertEqual(1, self.sum_members(result, Membership.BENEFACTOR))
result = gen_stats_member_type(member_types)
result = {k: v for k, v in gen_stats_member_type().items()}
self.assertEqual(10, self.sum_member_types(result))
self.assertEqual(9, result[Membership.MEMBER])
self.assertEqual(1, result[Membership.BENEFACTOR])
self.assertEqual(9, result[Membership.MEMBERSHIP_TYPES[0][1]])
self.assertEqual(1, result[Membership.MEMBERSHIP_TYPES[1][1]])
# Same for honorary members
m = Membership.objects.all()[1]
m.type = Membership.HONORARY
m.save()
result = gen_stats_year(member_types)
result = gen_stats_year()
self.assertEqual(10, self.sum_members(result))
self.assertEqual(8, self.sum_members(result, Membership.MEMBER))
self.assertEqual(1, self.sum_members(result, Membership.BENEFACTOR))
self.assertEqual(1, self.sum_members(result, Membership.HONORARY))
result = gen_stats_member_type(member_types)
result = {k: v for k, v in gen_stats_member_type().items()}
self.assertEqual(10, self.sum_member_types(result))
self.assertEqual(8, result[Membership.MEMBER])
self.assertEqual(1, result[Membership.BENEFACTOR])
self.assertEqual(1, result[Membership.HONORARY])
self.assertEqual(8, result[Membership.MEMBERSHIP_TYPES[0][1]])
self.assertEqual(1, result[Membership.MEMBERSHIP_TYPES[1][1]])
self.assertEqual(1, result[Membership.MEMBERSHIP_TYPES[2][1]])
# 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)
result = gen_stats_year()
self.assertEqual(9, self.sum_members(result))
self.assertEqual(7, self.sum_members(result, Membership.MEMBER))
self.assertEqual(1, self.sum_members(result, Membership.BENEFACTOR))
self.assertEqual(1, self.sum_members(result, Membership.HONORARY))
result = gen_stats_member_type(member_types)
result = {k: v for k, v in gen_stats_member_type().items()}
self.assertEqual(9, self.sum_member_types(result))
self.assertEqual(7, result[Membership.MEMBER])
self.assertEqual(1, result[Membership.BENEFACTOR])
self.assertEqual(1, result[Membership.HONORARY])
self.assertEqual(7, result[Membership.MEMBERSHIP_TYPES[0][1]])
self.assertEqual(1, result[Membership.MEMBERSHIP_TYPES[1][1]])
self.assertEqual(1, result[Membership.MEMBERSHIP_TYPES[2][1]])
def test_gen_stats_different_years(self):
member_types = [t[0] for t in Membership.MEMBERSHIP_TYPES]
current_year = datetime_to_lectureyear(date.today())
# postgres does not define random access directly on unsorted querysets
......@@ -138,27 +136,27 @@ class StatisticsTest(TestCase):
m.profile.save()
# 4 active members
result = gen_stats_year(member_types)
result = gen_stats_year()
self.assertEqual(4, self.sum_members(result))
self.assertEqual(4, self.sum_members(result, Membership.MEMBER))
# one first year student
self.assertEqual(1, result[0][Membership.MEMBER])
self.assertEqual(1, result['2019'][Membership.MEMBER])
# one second year student
self.assertEqual(1, result[1][Membership.MEMBER])
self.assertEqual(1, result['2018'][Membership.MEMBER])
# no third year students
self.assertEqual(0, result[2][Membership.MEMBER])
self.assertEqual(0, result['2017'][Membership.MEMBER])
# one fourth year student
self.assertEqual(1, result[3][Membership.MEMBER])
self.assertEqual(1, result['2016'][Membership.MEMBER])
# no fifth year students
self.assertEqual(0, result[4][Membership.MEMBER])
self.assertEqual(0, result['2015'][Membership.MEMBER])
# one >5 year student
self.assertEqual(1, result[5][Membership.MEMBER])
self.assertEqual(1, result['Older'][Membership.MEMBER])
class EmailChangeTest(TestCase):
......
......@@ -27,6 +27,8 @@ from . import models
from .forms import ProfileForm
from .services import member_achievements
from .services import member_societies
import events.services as event_services
import activemembers.services as activemembers_services
class ObtainThaliaAuthToken(ObtainAuthToken):
......@@ -219,18 +221,23 @@ class StatisticsView(TemplateView):
def get_context_data(self, **kwargs) -> dict:
context = super().get_context_data(**kwargs)
member_types = [t[0] for t in Membership.MEMBERSHIP_TYPES]
total = models.Member.current_members.count()
context.update({
"total_members": total,
"statistics": json.dumps({
"cohort_sizes": services.gen_stats_year(member_types),
"cohort_sizes":
services.gen_stats_year(),
"member_type_distribution":
services.gen_stats_member_type(member_types),
"total_pizza_orders": pizzas.services.gen_stats_pizza_orders(),
services.gen_stats_member_type(),
"total_pizza_orders":
pizzas.services.gen_stats_pizza_orders(),
"current_pizza_orders":
pizzas.services.gen_stats_current_pizza_orders(),
"committee_sizes":
activemembers_services.generate_statistics(),
"event_categories":
event_services.generate_category_statistics(),
})
})
......
......@@ -3,38 +3,40 @@ from . models import Product, Order, PizzaEvent
def gen_stats_pizza_orders():
total = []
total = {}
for product in Product.objects.all():
total.append({
'name': product.name,
'total': Order.objects.filter(product=product).count(),
total.update({
product.name: Order.objects.filter(product=product).count(),
})
total.sort(key=lambda prod: prod['total'], reverse=True)
return total
return {
i[0]: i[1]
for i in sorted(total.items(), key=lambda x: x[1], reverse=True)[:5]
if i[1] > 0
}
def gen_stats_current_pizza_orders():
total = []
total = {}
current_pizza_event = PizzaEvent.current()
if not current_pizza_event:
return None
for product in Product.objects.filter():
total.append({
'name': product.name,
'total': Order.objects.filter(
total.update({
product.name: Order.objects.filter(
product=product,
pizza_event=current_pizza_event,
).count(),
})
total.sort(key=lambda prod: prod['total'], reverse=True)
return total
return {
i[0]: i[1]
for i in sorted(total.items(), key=lambda x: x[1], reverse=True)[:5]
if i[1] > 0
}
def can_change_order(member, pizza_event):
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment