members/ljp: fix calculation of participants

Implements the correct formula for computing the participant count according to the LJP
regulations. Also adds extensive unit tests for the formula.
pull/104/head
Christian Merten 11 months ago
parent afdbb56d81
commit 98a03e4abd
Signed by: christian.merten
GPG Key ID: D953D69721B948B3

@ -1,5 +1,6 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
import uuid import uuid
import math
import pytz import pytz
import unicodedata import unicodedata
import re import re
@ -1150,13 +1151,47 @@ class Freizeit(CommonModel):
base_count = 2 + math.ceil((participant_count - 7) / 7) base_count = 2 + math.ceil((participant_count - 7) / 7)
return base_count + self.approved_extra_youth_leader_count return base_count + self.approved_extra_youth_leader_count
@property
def theoretic_ljp_participant_count(self):
"""
Calculate the participant count in the sense of the LJP regulations. This means
that all youth leaders are counted and all participants which are at least 6 years old and
strictly less than 27 years old. Additionally, up to 20% of the participants may violate the
age restrictions.
This is the theoretic value, ignoring the cutoff at 5 participants.
"""
# participants (possibly including youth leaders)
ps = {x.member for x in self.membersonlist.distinct()}
# youth leaders
jls = set(self.jugendleiter.distinct())
# non-youth leader participants
ps_only = ps - jls
# participants of the correct age
ps_correct_age = {m for m in ps_only if m.age() >= 6 and m.age() < 27}
# m = the official non-youth-leader participant count
# and, assuming there exist enough participants, unrounded m satisfies the equation
# len(ps_correct_age) + 1/5 * m = m
# if there are not enough participants,
# m = len(ps_only)
m = min(len(ps_only), math.floor(5/4 * len(ps_correct_age)))
return m + len(jls)
@property @property
def ljp_participant_count(self): def ljp_participant_count(self):
ps = set(map(lambda x: x.member, self.membersonlist.distinct())) """
The number of participants in the sense of LJP regulations. If the total
number of participants (including youth leaders and too old / young ones) is less
than 5, this is zero, otherwise it is `theoretic_ljp_participant_count`.
"""
# participants (possibly including youth leaders)
ps = {x.member for x in self.membersonlist.distinct()}
# youth leaders
jls = set(self.jugendleiter.distinct()) jls = set(self.jugendleiter.distinct())
count = len(ps.union(jls)) if len(ps.union(jls)) < 5:
return count return 0
#return count if count >= 5 else 0 else:
return self.theoretic_ljp_participant_count
@property @property
def maximal_ljp_contributions(self): def maximal_ljp_contributions(self):

