diff --git a/jdav_web/members/models.py b/jdav_web/members/models.py index 58adb57..b46cebd 100644 --- a/jdav_web/members/models.py +++ b/jdav_web/members/models.py @@ -1,5 +1,6 @@ from datetime import datetime, timedelta import uuid +import math import pytz import unicodedata import re @@ -1150,13 +1151,47 @@ class Freizeit(CommonModel): base_count = 2 + math.ceil((participant_count - 7) / 7) 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 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()) - count = len(ps.union(jls)) - return count - #return count if count >= 5 else 0 + if len(ps.union(jls)) < 5: + return 0 + else: + return self.theoretic_ljp_participant_count @property def maximal_ljp_contributions(self): diff --git a/jdav_web/members/tests.py b/jdav_web/members/tests.py index 9c35f57..fe73cf3 100644 --- a/jdav_web/members/tests.py +++ b/jdav_web/members/tests.py @@ -15,6 +15,8 @@ from django.db import connection from django.db.migrations.executor import MigrationExecutor import random import datetime +from dateutil.relativedelta import relativedelta +import math 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.alp) 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(), email=settings.TEST_MAIL, gender=DIVERSE) self.lara.group.add(self.alp) @@ -130,8 +140,10 @@ class PDFTestCase(TestCase): self.note = MemberNoteList.objects.create(title='Cool list') for i in range(7): - m = Member.objects.create(prename='Lise {}'.format(i), lastname='Walter', birth_date=timezone.now().date(), - email=settings.TEST_MAIL, gender=FEMALE) + m = Member.objects.create(prename='Lise {}'.format(i), + lastname='Walter', + birth_date=timezone.now().date(), + 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.note) @@ -383,6 +395,81 @@ class MemberAdminTestCase(AdminTestCase): 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): def setUp(self): super().setUp(model=Freizeit, admin=FreizeitAdmin)