@ -15,6 +15,8 @@ from django.db import connection
from django.db.migrations.executor import MigrationExecutor from django.db.migrations.executor import MigrationExecutor
import random import random
import datetime import datetime
from dateutil.relativedelta import relativedelta
import math
def create_custom_user(username, groups, prename, lastname): def create_custom_user(username, groups, prename, lastname):
@ -44,6 +46,14 @@ class BasicMemberTestCase(TestCase):
self.fritz.group.add(self.jl) self.fritz.group.add(self.jl)
self.fritz.group.add(self.alp) self.fritz.group.add(self.alp)
self.fritz.save() self.fritz.save()
self.peter = Member.objects.create(prename="Peter", lastname="Wulter",
birth_date=timezone.now().date(),
email=settings.TEST_MAIL, gender=MALE)
self.peter.group.add(self.jl)
self.peter.group.add(self.alp)
self.peter.save()
self.lara = Member.objects.create(prename="Lara", lastname="Wallis", birth_date=timezone.now().date(), self.lara = Member.objects.create(prename="Lara", lastname="Wallis", birth_date=timezone.now().date(),
email=settings.TEST_MAIL, gender=DIVERSE) email=settings.TEST_MAIL, gender=DIVERSE)
self.lara.group.add(self.alp) self.lara.group.add(self.alp)
@ -130,7 +140,9 @@ class PDFTestCase(TestCase):
self.note = MemberNoteList.objects.create(title='Cool list') self.note = MemberNoteList.objects.create(title='Cool list')
for i in range(7): for i in range(7):
m = Member.objects.create(prename='Lise {}'.format(i), lastname='Walter', birth_date=timezone.now().date(), m = Member.objects.create(prename='Lise {}'.format(i),
lastname='Walter',
birth_date=timezone.now().date(),
email=settings.TEST_MAIL, gender=FEMALE) email=settings.TEST_MAIL, gender=FEMALE)
NewMemberOnList.objects.create(member=m, comments='a' * i, memberlist=self.ex) NewMemberOnList.objects.create(member=m, comments='a' * i, memberlist=self.ex)
NewMemberOnList.objects.create(member=m, comments='a' * i, memberlist=self.note) NewMemberOnList.objects.create(member=m, comments='a' * i, memberlist=self.note)
@ -383,6 +395,81 @@ class MemberAdminTestCase(AdminTestCase):
self.assertEqual(final, final_target, 'Did redirect to wrong url.') self.assertEqual(final, final_target, 'Did redirect to wrong url.')
class FreizeitTestCase(BasicMemberTestCase):
def setUp(self):
super().setUp()
self.ex = Freizeit.objects.create(name='Wild trip', kilometers_traveled=120,
tour_type=GEMEINSCHAFTS_TOUR,
tour_approach=MUSKELKRAFT_ANREISE,
difficulty=1)
def _setup_test_ljp_participant_count(self, n_yl, n_correct_age, n_too_old):
for i in range(n_yl):
# a 50 years old
m = Member.objects.create(prename='Peter {}'.format(i),
lastname='Wulter',
birth_date=datetime.datetime.today() - relativedelta(years=50),
email=settings.TEST_MAIL,
gender=FEMALE)
self.ex.jugendleiter.add(m)
for i in range(n_correct_age):
# a 10 years old
m = Member.objects.create(prename='Lise {}'.format(i),
lastname='Walter',
birth_date=datetime.datetime.today() - relativedelta(years=10),
email=settings.TEST_MAIL,
gender=FEMALE)
NewMemberOnList.objects.create(member=m, comments='a', memberlist=self.ex)
for i in range(n_too_old):
# a 27 years old
m = Member.objects.create(prename='Lise {}'.format(i),
lastname='Walter',
birth_date=datetime.datetime.today() - relativedelta(years=27),
email=settings.TEST_MAIL,
gender=FEMALE)
NewMemberOnList.objects.create(member=m, comments='a', memberlist=self.ex)
def _cleanup_excursion(self):
# delete all members on excursion for clean up
NewMemberOnList.objects.all().delete()
self.ex.jugendleiter.all().delete()
def _test_theoretic_ljp_participant_count_proportion(self, n_yl, n_correct_age, n_too_old):
self._setup_test_ljp_participant_count(n_yl, n_correct_age, n_too_old)
self.assertGreaterEqual(self.ex.theoretic_ljp_participant_count, n_yl,
'An excursion with {n_yl} youth leaders and {n_correct_age} participants in the correct age range should have at least {n} participants.'.format(n_yl=n_yl, n_correct_age=n_correct_age, n=n_yl + n_correct_age))
self.assertLessEqual(self.ex.theoretic_ljp_participant_count, n_yl + n_correct_age + n_too_old,
'An excursion with a total number of youth leaders and participants of {n} should have not more than {n} participants'.format(n=n_yl + n_correct_age + n_too_old))
n_parts_only = self.ex.theoretic_ljp_participant_count - n_yl
self.assertLessEqual(n_parts_only - n_correct_age, 1/5 * n_parts_only,
'An excursion with {n_parts_only} non-youth-leaders, of which {n_correct_age} have the correct age, the number of participants violating the age range must not exceed 20% of the total participants, i.e. {d}'.format(n_parts_only=n_parts_only, n_correct_age=n_correct_age, d=1/5 * n_parts_only))
self.assertEqual(n_parts_only - n_correct_age, min(math.floor(1/5 * n_parts_only), n_too_old),
'An excursion with {n_parts_only} non-youth-leaders, of which {n_correct_age} have the correct age, the number of participants violating the age range must be equal to the minimum of {n_too_old} and the smallest integer less than 20% of the total participants, i.e. {d}'.format(n_parts_only=n_parts_only, n_correct_age=n_correct_age, d=math.floor(1/5 * n_parts_only), n_too_old=n_too_old))
# cleanup
self._cleanup_excursion()
def _test_ljp_participant_count_proportion(self, n_yl, n_correct_age, n_too_old):
self._setup_test_ljp_participant_count(n_yl, n_correct_age, n_too_old)
if n_yl + n_correct_age + n_too_old < 5:
self.assertEqual(self.ex.ljp_participant_count, 0)
else:
self.assertEqual(self.ex.ljp_participant_count, self.ex.theoretic_ljp_participant_count)
# cleanup
self._cleanup_excursion()
def test_theoretic_ljp_participant_count(self):
self._test_theoretic_ljp_participant_count_proportion(2, 0, 0)
for i in range(10):
self._test_theoretic_ljp_participant_count_proportion(2, 10 - i, i)
def test_ljp_participant_count(self):
self._test_ljp_participant_count_proportion(2, 1, 1)
self._test_ljp_participant_count_proportion(2, 5, 1)
class FreizeitAdminTestCase(AdminTestCase): class FreizeitAdminTestCase(AdminTestCase):
def setUp(self): def setUp(self):
super().setUp(model=Freizeit, admin=FreizeitAdmin) super().setUp(model=Freizeit, admin=FreizeitAdmin)

Loading…
Cancel
Save