From a69d84e1ccbc75682fb853b617b47ebd7ca9a5c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Fri, 6 Dec 2024 16:58:43 +0100 Subject: [PATCH 01/50] Added tests around DatapointView.get_datapoint --- .../statistics/tests/tests_datapoint_view.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 tapir/statistics/tests/tests_datapoint_view.py diff --git a/tapir/statistics/tests/tests_datapoint_view.py b/tapir/statistics/tests/tests_datapoint_view.py new file mode 100644 index 00000000..23f16877 --- /dev/null +++ b/tapir/statistics/tests/tests_datapoint_view.py @@ -0,0 +1,58 @@ +import datetime +from unittest.mock import patch, Mock + +from tapir.statistics.models import FancyGraphCache +from tapir.statistics.views.fancy_graph.base_view import DatapointView +from tapir.utils.tests_utils import TapirFactoryTestBase, mock_timezone_now + + +class DummyDatapointView(DatapointView): + def calculate_datapoint(self, reference_time: datetime.datetime) -> int: + return 1 + + +class TestDatapointView(TapirFactoryTestBase): + NOW = datetime.datetime(year=2023, month=4, day=1, hour=12) + + def setUp(self) -> None: + super().setUp() + self.NOW = mock_timezone_now(self, self.NOW) + + @patch.object(DummyDatapointView, "calculate_datapoint") + def test_getDatapoint_noCache_callsCalculate(self, mock_calculate_datapoint: Mock): + date = self.NOW - datetime.timedelta(days=5) + mock_calculate_datapoint.return_value = 2 + + result = DummyDatapointView().get_datapoint(date) + + mock_calculate_datapoint.assert_called_once_with(date) + self.assertEqual(2, result) + + @patch.object(DummyDatapointView, "calculate_datapoint") + def test_getDatapoint_hasCache_returnsCachedValue( + self, mock_calculate_datapoint: Mock + ): + date = self.NOW - datetime.timedelta(days=5) + mock_calculate_datapoint.return_value = 2 + view_name = f"{DummyDatapointView.__module__}.{DummyDatapointView.__name__}" + FancyGraphCache.objects.create(view_name=view_name, date=date.date(), value=5) + + result = DummyDatapointView().get_datapoint(date) + + mock_calculate_datapoint.assert_not_called() + self.assertEqual(5, result) + + @patch.object(DummyDatapointView, "calculate_datapoint") + def test_getDatapoint_askingForTodaysData_doesntUseCachedValue( + self, mock_calculate_datapoint: Mock + ): + mock_calculate_datapoint.return_value = 2 + view_name = f"{DummyDatapointView.__module__}.{DummyDatapointView.__name__}" + FancyGraphCache.objects.create( + view_name=view_name, date=self.NOW.date(), value=5 + ) + + result = DummyDatapointView().get_datapoint(self.NOW) + + mock_calculate_datapoint.assert_called_once_with(self.NOW) + self.assertEqual(2, result) From 579d8ca168185617981cdc631bbe81000429ca48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Fri, 6 Dec 2024 17:14:34 +0100 Subject: [PATCH 02/50] Added tests around DatapointView.get --- .../statistics/tests/tests_datapoint_view.py | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/tapir/statistics/tests/tests_datapoint_view.py b/tapir/statistics/tests/tests_datapoint_view.py index 23f16877..799aa323 100644 --- a/tapir/statistics/tests/tests_datapoint_view.py +++ b/tapir/statistics/tests/tests_datapoint_view.py @@ -1,14 +1,23 @@ import datetime from unittest.mock import patch, Mock +from django.test import RequestFactory + +from tapir.accounts.tests.factories.factories import TapirUserFactory from tapir.statistics.models import FancyGraphCache from tapir.statistics.views.fancy_graph.base_view import DatapointView from tapir.utils.tests_utils import TapirFactoryTestBase, mock_timezone_now class DummyDatapointView(DatapointView): + VALUES = { + datetime.date(year=2023, month=4, day=5): 10, + datetime.date(year=2023, month=4, day=1): 8, + datetime.date(year=2023, month=3, day=1): 5, + } + def calculate_datapoint(self, reference_time: datetime.datetime) -> int: - return 1 + return self.VALUES[reference_time.date()] class TestDatapointView(TapirFactoryTestBase): @@ -56,3 +65,36 @@ def test_getDatapoint_askingForTodaysData_doesntUseCachedValue( mock_calculate_datapoint.assert_called_once_with(self.NOW) self.assertEqual(2, result) + + def test_viewGet_notRelative_returnsDatapoint(self): + request_factory = RequestFactory() + request = request_factory.get( + "", query_params={"relative": "false", "at_date": "2023-4-1"} + ) + request.user = TapirUserFactory.create(is_in_member_office=True) + + response = DummyDatapointView.as_view()(request) + + self.assertEqual(8, response.data) + + def test_viewGet_relativeFromToday_returnsDiffFromTodayToStartOfMonth(self): + request_factory = RequestFactory() + request = request_factory.get( + "", query_params={"relative": "true", "at_date": "2023-4-5"} + ) + request.user = TapirUserFactory.create(is_in_member_office=True) + + response = DummyDatapointView.as_view()(request) + + self.assertEqual(2, response.data) + + def test_viewGet_relativeFromFirstOfMonth_returnsDiffFromLastMonth(self): + request_factory = RequestFactory() + request = request_factory.get( + "", query_params={"relative": "true", "at_date": "2023-4-1"} + ) + request.user = TapirUserFactory.create(is_in_member_office=True) + + response = DummyDatapointView.as_view()(request) + + self.assertEqual(3, response.data) From c673abc77150f8b1071911d9a87d8f5833eb5160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Fri, 6 Dec 2024 18:24:39 +0100 Subject: [PATCH 03/50] Added a shift_exemption_service.py and an annotation function to shift_expectation_service.py Use those new things in number_of_abcd_members_view.py --- .../services/shift_exemption_service.py | 37 ++++++++++++ .../services/shift_expectation_service.py | 56 +++++++++++++++++++ .../number_of_abcd_members_view.py | 20 ++++--- tapir/statistics/views/main_view.py | 9 +-- tapir/utils/shortcuts.py | 5 ++ 5 files changed, 112 insertions(+), 15 deletions(-) create mode 100644 tapir/shifts/services/shift_exemption_service.py diff --git a/tapir/shifts/services/shift_exemption_service.py b/tapir/shifts/services/shift_exemption_service.py new file mode 100644 index 00000000..ff193ac3 --- /dev/null +++ b/tapir/shifts/services/shift_exemption_service.py @@ -0,0 +1,37 @@ +import datetime + +from django.db.models import QuerySet, Q, Count, Value, Case, When +from django.utils import timezone + +from tapir.shifts.models import ShiftUserData +from tapir.utils.shortcuts import ensure_date + + +class ShiftExemptionService: + ANNOTATION_HAS_EXEMPTION_AT_DATE = "has_exemption_at_date" + ANNOTATION_HAS_EXEMPTION_DATE_CHECK = "has_exemption_date_check" + + @classmethod + def annotate_shift_user_data_queryset_with_has_exemption_at_date( + cls, queryset: QuerySet[ShiftUserData], reference_date: datetime.date + ): + if reference_date is None: + reference_date = timezone.now().date() + reference_date = ensure_date(reference_date) + + filters = Q(shift_exemptions__start_date__lte=reference_date) & ( + Q(shift_exemptions__end_date__gte=reference_date) + | Q(shift_exemptions__end_date__isnull=True) + ) + + queryset = queryset.annotate( + nb_active_exemptions=Count("shift_exemptions", filter=filters) + ) + + annotate_kwargs = { + cls.ANNOTATION_HAS_EXEMPTION_AT_DATE: Case( + When(nb_active_exemptions__gt=0, then=Value(True)), default=Value(False) + ), + cls.ANNOTATION_HAS_EXEMPTION_DATE_CHECK: Value(reference_date), + } + return queryset.annotate(**annotate_kwargs) diff --git a/tapir/shifts/services/shift_expectation_service.py b/tapir/shifts/services/shift_expectation_service.py index f321f2f1..a4918798 100644 --- a/tapir/shifts/services/shift_expectation_service.py +++ b/tapir/shifts/services/shift_expectation_service.py @@ -1,15 +1,21 @@ import datetime +from django.db.models import QuerySet, Value, Case, When from django.utils import timezone +from tapir.coop.models import ShareOwner, MemberStatus from tapir.shifts.models import ShiftUserData from tapir.shifts.services.frozen_status_history_service import ( FrozenStatusHistoryService, ) +from tapir.shifts.services.shift_exemption_service import ShiftExemptionService from tapir.utils.shortcuts import get_timezone_aware_datetime class ShiftExpectationService: + ANNOTATION_IS_WORKING_AT_DATE = "is_working_at_date" + ANNOTATION_IS_WORKING_DATE_CHECK = "is_working_date_check" + @staticmethod def is_member_expected_to_do_shifts( shift_user_data: ShiftUserData, at_datetime: datetime.datetime | None = None @@ -39,6 +45,56 @@ def is_member_expected_to_do_shifts( return True + @classmethod + def annotate_shift_user_data_queryset_with_working_status_at_datetime( + cls, + shift_user_datas: QuerySet[ShiftUserData], + reference_time: datetime.datetime, + ): + reference_date = reference_time.date() + + # not frozen + working_shift_user_datas = FrozenStatusHistoryService.annotate_shift_user_data_queryset_with_is_frozen_at_datetime( + shift_user_datas, reference_time + ).filter( + **{FrozenStatusHistoryService.ANNOTATION_IS_FROZEN_AT_DATE: False} + ) + + # joined before date + working_shift_user_datas = working_shift_user_datas.filter( + user__date_joined__lte=reference_date + ) + + # member status active + active_member_ids = ( + ShareOwner.objects.filter( + user__shift_user_data__in=working_shift_user_datas + ) + .with_status(MemberStatus.ACTIVE) + .values_list("id", flat=True) + ) + working_shift_user_datas = working_shift_user_datas.filter( + user__share_owner__id__in=active_member_ids + ) + + # not exempted + working_shift_user_datas = ShiftExemptionService.annotate_shift_user_data_queryset_with_has_exemption_at_date( + working_shift_user_datas, reference_date + ).filter( + **{ShiftExemptionService.ANNOTATION_HAS_EXEMPTION_AT_DATE: False} + ) + + working_ids = working_shift_user_datas.values_list("id", flat=True) + + return shift_user_datas.annotate( + **{ + cls.ANNOTATION_IS_WORKING_AT_DATE: Case( + When(id__in=working_ids, then=Value(True)), default=Value(False) + ), + cls.ANNOTATION_IS_WORKING_DATE_CHECK: Value(reference_time), + } + ) + @classmethod def get_credit_requirement_for_cycle( cls, shift_user_data: ShiftUserData, cycle_start_date: datetime.date diff --git a/tapir/statistics/views/fancy_graph/number_of_abcd_members_view.py b/tapir/statistics/views/fancy_graph/number_of_abcd_members_view.py index 148b9762..647b73fb 100644 --- a/tapir/statistics/views/fancy_graph/number_of_abcd_members_view.py +++ b/tapir/statistics/views/fancy_graph/number_of_abcd_members_view.py @@ -1,27 +1,31 @@ import datetime -from tapir.shifts.models import ShiftAttendanceMode +from tapir.shifts.models import ShiftAttendanceMode, ShiftUserData from tapir.shifts.services.shift_attendance_mode_service import ( ShiftAttendanceModeService, ) +from tapir.shifts.services.shift_expectation_service import ShiftExpectationService from tapir.statistics.views.fancy_graph.base_view import ( DatapointView, - get_shift_user_datas_of_working_members_annotated_with_attendance_mode, ) class NumberOfAbcdMembersAtDateView(DatapointView): def calculate_datapoint(self, reference_time: datetime.datetime) -> int: - shift_user_datas = ( - get_shift_user_datas_of_working_members_annotated_with_attendance_mode( - reference_time + shift_user_datas = ShiftUserData.objects.all() + + working_members = ( + ShiftExpectationService.annotate_shift_user_data_queryset_with_working_status_at_datetime( + shift_user_datas, reference_time ) - ) + ).filter(**{ShiftExpectationService.ANNOTATION_IS_WORKING_AT_DATE: True}) - shift_user_datas = shift_user_datas.filter( + working_and_abcd_members = ShiftAttendanceModeService.annotate_shift_user_data_queryset_with_attendance_mode_at_datetime( + working_members, reference_time + ).filter( **{ ShiftAttendanceModeService.ANNOTATION_SHIFT_ATTENDANCE_MODE_AT_DATE: ShiftAttendanceMode.REGULAR } ) - return shift_user_datas.count() + return working_and_abcd_members.count() diff --git a/tapir/statistics/views/main_view.py b/tapir/statistics/views/main_view.py index 7bb3538e..35b5bdc4 100644 --- a/tapir/statistics/views/main_view.py +++ b/tapir/statistics/views/main_view.py @@ -45,7 +45,7 @@ build_line_chart_data, build_bar_chart_data, ) -from tapir.utils.shortcuts import get_first_of_next_month +from tapir.utils.shortcuts import get_first_of_next_month, transfer_attributes class MainStatisticsView(LoginRequiredMixin, generic.TemplateView): @@ -134,7 +134,7 @@ def annotate_attendance_modes(cls, share_owners, date): for share_owner in share_owners: if not share_owner.user: continue - cls.transfer_attributes( + transfer_attributes( shift_user_datas[share_owner.user.shift_user_data.id], share_owner.user.shift_user_data, [ @@ -144,11 +144,6 @@ def annotate_attendance_modes(cls, share_owners, date): ) return share_owners - @staticmethod - def transfer_attributes(source, target, attributes): - for attribute in attributes: - setattr(target, attribute, getattr(source, attribute)) - def get_working_members_context(self): shift_user_datas = ( ShiftUserData.objects.filter(user__share_owner__isnull=False) diff --git a/tapir/utils/shortcuts.py b/tapir/utils/shortcuts.py index 36941162..9f2b2bda 100644 --- a/tapir/utils/shortcuts.py +++ b/tapir/utils/shortcuts.py @@ -241,3 +241,8 @@ def ensure_datetime(obj: datetime.date | datetime.datetime): if isinstance(obj, datetime.datetime): return obj return get_timezone_aware_datetime(obj, datetime.time()) + + +def transfer_attributes(source, target, attributes): + for attribute in attributes: + setattr(target, attribute, getattr(source, attribute)) From 91d5cad649bcf7f2874ec10cd87feea3dd3ab785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Fri, 6 Dec 2024 20:12:52 +0100 Subject: [PATCH 04/50] Removed get_shift_user_datas_of_working_members_annotated_with_attendance_mode, replacing it with usages of ShiftExpectationService and ShiftAttendanceModeService --- .../statistics/views/fancy_graph/base_view.py | 72 ------------------- .../number_of_flying_members_view.py | 20 +++--- .../number_of_shift_partners_view.py | 11 +-- 3 files changed, 19 insertions(+), 84 deletions(-) diff --git a/tapir/statistics/views/fancy_graph/base_view.py b/tapir/statistics/views/fancy_graph/base_view.py index 971b4c64..b8cf2639 100644 --- a/tapir/statistics/views/fancy_graph/base_view.py +++ b/tapir/statistics/views/fancy_graph/base_view.py @@ -9,19 +9,7 @@ from rest_framework.response import Response from rest_framework.views import APIView -from tapir.coop.models import ShareOwner -from tapir.coop.services.investing_status_service import InvestingStatusService -from tapir.coop.services.membership_pause_service import MembershipPauseService -from tapir.coop.services.number_of_shares_service import NumberOfSharesService from tapir.settings import PERMISSION_COOP_MANAGE -from tapir.shifts.models import ShiftUserData -from tapir.shifts.services.frozen_status_history_service import ( - FrozenStatusHistoryService, -) -from tapir.shifts.services.shift_attendance_mode_service import ( - ShiftAttendanceModeService, -) -from tapir.shifts.services.shift_expectation_service import ShiftExpectationService from tapir.statistics.models import FancyGraphCache @@ -96,63 +84,3 @@ def get(self, request): result, status=status.HTTP_200_OK, ) - - -def get_shift_user_datas_of_working_members_annotated_with_attendance_mode( - reference_time: datetime.datetime, -): - reference_date = reference_time.date() - - shift_user_datas = ( - ShiftUserData.objects.filter(user__share_owner__isnull=False) - .prefetch_related("user") - .prefetch_related("user__share_owner") - .prefetch_related("user__share_owner__share_ownerships") - .prefetch_related("shift_exemptions") - ) - shift_user_datas = FrozenStatusHistoryService.annotate_shift_user_data_queryset_with_is_frozen_at_datetime( - shift_user_datas, reference_time - ) - share_owners = ( - NumberOfSharesService.annotate_share_owner_queryset_with_nb_of_active_shares( - ShareOwner.objects.all(), reference_date - ) - ) - share_owners = ( - MembershipPauseService.annotate_share_owner_queryset_with_has_active_pause( - share_owners, reference_date - ) - ) - share_owners = InvestingStatusService.annotate_share_owner_queryset_with_investing_status_at_datetime( - share_owners, reference_time - ) - share_owners = {share_owner.id: share_owner for share_owner in share_owners} - for shift_user_data in shift_user_datas: - DatapointView.transfer_attributes( - share_owners[shift_user_data.user.share_owner.id], - shift_user_data.user.share_owner, - [ - NumberOfSharesService.ANNOTATION_NUMBER_OF_ACTIVE_SHARES, - NumberOfSharesService.ANNOTATION_SHARES_ACTIVE_AT_DATE, - MembershipPauseService.ANNOTATION_HAS_ACTIVE_PAUSE, - MembershipPauseService.ANNOTATION_HAS_ACTIVE_PAUSE_AT_DATE, - InvestingStatusService.ANNOTATION_WAS_INVESTING, - InvestingStatusService.ANNOTATION_WAS_INVESTING_AT_DATE, - ], - ) - - ids_of_suds_of_members_that_do_shifts = [ - shift_user_data.id - for shift_user_data in shift_user_datas - if ShiftExpectationService.is_member_expected_to_do_shifts( - shift_user_data, reference_time - ) - ] - - shift_user_datas = ShiftUserData.objects.filter( - id__in=ids_of_suds_of_members_that_do_shifts - ) - - return ShiftAttendanceModeService.annotate_shift_user_data_queryset_with_attendance_mode_at_datetime( - shift_user_datas, reference_time - ) diff --git a/tapir/statistics/views/fancy_graph/number_of_flying_members_view.py b/tapir/statistics/views/fancy_graph/number_of_flying_members_view.py index 44a4741b..fd1a8140 100644 --- a/tapir/statistics/views/fancy_graph/number_of_flying_members_view.py +++ b/tapir/statistics/views/fancy_graph/number_of_flying_members_view.py @@ -1,27 +1,31 @@ import datetime -from tapir.shifts.models import ShiftAttendanceMode +from tapir.shifts.models import ShiftAttendanceMode, ShiftUserData from tapir.shifts.services.shift_attendance_mode_service import ( ShiftAttendanceModeService, ) +from tapir.shifts.services.shift_expectation_service import ShiftExpectationService from tapir.statistics.views.fancy_graph.base_view import ( DatapointView, - get_shift_user_datas_of_working_members_annotated_with_attendance_mode, ) class NumberOfFlyingMembersAtDateView(DatapointView): def calculate_datapoint(self, reference_time: datetime.datetime) -> int: - shift_user_datas = ( - get_shift_user_datas_of_working_members_annotated_with_attendance_mode( - reference_time + shift_user_datas = ShiftUserData.objects.all() + + working_members = ( + ShiftExpectationService.annotate_shift_user_data_queryset_with_working_status_at_datetime( + shift_user_datas, reference_time ) - ) + ).filter(**{ShiftExpectationService.ANNOTATION_IS_WORKING_AT_DATE: True}) - shift_user_datas = shift_user_datas.filter( + working_and_abcd_members = ShiftAttendanceModeService.annotate_shift_user_data_queryset_with_attendance_mode_at_datetime( + working_members, reference_time + ).filter( **{ ShiftAttendanceModeService.ANNOTATION_SHIFT_ATTENDANCE_MODE_AT_DATE: ShiftAttendanceMode.FLYING } ) - return shift_user_datas.count() + return working_and_abcd_members.count() diff --git a/tapir/statistics/views/fancy_graph/number_of_shift_partners_view.py b/tapir/statistics/views/fancy_graph/number_of_shift_partners_view.py index 4b5cdfe3..eff72d81 100644 --- a/tapir/statistics/views/fancy_graph/number_of_shift_partners_view.py +++ b/tapir/statistics/views/fancy_graph/number_of_shift_partners_view.py @@ -1,19 +1,22 @@ +from tapir.shifts.models import ShiftUserData +from tapir.shifts.services.shift_expectation_service import ShiftExpectationService from tapir.shifts.services.shift_partner_history_service import ( ShiftPartnerHistoryService, ) from tapir.statistics.views.fancy_graph.base_view import ( DatapointView, - get_shift_user_datas_of_working_members_annotated_with_attendance_mode, ) class NumberOfShiftPartnersAtDateView(DatapointView): def calculate_datapoint(self, reference_time) -> int: + shift_user_datas = ShiftUserData.objects.all() + shift_user_datas = ( - get_shift_user_datas_of_working_members_annotated_with_attendance_mode( - reference_time + ShiftExpectationService.annotate_shift_user_data_queryset_with_working_status_at_datetime( + shift_user_datas, reference_time ) - ) + ).filter(**{ShiftExpectationService.ANNOTATION_IS_WORKING_AT_DATE: True}) shift_user_datas = ShiftPartnerHistoryService.annotate_shift_user_data_queryset_with_has_shift_partner_at_date( shift_user_datas, reference_time From 36eb6fae618ac85e35d1f8cd1c439a16440c7db1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Fri, 6 Dec 2024 20:25:59 +0100 Subject: [PATCH 05/50] Added tests for ShiftExemptionService --- .../test_frozen_status_history_service.py | 10 ++ .../tests/test_shift_exemption_service.py | 98 +++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 tapir/shifts/tests/test_shift_exemption_service.py diff --git a/tapir/shifts/tests/test_frozen_status_history_service.py b/tapir/shifts/tests/test_frozen_status_history_service.py index 86651fe9..cc231b0b 100644 --- a/tapir/shifts/tests/test_frozen_status_history_service.py +++ b/tapir/shifts/tests/test_frozen_status_history_service.py @@ -249,6 +249,16 @@ def test_annotateShiftUserDataQuerysetWithIsFrozenAtDatetimeAfterRefactor_noRele ) log_entry_in_the_past.save() + not_relevant_log_entry = UpdateShiftUserDataLogEntry.objects.create( + user=tapir_user, + old_values={"shift_partner": 182}, + new_values={"shift_partner": 25}, + ) + not_relevant_log_entry.created_date = reference_datetime + datetime.timedelta( + days=3 + ) + not_relevant_log_entry.save() + queryset = FrozenStatusHistoryService._annotate_shift_user_data_queryset_with_is_frozen_at_datetime_after_refactor( ShiftUserData.objects.all(), reference_datetime ) diff --git a/tapir/shifts/tests/test_shift_exemption_service.py b/tapir/shifts/tests/test_shift_exemption_service.py new file mode 100644 index 00000000..866da7ef --- /dev/null +++ b/tapir/shifts/tests/test_shift_exemption_service.py @@ -0,0 +1,98 @@ +import datetime + +from django.utils import timezone + +from tapir.accounts.tests.factories.factories import TapirUserFactory +from tapir.shifts.models import ShiftUserData, ShiftExemption +from tapir.shifts.services.shift_exemption_service import ShiftExemptionService +from tapir.utils.tests_utils import TapirFactoryTestBase + + +class TestShiftExemptionService(TapirFactoryTestBase): + def test_annotateShiftUserDataQuerysetWithHasExemptionAtDate_noExemptions_annotatesFalse( + self, + ): + TapirUserFactory.create() + + queryset = ShiftExemptionService.annotate_shift_user_data_queryset_with_has_exemption_at_date( + ShiftUserData.objects.all(), timezone.now() + ) + + self.assertEqual( + 1, + queryset.filter( + **{ShiftExemptionService.ANNOTATION_HAS_EXEMPTION_AT_DATE: False} + ).count(), + ) + + def test_annotateShiftUserDataQuerysetWithHasExemptionAtDate_hasInvalidExemptions_annotatesFalse( + self, + ): + tapir_user = TapirUserFactory.create() + + ShiftExemption.objects.create( # in the past + start_date=datetime.date(year=2023, month=1, day=1), + end_date=datetime.date(year=2024, month=1, day=1), + shift_user_data=tapir_user.shift_user_data, + ) + + ShiftExemption.objects.create( # in the future + start_date=datetime.date(year=2024, month=6, day=15), + end_date=None, + shift_user_data=tapir_user.shift_user_data, + ) + + queryset = ShiftExemptionService.annotate_shift_user_data_queryset_with_has_exemption_at_date( + ShiftUserData.objects.all(), datetime.date(year=2024, month=6, day=1) + ) + + self.assertEqual( + 1, + queryset.filter( + **{ShiftExemptionService.ANNOTATION_HAS_EXEMPTION_AT_DATE: False} + ).count(), + ) + + def test_annotateShiftUserDataQuerysetWithHasExemptionAtDate_hasValidInfiniteExemption_annotatesTrue( + self, + ): + tapir_user = TapirUserFactory.create() + + ShiftExemption.objects.create( + start_date=datetime.date(year=2023, month=1, day=1), + end_date=None, + shift_user_data=tapir_user.shift_user_data, + ) + + queryset = ShiftExemptionService.annotate_shift_user_data_queryset_with_has_exemption_at_date( + ShiftUserData.objects.all(), datetime.date(year=2024, month=6, day=1) + ) + + self.assertEqual( + 1, + queryset.filter( + **{ShiftExemptionService.ANNOTATION_HAS_EXEMPTION_AT_DATE: True} + ).count(), + ) + + def test_annotateShiftUserDataQuerysetWithHasExemptionAtDate_hasValidFiniteExemption_annotatesTrue( + self, + ): + tapir_user = TapirUserFactory.create() + + ShiftExemption.objects.create( + start_date=datetime.date(year=2024, month=6, day=1), + end_date=datetime.date(year=2024, month=7, day=1), + shift_user_data=tapir_user.shift_user_data, + ) + + queryset = ShiftExemptionService.annotate_shift_user_data_queryset_with_has_exemption_at_date( + ShiftUserData.objects.all(), datetime.date(year=2024, month=6, day=15) + ) + + self.assertEqual( + 1, + queryset.filter( + **{ShiftExemptionService.ANNOTATION_HAS_EXEMPTION_AT_DATE: True} + ).count(), + ) From ab1573927008844c1c07ca1d0c4b42f5a2d82bf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Fri, 6 Dec 2024 20:53:26 +0100 Subject: [PATCH 06/50] Added tests for ShiftExpectationService.is_member_expected_to_do_shifts --- .../shift_expectation_service/__init__.py | 0 .../test_is_member_expected_to_do_shifts.py | 163 ++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 tapir/shifts/tests/shift_expectation_service/__init__.py create mode 100644 tapir/shifts/tests/shift_expectation_service/test_is_member_expected_to_do_shifts.py diff --git a/tapir/shifts/tests/shift_expectation_service/__init__.py b/tapir/shifts/tests/shift_expectation_service/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tapir/shifts/tests/shift_expectation_service/test_is_member_expected_to_do_shifts.py b/tapir/shifts/tests/shift_expectation_service/test_is_member_expected_to_do_shifts.py new file mode 100644 index 00000000..2d8e34a2 --- /dev/null +++ b/tapir/shifts/tests/shift_expectation_service/test_is_member_expected_to_do_shifts.py @@ -0,0 +1,163 @@ +import datetime +from unittest.mock import patch, Mock + +from django.test import SimpleTestCase + +from tapir.shifts.services.frozen_status_history_service import ( + FrozenStatusHistoryService, +) +from tapir.shifts.services.shift_expectation_service import ShiftExpectationService + + +class TestIsMemberExpectedToDoShifts(SimpleTestCase): + @staticmethod + def create_mock_user_that_should_do_shifts( + mock_is_frozen_at_datetime, reference_time + ): + shift_user_data = Mock() + + shift_user_data.user.share_owner = Mock() + mock_is_frozen_at_datetime.return_value = False + shift_user_data.user.date_joined = reference_time - datetime.timedelta(days=1) + shift_user_data.user.share_owner.is_active.return_value = True + shift_user_data.is_currently_exempted_from_shifts.return_value = False + + return shift_user_data + + @patch.object(FrozenStatusHistoryService, "is_frozen_at_datetime") + def test_isMemberExpectedToDoShifts_memberShouldDoShifts_returnsTrue( + self, mock_is_frozen_at_datetime: Mock + ): + reference_time = datetime.datetime(year=2024, month=6, day=1) + shift_user_data = self.create_mock_user_that_should_do_shifts( + mock_is_frozen_at_datetime, reference_time + ) + + self.assertTrue( + ShiftExpectationService.is_member_expected_to_do_shifts( + shift_user_data, reference_time + ) + ) + + mock_is_frozen_at_datetime.assert_called_once_with( + shift_user_data, reference_time + ) + shift_user_data.user.share_owner.is_active.assert_called_once_with( + reference_time + ) + shift_user_data.is_currently_exempted_from_shifts.assert_called_once_with( + reference_time.date() + ) + + @patch.object(FrozenStatusHistoryService, "is_frozen_at_datetime") + def test_isMemberExpectedToDoShifts_noShareOwner_returnsFalse( + self, mock_is_frozen_at_datetime: Mock + ): + reference_time = datetime.datetime(year=2024, month=6, day=1) + shift_user_data = self.create_mock_user_that_should_do_shifts( + mock_is_frozen_at_datetime, reference_time + ) + shift_user_data.user.share_owner = None + + self.assertFalse( + ShiftExpectationService.is_member_expected_to_do_shifts( + shift_user_data, reference_time + ) + ) + + mock_is_frozen_at_datetime.assert_not_called() + shift_user_data.is_currently_exempted_from_shifts.assert_not_called() + + @patch.object(FrozenStatusHistoryService, "is_frozen_at_datetime") + def test_isMemberExpectedToDoShifts_isFrozen_returnsFalse( + self, mock_is_frozen_at_datetime: Mock + ): + reference_time = datetime.datetime(year=2024, month=6, day=1) + shift_user_data = self.create_mock_user_that_should_do_shifts( + mock_is_frozen_at_datetime, reference_time + ) + mock_is_frozen_at_datetime.return_value = True + + self.assertFalse( + ShiftExpectationService.is_member_expected_to_do_shifts( + shift_user_data, reference_time + ) + ) + + mock_is_frozen_at_datetime.assert_called_once_with( + shift_user_data, reference_time + ) + shift_user_data.user.share_owner.is_active.assert_not_called() + shift_user_data.is_currently_exempted_from_shifts.assert_not_called() + + @patch.object(FrozenStatusHistoryService, "is_frozen_at_datetime") + def test_isMemberExpectedToDoShifts_userJoinedAfterDate_returnsFalse( + self, mock_is_frozen_at_datetime: Mock + ): + reference_time = datetime.datetime(year=2024, month=6, day=1) + shift_user_data = self.create_mock_user_that_should_do_shifts( + mock_is_frozen_at_datetime, reference_time + ) + shift_user_data.user.date_joined = reference_time + datetime.timedelta(days=1) + + self.assertFalse( + ShiftExpectationService.is_member_expected_to_do_shifts( + shift_user_data, reference_time + ) + ) + + mock_is_frozen_at_datetime.assert_called_once_with( + shift_user_data, reference_time + ) + shift_user_data.user.share_owner.is_active.assert_not_called() + shift_user_data.is_currently_exempted_from_shifts.assert_not_called() + + @patch.object(FrozenStatusHistoryService, "is_frozen_at_datetime") + def test_isMemberExpectedToDoShifts_userIsNotActive_returnsFalse( + self, mock_is_frozen_at_datetime: Mock + ): + reference_time = datetime.datetime(year=2024, month=6, day=1) + shift_user_data = self.create_mock_user_that_should_do_shifts( + mock_is_frozen_at_datetime, reference_time + ) + shift_user_data.user.share_owner.is_active.return_value = False + + self.assertFalse( + ShiftExpectationService.is_member_expected_to_do_shifts( + shift_user_data, reference_time + ) + ) + + mock_is_frozen_at_datetime.assert_called_once_with( + shift_user_data, reference_time + ) + shift_user_data.user.share_owner.is_active.assert_called_once_with( + reference_time + ) + shift_user_data.is_currently_exempted_from_shifts.assert_not_called() + + @patch.object(FrozenStatusHistoryService, "is_frozen_at_datetime") + def test_isMemberExpectedToDoShifts_userIsExempted_returnsFalse( + self, mock_is_frozen_at_datetime: Mock + ): + reference_time = datetime.datetime(year=2024, month=6, day=1) + shift_user_data = self.create_mock_user_that_should_do_shifts( + mock_is_frozen_at_datetime, reference_time + ) + shift_user_data.is_currently_exempted_from_shifts.return_value = True + + self.assertFalse( + ShiftExpectationService.is_member_expected_to_do_shifts( + shift_user_data, reference_time + ) + ) + + mock_is_frozen_at_datetime.assert_called_once_with( + shift_user_data, reference_time + ) + shift_user_data.user.share_owner.is_active.assert_called_once_with( + reference_time + ) + shift_user_data.is_currently_exempted_from_shifts.assert_called_once_with( + reference_time.date() + ) From 6d3c1592bf56582178c7b105829c571fcf2715c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Sat, 7 Dec 2024 11:49:37 +0100 Subject: [PATCH 07/50] Added tests for ShiftExemptionService.get_credit_requirement_for_cycle --- .../test_get_credit_requirement_for_cycle.py | 52 +++++++++++++++++++ tapir/utils/tests_utils.py | 6 ++- 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 tapir/shifts/tests/shift_expectation_service/test_get_credit_requirement_for_cycle.py diff --git a/tapir/shifts/tests/shift_expectation_service/test_get_credit_requirement_for_cycle.py b/tapir/shifts/tests/shift_expectation_service/test_get_credit_requirement_for_cycle.py new file mode 100644 index 00000000..f75fdd25 --- /dev/null +++ b/tapir/shifts/tests/shift_expectation_service/test_get_credit_requirement_for_cycle.py @@ -0,0 +1,52 @@ +import datetime +from unittest.mock import patch, Mock + +from django.test import SimpleTestCase + +from tapir.shifts.services.shift_expectation_service import ShiftExpectationService +from tapir.utils.shortcuts import get_timezone_aware_datetime +from tapir.utils.tests_utils import mock_timezone_now + + +class TestGetCreditRequirementForCycle(SimpleTestCase): + NOW = datetime.datetime(year=2021, month=3, day=15) + + def setUp(self): + super().setUp() + self.NOW = mock_timezone_now(self, self.NOW) + + @patch.object(ShiftExpectationService, "is_member_expected_to_do_shifts") + def test_getCreditRequirementForCycle_memberShouldDoShifts_returns1( + self, mock_is_member_expected_to_do_shifts: Mock + ): + cycle_start_date = datetime.date(year=2024, month=6, day=1) + shift_user_data = Mock() + mock_is_member_expected_to_do_shifts.return_value = True + + result = ShiftExpectationService.get_credit_requirement_for_cycle( + shift_user_data, cycle_start_date + ) + + self.assertEqual(1, result) + mock_is_member_expected_to_do_shifts.assert_called_once_with( + shift_user_data, + get_timezone_aware_datetime(cycle_start_date, self.NOW.time()), + ) + + @patch.object(ShiftExpectationService, "is_member_expected_to_do_shifts") + def test_getCreditRequirementForCycle_memberShouldNotDoShifts_returns0( + self, mock_is_member_expected_to_do_shifts: Mock + ): + cycle_start_date = datetime.date(year=2024, month=6, day=1) + shift_user_data = Mock() + mock_is_member_expected_to_do_shifts.return_value = False + + result = ShiftExpectationService.get_credit_requirement_for_cycle( + shift_user_data, cycle_start_date + ) + + self.assertEqual(0, result) + mock_is_member_expected_to_do_shifts.assert_called_once_with( + shift_user_data, + get_timezone_aware_datetime(cycle_start_date, self.NOW.time()), + ) diff --git a/tapir/utils/tests_utils.py b/tapir/utils/tests_utils.py index daec9dd2..181f927d 100644 --- a/tapir/utils/tests_utils.py +++ b/tapir/utils/tests_utils.py @@ -10,7 +10,7 @@ import factory.random from django.contrib.staticfiles.testing import StaticLiveServerTestCase from django.core.mail import EmailMessage -from django.test import TestCase, override_settings, Client +from django.test import TestCase, override_settings, Client, SimpleTestCase from django.urls import reverse from django.utils import timezone from parameterized import parameterized @@ -230,7 +230,9 @@ def assertEmailAttachmentIsAPdf(self, attachment): self.assertEqual(CONTENT_TYPE_PDF, attachment_type) -def mock_timezone_now(test: TestCase, now: datetime.datetime) -> datetime.datetime: +def mock_timezone_now( + test: SimpleTestCase, now: datetime.datetime +) -> datetime.datetime: now = timezone.make_aware(now) if timezone.is_naive(now) else now patcher = patch("django.utils.timezone.now") test.mock_now = patcher.start() From 1741f0f27d4a2e4826c3fe5275e937e9ede0c83d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Sat, 7 Dec 2024 12:03:03 +0100 Subject: [PATCH 08/50] Added type hints for factories. --- tapir/accounts/tests/factories/factories.py | 2 +- .../tests/factories/user_data_factory.py | 6 +++++- tapir/coop/tests/factories.py | 16 +++++++++------- tapir/shifts/tests/factories.py | 8 ++++---- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/tapir/accounts/tests/factories/factories.py b/tapir/accounts/tests/factories/factories.py index 939c4a9c..6db30170 100644 --- a/tapir/accounts/tests/factories/factories.py +++ b/tapir/accounts/tests/factories/factories.py @@ -7,7 +7,7 @@ from tapir.utils.shortcuts import set_group_membership -class TapirUserFactory(UserDataFactory): +class TapirUserFactory(UserDataFactory[TapirUser]): class Meta: model = TapirUser skip_postgeneration_save = True diff --git a/tapir/accounts/tests/factories/user_data_factory.py b/tapir/accounts/tests/factories/user_data_factory.py index 3d36e271..ed5285f9 100644 --- a/tapir/accounts/tests/factories/user_data_factory.py +++ b/tapir/accounts/tests/factories/user_data_factory.py @@ -1,3 +1,5 @@ +from typing import TypeVar + import factory import phonenumbers from faker import Faker @@ -27,8 +29,10 @@ def phone_number(self): fake = Faker() fake.add_provider(CustomPhoneProvider) +T = TypeVar("T") + -class UserDataFactory(factory.django.DjangoModelFactory): +class UserDataFactory(factory.django.DjangoModelFactory[T]): class Meta: abstract = True exclude = ("ATTRIBUTES",) diff --git a/tapir/coop/tests/factories.py b/tapir/coop/tests/factories.py index 6fd5b6da..8ed01be9 100644 --- a/tapir/coop/tests/factories.py +++ b/tapir/coop/tests/factories.py @@ -19,7 +19,7 @@ fake = Faker() -class ShareOwnershipFactory(factory.django.DjangoModelFactory): +class ShareOwnershipFactory(factory.django.DjangoModelFactory[ShareOwnership]): class Meta: model = ShareOwnership @@ -27,7 +27,7 @@ class Meta: amount_paid = factory.Faker("pydecimal", min_value=0, max_value=COOP_SHARE_PRICE) -class ShareOwnerFactory(UserDataFactory): +class ShareOwnerFactory(UserDataFactory[ShareOwner]): class Meta: model = ShareOwner skip_postgeneration_save = True @@ -44,7 +44,7 @@ def nb_shares(self, create, nb_shares=None, **kwargs): ShareOwnershipFactory.create(share_owner=self) -class DraftUserFactory(UserDataFactory): +class DraftUserFactory(UserDataFactory[DraftUser]): class Meta: model = DraftUser @@ -67,7 +67,7 @@ class Meta: signed_membership_agreement = factory.Faker("pybool") -class MembershipPauseFactory(factory.django.DjangoModelFactory): +class MembershipPauseFactory(factory.django.DjangoModelFactory[MembershipPause]): class Meta: model = MembershipPause exclude = "pause_duration" @@ -81,7 +81,9 @@ class Meta: share_owner = factory.SubFactory(ShareOwnerFactory) -class MembershipResignationFactory(factory.django.DjangoModelFactory): +class MembershipResignationFactory( + factory.django.DjangoModelFactory[MembershipResignation] +): class Meta: model = MembershipResignation @@ -118,7 +120,7 @@ class Meta: ) -class PurchaseBasketFactory(factory.django.DjangoModelFactory): +class PurchaseBasketFactory(factory.django.DjangoModelFactory[PurchaseBasket]): class Meta: model = PurchaseBasket @@ -129,7 +131,7 @@ class Meta: discount = 0 -class GeneralAccountFactory(UserDataFactory): +class GeneralAccountFactory(UserDataFactory[TapirUser]): class Meta: model = TapirUser diff --git a/tapir/shifts/tests/factories.py b/tapir/shifts/tests/factories.py index b081d7a9..0c564c16 100644 --- a/tapir/shifts/tests/factories.py +++ b/tapir/shifts/tests/factories.py @@ -12,14 +12,14 @@ ) -class ShiftSlotTemplateFactory(factory.django.DjangoModelFactory): +class ShiftSlotTemplateFactory(factory.django.DjangoModelFactory[ShiftSlotTemplate]): class Meta: model = ShiftSlotTemplate name = factory.Faker("job") -class ShiftTemplateFactory(factory.django.DjangoModelFactory): +class ShiftTemplateFactory(factory.django.DjangoModelFactory[ShiftTemplate]): class Meta: model = ShiftTemplate exclude = ("start_hour", "start_minute", "duration") @@ -56,14 +56,14 @@ def nb_slots(self, create, nb_slots, **kwargs): self.save() -class ShiftSlotFactory(factory.django.DjangoModelFactory): +class ShiftSlotFactory(factory.django.DjangoModelFactory[ShiftSlot]): class Meta: model = ShiftSlot name = factory.Faker("job") -class ShiftFactory(factory.django.DjangoModelFactory): +class ShiftFactory(factory.django.DjangoModelFactory[Shift]): class Meta: model = Shift exclude = ("start_hour", "start_minute", "duration", "nb_slots") From 2bedbada883a5fe4a6f5226f090a409cc8a81948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Sat, 7 Dec 2024 12:03:34 +0100 Subject: [PATCH 09/50] Fixed usages for transfer_attributes --- .../views/fancy_graph/number_of_working_members_view.py | 3 ++- tapir/statistics/views/main_view.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tapir/statistics/views/fancy_graph/number_of_working_members_view.py b/tapir/statistics/views/fancy_graph/number_of_working_members_view.py index 10fc6f4e..327d1760 100644 --- a/tapir/statistics/views/fancy_graph/number_of_working_members_view.py +++ b/tapir/statistics/views/fancy_graph/number_of_working_members_view.py @@ -10,6 +10,7 @@ ) from tapir.shifts.services.shift_expectation_service import ShiftExpectationService from tapir.statistics.views.fancy_graph.base_view import DatapointView +from tapir.utils.shortcuts import transfer_attributes class NumberOfWorkingMembersAtDateView(DatapointView): @@ -39,7 +40,7 @@ def calculate_datapoint(self, reference_time: datetime.datetime) -> int: ) share_owners = {share_owner.id: share_owner for share_owner in share_owners} for shift_user_data in shift_user_datas: - self.transfer_attributes( + transfer_attributes( share_owners[shift_user_data.user.share_owner.id], shift_user_data.user.share_owner, [ diff --git a/tapir/statistics/views/main_view.py b/tapir/statistics/views/main_view.py index 35b5bdc4..1cd8c4db 100644 --- a/tapir/statistics/views/main_view.py +++ b/tapir/statistics/views/main_view.py @@ -165,7 +165,7 @@ def get_working_members_context(self): ) share_owners = {share_owner.id: share_owner for share_owner in share_owners} for shift_user_data in shift_user_datas: - self.transfer_attributes( + transfer_attributes( share_owners[shift_user_data.user.share_owner.id], shift_user_data.user.share_owner, [ From 21a09edc4d63cc73480b676b645bbe84b7204a6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Sat, 7 Dec 2024 12:20:10 +0100 Subject: [PATCH 10/50] Added tests for ShiftExpectationService.annotate_shift_user_data_queryset_with_working_status_at_datetime --- .../services/shift_expectation_service.py | 2 +- .../test_annotate_with_working_status.py | 129 ++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 tapir/shifts/tests/shift_expectation_service/test_annotate_with_working_status.py diff --git a/tapir/shifts/services/shift_expectation_service.py b/tapir/shifts/services/shift_expectation_service.py index a4918798..ca340c35 100644 --- a/tapir/shifts/services/shift_expectation_service.py +++ b/tapir/shifts/services/shift_expectation_service.py @@ -62,7 +62,7 @@ def annotate_shift_user_data_queryset_with_working_status_at_datetime( # joined before date working_shift_user_datas = working_shift_user_datas.filter( - user__date_joined__lte=reference_date + user__date_joined__lte=reference_time ) # member status active diff --git a/tapir/shifts/tests/shift_expectation_service/test_annotate_with_working_status.py b/tapir/shifts/tests/shift_expectation_service/test_annotate_with_working_status.py new file mode 100644 index 00000000..32a56c29 --- /dev/null +++ b/tapir/shifts/tests/shift_expectation_service/test_annotate_with_working_status.py @@ -0,0 +1,129 @@ +import datetime + +from django.utils import timezone + +from tapir.accounts.tests.factories.factories import TapirUserFactory +from tapir.shifts.models import ShiftUserData, ShiftExemption +from tapir.shifts.services.shift_expectation_service import ShiftExpectationService +from tapir.utils.tests_utils import TapirFactoryTestBase, mock_timezone_now + + +class TestAnnotateWithWorkingStatus(TapirFactoryTestBase): + NOW = datetime.datetime(year=2024, month=10, day=5) + REFERENCE_DATETIME = timezone.make_aware( + datetime.datetime(year=2024, month=6, day=10) + ) + + def setUp(self) -> None: + super().setUp() + self.NOW = mock_timezone_now(self, self.NOW) + + def test_annotateShiftUserDataQuerysetWithWorkingStatusAtDatetime_memberShouldWork_annotatesTrue( + self, + ): + TapirUserFactory.create( + date_joined=self.REFERENCE_DATETIME - datetime.timedelta(days=1) + ) + + result = ShiftExpectationService.annotate_shift_user_data_queryset_with_working_status_at_datetime( + ShiftUserData.objects.all(), self.REFERENCE_DATETIME + ) + + shift_user_data = result.get() + self.assertEqual( + True, + getattr( + shift_user_data, ShiftExpectationService.ANNOTATION_IS_WORKING_AT_DATE + ), + ) + self.assertEqual( + self.REFERENCE_DATETIME, + getattr( + shift_user_data, + ShiftExpectationService.ANNOTATION_IS_WORKING_DATE_CHECK, + ), + ) + + def test_annotateShiftUserDataQuerysetWithWorkingStatusAtDatetime_memberIsFrozen_annotatesFalse( + self, + ): + tapir_user = TapirUserFactory.create( + date_joined=self.REFERENCE_DATETIME - datetime.timedelta(days=1) + ) + tapir_user.shift_user_data.is_frozen = True + tapir_user.shift_user_data.save() + + result = ShiftExpectationService.annotate_shift_user_data_queryset_with_working_status_at_datetime( + ShiftUserData.objects.all(), self.REFERENCE_DATETIME + ) + + shift_user_data = result.get() + self.assertEqual( + False, + getattr( + shift_user_data, ShiftExpectationService.ANNOTATION_IS_WORKING_AT_DATE + ), + ) + + def test_annotateShiftUserDataQuerysetWithWorkingStatusAtDatetime_memberJoinedAfterData_annotatesFalse( + self, + ): + TapirUserFactory.create( + date_joined=self.REFERENCE_DATETIME + datetime.timedelta(days=1) + ) + + result = ShiftExpectationService.annotate_shift_user_data_queryset_with_working_status_at_datetime( + ShiftUserData.objects.all(), self.REFERENCE_DATETIME + ) + + shift_user_data = result.get() + self.assertEqual( + False, + getattr( + shift_user_data, ShiftExpectationService.ANNOTATION_IS_WORKING_AT_DATE + ), + ) + + def test_annotateShiftUserDataQuerysetWithWorkingStatusAtDatetime_memberIsNotActive_annotatesFalse( + self, + ): + TapirUserFactory.create( + date_joined=self.REFERENCE_DATETIME + datetime.timedelta(days=1), + share_owner__nb_shares=0, + ) + + result = ShiftExpectationService.annotate_shift_user_data_queryset_with_working_status_at_datetime( + ShiftUserData.objects.all(), self.REFERENCE_DATETIME + ) + + shift_user_data = result.get() + self.assertEqual( + False, + getattr( + shift_user_data, ShiftExpectationService.ANNOTATION_IS_WORKING_AT_DATE + ), + ) + + def test_annotateShiftUserDataQuerysetWithWorkingStatusAtDatetime_memberHasExemption_annotatesFalse( + self, + ): + tapir_user = TapirUserFactory.create( + date_joined=self.REFERENCE_DATETIME + datetime.timedelta(days=1) + ) + ShiftExemption.objects.create( + start_date=self.REFERENCE_DATETIME.date() - datetime.timedelta(days=1), + end_date=self.REFERENCE_DATETIME.date() + datetime.timedelta(days=1), + shift_user_data=tapir_user.shift_user_data, + ) + + result = ShiftExpectationService.annotate_shift_user_data_queryset_with_working_status_at_datetime( + ShiftUserData.objects.all(), self.REFERENCE_DATETIME + ) + + shift_user_data = result.get() + self.assertEqual( + False, + getattr( + shift_user_data, ShiftExpectationService.ANNOTATION_IS_WORKING_AT_DATE + ), + ) From 2eac89b76de04a0e567e00541c462e9cd87efcfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Sat, 7 Dec 2024 12:25:50 +0100 Subject: [PATCH 11/50] Improved test_annotateShiftUserDataQuerysetWithIsFrozenAtDatetimeAfterRefactor_hasRelevantLogEntry_annotatesLogEntryValue --- .../tests/test_frozen_status_history_service.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tapir/shifts/tests/test_frozen_status_history_service.py b/tapir/shifts/tests/test_frozen_status_history_service.py index cc231b0b..657d778a 100644 --- a/tapir/shifts/tests/test_frozen_status_history_service.py +++ b/tapir/shifts/tests/test_frozen_status_history_service.py @@ -285,10 +285,20 @@ def test_annotateShiftUserDataQuerysetWithIsFrozenAtDatetimeAfterRefactor_hasRel new_values={"is_frozen": False}, ) log_entry_in_the_past.created_date = reference_datetime + datetime.timedelta( - days=1 + days=5 ) log_entry_in_the_past.save() + not_relevant_log_entry = UpdateShiftUserDataLogEntry.objects.create( + user=tapir_user, + old_values={"shift_partner": 182}, + new_values={"shift_partner": 25}, + ) + not_relevant_log_entry.created_date = reference_datetime + datetime.timedelta( + days=3 + ) + not_relevant_log_entry.save() + queryset = FrozenStatusHistoryService._annotate_shift_user_data_queryset_with_is_frozen_at_datetime_after_refactor( ShiftUserData.objects.all(), reference_datetime ) From f7554270e9b16b800338f2f993ffba0ab1c4da4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Sat, 7 Dec 2024 12:42:04 +0100 Subject: [PATCH 12/50] Added tests for NumberOfAbcdMembersAtDateView --- .../test_shift_attendance_mode_service.py | 33 ++-------- .../statistics/tests/fancy_graph/__init__.py | 0 .../test_number_of_abcd_members_view.py | 64 +++++++++++++++++++ tapir/utils/tests_utils.py | 25 ++++++++ 4 files changed, 96 insertions(+), 26 deletions(-) create mode 100644 tapir/statistics/tests/fancy_graph/__init__.py create mode 100644 tapir/statistics/tests/fancy_graph/test_number_of_abcd_members_view.py diff --git a/tapir/shifts/tests/test_shift_attendance_mode_service.py b/tapir/shifts/tests/test_shift_attendance_mode_service.py index 10d0c46d..44431271 100644 --- a/tapir/shifts/tests/test_shift_attendance_mode_service.py +++ b/tapir/shifts/tests/test_shift_attendance_mode_service.py @@ -10,14 +10,14 @@ ShiftAttendanceMode, CreateShiftAttendanceTemplateLogEntry, DeleteShiftAttendanceTemplateLogEntry, - ShiftTemplate, - ShiftAttendanceTemplate, ) from tapir.shifts.services.shift_attendance_mode_service import ( ShiftAttendanceModeService, ) -from tapir.shifts.tests.factories import ShiftTemplateFactory -from tapir.utils.tests_utils import TapirFactoryTestBase +from tapir.utils.tests_utils import ( + TapirFactoryTestBase, + create_attendance_template_log_entry_in_the_past, +) class TestShiftAttendanceModeService(TapirFactoryTestBase): @@ -117,25 +117,6 @@ def test_annotateQuerysetWithHasAbcdAttendanceAtDate_noLogEntries_annotatesFalse ) @staticmethod - def create_attendance_template_log_entry_in_the_past( - log_class, tapir_user, reference_datetime - ): - shift_template: ShiftTemplate = ShiftTemplateFactory.create() - shift_attendance_template = ShiftAttendanceTemplate.objects.create( - user=tapir_user, slot_template=shift_template.slot_templates.first() - ) - kwargs = { - "actor": None, - "tapir_user": tapir_user, - "shift_attendance_template": shift_attendance_template, - } - if log_class == DeleteShiftAttendanceTemplateLogEntry: - kwargs["comment"] = "A test comment" - log_entry = log_class().populate(**kwargs) - log_entry.save() - log_entry.created_date = reference_datetime - datetime.timedelta(days=1) - log_entry.save() - def test_annotateQuerysetWithHasAbcdAttendanceAtDate_moreDeleteThanCreateEntries_annotatesFalse( self, ): @@ -169,10 +150,10 @@ def test_annotateQuerysetWithHasAbcdAttendanceAtDate_lessDeleteThanCreateEntries reference_datetime = timezone.now() for _ in range(2): - self.create_attendance_template_log_entry_in_the_past( + create_attendance_template_log_entry_in_the_past( CreateShiftAttendanceTemplateLogEntry, tapir_user, reference_datetime ) - self.create_attendance_template_log_entry_in_the_past( + create_attendance_template_log_entry_in_the_past( DeleteShiftAttendanceTemplateLogEntry, tapir_user, reference_datetime ) @@ -217,7 +198,7 @@ def test_annotateShiftUserDataQuerysetWithAttendanceModeAtDatetime_userHasAbcdSh tapir_user.shift_user_data.is_frozen = False tapir_user.shift_user_data.save() reference_datetime = timezone.now() - self.create_attendance_template_log_entry_in_the_past( + create_attendance_template_log_entry_in_the_past( CreateShiftAttendanceTemplateLogEntry, tapir_user, reference_datetime ) diff --git a/tapir/statistics/tests/fancy_graph/__init__.py b/tapir/statistics/tests/fancy_graph/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tapir/statistics/tests/fancy_graph/test_number_of_abcd_members_view.py b/tapir/statistics/tests/fancy_graph/test_number_of_abcd_members_view.py new file mode 100644 index 00000000..a1438208 --- /dev/null +++ b/tapir/statistics/tests/fancy_graph/test_number_of_abcd_members_view.py @@ -0,0 +1,64 @@ +import datetime + +from django.utils import timezone + +from tapir.accounts.tests.factories.factories import TapirUserFactory +from tapir.shifts.models import CreateShiftAttendanceTemplateLogEntry +from tapir.statistics.views.fancy_graph.number_of_abcd_members_view import ( + NumberOfAbcdMembersAtDateView, +) +from tapir.utils.tests_utils import ( + TapirFactoryTestBase, + mock_timezone_now, + create_attendance_template_log_entry_in_the_past, +) + + +class TestNumberOfAbcdMembersView(TapirFactoryTestBase): + NOW = datetime.datetime(year=2023, month=4, day=1, hour=12) + REFERENCE_TIME = timezone.make_aware( + datetime.datetime(year=2022, month=6, day=15, hour=12) + ) + + def setUp(self) -> None: + super().setUp() + self.NOW = mock_timezone_now(self, self.NOW) + + def test_calculateDatapoint_memberIsAbcdButIsNotWorking_notCounted(self): + tapir_user = TapirUserFactory.create( + date_joined=self.REFERENCE_TIME + datetime.timedelta(days=1) + ) + create_attendance_template_log_entry_in_the_past( + CreateShiftAttendanceTemplateLogEntry, tapir_user, self.REFERENCE_TIME + ) + + result = NumberOfAbcdMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(0, result) + + def test_calculateDatapoint_memberIsWorkingButIsNotAbcd_notCounted(self): + TapirUserFactory.create( + date_joined=self.REFERENCE_TIME - datetime.timedelta(days=1) + ) + + result = NumberOfAbcdMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(0, result) + + def test_calculateDatapoint_memberIsWorkingAnAbcd_counted(self): + tapir_user = TapirUserFactory.create( + date_joined=self.REFERENCE_TIME - datetime.timedelta(days=1) + ) + create_attendance_template_log_entry_in_the_past( + CreateShiftAttendanceTemplateLogEntry, tapir_user, self.REFERENCE_TIME + ) + + result = NumberOfAbcdMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(1, result) diff --git a/tapir/utils/tests_utils.py b/tapir/utils/tests_utils.py index 181f927d..d7582e93 100644 --- a/tapir/utils/tests_utils.py +++ b/tapir/utils/tests_utils.py @@ -28,6 +28,11 @@ from tapir.accounts.tests.factories.factories import TapirUserFactory from tapir.coop.pdfs import CONTENT_TYPE_PDF from tapir.core.tapir_email_base import TapirEmailBase +from tapir.shifts.models import ( + ShiftAttendanceTemplate, + DeleteShiftAttendanceTemplateLogEntry, +) +from tapir.shifts.tests.factories import ShiftTemplateFactory from tapir.utils.expection_utils import TapirException from tapir.utils.json_user import JsonUser from tapir.utils.shortcuts import get_admin_ldap_connection, set_group_membership @@ -286,3 +291,23 @@ def test_accessView_loggedInAsMemberOfGroup_accessAsExpected(self, group): else HTTPStatus.FORBIDDEN ), ) + + +def create_attendance_template_log_entry_in_the_past( + log_class, tapir_user, reference_datetime +): + shift_template = ShiftTemplateFactory.create() + shift_attendance_template = ShiftAttendanceTemplate.objects.create( + user=tapir_user, slot_template=shift_template.slot_templates.first() + ) + kwargs = { + "actor": None, + "tapir_user": tapir_user, + "shift_attendance_template": shift_attendance_template, + } + if log_class == DeleteShiftAttendanceTemplateLogEntry: + kwargs["comment"] = "A test comment" + log_entry = log_class().populate(**kwargs) + log_entry.save() + log_entry.created_date = reference_datetime - datetime.timedelta(days=1) + log_entry.save() From 5f2f87771950032ffc7a99518f1e5005ade4f27a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Sat, 7 Dec 2024 13:41:13 +0100 Subject: [PATCH 13/50] Added tests for NumberOfActiveMembersAtDateView --- .../test_number_of_active_members_view.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 tapir/statistics/tests/fancy_graph/test_number_of_active_members_view.py diff --git a/tapir/statistics/tests/fancy_graph/test_number_of_active_members_view.py b/tapir/statistics/tests/fancy_graph/test_number_of_active_members_view.py new file mode 100644 index 00000000..1598d3cf --- /dev/null +++ b/tapir/statistics/tests/fancy_graph/test_number_of_active_members_view.py @@ -0,0 +1,45 @@ +import datetime + +from django.utils import timezone + +from tapir.coop.models import ShareOwnership +from tapir.coop.tests.factories import ShareOwnerFactory +from tapir.statistics.views.fancy_graph.number_of_active_members_view import ( + NumberOfActiveMembersAtDateView, +) +from tapir.utils.tests_utils import ( + TapirFactoryTestBase, + mock_timezone_now, +) + + +class TestNumberOfActiveMembersView(TapirFactoryTestBase): + NOW = datetime.datetime(year=2023, month=4, day=1, hour=12) + REFERENCE_TIME = timezone.make_aware( + datetime.datetime(year=2022, month=6, day=15, hour=12) + ) + + def setUp(self) -> None: + super().setUp() + self.NOW = mock_timezone_now(self, self.NOW) + + def test_calculateDatapoint_memberIsNotActive_notCounted(self): + ShareOwnerFactory.create(nb_shares=0) + + result = NumberOfActiveMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(0, result) + + def test_calculateDatapoint_memberIsActive_counted(self): + ShareOwnerFactory.create(nb_shares=1, is_investing=False) + ShareOwnership.objects.update( + start_date=self.REFERENCE_TIME.date() - datetime.timedelta(days=1) + ) + + result = NumberOfActiveMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(1, result) From 56128521df95efa112977b63047e014dfb7f3d47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Sat, 7 Dec 2024 13:52:05 +0100 Subject: [PATCH 14/50] Translation file and fix for test_annotateQuerysetWithHasAbcdAttendanceAtDate_moreDeleteThanCreateEntries_annotatesFalse --- .../tests/test_shift_attendance_mode_service.py | 5 ++--- .../translations/locale/de/LC_MESSAGES/django.po | 16 ++++++++-------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/tapir/shifts/tests/test_shift_attendance_mode_service.py b/tapir/shifts/tests/test_shift_attendance_mode_service.py index 44431271..338c4949 100644 --- a/tapir/shifts/tests/test_shift_attendance_mode_service.py +++ b/tapir/shifts/tests/test_shift_attendance_mode_service.py @@ -116,17 +116,16 @@ def test_annotateQuerysetWithHasAbcdAttendanceAtDate_noLogEntries_annotatesFalse False, ) - @staticmethod def test_annotateQuerysetWithHasAbcdAttendanceAtDate_moreDeleteThanCreateEntries_annotatesFalse( self, ): tapir_user = TapirUserFactory.create() reference_datetime = timezone.now() - self.create_attendance_template_log_entry_in_the_past( + create_attendance_template_log_entry_in_the_past( CreateShiftAttendanceTemplateLogEntry, tapir_user, reference_datetime ) - self.create_attendance_template_log_entry_in_the_past( + create_attendance_template_log_entry_in_the_past( DeleteShiftAttendanceTemplateLogEntry, tapir_user, reference_datetime ) diff --git a/tapir/translations/locale/de/LC_MESSAGES/django.po b/tapir/translations/locale/de/LC_MESSAGES/django.po index f46123d7..61b060a3 100644 --- a/tapir/translations/locale/de/LC_MESSAGES/django.po +++ b/tapir/translations/locale/de/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-06 14:19+0100\n" +"POT-Creation-Date: 2024-12-07 13:51+0100\n" "PO-Revision-Date: 2024-11-22 08:33+0100\n" "Last-Translator: \n" "Language-Team: \n" @@ -1069,7 +1069,7 @@ msgid "" msgstr "" #: coop/templates/coop/email/accounting_recap.body.default.html:21 -#: statistics/views/main_view.py:304 +#: statistics/views/main_view.py:299 msgid "New members" msgstr "Neue Mitglieder" @@ -4621,7 +4621,7 @@ msgid "Main statistics" msgstr "Hauptstatistik" #: statistics/templates/statistics/main_statistics.html:19 -#: statistics/views/main_view.py:263 +#: statistics/views/main_view.py:258 msgid "Total number of members" msgstr "Gesamte Mitgliederzahl" @@ -4718,7 +4718,7 @@ msgstr "" #: statistics/templates/statistics/main_statistics.html:118 #: statistics/templates/statistics/main_statistics.html:129 -#: statistics/views/main_view.py:385 +#: statistics/views/main_view.py:380 msgid "Frozen members" msgstr "Eingefrorene Mitglieder" @@ -4844,19 +4844,19 @@ msgstr "" msgid "Evolution of total spends per month" msgstr "Entwicklung der Gesamtausgaben pro Monat" -#: statistics/views/main_view.py:335 +#: statistics/views/main_view.py:330 msgid "Total spends per month" msgstr "Gesamtausgaben pro Monat" -#: statistics/views/main_view.py:385 +#: statistics/views/main_view.py:380 msgid "Purchasing members" msgstr "Einkaufsberechtigten Mitglieder*innen" -#: statistics/views/main_view.py:403 +#: statistics/views/main_view.py:398 msgid "Percentage of members with a co-purchaser relative to the number of active members" msgstr "Prozentualer Anteil der Mitglieder mit einem Miterwerber im Verhältnis zur Zahl der aktiven Mitglieder" -#: statistics/views/main_view.py:555 +#: statistics/views/main_view.py:550 msgid "Purchase data updated" msgstr "Kaufdaten aktualisiert" From 48291ea60da015255a9da7ddfe9519b15164a25a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Sat, 7 Dec 2024 14:31:34 +0100 Subject: [PATCH 15/50] Moved ShareOwner.can_shop to it's own service. --- tapir/coop/models.py | 8 ---- .../coop/services/investing_status_service.py | 4 +- .../coop/services/member_can_shop_service.py | 47 +++++++++++++++++++ .../coop/services/membership_pause_service.py | 4 +- .../coop/services/number_of_shares_service.py | 4 +- .../shifts/services/shift_can_shop_service.py | 23 +++++++++ .../number_of_co_purchasers_view.py | 13 +++-- .../number_of_purchasing_members_view.py | 46 +++++------------- tapir/statistics/views/main_view.py | 10 ++-- tapir/statistics/views/stats_for_marie.py | 3 +- tapir/welcomedesk/serializers.py | 3 +- 11 files changed, 106 insertions(+), 59 deletions(-) create mode 100644 tapir/coop/services/member_can_shop_service.py diff --git a/tapir/coop/models.py b/tapir/coop/models.py index c3add19d..a95e2d29 100644 --- a/tapir/coop/models.py +++ b/tapir/coop/models.py @@ -18,7 +18,6 @@ from tapir.coop.services.number_of_shares_service import NumberOfSharesService from tapir.core.config import help_text_displayed_name from tapir.log.models import UpdateModelLogEntry, ModelLogEntry, LogEntry -from tapir.shifts.services.shift_can_shop_service import ShiftCanShopService from tapir.utils.expection_utils import TapirException from tapir.utils.models import ( DurationModelMixin, @@ -286,13 +285,6 @@ def get_member_status(self, at_datetime: datetime.datetime | datetime.date = Non return MemberStatus.ACTIVE - def can_shop(self, at_datetime: datetime.datetime | datetime.date | None = None): - return ( - self.user is not None - and self.is_active(at_datetime) - and ShiftCanShopService.can_shop(self, at_datetime) - ) - def is_active( self, at_datetime: datetime.datetime | datetime.date | None = None ) -> bool: diff --git a/tapir/coop/services/investing_status_service.py b/tapir/coop/services/investing_status_service.py index 841e3919..de8aa044 100644 --- a/tapir/coop/services/investing_status_service.py +++ b/tapir/coop/services/investing_status_service.py @@ -3,7 +3,7 @@ import datetime from typing import TYPE_CHECKING -from django.db.models import Value, OuterRef, Q, Case, When +from django.db.models import Value, OuterRef, Q, Case, When, QuerySet from django.db.models.functions import Coalesce from django.utils import timezone @@ -42,7 +42,7 @@ def is_investing( @classmethod def annotate_share_owner_queryset_with_investing_status_at_datetime( cls, - queryset: ShareOwner.ShareOwnerQuerySet, + queryset: QuerySet[ShareOwner], at_datetime: datetime.datetime = None, ): if at_datetime is None: diff --git a/tapir/coop/services/member_can_shop_service.py b/tapir/coop/services/member_can_shop_service.py new file mode 100644 index 00000000..cd481c6d --- /dev/null +++ b/tapir/coop/services/member_can_shop_service.py @@ -0,0 +1,47 @@ +import datetime + +from django.db.models import QuerySet, Case, When, Value + +from tapir.coop.models import ShareOwner, MemberStatus +from tapir.shifts.services.shift_can_shop_service import ShiftCanShopService + + +class MemberCanShopService: + ANNOTATION_CAN_SHOP = "can_shop_at_date" + ANNOTATION_CAN_SHOP_DATE_CHECK = "can_shop_date_check" + + @staticmethod + def can_shop( + share_owner: ShareOwner, + at_datetime: datetime.datetime | datetime.date | None = None, + ): + return ( + share_owner.user is not None + and share_owner.is_active(at_datetime) + and ShiftCanShopService.can_shop(share_owner, at_datetime) + ) + + @classmethod + def annotate_share_owner_queryset_with_shopping_status_at_datetime( + cls, share_owners: QuerySet[ShareOwner], reference_datetime: datetime.datetime + ): + members_who_can_shop = share_owners.filter(user__isnull=False) + members_who_can_shop = members_who_can_shop.with_status(MemberStatus.ACTIVE) + members_who_can_shop = ( + ShiftCanShopService.annotate_share_owner_queryset_with_can_shop_at_datetime( + members_who_can_shop, reference_datetime + ) + ) + members_who_can_shop = members_who_can_shop.filter( + **{ShiftCanShopService.ANNOTATION_SHIFT_CAN_SHOP: True} + ) + ids = members_who_can_shop.values_list("id", flat=True) + + return share_owners.annotate( + **{ + cls.ANNOTATION_CAN_SHOP: Case( + When(id__in=ids, then=True), default=False + ), + cls.ANNOTATION_CAN_SHOP_DATE_CHECK: Value(reference_datetime), + } + ) diff --git a/tapir/coop/services/membership_pause_service.py b/tapir/coop/services/membership_pause_service.py index ac406ecd..c39d7084 100644 --- a/tapir/coop/services/membership_pause_service.py +++ b/tapir/coop/services/membership_pause_service.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING from django.contrib.auth.models import User -from django.db.models import Q, Value, Count +from django.db.models import Q, Value, Count, QuerySet from django.utils import timezone from tapir.accounts.models import TapirUser @@ -86,7 +86,7 @@ def has_active_pause(cls, share_owner: ShareOwner, at_date: datetime.date = None @classmethod def annotate_share_owner_queryset_with_has_active_pause( - cls, queryset: ShareOwner.ShareOwnerQuerySet, at_date: datetime.date = None + cls, queryset: QuerySet[ShareOwner], at_date: datetime.date = None ): if at_date is None: at_date = timezone.now().date() diff --git a/tapir/coop/services/number_of_shares_service.py b/tapir/coop/services/number_of_shares_service.py index b9e5eedb..d3b01325 100644 --- a/tapir/coop/services/number_of_shares_service.py +++ b/tapir/coop/services/number_of_shares_service.py @@ -3,7 +3,7 @@ import datetime from typing import TYPE_CHECKING -from django.db.models import Count, Q, Value +from django.db.models import Count, Q, Value, QuerySet from django.utils import timezone if TYPE_CHECKING: @@ -36,7 +36,7 @@ def get_number_of_active_shares( @classmethod def annotate_share_owner_queryset_with_nb_of_active_shares( - cls, queryset: ShareOwner.ShareOwnerQuerySet, at_date: datetime.date = None + cls, queryset: QuerySet[ShareOwner], at_date: datetime.date = None ): if at_date is None: at_date = timezone.now().date() diff --git a/tapir/shifts/services/shift_can_shop_service.py b/tapir/shifts/services/shift_can_shop_service.py index aa16d150..4ed700d3 100644 --- a/tapir/shifts/services/shift_can_shop_service.py +++ b/tapir/shifts/services/shift_can_shop_service.py @@ -3,6 +3,8 @@ import datetime import typing +from django.db.models import QuerySet, Case, When + from tapir.shifts.models import ShiftUserData from tapir.shifts.services.frozen_status_history_service import ( FrozenStatusHistoryService, @@ -13,6 +15,9 @@ class ShiftCanShopService: + ANNOTATION_SHIFT_CAN_SHOP = "shift_can_shop" + ANNOTATION_SHIFT_CAN_SHOP_DATE_CHECK = "shift_can_shop_date_check" + @classmethod def can_shop( cls, @@ -22,3 +27,21 @@ def can_shop( return not FrozenStatusHistoryService.is_frozen_at_datetime( member_object, at_datetime ) + + @staticmethod + def annotate_share_owner_queryset_with_can_shop_at_datetime( + share_owners: QuerySet[ShareOwner], reference_datetime: datetime.datetime + ): + share_owners = FrozenStatusHistoryService.annotate_share_owner_queryset_with_is_frozen_at_datetime( + share_owners, reference_datetime + ) + + return share_owners.annotate( + Case( + When( + **{FrozenStatusHistoryService.ANNOTATION_IS_FROZEN_AT_DATE: True}, + then=True, + ), + default=False, + ) + ) diff --git a/tapir/statistics/views/fancy_graph/number_of_co_purchasers_view.py b/tapir/statistics/views/fancy_graph/number_of_co_purchasers_view.py index 770c322a..af29e720 100644 --- a/tapir/statistics/views/fancy_graph/number_of_co_purchasers_view.py +++ b/tapir/statistics/views/fancy_graph/number_of_co_purchasers_view.py @@ -4,17 +4,20 @@ from tapir.accounts.services.co_purchaser_history_service import ( CoPurchaserHistoryService, ) +from tapir.coop.models import ShareOwner +from tapir.coop.services.member_can_shop_service import MemberCanShopService from tapir.statistics.views.fancy_graph.base_view import DatapointView -from tapir.statistics.views.fancy_graph.number_of_purchasing_members_view import ( - NumberOfPurchasingMembersAtDateView, -) class NumberOfCoPurchasersAtDateView(DatapointView): def calculate_datapoint(self, reference_time: datetime.datetime) -> int: tapir_users = TapirUser.objects.all() - purchasing_members = NumberOfPurchasingMembersAtDateView.get_purchasing_members( - reference_time + + purchasing_members = MemberCanShopService.annotate_share_owner_queryset_with_shopping_status_at_datetime( + ShareOwner.objects.all(), reference_time + ) + purchasing_members = purchasing_members.filter( + **{MemberCanShopService.ANNOTATION_CAN_SHOP: True} ) tapir_users = tapir_users.filter(share_owner__in=purchasing_members) diff --git a/tapir/statistics/views/fancy_graph/number_of_purchasing_members_view.py b/tapir/statistics/views/fancy_graph/number_of_purchasing_members_view.py index 7dec5317..c8e7e60b 100644 --- a/tapir/statistics/views/fancy_graph/number_of_purchasing_members_view.py +++ b/tapir/statistics/views/fancy_graph/number_of_purchasing_members_view.py @@ -1,44 +1,22 @@ from tapir.coop.models import ShareOwner -from tapir.coop.services.investing_status_service import InvestingStatusService -from tapir.coop.services.membership_pause_service import MembershipPauseService -from tapir.coop.services.number_of_shares_service import NumberOfSharesService -from tapir.shifts.services.frozen_status_history_service import ( - FrozenStatusHistoryService, -) +from tapir.coop.services.member_can_shop_service import MemberCanShopService from tapir.statistics.views.fancy_graph.base_view import DatapointView class NumberOfPurchasingMembersAtDateView(DatapointView): def calculate_datapoint(self, reference_time) -> int: - return len(self.get_purchasing_members(reference_time)) + share_owners = MemberCanShopService.annotate_share_owner_queryset_with_shopping_status_at_datetime( + ShareOwner.objects.all(), reference_time + ) + return share_owners.filter( + **{MemberCanShopService.ANNOTATION_CAN_SHOP: True} + ).count() @staticmethod def get_purchasing_members(reference_time): - reference_date = reference_time.date() - - share_owners = ( - ShareOwner.objects.all() - .prefetch_related("user") - .prefetch_related("user__shift_user_data") - .prefetch_related("share_ownerships") - ) - share_owners = NumberOfSharesService.annotate_share_owner_queryset_with_nb_of_active_shares( - share_owners, reference_date - ) - share_owners = ( - MembershipPauseService.annotate_share_owner_queryset_with_has_active_pause( - share_owners, reference_date - ) + share_owners = MemberCanShopService.annotate_share_owner_queryset_with_shopping_status_at_datetime( + ShareOwner.objects.all(), reference_time ) - share_owners = InvestingStatusService.annotate_share_owner_queryset_with_investing_status_at_datetime( - share_owners, reference_time - ) - share_owners = FrozenStatusHistoryService.annotate_share_owner_queryset_with_is_frozen_at_datetime( - share_owners, reference_time - ) - - return [ - share_owner - for share_owner in share_owners - if share_owner.can_shop(reference_time) - ] + return share_owners.filter( + **{MemberCanShopService.ANNOTATION_CAN_SHOP: True} + ).count() diff --git a/tapir/statistics/views/main_view.py b/tapir/statistics/views/main_view.py index 1cd8c4db..5986a7ac 100644 --- a/tapir/statistics/views/main_view.py +++ b/tapir/statistics/views/main_view.py @@ -19,6 +19,7 @@ ) from tapir.coop.models import ShareOwnership, ShareOwner, MemberStatus from tapir.coop.services.investing_status_service import InvestingStatusService +from tapir.coop.services.member_can_shop_service import MemberCanShopService from tapir.coop.services.membership_pause_service import MembershipPauseService from tapir.coop.services.number_of_shares_service import NumberOfSharesService from tapir.coop.views import ShareCountEvolutionJsonView @@ -100,7 +101,7 @@ def get_purchasing_members_context(self): [ share_owner for share_owner in share_owners - if share_owner.can_shop(self.reference_time) + if MemberCanShopService.can_shop(share_owner, self.reference_time) ] ) @@ -188,9 +189,10 @@ def get_working_members_context(self): ] ) - context = dict() - context["target_count"] = ShiftSlotTemplate.objects.count() - context["current_count"] = current_number_of_working_members + context = { + "target_count": ShiftSlotTemplate.objects.count(), + "current_count": current_number_of_working_members, + } context["missing_count"] = context["target_count"] - context["current_count"] context["progress"] = round( 100 * context["current_count"] / context["target_count"] diff --git a/tapir/statistics/views/stats_for_marie.py b/tapir/statistics/views/stats_for_marie.py index e63ee59c..0d5130d5 100644 --- a/tapir/statistics/views/stats_for_marie.py +++ b/tapir/statistics/views/stats_for_marie.py @@ -10,6 +10,7 @@ from tapir.coop.models import ShareOwner, MemberStatus, ShareOwnership from tapir.coop.services.investing_status_service import InvestingStatusService +from tapir.coop.services.member_can_shop_service import MemberCanShopService from tapir.coop.services.membership_pause_service import MembershipPauseService from tapir.coop.services.number_of_shares_service import NumberOfSharesService from tapir.settings import PERMISSION_COOP_MANAGE @@ -224,7 +225,7 @@ def get_data(dates): [ share_owner for share_owner in share_owners - if share_owner.can_shop(date_with_time) + if MemberCanShopService.can_shop(date_with_time) ] ) data.append(number_of_purchasing_members_at_date) diff --git a/tapir/welcomedesk/serializers.py b/tapir/welcomedesk/serializers.py index 98d44755..efe58301 100644 --- a/tapir/welcomedesk/serializers.py +++ b/tapir/welcomedesk/serializers.py @@ -4,6 +4,7 @@ from rest_framework import serializers from tapir.coop.models import ShareOwner +from tapir.coop.services.member_can_shop_service import MemberCanShopService from tapir.utils.user_utils import UserUtils from tapir.welcomedesk.services.welcome_desk_reasons_cannot_shop_service import ( WelcomeDeskReasonsCannotShopService, @@ -46,7 +47,7 @@ def get_display_name(self, share_owner: ShareOwner) -> str: return UserUtils.build_display_name(share_owner, display_type) def get_can_shop(self, share_owner: ShareOwner) -> bool: - return share_owner.can_shop(self.reference_time) + return MemberCanShopService.can_shop(share_owner, self.reference_time) @staticmethod def get_co_purchaser(share_owner: ShareOwner) -> str | None: From c19bc434e196b8aa3d238fd1e921f0fd4ba15008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Tue, 10 Dec 2024 18:15:01 +0100 Subject: [PATCH 16/50] Fixed date range picked having to "date from" fields --- src/statistics/components/DateRangePicker.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/statistics/components/DateRangePicker.tsx b/src/statistics/components/DateRangePicker.tsx index 61a9840b..e0d7b2d6 100644 --- a/src/statistics/components/DateRangePicker.tsx +++ b/src/statistics/components/DateRangePicker.tsx @@ -35,7 +35,7 @@ const DateRangePicker: React.FC = ({ - + Date: Tue, 10 Dec 2024 18:59:49 +0100 Subject: [PATCH 17/50] Added options to the fancy graph: - add today's date or not - use first of month or last of month --- src/statistics/FancyGraphCard.tsx | 25 +---- src/statistics/components/DateRangePicker.tsx | 101 +++++++++++++++--- src/statistics/utils.tsx | 4 + 3 files changed, 94 insertions(+), 36 deletions(-) diff --git a/src/statistics/FancyGraphCard.tsx b/src/statistics/FancyGraphCard.tsx index a240225c..32cda775 100644 --- a/src/statistics/FancyGraphCard.tsx +++ b/src/statistics/FancyGraphCard.tsx @@ -68,27 +68,6 @@ const FancyGraphCard: React.FC = () => { setDateFrom(getFirstOfMonth(dateFromOnPageLoad)); }, []); - useEffect(() => { - if (!dateFrom || !dateTo) return; - - let currentDate = new Date(dateFrom); - const dates = []; - while (currentDate <= dateTo) { - dates.push(currentDate); - currentDate = new Date(currentDate); - currentDate.setDate(currentDate.getDate() + 32); - currentDate.setDate(1); - } - dates.push(currentDate); - - const tomorrow = new Date(); - tomorrow.setDate(tomorrow.getDate() + 1); - dates.push(tomorrow); - - setDates(dates); - setGraphLabels(dates.map((date) => formatDate(date))); - }, [dateFrom, dateTo]); - useEffect(() => { fillCachedData(); buildAndSetGraphData(); @@ -253,12 +232,14 @@ const FancyGraphCard: React.FC = () => {
{gettext("Graph")} {fetching && }
- + string; @@ -9,6 +10,8 @@ interface DateRangePickerProps { setDateFrom: (date: Date) => void; dateTo: Date; setDateTo: (date: Date) => void; + setDates: (dates: Date[]) => void; + setGraphLabels: (graphLabels: string[]) => void; } const DateRangePicker: React.FC = ({ @@ -16,20 +19,94 @@ const DateRangePicker: React.FC = ({ setDateFrom, dateTo, setDateTo, + setDates, + setGraphLabels, }) => { + const [includeToday, setIncludeToday] = useState(true); + const [startOfMonth, setStartOfMonth] = useState(true); + + useEffect(() => { + if (!dateFrom || !dateTo) return; + + let currentDate = new Date(dateFrom); + const dates = []; + while (currentDate <= dateTo) { + dates.push(currentDate); + currentDate = new Date(currentDate); + currentDate.setDate(currentDate.getDate() + (startOfMonth ? 32 : 1)); + currentDate.setDate(1); + currentDate = adaptDate(currentDate); + } + dates.push(currentDate); + + if (includeToday) { + const today = new Date(); + let todayAlreadyInArray = false; + for (const date of dates) { + if ( + date.getDate() === today.getDate() && + date.getMonth() === today.getMonth() && + date.getFullYear() === today.getFullYear() + ) { + todayAlreadyInArray = true; + break; + } + } + if (!todayAlreadyInArray) { + dates.push(today); + } + } + + dates.sort((date1, date2) => date1.getTime() - date2.getTime()); + + setDates(dates); + setGraphLabels(dates.map((date) => formatDate(date))); + }, [dateFrom, dateTo, includeToday, startOfMonth]); + + function adaptDate(date: Date) { + let dateAdapter = getFirstOfMonth; + if (!startOfMonth) { + dateAdapter = getLastOfMonth; + } + return dateAdapter(date); + } + + function getDateInputValue(date: Date) { + if (isNaN(date.getTime())) { + return undefined; + } + return date.toISOString().substring(0, 10); + } + return ( <> + + { + setIncludeToday(e.target.checked); + }} + label={"Include today"} + /> + + + { + setStartOfMonth(event.target.value === "startOfMonth"); + }} + > + + + + { - setDateFrom(getFirstOfMonth(new Date(event.target.value))); + setDateFrom(adaptDate(new Date(event.target.value))); }} /> @@ -38,13 +115,9 @@ const DateRangePicker: React.FC = ({ { - setDateTo(getFirstOfMonth(new Date(event.target.value))); + setDateTo(adaptDate(new Date(event.target.value))); }} /> diff --git a/src/statistics/utils.tsx b/src/statistics/utils.tsx index 2ed6af2b..02fcc07a 100644 --- a/src/statistics/utils.tsx +++ b/src/statistics/utils.tsx @@ -2,3 +2,7 @@ export function getFirstOfMonth(date: Date) { date.setDate(1); return date; } + +export function getLastOfMonth(date: Date) { + return new Date(date.getFullYear(), date.getMonth() + 1, 0); +} From 77ca71f97e8c66ff73575afb8b9dd5eb0ae7a753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Tue, 10 Dec 2024 19:05:09 +0100 Subject: [PATCH 18/50] Added a distinct() to most stat views. --- .../fancy_graph/number_of_abcd_members_view.py | 2 +- .../fancy_graph/number_of_active_members_view.py | 8 +++++--- .../fancy_graph/number_of_co_purchasers_view.py | 2 +- .../number_of_created_resignations_view.py | 12 ++++++++---- .../number_of_exempted_members_view.py | 11 +++++------ .../fancy_graph/number_of_flying_members_view.py | 4 ++-- .../fancy_graph/number_of_frozen_members_view.py | 2 +- .../number_of_investing_members_view.py | 8 +++++--- .../views/fancy_graph/number_of_members_view.py | 8 +++++--- .../fancy_graph/number_of_paused_members_view.py | 8 +++++--- .../number_of_pending_resignations_view.py | 10 +++++++--- .../number_of_purchasing_members_view.py | 15 ++++----------- .../fancy_graph/number_of_shift_partners_view.py | 2 +- .../fancy_graph/number_of_working_members_view.py | 3 ++- 14 files changed, 52 insertions(+), 43 deletions(-) diff --git a/tapir/statistics/views/fancy_graph/number_of_abcd_members_view.py b/tapir/statistics/views/fancy_graph/number_of_abcd_members_view.py index 647b73fb..9e4357b1 100644 --- a/tapir/statistics/views/fancy_graph/number_of_abcd_members_view.py +++ b/tapir/statistics/views/fancy_graph/number_of_abcd_members_view.py @@ -28,4 +28,4 @@ def calculate_datapoint(self, reference_time: datetime.datetime) -> int: } ) - return working_and_abcd_members.count() + return working_and_abcd_members.distinct().count() diff --git a/tapir/statistics/views/fancy_graph/number_of_active_members_view.py b/tapir/statistics/views/fancy_graph/number_of_active_members_view.py index a81bd7ba..64042141 100644 --- a/tapir/statistics/views/fancy_graph/number_of_active_members_view.py +++ b/tapir/statistics/views/fancy_graph/number_of_active_members_view.py @@ -7,6 +7,8 @@ class NumberOfActiveMembersAtDateView(DatapointView): def calculate_datapoint(self, reference_time: datetime.datetime) -> int: reference_date = reference_time.date() - return ShareOwner.objects.with_status( - MemberStatus.ACTIVE, reference_date - ).count() + return ( + ShareOwner.objects.with_status(MemberStatus.ACTIVE, reference_date) + .distinct() + .count() + ) diff --git a/tapir/statistics/views/fancy_graph/number_of_co_purchasers_view.py b/tapir/statistics/views/fancy_graph/number_of_co_purchasers_view.py index af29e720..0d770e10 100644 --- a/tapir/statistics/views/fancy_graph/number_of_co_purchasers_view.py +++ b/tapir/statistics/views/fancy_graph/number_of_co_purchasers_view.py @@ -28,4 +28,4 @@ def calculate_datapoint(self, reference_time: datetime.datetime) -> int: **{CoPurchaserHistoryService.ANNOTATION_HAS_CO_PURCHASER: True} ) - return tapir_users.count() + return tapir_users.distinct().count() diff --git a/tapir/statistics/views/fancy_graph/number_of_created_resignations_view.py b/tapir/statistics/views/fancy_graph/number_of_created_resignations_view.py index f2dde1b7..891d2b6b 100644 --- a/tapir/statistics/views/fancy_graph/number_of_created_resignations_view.py +++ b/tapir/statistics/views/fancy_graph/number_of_created_resignations_view.py @@ -8,7 +8,11 @@ class NumberOfCreatedResignationsInSameMonthView(DatapointView): def calculate_datapoint(self, reference_time: datetime.datetime) -> int: reference_date = reference_time.date() - return MembershipResignation.objects.filter( - cancellation_date__year=reference_date.year, - cancellation_date__month=reference_date.month, - ).count() + return ( + MembershipResignation.objects.filter( + cancellation_date__year=reference_date.year, + cancellation_date__month=reference_date.month, + ) + .distinct() + .count() + ) diff --git a/tapir/statistics/views/fancy_graph/number_of_exempted_members_view.py b/tapir/statistics/views/fancy_graph/number_of_exempted_members_view.py index b14cb0b3..c75b3f1e 100644 --- a/tapir/statistics/views/fancy_graph/number_of_exempted_members_view.py +++ b/tapir/statistics/views/fancy_graph/number_of_exempted_members_view.py @@ -28,10 +28,9 @@ def calculate_datapoint(self, reference_time: datetime.datetime) -> int: ) exemptions = ShiftExemption.objects.active_temporal(reference_date) - exemptions.filter( - shift_user_data__user__share_owner__in=members_that_joined_before_date - ) - return exemptions.filter( - shift_user_data__user__share_owner__in=members_that_joined_before_date - ).count() + exempted_members = members_that_joined_before_date.filter( + user__shift_user_data__exemption__in=exemptions + ).distinct() + + return exempted_members.count() diff --git a/tapir/statistics/views/fancy_graph/number_of_flying_members_view.py b/tapir/statistics/views/fancy_graph/number_of_flying_members_view.py index fd1a8140..c176fa84 100644 --- a/tapir/statistics/views/fancy_graph/number_of_flying_members_view.py +++ b/tapir/statistics/views/fancy_graph/number_of_flying_members_view.py @@ -20,7 +20,7 @@ def calculate_datapoint(self, reference_time: datetime.datetime) -> int: ) ).filter(**{ShiftExpectationService.ANNOTATION_IS_WORKING_AT_DATE: True}) - working_and_abcd_members = ShiftAttendanceModeService.annotate_shift_user_data_queryset_with_attendance_mode_at_datetime( + working_and_flying_members = ShiftAttendanceModeService.annotate_shift_user_data_queryset_with_attendance_mode_at_datetime( working_members, reference_time ).filter( **{ @@ -28,4 +28,4 @@ def calculate_datapoint(self, reference_time: datetime.datetime) -> int: } ) - return working_and_abcd_members.count() + return working_and_flying_members.distinct().count() diff --git a/tapir/statistics/views/fancy_graph/number_of_frozen_members_view.py b/tapir/statistics/views/fancy_graph/number_of_frozen_members_view.py index 70e693ea..ce4b5820 100644 --- a/tapir/statistics/views/fancy_graph/number_of_frozen_members_view.py +++ b/tapir/statistics/views/fancy_graph/number_of_frozen_members_view.py @@ -45,4 +45,4 @@ def get_members_frozen_at_datetime(reference_time): return share_owners.filter( **{FrozenStatusHistoryService.ANNOTATION_IS_FROZEN_AT_DATE: True} - ) + ).distinct() diff --git a/tapir/statistics/views/fancy_graph/number_of_investing_members_view.py b/tapir/statistics/views/fancy_graph/number_of_investing_members_view.py index 0d40dcc3..371ad30c 100644 --- a/tapir/statistics/views/fancy_graph/number_of_investing_members_view.py +++ b/tapir/statistics/views/fancy_graph/number_of_investing_members_view.py @@ -8,6 +8,8 @@ class NumberOfInvestingMembersAtDateView(DatapointView): def calculate_datapoint(self, reference_time: datetime.datetime) -> int: reference_date = reference_time.date() - return ShareOwner.objects.with_status( - MemberStatus.INVESTING, reference_date - ).count() + return ( + ShareOwner.objects.with_status(MemberStatus.INVESTING, reference_date) + .distinct() + .count() + ) diff --git a/tapir/statistics/views/fancy_graph/number_of_members_view.py b/tapir/statistics/views/fancy_graph/number_of_members_view.py index 19ec0437..5c3c6cfa 100644 --- a/tapir/statistics/views/fancy_graph/number_of_members_view.py +++ b/tapir/statistics/views/fancy_graph/number_of_members_view.py @@ -13,7 +13,9 @@ def calculate_datapoint(self, reference_time: datetime.datetime) -> int: MemberStatus.PAUSED, MemberStatus.INVESTING, ]: - total_count += ShareOwner.objects.with_status( - member_status, reference_date - ).count() + total_count += ( + ShareOwner.objects.with_status(member_status, reference_date) + .distinct() + .count() + ) return total_count diff --git a/tapir/statistics/views/fancy_graph/number_of_paused_members_view.py b/tapir/statistics/views/fancy_graph/number_of_paused_members_view.py index 5d9da660..8ba7142c 100644 --- a/tapir/statistics/views/fancy_graph/number_of_paused_members_view.py +++ b/tapir/statistics/views/fancy_graph/number_of_paused_members_view.py @@ -7,6 +7,8 @@ class NumberOfPausedMembersAtDateView(DatapointView): def calculate_datapoint(self, reference_time: datetime.datetime) -> int: reference_date = reference_time.date() - return ShareOwner.objects.with_status( - MemberStatus.PAUSED, reference_date - ).count() + return ( + ShareOwner.objects.with_status(MemberStatus.PAUSED, reference_date) + .distinct() + .count() + ) diff --git a/tapir/statistics/views/fancy_graph/number_of_pending_resignations_view.py b/tapir/statistics/views/fancy_graph/number_of_pending_resignations_view.py index c0ddaa2d..1a79b00b 100644 --- a/tapir/statistics/views/fancy_graph/number_of_pending_resignations_view.py +++ b/tapir/statistics/views/fancy_graph/number_of_pending_resignations_view.py @@ -8,6 +8,10 @@ class NumberOfPendingResignationsAtDateView(DatapointView): def calculate_datapoint(self, reference_time: datetime.datetime) -> int: reference_date = reference_time.date() - return MembershipResignation.objects.filter( - cancellation_date__lte=reference_date, pay_out_day__gte=reference_date - ).count() + return ( + MembershipResignation.objects.filter( + cancellation_date__lte=reference_date, pay_out_day__gte=reference_date + ) + .distinct() + .count() + ) diff --git a/tapir/statistics/views/fancy_graph/number_of_purchasing_members_view.py b/tapir/statistics/views/fancy_graph/number_of_purchasing_members_view.py index c8e7e60b..648b2c81 100644 --- a/tapir/statistics/views/fancy_graph/number_of_purchasing_members_view.py +++ b/tapir/statistics/views/fancy_graph/number_of_purchasing_members_view.py @@ -8,15 +8,8 @@ def calculate_datapoint(self, reference_time) -> int: share_owners = MemberCanShopService.annotate_share_owner_queryset_with_shopping_status_at_datetime( ShareOwner.objects.all(), reference_time ) - return share_owners.filter( - **{MemberCanShopService.ANNOTATION_CAN_SHOP: True} - ).count() - - @staticmethod - def get_purchasing_members(reference_time): - share_owners = MemberCanShopService.annotate_share_owner_queryset_with_shopping_status_at_datetime( - ShareOwner.objects.all(), reference_time + return ( + share_owners.filter(**{MemberCanShopService.ANNOTATION_CAN_SHOP: True}) + .distinct() + .count() ) - return share_owners.filter( - **{MemberCanShopService.ANNOTATION_CAN_SHOP: True} - ).count() diff --git a/tapir/statistics/views/fancy_graph/number_of_shift_partners_view.py b/tapir/statistics/views/fancy_graph/number_of_shift_partners_view.py index eff72d81..8309eaf2 100644 --- a/tapir/statistics/views/fancy_graph/number_of_shift_partners_view.py +++ b/tapir/statistics/views/fancy_graph/number_of_shift_partners_view.py @@ -25,4 +25,4 @@ def calculate_datapoint(self, reference_time) -> int: **{ShiftPartnerHistoryService.ANNOTATION_HAS_SHIFT_PARTNER: True} ) - return shift_user_datas.count() + return shift_user_datas.distinct().count() diff --git a/tapir/statistics/views/fancy_graph/number_of_working_members_view.py b/tapir/statistics/views/fancy_graph/number_of_working_members_view.py index 327d1760..a16f0423 100644 --- a/tapir/statistics/views/fancy_graph/number_of_working_members_view.py +++ b/tapir/statistics/views/fancy_graph/number_of_working_members_view.py @@ -26,7 +26,8 @@ def calculate_datapoint(self, reference_time: datetime.datetime) -> int: ) shift_user_datas = FrozenStatusHistoryService.annotate_shift_user_data_queryset_with_is_frozen_at_datetime( shift_user_datas, reference_time - ) + ).distinct() + share_owners = NumberOfSharesService.annotate_share_owner_queryset_with_nb_of_active_shares( ShareOwner.objects.all(), reference_date ) From e658726f8c694874a57fa502f9c15760b9f98d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Tue, 10 Dec 2024 19:44:52 +0100 Subject: [PATCH 19/50] Added description for data set long-term frozen members. --- src/statistics/datasets.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/statistics/datasets.tsx b/src/statistics/datasets.tsx index 2227fb99..036ea8f3 100644 --- a/src/statistics/datasets.tsx +++ b/src/statistics/datasets.tsx @@ -109,6 +109,9 @@ export const datasets: { [key: string]: Dataset } = { }, [datasetNumberOfLongTermFrozenMembers]: { display_name: gettext("Long-term frozen members"), + description: gettext( + "Members that are frozen since more than 180 days (roughly 6 month)", + ), apiCall: api.statisticsNumberOfLongTermFrozenMembersAtDateRetrieve, chart_type: "line", relative: false, From 32e320673e3aab0cdf19171463fc9b015dd4454e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Tue, 10 Dec 2024 19:52:47 +0100 Subject: [PATCH 20/50] Removed all references to the UpdateShiftUserDataLogEntry of the "old style" (with attendance_mode), since now they have been updated to the "new style" (with is_frozen) --- tapir/shifts/config.py | 7 - .../services/frozen_status_history_service.py | 94 ++-------- .../frozen_status_management_service.py | 18 -- .../test_frozen_status_history_service.py | 162 +----------------- .../test_frozen_status_management_service.py | 64 ------- .../number_of_frozen_members_view.py | 23 +-- ...number_of_long_term_frozen_members_view.py | 32 +--- 7 files changed, 27 insertions(+), 373 deletions(-) diff --git a/tapir/shifts/config.py b/tapir/shifts/config.py index 378fbb30..02afd422 100644 --- a/tapir/shifts/config.py +++ b/tapir/shifts/config.py @@ -1,7 +1,5 @@ import datetime -from tapir.utils.shortcuts import get_timezone_aware_datetime - cycle_start_dates = [ datetime.date(year=2022, month=4, day=11), ] @@ -18,8 +16,3 @@ FEATURE_FLAG_FLYING_MEMBERS_REGISTRATION_REMINDER = ( "feature_flags.shifts.flying_members_registration_reminder" ) - -ATTENDANCE_MODE_REFACTOR_DATE = datetime.date(year=2024, month=11, day=11) -ATTENDANCE_MODE_REFACTOR_DATETIME = get_timezone_aware_datetime( - ATTENDANCE_MODE_REFACTOR_DATE, datetime.time(hour=0, minute=0) -) diff --git a/tapir/shifts/services/frozen_status_history_service.py b/tapir/shifts/services/frozen_status_history_service.py index 9aa41da1..892ac4fd 100644 --- a/tapir/shifts/services/frozen_status_history_service.py +++ b/tapir/shifts/services/frozen_status_history_service.py @@ -12,10 +12,8 @@ from django.db.models.functions import Coalesce from django.utils import timezone -from tapir.shifts.config import ATTENDANCE_MODE_REFACTOR_DATETIME from tapir.shifts.models import ( ShiftUserData, - ShiftAttendanceMode, UpdateShiftUserDataLogEntry, ) from tapir.utils.shortcuts import ensure_datetime @@ -59,77 +57,6 @@ def annotate_shift_user_data_queryset_with_is_frozen_at_datetime( queryset = queryset.annotate( **{cls.ANNOTATION_IS_FROZEN_DATE_CHECK: Value(at_datetime)} ) - if at_datetime < ATTENDANCE_MODE_REFACTOR_DATETIME: - return cls._annotate_shift_user_data_queryset_with_is_frozen_at_datetime_before_refactor( - queryset, at_datetime, attendance_mode_prefix - ) - return cls._annotate_shift_user_data_queryset_with_is_frozen_at_datetime_after_refactor( - queryset, at_datetime, attendance_mode_prefix - ) - - @classmethod - def _annotate_shift_user_data_queryset_with_is_frozen_at_datetime_before_refactor( - cls, - queryset: QuerySet, - at_datetime: datetime.datetime = None, - attendance_mode_prefix=None, - ): - queryset = queryset.annotate( - attendance_mode_from_log_entry=Subquery( - UpdateShiftUserDataLogEntry.objects.filter( - user_id=OuterRef("user_id"), - created_date__gte=at_datetime, - old_values__attendance_mode__isnull=False, - ) - .order_by("created_date") - .values("old_values__attendance_mode")[:1], - output_field=CharField(), - ) - ) - - queryset = queryset.annotate( - is_frozen_from_log_entry=Subquery( - UpdateShiftUserDataLogEntry.objects.filter( - user_id=OuterRef("user_id"), - created_date__gte=at_datetime, - ) - .order_by("created_date") - .values("old_values__is_frozen")[:1], - output_field=CharField(), - ) - ) - - queryset = queryset.annotate( - is_frozen_at_date=Case( - When( - attendance_mode_from_log_entry=ShiftAttendanceMode.FROZEN, - then=Value(True), - ), - When( - is_frozen_from_log_entry="True", - then=Value(True), - ), - When( - is_frozen_from_log_entry="False", - then=Value(False), - ), - default=( - "is_frozen" - if not attendance_mode_prefix - else f"{attendance_mode_prefix}__is_frozen" - ), - ) - ) - - return queryset - - @classmethod - def _annotate_shift_user_data_queryset_with_is_frozen_at_datetime_after_refactor( - cls, - queryset: QuerySet, - at_datetime: datetime.datetime = None, - attendance_mode_prefix=None, - ): queryset = queryset.annotate( is_frozen_from_log_entry_as_string=Subquery( UpdateShiftUserDataLogEntry.objects.filter( @@ -151,19 +78,20 @@ def _annotate_shift_user_data_queryset_with_is_frozen_at_datetime_after_refactor ) ) - queryset = queryset.annotate( - is_frozen_at_date=Coalesce( - "is_frozen_from_log_entry_as_bool", - ( - "is_frozen" - if attendance_mode_prefix is None - else f"{attendance_mode_prefix}__is_frozen" + return queryset.annotate( + **{ + cls.ANNOTATION_IS_FROZEN_DATE_CHECK: Value(at_datetime), + cls.ANNOTATION_IS_FROZEN_AT_DATE: Coalesce( + "is_frozen_from_log_entry_as_bool", + ( + "is_frozen" + if attendance_mode_prefix is None + else f"{attendance_mode_prefix}__is_frozen" + ), ), - ), + } ) - return queryset - @classmethod def annotate_share_owner_queryset_with_is_frozen_at_datetime( cls, queryset: QuerySet, at_datetime: datetime.datetime = None diff --git a/tapir/shifts/services/frozen_status_management_service.py b/tapir/shifts/services/frozen_status_management_service.py index 523b7f61..ae92a9ac 100644 --- a/tapir/shifts/services/frozen_status_management_service.py +++ b/tapir/shifts/services/frozen_status_management_service.py @@ -17,7 +17,6 @@ from tapir.shifts.emails.unfreeze_notification_email import UnfreezeNotificationEmail from tapir.shifts.models import ( ShiftUserData, - ShiftAttendanceMode, UpdateShiftUserDataLogEntry, ShiftAttendanceTemplate, ShiftAccountEntry, @@ -179,20 +178,3 @@ def unfreeze_and_send_notification_email( ) email = UnfreezeNotificationEmail() email.send_to_tapir_user(actor=actor, recipient=shift_user_data.user) - - @staticmethod - def _get_last_attendance_mode_before_frozen(shift_user_data: ShiftUserData): - last_freeze_log_entry = ( - UpdateShiftUserDataLogEntry.objects.filter( - new_values__attendance_mode=ShiftAttendanceMode.FROZEN, - user=shift_user_data.user, - ) - .order_by("-created_date") - .first() - ) - - return ( - last_freeze_log_entry.old_values["attendance_mode"] - if last_freeze_log_entry - else ShiftAttendanceMode.FLYING - ) diff --git a/tapir/shifts/tests/test_frozen_status_history_service.py b/tapir/shifts/tests/test_frozen_status_history_service.py index 657d778a..ffa1fcba 100644 --- a/tapir/shifts/tests/test_frozen_status_history_service.py +++ b/tapir/shifts/tests/test_frozen_status_history_service.py @@ -5,11 +5,9 @@ from tapir.accounts.models import TapirUser from tapir.accounts.tests.factories.factories import TapirUserFactory -from tapir.shifts import config from tapir.shifts.models import ( ShiftUserData, UpdateShiftUserDataLogEntry, - ShiftAttendanceMode, ) from tapir.shifts.services.frozen_status_history_service import ( FrozenStatusHistoryService, @@ -92,123 +90,8 @@ def test_isFrozenAtDatetime_annotatedNotFrozen_returnsFalse(self): ) ) - @patch.object( - FrozenStatusHistoryService, - "_annotate_shift_user_data_queryset_with_is_frozen_at_datetime_before_refactor", - ) - @patch.object( - FrozenStatusHistoryService, - "_annotate_shift_user_data_queryset_with_is_frozen_at_datetime_after_refactor", - ) - def test_annotateShiftUserDataQuerysetWithIsFrozenAtDatetime_givenDateIsBeforeRefactor_useCorrectAnnotationMethod( - self, - mock_annotate_shift_user_data_queryset_with_is_frozen_at_datetime_after_refactor: Mock, - mock_annotate_shift_user_data_queryset_with_is_frozen_at_datetime_before_refactor: Mock, - ): - expected_result = Mock() - mock_annotate_shift_user_data_queryset_with_is_frozen_at_datetime_before_refactor.return_value = ( - expected_result - ) - reference_datetime = ( - config.ATTENDANCE_MODE_REFACTOR_DATETIME - datetime.timedelta(days=1) - ) - - actual_result = FrozenStatusHistoryService.annotate_shift_user_data_queryset_with_is_frozen_at_datetime( - ShiftUserData.objects.none(), - reference_datetime, - ) - - self.assertEqual(actual_result, expected_result) - mock_annotate_shift_user_data_queryset_with_is_frozen_at_datetime_before_refactor.assert_called_once() - mock_annotate_shift_user_data_queryset_with_is_frozen_at_datetime_after_refactor.assert_not_called() - - @patch.object( - FrozenStatusHistoryService, - "_annotate_shift_user_data_queryset_with_is_frozen_at_datetime_before_refactor", - ) - @patch.object( - FrozenStatusHistoryService, - "_annotate_shift_user_data_queryset_with_is_frozen_at_datetime_after_refactor", - ) - def test_annotateShiftUserDataQuerysetWithIsFrozenAtDatetime_givenDateIsAfterRefactor_useCorrectAnnotationMethod( - self, - mock_annotate_shift_user_data_queryset_with_is_frozen_at_datetime_after_refactor: Mock, - mock_annotate_shift_user_data_queryset_with_is_frozen_at_datetime_before_refactor: Mock, - ): - expected_result = Mock() - mock_annotate_shift_user_data_queryset_with_is_frozen_at_datetime_after_refactor.return_value = ( - expected_result - ) - reference_datetime = ( - config.ATTENDANCE_MODE_REFACTOR_DATETIME + datetime.timedelta(days=1) - ) - - actual_result = FrozenStatusHistoryService.annotate_shift_user_data_queryset_with_is_frozen_at_datetime( - ShiftUserData.objects.none(), - reference_datetime, - ) - - self.assertEqual(actual_result, expected_result) - mock_annotate_shift_user_data_queryset_with_is_frozen_at_datetime_before_refactor.assert_not_called() - mock_annotate_shift_user_data_queryset_with_is_frozen_at_datetime_after_refactor.assert_called_once() - - def test_annotateShiftUserDataQuerysetWithIsFrozenAtDatetimeBeforeRefactor_noRelevantLogEntry_annotatesCurrentValue( - self, - ): - tapir_user: TapirUser = TapirUserFactory.create() - tapir_user.shift_user_data.is_frozen = False - tapir_user.shift_user_data.save() - reference_datetime = timezone.now() - log_entry_in_the_past = UpdateShiftUserDataLogEntry.objects.create( - user=tapir_user, - old_values={"attendance_mode": ShiftAttendanceMode.FROZEN}, - new_values={}, - ) - log_entry_in_the_past.created_date = reference_datetime - datetime.timedelta( - days=1 - ) - log_entry_in_the_past.save() - - log_entry_irrelevant = UpdateShiftUserDataLogEntry.objects.create( - user=tapir_user, - old_values={"shift_partner": "12"}, - new_values={"shift_partner": "13"}, - ) - log_entry_irrelevant.created_date = reference_datetime + datetime.timedelta( - days=1 - ) - log_entry_irrelevant.save() - - queryset = FrozenStatusHistoryService._annotate_shift_user_data_queryset_with_is_frozen_at_datetime_before_refactor( - ShiftUserData.objects.all(), reference_datetime - ) - - shift_user_data = queryset.first() - self.assertEqual( - getattr( - shift_user_data, - FrozenStatusHistoryService.ANNOTATION_IS_FROZEN_AT_DATE, - ), - False, - ) - - def test_annotateShiftUserDataQuerysetWithIsFrozenAtDatetimeBeforeRefactor_hasRelevantLogEntry_annotatesLogEntryValue( - self, - ): - tapir_user: TapirUser = TapirUserFactory.create() - tapir_user.shift_user_data.is_frozen = False - tapir_user.shift_user_data.save() - reference_datetime = timezone.now() - relevant_log_entry = UpdateShiftUserDataLogEntry.objects.create( - user=tapir_user, - old_values={"attendance_mode": ShiftAttendanceMode.FROZEN}, - new_values={}, - ) - relevant_log_entry.created_date = reference_datetime + datetime.timedelta( - days=5 - ) - relevant_log_entry.save() - + @staticmethod + def create_irrelevant_log_entry(tapir_user, reference_datetime): not_relevant_log_entry = UpdateShiftUserDataLogEntry.objects.create( user=tapir_user, old_values={"shift_partner": 182}, @@ -219,20 +102,7 @@ def test_annotateShiftUserDataQuerysetWithIsFrozenAtDatetimeBeforeRefactor_hasRe ) not_relevant_log_entry.save() - queryset = FrozenStatusHistoryService._annotate_shift_user_data_queryset_with_is_frozen_at_datetime_before_refactor( - ShiftUserData.objects.all(), reference_datetime - ) - - shift_user_data = queryset.first() - self.assertEqual( - getattr( - shift_user_data, - FrozenStatusHistoryService.ANNOTATION_IS_FROZEN_AT_DATE, - ), - True, - ) - - def test_annotateShiftUserDataQuerysetWithIsFrozenAtDatetimeAfterRefactor_noRelevantLogEntry_annotatesCurrentValue( + def test_annotateShiftUserDataQuerysetWithIsFrozenAtDatetime_noRelevantLogEntry_annotatesCurrentValue( self, ): tapir_user: TapirUser = TapirUserFactory.create() @@ -249,17 +119,9 @@ def test_annotateShiftUserDataQuerysetWithIsFrozenAtDatetimeAfterRefactor_noRele ) log_entry_in_the_past.save() - not_relevant_log_entry = UpdateShiftUserDataLogEntry.objects.create( - user=tapir_user, - old_values={"shift_partner": 182}, - new_values={"shift_partner": 25}, - ) - not_relevant_log_entry.created_date = reference_datetime + datetime.timedelta( - days=3 - ) - not_relevant_log_entry.save() + self.create_irrelevant_log_entry(tapir_user, reference_datetime) - queryset = FrozenStatusHistoryService._annotate_shift_user_data_queryset_with_is_frozen_at_datetime_after_refactor( + queryset = FrozenStatusHistoryService.annotate_shift_user_data_queryset_with_is_frozen_at_datetime( ShiftUserData.objects.all(), reference_datetime ) @@ -272,7 +134,7 @@ def test_annotateShiftUserDataQuerysetWithIsFrozenAtDatetimeAfterRefactor_noRele False, ) - def test_annotateShiftUserDataQuerysetWithIsFrozenAtDatetimeAfterRefactor_hasRelevantLogEntry_annotatesLogEntryValue( + def test_annotateShiftUserDataQuerysetWithIsFrozenAtDatetime_hasRelevantLogEntry_annotatesLogEntryValue( self, ): tapir_user: TapirUser = TapirUserFactory.create() @@ -289,17 +151,9 @@ def test_annotateShiftUserDataQuerysetWithIsFrozenAtDatetimeAfterRefactor_hasRel ) log_entry_in_the_past.save() - not_relevant_log_entry = UpdateShiftUserDataLogEntry.objects.create( - user=tapir_user, - old_values={"shift_partner": 182}, - new_values={"shift_partner": 25}, - ) - not_relevant_log_entry.created_date = reference_datetime + datetime.timedelta( - days=3 - ) - not_relevant_log_entry.save() + self.create_irrelevant_log_entry(tapir_user, reference_datetime) - queryset = FrozenStatusHistoryService._annotate_shift_user_data_queryset_with_is_frozen_at_datetime_after_refactor( + queryset = FrozenStatusHistoryService.annotate_shift_user_data_queryset_with_is_frozen_at_datetime( ShiftUserData.objects.all(), reference_datetime ) diff --git a/tapir/shifts/tests/test_frozen_status_management_service.py b/tapir/shifts/tests/test_frozen_status_management_service.py index 174c6c0b..e02b5c13 100644 --- a/tapir/shifts/tests/test_frozen_status_management_service.py +++ b/tapir/shifts/tests/test_frozen_status_management_service.py @@ -584,10 +584,6 @@ def test_shouldUnfreezeMember_memberNotRegisteredToEnoughShifts_returnsFalse( @patch( "tapir.shifts.services.frozen_status_management_service.UnfreezeNotificationEmail" ) - @patch.object( - FrozenStatusManagementService, - "_get_last_attendance_mode_before_frozen", - ) @patch.object( FrozenStatusManagementService, "_update_frozen_status_and_create_log_entry", @@ -595,15 +591,11 @@ def test_shouldUnfreezeMember_memberNotRegisteredToEnoughShifts_returnsFalse( def test_unfreezeAndSendNotificationEmail( self, mock_update_frozen_status_and_create_log_entry: Mock, - mock_get_last_attendance_mode_before_frozen: Mock, mock_unfreeze_notification_email_class: Mock, ): shift_user_data = ShiftUserData() shift_user_data.user = TapirUser() actor = TapirUser() - mock_get_last_attendance_mode_before_frozen.return_value = ( - ShiftAttendanceMode.REGULAR - ) FrozenStatusManagementService.unfreeze_and_send_notification_email( shift_user_data, actor @@ -619,59 +611,3 @@ def test_unfreezeAndSendNotificationEmail( mock_email.send_to_tapir_user.assert_called_once_with( actor=actor, recipient=shift_user_data.user ) - - @patch( - "tapir.shifts.services.frozen_status_management_service.UpdateShiftUserDataLogEntry.objects.filter" - ) - def test_getLastAttendanceModeBeforeFrozen_noLogEntryFound_returnsFlying( - self, mock_filter: Mock - ): - shift_user_data = ShiftUserData() - shift_user_data.user = TapirUser() - mock_order_by = mock_filter.return_value.order_by - mock_first = mock_order_by.return_value.first - mock_first.return_value = None - - self.assertEqual( - ShiftAttendanceMode.FLYING, - FrozenStatusManagementService._get_last_attendance_mode_before_frozen( - shift_user_data - ), - ) - - mock_filter.assert_called_once_with( - new_values__attendance_mode=ShiftAttendanceMode.FROZEN, - user=shift_user_data.user, - ) - mock_order_by.assert_called_once_with("-created_date") - mock_first.assert_called_once_with() - - @patch( - "tapir.shifts.services.frozen_status_management_service.UpdateShiftUserDataLogEntry.objects.filter" - ) - def test_getLastAttendanceModeBeforeFrozen_default_returnsLastMode( - self, mock_filter: Mock - ): - shift_user_data = ShiftUserData() - shift_user_data.user = TapirUser() - mock_order_by = mock_filter.return_value.order_by - mock_first = mock_order_by.return_value.first - mock_first.return_value = Mock() - mock_first.return_value.old_values = dict() - mock_first.return_value.old_values["attendance_mode"] = ( - ShiftAttendanceMode.REGULAR - ) - - self.assertEqual( - ShiftAttendanceMode.REGULAR, - FrozenStatusManagementService._get_last_attendance_mode_before_frozen( - shift_user_data - ), - ) - - mock_filter.assert_called_once_with( - new_values__attendance_mode=ShiftAttendanceMode.FROZEN, - user=shift_user_data.user, - ) - mock_order_by.assert_called_once_with("-created_date") - mock_first.assert_called_once_with() diff --git a/tapir/statistics/views/fancy_graph/number_of_frozen_members_view.py b/tapir/statistics/views/fancy_graph/number_of_frozen_members_view.py index ce4b5820..77e8be0a 100644 --- a/tapir/statistics/views/fancy_graph/number_of_frozen_members_view.py +++ b/tapir/statistics/views/fancy_graph/number_of_frozen_members_view.py @@ -1,9 +1,6 @@ import datetime from tapir.coop.models import ShareOwner, MemberStatus -from tapir.coop.services.investing_status_service import InvestingStatusService -from tapir.coop.services.membership_pause_service import MembershipPauseService -from tapir.coop.services.number_of_shares_service import NumberOfSharesService from tapir.settings import PERMISSION_COOP_MANAGE from tapir.shifts.services.frozen_status_history_service import ( FrozenStatusHistoryService, @@ -19,25 +16,9 @@ def calculate_datapoint(self, reference_time: datetime.datetime) -> int: @staticmethod def get_members_frozen_at_datetime(reference_time): - reference_date = reference_time.date() - share_owners = ( - ShareOwner.objects.all() - .prefetch_related("user") - .prefetch_related("user__shift_user_data") - .prefetch_related("share_ownerships") + share_owners = ShareOwner.objects.with_status( + MemberStatus.ACTIVE, reference_time ) - share_owners = NumberOfSharesService.annotate_share_owner_queryset_with_nb_of_active_shares( - share_owners, reference_date - ) - share_owners = ( - MembershipPauseService.annotate_share_owner_queryset_with_has_active_pause( - share_owners, reference_date - ) - ) - share_owners = InvestingStatusService.annotate_share_owner_queryset_with_investing_status_at_datetime( - share_owners, reference_time - ) - share_owners = share_owners.with_status(MemberStatus.ACTIVE) share_owners = FrozenStatusHistoryService.annotate_share_owner_queryset_with_is_frozen_at_datetime( share_owners, reference_time diff --git a/tapir/statistics/views/fancy_graph/number_of_long_term_frozen_members_view.py b/tapir/statistics/views/fancy_graph/number_of_long_term_frozen_members_view.py index 027f5d26..1e6e5a1d 100644 --- a/tapir/statistics/views/fancy_graph/number_of_long_term_frozen_members_view.py +++ b/tapir/statistics/views/fancy_graph/number_of_long_term_frozen_members_view.py @@ -1,7 +1,7 @@ import datetime from tapir.settings import PERMISSION_COOP_MANAGE -from tapir.shifts.models import UpdateShiftUserDataLogEntry, ShiftAttendanceMode +from tapir.shifts.models import UpdateShiftUserDataLogEntry from tapir.statistics.views.fancy_graph.base_view import DatapointView from tapir.statistics.views.fancy_graph.number_of_frozen_members_view import ( NumberOfFrozenMembersAtDateView, @@ -28,31 +28,11 @@ def calculate_datapoint(self, reference_time: datetime.datetime) -> int: .first() ) - if status_change_log_entry: - if ( - reference_time - status_change_log_entry.created_date - ).days > 30 * 6: - count += 1 - continue + if not status_change_log_entry: + # could not find any log entry, we assume the member is frozen long-term + count += 1 - status_change_log_entry = ( - UpdateShiftUserDataLogEntry.objects.filter( - user=share_owner.user, - created_date__lte=reference_time, - new_values__attendance_mode=ShiftAttendanceMode.FROZEN, - ) - .order_by("-created_date") - .first() - ) - - if status_change_log_entry: - if ( - reference_time - status_change_log_entry.created_date - ).days > 30 * 6: - count += 1 - continue - - # could not find any log entry, we assume the member is frozen long-term - count += 1 + if (reference_time - status_change_log_entry.created_date).days > 30 * 6: + count += 1 return count From 125155a975cfbafbcd494427b5d2462ac283aa87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Wed, 11 Dec 2024 11:15:13 +0100 Subject: [PATCH 21/50] Added tests for MemberCanShopService --- .../coop/services/member_can_shop_service.py | 24 ++- .../tests/test_member_can_shop_service.py | 147 ++++++++++++++++++ .../shifts/services/shift_can_shop_service.py | 23 +-- 3 files changed, 179 insertions(+), 15 deletions(-) create mode 100644 tapir/coop/tests/test_member_can_shop_service.py diff --git a/tapir/coop/services/member_can_shop_service.py b/tapir/coop/services/member_can_shop_service.py index cd481c6d..823be5ee 100644 --- a/tapir/coop/services/member_can_shop_service.py +++ b/tapir/coop/services/member_can_shop_service.py @@ -3,6 +3,9 @@ from django.db.models import QuerySet, Case, When, Value from tapir.coop.models import ShareOwner, MemberStatus +from tapir.shifts.services.frozen_status_history_service import ( + FrozenStatusHistoryService, +) from tapir.shifts.services.shift_can_shop_service import ShiftCanShopService @@ -15,18 +18,27 @@ def can_shop( share_owner: ShareOwner, at_datetime: datetime.datetime | datetime.date | None = None, ): - return ( - share_owner.user is not None - and share_owner.is_active(at_datetime) - and ShiftCanShopService.can_shop(share_owner, at_datetime) - ) + if share_owner.user is None: + return False + if not share_owner.is_active(at_datetime): + return False + + member_object = share_owner + if not hasattr( + member_object, FrozenStatusHistoryService.ANNOTATION_IS_FROZEN_AT_DATE + ): + member_object = share_owner.user.shift_user_data + + return ShiftCanShopService.can_shop(member_object, at_datetime) @classmethod def annotate_share_owner_queryset_with_shopping_status_at_datetime( cls, share_owners: QuerySet[ShareOwner], reference_datetime: datetime.datetime ): members_who_can_shop = share_owners.filter(user__isnull=False) - members_who_can_shop = members_who_can_shop.with_status(MemberStatus.ACTIVE) + members_who_can_shop = members_who_can_shop.with_status( + MemberStatus.ACTIVE, reference_datetime + ) members_who_can_shop = ( ShiftCanShopService.annotate_share_owner_queryset_with_can_shop_at_datetime( members_who_can_shop, reference_datetime diff --git a/tapir/coop/tests/test_member_can_shop_service.py b/tapir/coop/tests/test_member_can_shop_service.py new file mode 100644 index 00000000..3b7ba34b --- /dev/null +++ b/tapir/coop/tests/test_member_can_shop_service.py @@ -0,0 +1,147 @@ +import datetime + +from django.utils import timezone + +from tapir.accounts.tests.factories.factories import TapirUserFactory +from tapir.coop.models import ShareOwner, ShareOwnership +from tapir.coop.services.member_can_shop_service import MemberCanShopService +from tapir.coop.tests.factories import ShareOwnerFactory +from tapir.shifts.models import ShiftUserData +from tapir.utils.tests_utils import TapirFactoryTestBase + + +class TestMemberCanShopService(TapirFactoryTestBase): + REFERENCE_TIME = timezone.make_aware(datetime.datetime(year=2024, month=6, day=1)) + + def test_canShop_memberCanShop_annotatedWithTrue( + self, + ): + tapir_user = TapirUserFactory.create( + share_owner__nb_shares=1, share_owner__is_investing=False + ) + ShareOwnership.objects.update( + start_date=self.REFERENCE_TIME.date() - datetime.timedelta(days=1) + ) + + self.assertTrue( + MemberCanShopService.can_shop(tapir_user.share_owner, self.REFERENCE_TIME) + ) + + def test_canShop_memberHasNoTapirUser_annotatedWithFalse( + self, + ): + share_owner = ShareOwnerFactory.create(nb_shares=1) + ShareOwnership.objects.update( + start_date=self.REFERENCE_TIME.date() - datetime.timedelta(days=1) + ) + + self.assertFalse( + MemberCanShopService.can_shop(share_owner, self.REFERENCE_TIME) + ) + + def test_canShop_memberIsNotActive_annotatedWithFalse( + self, + ): + tapir_user = TapirUserFactory.create( + share_owner__nb_shares=1, share_owner__is_investing=False + ) + ShareOwnership.objects.update( + start_date=self.REFERENCE_TIME.date() + datetime.timedelta(days=1) + ) + + self.assertFalse( + MemberCanShopService.can_shop(tapir_user.share_owner, self.REFERENCE_TIME) + ) + + def test_canShop_memberIsFrozen_annotatedWithFalse( + self, + ): + tapir_user = TapirUserFactory.create( + share_owner__nb_shares=1, share_owner__is_investing=False + ) + ShareOwnership.objects.update( + start_date=self.REFERENCE_TIME.date() - datetime.timedelta(days=1) + ) + ShiftUserData.objects.update(is_frozen=True) + + self.assertFalse( + MemberCanShopService.can_shop(tapir_user.share_owner, self.REFERENCE_TIME) + ) + + def test_annotateShareOwnerQuerysetWithShoppingStatusAtDatetime_memberCanShop_annotatedWithTrue( + self, + ): + TapirUserFactory.create( + share_owner__nb_shares=1, share_owner__is_investing=False + ) + ShareOwnership.objects.update( + start_date=self.REFERENCE_TIME.date() - datetime.timedelta(days=1) + ) + + queryset = MemberCanShopService.annotate_share_owner_queryset_with_shopping_status_at_datetime( + ShareOwner.objects.all(), self.REFERENCE_TIME + ) + + self.assertEqual(1, queryset.count()) + self.assertTrue( + getattr(queryset.first(), MemberCanShopService.ANNOTATION_CAN_SHOP) + ) + self.assertEqual( + self.REFERENCE_TIME, + getattr( + queryset.first(), MemberCanShopService.ANNOTATION_CAN_SHOP_DATE_CHECK + ), + ) + + def test_annotateShareOwnerQuerysetWithShoppingStatusAtDatetime_memberHasNoTapirUser_annotatedWithFalse( + self, + ): + ShareOwnerFactory.create(nb_shares=1) + ShareOwnership.objects.update( + start_date=self.REFERENCE_TIME.date() - datetime.timedelta(days=1) + ) + + queryset = MemberCanShopService.annotate_share_owner_queryset_with_shopping_status_at_datetime( + ShareOwner.objects.all(), self.REFERENCE_TIME + ) + + self.assertFalse( + getattr(queryset.first(), MemberCanShopService.ANNOTATION_CAN_SHOP) + ) + + def test_annotateShareOwnerQuerysetWithShoppingStatusAtDatetime_memberIsNotActive_annotatedWithFalse( + self, + ): + TapirUserFactory.create( + share_owner__nb_shares=1, share_owner__is_investing=False + ) + ShareOwnership.objects.update( + start_date=self.REFERENCE_TIME.date() + datetime.timedelta(days=1) + ) + + queryset = MemberCanShopService.annotate_share_owner_queryset_with_shopping_status_at_datetime( + ShareOwner.objects.all(), self.REFERENCE_TIME + ) + + self.assertFalse( + getattr(queryset.first(), MemberCanShopService.ANNOTATION_CAN_SHOP) + ) + + def test_annotateShareOwnerQuerysetWithShoppingStatusAtDatetime_memberIsFrozen_annotatedWithFalse( + self, + ): + TapirUserFactory.create( + share_owner__nb_shares=1, share_owner__is_investing=False + ) + ShareOwnership.objects.update( + start_date=self.REFERENCE_TIME.date() - datetime.timedelta(days=1) + ) + ShiftUserData.objects.update(is_frozen=True) + + queryset = MemberCanShopService.annotate_share_owner_queryset_with_shopping_status_at_datetime( + ShareOwner.objects.all(), self.REFERENCE_TIME + ) + + self.assertFalse( + getattr(queryset.first(), MemberCanShopService.ANNOTATION_CAN_SHOP) + ) diff --git a/tapir/shifts/services/shift_can_shop_service.py b/tapir/shifts/services/shift_can_shop_service.py index 4ed700d3..f3c9d77e 100644 --- a/tapir/shifts/services/shift_can_shop_service.py +++ b/tapir/shifts/services/shift_can_shop_service.py @@ -3,7 +3,7 @@ import datetime import typing -from django.db.models import QuerySet, Case, When +from django.db.models import QuerySet, Case, When, Value from tapir.shifts.models import ShiftUserData from tapir.shifts.services.frozen_status_history_service import ( @@ -28,20 +28,25 @@ def can_shop( member_object, at_datetime ) - @staticmethod + @classmethod def annotate_share_owner_queryset_with_can_shop_at_datetime( - share_owners: QuerySet[ShareOwner], reference_datetime: datetime.datetime + cls, share_owners: QuerySet[ShareOwner], reference_datetime: datetime.datetime ): share_owners = FrozenStatusHistoryService.annotate_share_owner_queryset_with_is_frozen_at_datetime( share_owners, reference_datetime ) return share_owners.annotate( - Case( - When( - **{FrozenStatusHistoryService.ANNOTATION_IS_FROZEN_AT_DATE: True}, - then=True, + **{ + cls.ANNOTATION_SHIFT_CAN_SHOP: Case( + When( + **{ + FrozenStatusHistoryService.ANNOTATION_IS_FROZEN_AT_DATE: True + }, + then=False, + ), + default=True, ), - default=False, - ) + cls.ANNOTATION_SHIFT_CAN_SHOP_DATE_CHECK: Value(reference_datetime), + } ) From 3b227a417a792e85a1c67152021084e3e85da63e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Wed, 11 Dec 2024 12:06:11 +0100 Subject: [PATCH 22/50] Added tests for ShiftCanShopService --- .../shifts/tests/test_ShiftCanShopService.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tapir/shifts/tests/test_ShiftCanShopService.py b/tapir/shifts/tests/test_ShiftCanShopService.py index 8fbf371b..964ffb65 100644 --- a/tapir/shifts/tests/test_ShiftCanShopService.py +++ b/tapir/shifts/tests/test_ShiftCanShopService.py @@ -1,9 +1,17 @@ +import datetime + +from django.utils import timezone + from tapir.accounts.tests.factories.factories import TapirUserFactory +from tapir.coop.models import ShareOwner +from tapir.shifts.models import ShiftUserData from tapir.shifts.services.shift_can_shop_service import ShiftCanShopService from tapir.utils.tests_utils import TapirFactoryTestBase class TestShiftCanShopService(TapirFactoryTestBase): + REFERENCE_TIME = timezone.make_aware(datetime.datetime(year=2023, month=2, day=12)) + def test_canShop_userNotFrozen_canShop(self): shift_user_data = TapirUserFactory.create().shift_user_data shift_user_data.is_frozen = False @@ -15,3 +23,43 @@ def test_canShop_userFrozen_cannotShop(self): shift_user_data.is_frozen = True shift_user_data.save() self.assertFalse(ShiftCanShopService.can_shop(shift_user_data)) + + def test_annotateShareOwnerQuerysetWithCanShopAtDatetime_memberIsNotFrozen_returnsTrue( + self, + ): + TapirUserFactory.create() + ShiftUserData.objects.update(is_frozen=False) + + queryset = ( + ShiftCanShopService.annotate_share_owner_queryset_with_can_shop_at_datetime( + ShareOwner.objects.all(), self.REFERENCE_TIME + ) + ) + + self.assertEqual(1, queryset.count()) + self.assertTrue( + getattr(queryset.first(), ShiftCanShopService.ANNOTATION_SHIFT_CAN_SHOP) + ) + self.assertEqual( + self.REFERENCE_TIME, + getattr( + queryset.first(), + ShiftCanShopService.ANNOTATION_SHIFT_CAN_SHOP_DATE_CHECK, + ), + ) + + def test_annotateShareOwnerQuerysetWithCanShopAtDatetime_memberIsFrozen_returnsFalse( + self, + ): + TapirUserFactory.create() + ShiftUserData.objects.update(is_frozen=True) + + queryset = ( + ShiftCanShopService.annotate_share_owner_queryset_with_can_shop_at_datetime( + ShareOwner.objects.all(), self.REFERENCE_TIME + ) + ) + + self.assertFalse( + getattr(queryset.first(), ShiftCanShopService.ANNOTATION_SHIFT_CAN_SHOP) + ) From b9a69dd48d532091e3576020970944dc5a82b945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Wed, 11 Dec 2024 12:23:00 +0100 Subject: [PATCH 23/50] Added tests for NumberOfCoPurchasersAtDateView --- .../services/co_purchaser_history_service.py | 31 ++++++--- .../test_number_of_abcd_members_view.py | 2 +- .../test_number_of_co_purchasers_view.py | 63 +++++++++++++++++++ 3 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 tapir/statistics/tests/fancy_graph/test_number_of_co_purchasers_view.py diff --git a/tapir/accounts/services/co_purchaser_history_service.py b/tapir/accounts/services/co_purchaser_history_service.py index 4072d466..a8f50873 100644 --- a/tapir/accounts/services/co_purchaser_history_service.py +++ b/tapir/accounts/services/co_purchaser_history_service.py @@ -2,7 +2,17 @@ import datetime -from django.db.models import Value, OuterRef, Case, When, QuerySet, Q +from django.db.models import ( + Value, + OuterRef, + Case, + When, + QuerySet, + Q, + CharField, + Subquery, +) +from django.db.models.functions import Coalesce from django.utils import timezone from tapir.accounts.models import TapirUser, UpdateTapirUserLogEntry @@ -42,13 +52,20 @@ def annotate_tapir_user_queryset_with_has_co_purchaser_at_date( at_datetime = timezone.now() queryset = queryset.annotate( - co_purchaser_at_date=UpdateTapirUserLogEntry.objects.filter( - user_id=OuterRef("id"), - created_date__lte=at_datetime, - new_values__co_purchaser__isnull=False, + co_purchaser_from_log_entry=Subquery( + UpdateTapirUserLogEntry.objects.filter( + user_id=OuterRef("id"), + created_date__lte=at_datetime, + new_values__co_purchaser__isnull=False, + ) + .order_by("-created_date") + .values("new_values__co_purchaser")[:1], + output_field=CharField(), ) - .order_by("-created_date") - .values("new_values__co_purchaser")[:1] + ) + + queryset = queryset.annotate( + co_purchaser_at_date=Coalesce("co_purchaser_from_log_entry", "co_purchaser") ) return queryset.annotate( diff --git a/tapir/statistics/tests/fancy_graph/test_number_of_abcd_members_view.py b/tapir/statistics/tests/fancy_graph/test_number_of_abcd_members_view.py index a1438208..711535c9 100644 --- a/tapir/statistics/tests/fancy_graph/test_number_of_abcd_members_view.py +++ b/tapir/statistics/tests/fancy_graph/test_number_of_abcd_members_view.py @@ -49,7 +49,7 @@ def test_calculateDatapoint_memberIsWorkingButIsNotAbcd_notCounted(self): self.assertEqual(0, result) - def test_calculateDatapoint_memberIsWorkingAnAbcd_counted(self): + def test_calculateDatapoint_memberIsWorkingAndAbcd_counted(self): tapir_user = TapirUserFactory.create( date_joined=self.REFERENCE_TIME - datetime.timedelta(days=1) ) diff --git a/tapir/statistics/tests/fancy_graph/test_number_of_co_purchasers_view.py b/tapir/statistics/tests/fancy_graph/test_number_of_co_purchasers_view.py new file mode 100644 index 00000000..dbcee24d --- /dev/null +++ b/tapir/statistics/tests/fancy_graph/test_number_of_co_purchasers_view.py @@ -0,0 +1,63 @@ +import datetime + +from django.utils import timezone + +from tapir.accounts.tests.factories.factories import TapirUserFactory +from tapir.statistics.views.fancy_graph.number_of_co_purchasers_view import ( + NumberOfCoPurchasersAtDateView, +) +from tapir.utils.tests_utils import ( + TapirFactoryTestBase, + mock_timezone_now, +) + + +class TestNumberOfCoPurchasersView(TapirFactoryTestBase): + NOW = datetime.datetime(year=2023, month=4, day=1, hour=12) + REFERENCE_TIME = timezone.make_aware( + datetime.datetime(year=2022, month=6, day=15, hour=12) + ) + + def setUp(self) -> None: + super().setUp() + self.NOW = mock_timezone_now(self, self.NOW) + + def test_calculateDatapoint_memberHasCoPurchaserButIsNotWorking_notCounted(self): + TapirUserFactory.create( + date_joined=self.REFERENCE_TIME + datetime.timedelta(days=1), + co_purchaser="A test co-purchaser", + ) + + result = NumberOfCoPurchasersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(0, result) + + def test_calculateDatapoint_memberIsWorkingButDoesntHaveACoPurchaser_notCounted( + self, + ): + TapirUserFactory.create( + date_joined=self.REFERENCE_TIME - datetime.timedelta(days=1), + share_owner__is_investing=False, + co_purchaser="", + ) + + result = NumberOfCoPurchasersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(0, result) + + def test_calculateDatapoint_memberIsWorkingAndHasCoPurchaser_counted(self): + TapirUserFactory.create( + date_joined=self.REFERENCE_TIME - datetime.timedelta(days=1), + co_purchaser="A test co-purchaser", + share_owner__is_investing=False, + ) + + result = NumberOfCoPurchasersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(1, result) From 7e00a69d82d6f920e9f61181be019babb9d9f6b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Wed, 11 Dec 2024 14:51:40 +0100 Subject: [PATCH 24/50] Added tests for CoPurchaserHistoryService --- .../services/co_purchaser_history_service.py | 20 --- .../test_co_purchaser_history_service.py | 169 ++++++++++++++++++ 2 files changed, 169 insertions(+), 20 deletions(-) create mode 100644 tapir/accounts/tests/test_co_purchaser_history_service.py diff --git a/tapir/accounts/services/co_purchaser_history_service.py b/tapir/accounts/services/co_purchaser_history_service.py index a8f50873..a90442d4 100644 --- a/tapir/accounts/services/co_purchaser_history_service.py +++ b/tapir/accounts/services/co_purchaser_history_service.py @@ -22,26 +22,6 @@ class CoPurchaserHistoryService: ANNOTATION_HAS_CO_PURCHASER = "has_co_purchaser" ANNOTATION_HAS_CO_PURCHASER_DATE_CHECK = "has_co_purchaser_date_check" - @classmethod - def has_co_purchaser( - cls, tapir_user: TapirUser, at_datetime: datetime.datetime = None - ): - if at_datetime is None: - at_datetime = timezone.now() - - if not hasattr(tapir_user, cls.ANNOTATION_HAS_CO_PURCHASER): - tapir_user = cls.annotate_tapir_user_queryset_with_has_co_purchaser_at_date( - TapirUser.objects.filter(id=tapir_user.id), at_datetime - ).first() - - annotated_date = getattr(tapir_user, cls.ANNOTATION_HAS_CO_PURCHASER_DATE_CHECK) - if annotated_date != at_datetime: - raise ValueError( - f"Trying to get 'has co purchaser' at date {at_datetime}, but the queryset has been " - f"annotated relative to {annotated_date}" - ) - return getattr(tapir_user, cls.ANNOTATION_HAS_CO_PURCHASER) - @classmethod def annotate_tapir_user_queryset_with_has_co_purchaser_at_date( cls, diff --git a/tapir/accounts/tests/test_co_purchaser_history_service.py b/tapir/accounts/tests/test_co_purchaser_history_service.py new file mode 100644 index 00000000..0ce21bd1 --- /dev/null +++ b/tapir/accounts/tests/test_co_purchaser_history_service.py @@ -0,0 +1,169 @@ +import datetime + +from django.utils import timezone + +from tapir.accounts.models import TapirUser, UpdateTapirUserLogEntry +from tapir.accounts.services.co_purchaser_history_service import ( + CoPurchaserHistoryService, +) +from tapir.accounts.tests.factories.factories import TapirUserFactory +from tapir.utils.tests_utils import TapirFactoryTestBase, mock_timezone_now + + +class TestCoPurchaserHistoryService(TapirFactoryTestBase): + NOW = datetime.datetime(year=2022, month=7, day=13, hour=12) + REFERENCE_TIME = timezone.make_aware( + datetime.datetime(year=2022, month=5, day=21, hour=15) + ) + + def setUp(self) -> None: + super().setUp() + self.NOW = mock_timezone_now(self, self.NOW) + + @staticmethod + def create_irrelevant_log_entry(tapir_user, reference_time): + log_entry = UpdateTapirUserLogEntry.objects.create( + user=tapir_user, + old_values={"city": "Berlin"}, + new_values={"city": "Munich"}, + ) + log_entry.created_date = reference_time - datetime.timedelta(hours=5) + log_entry.save() + + def test_annotateTapirUserQuerysetWithHasCoPurchaserAtDate_noRelevantLogEntriesAndMemberHasCoPurchaser_annotatesTrue( + self, + ): + tapir_user = TapirUserFactory.create(co_purchaser="A test co-purchaser") + self.create_irrelevant_log_entry(tapir_user, self.REFERENCE_TIME) + + queryset = CoPurchaserHistoryService.annotate_tapir_user_queryset_with_has_co_purchaser_at_date( + TapirUser.objects.all(), self.REFERENCE_TIME + ) + + self.assertEqual( + "A test co-purchaser", getattr(queryset.get(), "co_purchaser_at_date") + ) + self.assertTrue( + getattr( + queryset.get(), CoPurchaserHistoryService.ANNOTATION_HAS_CO_PURCHASER + ) + ) + self.assertTrue( + self.REFERENCE_TIME, + getattr( + queryset.get(), + CoPurchaserHistoryService.ANNOTATION_HAS_CO_PURCHASER_DATE_CHECK, + ), + ) + + def test_annotateTapirUserQuerysetWithHasCoPurchaserAtDate_noRelevantLogEntriesAndMemberHasNoCoPurchaser_annotatesFalse( + self, + ): + tapir_user = TapirUserFactory.create(co_purchaser="") + self.create_irrelevant_log_entry(tapir_user, self.REFERENCE_TIME) + + queryset = CoPurchaserHistoryService.annotate_tapir_user_queryset_with_has_co_purchaser_at_date( + TapirUser.objects.all(), self.REFERENCE_TIME + ) + + self.assertEqual("", getattr(queryset.get(), "co_purchaser_at_date")) + self.assertFalse( + getattr( + queryset.get(), CoPurchaserHistoryService.ANNOTATION_HAS_CO_PURCHASER + ) + ) + self.assertTrue( + self.REFERENCE_TIME, + getattr( + queryset.get(), + CoPurchaserHistoryService.ANNOTATION_HAS_CO_PURCHASER_DATE_CHECK, + ), + ) + + def test_annotateTapirUserQuerysetWithHasCoPurchaserAtDate_hasRelevantLogEntriesWithCoPurchaser_annotatesTrue( + self, + ): + tapir_user = TapirUserFactory.create(co_purchaser="") + self.create_irrelevant_log_entry(tapir_user, self.REFERENCE_TIME) + + log_entry_in_the_past = UpdateTapirUserLogEntry.objects.create( + user=tapir_user, + old_values={"co_purchaser": ""}, + new_values={"co_purchaser": "Someone"}, + ) + log_entry_in_the_past.created_date = self.REFERENCE_TIME - datetime.timedelta( + hours=5 + ) + log_entry_in_the_past.save() + + log_entry_in_the_future = UpdateTapirUserLogEntry.objects.create( + user=tapir_user, + old_values={"co_purchaser": "Someone"}, + new_values={"co_purchaser": ""}, + ) + log_entry_in_the_future.created_date = self.REFERENCE_TIME + datetime.timedelta( + hours=5 + ) + log_entry_in_the_future.save() + + queryset = CoPurchaserHistoryService.annotate_tapir_user_queryset_with_has_co_purchaser_at_date( + TapirUser.objects.all(), self.REFERENCE_TIME + ) + + self.assertEqual("Someone", getattr(queryset.get(), "co_purchaser_at_date")) + self.assertTrue( + getattr( + queryset.get(), CoPurchaserHistoryService.ANNOTATION_HAS_CO_PURCHASER + ) + ) + self.assertTrue( + self.REFERENCE_TIME, + getattr( + queryset.get(), + CoPurchaserHistoryService.ANNOTATION_HAS_CO_PURCHASER_DATE_CHECK, + ), + ) + + def test_annotateTapirUserQuerysetWithHasCoPurchaserAtDate_hasRelevantLogEntriesWithNoCoPurchaser_annotatesFalse( + self, + ): + tapir_user = TapirUserFactory.create(co_purchaser="Someone") + self.create_irrelevant_log_entry(tapir_user, self.REFERENCE_TIME) + + log_entry_in_the_past = UpdateTapirUserLogEntry.objects.create( + user=tapir_user, + old_values={"co_purchaser": "Someone"}, + new_values={"co_purchaser": ""}, + ) + log_entry_in_the_past.created_date = self.REFERENCE_TIME - datetime.timedelta( + hours=5 + ) + log_entry_in_the_past.save() + + log_entry_in_the_future = UpdateTapirUserLogEntry.objects.create( + user=tapir_user, + old_values={"co_purchaser": ""}, + new_values={"co_purchaser": "Someone"}, + ) + log_entry_in_the_future.created_date = self.REFERENCE_TIME + datetime.timedelta( + hours=5 + ) + log_entry_in_the_future.save() + + queryset = CoPurchaserHistoryService.annotate_tapir_user_queryset_with_has_co_purchaser_at_date( + TapirUser.objects.all(), self.REFERENCE_TIME + ) + + self.assertEqual("", getattr(queryset.get(), "co_purchaser_at_date")) + self.assertFalse( + getattr( + queryset.get(), CoPurchaserHistoryService.ANNOTATION_HAS_CO_PURCHASER + ) + ) + self.assertTrue( + self.REFERENCE_TIME, + getattr( + queryset.get(), + CoPurchaserHistoryService.ANNOTATION_HAS_CO_PURCHASER_DATE_CHECK, + ), + ) From 9bf123c6a200119b3c3ce4f0443e28419f4195b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Wed, 11 Dec 2024 15:00:56 +0100 Subject: [PATCH 25/50] Added tests for NumberOfCreatedResignationsInSameMonthView --- ...est_number_of_created_resignations_view.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tapir/statistics/tests/fancy_graph/test_number_of_created_resignations_view.py diff --git a/tapir/statistics/tests/fancy_graph/test_number_of_created_resignations_view.py b/tapir/statistics/tests/fancy_graph/test_number_of_created_resignations_view.py new file mode 100644 index 00000000..234ac15d --- /dev/null +++ b/tapir/statistics/tests/fancy_graph/test_number_of_created_resignations_view.py @@ -0,0 +1,40 @@ +import datetime + +from django.utils import timezone + +from tapir.coop.tests.factories import MembershipResignationFactory +from tapir.statistics.views.fancy_graph.number_of_created_resignations_view import ( + NumberOfCreatedResignationsInSameMonthView, +) +from tapir.utils.tests_utils import ( + TapirFactoryTestBase, + mock_timezone_now, +) + + +class TestNumberOfCreatedResignationsInSameMonthView(TapirFactoryTestBase): + NOW = datetime.datetime(year=2023, month=4, day=1, hour=12) + REFERENCE_TIME = timezone.make_aware( + datetime.datetime(year=2022, month=6, day=15, hour=12) + ) + + def setUp(self) -> None: + super().setUp() + self.NOW = mock_timezone_now(self, self.NOW) + + def test_calculateDatapoint_default_countsOnlyRelevantResignations(self): + MembershipResignationFactory.create( + cancellation_date=datetime.date(year=2022, month=5, day=1) + ) + MembershipResignationFactory.create( + cancellation_date=datetime.date(year=2022, month=6, day=30) + ) + MembershipResignationFactory.create( + cancellation_date=datetime.date(year=2023, month=6, day=30) + ) + + result = NumberOfCreatedResignationsInSameMonthView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(1, result) From e998e07a92c09c0649fbae822e03c2cba337f47e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Wed, 11 Dec 2024 15:17:46 +0100 Subject: [PATCH 26/50] Added tests for NumberOfExemptedMembersAtDateView --- .../test_number_of_exempted_members_view.py | 97 +++++++++++++++++++ .../number_of_exempted_members_view.py | 4 +- 2 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 tapir/statistics/tests/fancy_graph/test_number_of_exempted_members_view.py diff --git a/tapir/statistics/tests/fancy_graph/test_number_of_exempted_members_view.py b/tapir/statistics/tests/fancy_graph/test_number_of_exempted_members_view.py new file mode 100644 index 00000000..c42d937b --- /dev/null +++ b/tapir/statistics/tests/fancy_graph/test_number_of_exempted_members_view.py @@ -0,0 +1,97 @@ +import datetime + +from django.utils import timezone + +from tapir.accounts.models import TapirUser +from tapir.accounts.tests.factories.factories import TapirUserFactory +from tapir.coop.models import ShareOwnership, ShareOwner +from tapir.shifts.models import ShiftExemption, ShiftUserData +from tapir.statistics.views.fancy_graph.number_of_exempted_members_view import ( + NumberOfExemptedMembersAtDateView, +) +from tapir.utils.tests_utils import ( + TapirFactoryTestBase, + mock_timezone_now, +) + + +class TestNumberOfExemptedMembersAtDateView(TapirFactoryTestBase): + NOW = datetime.datetime(year=2022, month=7, day=1, hour=12) + REFERENCE_TIME = timezone.make_aware( + datetime.datetime(year=2023, month=8, day=15, hour=18) + ) + + def setUp(self) -> None: + super().setUp() + self.NOW = mock_timezone_now(self, self.NOW) + + @classmethod + def create_member_where_the_only_reason_for_not_working_is_an_exemption(cls): + tapir_user = TapirUserFactory.create( + date_joined=cls.REFERENCE_TIME - datetime.timedelta(days=1), + share_owner__is_investing=False, + ) + ShareOwnership.objects.update( + start_date=cls.REFERENCE_TIME.date() - datetime.timedelta(days=1) + ) + ShiftExemption.objects.create( + start_date=cls.REFERENCE_TIME.date() - datetime.timedelta(days=1), + end_date=cls.REFERENCE_TIME.date() + datetime.timedelta(days=1), + shift_user_data=tapir_user.shift_user_data, + ) + + def test_calculateDatapoint_exemptedMemberThatWouldWorkOtherwise_counted(self): + self.create_member_where_the_only_reason_for_not_working_is_an_exemption() + + result = NumberOfExemptedMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(1, result) + + def test_calculateDatapoint_memberHasExemptionButIsNotActive_notCounted(self): + self.create_member_where_the_only_reason_for_not_working_is_an_exemption() + ShareOwner.objects.update(is_investing=True) + + result = NumberOfExemptedMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(0, result) + + def test_calculateDatapoint_memberHasExemptionButIsFrozen_notCounted(self): + self.create_member_where_the_only_reason_for_not_working_is_an_exemption() + ShiftUserData.objects.update(is_frozen=True) + + result = NumberOfExemptedMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(0, result) + + def test_calculateDatapoint_memberHasExemptionButJoinedAfterDate_notCounted(self): + self.create_member_where_the_only_reason_for_not_working_is_an_exemption() + TapirUser.objects.update( + date_joined=self.REFERENCE_TIME + datetime.timedelta(days=1) + ) + + result = NumberOfExemptedMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(0, result) + + def test_calculateDatapoint_memberHasExemptionThatIsNotActiveAtGivenDate_notCounted( + self, + ): + self.create_member_where_the_only_reason_for_not_working_is_an_exemption() + ShiftExemption.objects.update( + start_date=self.REFERENCE_TIME.date() + datetime.timedelta(days=1), + end_date=self.REFERENCE_TIME.date() + datetime.timedelta(days=2), + ) + + result = NumberOfExemptedMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(0, result) diff --git a/tapir/statistics/views/fancy_graph/number_of_exempted_members_view.py b/tapir/statistics/views/fancy_graph/number_of_exempted_members_view.py index c75b3f1e..66df9752 100644 --- a/tapir/statistics/views/fancy_graph/number_of_exempted_members_view.py +++ b/tapir/statistics/views/fancy_graph/number_of_exempted_members_view.py @@ -12,7 +12,7 @@ class NumberOfExemptedMembersAtDateView(DatapointView): def calculate_datapoint(self, reference_time: datetime.datetime) -> int: reference_date = reference_time.date() active_members = ShareOwner.objects.with_status( - MemberStatus.ACTIVE, reference_date + MemberStatus.ACTIVE, reference_time ) active_members = FrozenStatusHistoryService.annotate_share_owner_queryset_with_is_frozen_at_datetime( @@ -30,7 +30,7 @@ def calculate_datapoint(self, reference_time: datetime.datetime) -> int: exemptions = ShiftExemption.objects.active_temporal(reference_date) exempted_members = members_that_joined_before_date.filter( - user__shift_user_data__exemption__in=exemptions + user__shift_user_data__shift_exemptions__in=exemptions ).distinct() return exempted_members.count() From 6c28d501c36a64b60268989bc224c6639a2c1a22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Wed, 11 Dec 2024 15:23:12 +0100 Subject: [PATCH 27/50] Added tests for NumberOfFlyingMembersAtDateView --- .../test_number_of_flying_members_view.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 tapir/statistics/tests/fancy_graph/test_number_of_flying_members_view.py diff --git a/tapir/statistics/tests/fancy_graph/test_number_of_flying_members_view.py b/tapir/statistics/tests/fancy_graph/test_number_of_flying_members_view.py new file mode 100644 index 00000000..2a23b603 --- /dev/null +++ b/tapir/statistics/tests/fancy_graph/test_number_of_flying_members_view.py @@ -0,0 +1,62 @@ +import datetime + +from django.utils import timezone + +from tapir.accounts.tests.factories.factories import TapirUserFactory +from tapir.shifts.models import CreateShiftAttendanceTemplateLogEntry +from tapir.statistics.views.fancy_graph.number_of_flying_members_view import ( + NumberOfFlyingMembersAtDateView, +) +from tapir.utils.tests_utils import ( + TapirFactoryTestBase, + mock_timezone_now, + create_attendance_template_log_entry_in_the_past, +) + + +class TestNumberOfFlyingMembersView(TapirFactoryTestBase): + NOW = datetime.datetime(year=2023, month=4, day=1, hour=12) + REFERENCE_TIME = timezone.make_aware( + datetime.datetime(year=2022, month=6, day=15, hour=12) + ) + + def setUp(self) -> None: + super().setUp() + self.NOW = mock_timezone_now(self, self.NOW) + + def test_calculateDatapoint_memberIsFlyingButIsNotWorking_notCounted(self): + TapirUserFactory.create( + date_joined=self.REFERENCE_TIME + datetime.timedelta(days=1) + ) + + result = NumberOfFlyingMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(0, result) + + def test_calculateDatapoint_memberIsWorkingButIsNotFlying_notCounted(self): + tapir_user = TapirUserFactory.create( + date_joined=self.REFERENCE_TIME - datetime.timedelta(days=1) + ) + create_attendance_template_log_entry_in_the_past( + CreateShiftAttendanceTemplateLogEntry, tapir_user, self.REFERENCE_TIME + ) + + result = NumberOfFlyingMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(0, result) + + def test_calculateDatapoint_memberIsWorkingAndFlying_counted(self): + TapirUserFactory.create( + date_joined=self.REFERENCE_TIME - datetime.timedelta(days=1), + share_owner__is_investing=False, + ) + + result = NumberOfFlyingMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(1, result) From fa6f701f3773ffae7a55f4be778db1dcebae045b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Wed, 11 Dec 2024 16:50:50 +0100 Subject: [PATCH 28/50] Translation file update, test for annotate_share_owner_queryset_with_is_frozen_at_datetime --- .../services/frozen_status_history_service.py | 6 +- .../test_frozen_status_history_service.py | 20 + .../locale/d/LC_MESSAGES/django.po | 5039 +++++++++++++++++ 3 files changed, 5064 insertions(+), 1 deletion(-) create mode 100644 tapir/translations/locale/d/LC_MESSAGES/django.po diff --git a/tapir/shifts/services/frozen_status_history_service.py b/tapir/shifts/services/frozen_status_history_service.py index 892ac4fd..878a6f5b 100644 --- a/tapir/shifts/services/frozen_status_history_service.py +++ b/tapir/shifts/services/frozen_status_history_service.py @@ -12,6 +12,7 @@ from django.db.models.functions import Coalesce from django.utils import timezone +from tapir.coop.models import ShareOwner from tapir.shifts.models import ( ShiftUserData, UpdateShiftUserDataLogEntry, @@ -94,8 +95,11 @@ def annotate_shift_user_data_queryset_with_is_frozen_at_datetime( @classmethod def annotate_share_owner_queryset_with_is_frozen_at_datetime( - cls, queryset: QuerySet, at_datetime: datetime.datetime = None + cls, queryset: QuerySet[ShareOwner], at_datetime: datetime.datetime = None ): + if at_datetime is None: + at_datetime = timezone.now() + return cls.annotate_shift_user_data_queryset_with_is_frozen_at_datetime( queryset, at_datetime, "user__shift_user_data" ) diff --git a/tapir/shifts/tests/test_frozen_status_history_service.py b/tapir/shifts/tests/test_frozen_status_history_service.py index ffa1fcba..5d993d26 100644 --- a/tapir/shifts/tests/test_frozen_status_history_service.py +++ b/tapir/shifts/tests/test_frozen_status_history_service.py @@ -5,6 +5,7 @@ from tapir.accounts.models import TapirUser from tapir.accounts.tests.factories.factories import TapirUserFactory +from tapir.coop.models import ShareOwner from tapir.shifts.models import ( ShiftUserData, UpdateShiftUserDataLogEntry, @@ -165,3 +166,22 @@ def test_annotateShiftUserDataQuerysetWithIsFrozenAtDatetime_hasRelevantLogEntry ), True, ) + + def test_annotateShareOwnerQuerysetWithIsFrozenAtDatetime_memberIsFrozen_annotatesTrue( + self, + ): + TapirUserFactory.create() + ShiftUserData.objects.update(is_frozen=True) + + queryset = FrozenStatusHistoryService.annotate_share_owner_queryset_with_is_frozen_at_datetime( + ShareOwner.objects.all() + ) + + share_owner = queryset.first() + self.assertEqual( + getattr( + share_owner, + FrozenStatusHistoryService.ANNOTATION_IS_FROZEN_AT_DATE, + ), + True, + ) diff --git a/tapir/translations/locale/d/LC_MESSAGES/django.po b/tapir/translations/locale/d/LC_MESSAGES/django.po new file mode 100644 index 00000000..c316b0b2 --- /dev/null +++ b/tapir/translations/locale/d/LC_MESSAGES/django.po @@ -0,0 +1,5039 @@ +# 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 , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-12-11 16:44+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: accounts/emails/create_account_reminder_email.py:20 +msgid "Create account reminder" +msgstr "" + +#: accounts/emails/create_account_reminder_email.py:25 +msgid "Sent to active member if they haven't created the account 1 month after becoming member." +msgstr "" + +#: accounts/forms.py:19 +msgid "Additional Emails" +msgstr "" + +#: accounts/forms.py:25 +msgid "Mandatory Emails" +msgstr "" + +#: accounts/forms.py:119 +msgid "This username is not available." +msgstr "" + +#: accounts/models.py:66 +msgid "Displayed name" +msgstr "" + +#: accounts/models.py:71 coop/models.py:70 coop/models.py:476 +msgid "Pronouns" +msgstr "" + +#: accounts/models.py:72 accounts/templates/accounts/user_detail.html:67 +#: coop/models.py:72 coop/models.py:478 +#: coop/templates/coop/draftuser_detail.html:71 +#: coop/templates/coop/draftuser_detail.html:142 +#: coop/templates/coop/shareowner_detail.html:51 +msgid "Phone number" +msgstr "" + +#: accounts/models.py:73 accounts/templates/accounts/user_detail.html:77 +#: coop/models.py:73 coop/models.py:479 +#: coop/templates/coop/draftuser_detail.html:146 +#: coop/templates/coop/shareowner_detail.html:55 +msgid "Birthdate" +msgstr "" + +#: accounts/models.py:74 coop/models.py:74 coop/models.py:480 +msgid "Street and house number" +msgstr "" + +#: accounts/models.py:75 coop/models.py:75 coop/models.py:481 +msgid "Extra address line" +msgstr "" + +#: accounts/models.py:76 coop/models.py:76 coop/models.py:482 +msgid "Postcode" +msgstr "" + +#: accounts/models.py:77 coop/models.py:77 coop/models.py:483 +msgid "City" +msgstr "" + +#: accounts/models.py:78 coop/models.py:78 coop/models.py:484 +msgid "Country" +msgstr "" + +#: accounts/models.py:79 accounts/templates/accounts/user_detail.html:111 +msgid "Co-Purchaser" +msgstr "" + +#: accounts/models.py:81 +msgid "Allow purchase tracking" +msgstr "" + +#: accounts/models.py:95 accounts/templates/accounts/user_detail.html:97 +#: coop/models.py:81 coop/models.py:487 +#: coop/templates/coop/shareowner_detail.html:75 +msgid "Preferred Language" +msgstr "" + +#: accounts/models.py:128 accounts/models.py:134 +#: coop/views/membership_pause.py:74 shifts/views/exemptions.py:214 +msgid "None" +msgstr "" + +#: accounts/templates/accounts/edit_username.default.html:13 +#, python-format +msgid "" +"\n" +" Edit username: %(display_name_full)s\n" +" " +msgstr "" + +#: accounts/templates/accounts/edit_username.default.html:22 +#, python-format +msgid "" +"\n" +" Edit username: %(display_name_full)s\n" +" " +msgstr "" + +#: accounts/templates/accounts/edit_username.default.html:28 +#, python-format +msgid "" +"\n" +"

Your old username is %(old_username)s.

\n" +"

If you ever updated the wiki, please contact the Wiki team on Slack #wiki to keep your\n" +" member's page there and the history of your changes.

\n" +" " +msgstr "" + +#: accounts/templates/accounts/edit_username.default.html:41 +#: accounts/templates/registration/password_update.html:16 +#: coop/templates/coop/draftuser_register_form.default.html:42 +#: coop/templates/coop/draftuser_register_form.html:45 +#: coop/templates/coop/membership_resignation_form.html:65 +#: core/templates/core/tapir_form.default.html:30 +#: core/templates/core/tapir_form.html:30 +#: shifts/templates/shifts/convert_exemption_to_pause_form.html:37 +#: shifts/templates/shifts/shift_form.html:35 +#: shifts/templates/shifts/shiftslot_form.html:32 +msgid "Save" +msgstr "" + +#: accounts/templates/accounts/email/create_account_reminder.body.html:6 +#, python-format +msgid "" +"\n" +"

Hi %(display_name_short)s,

\n" +"

We've noticed that you've been a SuperCoop member for a month/week but haven't set up a Tapir account to sign up for your first shift.

\n" +"

\n" +" As a reminder, to sign up for your first shift (or indicate that you are eligible for an\n" +" exemption),\n" +" please visit the Member Office in our shop.\n" +" There, an account will be set up for you on Tapir, our online shift management system, \n" +" and you will be guided through the steps to select your shift and working group.\n" +"

\n" +"

\n" +" For general questions about your membership, please contact the Member Office by email (mitglied@supercoop.de)\n" +" or visit SuperCoop.\n" +" Feel free to drop by and meet other SuperCoopies in person! Opening hours are::\n" +"

    \n" +"
  • Monday - Friday from 16:30 to 19:30
  • \n" +"
  • Saturdays from 11:00 to 14:00
  • \n" +"
\n" +"

\n" +"

\n" +" How do you find SuperCoop?\n" +"

\n" +"

\n" +" You can find our shop at Oudenarder Straße 16, 13347 Berlin, on the corner of Seestraße. \n" +" There is also a back entrance to the shop, which you can reach via the former Osramhöfe. This entrance is also barrier-free.\n" +"

\n" +"

\n" +" Cooperative Greetings,
\n" +" The Member Office\n" +"

\n" +" " +msgstr "" + +#: accounts/templates/accounts/email/create_account_reminder.subject.html:2 +msgid "" +"\n" +" Don't forget to sign up for your first shift at SuperCoop!\n" +msgstr "" + +#: accounts/templates/accounts/ldap_group_list.html:7 +#: accounts/templates/accounts/user_detail.html:102 +msgid "Groups" +msgstr "" + +#: accounts/templates/accounts/purchase_tracking_card.html:5 +msgid "Purchase tracking" +msgstr "" + +#: accounts/templates/accounts/purchase_tracking_card.html:11 +msgid "Get barcode as PDF" +msgstr "" + +#: accounts/templates/accounts/purchase_tracking_card.html:17 +msgid "More information" +msgstr "" + +#: accounts/templates/accounts/purchase_tracking_card.html:25 +msgid "Das Kassensystem verknüpft deinen Einkauf mit deinem Mitgliedskonto. Dabei wird jedesmal der Gesamtbetrag deines Einkaufs gespeichert. Nicht erfasst wird dagegen, welche konkreten Produkte du gekauft hast. Auch kannst du bei jedem Einkauf immer noch entscheiden, ob du deine Mitgliedskarte scannen lassen möchtest oder nicht. Mit deiner generellen Zustimmung hier auf Tapir gehst du keine Verpflichtung zum Scannen ein. Die Zustimmung kannst du jederzeit widerrufen, indem du die Checkbox oben deaktivierst. Du hilfst damit Supercoop, die Einkaufsgewohnheiten der Mitglieder besser zu verstehen. Das ist wichtig für die Weiterentwicklung unseres Supermarktes. Mehr Informationen: https://wiki.supercoop.de/wiki/Mitgliederkarte" +msgstr "" + +#: accounts/templates/accounts/purchase_tracking_card.html:29 +#: coop/templates/coop/draftuser_detail.html:235 +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:168 +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:172 +msgid "Yes,No" +msgstr "" + +#: accounts/templates/accounts/purchase_tracking_card.html:35 +msgid "Disable" +msgstr "" + +#: accounts/templates/accounts/purchase_tracking_card.html:40 +msgid "Enable" +msgstr "" + +#: accounts/templates/accounts/user_detail.html:20 coop/models.py:753 +#: coop/templates/coop/draftuser_detail.html:86 +#: coop/templates/coop/shareowner_detail.html:10 log/views.py:94 +#: log/views.py:152 shifts/templates/shifts/shift_day_printable.html:55 +#: shifts/templates/shifts/shift_detail_printable.html:50 +msgid "Member" +msgstr "" + +#: accounts/templates/accounts/user_detail.html:27 +#: coop/templates/coop/shareowner_detail.html:17 +msgid "Personal Data" +msgstr "" + +#: accounts/templates/accounts/user_detail.html:32 +msgid "Edit groups" +msgstr "" + +#: accounts/templates/accounts/user_detail.html:37 +msgid "Edit username" +msgstr "" + +#: accounts/templates/accounts/user_detail.html:43 +#: coop/templates/coop/draftuser_detail.html:242 +#: coop/templates/coop/membershipresignation_detail.html:107 +#: coop/templates/coop/shareowner_detail.html:30 +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:22 +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:90 +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:113 +#: core/templates/core/featureflag_list.html:30 +#: shifts/templates/shifts/shift_detail.html:44 +#: shifts/templates/shifts/shift_template_detail.html:25 +#: shifts/templates/shifts/shift_template_detail.html:113 +#: shifts/templates/shifts/user_shifts_overview_tag.html:14 +msgid "Edit" +msgstr "" + +#: accounts/templates/accounts/user_detail.html:48 +msgid "Edit name and pronouns" +msgstr "" + +#: accounts/templates/accounts/user_detail.html:55 +#: coop/templates/coop/draftuser_detail.html:70 +#: coop/templates/coop/draftuser_detail.html:130 +#: coop/templates/coop/shareowner_detail.html:43 coop/views/draftuser.py:271 +#: core/templates/core/email_list.html:38 +#: core/templates/core/featureflag_list.html:18 +#: financingcampaign/templates/financingcampaign/general.html:25 +#: financingcampaign/templates/financingcampaign/general.html:70 +msgid "Name" +msgstr "" + +#: accounts/templates/accounts/user_detail.html:59 +#: coop/templates/coop/draftuser_detail.html:134 +msgid "Username" +msgstr "" + +#: accounts/templates/accounts/user_detail.html:63 +#: coop/templates/coop/draftuser_detail.html:73 +#: coop/templates/coop/draftuser_detail.html:138 +#: coop/templates/coop/shareowner_detail.html:47 +msgid "Email" +msgstr "" + +#: accounts/templates/accounts/user_detail.html:72 +#: accounts/templates/accounts/user_detail.html:82 +#: accounts/templates/accounts/user_detail.html:92 +#: coop/templates/coop/draftuser_detail.html:161 +#: coop/templates/coop/shareowner_detail.html:60 +#: coop/templates/coop/shareowner_detail.html:70 +#: shifts/templates/shifts/user_shifts_overview_tag.html:40 +#: statistics/templates/statistics/main_statistics.html:76 +#: statistics/templates/statistics/main_statistics.html:108 +msgid "Missing" +msgstr "" + +#: accounts/templates/accounts/user_detail.html:87 +#: coop/templates/coop/draftuser_detail.html:72 +#: coop/templates/coop/draftuser_detail.html:156 +#: coop/templates/coop/shareowner_detail.html:65 +msgid "Address" +msgstr "" + +#: accounts/templates/accounts/user_detail.html:106 +msgid "Permissions" +msgstr "" + +#: accounts/templates/accounts/user_detail.html:127 +msgid "Resend account activation email" +msgstr "" + +#: accounts/templates/accounts/user_detail.html:135 +msgid "Change Password" +msgstr "" + +#: accounts/templates/registration/email/password_reset_email.html:7 +msgid "Password reset" +msgstr "" + +#: accounts/templates/registration/email/password_reset_email.html:14 +#, python-format +msgid "" +"\n" +"

Hi %(display_name_short)s,

\n" +"

\n" +" Someone asked for password reset for %(email)s.
\n" +" Your username is %(username)s
\n" +" Follow this link to reset your password: %(password_reset_url_full)s\n" +" If the link doesn't work, try to open it in a private browser window (sometimes called \"incognito mode\"), or in another browser.\n" +"

\n" +" " +msgstr "" + +#: accounts/templates/registration/email/password_reset_subject.html:2 +msgid "" +"\n" +" Reset your password\n" +msgstr "" + +#: accounts/templates/registration/login.html:7 +msgid "Login" +msgstr "" + +#: accounts/templates/registration/login.html:16 +#: accounts/templates/registration/login.html:59 +msgid "Sign in" +msgstr "" + +#: accounts/templates/registration/login.html:45 +msgid "Click here to show/hide the password" +msgstr "" + +#: accounts/templates/registration/login.html:55 +msgid "Forgot your password or your username?" +msgstr "" + +#: accounts/templates/registration/password_reset_complete.html:7 +msgid "Password set" +msgstr "" + +#: accounts/templates/registration/password_reset_complete.html:9 +msgid "Your password has been set. You may go ahead and sign in now." +msgstr "" + +#: accounts/templates/registration/password_reset_complete.html:11 +msgid "Go to login page" +msgstr "" + +#: accounts/templates/registration/password_reset_confirm.html:9 +#: accounts/templates/registration/password_update.html:8 +msgid "Set a new password" +msgstr "" + +#: accounts/templates/registration/password_reset_confirm.html:11 +msgid "Please enter a new password" +msgstr "" + +#: accounts/templates/registration/password_reset_confirm.html:15 +msgid "Change password" +msgstr "" + +#: accounts/templates/registration/password_reset_confirm.html:21 +msgid "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." +msgstr "" + +#: accounts/templates/registration/password_reset_done.html:7 +msgid "Password reset instructions have been sent." +msgstr "" + +#: accounts/templates/registration/password_reset_done.html:9 +msgid "We have emailed you instructions for setting your password, if an account exists with the email you entered. You should receive them shortly. If you don't receive an email, please make sure you've entered the address you registered with, and check your spam folder." +msgstr "" + +#: accounts/templates/registration/password_reset_form.html:10 +msgid "Problems with logging in?" +msgstr "" + +#: accounts/templates/registration/password_reset_form.html:16 +#, python-format +msgid "" +"\n" +" Please enter your email address, we will send you instructions to reset your password.
\n" +" Your username will be included in the email, in case you forgot it.
\n" +" If you forgot both your email address and your username, email the member office : %(contact_member_office)s, specifying your full name and, if you know it, your member's number.\n" +" " +msgstr "" + +#: accounts/templates/registration/password_reset_form.html:24 +msgid "Back to login form" +msgstr "" + +#: accounts/templates/registration/password_reset_form.html:26 +msgid "Send me instructions!" +msgstr "" + +#: accounts/validators.py:11 +msgid "Enter a valid username. This value may contain only letters, numbers, and ./-/_ characters." +msgstr "" + +#: accounts/views.py:99 accounts/views.py:104 coop/views/shareowner.py:261 +#: coop/views/shareowner.py:266 +#, python-format +msgid "Edit member: %(name)s" +msgstr "" + +#: accounts/views.py:142 +msgid "Account welcome email sent." +msgstr "" + +#: accounts/views.py:187 +msgid "You can only look at your own barcode unless you have member office rights" +msgstr "" + +#: accounts/views.py:246 accounts/views.py:251 +#, python-format +msgid "Edit member groups: %(name)s" +msgstr "" + +#: coop/apps.py:22 coop/apps.py:33 coop/templates/coop/shareowner_list.html:10 +msgid "Members" +msgstr "" + +#: coop/apps.py:25 coop/templates/coop/draftuser_list.html:9 +#: coop/templates/coop/draftuser_list.html:18 +#: coop/templates/coop/draftuser_register_form.default.html:11 +#: coop/templates/coop/draftuser_register_form.html:11 +msgid "Applicants" +msgstr "" + +#: coop/apps.py:41 coop/templates/coop/member_management.html:7 +msgid "Member management" +msgstr "" + +#: coop/apps.py:48 core/apps.py:17 financingcampaign/apps.py:14 log/apps.py:17 +msgid "Management" +msgstr "" + +#: coop/apps.py:51 coop/templates/coop/incoming_payment_list.html:19 +msgid "Incoming payments" +msgstr "" + +#: coop/emails/co_purchaser_updated_mail.py:22 +#: coop/templates/coop/email/co_purchaser_updated.subject.default.html:2 +msgid "Co-purchaser updated" +msgstr "" + +#: coop/emails/co_purchaser_updated_mail.py:27 +msgid "Sent to a member when their new co-purchaser gets registered on their profile." +msgstr "" + +#: coop/emails/extra_shares_confirmation_email.py:26 +msgid "Extra shares bought" +msgstr "" + +#: coop/emails/extra_shares_confirmation_email.py:30 +msgid "Sent when someone who is already a member buys more shares" +msgstr "" + +#: coop/emails/membership_confirmation_email_for_active_member.py:26 +msgid "Membership confirmation for active users" +msgstr "" + +#: coop/emails/membership_confirmation_email_for_investing_member.py:26 +msgid "Membership confirmation for investing users" +msgstr "" + +#: coop/emails/membershipresignation_confirmation_email.py:22 +msgid "Confirmation Email for resigned members." +msgstr "" + +#: coop/emails/membershipresignation_confirmation_email.py:26 +msgid "Automatically sent after a member has been resigned." +msgstr "" + +#: coop/emails/membershipresignation_transferred_shares_confirmation.py:21 +msgid "Confirmation Email for transferred shares." +msgstr "" + +#: coop/emails/membershipresignation_transferred_shares_confirmation.py:26 +msgid "Automatically sent to the member who received shares after a resignation." +msgstr "" + +#: coop/emails/tapir_account_created_email.py:25 +msgid "Tapir account created" +msgstr "" + +#: coop/emails/tapir_account_created_email.py:29 +msgid "Sent to a member when the accounts gets created." +msgstr "" + +#: coop/forms.py:41 +#: financingcampaign/templates/financingcampaign/general.html:27 +msgid "Start date" +msgstr "" + +#: coop/forms.py:45 +msgid "Usually, the date on the membership agreement, or today. In the case of sold or gifted shares, can be set in the future." +msgstr "" + +#: coop/forms.py:50 +#: financingcampaign/templates/financingcampaign/general.html:28 +msgid "End date" +msgstr "" + +#: coop/forms.py:54 +msgid "Usually left empty. Can be set to a point in the future if it is already known that the shares will be transferred to another member in the future." +msgstr "" + +#: coop/forms.py:60 +msgid "Number of shares to create" +msgstr "" + +#: coop/forms.py:68 +msgid "The end date must be later than the start date." +msgstr "" + +#: coop/forms.py:107 coop/models.py:493 +msgid "Number of Shares" +msgstr "" + +#: coop/forms.py:112 +#, python-format +msgid "Number of shares you would like to purchase. The price of one share is EUR %(share_price)s. You need to purchase at least one share to become member of the cooperative. To support our cooperative even more, you may voluntarily purchase more shares." +msgstr "" + +#: coop/forms.py:122 +msgid "I would like to join the membership list as an investing member (= sponsoring member)" +msgstr "" + +#: coop/forms.py:125 +msgid "Note: Investing members are sponsoring members. They have no voting rights in the General Assembly and cannot use the services of the cooperative that are exclusive to ordinary members. " +msgstr "" + +#: coop/forms.py:235 +msgid "Usually, the credited member is the same as the paying member. Only if a person if gifting another person a share through the matching program, then the fields can be different." +msgstr "" + +#: coop/forms.py:257 +msgid "Please not more than 1000 characters." +msgstr "" + +#: coop/forms.py:260 +msgid "Member to resign" +msgstr "" + +#: coop/forms.py:263 +msgid "Transferring share(s) to" +msgstr "" + +#: coop/forms.py:271 +msgid "The member stays active" +msgstr "" + +#: coop/forms.py:273 +msgid "The member becomes investing" +msgstr "" + +#: coop/forms.py:277 +msgid "Member status" +msgstr "" + +#: coop/forms.py:281 +msgid "In the case where the member wants their money back, they stay a member for 3 more years. However, it is very likely that the member doesn't want to be active anymore. If they haven't explicitly mentioned it, please ask them if we can switch them to investing." +msgstr "" + +#: coop/forms.py:348 +msgid "Please pick an option" +msgstr "" + +#: coop/forms.py:360 +msgid "This member is already resigned." +msgstr "" + +#: coop/forms.py:372 +msgid "Please select the member that the shares should be transferred to." +msgstr "" + +#: coop/forms.py:385 +msgid "If the shares don't get transferred to another member, this field should be empty." +msgstr "" + +#: coop/forms.py:398 +msgid "Sender and receiver of transferring the share(s) cannot be the same." +msgstr "" + +#: coop/forms.py:410 +msgid "Cannot pay out, because shares have been gifted." +msgstr "" + +#: coop/models.py:54 +msgid "Is company" +msgstr "" + +#: coop/models.py:61 coop/models.py:467 +msgid "Administrative first name" +msgstr "" + +#: coop/models.py:63 coop/models.py:469 +msgid "Last name" +msgstr "" + +#: coop/models.py:65 coop/models.py:471 +msgid "Usage name" +msgstr "" + +#: coop/models.py:71 coop/models.py:477 +msgid "Email address" +msgstr "" + +#: coop/models.py:88 +msgid "Is investing member" +msgstr "" + +#: coop/models.py:90 coop/models.py:508 +#: coop/templates/coop/draftuser_detail.html:176 +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:48 +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:167 +msgid "Ratenzahlung" +msgstr "" + +#: coop/models.py:92 coop/models.py:500 +msgid "Attended Welcome Session" +msgstr "" + +#: coop/models.py:94 coop/models.py:505 +msgid "Paid Entrance Fee" +msgstr "" + +#: coop/models.py:96 +msgid "Is willing to gift a share" +msgstr "" + +#: coop/models.py:208 +msgid "Cannot be a company and have a Tapir account" +msgstr "" + +#: coop/models.py:224 +msgid "User info should be stored in associated Tapir account" +msgstr "" + +#: coop/models.py:376 +msgid "Not a member" +msgstr "" + +#: coop/models.py:377 coop/templates/coop/draftuser_detail.html:169 +msgid "Investing" +msgstr "" + +#: coop/models.py:378 coop/templates/coop/draftuser_detail.html:171 +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:103 +#: coop/views/statistics.py:142 +msgid "Active" +msgstr "" + +#: coop/models.py:379 +msgid "Paused" +msgstr "" + +#: coop/models.py:434 +msgid "Amount paid for a share can't be negative" +msgstr "" + +#: coop/models.py:438 +#, python-brace-format +msgid "Amount paid for a share can't more than {COOP_SHARE_PRICE} (the price of a share)" +msgstr "" + +#: coop/models.py:496 +msgid "Investing member" +msgstr "" + +#: coop/models.py:503 +msgid "Signed Beteiligungserklärung" +msgstr "" + +#: coop/models.py:506 coop/templates/coop/draftuser_detail.html:234 +msgid "Paid Shares" +msgstr "" + +#: coop/models.py:563 +msgid "Email address must be set." +msgstr "" + +#: coop/models.py:565 +msgid "First name must be set." +msgstr "" + +#: coop/models.py:567 +msgid "Last name must be set." +msgstr "" + +#: coop/models.py:571 +msgid "Membership agreement must be signed." +msgstr "" + +#: coop/models.py:573 +msgid "Amount of requested shares must be positive." +msgstr "" + +#: coop/models.py:575 +msgid "Member already created." +msgstr "" + +#: coop/models.py:602 +msgid "Paying member" +msgstr "" + +#: coop/models.py:610 +msgid "Credited member" +msgstr "" + +#: coop/models.py:617 +msgid "Amount" +msgstr "" + +#: coop/models.py:623 +msgid "Payment date" +msgstr "" + +#: coop/models.py:626 +msgid "Creation date" +msgstr "" + +#: coop/models.py:631 +msgid "Created by" +msgstr "" + +#: coop/models.py:803 +msgid "The cooperative buys the shares back from the member" +msgstr "" + +#: coop/models.py:806 +msgid "The member gifts the shares to the cooperative" +msgstr "" + +#: coop/models.py:808 +msgid "The shares get transferred to another member" +msgstr "" + +#: coop/models.py:811 +msgid "Financial reasons" +msgstr "" + +#: coop/models.py:812 +msgid "Health reasons" +msgstr "" + +#: coop/models.py:813 +msgid "Distance" +msgstr "" + +#: coop/models.py:814 +msgid "Strategic orientation of SuperCoop" +msgstr "" + +#: coop/models.py:815 +msgid "Other" +msgstr "" + +#: coop/models.py:820 +msgid "Shareowner" +msgstr "" + +#: coop/models.py:839 +msgid "Leave this empty if the resignation type is not a transfer to another member" +msgstr "" + +#: coop/templates/coop/about.html:5 coop/templates/coop/about.html:11 +msgid "About Tapir" +msgstr "" + +#: coop/templates/coop/about.html:26 +msgid "Latest changes" +msgstr "" + +#: coop/templates/coop/active_members_progress_bar.html:4 +#, python-format +msgid "" +"\n" +" Grey : %(member_count_on_start_date)s active members on 15.11.2022
\n" +" Blue : %(new_member_count_since_start_date)s new members since 15.11.2022
\n" +" Click for more statistics\n" +" " +msgstr "" + +#: coop/templates/coop/active_members_progress_bar.html:11 +msgid "Active Members with a Tapir Account" +msgstr "" + +#: coop/templates/coop/confirm_delete_incoming_payment.html:7 +#: coop/templates/coop/confirm_delete_incoming_payment.html:13 +#: coop/templates/coop/confirm_delete_share_ownership.html:6 +#: coop/templates/coop/confirm_delete_share_ownership.html:12 +#: coop/templates/coop/draftuser_detail.html:20 +#: financingcampaign/templates/financingcampaign/confirm_delete.html:6 +#: financingcampaign/templates/financingcampaign/confirm_delete.html:12 +msgid "Confirm delete" +msgstr "" + +#: coop/templates/coop/confirm_delete_incoming_payment.html:31 +#: coop/templates/coop/confirm_delete_share_ownership.html:24 +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:127 +#: financingcampaign/templates/financingcampaign/confirm_delete.html:24 +msgid "Delete" +msgstr "" + +#: coop/templates/coop/create_user_from_shareowner_form.html:9 +#: coop/templates/coop/create_user_from_shareowner_form.html:19 +#: coop/templates/coop/shareowner_detail.html:36 coop/views/management.py:45 +#: coop/views/management.py:46 +msgid "Create Tapir account" +msgstr "" + +#: coop/templates/coop/create_user_from_shareowner_form.html:28 +msgid "Create account" +msgstr "" + +#: coop/templates/coop/draftuser_confirm_registration.html:6 +msgid "Registration confirmed" +msgstr "" + +#: coop/templates/coop/draftuser_confirm_registration.html:8 +msgid "" +"\n" +"

Registration successful! Congratulations!

\n" +"

We just emailed your confirmation of your pre-registration with the Beteiligungserklärung already filled with your information. You just have to print it and send it back to us per post or scan it and send it per email.

\n" +"

Once we have received both your Beteiligungserklärung and the corresponding payment, we'll send you the confirmation of your membership.

\n" +"

We will also create for you an account for this website and for the wiki.

\n" +" " +msgstr "" + +#: coop/templates/coop/draftuser_detail.html:8 +#: coop/templates/coop/draftuser_detail.html:78 +#: coop/templates/coop/draftuser_detail.html:127 +msgid "Applicant" +msgstr "" + +#: coop/templates/coop/draftuser_detail.html:49 +msgid "Confirm member creation" +msgstr "" + +#: coop/templates/coop/draftuser_detail.html:58 +msgid "" +"\n" +"

\n" +" Members with similar information as the person you are trying to create have been found.\n" +" Please double-check that this is not a duplicate.\n" +"

\n" +" " +msgstr "" + +#: coop/templates/coop/draftuser_detail.html:66 +msgid "List of similar members" +msgstr "" + +#: coop/templates/coop/draftuser_detail.html:69 +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:31 +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:88 +#: coop/views/shareowner.py:583 +#: shifts/templates/shifts/user_shifts_overview_tag.html:21 +msgid "Status" +msgstr "" + +#: coop/templates/coop/draftuser_detail.html:101 +msgid "I confirm that I have checked that this is not a duplicate." +msgstr "" + +#: coop/templates/coop/draftuser_detail.html:119 +#: coop/templates/coop/draftuser_detail.html:254 +msgid "Create Member" +msgstr "" + +#: coop/templates/coop/draftuser_detail.html:166 +msgid "Member Type" +msgstr "" + +#: coop/templates/coop/draftuser_detail.html:186 +msgid "Shares requested" +msgstr "" + +#: coop/templates/coop/draftuser_detail.html:190 +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:53 +#: shifts/models.py:44 +msgid "Welcome Session" +msgstr "" + +#: coop/templates/coop/draftuser_detail.html:208 +msgid "Beteiligungserklärung" +msgstr "" + +#: coop/templates/coop/draftuser_list.html:22 +#: coop/templates/coop/membership_pause/membership_pause_list.html:24 +#: coop/templates/coop/membership_resignation_list.html:25 +#: coop/templates/coop/shareowner_list.html:23 +#: shifts/templates/shifts/members_on_alert_list.html:19 +#: shifts/templates/shifts/shiftexemption_list.html:24 +msgid "Export" +msgstr "" + +#: coop/templates/coop/draftuser_list.html:42 +#: coop/templates/coop/membership_resignation_list.html:50 +#: coop/templates/coop/shareowner_list.html:38 +#: log/templates/log/log_overview.html:34 +#: shifts/templates/shifts/shift_filters.html:3 +msgid "Filters" +msgstr "" + +#: coop/templates/coop/draftuser_list.html:55 +#: coop/templates/coop/incoming_payment_list.html:35 +#: coop/templates/coop/membership_pause/membership_pause_list.html:47 +#: coop/templates/coop/membership_resignation_list.html:62 +#: coop/templates/coop/shareowner_list.html:51 +#: log/templates/log/log_overview.html:46 +#: shifts/templates/shifts/shift_calendar_template.html:66 +#: shifts/templates/shifts/shiftexemption_list.html:50 +msgid "Filter" +msgstr "" + +#: coop/templates/coop/draftuser_list.html:60 +#: coop/templates/coop/incoming_payment_list.html:40 +#: coop/templates/coop/membership_pause/membership_pause_list.html:52 +#: coop/templates/coop/membership_resignation_list.html:67 +#: coop/templates/coop/shareowner_list.html:56 +#: shifts/templates/shifts/shiftexemption_list.html:55 +msgid "Clear all filters" +msgstr "" + +#: coop/templates/coop/draftuser_list.html:63 +#, python-format +msgid "" +"\n" +" Filtered %(filtered)s of %(total)s\n" +" " +msgstr "" + +#: coop/templates/coop/draftuser_register_form.default.html:27 +msgid "Create Applicant" +msgstr "" + +#: coop/templates/coop/draftuser_register_form.default.html:29 +#: coop/templates/coop/draftuser_register_form.html:29 +#: shifts/templates/shifts/register_user_to_shift_slot.html:27 +#: shifts/templates/shifts/register_user_to_shift_slot_template.html:26 +#: shifts/templates/shifts/shift_detail.html:217 +#: shifts/templates/shifts/shift_template_detail.html:84 +msgid "Register" +msgstr "" + +#: coop/templates/coop/draftuser_register_form.html:27 +#: coop/views/draftuser.py:58 coop/views/draftuser.py:59 +msgid "Create applicant" +msgstr "" + +#: coop/templates/coop/email/accounting_recap.body.default.html:5 +#, python-format +msgid "" +"\n" +"

\n" +" Hello Accounting team, hello member office,\n" +"

\n" +"

\n" +" This is the Tapir accounting recap for the past week.\n" +"

\n" +"

\n" +" %(num_new_members)s new members have been created. For those new members, %(total_num_shares_new_members)s shares\n" +" have been created.
\n" +" Additionally, %(total_num_shares_existing_members)s shares have been created for existing members.\n" +"

\n" +" " +msgstr "" + +#: coop/templates/coop/email/accounting_recap.body.default.html:21 +#: statistics/views/main_view.py:301 +msgid "New members" +msgstr "" + +#: coop/templates/coop/email/accounting_recap.body.default.html:26 +#: coop/templates/coop/email/accounting_recap.body.default.html:38 +msgid "share(s)" +msgstr "" + +#: coop/templates/coop/email/accounting_recap.body.default.html:33 +msgid "New shares for existing members upload only after approval in Duo" +msgstr "" + +#: coop/templates/coop/email/accounting_recap.body.default.html:47 +msgid "This email is send automatically every Sunday. Contact the Tapir team on Slack if you have any question." +msgstr "" + +#: coop/templates/coop/email/accounting_recap.subject.default.html:2 +msgid "" +"\n" +" Accounting recap\n" +msgstr "" + +#: coop/templates/coop/email/co_purchaser_updated.body.html:6 +#, python-format +msgid "" +"\n" +"

Hello %(display_name_short)s,

\n" +"

\n" +" This is an automatic e-mail from SuperCoop. We would like to inform you that a co-shopper has just been\n" +" added to your Tapir account.\n" +"

\n" +"

\n" +" From now on %(co_purchaser_name)s can shop in the store using your membership number.\n" +"

\n" +"

\n" +" If this is an error and does not correspond to your request, please send a short\n" +" email to the Member Office to let us know.\n" +"

\n" +"

\n" +" Cooperative greetings,
\n" +" The Member Office\n" +"

\n" +" " +msgstr "" + +#: coop/templates/coop/email/extra_shares_bought.body.html:6 +#, python-format +msgid "" +"\n" +"

Hello %(display_name_short)s,

\n" +"

Please find the confirmation that you have purchased %(num_shares)s additional shares in the email attachment.

\n" +"

Thank you sincerely for your continued support!

\n" +"

\n" +" Cooperative greetings
\n" +" Your SuperCoop-Board (Vorstand)
\n" +"

\n" +" " +msgstr "" + +#: coop/templates/coop/email/extra_shares_bought.subject.html:2 +#, python-format +msgid "" +"\n" +" Confirmation acquisition of additional shares in %(coop_name)s\n" +msgstr "" + +#: coop/templates/coop/email/membership_confirmation.active.body.html:6 +#, python-format +msgid "" +"\n" +"

Hello %(display_name)s,

\n" +"

\n" +" Welcome as an official member of our cooperative, %(coop_full_name)s! Please find the details of your\n" +" membership confirmation attached. We are thrilled to welcome you aboard and begin running SuperCoop together with you!\n" +"

\n" +" If you haven't done so already, please transfer the amount for your shares together with the 10 € \n" +" admission fee to our account DE98430609671121379000. \n" +" It is also important that you complete METRO's online hygiene training before\n" +" starting your first shift at the Coop. You can find all the information on this under point 3.\n" +"

\n" +"

See below for key details to kickstart your SuperCoop membership:

\n" +"

1. Member Office

\n" +"

The Member Office is the hub for supporting members in all things SuperCoop!

\n" +"

\n" +" Please visit the Member Office to sign up for your first shift (or indicate that you qualify for a work\n" +" exemption). They will create an account for you on Tapir, our online shift management system, and guide you\n" +" through the steps of choosing your shift and working group.\n" +"

\n" +"
\n" +" For general questions about your membership, please contact the Member Office via email (%(contact_email_address)s) or by visiting SuperCoop.\n" +" Their opening hours are:\n" +"
    \n" +"
  • Monday - Friday from 4:30 - 7:30 p.m.
  • \n" +"
  • Saturdays from 11 a.m. - 2 p.m.
  • \n" +"
\n" +"
\n" +"

\n" +" 2. Tapir\n" +"

\n" +"

\n" +" Tapir is our shift management system created by and for SuperCoop\n" +" members! There you can sign up for shifts and see additional information about your membership, such as how many\n" +" shares you own.\n" +"

\n" +"
\n" +" Links to the following are also available on Tapir:\n" +"
    \n" +"
  • The current week in the ABCD calendar (including also the ABCD calendar for the rest of the year),
  • \n" +"
  • wiki,
  • \n" +"
  • member manual,
  • \n" +"
  • opening hours,
  • \n" +"
  • Member Office contact information
  • \n" +"
  • as well as statistics about shifts and our cooperative in general.
  • \n" +"
\n" +"
\n" +"

\n" +" To create a profile on Tapir, please visit the Member Office in the supermarket (see office hours above). Don’t\n" +" forget to\n" +" \n" +" add a co-shopper to your account too!\n" +"

\n" +"

\n" +" Once your profile has been created, you will receive a confirmation email including a link to create a password.\n" +" You can then use these same login credentials to log into the SuperCoop Wiki.\n" +"

\n" +"

\n" +" Tip: Bookmark the link to Tapir in your browser so\n" +" it’s easy to find in the future!\n" +"

\n" +"

3. Hygiene Training

\n" +"

\n" +" New members must complete an online hygiene training from METRO BEFORE their first shift:\n" +" https://kw.my/jEM8PK/#/. The training takes about 30 minutes and ends with\n" +" a short questionnaire. If you answer\n" +" 80%% of the questions correctly, you will receive a certificate. Please show this to an employee during your\n" +" first shift.\n" +"

\n" +"

\n" +" During your first shift, the team leader or an employee will provide an additional 10 minute introduction and\n" +" show you the most important stations and rules in the store. If you could not do the online training at home,\n" +" there is also the possibility to do it at our Welcome Desk during your shift.\n" +"

\n" +"

4. Member Card

\n" +"

\n" +" Your Member Card can be activated through your Tapir account. Member cards are consent based because we use them\n" +" to collect data regarding the sum of the total purchase as well as the frequency of purchases. We use this\n" +" information to support our financial planning and calculate projections of future revenues. It also helps us\n" +" understand if certain measures we are taking to improve the assortment are having an impact on how regularly\n" +" people shop at the coop. Conversely, if we see the regularity or total sum of purchases drop, we know we have to\n" +" investigate this further. In this way, using the member card to track certain data also helps us better\n" +" understand the needs of our members!\n" +"

\n" +"

\n" +" To learn more about the member card and data collection, please visit our wiki:\n" +" https://wiki.supercoop.de/wiki/Mitgliederkarte. Any\n" +" concerns about data protection can be addressed to our data\n" +" protection officer and working group at datenschutz-supercoop@posteo.de.\n" +"

\n" +"

Here’s how it works:

\n" +"
\n" +"
    \n" +"
  1. \n" +" Log into your Tapir profile: https://members.supercoop.de/\n" +"
  2. \n" +"
  3. \n" +" Scroll down and you will see an option to give your consent, including an explanation of what data we\n" +" are\n" +" collecting. (Please note that you can give or remove consent at any time with just one click.)\n" +"
  4. \n" +"
  5. Once you have given consent, you will see a barcode that has been created especially for you.
  6. \n" +"
  7. \n" +" The final and most important step (besides giving consent of course) is to remember to scan it at the\n" +" checkout counter! You have different options for how you can do this, including: logging into Tapir on\n" +" your\n" +" phone and showing it from there, downloading it as a PDF and saving it to your phone, taking a\n" +" screenshot,\n" +" or printing it out! (We're happy to print them out for you in the shop if needed!)\n" +"
  8. \n" +"
\n" +"
\n" +"

\n" +" Last, but not certainly least, you can also load credit onto your member card and use it to pay for your\n" +" groceries! This helps both you and the coop avoid transaction fees when making a purchase, and it can be done\n" +" directly at the checkout counter (Kasse)! 🎉\n" +"

\n" +"

5. Working groups

\n" +"

\n" +" Your Working Group tells you what kind of work you do in the Coop.\n" +"

\n" +"

\n" +" Please note: The most important thing is to select a shift time which you can consistently\n" +" attend. As we are self-organised, we rely on the individual responsibility of each member to attend their shift\n" +" and keep SuperCoop running.\n" +"

\n" +"
\n" +" Members (barring any exceptional cases) should complete their first six shifts with one of the following core\n" +" working groups:\n" +"
    \n" +"
  • \n" +" Receiving and Stocking (receiving deliveries, keeping our shelves and coolers stocked,\n" +" and\n" +" checking expiration dates - Rote Karte not required)\n" +"
  • \n" +"
  • \n" +" Cashier\n" +"
  • \n" +"
  • \n" +" Welcome Desk (greeting people at the store entrance, checking member numbers and\n" +" answering\n" +" visitors' questions about SuperCoop)\n" +"
  • \n" +"
  • \n" +" Maintenance (cleaning the coop after closing)\n" +"
  • \n" +"
\n" +"
\n" +"

\n" +" Please note that working groups and tasks dynamically adapt to the development of SuperCoop and are constantly\n" +" updated. You may change working groups as needed.\n" +"

\n" +"

\n" +" You also have opportunities to get involved at SuperCoop in addition to your regular shift. To make it as easy\n" +" as\n" +" possible for you to get started, we have summarized all additional\n" +" working groups in the wiki. Please see the Wiki for further information on how to get involved.\n" +"

\n" +"

6. Member Manual

\n" +"

\n" +" Before sending your questions to the Member Office, please consult the member manual. It\n" +" contains all your need-to-know information about membership at SuperCoop: how to get started, how the shift\n" +" systems work, our governance structure and much more!\n" +"

\n" +"

7. SuperCoopWiki

\n" +"

\n" +" Another good source of information is the SuperCoop Wiki.\n" +" There you will find all the important details on our working groups, member meetings, webshop, your fellow\n" +" co-owners/colleagues and more! It is a \"living\" tool that we all may edit and\n" +" add to as our coop grows. Check out the get started\n" +" page to... well, get started!\n" +"

\n" +"

8. Plenum

\n" +"

\n" +" All members are invited to our plenum (currently held online\n" +" when decision-making is required) to vote on proposals and raise topics for discussion. Dates, links and other\n" +" details are announced on the wiki and in our bi-weekly newsletter.\n" +"

\n" +"

9. Newsletter

\n" +"

\n" +" Wednesday is SuperCoop day! Every two weeks fresh news about current events, information on products or\n" +" publications will land in your email inbox.\n" +"

\n" +"

10. Slack

\n" +"

\n" +" We use the chat app Slack as a direct communication channel for the working groups, projects and general\n" +" information. You can register for free and use Slack directly in your browser or via the app. Join\n" +" here! If you want help getting started, please contact the Membership Office (see above).\n" +"

\n" +"

11. Direct Contact

\n" +"

\n" +" If you wish to contact the Vorstand directly, please use the email %(management_email_address)s.\n" +"

\n" +"

\n" +" To contact the Aufsichtsrat (advisory board), please use the email address %(supervisors_email_address)s. They may be contacted\n" +" regarding any topic which is relevant to the interests of SuperCoop members.\n" +"

\n" +"

12. Social Media Channels

\n" +"

\n" +" Oh yeah, and we are also on the Internet! Follow us on Facebook, Instagram, LinkedIn\n" +" or Nebenan.de and spread the word about SuperCoop.\n" +"

\n" +"

\n" +" We're so glad you're joining us and we hope to see you soon whether on the wiki, Slack, at the Plenum or at\n" +" Osram-Höfe!\n" +"

\n" +"

\n" +" Cooperative Greetings,\n" +"
\n" +" Your SuperCoop-Board\n" +"

\n" +" " +msgstr "" + +#: coop/templates/coop/email/membership_confirmation.active.subject.html:2 +#, python-format +msgid "" +"\n" +" Welcome at %(organization_name)s!\n" +msgstr "" + +#: coop/templates/coop/email/membership_confirmation.investing.body.html:6 +#, python-format +msgid "" +"\n" +"

Dear %(display_name)s,

\n" +"

\n" +" Welcome as an official investing member of %(coop_full_name)s! By subscribing to %(num_shares)s cooperative share/s, \n" +" you have decided to shape the food system together with us and many other coop members!\n" +"

\n" +"

\n" +" We are very happy to confirm your admission to the cooperative with the membership number %(owner_id)s. See the\n" +" attached PDF for full details.\n" +"

\n" +"

\n" +" If you have questions, please contact our member-office (mitglied@supercoop.de)\n" +"

\n" +"

\n" +" Cooperative greetings,\n" +"
\n" +" Your SuperCoop-Board\n" +"

\n" +" " +msgstr "" + +#: coop/templates/coop/email/membership_confirmation.investing.subject.html:2 +#, python-format +msgid "" +"\n" +" Confirmation of the investing membership at %(organization_name)s!\n" +msgstr "" + +#: coop/templates/coop/email/membershipresignation_confirmation_body.html:6 +#, python-format +msgid "" +"\n" +"

Dear %(display_name)s,

\n" +"

\n" +" what a pity that you want to leave us. We hereby confirm the cancellation of your membership with SuperCoop Berlin eG.\n" +"
\n" +"

\n" +" " +msgstr "" + +#: coop/templates/coop/email/membershipresignation_confirmation_body.html:14 +msgid "" +"\n" +"

\n" +" As you stated in your letter of cancellation that you want to transfer your shares to SuperCoop,\n" +" your termination is effective immediately.\n" +"

\n" +" " +msgstr "" + +#: coop/templates/coop/email/membershipresignation_confirmation_body.html:22 +#, python-format +msgid "" +"\n" +"

\n" +" You stated in your letter of cancellation that you want to transfer your shares to another member. We've informed\n" +" %(transferred_member)s about this separately.\n" +" Your termination is effective immediately.\n" +"

\n" +" " +msgstr "" + +#: coop/templates/coop/email/membershipresignation_confirmation_body.html:30 +#, python-format +msgid "" +"\n" +"

\n" +" As you wished in your cancellation letter your membership will run until the end of the regular cancellation period specified in our Articles of Association, i.e.\n" +" until the end of the financial year in three years. Please note the passage stated in our Articles of Association\n" +" under §4 Termination of membership, transfer of business assets, death and exclusion.\n" +" As a precaution, the articles of association are attached to this e-mail.\n" +"

\n" +"

\n" +" As your membership will continue to run as normal for the next three years and is still entered as \"active\" in Tapir,\n" +" we would like to take this opportunity to point out that you can continue to complete shifts in the next three years\n" +" and therefore retain your right to shop. If you no longer wish to complete shifts or shop in Supercoop, we would ask you to apply to\n" +" the member-office (%(contact_email_address)s) to switch your membership to \"investing\".\n" +" \n" +"

\n" +"

\n" +" This will prevent your shift backlog from going into the red and you from receiving emails with warnings from us in the future.\n" +"

\n" +" " +msgstr "" + +#: coop/templates/coop/email/membershipresignation_confirmation_body.html:49 +#, python-format +msgid "" +"\n" +"

\n" +" We wish you all the best for the future and please do not hesitate to contact the member-office\n" +" (%(contact_email_address)s) if you have any further questions.\n" +"

\n" +"

Cooperative greetings,

\n" +"

Your SuperCoop-Board

\n" +" " +msgstr "" + +#: coop/templates/coop/email/membershipresignation_confirmation_subject.html:4 +#, python-format +msgid "" +"\n" +" Confirmation of the cancellation of membership: %(share_owner_name)s\n" +msgstr "" + +#: coop/templates/coop/email/membershipresignation_transferred_shares_confirmation_body.html:7 +#, python-format +msgid "" +"\n" +"

Dear %(receiving_member)s,

\n" +"

\n" +" Good news for you and unfortunately bad news for us!\n" +"
\n" +" %(resigning_member)s has cancelled his membership\n" +" of SuperCoop Berlin eG and bequeathed her/his share(s) to you.\n" +" Don't be surprised if you suddenly see more shares in your Tapir account.\n" +" If this was done without your consent you can contact the member-office\n" +" (%(contact_email_address)s).\n" +"

\n" +"

Cooperative greetings,

\n" +"

Your SuperCoop-Board

\n" +" " +msgstr "" + +#: coop/templates/coop/email/membershipresignation_transferred_shares_confirmation_subject.html:4 +#, python-format +msgid "" +"\n" +" %(resigned_member_full)s bequeathed her / his share(s) to you because of resignation\n" +msgstr "" + +#: coop/templates/coop/email/tapir_account_created.body.html:6 +#, python-format +msgid "" +"\n" +"

Dear %(user_display_name)s,

\n" +"

we just created an account for you in our SuperCoop member system. Here you can view your upcoming shifts, book an additional shift if you'd like or mark your shift as “looking for a stand-in” (for example when you go on vacation).
\n" +" Please see the Member Manual section III for more information on the Stand-in System.

\n" +" " +msgstr "" + +#: coop/templates/coop/email/tapir_account_created.body.html:14 +#, python-format +msgid "" +"\n" +" Your regular ABCD-shift is: %(slot_display_name)s. You will receive a reminder email in advance of your first shift.\n" +" " +msgstr "" + +#: coop/templates/coop/email/tapir_account_created.body.html:19 +msgid "" +"\n" +" Flying members: Please keep in mind that you must have at least one shift “banked” for each shift cycle. For more information please see the Member Manual .\n" +" " +msgstr "" + +#: coop/templates/coop/email/tapir_account_created.body.html:27 +#, python-format +msgid "" +"\n" +"

Your username is *%(username)s* . In order to login to your account, you first have to set a password : Click here to set your password

\n" +"

This link is only valid for a few weeks. Should it expire, you can get a new one here : Click here to get a new link

\n" +"

You can also login to the SuperCoop wiki with that account.

\n" +"

Alternatively, as for any other question, you can always contact the Member Office

\n" +" " +msgstr "" + +#: coop/templates/coop/email/tapir_account_created.body.html:35 +msgid "" +"\n" +" Cooperative regards,
\n" +" Your SuperCoop Berlin Member Office team.\n" +" " +msgstr "" + +#: coop/templates/coop/email/tapir_account_created.subject.html:2 +#, python-format +msgid "Your account in Tapir, %(coop_name)s's member system" +msgstr "" + +#: coop/templates/coop/general_accounts_list.html:7 +#: coop/templates/coop/general_accounts_list.html:16 +msgid "General Tapir Accounts" +msgstr "" + +#: coop/templates/coop/incoming_payment_list.html:9 +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:160 +msgid "Payments" +msgstr "" + +#: coop/templates/coop/incoming_payment_list.html:24 +msgid "Register a new payment" +msgstr "" + +#: coop/templates/coop/log/create_membership_pause_log_entry.html:3 +#, python-format +msgid "" +"\n" +" New membership pause starting %(start_date)s: %(description)s.\n" +msgstr "" + +#: coop/templates/coop/log/create_payment_log_entry.html:2 +#, python-format +msgid "New Payment: %(amount)s € on %(payment_date)s" +msgstr "" + +#: coop/templates/coop/log/create_resignmember_log_entry.html:2 +#, python-format +msgid "" +"\n" +"Member resigned for reason: %(cancellation_reason)s\n" +msgstr "" + +#: coop/templates/coop/log/delete_resignmember_log_entry.html:2 +msgid "" +"\n" +" Reactivated resigned member\n" +msgstr "" + +#: coop/templates/coop/log/update_incoming_payment_log_entry.html:2 +msgid "Updated payment:" +msgstr "" + +#: coop/templates/coop/log/update_membership_pause_log_entry.html:2 +msgid "Updated membership pause:" +msgstr "" + +#: coop/templates/coop/log/update_resignmember_log_entry.html:2 +msgid "Updated resigned membership:" +msgstr "" + +#: coop/templates/coop/log/update_share_owner_log_entry.html:2 +msgid "Update Member:" +msgstr "" + +#: coop/templates/coop/matching_program.html:7 +#: coop/templates/coop/member_management.html:30 +msgid "Matching program" +msgstr "" + +#: coop/templates/coop/matching_program.html:16 +msgid "Matching program members" +msgstr "" + +#: coop/templates/coop/matching_program.html:20 +msgid "" +"\n" +" The members in this list have expressed that they are willing to pay the share for a potential new member that otherwise couldn't afford it.\n" +" " +msgstr "" + +#: coop/templates/coop/member_management.html:16 +#: shifts/templates/shifts/shift_management.html:29 +msgid "Lists" +msgstr "" + +#: coop/templates/coop/member_management.html:21 +msgid "Access right groups" +msgstr "" + +#: coop/templates/coop/member_management.html:26 +#: coop/templates/coop/membership_pause/membership_pause_list.html:10 +#: coop/templates/coop/membership_pause/membership_pause_list.html:19 +msgid "Membership pauses" +msgstr "" + +#: coop/templates/coop/member_management.html:36 +msgid "Membership resignations" +msgstr "" + +#: coop/templates/coop/member_management.html:42 +#: coop/templates/coop/member_management.html:51 +msgid "Old member statistics" +msgstr "" + +#: coop/templates/coop/member_management.html:45 +msgid "" +"\n" +"

This statistics have been hidden from members in order to focus on the more important\n" +" ones.

\n" +"

They may not be relevant or well presented.

\n" +" " +msgstr "" + +#: coop/templates/coop/membership_pause/membership_pause_list.html:36 +msgid "Create a new pause" +msgstr "" + +#: coop/templates/coop/membership_pause/membership_pause_list.html:55 +#, python-format +msgid "" +"\n" +" Filtered %(filtered_pause_count)s of %(total_pause_count)s\n" +" " +msgstr "" + +#: coop/templates/coop/membership_resignation_form.html:51 +#: coop/templates/coop/membership_resignation_form.html:56 +#: coop/views/membership_resignation.py:255 +msgid "Resign a new membership" +msgstr "" + +#: coop/templates/coop/membership_resignation_list.html:12 +#: coop/templates/coop/membership_resignation_list.html:21 +msgid "List of membership resignations" +msgstr "" + +#: coop/templates/coop/membership_resignation_list.html:33 +msgid "" +"\n" +" Only the vorstand and the employees can create resignations.\n" +" " +msgstr "" + +#: coop/templates/coop/membership_resignation_list.html:41 +msgid "Resign new member" +msgstr "" + +#: coop/templates/coop/membershipresignation_detail.html:8 +#: coop/templates/coop/membershipresignation_detail.html:51 +msgid "Membership resignation" +msgstr "" + +#: coop/templates/coop/membershipresignation_detail.html:19 +msgid "Confirm cancellation of resignation" +msgstr "" + +#: coop/templates/coop/membershipresignation_detail.html:28 +#, python-format +msgid "" +"\n" +" Are you sure you want to\n" +" cancel the resignation of %(member_name)s?\n" +" The person's shares will be reactivated.\n" +" " +msgstr "" + +#: coop/templates/coop/membershipresignation_detail.html:55 +msgid "Share owner" +msgstr "" + +#: coop/templates/coop/membershipresignation_detail.html:61 +msgid "Cancellation reason" +msgstr "" + +#: coop/templates/coop/membershipresignation_detail.html:65 +msgid "Cancellation date" +msgstr "" + +#: coop/templates/coop/membershipresignation_detail.html:70 +msgid "Membership ends" +msgstr "" + +#: coop/templates/coop/membershipresignation_detail.html:75 +msgid "Resignation type" +msgstr "" + +#: coop/templates/coop/membershipresignation_detail.html:79 +msgid "Paid out" +msgstr "" + +#: coop/templates/coop/membershipresignation_detail.html:82 +#: coop/views/draftuser.py:286 +msgid "Yes" +msgstr "" + +#: coop/templates/coop/membershipresignation_detail.html:84 +#: coop/views/draftuser.py:286 +msgid "No" +msgstr "" + +#: coop/templates/coop/membershipresignation_detail.html:91 +msgid "" +"\n" +" Only the vorstand and the employees can edit resignations.\n" +" " +msgstr "" + +#: coop/templates/coop/pdf/extra_shares_confirmation_pdf.default.html:12 +#, python-format +msgid "" +"\n" +" Confirmation acquisition of additional shares in %(coop_name)s for %(display_name)s\n" +" " +msgstr "" + +#: coop/templates/coop/pdf/extra_shares_confirmation_pdf.default.html:61 +#, python-format +msgid "Confirmation acquisition of additional shares in %(coop_full_name)s" +msgstr "" + +#: coop/templates/coop/pdf/extra_shares_confirmation_pdf.default.html:66 +#, python-format +msgid "" +"\n" +"

Dear %(display_name_short)s,

\n" +"\n" +"

\n" +" Thank you for your support!
\n" +" This document confirms that you have purchased %(num_shares)s additional shares.\n" +" Once payment is received, this information will also be reflected in your Tapir profile.\n" +"

\n" +"\n" +"

\n" +" Reminder: Your membership number is %(member_number)s.
\n" +" This number is important for your communication with us and the Member Office.\n" +" Please save it as you will need it often.\n" +"

\n" +"\n" +"

\n" +" If you have any questions, feel free to contact the Member Office\n" +" (%(contact_email_address)s)\n" +" and we will answer you as soon as possible.\n" +"

\n" +"\n" +"

\n" +" Thank you for your continued support!\n" +"

\n" +"\n" +"

\n" +" Cooperative greetings
\n" +" Your SuperCoop-Board\n" +"

\n" +" " +msgstr "" + +#: coop/templates/coop/pdf/membership_agreement_pdf.default.html:53 +#: coop/templates/coop/pdf/membership_confirmation_pdf.default.html:45 +msgid "Organisation logo" +msgstr "" + +#: coop/templates/coop/shareowner_detail.html:22 +msgid "Go to user page" +msgstr "" + +#: coop/templates/coop/shareowner_list.html:19 +msgid "Cooperative Members" +msgstr "" + +#: coop/templates/coop/shareowner_list.html:59 +#, python-format +msgid "" +"\n" +" Filtered %(filtered_member_count)s of %(total_member_count)s\n" +" " +msgstr "" + +#: coop/templates/coop/statistics.html:10 +msgid "Members statistics" +msgstr "" + +#: coop/templates/coop/statistics.html:28 +msgid "Loading..." +msgstr "" + +#: coop/templates/coop/statistics.html:48 +msgid "Statistics on members" +msgstr "" + +#: coop/templates/coop/statistics.html:52 +msgid "Active members with account at end of month as CSV" +msgstr "" + +#: coop/templates/coop/statistics.html:72 +#: statistics/templates/statistics/main_statistics.html:27 +msgid "Evolution of member count" +msgstr "" + +#: coop/templates/coop/statistics.html:74 +msgid "Member age distribution" +msgstr "" + +#: coop/templates/coop/statistics.html:82 +msgid "Statistics on shares" +msgstr "" + +#: coop/templates/coop/statistics.html:126 +msgid "Evolution of the number of shares" +msgstr "" + +#: coop/templates/coop/statistics.html:134 +msgid "Members who bought extra shares" +msgstr "" + +#: coop/templates/coop/statistics.html:150 +#: coop/templates/coop/statistics.html:158 +msgid "Member status updates" +msgstr "" + +#: coop/templates/coop/statistics.html:154 +#: coop/templates/coop/statistics.html:170 +msgid "Get as CSV" +msgstr "" + +#: coop/templates/coop/statistics.html:166 +#: coop/templates/coop/statistics.html:174 +msgid "Number of co-purchasers per month" +msgstr "" + +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:9 +#, python-format +msgid "" +"\n" +" Member #%(coop_share_owner_id)s\n" +" " +msgstr "" + +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:17 +msgid "Membership confirmation" +msgstr "" + +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:35 +#, python-format +msgid "" +"\n" +" (membership starting %(start_date)s)\n" +" " +msgstr "" + +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:40 +#, python-format +msgid "" +"\n" +" (membership ending %(end_date)s)\n" +" " +msgstr "" + +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:56 +#: shifts/models.py:945 shifts/templates/shifts/shift_day_printable.html:56 +#: shifts/templates/shifts/shift_detail.html:272 +#: shifts/templates/shifts/shift_detail_printable.html:51 +msgid "Attended" +msgstr "" + +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:58 +#: shifts/models.py:944 +msgid "Pending" +msgstr "" + +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:65 +msgid "Mark Attended" +msgstr "" + +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:74 +msgid "Owned shares" +msgstr "" + +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:82 +msgid "List of shares owned by this member" +msgstr "" + +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:86 +msgid "Starts at" +msgstr "" + +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:87 +msgid "Ends at" +msgstr "" + +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:105 +msgid "Sold or future" +msgstr "" + +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:130 +msgid "" +"\n" +" Only use this to correct mistakes, i.e. if the share was\n" +" erroneously\n" +" entered into the system and the person never actually\n" +" bought it. If the person simply sold their share back to the\n" +" coop, please mark the share as sold instead.\n" +" " +msgstr "" + +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:153 +msgid "Add Shares" +msgstr "" + +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:171 +msgid "Willing to gift a share" +msgstr "" + +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:181 +msgid "Send membership confirmation email" +msgstr "" + +#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:187 +msgid "User is not a cooperative member." +msgstr "" + +#: coop/views/draftuser.py:89 coop/views/draftuser.py:94 +#, python-format +msgid "Edit applicant: %(name)s" +msgstr "" + +#: coop/views/draftuser.py:198 +msgid "Can't create member: " +msgstr "" + +#: coop/views/draftuser.py:276 coop/views/draftuser.py:312 +msgid "Member can be created" +msgstr "" + +#: coop/views/incoming_payments.py:55 +msgid "Payment ID" +msgstr "" + +#: coop/views/incoming_payments.py:78 +msgid "Other member" +msgstr "" + +#: coop/views/incoming_payments.py:166 +msgid "Register payment" +msgstr "" + +#: coop/views/incoming_payments.py:167 +msgid "Register a new incoming payment" +msgstr "" + +#: coop/views/incoming_payments.py:187 coop/views/incoming_payments.py:188 +msgid "Edit payment" +msgstr "" + +#: coop/views/incoming_payments.py:192 +msgid "Payment updated." +msgstr "" + +#: coop/views/incoming_payments.py:216 +msgid "Payment deleted" +msgstr "" + +#: coop/views/management.py:39 +msgid "Required. Please insert a valid email address." +msgstr "" + +#: coop/views/membership_pause.py:139 +msgid "Create a new membership pause" +msgstr "" + +#: coop/views/membership_pause.py:173 coop/views/membership_pause.py:178 +#, python-format +msgid "Edit membership pause: %(name)s" +msgstr "" + +#: coop/views/membership_resignation.py:111 +msgid "Share(s) gifted {chr(8594)} SuperCoop" +msgstr "" + +#: coop/views/membership_resignation.py:117 +msgid "Share(s) gifted" +msgstr "" + +#: coop/views/membership_resignation.py:137 +msgid "Search member or ID" +msgstr "" + +#: coop/views/membership_resignation.py:143 +msgid "Pay out start date" +msgstr "" + +#: coop/views/membership_resignation.py:149 +msgid "Pay out end date" +msgstr "" + +#: coop/views/membership_resignation.py:211 +#: coop/views/membership_resignation.py:216 +#, python-format +msgid "Cancel membership of %(name)s" +msgstr "" + +#: coop/views/shareowner.py:123 coop/views/shareowner.py:128 +#, python-format +msgid "Edit share: %(name)s" +msgstr "" + +#: coop/views/shareowner.py:148 coop/views/shareowner.py:153 +#, python-format +msgid "Add shares to %(name)s" +msgstr "" + +#: coop/views/shareowner.py:362 +msgid "Membership confirmation email sent." +msgstr "" + +#: coop/views/shareowner.py:584 shifts/templates/shifts/shift_filters.html:45 +msgid "Any" +msgstr "" + +#: coop/views/shareowner.py:589 +#: shifts/templates/shifts/user_shifts_overview_tag.html:74 +msgid "Shift Status" +msgstr "" + +#: coop/views/shareowner.py:597 +msgid "Is registered to an ABCD-slot that requires a qualification" +msgstr "" + +#: coop/views/shareowner.py:605 +msgid "Is registered to a slot that requires a qualification" +msgstr "" + +#: coop/views/shareowner.py:613 +msgid "Has qualification" +msgstr "" + +#: coop/views/shareowner.py:621 +msgid "Does not have qualification" +msgstr "" + +#: coop/views/shareowner.py:628 +msgid "ABCD Week" +msgstr "" + +#: coop/views/shareowner.py:631 +msgid "Has unpaid shares" +msgstr "" + +#: coop/views/shareowner.py:634 +msgid "Is fully paid" +msgstr "" + +#: coop/views/shareowner.py:637 +msgid "Name or member ID" +msgstr "" + +#: coop/views/shareowner.py:641 +msgid "Is currently exempted from shifts" +msgstr "" + +#: coop/views/shareowner.py:646 +msgid "Shift Name" +msgstr "" + +#: coop/views/shareowner.py:919 +msgctxt "Willing to give a share" +msgid "No" +msgstr "" + +#: coop/views/statistics.py:142 +msgid "All members" +msgstr "" + +#: coop/views/statistics.py:142 +msgid "Active with account" +msgstr "" + +#: coop/views/statistics.py:187 +msgid "Number of shares" +msgstr "" + +#: coop/views/statistics.py:235 +msgid "Number of members (X-axis) by age (Y-axis)" +msgstr "" + +#: coop/views/statistics.py:283 +msgid "New active members" +msgstr "" + +#: coop/views/statistics.py:284 +msgid "New investing members" +msgstr "" + +#: coop/views/statistics.py:285 +msgid "New active members without account" +msgstr "" + +#: coop/views/statistics.py:286 +msgid "Active to investing" +msgstr "" + +#: coop/views/statistics.py:287 +msgid "Investing to active" +msgstr "" + +#: coop/views/statistics.py:487 +msgid "Number of members with a co-purchaser (X-axis) by month (Y-axis)" +msgstr "" + +#: core/apps.py:19 +msgid "Emails" +msgstr "" + +#: core/apps.py:26 core/templates/core/featureflag_list.html:5 +msgid "Features" +msgstr "" + +#: core/apps.py:33 statistics/apps.py:15 +msgid "Miscellaneous" +msgstr "" + +#: core/apps.py:36 +msgid "Wiki" +msgstr "" + +#: core/apps.py:43 +msgid "Member manual" +msgstr "" + +#: core/apps.py:50 +msgid "Shop opening hours" +msgstr "" + +#: core/apps.py:57 +msgid "Slack chat" +msgstr "" + +#: core/apps.py:64 +msgid "Contact the member office" +msgstr "" + +#: core/apps.py:71 +msgid "About tapir" +msgstr "" + +#: core/models.py:7 +msgid "Flag name" +msgstr "" + +#: core/models.py:10 +msgid "Flag value" +msgstr "" + +#: core/templates/core/base.html:79 +msgid "Use this form to search for members" +msgstr "" + +#: core/templates/core/base.html:80 +msgid "Search Members" +msgstr "" + +#: core/templates/core/email_list.html:5 core/templates/core/email_list.html:10 +msgid "Email list" +msgstr "" + +#: core/templates/core/email_list.html:24 +msgid "This is the list of all emails that can get sent by Tapir." +msgstr "" + +#: core/templates/core/email_list.html:26 +msgid "The previews in the \"Subject\" and \"Body\" columns are fake : they show what the email would look like if it was sent to a random user, using random data." +msgstr "" + +#: core/templates/core/email_list.html:29 +msgid "The files in the \"Last sent example\" column are real : they have been sent to a user exactly as shown. They can therefore contain private information." +msgstr "" + +#: core/templates/core/email_list.html:35 +msgid "List of all emails" +msgstr "" + +#: core/templates/core/email_list.html:39 shifts/models.py:513 +#: shifts/models.py:1083 shifts/templates/shifts/user_shift_account_log.html:29 +msgid "Description" +msgstr "" + +#: core/templates/core/email_list.html:40 +msgid "Subject" +msgstr "" + +#: core/templates/core/email_list.html:41 +msgid "Body" +msgstr "" + +#: core/templates/core/email_list.html:42 +msgid "Last sent example" +msgstr "" + +#: core/templates/core/featureflag_list.html:10 +msgid "Enable or disable features" +msgstr "" + +#: core/templates/core/featureflag_list.html:15 +msgid "List of all feature flags" +msgstr "" + +#: core/templates/core/featureflag_list.html:19 +msgid "Current value" +msgstr "" + +#: core/templates/core/tags/financing_campaign_progress_bar.html:6 +msgid "Current: " +msgstr "" + +#: core/templates/core/tags/financing_campaign_progress_bar.html:6 +msgid "Click this link to learn more." +msgstr "" + +#: core/templates/core/tags/financing_campaign_progress_bar.html:13 +msgid "Goal: " +msgstr "" + +#: core/views.py:47 core/views.py:49 log/models.py:147 +msgid "Not available" +msgstr "" + +#: core/views.py:85 +#, python-format +msgid "Feature: %(name)s" +msgstr "" + +#: financingcampaign/apps.py:16 +msgid "Financing campaigns" +msgstr "" + +#: financingcampaign/templates/financingcampaign/general.html:6 +msgid "Financing campaigns overview" +msgstr "" + +#: financingcampaign/templates/financingcampaign/general.html:12 +msgid "Campaigns" +msgstr "" + +#: financingcampaign/templates/financingcampaign/general.html:16 +msgid "Create campaign" +msgstr "" + +#: financingcampaign/templates/financingcampaign/general.html:22 +msgid "The list of all financing campaigns" +msgstr "" + +#: financingcampaign/templates/financingcampaign/general.html:26 +msgid "Goal" +msgstr "" + +#: financingcampaign/templates/financingcampaign/general.html:29 +#: financingcampaign/templates/financingcampaign/general.html:72 +#: financingcampaign/templates/financingcampaign/general.html:114 +#: shifts/templates/shifts/shift_detail.html:83 +msgid "Actions" +msgstr "" + +#: financingcampaign/templates/financingcampaign/general.html:57 +msgid "Sources" +msgstr "" + +#: financingcampaign/templates/financingcampaign/general.html:61 +msgid "Create source" +msgstr "" + +#: financingcampaign/templates/financingcampaign/general.html:67 +msgid "The list of all financing sources" +msgstr "" + +#: financingcampaign/templates/financingcampaign/general.html:71 +msgid "Linked campaign" +msgstr "" + +#: financingcampaign/templates/financingcampaign/general.html:98 +msgid "Datapoints" +msgstr "" + +#: financingcampaign/templates/financingcampaign/general.html:102 +msgid "Create datapoint" +msgstr "" + +#: financingcampaign/templates/financingcampaign/general.html:108 +msgid "The list of all datapoints" +msgstr "" + +#: financingcampaign/templates/financingcampaign/general.html:111 +#: log/templates/log/log_entry_list_tag.html:23 +#: shifts/templates/shifts/user_shift_account_log.html:26 +#: statistics/templates/statistics/tags/purchase_statistics_card.html:18 +msgid "Date" +msgstr "" + +#: financingcampaign/templates/financingcampaign/general.html:112 +#: shifts/templates/shifts/user_shift_account_log.html:27 +msgid "Value" +msgstr "" + +#: financingcampaign/templates/financingcampaign/general.html:113 +msgid "Linked source" +msgstr "" + +#: financingcampaign/views.py:65 +msgid "Create a new financing campaign" +msgstr "" + +#: financingcampaign/views.py:76 +#, python-format +msgid "Edit financing campaign: %(name)s" +msgstr "" + +#: financingcampaign/views.py:93 +msgid "Create a new financing source" +msgstr "" + +#: financingcampaign/views.py:104 +#, python-format +msgid "Edit financing source: %(name)s" +msgstr "" + +#: financingcampaign/views.py:123 +msgid "Create a new financing source datapoint" +msgstr "" + +#: financingcampaign/views.py:136 +#, python-format +msgid "Edit financing source datapoint: %(name)s" +msgstr "" + +#: log/apps.py:18 log/templates/log/log_overview.html:24 +msgid "Logs" +msgstr "" + +#: log/models.py:21 +msgid "Creation Date" +msgstr "" + +#: log/templates/log/email_log_entry.html:3 +msgid "Email sent." +msgstr "" + +#: log/templates/log/email_log_entry.html:16 +msgid "Email-Inhalt ist aus Sicherheitsgründen nicht verfügbar" +msgstr "" + +#: log/templates/log/log_entry_list_tag.html:8 +msgid "Last 5 log entries" +msgstr "" + +#: log/templates/log/log_entry_list_tag.html:13 +msgid "See all log entries" +msgstr "" + +#: log/templates/log/log_entry_list_tag.html:20 +msgid "List of log entries for this member" +msgstr "" + +#: log/templates/log/log_entry_list_tag.html:24 log/views.py:97 +#: log/views.py:151 +msgid "Actor" +msgstr "" + +#: log/templates/log/log_entry_list_tag.html:25 log/views.py:90 +msgid "Message" +msgstr "" + +#: log/templates/log/log_entry_list_tag.html:57 +#: log/templates/log/log_entry_list_tag.html:58 +msgid "Notes about this user" +msgstr "" + +#: log/templates/log/log_entry_list_tag.html:63 +msgid "Add Note" +msgstr "" + +#: log/views.py:153 +msgid "Log Type" +msgstr "" + +#: shifts/apps.py:36 shifts/templates/shifts/user_shifts_overview_tag.html:8 +msgid "Shifts" +msgstr "" + +#: shifts/apps.py:39 shifts/templates/shifts/shift_calendar_future.html:4 +#: shifts/templates/shifts/shift_calendar_future.html:7 +msgid "Shift calendar" +msgstr "" + +#: shifts/apps.py:46 +msgid "ABCD-shifts week-plan" +msgstr "" + +#: shifts/apps.py:69 shifts/templates/shifts/shift_management.html:7 +msgid "Shift management" +msgstr "" + +#: shifts/apps.py:86 +#, python-brace-format +msgid "ABCD annual calendar, current week: {current_week_group_name}" +msgstr "" + +#: shifts/emails/flying_member_registration_reminder_email.py:17 +msgid "Flying member registration reminder" +msgstr "" + +#: shifts/emails/flying_member_registration_reminder_email.py:22 +#, python-format +msgid "Sent to flying members %(nb_days)s days after a cycle has begun, if they haven't registered to a shift for this cycle." +msgstr "" + +#: shifts/emails/freeze_warning_email.py:28 +msgid "Freeze warning" +msgstr "" + +#: shifts/emails/freeze_warning_email.py:33 +msgid "Sent to a member when their shift status is not frozen yet but will be set to frozen if they don't register for make-up shifts." +msgstr "" + +#: shifts/emails/member_frozen_email.py:23 +msgid "Shift status set to frozen" +msgstr "" + +#: shifts/emails/member_frozen_email.py:28 +msgid "Sent to a member when their shift status gets set to frozen. Usually happens if they miss to many shifts." +msgstr "" + +#: shifts/emails/shift_missed_email.py:22 +msgid "Shift missed" +msgstr "" + +#: shifts/emails/shift_missed_email.py:26 +msgid "Sent to a member when the member office marks the shift as missed" +msgstr "" + +#: shifts/emails/shift_reminder_email.py:22 +msgid "Shift reminder" +msgstr "" + +#: shifts/emails/shift_reminder_email.py:27 +#, python-brace-format +msgid "Sent to a member {config.REMINDER_EMAIL_DAYS_BEFORE_SHIFT} days before their shift" +msgstr "" + +#: shifts/emails/stand_in_found_email.py:22 +msgid "Stand-in found" +msgstr "" + +#: shifts/emails/stand_in_found_email.py:27 +msgid "Sent to a member that was looking for a stand-inwhen the corresponding slot is taken over by another member." +msgstr "" + +#: shifts/emails/unfreeze_notification_email.py:17 +msgid "Unfreeze Notification" +msgstr "" + +#: shifts/emails/unfreeze_notification_email.py:22 +msgid "Sent to a member when their shift status gets set from frozen to flying." +msgstr "" + +#: shifts/forms.py:60 +msgid "The shift must end after it starts." +msgstr "" + +#: shifts/forms.py:110 +msgid "I have read the warning about the missing qualification and confirm that the user should get registered to the shift" +msgstr "" + +#: shifts/forms.py:138 +#, python-brace-format +msgid "The selected user is missing the required qualification for this shift : {missing_capabilities}" +msgstr "" + +#: shifts/forms.py:181 +msgid "Please set the chosen time after the start of the shift" +msgstr "" + +#: shifts/forms.py:186 +msgid "Please set the chosen time before the end of the shift" +msgstr "" + +#: shifts/forms.py:223 +msgid "This user is already registered to another slot in this ABCD shift." +msgstr "" + +#: shifts/forms.py:266 +msgid "You need the shifts.manage permission to do this." +msgstr "" + +#: shifts/forms.py:270 +msgid "This user is already registered to another slot in this shift." +msgstr "" + +#: shifts/forms.py:286 +msgid "I am aware that the member will be unregistered from their ABCD shift" +msgstr "" + +#: shifts/forms.py:295 shifts/templates/shifts/user_shifts_overview_tag.html:93 +msgid "Qualifications" +msgstr "" + +#: shifts/forms.py:323 +#, python-brace-format +msgid "{own_name} is already partner of {partner_of_name}, they can't have a partner of their own" +msgstr "" + +#: shifts/forms.py:383 +msgid "This member is registered to at least one ABCD shift. Please confirm the change of attendance mode with the checkbox below." +msgstr "" + +#: shifts/forms.py:428 +msgid "I have read the warning about the cancelled attendances and confirm that the exemption should be created" +msgstr "" + +#: shifts/forms.py:435 +msgid "I have read the warning about the cancelled ABCD attendances and confirm that the exemption should be created" +msgstr "" + +#: shifts/forms.py:468 +#, python-brace-format +msgid "The member will be unregistered from the following shifts because they are within the range of the exemption : {attendances_display}" +msgstr "" + +#: shifts/forms.py:493 +#, python-format +msgid "The user will be unregistered from the following ABCD shifts because the exemption is longer than %(number_of_cycles)s cycles: %(attendances_display)s " +msgstr "" + +#: shifts/forms.py:536 +msgid "I understand that updating this ABCD shift will update all the corresponding future shifts" +msgstr "" + +#: shifts/forms.py:557 +msgid "I understand that adding or editing a slot to this ABCD shift will affect all the corresponding future shifts" +msgstr "" + +#: shifts/forms.py:566 +msgid "I understand that this will delete the shift exemption and create a membership pause" +msgstr "" + +#: shifts/models.py:38 +msgid "Teamleader" +msgstr "" + +#: shifts/models.py:39 +msgid "Cashier" +msgstr "" + +#: shifts/models.py:40 +msgid "Member Office" +msgstr "" + +#: shifts/models.py:41 +msgid "Bread Delivery" +msgstr "" + +#: shifts/models.py:42 +msgid "Red Card" +msgstr "" + +#: shifts/models.py:43 +msgid "First Aid" +msgstr "" + +#: shifts/models.py:45 +msgid "Handling Cheese" +msgstr "" + +#: shifts/models.py:46 +msgid "Train cheese handlers" +msgstr "" + +#: shifts/models.py:47 +msgid "Inventory" +msgstr "" + +#: shifts/models.py:48 +msgid "Nebenan.de-Support" +msgstr "" + +#: shifts/models.py:62 +msgid "I understand that all working groups help the Warenannahme & Lager working group until the shop opens." +msgstr "" + +#: shifts/models.py:65 +msgid "I understand that all working groups help the Reinigung & Aufräumen working group after the shop closes." +msgstr "" + +#: shifts/models.py:68 +msgid "I understand that I need my own vehicle in order to pick up the bread. A cargo bike can be borrowed, more infos in Slack in the #cargobike channel" +msgstr "" + +#: shifts/models.py:71 +msgid "I understand that I may need to carry heavy weights for this shift." +msgstr "" + +#: shifts/models.py:74 +msgid "I understand that I may need to work high, for example up a ladder. I do not suffer from fear of heights." +msgstr "" + +#: shifts/models.py:185 +msgid "This determines from which date shifts should be generated from this ABCD shift." +msgstr "" + +#: shifts/models.py:189 shifts/models.py:521 +#: shifts/templates/shifts/shift_block_tag.html:9 +msgid "Flexible time" +msgstr "" + +#: shifts/models.py:191 shifts/models.py:523 +msgid "If enabled, members who register for that shift can choose themselves the time where they come do their shift." +msgstr "" + +#: shifts/models.py:417 shifts/models.py:836 +#: shifts/templates/shifts/shift_detail.html:86 +#: shifts/templates/shifts/shift_template_detail.html:42 +msgid "Chosen time" +msgstr "" + +#: shifts/models.py:419 +msgid "This shift lets you choose at what time you come during the day of the shift. In order to help organising the attendance, please specify when you expect to come.Setting or updating this field will set the time for all individual shifts generated from this ABCD shift.You can update the time of a single shift individually and at any time on the shift page." +msgstr "" + +#: shifts/models.py:503 +msgid "Number of required attendances" +msgstr "" + +#: shifts/models.py:505 +msgid "If there are less members registered to a shift than that number, it will be highlighted in the shift calendar." +msgstr "" + +#: shifts/models.py:514 +msgid "Is shown on the shift page below the title" +msgstr "" + +#: shifts/models.py:534 shifts/models.py:540 +msgid "If 'flexible time' is enabled, then the time component is ignored" +msgstr "" + +#: shifts/models.py:838 +msgid "This shift lets you choose at what time you come during the day of the shift. In order to help organising the attendance, please specify when you expect to come." +msgstr "" + +#: shifts/models.py:946 shifts/templates/shifts/shift_day_printable.html:57 +#: shifts/templates/shifts/shift_detail.html:282 +#: shifts/templates/shifts/shift_detail_printable.html:52 +msgid "Missed" +msgstr "" + +#: shifts/models.py:947 shifts/templates/shifts/shift_day_printable.html:58 +#: shifts/templates/shifts/shift_detail.html:312 +#: shifts/templates/shifts/shift_detail_printable.html:53 +msgid "Excused" +msgstr "" + +#: shifts/models.py:948 shifts/templates/shifts/shift_detail.html:320 +msgid "Cancelled" +msgstr "" + +#: shifts/models.py:949 shifts/templates/shifts/shift_day_printable.html:97 +#: shifts/templates/shifts/shift_detail.html:304 +#: shifts/templates/shifts/shift_detail_printable.html:94 +#: shifts/templates/shifts/shift_filters.html:83 +msgid "Looking for a stand-in" +msgstr "" + +#: shifts/models.py:982 +msgid "🏠 ABCD" +msgstr "" + +#: shifts/models.py:983 +msgid "✈ Flying" +msgstr "" + +#: shifts/models.py:984 +msgid "❄ Frozen" +msgstr "" + +#: shifts/models.py:1015 +msgid "Is frozen" +msgstr "" + +#: shifts/models.py:1189 +msgid "Cycle start date" +msgstr "" + +#: shifts/templates/shifts/cancel_shift.html:7 +#: shifts/templates/shifts/cancel_shift.html:13 +msgid "Cancel shift:" +msgstr "" + +#: shifts/templates/shifts/cancel_shift.html:17 +msgid "" +"\n" +" Cancelling a shift is used for example for holidays. It has the following consequences :\n" +"
    \n" +"
  • It is not possible to register to the shift anymore.
  • \n" +"
  • Members who are registered from their ABCD shift get a shift point.
  • \n" +"
  • Members who registered just to this shift don't get a point.
  • \n" +"
\n" +" " +msgstr "" + +#: shifts/templates/shifts/cancel_shift.html:32 +msgid "Confirm cancellation" +msgstr "" + +#: shifts/templates/shifts/convert_exemption_to_pause_form.html:12 +#: shifts/templates/shifts/convert_exemption_to_pause_form.html:18 +msgid "Convert exemption to pause" +msgstr "" + +#: shifts/templates/shifts/convert_exemption_to_pause_form.html:22 +#, python-format +msgid "" +"\n" +"
    \n" +"
  • Start date: %(start_date)s
  • \n" +"
  • End date: %(end_date)s
  • \n" +"
  • Description: %(description)s
  • \n" +"
\n" +" " +msgstr "" + +#: shifts/templates/shifts/email/flying_member_registration_reminder_email.body.html:6 +#, python-format +msgid "" +"\n" +"

Hi %(display_name_short)s,

\n" +"\n" +"

\n" +" we would like to remind you about your next shift\n" +" registration.\n" +"

\n" +"

\n" +" A new shift cycle has been running for a week and we have noticed\n" +" that you have not yet registered for a suitable shift. As a flying member,\n" +" unlike members with a regular ABCD shift, you have to take care of this yourself every four weeks.\n" +"

\n" +"

\n" +" So take a look at the shift calendar on Tapir\n" +" and register for one of the shifts highlighted in blue - this is where the most support is currently needed.\n" +"

\n" +"

\n" +" Cooperative greetings,
\n" +" The Member Office\n" +"

\n" +" " +msgstr "" + +#: shifts/templates/shifts/email/flying_member_registration_reminder_email.subject.html:2 +msgid "Sign up for your next SuperCoop shift" +msgstr "" + +#: shifts/templates/shifts/email/freeze_warning.body.html:6 +#, python-format +msgid "" +"\n" +"

\n" +" Hi %(display_name_short)s,\n" +"

\n" +"\n" +"

\n" +" This is an automated email from %(coop_name)s. This is to kindly inform you that your shift log has\n" +" reached %(account_balance)s.\n" +"

\n" +"\n" +"

\n" +" You now have %(freeze_after_days)s days to register for the outstanding shifts as make-up shifts. These shifts\n" +" must be within the next %(nb_weeks_in_the_future)s weeks.\n" +"

\n" +"

\n" +" Please note: If you do not sign up for your catch-up shifts within the next %(freeze_after_days)s days,\n" +" your account will be frozen. This means you will not be able to shop or vote in general meetings. To\n" +" unfreeze your account, you will need to sign up for your outstanding shifts.\n" +"

\n" +"

\n" +" If you are an ABCD member and do not complete your catch-up shifts, nor your regular shifts, you will be\n" +" removed from your ABCD shift.\n" +"

\n" +"

\n" +" So please sign up for your outstanding shifts as soon as possible. For more information on the shift system,\n" +" please see the Member Manual.\n" +"

\n" +"

\n" +" Thank you!\n" +"

\n" +"

\n" +" Cooperative greetings,
\n" +" The member office\n" +"

\n" +" " +msgstr "" + +#: shifts/templates/shifts/email/freeze_warning.subject.html:2 +#, python-format +msgid "" +"\n" +" Your shift log has reached %(account_balance)s shifts\n" +msgstr "" + +#: shifts/templates/shifts/email/member_frozen.body.html:6 +#, python-format +msgid "" +"\n" +"

Hi %(display_name_short)s,

\n" +"

\n" +" This is an automated email from %(coop_name)s. This is to kindly inform you that your Tapir account at\n" +" SuperCoop has been temporarily frozen. Unfortunately, your shift log has been below %(freeze_threshold)s\n" +" shifts for more than %(freeze_after_days)s days.\n" +"

\n" +"

\n" +" While your account is frozen, you cannot shop or vote in the General Assembly.\n" +"

\n" +"

\n" +" To unfreeze your account, simply sign up for your outstanding shifts as catch-up shifts. Please note that\n" +" these shifts must be within the next %(nb_weeks_in_the_future_for_make_up_shifts)s weeks.\n" +"

\n" +"

\n" +" So please sign up for your outstanding shifts as soon as possible. For more information on the shift system,\n" +" please see the Member Manual.\n" +"

\n" +"

\n" +" Thank you very much!\n" +"

\n" +"

\n" +" Cooperative greetings,
\n" +" The Member Office\n" +"

\n" +" " +msgstr "" + +#: shifts/templates/shifts/email/member_frozen.subject.html:2 +msgid "" +"\n" +" Your Tapir account has been frozen\n" +msgstr "" + +#: shifts/templates/shifts/email/shift_missed.body.html:6 +#, python-format +msgid "" +"\n" +"

Hi %(display_name_short)s,

\n" +"\n" +"

This is an automated email from SuperCoop.

\n" +"\n" +"

\n" +" It has been registered into Tapir that you did not show up for the following shift even though you were\n" +" registered for it: %(shift_display_name)s\n" +"

\n" +"

\n" +" If this is an error, such as you were excused for the shift, please contact the Member Office as soon as\n" +" possible by simply replying to this email.\n" +"

\n" +"

\n" +" In order for the shift system to work in case of absences, the stand-in system (an internal substitution\n" +" system) has been set up. This is to relieve both you and your team members. You can find out exactly how the\n" +" substitution system works in the\n" +" Member\n" +" Manual. Please note that it is your responsibility to actively look for a shift substitute, this\n" +" includes an entry in Tapir as well as actively searching for a substitute e.g. via Slack.\n" +"

\n" +"

\n" +" As a general rule, a member is required to work two make-up shifts for each missed shift for which a\n" +" replacement could not be found. Your Tapir account now contains a note to that effect.\n" +"

\n" +"

\n" +" We ask that you sign up for your make-up shifts in Tapir within the next four weeks to balance your account. Failure to do so can result in the pausing of your purchasing and voting rights until you begin actively working again (see Member\n" +" Manual (Section III.F)\n" +"

\n" +"

Thank you for your understanding.

\n" +"

\n" +" Cooperative Greetings,
\n" +" The Membership Office\n" +"

\n" +" " +msgstr "" + +#: shifts/templates/shifts/email/shift_missed.subject.html:2 +msgid "" +"\n" +" You missed your shift!\n" +msgstr "" + +#: shifts/templates/shifts/email/shift_reminder.body.html:6 +#, python-format +msgid "" +"\n" +"

Hello %(display_name_short)s,

\n" +"\n" +"

We'll see you on %(shift_display_name)s

\n" +"
\n" +" If you can't make it to your shift:\n" +"
    \n" +"
  1. Please set yourself to ‘Looking for a stand-in’ in Tapir
  2. \n" +"
  3. Register for another shift
  4. \n" +"
  5. Contact your team leader or the Member Office within the next 48 hours. This way you avoid an additional catch-up shift.
  6. \n" +"
\n" +"
\n" +"

\n" +"

If you have already contacted your team leader and/or the Member Office about this matter, you can ignore this message.

\n" +"

If you can't be on time for your shift: Please come anyway, any support is important!

\n" +"

You can find more information about the stand-in system in the member manual.

\n" +"

\n" +" Cooperative greetings,
\n" +" The Member Office\n" +"

\n" +" " +msgstr "" + +#: shifts/templates/shifts/email/shift_reminder.subject.html:2 +#, python-format +msgid "" +"\n" +" Your upcoming %(coop_name)s shift: %(shift_name)s\n" +msgstr "" + +#: shifts/templates/shifts/email/stand_in_found.body.html:6 +#, python-format +msgid "" +"\n" +"

Dear %(display_name_short)s,

\n" +"\n" +"

This is an automated email from %(coop_name)s.

\n" +"

\n" +" You were looking for a stand-in for the following shift: %(shift_display_name)s.
\n" +" Another member has taken over your slot for that shift.\n" +"

\n" +"\n" +"

If you haven't done so already, don't forget to attend another shift to compensate for the one you will miss!

\n" +"

\n" +" Cooperative Greetings,
\n" +" The member office\n" +"

\n" +" " +msgstr "" + +#: shifts/templates/shifts/email/stand_in_found.subject.html:2 +msgid "" +"\n" +" You found a stand-in!\n" +msgstr "" + +#: shifts/templates/shifts/email/unfreeze_notification.body.html:6 +#, python-format +msgid "" +"\n" +"

Hi %(display_name_short)s,

\n" +"

\n" +" This is an automatic email from %(coop_name)s. This is to inform you that your Tapir account at %(coop_name)s\n" +" has been reactivated.\n" +"

\n" +"

\n" +" Your shopping and voting rights have been reactivated and you can now shop at %(coop_name)s and vote at\n" +" general meetings again.\n" +"

\n" +"

\n" +" In the future, please make sure to catch up on your missed shifts in a timely manner.\n" +"

\n" +"

\n" +" For more information on the shift system, please see the Member Manual.\n" +"

\n" +"

\n" +" Cooperative Greetings,
\n" +" The member office\n" +"

\n" +" " +msgstr "" + +#: shifts/templates/shifts/email/unfreeze_notification.subject.html:2 +msgid "" +"\n" +" Your Tapir account has been unfrozen.\n" +msgstr "" + +#: shifts/templates/shifts/log/create_exemption_log_entry.html:2 +msgid "without end date" +msgstr "" + +#: shifts/templates/shifts/log/create_exemption_log_entry.html:4 +#, python-format +msgid "Added exemption starting %(start_date)s until %(ending)s" +msgstr "" + +#: shifts/templates/shifts/log/create_shift_attendance_log_entry.html:2 +msgid "Registered for Shift" +msgstr "" + +#: shifts/templates/shifts/log/create_shift_attendance_template_log_entry.html:2 +msgid "Registered for ABCD Shift" +msgstr "" + +#: shifts/templates/shifts/log/delete_shift_attendance_template_log_entry.html:2 +msgid "Unregistered from ABCD Shift" +msgstr "" + +#: shifts/templates/shifts/log/shift_attendance_taken_over_log_entry.html:2 +msgid "Someone took over your shift" +msgstr "" + +#: shifts/templates/shifts/log/update_exemption_log_entry.html:2 +msgid "Update Exemption" +msgstr "" + +#: shifts/templates/shifts/log/update_shift_attendance_state_log_entry.html:2 +msgid "Set shift attendance to" +msgstr "" + +#: shifts/templates/shifts/log/update_shift_user_data_log_entry.html:2 +msgid "Update Shift User Data" +msgstr "" + +#: shifts/templates/shifts/members_on_alert_list.html:10 +#: shifts/templates/shifts/members_on_alert_list.html:15 +#: shifts/templates/shifts/shift_management.html:33 +msgid "Members on alert" +msgstr "" + +#: shifts/templates/shifts/members_on_alert_list.html:31 +msgid "" +"\n" +" The members in this list are on alert relative to their shift account : the current balance is -2\n" +" or less.\n" +" " +msgstr "" + +#: shifts/templates/shifts/register_user_to_shift_slot.html:7 +#: shifts/templates/shifts/register_user_to_shift_slot.html:16 +#: shifts/templates/shifts/register_user_to_shift_slot_template.html:7 +msgid "Register for shift slot" +msgstr "" + +#: shifts/templates/shifts/register_user_to_shift_slot_template.html:16 +msgid "Register for ABCD Shift" +msgstr "" + +#: shifts/templates/shifts/register_user_to_shift_slot_template.html:34 +msgid "Some shifts are already occupied" +msgstr "" + +#: shifts/templates/shifts/register_user_to_shift_slot_template.html:36 +msgid "" +"\n" +"

A flying member has already registered to the shifts linked below.

\n" +"

It is still possible to sign someone up for this shift using the ABCD system. This would mean that the member is signing up to work this shift on a recurring 4-week basis (i.e. once per shift cycle).

\n" +"

IMPORTANT: If a flying member had previously signed up for the same shift, the ABCD member will have to sign up for a different shift for that particular cycle.

\n" +" " +msgstr "" + +#: shifts/templates/shifts/register_user_to_shift_slot_template.html:42 +msgid "Already occupied shifts :" +msgstr "" + +#: shifts/templates/shifts/shift_calendar_future.html:10 +#, python-format +msgid "" +"\n" +"

Below you can find all the upcoming shifts, and who is registered for each shift.

\n" +"

You can sign yourself up for any shift that has a free slot here on TAPIR. You may also cancel it yourself if done at least %(nb_days_for_self_unregister)s days before the shift.
\n" +" When you sign up for one of these shifts, you only commit to the shift on that specific day.

\n" +"

To register for a regular ABCD shift you must contact the Member Office.

\n" +" " +msgstr "" + +#: shifts/templates/shifts/shift_calendar_past.html:4 +msgid "Past shifts" +msgstr "" + +#: shifts/templates/shifts/shift_calendar_template.html:27 +msgid "Legend & Filters" +msgstr "" + +#: shifts/templates/shifts/shift_calendar_template.html:50 +msgid "starting" +msgstr "" + +#: shifts/templates/shifts/shift_calendar_template.html:51 +#: shifts/templates/shifts/shift_template_overview.html:40 +msgid "Week " +msgstr "" + +#: shifts/templates/shifts/shift_calendar_template.html:55 +msgid "From" +msgstr "" + +#: shifts/templates/shifts/shift_calendar_template.html:62 +msgid "To" +msgstr "" + +#: shifts/templates/shifts/shift_day_printable.html:54 +#: shifts/templates/shifts/shift_detail.html:79 +#: shifts/templates/shifts/shift_detail_printable.html:49 +msgid "Slot" +msgstr "" + +#: shifts/templates/shifts/shift_day_printable.html:72 +#: shifts/templates/shifts/shift_detail_printable.html:68 +msgid "Required qualifications: " +msgstr "" + +#: shifts/templates/shifts/shift_day_printable.html:86 +#: shifts/templates/shifts/shift_detail_printable.html:82 +msgid "Expected to come at" +msgstr "" + +#: shifts/templates/shifts/shift_day_printable.html:88 +#: shifts/templates/shifts/shift_detail_printable.html:84 +msgid "Flexible time not specified" +msgstr "" + +#: shifts/templates/shifts/shift_day_printable.html:94 +#: shifts/templates/shifts/shift_detail.html:113 +#: shifts/templates/shifts/shift_detail_printable.html:90 +#: shifts/templates/shifts/shift_template_detail.html:64 +msgid "Shift partner: " +msgstr "" + +#: shifts/templates/shifts/shift_day_printable.html:104 +#: shifts/templates/shifts/shift_detail_printable.html:100 +msgid "Has qualifications: " +msgstr "" + +#: shifts/templates/shifts/shift_detail.html:7 +#: shifts/templates/shifts/shift_detail.html:29 +msgid "Shift" +msgstr "" + +#: shifts/templates/shifts/shift_detail.html:35 +msgid "Get printable version" +msgstr "" + +#: shifts/templates/shifts/shift_detail.html:39 +#: shifts/templates/shifts/shift_template_detail.html:20 +msgid "Add a slot" +msgstr "" + +#: shifts/templates/shifts/shift_detail.html:49 +msgid "Cancel shift" +msgstr "" + +#: shifts/templates/shifts/shift_detail.html:57 +msgid "Generated from" +msgstr "" + +#: shifts/templates/shifts/shift_detail.html:67 +msgid "This shift has been cancelled: " +msgstr "" + +#: shifts/templates/shifts/shift_detail.html:76 +msgid "List of slots for this shift" +msgstr "" + +#: shifts/templates/shifts/shift_detail.html:79 +msgid "Number" +msgstr "" + +#: shifts/templates/shifts/shift_detail.html:80 +#: shifts/templates/shifts/shift_template_detail.html:38 +msgid "Details" +msgstr "" + +#: shifts/templates/shifts/shift_detail.html:81 +#: shifts/templates/shifts/shift_template_detail.html:40 +msgid "Registered user" +msgstr "" + +#: shifts/templates/shifts/shift_detail.html:82 +msgid "Attendance" +msgstr "" + +#: shifts/templates/shifts/shift_detail.html:84 +msgid "Do you meet the requirements?" +msgstr "" + +#: shifts/templates/shifts/shift_detail.html:89 +#: shifts/templates/shifts/shift_template_detail.html:45 +msgid "Member-Office actions" +msgstr "" + +#: shifts/templates/shifts/shift_detail.html:90 +msgid "Previous attendances" +msgstr "" + +#: shifts/templates/shifts/shift_detail.html:121 +msgid "since" +msgstr "" + +#: shifts/templates/shifts/shift_detail.html:134 +msgid "Vacant" +msgstr "" + +#: shifts/templates/shifts/shift_detail.html:151 +msgid "Cancels the search for a stand-in. Use this if you want to attend the shift." +msgstr "" + +#: shifts/templates/shifts/shift_detail.html:153 +#: shifts/templates/shifts/shift_detail.html:293 +msgid "Cancel looking for a stand-in" +msgstr "" + +#: shifts/templates/shifts/shift_detail.html:161 +#, python-format +msgid "" +"You can only look for\n" +" a\n" +" stand-in\n" +" %(NB_DAYS_FOR_SELF_LOOK_FOR_STAND_IN)s days before the\n" +" shift. If\n" +" you can't\n" +" attend, contact your shift leader as soon as\n" +" possible." +msgstr "" + +#: shifts/templates/shifts/shift_detail.html:174 +msgid "Look for a stand-in" +msgstr "" + +#: shifts/templates/shifts/shift_detail.html:183 +#, python-format +msgid "" +"You can only\n" +" unregister\n" +" yourself\n" +" %(NB_DAYS_FOR_SELF_UNREGISTER)s days before the shift. Also,\n" +" ABCD-Shifts\n" +" can't be unregistered from. If you can't\n" +" attend, look for a stand-in or contact your shift leader as soon\n" +" as\n" +" possible." +msgstr "" + +#: shifts/templates/shifts/shift_detail.html:198 +msgid "Unregister myself" +msgstr "" + +#: shifts/templates/shifts/shift_detail.html:203 +msgid "" +"You can only register\n" +" yourself\n" +" for a shift if:
\n" +" -You are not registered to another slot in that shift
\n" +" -You have the required qualification (if you want to get a\n" +" qualification, contact the member office)
\n" +" -The shift is in the future
\n" +" -The shift is not cancelled (holidays...)
\n" +" " +msgstr "" + +#: shifts/templates/shifts/shift_detail.html:245 +#: shifts/templates/shifts/shift_template_detail.html:95 +msgid "Not specified" +msgstr "" + +#: shifts/templates/shifts/shift_detail.html:328 +msgid "Edit slot" +msgstr "" + +#: shifts/templates/shifts/shift_filters.html:7 +msgid "Use the buttons below to filter the shifts that are relevant to you." +msgstr "" + +#: shifts/templates/shifts/shift_filters.html:16 +msgid "No filter" +msgstr "" + +#: shifts/templates/shifts/shift_filters.html:23 +msgid "Has a free slot" +msgstr "" + +#: shifts/templates/shifts/shift_filters.html:30 +msgid "Most help needed" +msgstr "" + +#: shifts/templates/shifts/shift_filters.html:38 +msgid "Filter for type of slot" +msgstr "" + +#: shifts/templates/shifts/shift_filters.html:60 +msgid "Legend" +msgstr "" + +#: shifts/templates/shifts/shift_filters.html:68 +msgctxt "Filters" +msgid "Other shifts" +msgstr "" + +#: shifts/templates/shifts/shift_filters.html:71 +msgctxt "Filters" +msgid "Cancelled" +msgstr "" + +#: shifts/templates/shifts/shift_filters.html:74 +msgid "Free slot" +msgstr "" + +#: shifts/templates/shifts/shift_filters.html:77 +msgid "ABCD attendance" +msgstr "" + +#: shifts/templates/shifts/shift_filters.html:80 +msgid "Flying attendance" +msgstr "" + +#: shifts/templates/shifts/shift_form.html:20 +msgid "" +"\n" +"

Be careful when creating a shift, as you won't be able to delete it afterwards.

\n" +"

Slots can be added to the shift after it has been created.

\n" +" " +msgstr "" + +#: shifts/templates/shifts/shift_management.html:16 +msgid "Add shifts" +msgstr "" + +#: shifts/templates/shifts/shift_management.html:20 +msgid "Add a shift" +msgstr "" + +#: shifts/templates/shifts/shift_management.html:24 +msgid "Add an ABCD shift" +msgstr "" + +#: shifts/templates/shifts/shift_management.html:37 +#: shifts/templates/shifts/shiftexemption_list.html:10 +#: shifts/templates/shifts/shiftexemption_list.html:19 +msgid "Shift exemptions" +msgstr "" + +#: shifts/templates/shifts/shift_management.html:44 +#: shifts/templates/shifts/shift_management.html:56 +msgid "Update frozen statuses" +msgstr "" + +#: shifts/templates/shifts/shift_management.html:47 +msgid "" +"\n" +"

For every member, checks if they should get the frozen status, if they should get\n" +" unfrozen, or if\n" +" they should get the warning that they will be frozen soon.

\n" +"

This command is run automatically once per day. You can trigger it here manually for test\n" +" purposes or to accelerate a status update.

\n" +" " +msgstr "" + +#: shifts/templates/shifts/shift_management.html:63 +msgid "Generate shifts from ABCD shifts" +msgstr "" + +#: shifts/templates/shifts/shift_management.html:66 +msgid "" +"\n" +"

This generates shifts for the coming 200 days according to the ABCD-shift-calendar.

\n" +"

This command is run automatically once per day. You can trigger it manually to see results\n" +" faster.

\n" +" " +msgstr "" + +#: shifts/templates/shifts/shift_management.html:73 +msgid "Generate shifts" +msgstr "" + +#: shifts/templates/shifts/shift_management.html:79 +#: shifts/templates/shifts/shift_management.html:87 +msgid "Old shift statistics" +msgstr "" + +#: shifts/templates/shifts/shift_management.html:82 +msgid "" +"\n" +"

This statistics have been hidden from members in order to focus on the more important ones.

\n" +"

They may not be relevant or well presented.

\n" +" " +msgstr "" + +#: shifts/templates/shifts/shift_template_detail.html:7 +#: shifts/templates/shifts/shift_template_detail.html:15 +#: shifts/templates/shifts/user_shifts_overview_tag.html:26 +#: shifts/templates/shifts/user_shifts_overview_tag.html:38 +msgid "ABCD Shift" +msgstr "" + +#: shifts/templates/shifts/shift_template_detail.html:34 +msgid "List of slots for this ABCD shifts" +msgstr "" + +#: shifts/templates/shifts/shift_template_detail.html:39 +msgid "Requirements" +msgstr "" + +#: shifts/templates/shifts/shift_template_detail.html:73 +msgid "Unregister" +msgstr "" + +#: shifts/templates/shifts/shift_template_detail.html:122 +msgid "Future generated Shifts" +msgstr "" + +#: shifts/templates/shifts/shift_template_detail.html:132 +msgid "Past generated Shifts" +msgstr "" + +#: shifts/templates/shifts/shift_template_group_calendar.html:9 +#: shifts/templates/shifts/shift_template_group_calendar.html:18 +msgid "ABCD weeks calendar" +msgstr "" + +#: shifts/templates/shifts/shift_template_group_calendar.html:27 +msgid "as pdf" +msgstr "" + +#: shifts/templates/shifts/shift_template_overview.html:12 +msgid "ABCD week-plan" +msgstr "" + +#: shifts/templates/shifts/shift_template_overview.html:19 +msgid "ABCD schedule overview, current week: " +msgstr "" + +#: shifts/templates/shifts/shift_template_overview.html:22 +msgid "" +"\n" +"

This is the ABCD shiftplan. It repeats every four weeks. That's why you see only weekdays\n" +" (Monday, Tuesday...) and the week (A,B,C or D), instead of specific dates (e.g. 23.8.2021).\n" +" To see the date of your next shift, please go to your profile.

\n" +"

If you would like to change your regular ABCD shift, please contact the Member Office.

\n" +" " +msgstr "" + +#: shifts/templates/shifts/shift_template_overview.html:35 +msgid "Calendar of ABCD shifts" +msgstr "" + +#: shifts/templates/shifts/shiftexemption_list.html:38 +msgid "Create shift exemption" +msgstr "" + +#: shifts/templates/shifts/shiftexemption_list.html:58 +#, python-format +msgid "" +"\n" +" Filtered %(filtered_exemption_count)s of %(total_exemption_count)s\n" +" " +msgstr "" + +#: shifts/templates/shifts/shiftslot_form.html:20 +msgid "" +"\n" +" Be careful when creating a shift slot, as you won't be able to delete it afterwards.\n" +" " +msgstr "" + +#: shifts/templates/shifts/solidarity_shifts_overview.html:14 +msgid "Available" +msgstr "" + +#: shifts/templates/shifts/solidarity_shifts_overview.html:17 +#, python-format +msgid "" +"\n" +"

There is %(available_solidarity_shifts)s solidarity shift available at the moment

\n" +" " +msgstr "" + +#: shifts/templates/shifts/solidarity_shifts_overview.html:21 +#, python-format +msgid "" +"\n" +"

There are %(available_solidarity_shifts)s solidarity shifts available at the moment

\n" +" " +msgstr "" + +#: shifts/templates/shifts/solidarity_shifts_overview.html:30 +msgid "Used" +msgstr "" + +#: shifts/templates/shifts/solidarity_shifts_overview.html:33 +#, python-format +msgid "" +"\n" +"

%(used_solidarity_shifts_total)s solidarity shift has been used in total

\n" +" " +msgstr "" + +#: shifts/templates/shifts/solidarity_shifts_overview.html:37 +#, python-format +msgid "" +"\n" +"

%(used_solidarity_shifts_total)s solidarity shifts have been used in total

\n" +" " +msgstr "" + +#: shifts/templates/shifts/solidarity_shifts_overview.html:41 +#: shifts/views/solidarity.py:208 +msgid "Solidarity shifts used" +msgstr "" + +#: shifts/templates/shifts/solidarity_shifts_overview.html:50 +msgid "Gifted" +msgstr "" + +#: shifts/templates/shifts/solidarity_shifts_overview.html:53 +#, python-format +msgid "" +"\n" +"

%(gifted_solidarity_shifts_total)s solidarity shift has been gifted in total

\n" +" " +msgstr "" + +#: shifts/templates/shifts/solidarity_shifts_overview.html:57 +#, python-format +msgid "" +"\n" +"

%(gifted_solidarity_shifts_total)s solidarity shifts have been gifted in total

\n" +" " +msgstr "" + +#: shifts/templates/shifts/solidarity_shifts_overview.html:61 +#: shifts/views/solidarity.py:179 +msgid "Solidarity shifts gifted" +msgstr "" + +#: shifts/templates/shifts/statistics.html:9 +msgid "Shift statistics" +msgstr "" + +#: shifts/templates/shifts/statistics.html:66 +msgid "Statistics on shifts" +msgstr "" + +#: shifts/templates/shifts/statistics.html:81 +msgid "Gives details of the attendance per slot type." +msgstr "" + +#: shifts/templates/shifts/statistics.html:107 +msgid "Gives details of the attendance for the past, current and next week." +msgstr "" + +#: shifts/templates/shifts/statistics.html:131 +msgid "Shift cycle list" +msgstr "" + +#: shifts/templates/shifts/statistics.html:135 +msgid "Gives details of the attendance per shift cycle." +msgstr "" + +#: shifts/templates/shifts/statistics.html:158 +msgid "Data exports" +msgstr "" + +#: shifts/templates/shifts/statistics.html:164 +msgid "Slot data" +msgstr "" + +#: shifts/templates/shifts/statistics.html:169 +msgid "ABCD shift data" +msgstr "" + +#: shifts/templates/shifts/statistics.html:174 +msgid "ABCD shift slot data" +msgstr "" + +#: shifts/templates/shifts/statistics.html:179 +msgid "Shift data" +msgstr "" + +#: shifts/templates/shifts/statistics.html:184 +msgid "Shift slot data" +msgstr "" + +#: shifts/templates/shifts/statistics.html:189 +msgid "ABCD attendance data" +msgstr "" + +#: shifts/templates/shifts/statistics.html:194 +msgid "Attendance data" +msgstr "" + +#: shifts/templates/shifts/statistics.html:199 +msgid "Attendance updates data" +msgstr "" + +#: shifts/templates/shifts/statistics.html:204 +msgid "Attendance takeover data" +msgstr "" + +#: shifts/templates/shifts/statistics.html:212 +msgid "Evolution of shift status" +msgstr "" + +#: shifts/templates/shifts/statistics.html:215 +msgid "Shift status evolution" +msgstr "" + +#: shifts/templates/shifts/user_shift_account_log.html:10 +msgid "Shift account log for: " +msgstr "" + +#: shifts/templates/shifts/user_shift_account_log.html:16 +msgid "Add a manual entry" +msgstr "" + +#: shifts/templates/shifts/user_shift_account_log.html:23 +msgid "List of changes to this members shift account " +msgstr "" + +#: shifts/templates/shifts/user_shift_account_log.html:28 +msgid "Balance at date" +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:45 +msgid "Find an ABCD shift" +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:52 +msgid "Upcoming Shift" +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:59 +msgid "Show more" +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:68 +msgctxt "No upcoming shift" +msgid "None" +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:77 +msgid "OK" +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:79 +msgid "Shift for ongoing cycle pending" +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:81 +#, python-format +msgid "" +"\n" +" %(num_banked_shifts)s banked shifts\n" +" " +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:86 +msgid "On alert" +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:89 +msgid "log" +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:98 +msgctxt "No qualifications" +msgid "None" +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:103 +msgid "Exemption" +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:108 +msgid "until" +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:115 +msgctxt "No shift exemption" +msgid "None" +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:123 +msgid "View all" +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:130 +msgid "Solidarity" +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:137 +msgid "Receive Solidarity" +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:147 +msgid "Give Solidarity" +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:167 +msgid "One of your banked shifts will be donated as a solidarity shift. Do you want to continue?" +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:172 +msgid "Cancel" +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:175 +msgid "Confirm" +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:185 +msgid "Solidarity Status" +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:189 +#, python-format +msgid "" +"\n" +"

You already used %(used_solidarity_shifts_current_year)s out of 2 Solidarity Shifts\n" +" this year

\n" +" " +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:194 +#, python-format +msgid "" +"\n" +"

There are Solidarity Shifts available for you to use. You\n" +" used %(used_solidarity_shifts_current_year)s out of 2 Solidarity Shifts this year

\n" +" " +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:199 +msgid "" +"\n" +"

\n" +" You cannot receive a Solidarity Shift at the moment

\n" +" " +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:204 +msgid "" +"\n" +"

There are no Solidarity Shifts available at the moment

\n" +" " +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:213 +msgid "Shift partner" +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:220 +msgid "" +"\n" +" You can email the member office to ask for a shift partner to be registered.
\n" +" See member manual section III.F.6\n" +" " +msgstr "" + +#: shifts/templates/shifts/user_shifts_overview_tag.html:225 +msgctxt "No shift partner" +msgid "None" +msgstr "" + +#: shifts/templatetags/shifts.py:61 shifts/templatetags/shifts.py:165 +#: shifts/views/views.py:144 +msgid "General" +msgstr "" + +#: shifts/utils.py:166 +#, python-brace-format +msgid "Unknown mode {attendance_mode}" +msgstr "" + +#: shifts/views/attendance.py:194 shifts/views/attendance.py:336 +#, python-format +msgid "Shift attendance: %(name)s" +msgstr "" + +#: shifts/views/attendance.py:200 shifts/views/attendance.py:342 +#, python-format +msgid "Updating shift attendance: %(member_link)s, %(slot_link)s" +msgstr "" + +#: shifts/views/attendance.py:376 +#, python-format +msgid "ABCD attendance: %(name)s" +msgstr "" + +#: shifts/views/attendance.py:383 +#, python-format +msgid "Updating ABCD attendance: %(member_link)s, %(slot_link)s" +msgstr "" + +#: shifts/views/exemptions.py:83 +msgid "Is covered by shift exemption: " +msgstr "" + +#: shifts/views/exemptions.py:108 shifts/views/exemptions.py:158 +#, python-format +msgid "Shift exemption: %(name)s" +msgstr "" + +#: shifts/views/exemptions.py:113 +#, python-format +msgid "Create shift exemption for: %(link)s" +msgstr "" + +#: shifts/views/exemptions.py:163 +#, python-format +msgid "Edit shift exemption for: %(link)s" +msgstr "" + +#: shifts/views/exemptions.py:295 +msgid "Shift exemption converted to membership pause." +msgstr "" + +#: shifts/views/management.py:36 +msgid "Create a shift" +msgstr "" + +#: shifts/views/management.py:76 +msgid "Edit slot: " +msgstr "" + +#: shifts/views/management.py:120 +msgid "Edit shift: " +msgstr "" + +#: shifts/views/management.py:134 +msgid "Edit shift template: " +msgstr "" + +#: shifts/views/management.py:153 +msgid "Create an ABCD shift" +msgstr "" + +#: shifts/views/management.py:155 +msgid "Shifts are generated every day at midnight. After you created the ABCD shift, come back tomorrow to see your shifts!" +msgstr "" + +#: shifts/views/management.py:226 +msgid "Shifts generated." +msgstr "" + +#: shifts/views/solidarity.py:70 +msgid "Solidarity Shift received. Account Balance increased by 1." +msgstr "" + +#: shifts/views/solidarity.py:99 +msgid "Could not find a shift attendance to use as solidarity shift. You have to finish at least one shift, before you can donate one." +msgstr "" + +#: shifts/views/solidarity.py:118 +msgid "Solidarity Shift given. Account Balance debited with -1." +msgstr "" + +#: shifts/views/views.py:124 shifts/views/views.py:129 +#, python-format +msgid "Edit user shift data: %(name)s" +msgstr "" + +#: shifts/views/views.py:197 +#, python-format +msgid "Shift account: %(name)s" +msgstr "" + +#: shifts/views/views.py:203 +#, python-format +msgid "Create manual shift account entry for: %(link)s" +msgstr "" + +#: shifts/views/views.py:354 +msgid "Frozen statuses updated." +msgstr "" + +#: statistics/apps.py:18 +msgid "Statistics" +msgstr "" + +#: statistics/templates/statistics/fancy_graph.html:10 +msgid "Fancy graph" +msgstr "" + +#: statistics/templates/statistics/main_statistics.html:9 +msgid "Main statistics" +msgstr "" + +#: statistics/templates/statistics/main_statistics.html:19 +#: statistics/views/main_view.py:260 +msgid "Total number of members" +msgstr "" + +#: statistics/templates/statistics/main_statistics.html:22 +#, python-format +msgid "" +"\n" +" All members of the cooperative - whether investing or active: %(number_of_members_now)s.\n" +" " +msgstr "" + +#: statistics/templates/statistics/main_statistics.html:31 +msgid "New members per month" +msgstr "" + +#: statistics/templates/statistics/main_statistics.html:39 +msgid "Targets for break-even" +msgstr "" + +#: statistics/templates/statistics/main_statistics.html:42 +msgid "Shopping basket" +msgstr "" + +#: statistics/templates/statistics/main_statistics.html:44 +#, python-format +msgid "" +"\n" +" The current target food basket value per member and per month to reach the break-even is\n" +" %(target_basket)s€. If you have enabled purchase tracking, you can see your average\n" +" basket value on your profile page.\n" +" " +msgstr "" + +#: statistics/templates/statistics/main_statistics.html:52 +msgid "Members eligible to purchase" +msgstr "" + +#: statistics/templates/statistics/main_statistics.html:54 +#, python-format +msgid "" +"\n" +"

\n" +" All working members and all members who have an exemption (such as parental leave,\n" +" over 70, etc.). Members who are frozen (and have not yet signed up for their\n" +" catch-up shifts) or on break (3 shift cycles or longer away) are not eligible to\n" +" purchase.\n" +"

\n" +"

\n" +" Target number of purchasing members for break-even: %(target_count)s.\n" +"

\n" +" " +msgstr "" + +#: statistics/templates/statistics/main_statistics.html:71 +#: statistics/templates/statistics/main_statistics.html:103 +msgid "Current" +msgstr "" + +#: statistics/templates/statistics/main_statistics.html:88 +msgid "Working members" +msgstr "" + +#: statistics/templates/statistics/main_statistics.html:92 +#, python-format +msgid "" +"\n" +" Working members are active members who do not have an exemption. Exemptions are, for\n" +" example, one year of parental leave, prolonged illness or members over 70 years of age.\n" +" Required number of working members to fulfil all shift placements: %(target_count)s.\n" +" " +msgstr "" + +#: statistics/templates/statistics/main_statistics.html:118 +#: statistics/templates/statistics/main_statistics.html:129 +#: statistics/views/main_view.py:382 +msgid "Frozen members" +msgstr "" + +#: statistics/templates/statistics/main_statistics.html:122 +msgid "" +"\n" +" Any member who is registered as an active member but is 4 or more shifts short and\n" +" therefore\n" +" not eligible to purchase again until they sign up for the appropriate make-up shifts.\n" +" " +msgstr "" + +#: statistics/templates/statistics/main_statistics.html:139 +msgid "Co-purchasers" +msgstr "" + +#: statistics/templates/statistics/main_statistics.html:143 +msgid "" +"\n" +" Each member can designate one person (whether in their own household or not) to shop\n" +" under the same membership number. This can be investing members or non-members.\n" +" " +msgstr "" + +#: statistics/templates/statistics/main_statistics.html:149 +msgid "Co-Purchasers" +msgstr "" + +#: statistics/templates/statistics/main_statistics.html:163 +msgid "" +"\n" +" Here you can follow the progress of the funding campaign. Both additional shares\n" +" (all shares that are subscribed to over and above the compulsory share) and\n" +" subordinated loans are counted. The period runs from 12.09.2023 - 09.12.2023.\n" +" What one person can't achieve, many can!\n" +" " +msgstr "" + +#: statistics/templates/statistics/shift_cancelling_rate.html:9 +msgid "Shift attendance rates" +msgstr "" + +#: statistics/templates/statistics/shift_cancelling_rate.html:19 +#: statistics/templates/statistics/shift_cancelling_rate.html:69 +msgid "Shift cancellation rate" +msgstr "" + +#: statistics/templates/statistics/shift_cancelling_rate.html:78 +msgid "Number of shifts by category" +msgstr "" + +#: statistics/templates/statistics/state_distribution.html:9 +#: statistics/templates/statistics/state_distribution.html:19 +#: statistics/templates/statistics/state_distribution.html:36 +msgid "State distribution" +msgstr "" + +#: statistics/templates/statistics/stats_for_marie.html:9 +msgid "Stats for Marie" +msgstr "" + +#: statistics/templates/statistics/stats_for_marie.html:20 +#: statistics/templates/statistics/stats_for_marie.html:26 +msgid "Number of frozen members per month" +msgstr "" + +#: statistics/templates/statistics/stats_for_marie.html:33 +#: statistics/templates/statistics/stats_for_marie.html:39 +msgid "Number of purchasing members per month" +msgstr "" + +#: statistics/templates/statistics/tags/on_demand_chart.html:5 +msgid "Show graph: " +msgstr "" + +#: statistics/templates/statistics/tags/purchase_statistics_card.html:6 +msgid "Purchases" +msgstr "" + +#: statistics/templates/statistics/tags/purchase_statistics_card.html:9 +#, python-format +msgid "" +"\n" +" Your average basket per month is %(average)s€.\n" +" " +msgstr "" + +#: statistics/templates/statistics/tags/purchase_statistics_card.html:15 +msgid "List of the last purchases" +msgstr "" + +#: statistics/templates/statistics/tags/purchase_statistics_card.html:19 +msgid "Gross amount" +msgstr "" + +#: statistics/templates/statistics/tags/purchase_statistics_card.html:35 +msgid "Evolution of total spends per month" +msgstr "" + +#: statistics/views/main_view.py:332 +msgid "Total spends per month" +msgstr "" + +#: statistics/views/main_view.py:382 +msgid "Purchasing members" +msgstr "" + +#: statistics/views/main_view.py:400 +msgid "Percentage of members with a co-purchaser relative to the number of active members" +msgstr "" + +#: statistics/views/main_view.py:552 +msgid "Purchase data updated" +msgstr "" + +#: utils/forms.py:25 +msgid "German phone number don't need a prefix (e.g. (0)1736160646), international always (e.g. +12125552368)" +msgstr "" + +#: utils/models.py:15 +msgid "Andorra" +msgstr "" + +#: utils/models.py:16 +msgid "United Arab Emirates" +msgstr "" + +#: utils/models.py:17 +msgid "Afghanistan" +msgstr "" + +#: utils/models.py:18 +msgid "Antigua & Barbuda" +msgstr "" + +#: utils/models.py:19 +msgid "Anguilla" +msgstr "" + +#: utils/models.py:20 +msgid "Albania" +msgstr "" + +#: utils/models.py:21 +msgid "Armenia" +msgstr "" + +#: utils/models.py:22 +msgid "Netherlands Antilles" +msgstr "" + +#: utils/models.py:23 +msgid "Angola" +msgstr "" + +#: utils/models.py:24 +msgid "Antarctica" +msgstr "" + +#: utils/models.py:25 +msgid "Argentina" +msgstr "" + +#: utils/models.py:26 +msgid "American Samoa" +msgstr "" + +#: utils/models.py:27 +msgid "Austria" +msgstr "" + +#: utils/models.py:28 +msgid "Australia" +msgstr "" + +#: utils/models.py:29 +msgid "Aruba" +msgstr "" + +#: utils/models.py:30 +msgid "Azerbaijan" +msgstr "" + +#: utils/models.py:31 +msgid "Bosnia and Herzegovina" +msgstr "" + +#: utils/models.py:32 +msgid "Barbados" +msgstr "" + +#: utils/models.py:33 +msgid "Bangladesh" +msgstr "" + +#: utils/models.py:34 +msgid "Belgium" +msgstr "" + +#: utils/models.py:35 +msgid "Burkina Faso" +msgstr "" + +#: utils/models.py:36 +msgid "Bulgaria" +msgstr "" + +#: utils/models.py:37 +msgid "Bahrain" +msgstr "" + +#: utils/models.py:38 +msgid "Burundi" +msgstr "" + +#: utils/models.py:39 +msgid "Benin" +msgstr "" + +#: utils/models.py:40 +msgid "Bermuda" +msgstr "" + +#: utils/models.py:41 +msgid "Brunei Darussalam" +msgstr "" + +#: utils/models.py:42 +msgid "Bolivia" +msgstr "" + +#: utils/models.py:43 +msgid "Brazil" +msgstr "" + +#: utils/models.py:44 +msgid "Bahama" +msgstr "" + +#: utils/models.py:45 +msgid "Bhutan" +msgstr "" + +#: utils/models.py:46 +msgid "Bouvet Island" +msgstr "" + +#: utils/models.py:47 +msgid "Botswana" +msgstr "" + +#: utils/models.py:48 +msgid "Belarus" +msgstr "" + +#: utils/models.py:49 +msgid "Belize" +msgstr "" + +#: utils/models.py:50 +msgid "Canada" +msgstr "" + +#: utils/models.py:51 +msgid "Cocos (Keeling) Islands" +msgstr "" + +#: utils/models.py:52 +msgid "Central African Republic" +msgstr "" + +#: utils/models.py:53 +msgid "Congo" +msgstr "" + +#: utils/models.py:54 +msgid "Switzerland" +msgstr "" + +#: utils/models.py:55 +msgid "Ivory Coast" +msgstr "" + +#: utils/models.py:56 +msgid "Cook Iislands" +msgstr "" + +#: utils/models.py:57 +msgid "Chile" +msgstr "" + +#: utils/models.py:58 +msgid "Cameroon" +msgstr "" + +#: utils/models.py:59 +msgid "China" +msgstr "" + +#: utils/models.py:60 +msgid "Colombia" +msgstr "" + +#: utils/models.py:61 +msgid "Costa Rica" +msgstr "" + +#: utils/models.py:62 +msgid "Cuba" +msgstr "" + +#: utils/models.py:63 +msgid "Cape Verde" +msgstr "" + +#: utils/models.py:64 +msgid "Christmas Island" +msgstr "" + +#: utils/models.py:65 +msgid "Cyprus" +msgstr "" + +#: utils/models.py:66 +msgid "Czech Republic" +msgstr "" + +#: utils/models.py:67 +msgid "Germany" +msgstr "" + +#: utils/models.py:68 +msgid "Djibouti" +msgstr "" + +#: utils/models.py:69 +msgid "Denmark" +msgstr "" + +#: utils/models.py:70 +msgid "Dominica" +msgstr "" + +#: utils/models.py:71 +msgid "Dominican Republic" +msgstr "" + +#: utils/models.py:72 +msgid "Algeria" +msgstr "" + +#: utils/models.py:73 +msgid "Ecuador" +msgstr "" + +#: utils/models.py:74 +msgid "Estonia" +msgstr "" + +#: utils/models.py:75 +msgid "Egypt" +msgstr "" + +#: utils/models.py:76 +msgid "Western Sahara" +msgstr "" + +#: utils/models.py:77 +msgid "Eritrea" +msgstr "" + +#: utils/models.py:78 +msgid "Spain" +msgstr "" + +#: utils/models.py:79 +msgid "Ethiopia" +msgstr "" + +#: utils/models.py:80 +msgid "Finland" +msgstr "" + +#: utils/models.py:81 +msgid "Fiji" +msgstr "" + +#: utils/models.py:82 +msgid "Falkland Islands (Malvinas)" +msgstr "" + +#: utils/models.py:83 +msgid "Micronesia" +msgstr "" + +#: utils/models.py:84 +msgid "Faroe Islands" +msgstr "" + +#: utils/models.py:85 +msgid "France" +msgstr "" + +#: utils/models.py:86 +msgid "France, Metropolitan" +msgstr "" + +#: utils/models.py:87 +msgid "Gabon" +msgstr "" + +#: utils/models.py:88 +msgid "United Kingdom (Great Britain)" +msgstr "" + +#: utils/models.py:89 +msgid "Grenada" +msgstr "" + +#: utils/models.py:90 +msgid "Georgia" +msgstr "" + +#: utils/models.py:91 +msgid "French Guiana" +msgstr "" + +#: utils/models.py:92 +msgid "Ghana" +msgstr "" + +#: utils/models.py:93 +msgid "Gibraltar" +msgstr "" + +#: utils/models.py:94 +msgid "Greenland" +msgstr "" + +#: utils/models.py:95 +msgid "Gambia" +msgstr "" + +#: utils/models.py:96 +msgid "Guinea" +msgstr "" + +#: utils/models.py:97 +msgid "Guadeloupe" +msgstr "" + +#: utils/models.py:98 +msgid "Equatorial Guinea" +msgstr "" + +#: utils/models.py:99 +msgid "Greece" +msgstr "" + +#: utils/models.py:100 +msgid "South Georgia and the South Sandwich Islands" +msgstr "" + +#: utils/models.py:101 +msgid "Guatemala" +msgstr "" + +#: utils/models.py:102 +msgid "Guam" +msgstr "" + +#: utils/models.py:103 +msgid "Guinea-Bissau" +msgstr "" + +#: utils/models.py:104 +msgid "Guyana" +msgstr "" + +#: utils/models.py:105 +msgid "Hong Kong" +msgstr "" + +#: utils/models.py:106 +msgid "Heard & McDonald Islands" +msgstr "" + +#: utils/models.py:107 +msgid "Honduras" +msgstr "" + +#: utils/models.py:108 +msgid "Croatia" +msgstr "" + +#: utils/models.py:109 +msgid "Haiti" +msgstr "" + +#: utils/models.py:110 +msgid "Hungary" +msgstr "" + +#: utils/models.py:111 +msgid "Indonesia" +msgstr "" + +#: utils/models.py:112 +msgid "Ireland" +msgstr "" + +#: utils/models.py:113 +msgid "Israel" +msgstr "" + +#: utils/models.py:114 +msgid "India" +msgstr "" + +#: utils/models.py:115 +msgid "British Indian Ocean Territory" +msgstr "" + +#: utils/models.py:116 +msgid "Iraq" +msgstr "" + +#: utils/models.py:117 +msgid "Islamic Republic of Iran" +msgstr "" + +#: utils/models.py:118 +msgid "Iceland" +msgstr "" + +#: utils/models.py:119 +msgid "Italy" +msgstr "" + +#: utils/models.py:120 +msgid "Jamaica" +msgstr "" + +#: utils/models.py:121 +msgid "Jordan" +msgstr "" + +#: utils/models.py:122 +msgid "Japan" +msgstr "" + +#: utils/models.py:123 +msgid "Kenya" +msgstr "" + +#: utils/models.py:124 +msgid "Kyrgyzstan" +msgstr "" + +#: utils/models.py:125 +msgid "Cambodia" +msgstr "" + +#: utils/models.py:126 +msgid "Kiribati" +msgstr "" + +#: utils/models.py:127 +msgid "Comoros" +msgstr "" + +#: utils/models.py:128 +msgid "St. Kitts and Nevis" +msgstr "" + +#: utils/models.py:129 +msgid "Korea, Democratic People's Republic of" +msgstr "" + +#: utils/models.py:130 +msgid "Korea, Republic of" +msgstr "" + +#: utils/models.py:131 +msgid "Kuwait" +msgstr "" + +#: utils/models.py:132 +msgid "Cayman Islands" +msgstr "" + +#: utils/models.py:133 +msgid "Kazakhstan" +msgstr "" + +#: utils/models.py:134 +msgid "Lao People's Democratic Republic" +msgstr "" + +#: utils/models.py:135 +msgid "Lebanon" +msgstr "" + +#: utils/models.py:136 +msgid "Saint Lucia" +msgstr "" + +#: utils/models.py:137 +msgid "Liechtenstein" +msgstr "" + +#: utils/models.py:138 +msgid "Sri Lanka" +msgstr "" + +#: utils/models.py:139 +msgid "Liberia" +msgstr "" + +#: utils/models.py:140 +msgid "Lesotho" +msgstr "" + +#: utils/models.py:141 +msgid "Lithuania" +msgstr "" + +#: utils/models.py:142 +msgid "Luxembourg" +msgstr "" + +#: utils/models.py:143 +msgid "Latvia" +msgstr "" + +#: utils/models.py:144 +msgid "Libyan Arab Jamahiriya" +msgstr "" + +#: utils/models.py:145 +msgid "Morocco" +msgstr "" + +#: utils/models.py:146 +msgid "Monaco" +msgstr "" + +#: utils/models.py:147 +msgid "Moldova, Republic of" +msgstr "" + +#: utils/models.py:148 +msgid "Madagascar" +msgstr "" + +#: utils/models.py:149 +msgid "Marshall Islands" +msgstr "" + +#: utils/models.py:150 +msgid "Mali" +msgstr "" + +#: utils/models.py:151 +msgid "Mongolia" +msgstr "" + +#: utils/models.py:152 +msgid "Myanmar" +msgstr "" + +#: utils/models.py:153 +msgid "Macau" +msgstr "" + +#: utils/models.py:154 +msgid "Northern Mariana Islands" +msgstr "" + +#: utils/models.py:155 +msgid "Martinique" +msgstr "" + +#: utils/models.py:156 +msgid "Mauritania" +msgstr "" + +#: utils/models.py:157 +msgid "Monserrat" +msgstr "" + +#: utils/models.py:158 +msgid "Malta" +msgstr "" + +#: utils/models.py:159 +msgid "Mauritius" +msgstr "" + +#: utils/models.py:160 +msgid "Maldives" +msgstr "" + +#: utils/models.py:161 +msgid "Malawi" +msgstr "" + +#: utils/models.py:162 +msgid "Mexico" +msgstr "" + +#: utils/models.py:163 +msgid "Malaysia" +msgstr "" + +#: utils/models.py:164 +msgid "Mozambique" +msgstr "" + +#: utils/models.py:165 +msgid "Namibia" +msgstr "" + +#: utils/models.py:166 +msgid "New Caledonia" +msgstr "" + +#: utils/models.py:167 +msgid "Niger" +msgstr "" + +#: utils/models.py:168 +msgid "Norfolk Island" +msgstr "" + +#: utils/models.py:169 +msgid "Nigeria" +msgstr "" + +#: utils/models.py:170 +msgid "Nicaragua" +msgstr "" + +#: utils/models.py:171 +msgid "Netherlands" +msgstr "" + +#: utils/models.py:172 +msgid "Norway" +msgstr "" + +#: utils/models.py:173 +msgid "Nepal" +msgstr "" + +#: utils/models.py:174 +msgid "Nauru" +msgstr "" + +#: utils/models.py:175 +msgid "Niue" +msgstr "" + +#: utils/models.py:176 +msgid "New Zealand" +msgstr "" + +#: utils/models.py:177 +msgid "Oman" +msgstr "" + +#: utils/models.py:178 +msgid "Panama" +msgstr "" + +#: utils/models.py:179 +msgid "Peru" +msgstr "" + +#: utils/models.py:180 +msgid "French Polynesia" +msgstr "" + +#: utils/models.py:181 +msgid "Papua New Guinea" +msgstr "" + +#: utils/models.py:182 +msgid "Philippines" +msgstr "" + +#: utils/models.py:183 +msgid "Pakistan" +msgstr "" + +#: utils/models.py:184 +msgid "Poland" +msgstr "" + +#: utils/models.py:185 +msgid "St. Pierre & Miquelon" +msgstr "" + +#: utils/models.py:186 +msgid "Pitcairn" +msgstr "" + +#: utils/models.py:187 +msgid "Puerto Rico" +msgstr "" + +#: utils/models.py:188 +msgid "Portugal" +msgstr "" + +#: utils/models.py:189 +msgid "Palau" +msgstr "" + +#: utils/models.py:190 +msgid "Paraguay" +msgstr "" + +#: utils/models.py:191 +msgid "Qatar" +msgstr "" + +#: utils/models.py:192 +msgid "Reunion" +msgstr "" + +#: utils/models.py:193 +msgid "Romania" +msgstr "" + +#: utils/models.py:194 +msgid "Russian Federation" +msgstr "" + +#: utils/models.py:195 +msgid "Rwanda" +msgstr "" + +#: utils/models.py:196 +msgid "Saudi Arabia" +msgstr "" + +#: utils/models.py:197 +msgid "Solomon Islands" +msgstr "" + +#: utils/models.py:198 +msgid "Seychelles" +msgstr "" + +#: utils/models.py:199 +msgid "Sudan" +msgstr "" + +#: utils/models.py:200 +msgid "Sweden" +msgstr "" + +#: utils/models.py:201 +msgid "Singapore" +msgstr "" + +#: utils/models.py:202 +msgid "St. Helena" +msgstr "" + +#: utils/models.py:203 +msgid "Slovenia" +msgstr "" + +#: utils/models.py:204 +msgid "Svalbard & Jan Mayen Islands" +msgstr "" + +#: utils/models.py:205 +msgid "Slovakia" +msgstr "" + +#: utils/models.py:206 +msgid "Sierra Leone" +msgstr "" + +#: utils/models.py:207 +msgid "San Marino" +msgstr "" + +#: utils/models.py:208 +msgid "Senegal" +msgstr "" + +#: utils/models.py:209 +msgid "Somalia" +msgstr "" + +#: utils/models.py:210 +msgid "Suriname" +msgstr "" + +#: utils/models.py:211 +msgid "Sao Tome & Principe" +msgstr "" + +#: utils/models.py:212 +msgid "El Salvador" +msgstr "" + +#: utils/models.py:213 +msgid "Syrian Arab Republic" +msgstr "" + +#: utils/models.py:214 +msgid "Swaziland" +msgstr "" + +#: utils/models.py:215 +msgid "Turks & Caicos Islands" +msgstr "" + +#: utils/models.py:216 +msgid "Chad" +msgstr "" + +#: utils/models.py:217 +msgid "French Southern Territories" +msgstr "" + +#: utils/models.py:218 +msgid "Togo" +msgstr "" + +#: utils/models.py:219 +msgid "Thailand" +msgstr "" + +#: utils/models.py:220 +msgid "Tajikistan" +msgstr "" + +#: utils/models.py:221 +msgid "Tokelau" +msgstr "" + +#: utils/models.py:222 +msgid "Turkmenistan" +msgstr "" + +#: utils/models.py:223 +msgid "Tunisia" +msgstr "" + +#: utils/models.py:224 +msgid "Tonga" +msgstr "" + +#: utils/models.py:225 +msgid "East Timor" +msgstr "" + +#: utils/models.py:226 +msgid "Turkey" +msgstr "" + +#: utils/models.py:227 +msgid "Trinidad & Tobago" +msgstr "" + +#: utils/models.py:228 +msgid "Tuvalu" +msgstr "" + +#: utils/models.py:229 +msgid "Taiwan, Province of China" +msgstr "" + +#: utils/models.py:230 +msgid "Tanzania, United Republic of" +msgstr "" + +#: utils/models.py:231 +msgid "Ukraine" +msgstr "" + +#: utils/models.py:232 +msgid "Uganda" +msgstr "" + +#: utils/models.py:233 +msgid "United States Minor Outlying Islands" +msgstr "" + +#: utils/models.py:234 +msgid "United States of America" +msgstr "" + +#: utils/models.py:235 +msgid "Uruguay" +msgstr "" + +#: utils/models.py:236 +msgid "Uzbekistan" +msgstr "" + +#: utils/models.py:237 +msgid "Vatican City State (Holy See)" +msgstr "" + +#: utils/models.py:238 +msgid "St. Vincent & the Grenadines" +msgstr "" + +#: utils/models.py:239 +msgid "Venezuela" +msgstr "" + +#: utils/models.py:240 +msgid "British Virgin Islands" +msgstr "" + +#: utils/models.py:241 +msgid "United States Virgin Islands" +msgstr "" + +#: utils/models.py:242 +msgid "Viet Nam" +msgstr "" + +#: utils/models.py:243 +msgid "Vanuatu" +msgstr "" + +#: utils/models.py:244 +msgid "Wallis & Futuna Islands" +msgstr "" + +#: utils/models.py:245 +msgid "Samoa" +msgstr "" + +#: utils/models.py:246 +msgid "Yemen" +msgstr "" + +#: utils/models.py:247 +msgid "Mayotte" +msgstr "" + +#: utils/models.py:248 +msgid "Yugoslavia" +msgstr "" + +#: utils/models.py:249 +msgid "South Africa" +msgstr "" + +#: utils/models.py:250 +msgid "Zambia" +msgstr "" + +#: utils/models.py:251 +msgid "Zaire" +msgstr "" + +#: utils/models.py:252 +msgid "Zimbabwe" +msgstr "" + +#: utils/models.py:253 +msgid "Unknown or unspecified country" +msgstr "" + +#: utils/models.py:257 +msgid "🇩🇪 Deutsch" +msgstr "" + +#: utils/models.py:258 +msgid "🇬🇧 English" +msgstr "" + +#: utils/models.py:366 +msgid "start date must be set" +msgstr "" + +#: utils/models.py:369 +msgid "Start date must be prior to end date" +msgstr "" + +#: utils/models.py:399 +msgid "Must be a positive number." +msgstr "" + +#: utils/user_utils.py:42 +msgid "NO NAME AVAILABLE" +msgstr "" + +#: welcomedesk/apps.py:17 welcomedesk/apps.py:20 +#: welcomedesk/templates/welcomedesk/welcome_desk_search.html:14 +msgid "Welcome Desk" +msgstr "" + +#: welcomedesk/services/welcome_desk_reasons_cannot_shop_service.py:44 +#, python-format +msgid "%(name)s does not have a Tapir account. Contact a member of the management team." +msgstr "" + +#: welcomedesk/services/welcome_desk_reasons_cannot_shop_service.py:47 +#, python-format +msgid "%(name)s is an investing member. If they want to shop, they have to become an active member. Contact a member of the management team." +msgstr "" + +#: welcomedesk/services/welcome_desk_reasons_cannot_shop_service.py:51 +#, python-format +msgid "%(name)s has been frozen because they missed too many shifts.If they want to shop, they must first be re-activated.Contact a member of the management team." +msgstr "" + +#: welcomedesk/services/welcome_desk_reasons_cannot_shop_service.py:56 +#, python-format +msgid "%(name)s has paused their membership. Contact a member of the management team." +msgstr "" + +#: welcomedesk/services/welcome_desk_reasons_cannot_shop_service.py:59 +#, python-format +msgid "%(name)s has is not a member of the cooperative. They may have transferred their shares to another member. Contact a member of the management team." +msgstr "" + +#: welcomedesk/services/welcome_desk_warnings_service.py:34 +#, python-format +msgid "%(name)s has not attended a welcome session yet. Make sure they plan to do it!" +msgstr "" From b2408360193df856ca98895d0b93130e43f4d789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Wed, 11 Dec 2024 16:54:23 +0100 Subject: [PATCH 29/50] Added tests for NumberOfFrozenMembersAtDateView --- .../test_number_of_frozen_members_view.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 tapir/statistics/tests/fancy_graph/test_number_of_frozen_members_view.py diff --git a/tapir/statistics/tests/fancy_graph/test_number_of_frozen_members_view.py b/tapir/statistics/tests/fancy_graph/test_number_of_frozen_members_view.py new file mode 100644 index 00000000..919ab332 --- /dev/null +++ b/tapir/statistics/tests/fancy_graph/test_number_of_frozen_members_view.py @@ -0,0 +1,62 @@ +import datetime + +from django.utils import timezone + +from tapir.accounts.tests.factories.factories import TapirUserFactory +from tapir.shifts.models import ShiftUserData +from tapir.statistics.views.fancy_graph.number_of_frozen_members_view import ( + NumberOfFrozenMembersAtDateView, +) +from tapir.utils.tests_utils import ( + TapirFactoryTestBase, + mock_timezone_now, +) + + +class TestNumberOfFrozenMembersView(TapirFactoryTestBase): + NOW = datetime.datetime(year=2023, month=4, day=1, hour=12) + REFERENCE_TIME = timezone.make_aware( + datetime.datetime(year=2022, month=6, day=15, hour=12) + ) + + def setUp(self) -> None: + super().setUp() + self.NOW = mock_timezone_now(self, self.NOW) + + def test_calculateDatapoint_memberIsFrozenButIsNoActive_notCounted(self): + TapirUserFactory.create( + date_joined=self.REFERENCE_TIME + datetime.timedelta(days=1) + ) + ShiftUserData.objects.update(is_frozen=True) + + result = NumberOfFrozenMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(0, result) + + def test_calculateDatapoint_memberIsActiveButIsNotFrozen_notCounted(self): + TapirUserFactory.create( + date_joined=self.REFERENCE_TIME - datetime.timedelta(days=1), + share_owner__is_investing=False, + ) + ShiftUserData.objects.update(is_frozen=False) + + result = NumberOfFrozenMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(0, result) + + def test_calculateDatapoint_memberIsActiveAndFrozen_counted(self): + TapirUserFactory.create( + date_joined=self.REFERENCE_TIME - datetime.timedelta(days=1), + share_owner__is_investing=False, + ) + ShiftUserData.objects.update(is_frozen=True) + + result = NumberOfFrozenMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(1, result) From 9c2976cf1117b6fbfeac2676ea0a4a7e7364c487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Wed, 11 Dec 2024 16:55:44 +0100 Subject: [PATCH 30/50] Translation file update --- .../locale/d/LC_MESSAGES/django.po | 5039 ----------------- .../locale/de/LC_MESSAGES/django.po | 126 +- 2 files changed, 63 insertions(+), 5102 deletions(-) delete mode 100644 tapir/translations/locale/d/LC_MESSAGES/django.po diff --git a/tapir/translations/locale/d/LC_MESSAGES/django.po b/tapir/translations/locale/d/LC_MESSAGES/django.po deleted file mode 100644 index c316b0b2..00000000 --- a/tapir/translations/locale/d/LC_MESSAGES/django.po +++ /dev/null @@ -1,5039 +0,0 @@ -# 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 , YEAR. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-11 16:44+0100\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: accounts/emails/create_account_reminder_email.py:20 -msgid "Create account reminder" -msgstr "" - -#: accounts/emails/create_account_reminder_email.py:25 -msgid "Sent to active member if they haven't created the account 1 month after becoming member." -msgstr "" - -#: accounts/forms.py:19 -msgid "Additional Emails" -msgstr "" - -#: accounts/forms.py:25 -msgid "Mandatory Emails" -msgstr "" - -#: accounts/forms.py:119 -msgid "This username is not available." -msgstr "" - -#: accounts/models.py:66 -msgid "Displayed name" -msgstr "" - -#: accounts/models.py:71 coop/models.py:70 coop/models.py:476 -msgid "Pronouns" -msgstr "" - -#: accounts/models.py:72 accounts/templates/accounts/user_detail.html:67 -#: coop/models.py:72 coop/models.py:478 -#: coop/templates/coop/draftuser_detail.html:71 -#: coop/templates/coop/draftuser_detail.html:142 -#: coop/templates/coop/shareowner_detail.html:51 -msgid "Phone number" -msgstr "" - -#: accounts/models.py:73 accounts/templates/accounts/user_detail.html:77 -#: coop/models.py:73 coop/models.py:479 -#: coop/templates/coop/draftuser_detail.html:146 -#: coop/templates/coop/shareowner_detail.html:55 -msgid "Birthdate" -msgstr "" - -#: accounts/models.py:74 coop/models.py:74 coop/models.py:480 -msgid "Street and house number" -msgstr "" - -#: accounts/models.py:75 coop/models.py:75 coop/models.py:481 -msgid "Extra address line" -msgstr "" - -#: accounts/models.py:76 coop/models.py:76 coop/models.py:482 -msgid "Postcode" -msgstr "" - -#: accounts/models.py:77 coop/models.py:77 coop/models.py:483 -msgid "City" -msgstr "" - -#: accounts/models.py:78 coop/models.py:78 coop/models.py:484 -msgid "Country" -msgstr "" - -#: accounts/models.py:79 accounts/templates/accounts/user_detail.html:111 -msgid "Co-Purchaser" -msgstr "" - -#: accounts/models.py:81 -msgid "Allow purchase tracking" -msgstr "" - -#: accounts/models.py:95 accounts/templates/accounts/user_detail.html:97 -#: coop/models.py:81 coop/models.py:487 -#: coop/templates/coop/shareowner_detail.html:75 -msgid "Preferred Language" -msgstr "" - -#: accounts/models.py:128 accounts/models.py:134 -#: coop/views/membership_pause.py:74 shifts/views/exemptions.py:214 -msgid "None" -msgstr "" - -#: accounts/templates/accounts/edit_username.default.html:13 -#, python-format -msgid "" -"\n" -" Edit username: %(display_name_full)s\n" -" " -msgstr "" - -#: accounts/templates/accounts/edit_username.default.html:22 -#, python-format -msgid "" -"\n" -" Edit username: %(display_name_full)s\n" -" " -msgstr "" - -#: accounts/templates/accounts/edit_username.default.html:28 -#, python-format -msgid "" -"\n" -"

Your old username is %(old_username)s.

\n" -"

If you ever updated the wiki, please contact the Wiki team on Slack #wiki to keep your\n" -" member's page there and the history of your changes.

\n" -" " -msgstr "" - -#: accounts/templates/accounts/edit_username.default.html:41 -#: accounts/templates/registration/password_update.html:16 -#: coop/templates/coop/draftuser_register_form.default.html:42 -#: coop/templates/coop/draftuser_register_form.html:45 -#: coop/templates/coop/membership_resignation_form.html:65 -#: core/templates/core/tapir_form.default.html:30 -#: core/templates/core/tapir_form.html:30 -#: shifts/templates/shifts/convert_exemption_to_pause_form.html:37 -#: shifts/templates/shifts/shift_form.html:35 -#: shifts/templates/shifts/shiftslot_form.html:32 -msgid "Save" -msgstr "" - -#: accounts/templates/accounts/email/create_account_reminder.body.html:6 -#, python-format -msgid "" -"\n" -"

Hi %(display_name_short)s,

\n" -"

We've noticed that you've been a SuperCoop member for a month/week but haven't set up a Tapir account to sign up for your first shift.

\n" -"

\n" -" As a reminder, to sign up for your first shift (or indicate that you are eligible for an\n" -" exemption),\n" -" please visit the Member Office in our shop.\n" -" There, an account will be set up for you on Tapir, our online shift management system, \n" -" and you will be guided through the steps to select your shift and working group.\n" -"

\n" -"

\n" -" For general questions about your membership, please contact the Member Office by email (mitglied@supercoop.de)\n" -" or visit SuperCoop.\n" -" Feel free to drop by and meet other SuperCoopies in person! Opening hours are::\n" -"

    \n" -"
  • Monday - Friday from 16:30 to 19:30
  • \n" -"
  • Saturdays from 11:00 to 14:00
  • \n" -"
\n" -"

\n" -"

\n" -" How do you find SuperCoop?\n" -"

\n" -"

\n" -" You can find our shop at Oudenarder Straße 16, 13347 Berlin, on the corner of Seestraße. \n" -" There is also a back entrance to the shop, which you can reach via the former Osramhöfe. This entrance is also barrier-free.\n" -"

\n" -"

\n" -" Cooperative Greetings,
\n" -" The Member Office\n" -"

\n" -" " -msgstr "" - -#: accounts/templates/accounts/email/create_account_reminder.subject.html:2 -msgid "" -"\n" -" Don't forget to sign up for your first shift at SuperCoop!\n" -msgstr "" - -#: accounts/templates/accounts/ldap_group_list.html:7 -#: accounts/templates/accounts/user_detail.html:102 -msgid "Groups" -msgstr "" - -#: accounts/templates/accounts/purchase_tracking_card.html:5 -msgid "Purchase tracking" -msgstr "" - -#: accounts/templates/accounts/purchase_tracking_card.html:11 -msgid "Get barcode as PDF" -msgstr "" - -#: accounts/templates/accounts/purchase_tracking_card.html:17 -msgid "More information" -msgstr "" - -#: accounts/templates/accounts/purchase_tracking_card.html:25 -msgid "Das Kassensystem verknüpft deinen Einkauf mit deinem Mitgliedskonto. Dabei wird jedesmal der Gesamtbetrag deines Einkaufs gespeichert. Nicht erfasst wird dagegen, welche konkreten Produkte du gekauft hast. Auch kannst du bei jedem Einkauf immer noch entscheiden, ob du deine Mitgliedskarte scannen lassen möchtest oder nicht. Mit deiner generellen Zustimmung hier auf Tapir gehst du keine Verpflichtung zum Scannen ein. Die Zustimmung kannst du jederzeit widerrufen, indem du die Checkbox oben deaktivierst. Du hilfst damit Supercoop, die Einkaufsgewohnheiten der Mitglieder besser zu verstehen. Das ist wichtig für die Weiterentwicklung unseres Supermarktes. Mehr Informationen: https://wiki.supercoop.de/wiki/Mitgliederkarte" -msgstr "" - -#: accounts/templates/accounts/purchase_tracking_card.html:29 -#: coop/templates/coop/draftuser_detail.html:235 -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:168 -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:172 -msgid "Yes,No" -msgstr "" - -#: accounts/templates/accounts/purchase_tracking_card.html:35 -msgid "Disable" -msgstr "" - -#: accounts/templates/accounts/purchase_tracking_card.html:40 -msgid "Enable" -msgstr "" - -#: accounts/templates/accounts/user_detail.html:20 coop/models.py:753 -#: coop/templates/coop/draftuser_detail.html:86 -#: coop/templates/coop/shareowner_detail.html:10 log/views.py:94 -#: log/views.py:152 shifts/templates/shifts/shift_day_printable.html:55 -#: shifts/templates/shifts/shift_detail_printable.html:50 -msgid "Member" -msgstr "" - -#: accounts/templates/accounts/user_detail.html:27 -#: coop/templates/coop/shareowner_detail.html:17 -msgid "Personal Data" -msgstr "" - -#: accounts/templates/accounts/user_detail.html:32 -msgid "Edit groups" -msgstr "" - -#: accounts/templates/accounts/user_detail.html:37 -msgid "Edit username" -msgstr "" - -#: accounts/templates/accounts/user_detail.html:43 -#: coop/templates/coop/draftuser_detail.html:242 -#: coop/templates/coop/membershipresignation_detail.html:107 -#: coop/templates/coop/shareowner_detail.html:30 -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:22 -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:90 -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:113 -#: core/templates/core/featureflag_list.html:30 -#: shifts/templates/shifts/shift_detail.html:44 -#: shifts/templates/shifts/shift_template_detail.html:25 -#: shifts/templates/shifts/shift_template_detail.html:113 -#: shifts/templates/shifts/user_shifts_overview_tag.html:14 -msgid "Edit" -msgstr "" - -#: accounts/templates/accounts/user_detail.html:48 -msgid "Edit name and pronouns" -msgstr "" - -#: accounts/templates/accounts/user_detail.html:55 -#: coop/templates/coop/draftuser_detail.html:70 -#: coop/templates/coop/draftuser_detail.html:130 -#: coop/templates/coop/shareowner_detail.html:43 coop/views/draftuser.py:271 -#: core/templates/core/email_list.html:38 -#: core/templates/core/featureflag_list.html:18 -#: financingcampaign/templates/financingcampaign/general.html:25 -#: financingcampaign/templates/financingcampaign/general.html:70 -msgid "Name" -msgstr "" - -#: accounts/templates/accounts/user_detail.html:59 -#: coop/templates/coop/draftuser_detail.html:134 -msgid "Username" -msgstr "" - -#: accounts/templates/accounts/user_detail.html:63 -#: coop/templates/coop/draftuser_detail.html:73 -#: coop/templates/coop/draftuser_detail.html:138 -#: coop/templates/coop/shareowner_detail.html:47 -msgid "Email" -msgstr "" - -#: accounts/templates/accounts/user_detail.html:72 -#: accounts/templates/accounts/user_detail.html:82 -#: accounts/templates/accounts/user_detail.html:92 -#: coop/templates/coop/draftuser_detail.html:161 -#: coop/templates/coop/shareowner_detail.html:60 -#: coop/templates/coop/shareowner_detail.html:70 -#: shifts/templates/shifts/user_shifts_overview_tag.html:40 -#: statistics/templates/statistics/main_statistics.html:76 -#: statistics/templates/statistics/main_statistics.html:108 -msgid "Missing" -msgstr "" - -#: accounts/templates/accounts/user_detail.html:87 -#: coop/templates/coop/draftuser_detail.html:72 -#: coop/templates/coop/draftuser_detail.html:156 -#: coop/templates/coop/shareowner_detail.html:65 -msgid "Address" -msgstr "" - -#: accounts/templates/accounts/user_detail.html:106 -msgid "Permissions" -msgstr "" - -#: accounts/templates/accounts/user_detail.html:127 -msgid "Resend account activation email" -msgstr "" - -#: accounts/templates/accounts/user_detail.html:135 -msgid "Change Password" -msgstr "" - -#: accounts/templates/registration/email/password_reset_email.html:7 -msgid "Password reset" -msgstr "" - -#: accounts/templates/registration/email/password_reset_email.html:14 -#, python-format -msgid "" -"\n" -"

Hi %(display_name_short)s,

\n" -"

\n" -" Someone asked for password reset for %(email)s.
\n" -" Your username is %(username)s
\n" -" Follow this link to reset your password: %(password_reset_url_full)s\n" -" If the link doesn't work, try to open it in a private browser window (sometimes called \"incognito mode\"), or in another browser.\n" -"

\n" -" " -msgstr "" - -#: accounts/templates/registration/email/password_reset_subject.html:2 -msgid "" -"\n" -" Reset your password\n" -msgstr "" - -#: accounts/templates/registration/login.html:7 -msgid "Login" -msgstr "" - -#: accounts/templates/registration/login.html:16 -#: accounts/templates/registration/login.html:59 -msgid "Sign in" -msgstr "" - -#: accounts/templates/registration/login.html:45 -msgid "Click here to show/hide the password" -msgstr "" - -#: accounts/templates/registration/login.html:55 -msgid "Forgot your password or your username?" -msgstr "" - -#: accounts/templates/registration/password_reset_complete.html:7 -msgid "Password set" -msgstr "" - -#: accounts/templates/registration/password_reset_complete.html:9 -msgid "Your password has been set. You may go ahead and sign in now." -msgstr "" - -#: accounts/templates/registration/password_reset_complete.html:11 -msgid "Go to login page" -msgstr "" - -#: accounts/templates/registration/password_reset_confirm.html:9 -#: accounts/templates/registration/password_update.html:8 -msgid "Set a new password" -msgstr "" - -#: accounts/templates/registration/password_reset_confirm.html:11 -msgid "Please enter a new password" -msgstr "" - -#: accounts/templates/registration/password_reset_confirm.html:15 -msgid "Change password" -msgstr "" - -#: accounts/templates/registration/password_reset_confirm.html:21 -msgid "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." -msgstr "" - -#: accounts/templates/registration/password_reset_done.html:7 -msgid "Password reset instructions have been sent." -msgstr "" - -#: accounts/templates/registration/password_reset_done.html:9 -msgid "We have emailed you instructions for setting your password, if an account exists with the email you entered. You should receive them shortly. If you don't receive an email, please make sure you've entered the address you registered with, and check your spam folder." -msgstr "" - -#: accounts/templates/registration/password_reset_form.html:10 -msgid "Problems with logging in?" -msgstr "" - -#: accounts/templates/registration/password_reset_form.html:16 -#, python-format -msgid "" -"\n" -" Please enter your email address, we will send you instructions to reset your password.
\n" -" Your username will be included in the email, in case you forgot it.
\n" -" If you forgot both your email address and your username, email the member office : %(contact_member_office)s, specifying your full name and, if you know it, your member's number.\n" -" " -msgstr "" - -#: accounts/templates/registration/password_reset_form.html:24 -msgid "Back to login form" -msgstr "" - -#: accounts/templates/registration/password_reset_form.html:26 -msgid "Send me instructions!" -msgstr "" - -#: accounts/validators.py:11 -msgid "Enter a valid username. This value may contain only letters, numbers, and ./-/_ characters." -msgstr "" - -#: accounts/views.py:99 accounts/views.py:104 coop/views/shareowner.py:261 -#: coop/views/shareowner.py:266 -#, python-format -msgid "Edit member: %(name)s" -msgstr "" - -#: accounts/views.py:142 -msgid "Account welcome email sent." -msgstr "" - -#: accounts/views.py:187 -msgid "You can only look at your own barcode unless you have member office rights" -msgstr "" - -#: accounts/views.py:246 accounts/views.py:251 -#, python-format -msgid "Edit member groups: %(name)s" -msgstr "" - -#: coop/apps.py:22 coop/apps.py:33 coop/templates/coop/shareowner_list.html:10 -msgid "Members" -msgstr "" - -#: coop/apps.py:25 coop/templates/coop/draftuser_list.html:9 -#: coop/templates/coop/draftuser_list.html:18 -#: coop/templates/coop/draftuser_register_form.default.html:11 -#: coop/templates/coop/draftuser_register_form.html:11 -msgid "Applicants" -msgstr "" - -#: coop/apps.py:41 coop/templates/coop/member_management.html:7 -msgid "Member management" -msgstr "" - -#: coop/apps.py:48 core/apps.py:17 financingcampaign/apps.py:14 log/apps.py:17 -msgid "Management" -msgstr "" - -#: coop/apps.py:51 coop/templates/coop/incoming_payment_list.html:19 -msgid "Incoming payments" -msgstr "" - -#: coop/emails/co_purchaser_updated_mail.py:22 -#: coop/templates/coop/email/co_purchaser_updated.subject.default.html:2 -msgid "Co-purchaser updated" -msgstr "" - -#: coop/emails/co_purchaser_updated_mail.py:27 -msgid "Sent to a member when their new co-purchaser gets registered on their profile." -msgstr "" - -#: coop/emails/extra_shares_confirmation_email.py:26 -msgid "Extra shares bought" -msgstr "" - -#: coop/emails/extra_shares_confirmation_email.py:30 -msgid "Sent when someone who is already a member buys more shares" -msgstr "" - -#: coop/emails/membership_confirmation_email_for_active_member.py:26 -msgid "Membership confirmation for active users" -msgstr "" - -#: coop/emails/membership_confirmation_email_for_investing_member.py:26 -msgid "Membership confirmation for investing users" -msgstr "" - -#: coop/emails/membershipresignation_confirmation_email.py:22 -msgid "Confirmation Email for resigned members." -msgstr "" - -#: coop/emails/membershipresignation_confirmation_email.py:26 -msgid "Automatically sent after a member has been resigned." -msgstr "" - -#: coop/emails/membershipresignation_transferred_shares_confirmation.py:21 -msgid "Confirmation Email for transferred shares." -msgstr "" - -#: coop/emails/membershipresignation_transferred_shares_confirmation.py:26 -msgid "Automatically sent to the member who received shares after a resignation." -msgstr "" - -#: coop/emails/tapir_account_created_email.py:25 -msgid "Tapir account created" -msgstr "" - -#: coop/emails/tapir_account_created_email.py:29 -msgid "Sent to a member when the accounts gets created." -msgstr "" - -#: coop/forms.py:41 -#: financingcampaign/templates/financingcampaign/general.html:27 -msgid "Start date" -msgstr "" - -#: coop/forms.py:45 -msgid "Usually, the date on the membership agreement, or today. In the case of sold or gifted shares, can be set in the future." -msgstr "" - -#: coop/forms.py:50 -#: financingcampaign/templates/financingcampaign/general.html:28 -msgid "End date" -msgstr "" - -#: coop/forms.py:54 -msgid "Usually left empty. Can be set to a point in the future if it is already known that the shares will be transferred to another member in the future." -msgstr "" - -#: coop/forms.py:60 -msgid "Number of shares to create" -msgstr "" - -#: coop/forms.py:68 -msgid "The end date must be later than the start date." -msgstr "" - -#: coop/forms.py:107 coop/models.py:493 -msgid "Number of Shares" -msgstr "" - -#: coop/forms.py:112 -#, python-format -msgid "Number of shares you would like to purchase. The price of one share is EUR %(share_price)s. You need to purchase at least one share to become member of the cooperative. To support our cooperative even more, you may voluntarily purchase more shares." -msgstr "" - -#: coop/forms.py:122 -msgid "I would like to join the membership list as an investing member (= sponsoring member)" -msgstr "" - -#: coop/forms.py:125 -msgid "Note: Investing members are sponsoring members. They have no voting rights in the General Assembly and cannot use the services of the cooperative that are exclusive to ordinary members. " -msgstr "" - -#: coop/forms.py:235 -msgid "Usually, the credited member is the same as the paying member. Only if a person if gifting another person a share through the matching program, then the fields can be different." -msgstr "" - -#: coop/forms.py:257 -msgid "Please not more than 1000 characters." -msgstr "" - -#: coop/forms.py:260 -msgid "Member to resign" -msgstr "" - -#: coop/forms.py:263 -msgid "Transferring share(s) to" -msgstr "" - -#: coop/forms.py:271 -msgid "The member stays active" -msgstr "" - -#: coop/forms.py:273 -msgid "The member becomes investing" -msgstr "" - -#: coop/forms.py:277 -msgid "Member status" -msgstr "" - -#: coop/forms.py:281 -msgid "In the case where the member wants their money back, they stay a member for 3 more years. However, it is very likely that the member doesn't want to be active anymore. If they haven't explicitly mentioned it, please ask them if we can switch them to investing." -msgstr "" - -#: coop/forms.py:348 -msgid "Please pick an option" -msgstr "" - -#: coop/forms.py:360 -msgid "This member is already resigned." -msgstr "" - -#: coop/forms.py:372 -msgid "Please select the member that the shares should be transferred to." -msgstr "" - -#: coop/forms.py:385 -msgid "If the shares don't get transferred to another member, this field should be empty." -msgstr "" - -#: coop/forms.py:398 -msgid "Sender and receiver of transferring the share(s) cannot be the same." -msgstr "" - -#: coop/forms.py:410 -msgid "Cannot pay out, because shares have been gifted." -msgstr "" - -#: coop/models.py:54 -msgid "Is company" -msgstr "" - -#: coop/models.py:61 coop/models.py:467 -msgid "Administrative first name" -msgstr "" - -#: coop/models.py:63 coop/models.py:469 -msgid "Last name" -msgstr "" - -#: coop/models.py:65 coop/models.py:471 -msgid "Usage name" -msgstr "" - -#: coop/models.py:71 coop/models.py:477 -msgid "Email address" -msgstr "" - -#: coop/models.py:88 -msgid "Is investing member" -msgstr "" - -#: coop/models.py:90 coop/models.py:508 -#: coop/templates/coop/draftuser_detail.html:176 -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:48 -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:167 -msgid "Ratenzahlung" -msgstr "" - -#: coop/models.py:92 coop/models.py:500 -msgid "Attended Welcome Session" -msgstr "" - -#: coop/models.py:94 coop/models.py:505 -msgid "Paid Entrance Fee" -msgstr "" - -#: coop/models.py:96 -msgid "Is willing to gift a share" -msgstr "" - -#: coop/models.py:208 -msgid "Cannot be a company and have a Tapir account" -msgstr "" - -#: coop/models.py:224 -msgid "User info should be stored in associated Tapir account" -msgstr "" - -#: coop/models.py:376 -msgid "Not a member" -msgstr "" - -#: coop/models.py:377 coop/templates/coop/draftuser_detail.html:169 -msgid "Investing" -msgstr "" - -#: coop/models.py:378 coop/templates/coop/draftuser_detail.html:171 -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:103 -#: coop/views/statistics.py:142 -msgid "Active" -msgstr "" - -#: coop/models.py:379 -msgid "Paused" -msgstr "" - -#: coop/models.py:434 -msgid "Amount paid for a share can't be negative" -msgstr "" - -#: coop/models.py:438 -#, python-brace-format -msgid "Amount paid for a share can't more than {COOP_SHARE_PRICE} (the price of a share)" -msgstr "" - -#: coop/models.py:496 -msgid "Investing member" -msgstr "" - -#: coop/models.py:503 -msgid "Signed Beteiligungserklärung" -msgstr "" - -#: coop/models.py:506 coop/templates/coop/draftuser_detail.html:234 -msgid "Paid Shares" -msgstr "" - -#: coop/models.py:563 -msgid "Email address must be set." -msgstr "" - -#: coop/models.py:565 -msgid "First name must be set." -msgstr "" - -#: coop/models.py:567 -msgid "Last name must be set." -msgstr "" - -#: coop/models.py:571 -msgid "Membership agreement must be signed." -msgstr "" - -#: coop/models.py:573 -msgid "Amount of requested shares must be positive." -msgstr "" - -#: coop/models.py:575 -msgid "Member already created." -msgstr "" - -#: coop/models.py:602 -msgid "Paying member" -msgstr "" - -#: coop/models.py:610 -msgid "Credited member" -msgstr "" - -#: coop/models.py:617 -msgid "Amount" -msgstr "" - -#: coop/models.py:623 -msgid "Payment date" -msgstr "" - -#: coop/models.py:626 -msgid "Creation date" -msgstr "" - -#: coop/models.py:631 -msgid "Created by" -msgstr "" - -#: coop/models.py:803 -msgid "The cooperative buys the shares back from the member" -msgstr "" - -#: coop/models.py:806 -msgid "The member gifts the shares to the cooperative" -msgstr "" - -#: coop/models.py:808 -msgid "The shares get transferred to another member" -msgstr "" - -#: coop/models.py:811 -msgid "Financial reasons" -msgstr "" - -#: coop/models.py:812 -msgid "Health reasons" -msgstr "" - -#: coop/models.py:813 -msgid "Distance" -msgstr "" - -#: coop/models.py:814 -msgid "Strategic orientation of SuperCoop" -msgstr "" - -#: coop/models.py:815 -msgid "Other" -msgstr "" - -#: coop/models.py:820 -msgid "Shareowner" -msgstr "" - -#: coop/models.py:839 -msgid "Leave this empty if the resignation type is not a transfer to another member" -msgstr "" - -#: coop/templates/coop/about.html:5 coop/templates/coop/about.html:11 -msgid "About Tapir" -msgstr "" - -#: coop/templates/coop/about.html:26 -msgid "Latest changes" -msgstr "" - -#: coop/templates/coop/active_members_progress_bar.html:4 -#, python-format -msgid "" -"\n" -" Grey : %(member_count_on_start_date)s active members on 15.11.2022
\n" -" Blue : %(new_member_count_since_start_date)s new members since 15.11.2022
\n" -" Click for more statistics\n" -" " -msgstr "" - -#: coop/templates/coop/active_members_progress_bar.html:11 -msgid "Active Members with a Tapir Account" -msgstr "" - -#: coop/templates/coop/confirm_delete_incoming_payment.html:7 -#: coop/templates/coop/confirm_delete_incoming_payment.html:13 -#: coop/templates/coop/confirm_delete_share_ownership.html:6 -#: coop/templates/coop/confirm_delete_share_ownership.html:12 -#: coop/templates/coop/draftuser_detail.html:20 -#: financingcampaign/templates/financingcampaign/confirm_delete.html:6 -#: financingcampaign/templates/financingcampaign/confirm_delete.html:12 -msgid "Confirm delete" -msgstr "" - -#: coop/templates/coop/confirm_delete_incoming_payment.html:31 -#: coop/templates/coop/confirm_delete_share_ownership.html:24 -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:127 -#: financingcampaign/templates/financingcampaign/confirm_delete.html:24 -msgid "Delete" -msgstr "" - -#: coop/templates/coop/create_user_from_shareowner_form.html:9 -#: coop/templates/coop/create_user_from_shareowner_form.html:19 -#: coop/templates/coop/shareowner_detail.html:36 coop/views/management.py:45 -#: coop/views/management.py:46 -msgid "Create Tapir account" -msgstr "" - -#: coop/templates/coop/create_user_from_shareowner_form.html:28 -msgid "Create account" -msgstr "" - -#: coop/templates/coop/draftuser_confirm_registration.html:6 -msgid "Registration confirmed" -msgstr "" - -#: coop/templates/coop/draftuser_confirm_registration.html:8 -msgid "" -"\n" -"

Registration successful! Congratulations!

\n" -"

We just emailed your confirmation of your pre-registration with the Beteiligungserklärung already filled with your information. You just have to print it and send it back to us per post or scan it and send it per email.

\n" -"

Once we have received both your Beteiligungserklärung and the corresponding payment, we'll send you the confirmation of your membership.

\n" -"

We will also create for you an account for this website and for the wiki.

\n" -" " -msgstr "" - -#: coop/templates/coop/draftuser_detail.html:8 -#: coop/templates/coop/draftuser_detail.html:78 -#: coop/templates/coop/draftuser_detail.html:127 -msgid "Applicant" -msgstr "" - -#: coop/templates/coop/draftuser_detail.html:49 -msgid "Confirm member creation" -msgstr "" - -#: coop/templates/coop/draftuser_detail.html:58 -msgid "" -"\n" -"

\n" -" Members with similar information as the person you are trying to create have been found.\n" -" Please double-check that this is not a duplicate.\n" -"

\n" -" " -msgstr "" - -#: coop/templates/coop/draftuser_detail.html:66 -msgid "List of similar members" -msgstr "" - -#: coop/templates/coop/draftuser_detail.html:69 -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:31 -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:88 -#: coop/views/shareowner.py:583 -#: shifts/templates/shifts/user_shifts_overview_tag.html:21 -msgid "Status" -msgstr "" - -#: coop/templates/coop/draftuser_detail.html:101 -msgid "I confirm that I have checked that this is not a duplicate." -msgstr "" - -#: coop/templates/coop/draftuser_detail.html:119 -#: coop/templates/coop/draftuser_detail.html:254 -msgid "Create Member" -msgstr "" - -#: coop/templates/coop/draftuser_detail.html:166 -msgid "Member Type" -msgstr "" - -#: coop/templates/coop/draftuser_detail.html:186 -msgid "Shares requested" -msgstr "" - -#: coop/templates/coop/draftuser_detail.html:190 -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:53 -#: shifts/models.py:44 -msgid "Welcome Session" -msgstr "" - -#: coop/templates/coop/draftuser_detail.html:208 -msgid "Beteiligungserklärung" -msgstr "" - -#: coop/templates/coop/draftuser_list.html:22 -#: coop/templates/coop/membership_pause/membership_pause_list.html:24 -#: coop/templates/coop/membership_resignation_list.html:25 -#: coop/templates/coop/shareowner_list.html:23 -#: shifts/templates/shifts/members_on_alert_list.html:19 -#: shifts/templates/shifts/shiftexemption_list.html:24 -msgid "Export" -msgstr "" - -#: coop/templates/coop/draftuser_list.html:42 -#: coop/templates/coop/membership_resignation_list.html:50 -#: coop/templates/coop/shareowner_list.html:38 -#: log/templates/log/log_overview.html:34 -#: shifts/templates/shifts/shift_filters.html:3 -msgid "Filters" -msgstr "" - -#: coop/templates/coop/draftuser_list.html:55 -#: coop/templates/coop/incoming_payment_list.html:35 -#: coop/templates/coop/membership_pause/membership_pause_list.html:47 -#: coop/templates/coop/membership_resignation_list.html:62 -#: coop/templates/coop/shareowner_list.html:51 -#: log/templates/log/log_overview.html:46 -#: shifts/templates/shifts/shift_calendar_template.html:66 -#: shifts/templates/shifts/shiftexemption_list.html:50 -msgid "Filter" -msgstr "" - -#: coop/templates/coop/draftuser_list.html:60 -#: coop/templates/coop/incoming_payment_list.html:40 -#: coop/templates/coop/membership_pause/membership_pause_list.html:52 -#: coop/templates/coop/membership_resignation_list.html:67 -#: coop/templates/coop/shareowner_list.html:56 -#: shifts/templates/shifts/shiftexemption_list.html:55 -msgid "Clear all filters" -msgstr "" - -#: coop/templates/coop/draftuser_list.html:63 -#, python-format -msgid "" -"\n" -" Filtered %(filtered)s of %(total)s\n" -" " -msgstr "" - -#: coop/templates/coop/draftuser_register_form.default.html:27 -msgid "Create Applicant" -msgstr "" - -#: coop/templates/coop/draftuser_register_form.default.html:29 -#: coop/templates/coop/draftuser_register_form.html:29 -#: shifts/templates/shifts/register_user_to_shift_slot.html:27 -#: shifts/templates/shifts/register_user_to_shift_slot_template.html:26 -#: shifts/templates/shifts/shift_detail.html:217 -#: shifts/templates/shifts/shift_template_detail.html:84 -msgid "Register" -msgstr "" - -#: coop/templates/coop/draftuser_register_form.html:27 -#: coop/views/draftuser.py:58 coop/views/draftuser.py:59 -msgid "Create applicant" -msgstr "" - -#: coop/templates/coop/email/accounting_recap.body.default.html:5 -#, python-format -msgid "" -"\n" -"

\n" -" Hello Accounting team, hello member office,\n" -"

\n" -"

\n" -" This is the Tapir accounting recap for the past week.\n" -"

\n" -"

\n" -" %(num_new_members)s new members have been created. For those new members, %(total_num_shares_new_members)s shares\n" -" have been created.
\n" -" Additionally, %(total_num_shares_existing_members)s shares have been created for existing members.\n" -"

\n" -" " -msgstr "" - -#: coop/templates/coop/email/accounting_recap.body.default.html:21 -#: statistics/views/main_view.py:301 -msgid "New members" -msgstr "" - -#: coop/templates/coop/email/accounting_recap.body.default.html:26 -#: coop/templates/coop/email/accounting_recap.body.default.html:38 -msgid "share(s)" -msgstr "" - -#: coop/templates/coop/email/accounting_recap.body.default.html:33 -msgid "New shares for existing members upload only after approval in Duo" -msgstr "" - -#: coop/templates/coop/email/accounting_recap.body.default.html:47 -msgid "This email is send automatically every Sunday. Contact the Tapir team on Slack if you have any question." -msgstr "" - -#: coop/templates/coop/email/accounting_recap.subject.default.html:2 -msgid "" -"\n" -" Accounting recap\n" -msgstr "" - -#: coop/templates/coop/email/co_purchaser_updated.body.html:6 -#, python-format -msgid "" -"\n" -"

Hello %(display_name_short)s,

\n" -"

\n" -" This is an automatic e-mail from SuperCoop. We would like to inform you that a co-shopper has just been\n" -" added to your Tapir account.\n" -"

\n" -"

\n" -" From now on %(co_purchaser_name)s can shop in the store using your membership number.\n" -"

\n" -"

\n" -" If this is an error and does not correspond to your request, please send a short\n" -" email to the Member Office to let us know.\n" -"

\n" -"

\n" -" Cooperative greetings,
\n" -" The Member Office\n" -"

\n" -" " -msgstr "" - -#: coop/templates/coop/email/extra_shares_bought.body.html:6 -#, python-format -msgid "" -"\n" -"

Hello %(display_name_short)s,

\n" -"

Please find the confirmation that you have purchased %(num_shares)s additional shares in the email attachment.

\n" -"

Thank you sincerely for your continued support!

\n" -"

\n" -" Cooperative greetings
\n" -" Your SuperCoop-Board (Vorstand)
\n" -"

\n" -" " -msgstr "" - -#: coop/templates/coop/email/extra_shares_bought.subject.html:2 -#, python-format -msgid "" -"\n" -" Confirmation acquisition of additional shares in %(coop_name)s\n" -msgstr "" - -#: coop/templates/coop/email/membership_confirmation.active.body.html:6 -#, python-format -msgid "" -"\n" -"

Hello %(display_name)s,

\n" -"

\n" -" Welcome as an official member of our cooperative, %(coop_full_name)s! Please find the details of your\n" -" membership confirmation attached. We are thrilled to welcome you aboard and begin running SuperCoop together with you!\n" -"

\n" -" If you haven't done so already, please transfer the amount for your shares together with the 10 € \n" -" admission fee to our account DE98430609671121379000. \n" -" It is also important that you complete METRO's online hygiene training before\n" -" starting your first shift at the Coop. You can find all the information on this under point 3.\n" -"

\n" -"

See below for key details to kickstart your SuperCoop membership:

\n" -"

1. Member Office

\n" -"

The Member Office is the hub for supporting members in all things SuperCoop!

\n" -"

\n" -" Please visit the Member Office to sign up for your first shift (or indicate that you qualify for a work\n" -" exemption). They will create an account for you on Tapir, our online shift management system, and guide you\n" -" through the steps of choosing your shift and working group.\n" -"

\n" -"
\n" -" For general questions about your membership, please contact the Member Office via email (%(contact_email_address)s) or by visiting SuperCoop.\n" -" Their opening hours are:\n" -"
    \n" -"
  • Monday - Friday from 4:30 - 7:30 p.m.
  • \n" -"
  • Saturdays from 11 a.m. - 2 p.m.
  • \n" -"
\n" -"
\n" -"

\n" -" 2. Tapir\n" -"

\n" -"

\n" -" Tapir is our shift management system created by and for SuperCoop\n" -" members! There you can sign up for shifts and see additional information about your membership, such as how many\n" -" shares you own.\n" -"

\n" -"
\n" -" Links to the following are also available on Tapir:\n" -"
    \n" -"
  • The current week in the ABCD calendar (including also the ABCD calendar for the rest of the year),
  • \n" -"
  • wiki,
  • \n" -"
  • member manual,
  • \n" -"
  • opening hours,
  • \n" -"
  • Member Office contact information
  • \n" -"
  • as well as statistics about shifts and our cooperative in general.
  • \n" -"
\n" -"
\n" -"

\n" -" To create a profile on Tapir, please visit the Member Office in the supermarket (see office hours above). Don’t\n" -" forget to\n" -" \n" -" add a co-shopper to your account too!\n" -"

\n" -"

\n" -" Once your profile has been created, you will receive a confirmation email including a link to create a password.\n" -" You can then use these same login credentials to log into the SuperCoop Wiki.\n" -"

\n" -"

\n" -" Tip: Bookmark the link to Tapir in your browser so\n" -" it’s easy to find in the future!\n" -"

\n" -"

3. Hygiene Training

\n" -"

\n" -" New members must complete an online hygiene training from METRO BEFORE their first shift:\n" -" https://kw.my/jEM8PK/#/. The training takes about 30 minutes and ends with\n" -" a short questionnaire. If you answer\n" -" 80%% of the questions correctly, you will receive a certificate. Please show this to an employee during your\n" -" first shift.\n" -"

\n" -"

\n" -" During your first shift, the team leader or an employee will provide an additional 10 minute introduction and\n" -" show you the most important stations and rules in the store. If you could not do the online training at home,\n" -" there is also the possibility to do it at our Welcome Desk during your shift.\n" -"

\n" -"

4. Member Card

\n" -"

\n" -" Your Member Card can be activated through your Tapir account. Member cards are consent based because we use them\n" -" to collect data regarding the sum of the total purchase as well as the frequency of purchases. We use this\n" -" information to support our financial planning and calculate projections of future revenues. It also helps us\n" -" understand if certain measures we are taking to improve the assortment are having an impact on how regularly\n" -" people shop at the coop. Conversely, if we see the regularity or total sum of purchases drop, we know we have to\n" -" investigate this further. In this way, using the member card to track certain data also helps us better\n" -" understand the needs of our members!\n" -"

\n" -"

\n" -" To learn more about the member card and data collection, please visit our wiki:\n" -" https://wiki.supercoop.de/wiki/Mitgliederkarte. Any\n" -" concerns about data protection can be addressed to our data\n" -" protection officer and working group at datenschutz-supercoop@posteo.de.\n" -"

\n" -"

Here’s how it works:

\n" -"
\n" -"
    \n" -"
  1. \n" -" Log into your Tapir profile: https://members.supercoop.de/\n" -"
  2. \n" -"
  3. \n" -" Scroll down and you will see an option to give your consent, including an explanation of what data we\n" -" are\n" -" collecting. (Please note that you can give or remove consent at any time with just one click.)\n" -"
  4. \n" -"
  5. Once you have given consent, you will see a barcode that has been created especially for you.
  6. \n" -"
  7. \n" -" The final and most important step (besides giving consent of course) is to remember to scan it at the\n" -" checkout counter! You have different options for how you can do this, including: logging into Tapir on\n" -" your\n" -" phone and showing it from there, downloading it as a PDF and saving it to your phone, taking a\n" -" screenshot,\n" -" or printing it out! (We're happy to print them out for you in the shop if needed!)\n" -"
  8. \n" -"
\n" -"
\n" -"

\n" -" Last, but not certainly least, you can also load credit onto your member card and use it to pay for your\n" -" groceries! This helps both you and the coop avoid transaction fees when making a purchase, and it can be done\n" -" directly at the checkout counter (Kasse)! 🎉\n" -"

\n" -"

5. Working groups

\n" -"

\n" -" Your Working Group tells you what kind of work you do in the Coop.\n" -"

\n" -"

\n" -" Please note: The most important thing is to select a shift time which you can consistently\n" -" attend. As we are self-organised, we rely on the individual responsibility of each member to attend their shift\n" -" and keep SuperCoop running.\n" -"

\n" -"
\n" -" Members (barring any exceptional cases) should complete their first six shifts with one of the following core\n" -" working groups:\n" -"
    \n" -"
  • \n" -" Receiving and Stocking (receiving deliveries, keeping our shelves and coolers stocked,\n" -" and\n" -" checking expiration dates - Rote Karte not required)\n" -"
  • \n" -"
  • \n" -" Cashier\n" -"
  • \n" -"
  • \n" -" Welcome Desk (greeting people at the store entrance, checking member numbers and\n" -" answering\n" -" visitors' questions about SuperCoop)\n" -"
  • \n" -"
  • \n" -" Maintenance (cleaning the coop after closing)\n" -"
  • \n" -"
\n" -"
\n" -"

\n" -" Please note that working groups and tasks dynamically adapt to the development of SuperCoop and are constantly\n" -" updated. You may change working groups as needed.\n" -"

\n" -"

\n" -" You also have opportunities to get involved at SuperCoop in addition to your regular shift. To make it as easy\n" -" as\n" -" possible for you to get started, we have summarized all additional\n" -" working groups in the wiki. Please see the Wiki for further information on how to get involved.\n" -"

\n" -"

6. Member Manual

\n" -"

\n" -" Before sending your questions to the Member Office, please consult the member manual. It\n" -" contains all your need-to-know information about membership at SuperCoop: how to get started, how the shift\n" -" systems work, our governance structure and much more!\n" -"

\n" -"

7. SuperCoopWiki

\n" -"

\n" -" Another good source of information is the SuperCoop Wiki.\n" -" There you will find all the important details on our working groups, member meetings, webshop, your fellow\n" -" co-owners/colleagues and more! It is a \"living\" tool that we all may edit and\n" -" add to as our coop grows. Check out the get started\n" -" page to... well, get started!\n" -"

\n" -"

8. Plenum

\n" -"

\n" -" All members are invited to our plenum (currently held online\n" -" when decision-making is required) to vote on proposals and raise topics for discussion. Dates, links and other\n" -" details are announced on the wiki and in our bi-weekly newsletter.\n" -"

\n" -"

9. Newsletter

\n" -"

\n" -" Wednesday is SuperCoop day! Every two weeks fresh news about current events, information on products or\n" -" publications will land in your email inbox.\n" -"

\n" -"

10. Slack

\n" -"

\n" -" We use the chat app Slack as a direct communication channel for the working groups, projects and general\n" -" information. You can register for free and use Slack directly in your browser or via the app. Join\n" -" here! If you want help getting started, please contact the Membership Office (see above).\n" -"

\n" -"

11. Direct Contact

\n" -"

\n" -" If you wish to contact the Vorstand directly, please use the email %(management_email_address)s.\n" -"

\n" -"

\n" -" To contact the Aufsichtsrat (advisory board), please use the email address %(supervisors_email_address)s. They may be contacted\n" -" regarding any topic which is relevant to the interests of SuperCoop members.\n" -"

\n" -"

12. Social Media Channels

\n" -"

\n" -" Oh yeah, and we are also on the Internet! Follow us on Facebook, Instagram, LinkedIn\n" -" or Nebenan.de and spread the word about SuperCoop.\n" -"

\n" -"

\n" -" We're so glad you're joining us and we hope to see you soon whether on the wiki, Slack, at the Plenum or at\n" -" Osram-Höfe!\n" -"

\n" -"

\n" -" Cooperative Greetings,\n" -"
\n" -" Your SuperCoop-Board\n" -"

\n" -" " -msgstr "" - -#: coop/templates/coop/email/membership_confirmation.active.subject.html:2 -#, python-format -msgid "" -"\n" -" Welcome at %(organization_name)s!\n" -msgstr "" - -#: coop/templates/coop/email/membership_confirmation.investing.body.html:6 -#, python-format -msgid "" -"\n" -"

Dear %(display_name)s,

\n" -"

\n" -" Welcome as an official investing member of %(coop_full_name)s! By subscribing to %(num_shares)s cooperative share/s, \n" -" you have decided to shape the food system together with us and many other coop members!\n" -"

\n" -"

\n" -" We are very happy to confirm your admission to the cooperative with the membership number %(owner_id)s. See the\n" -" attached PDF for full details.\n" -"

\n" -"

\n" -" If you have questions, please contact our member-office (mitglied@supercoop.de)\n" -"

\n" -"

\n" -" Cooperative greetings,\n" -"
\n" -" Your SuperCoop-Board\n" -"

\n" -" " -msgstr "" - -#: coop/templates/coop/email/membership_confirmation.investing.subject.html:2 -#, python-format -msgid "" -"\n" -" Confirmation of the investing membership at %(organization_name)s!\n" -msgstr "" - -#: coop/templates/coop/email/membershipresignation_confirmation_body.html:6 -#, python-format -msgid "" -"\n" -"

Dear %(display_name)s,

\n" -"

\n" -" what a pity that you want to leave us. We hereby confirm the cancellation of your membership with SuperCoop Berlin eG.\n" -"
\n" -"

\n" -" " -msgstr "" - -#: coop/templates/coop/email/membershipresignation_confirmation_body.html:14 -msgid "" -"\n" -"

\n" -" As you stated in your letter of cancellation that you want to transfer your shares to SuperCoop,\n" -" your termination is effective immediately.\n" -"

\n" -" " -msgstr "" - -#: coop/templates/coop/email/membershipresignation_confirmation_body.html:22 -#, python-format -msgid "" -"\n" -"

\n" -" You stated in your letter of cancellation that you want to transfer your shares to another member. We've informed\n" -" %(transferred_member)s about this separately.\n" -" Your termination is effective immediately.\n" -"

\n" -" " -msgstr "" - -#: coop/templates/coop/email/membershipresignation_confirmation_body.html:30 -#, python-format -msgid "" -"\n" -"

\n" -" As you wished in your cancellation letter your membership will run until the end of the regular cancellation period specified in our Articles of Association, i.e.\n" -" until the end of the financial year in three years. Please note the passage stated in our Articles of Association\n" -" under §4 Termination of membership, transfer of business assets, death and exclusion.\n" -" As a precaution, the articles of association are attached to this e-mail.\n" -"

\n" -"

\n" -" As your membership will continue to run as normal for the next three years and is still entered as \"active\" in Tapir,\n" -" we would like to take this opportunity to point out that you can continue to complete shifts in the next three years\n" -" and therefore retain your right to shop. If you no longer wish to complete shifts or shop in Supercoop, we would ask you to apply to\n" -" the member-office (%(contact_email_address)s) to switch your membership to \"investing\".\n" -" \n" -"

\n" -"

\n" -" This will prevent your shift backlog from going into the red and you from receiving emails with warnings from us in the future.\n" -"

\n" -" " -msgstr "" - -#: coop/templates/coop/email/membershipresignation_confirmation_body.html:49 -#, python-format -msgid "" -"\n" -"

\n" -" We wish you all the best for the future and please do not hesitate to contact the member-office\n" -" (%(contact_email_address)s) if you have any further questions.\n" -"

\n" -"

Cooperative greetings,

\n" -"

Your SuperCoop-Board

\n" -" " -msgstr "" - -#: coop/templates/coop/email/membershipresignation_confirmation_subject.html:4 -#, python-format -msgid "" -"\n" -" Confirmation of the cancellation of membership: %(share_owner_name)s\n" -msgstr "" - -#: coop/templates/coop/email/membershipresignation_transferred_shares_confirmation_body.html:7 -#, python-format -msgid "" -"\n" -"

Dear %(receiving_member)s,

\n" -"

\n" -" Good news for you and unfortunately bad news for us!\n" -"
\n" -" %(resigning_member)s has cancelled his membership\n" -" of SuperCoop Berlin eG and bequeathed her/his share(s) to you.\n" -" Don't be surprised if you suddenly see more shares in your Tapir account.\n" -" If this was done without your consent you can contact the member-office\n" -" (%(contact_email_address)s).\n" -"

\n" -"

Cooperative greetings,

\n" -"

Your SuperCoop-Board

\n" -" " -msgstr "" - -#: coop/templates/coop/email/membershipresignation_transferred_shares_confirmation_subject.html:4 -#, python-format -msgid "" -"\n" -" %(resigned_member_full)s bequeathed her / his share(s) to you because of resignation\n" -msgstr "" - -#: coop/templates/coop/email/tapir_account_created.body.html:6 -#, python-format -msgid "" -"\n" -"

Dear %(user_display_name)s,

\n" -"

we just created an account for you in our SuperCoop member system. Here you can view your upcoming shifts, book an additional shift if you'd like or mark your shift as “looking for a stand-in” (for example when you go on vacation).
\n" -" Please see the Member Manual section III for more information on the Stand-in System.

\n" -" " -msgstr "" - -#: coop/templates/coop/email/tapir_account_created.body.html:14 -#, python-format -msgid "" -"\n" -" Your regular ABCD-shift is: %(slot_display_name)s. You will receive a reminder email in advance of your first shift.\n" -" " -msgstr "" - -#: coop/templates/coop/email/tapir_account_created.body.html:19 -msgid "" -"\n" -" Flying members: Please keep in mind that you must have at least one shift “banked” for each shift cycle. For more information please see the Member Manual .\n" -" " -msgstr "" - -#: coop/templates/coop/email/tapir_account_created.body.html:27 -#, python-format -msgid "" -"\n" -"

Your username is *%(username)s* . In order to login to your account, you first have to set a password : Click here to set your password

\n" -"

This link is only valid for a few weeks. Should it expire, you can get a new one here : Click here to get a new link

\n" -"

You can also login to the SuperCoop wiki with that account.

\n" -"

Alternatively, as for any other question, you can always contact the Member Office

\n" -" " -msgstr "" - -#: coop/templates/coop/email/tapir_account_created.body.html:35 -msgid "" -"\n" -" Cooperative regards,
\n" -" Your SuperCoop Berlin Member Office team.\n" -" " -msgstr "" - -#: coop/templates/coop/email/tapir_account_created.subject.html:2 -#, python-format -msgid "Your account in Tapir, %(coop_name)s's member system" -msgstr "" - -#: coop/templates/coop/general_accounts_list.html:7 -#: coop/templates/coop/general_accounts_list.html:16 -msgid "General Tapir Accounts" -msgstr "" - -#: coop/templates/coop/incoming_payment_list.html:9 -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:160 -msgid "Payments" -msgstr "" - -#: coop/templates/coop/incoming_payment_list.html:24 -msgid "Register a new payment" -msgstr "" - -#: coop/templates/coop/log/create_membership_pause_log_entry.html:3 -#, python-format -msgid "" -"\n" -" New membership pause starting %(start_date)s: %(description)s.\n" -msgstr "" - -#: coop/templates/coop/log/create_payment_log_entry.html:2 -#, python-format -msgid "New Payment: %(amount)s € on %(payment_date)s" -msgstr "" - -#: coop/templates/coop/log/create_resignmember_log_entry.html:2 -#, python-format -msgid "" -"\n" -"Member resigned for reason: %(cancellation_reason)s\n" -msgstr "" - -#: coop/templates/coop/log/delete_resignmember_log_entry.html:2 -msgid "" -"\n" -" Reactivated resigned member\n" -msgstr "" - -#: coop/templates/coop/log/update_incoming_payment_log_entry.html:2 -msgid "Updated payment:" -msgstr "" - -#: coop/templates/coop/log/update_membership_pause_log_entry.html:2 -msgid "Updated membership pause:" -msgstr "" - -#: coop/templates/coop/log/update_resignmember_log_entry.html:2 -msgid "Updated resigned membership:" -msgstr "" - -#: coop/templates/coop/log/update_share_owner_log_entry.html:2 -msgid "Update Member:" -msgstr "" - -#: coop/templates/coop/matching_program.html:7 -#: coop/templates/coop/member_management.html:30 -msgid "Matching program" -msgstr "" - -#: coop/templates/coop/matching_program.html:16 -msgid "Matching program members" -msgstr "" - -#: coop/templates/coop/matching_program.html:20 -msgid "" -"\n" -" The members in this list have expressed that they are willing to pay the share for a potential new member that otherwise couldn't afford it.\n" -" " -msgstr "" - -#: coop/templates/coop/member_management.html:16 -#: shifts/templates/shifts/shift_management.html:29 -msgid "Lists" -msgstr "" - -#: coop/templates/coop/member_management.html:21 -msgid "Access right groups" -msgstr "" - -#: coop/templates/coop/member_management.html:26 -#: coop/templates/coop/membership_pause/membership_pause_list.html:10 -#: coop/templates/coop/membership_pause/membership_pause_list.html:19 -msgid "Membership pauses" -msgstr "" - -#: coop/templates/coop/member_management.html:36 -msgid "Membership resignations" -msgstr "" - -#: coop/templates/coop/member_management.html:42 -#: coop/templates/coop/member_management.html:51 -msgid "Old member statistics" -msgstr "" - -#: coop/templates/coop/member_management.html:45 -msgid "" -"\n" -"

This statistics have been hidden from members in order to focus on the more important\n" -" ones.

\n" -"

They may not be relevant or well presented.

\n" -" " -msgstr "" - -#: coop/templates/coop/membership_pause/membership_pause_list.html:36 -msgid "Create a new pause" -msgstr "" - -#: coop/templates/coop/membership_pause/membership_pause_list.html:55 -#, python-format -msgid "" -"\n" -" Filtered %(filtered_pause_count)s of %(total_pause_count)s\n" -" " -msgstr "" - -#: coop/templates/coop/membership_resignation_form.html:51 -#: coop/templates/coop/membership_resignation_form.html:56 -#: coop/views/membership_resignation.py:255 -msgid "Resign a new membership" -msgstr "" - -#: coop/templates/coop/membership_resignation_list.html:12 -#: coop/templates/coop/membership_resignation_list.html:21 -msgid "List of membership resignations" -msgstr "" - -#: coop/templates/coop/membership_resignation_list.html:33 -msgid "" -"\n" -" Only the vorstand and the employees can create resignations.\n" -" " -msgstr "" - -#: coop/templates/coop/membership_resignation_list.html:41 -msgid "Resign new member" -msgstr "" - -#: coop/templates/coop/membershipresignation_detail.html:8 -#: coop/templates/coop/membershipresignation_detail.html:51 -msgid "Membership resignation" -msgstr "" - -#: coop/templates/coop/membershipresignation_detail.html:19 -msgid "Confirm cancellation of resignation" -msgstr "" - -#: coop/templates/coop/membershipresignation_detail.html:28 -#, python-format -msgid "" -"\n" -" Are you sure you want to\n" -" cancel the resignation of %(member_name)s?\n" -" The person's shares will be reactivated.\n" -" " -msgstr "" - -#: coop/templates/coop/membershipresignation_detail.html:55 -msgid "Share owner" -msgstr "" - -#: coop/templates/coop/membershipresignation_detail.html:61 -msgid "Cancellation reason" -msgstr "" - -#: coop/templates/coop/membershipresignation_detail.html:65 -msgid "Cancellation date" -msgstr "" - -#: coop/templates/coop/membershipresignation_detail.html:70 -msgid "Membership ends" -msgstr "" - -#: coop/templates/coop/membershipresignation_detail.html:75 -msgid "Resignation type" -msgstr "" - -#: coop/templates/coop/membershipresignation_detail.html:79 -msgid "Paid out" -msgstr "" - -#: coop/templates/coop/membershipresignation_detail.html:82 -#: coop/views/draftuser.py:286 -msgid "Yes" -msgstr "" - -#: coop/templates/coop/membershipresignation_detail.html:84 -#: coop/views/draftuser.py:286 -msgid "No" -msgstr "" - -#: coop/templates/coop/membershipresignation_detail.html:91 -msgid "" -"\n" -" Only the vorstand and the employees can edit resignations.\n" -" " -msgstr "" - -#: coop/templates/coop/pdf/extra_shares_confirmation_pdf.default.html:12 -#, python-format -msgid "" -"\n" -" Confirmation acquisition of additional shares in %(coop_name)s for %(display_name)s\n" -" " -msgstr "" - -#: coop/templates/coop/pdf/extra_shares_confirmation_pdf.default.html:61 -#, python-format -msgid "Confirmation acquisition of additional shares in %(coop_full_name)s" -msgstr "" - -#: coop/templates/coop/pdf/extra_shares_confirmation_pdf.default.html:66 -#, python-format -msgid "" -"\n" -"

Dear %(display_name_short)s,

\n" -"\n" -"

\n" -" Thank you for your support!
\n" -" This document confirms that you have purchased %(num_shares)s additional shares.\n" -" Once payment is received, this information will also be reflected in your Tapir profile.\n" -"

\n" -"\n" -"

\n" -" Reminder: Your membership number is %(member_number)s.
\n" -" This number is important for your communication with us and the Member Office.\n" -" Please save it as you will need it often.\n" -"

\n" -"\n" -"

\n" -" If you have any questions, feel free to contact the Member Office\n" -" (%(contact_email_address)s)\n" -" and we will answer you as soon as possible.\n" -"

\n" -"\n" -"

\n" -" Thank you for your continued support!\n" -"

\n" -"\n" -"

\n" -" Cooperative greetings
\n" -" Your SuperCoop-Board\n" -"

\n" -" " -msgstr "" - -#: coop/templates/coop/pdf/membership_agreement_pdf.default.html:53 -#: coop/templates/coop/pdf/membership_confirmation_pdf.default.html:45 -msgid "Organisation logo" -msgstr "" - -#: coop/templates/coop/shareowner_detail.html:22 -msgid "Go to user page" -msgstr "" - -#: coop/templates/coop/shareowner_list.html:19 -msgid "Cooperative Members" -msgstr "" - -#: coop/templates/coop/shareowner_list.html:59 -#, python-format -msgid "" -"\n" -" Filtered %(filtered_member_count)s of %(total_member_count)s\n" -" " -msgstr "" - -#: coop/templates/coop/statistics.html:10 -msgid "Members statistics" -msgstr "" - -#: coop/templates/coop/statistics.html:28 -msgid "Loading..." -msgstr "" - -#: coop/templates/coop/statistics.html:48 -msgid "Statistics on members" -msgstr "" - -#: coop/templates/coop/statistics.html:52 -msgid "Active members with account at end of month as CSV" -msgstr "" - -#: coop/templates/coop/statistics.html:72 -#: statistics/templates/statistics/main_statistics.html:27 -msgid "Evolution of member count" -msgstr "" - -#: coop/templates/coop/statistics.html:74 -msgid "Member age distribution" -msgstr "" - -#: coop/templates/coop/statistics.html:82 -msgid "Statistics on shares" -msgstr "" - -#: coop/templates/coop/statistics.html:126 -msgid "Evolution of the number of shares" -msgstr "" - -#: coop/templates/coop/statistics.html:134 -msgid "Members who bought extra shares" -msgstr "" - -#: coop/templates/coop/statistics.html:150 -#: coop/templates/coop/statistics.html:158 -msgid "Member status updates" -msgstr "" - -#: coop/templates/coop/statistics.html:154 -#: coop/templates/coop/statistics.html:170 -msgid "Get as CSV" -msgstr "" - -#: coop/templates/coop/statistics.html:166 -#: coop/templates/coop/statistics.html:174 -msgid "Number of co-purchasers per month" -msgstr "" - -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:9 -#, python-format -msgid "" -"\n" -" Member #%(coop_share_owner_id)s\n" -" " -msgstr "" - -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:17 -msgid "Membership confirmation" -msgstr "" - -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:35 -#, python-format -msgid "" -"\n" -" (membership starting %(start_date)s)\n" -" " -msgstr "" - -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:40 -#, python-format -msgid "" -"\n" -" (membership ending %(end_date)s)\n" -" " -msgstr "" - -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:56 -#: shifts/models.py:945 shifts/templates/shifts/shift_day_printable.html:56 -#: shifts/templates/shifts/shift_detail.html:272 -#: shifts/templates/shifts/shift_detail_printable.html:51 -msgid "Attended" -msgstr "" - -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:58 -#: shifts/models.py:944 -msgid "Pending" -msgstr "" - -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:65 -msgid "Mark Attended" -msgstr "" - -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:74 -msgid "Owned shares" -msgstr "" - -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:82 -msgid "List of shares owned by this member" -msgstr "" - -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:86 -msgid "Starts at" -msgstr "" - -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:87 -msgid "Ends at" -msgstr "" - -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:105 -msgid "Sold or future" -msgstr "" - -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:130 -msgid "" -"\n" -" Only use this to correct mistakes, i.e. if the share was\n" -" erroneously\n" -" entered into the system and the person never actually\n" -" bought it. If the person simply sold their share back to the\n" -" coop, please mark the share as sold instead.\n" -" " -msgstr "" - -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:153 -msgid "Add Shares" -msgstr "" - -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:171 -msgid "Willing to gift a share" -msgstr "" - -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:181 -msgid "Send membership confirmation email" -msgstr "" - -#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:187 -msgid "User is not a cooperative member." -msgstr "" - -#: coop/views/draftuser.py:89 coop/views/draftuser.py:94 -#, python-format -msgid "Edit applicant: %(name)s" -msgstr "" - -#: coop/views/draftuser.py:198 -msgid "Can't create member: " -msgstr "" - -#: coop/views/draftuser.py:276 coop/views/draftuser.py:312 -msgid "Member can be created" -msgstr "" - -#: coop/views/incoming_payments.py:55 -msgid "Payment ID" -msgstr "" - -#: coop/views/incoming_payments.py:78 -msgid "Other member" -msgstr "" - -#: coop/views/incoming_payments.py:166 -msgid "Register payment" -msgstr "" - -#: coop/views/incoming_payments.py:167 -msgid "Register a new incoming payment" -msgstr "" - -#: coop/views/incoming_payments.py:187 coop/views/incoming_payments.py:188 -msgid "Edit payment" -msgstr "" - -#: coop/views/incoming_payments.py:192 -msgid "Payment updated." -msgstr "" - -#: coop/views/incoming_payments.py:216 -msgid "Payment deleted" -msgstr "" - -#: coop/views/management.py:39 -msgid "Required. Please insert a valid email address." -msgstr "" - -#: coop/views/membership_pause.py:139 -msgid "Create a new membership pause" -msgstr "" - -#: coop/views/membership_pause.py:173 coop/views/membership_pause.py:178 -#, python-format -msgid "Edit membership pause: %(name)s" -msgstr "" - -#: coop/views/membership_resignation.py:111 -msgid "Share(s) gifted {chr(8594)} SuperCoop" -msgstr "" - -#: coop/views/membership_resignation.py:117 -msgid "Share(s) gifted" -msgstr "" - -#: coop/views/membership_resignation.py:137 -msgid "Search member or ID" -msgstr "" - -#: coop/views/membership_resignation.py:143 -msgid "Pay out start date" -msgstr "" - -#: coop/views/membership_resignation.py:149 -msgid "Pay out end date" -msgstr "" - -#: coop/views/membership_resignation.py:211 -#: coop/views/membership_resignation.py:216 -#, python-format -msgid "Cancel membership of %(name)s" -msgstr "" - -#: coop/views/shareowner.py:123 coop/views/shareowner.py:128 -#, python-format -msgid "Edit share: %(name)s" -msgstr "" - -#: coop/views/shareowner.py:148 coop/views/shareowner.py:153 -#, python-format -msgid "Add shares to %(name)s" -msgstr "" - -#: coop/views/shareowner.py:362 -msgid "Membership confirmation email sent." -msgstr "" - -#: coop/views/shareowner.py:584 shifts/templates/shifts/shift_filters.html:45 -msgid "Any" -msgstr "" - -#: coop/views/shareowner.py:589 -#: shifts/templates/shifts/user_shifts_overview_tag.html:74 -msgid "Shift Status" -msgstr "" - -#: coop/views/shareowner.py:597 -msgid "Is registered to an ABCD-slot that requires a qualification" -msgstr "" - -#: coop/views/shareowner.py:605 -msgid "Is registered to a slot that requires a qualification" -msgstr "" - -#: coop/views/shareowner.py:613 -msgid "Has qualification" -msgstr "" - -#: coop/views/shareowner.py:621 -msgid "Does not have qualification" -msgstr "" - -#: coop/views/shareowner.py:628 -msgid "ABCD Week" -msgstr "" - -#: coop/views/shareowner.py:631 -msgid "Has unpaid shares" -msgstr "" - -#: coop/views/shareowner.py:634 -msgid "Is fully paid" -msgstr "" - -#: coop/views/shareowner.py:637 -msgid "Name or member ID" -msgstr "" - -#: coop/views/shareowner.py:641 -msgid "Is currently exempted from shifts" -msgstr "" - -#: coop/views/shareowner.py:646 -msgid "Shift Name" -msgstr "" - -#: coop/views/shareowner.py:919 -msgctxt "Willing to give a share" -msgid "No" -msgstr "" - -#: coop/views/statistics.py:142 -msgid "All members" -msgstr "" - -#: coop/views/statistics.py:142 -msgid "Active with account" -msgstr "" - -#: coop/views/statistics.py:187 -msgid "Number of shares" -msgstr "" - -#: coop/views/statistics.py:235 -msgid "Number of members (X-axis) by age (Y-axis)" -msgstr "" - -#: coop/views/statistics.py:283 -msgid "New active members" -msgstr "" - -#: coop/views/statistics.py:284 -msgid "New investing members" -msgstr "" - -#: coop/views/statistics.py:285 -msgid "New active members without account" -msgstr "" - -#: coop/views/statistics.py:286 -msgid "Active to investing" -msgstr "" - -#: coop/views/statistics.py:287 -msgid "Investing to active" -msgstr "" - -#: coop/views/statistics.py:487 -msgid "Number of members with a co-purchaser (X-axis) by month (Y-axis)" -msgstr "" - -#: core/apps.py:19 -msgid "Emails" -msgstr "" - -#: core/apps.py:26 core/templates/core/featureflag_list.html:5 -msgid "Features" -msgstr "" - -#: core/apps.py:33 statistics/apps.py:15 -msgid "Miscellaneous" -msgstr "" - -#: core/apps.py:36 -msgid "Wiki" -msgstr "" - -#: core/apps.py:43 -msgid "Member manual" -msgstr "" - -#: core/apps.py:50 -msgid "Shop opening hours" -msgstr "" - -#: core/apps.py:57 -msgid "Slack chat" -msgstr "" - -#: core/apps.py:64 -msgid "Contact the member office" -msgstr "" - -#: core/apps.py:71 -msgid "About tapir" -msgstr "" - -#: core/models.py:7 -msgid "Flag name" -msgstr "" - -#: core/models.py:10 -msgid "Flag value" -msgstr "" - -#: core/templates/core/base.html:79 -msgid "Use this form to search for members" -msgstr "" - -#: core/templates/core/base.html:80 -msgid "Search Members" -msgstr "" - -#: core/templates/core/email_list.html:5 core/templates/core/email_list.html:10 -msgid "Email list" -msgstr "" - -#: core/templates/core/email_list.html:24 -msgid "This is the list of all emails that can get sent by Tapir." -msgstr "" - -#: core/templates/core/email_list.html:26 -msgid "The previews in the \"Subject\" and \"Body\" columns are fake : they show what the email would look like if it was sent to a random user, using random data." -msgstr "" - -#: core/templates/core/email_list.html:29 -msgid "The files in the \"Last sent example\" column are real : they have been sent to a user exactly as shown. They can therefore contain private information." -msgstr "" - -#: core/templates/core/email_list.html:35 -msgid "List of all emails" -msgstr "" - -#: core/templates/core/email_list.html:39 shifts/models.py:513 -#: shifts/models.py:1083 shifts/templates/shifts/user_shift_account_log.html:29 -msgid "Description" -msgstr "" - -#: core/templates/core/email_list.html:40 -msgid "Subject" -msgstr "" - -#: core/templates/core/email_list.html:41 -msgid "Body" -msgstr "" - -#: core/templates/core/email_list.html:42 -msgid "Last sent example" -msgstr "" - -#: core/templates/core/featureflag_list.html:10 -msgid "Enable or disable features" -msgstr "" - -#: core/templates/core/featureflag_list.html:15 -msgid "List of all feature flags" -msgstr "" - -#: core/templates/core/featureflag_list.html:19 -msgid "Current value" -msgstr "" - -#: core/templates/core/tags/financing_campaign_progress_bar.html:6 -msgid "Current: " -msgstr "" - -#: core/templates/core/tags/financing_campaign_progress_bar.html:6 -msgid "Click this link to learn more." -msgstr "" - -#: core/templates/core/tags/financing_campaign_progress_bar.html:13 -msgid "Goal: " -msgstr "" - -#: core/views.py:47 core/views.py:49 log/models.py:147 -msgid "Not available" -msgstr "" - -#: core/views.py:85 -#, python-format -msgid "Feature: %(name)s" -msgstr "" - -#: financingcampaign/apps.py:16 -msgid "Financing campaigns" -msgstr "" - -#: financingcampaign/templates/financingcampaign/general.html:6 -msgid "Financing campaigns overview" -msgstr "" - -#: financingcampaign/templates/financingcampaign/general.html:12 -msgid "Campaigns" -msgstr "" - -#: financingcampaign/templates/financingcampaign/general.html:16 -msgid "Create campaign" -msgstr "" - -#: financingcampaign/templates/financingcampaign/general.html:22 -msgid "The list of all financing campaigns" -msgstr "" - -#: financingcampaign/templates/financingcampaign/general.html:26 -msgid "Goal" -msgstr "" - -#: financingcampaign/templates/financingcampaign/general.html:29 -#: financingcampaign/templates/financingcampaign/general.html:72 -#: financingcampaign/templates/financingcampaign/general.html:114 -#: shifts/templates/shifts/shift_detail.html:83 -msgid "Actions" -msgstr "" - -#: financingcampaign/templates/financingcampaign/general.html:57 -msgid "Sources" -msgstr "" - -#: financingcampaign/templates/financingcampaign/general.html:61 -msgid "Create source" -msgstr "" - -#: financingcampaign/templates/financingcampaign/general.html:67 -msgid "The list of all financing sources" -msgstr "" - -#: financingcampaign/templates/financingcampaign/general.html:71 -msgid "Linked campaign" -msgstr "" - -#: financingcampaign/templates/financingcampaign/general.html:98 -msgid "Datapoints" -msgstr "" - -#: financingcampaign/templates/financingcampaign/general.html:102 -msgid "Create datapoint" -msgstr "" - -#: financingcampaign/templates/financingcampaign/general.html:108 -msgid "The list of all datapoints" -msgstr "" - -#: financingcampaign/templates/financingcampaign/general.html:111 -#: log/templates/log/log_entry_list_tag.html:23 -#: shifts/templates/shifts/user_shift_account_log.html:26 -#: statistics/templates/statistics/tags/purchase_statistics_card.html:18 -msgid "Date" -msgstr "" - -#: financingcampaign/templates/financingcampaign/general.html:112 -#: shifts/templates/shifts/user_shift_account_log.html:27 -msgid "Value" -msgstr "" - -#: financingcampaign/templates/financingcampaign/general.html:113 -msgid "Linked source" -msgstr "" - -#: financingcampaign/views.py:65 -msgid "Create a new financing campaign" -msgstr "" - -#: financingcampaign/views.py:76 -#, python-format -msgid "Edit financing campaign: %(name)s" -msgstr "" - -#: financingcampaign/views.py:93 -msgid "Create a new financing source" -msgstr "" - -#: financingcampaign/views.py:104 -#, python-format -msgid "Edit financing source: %(name)s" -msgstr "" - -#: financingcampaign/views.py:123 -msgid "Create a new financing source datapoint" -msgstr "" - -#: financingcampaign/views.py:136 -#, python-format -msgid "Edit financing source datapoint: %(name)s" -msgstr "" - -#: log/apps.py:18 log/templates/log/log_overview.html:24 -msgid "Logs" -msgstr "" - -#: log/models.py:21 -msgid "Creation Date" -msgstr "" - -#: log/templates/log/email_log_entry.html:3 -msgid "Email sent." -msgstr "" - -#: log/templates/log/email_log_entry.html:16 -msgid "Email-Inhalt ist aus Sicherheitsgründen nicht verfügbar" -msgstr "" - -#: log/templates/log/log_entry_list_tag.html:8 -msgid "Last 5 log entries" -msgstr "" - -#: log/templates/log/log_entry_list_tag.html:13 -msgid "See all log entries" -msgstr "" - -#: log/templates/log/log_entry_list_tag.html:20 -msgid "List of log entries for this member" -msgstr "" - -#: log/templates/log/log_entry_list_tag.html:24 log/views.py:97 -#: log/views.py:151 -msgid "Actor" -msgstr "" - -#: log/templates/log/log_entry_list_tag.html:25 log/views.py:90 -msgid "Message" -msgstr "" - -#: log/templates/log/log_entry_list_tag.html:57 -#: log/templates/log/log_entry_list_tag.html:58 -msgid "Notes about this user" -msgstr "" - -#: log/templates/log/log_entry_list_tag.html:63 -msgid "Add Note" -msgstr "" - -#: log/views.py:153 -msgid "Log Type" -msgstr "" - -#: shifts/apps.py:36 shifts/templates/shifts/user_shifts_overview_tag.html:8 -msgid "Shifts" -msgstr "" - -#: shifts/apps.py:39 shifts/templates/shifts/shift_calendar_future.html:4 -#: shifts/templates/shifts/shift_calendar_future.html:7 -msgid "Shift calendar" -msgstr "" - -#: shifts/apps.py:46 -msgid "ABCD-shifts week-plan" -msgstr "" - -#: shifts/apps.py:69 shifts/templates/shifts/shift_management.html:7 -msgid "Shift management" -msgstr "" - -#: shifts/apps.py:86 -#, python-brace-format -msgid "ABCD annual calendar, current week: {current_week_group_name}" -msgstr "" - -#: shifts/emails/flying_member_registration_reminder_email.py:17 -msgid "Flying member registration reminder" -msgstr "" - -#: shifts/emails/flying_member_registration_reminder_email.py:22 -#, python-format -msgid "Sent to flying members %(nb_days)s days after a cycle has begun, if they haven't registered to a shift for this cycle." -msgstr "" - -#: shifts/emails/freeze_warning_email.py:28 -msgid "Freeze warning" -msgstr "" - -#: shifts/emails/freeze_warning_email.py:33 -msgid "Sent to a member when their shift status is not frozen yet but will be set to frozen if they don't register for make-up shifts." -msgstr "" - -#: shifts/emails/member_frozen_email.py:23 -msgid "Shift status set to frozen" -msgstr "" - -#: shifts/emails/member_frozen_email.py:28 -msgid "Sent to a member when their shift status gets set to frozen. Usually happens if they miss to many shifts." -msgstr "" - -#: shifts/emails/shift_missed_email.py:22 -msgid "Shift missed" -msgstr "" - -#: shifts/emails/shift_missed_email.py:26 -msgid "Sent to a member when the member office marks the shift as missed" -msgstr "" - -#: shifts/emails/shift_reminder_email.py:22 -msgid "Shift reminder" -msgstr "" - -#: shifts/emails/shift_reminder_email.py:27 -#, python-brace-format -msgid "Sent to a member {config.REMINDER_EMAIL_DAYS_BEFORE_SHIFT} days before their shift" -msgstr "" - -#: shifts/emails/stand_in_found_email.py:22 -msgid "Stand-in found" -msgstr "" - -#: shifts/emails/stand_in_found_email.py:27 -msgid "Sent to a member that was looking for a stand-inwhen the corresponding slot is taken over by another member." -msgstr "" - -#: shifts/emails/unfreeze_notification_email.py:17 -msgid "Unfreeze Notification" -msgstr "" - -#: shifts/emails/unfreeze_notification_email.py:22 -msgid "Sent to a member when their shift status gets set from frozen to flying." -msgstr "" - -#: shifts/forms.py:60 -msgid "The shift must end after it starts." -msgstr "" - -#: shifts/forms.py:110 -msgid "I have read the warning about the missing qualification and confirm that the user should get registered to the shift" -msgstr "" - -#: shifts/forms.py:138 -#, python-brace-format -msgid "The selected user is missing the required qualification for this shift : {missing_capabilities}" -msgstr "" - -#: shifts/forms.py:181 -msgid "Please set the chosen time after the start of the shift" -msgstr "" - -#: shifts/forms.py:186 -msgid "Please set the chosen time before the end of the shift" -msgstr "" - -#: shifts/forms.py:223 -msgid "This user is already registered to another slot in this ABCD shift." -msgstr "" - -#: shifts/forms.py:266 -msgid "You need the shifts.manage permission to do this." -msgstr "" - -#: shifts/forms.py:270 -msgid "This user is already registered to another slot in this shift." -msgstr "" - -#: shifts/forms.py:286 -msgid "I am aware that the member will be unregistered from their ABCD shift" -msgstr "" - -#: shifts/forms.py:295 shifts/templates/shifts/user_shifts_overview_tag.html:93 -msgid "Qualifications" -msgstr "" - -#: shifts/forms.py:323 -#, python-brace-format -msgid "{own_name} is already partner of {partner_of_name}, they can't have a partner of their own" -msgstr "" - -#: shifts/forms.py:383 -msgid "This member is registered to at least one ABCD shift. Please confirm the change of attendance mode with the checkbox below." -msgstr "" - -#: shifts/forms.py:428 -msgid "I have read the warning about the cancelled attendances and confirm that the exemption should be created" -msgstr "" - -#: shifts/forms.py:435 -msgid "I have read the warning about the cancelled ABCD attendances and confirm that the exemption should be created" -msgstr "" - -#: shifts/forms.py:468 -#, python-brace-format -msgid "The member will be unregistered from the following shifts because they are within the range of the exemption : {attendances_display}" -msgstr "" - -#: shifts/forms.py:493 -#, python-format -msgid "The user will be unregistered from the following ABCD shifts because the exemption is longer than %(number_of_cycles)s cycles: %(attendances_display)s " -msgstr "" - -#: shifts/forms.py:536 -msgid "I understand that updating this ABCD shift will update all the corresponding future shifts" -msgstr "" - -#: shifts/forms.py:557 -msgid "I understand that adding or editing a slot to this ABCD shift will affect all the corresponding future shifts" -msgstr "" - -#: shifts/forms.py:566 -msgid "I understand that this will delete the shift exemption and create a membership pause" -msgstr "" - -#: shifts/models.py:38 -msgid "Teamleader" -msgstr "" - -#: shifts/models.py:39 -msgid "Cashier" -msgstr "" - -#: shifts/models.py:40 -msgid "Member Office" -msgstr "" - -#: shifts/models.py:41 -msgid "Bread Delivery" -msgstr "" - -#: shifts/models.py:42 -msgid "Red Card" -msgstr "" - -#: shifts/models.py:43 -msgid "First Aid" -msgstr "" - -#: shifts/models.py:45 -msgid "Handling Cheese" -msgstr "" - -#: shifts/models.py:46 -msgid "Train cheese handlers" -msgstr "" - -#: shifts/models.py:47 -msgid "Inventory" -msgstr "" - -#: shifts/models.py:48 -msgid "Nebenan.de-Support" -msgstr "" - -#: shifts/models.py:62 -msgid "I understand that all working groups help the Warenannahme & Lager working group until the shop opens." -msgstr "" - -#: shifts/models.py:65 -msgid "I understand that all working groups help the Reinigung & Aufräumen working group after the shop closes." -msgstr "" - -#: shifts/models.py:68 -msgid "I understand that I need my own vehicle in order to pick up the bread. A cargo bike can be borrowed, more infos in Slack in the #cargobike channel" -msgstr "" - -#: shifts/models.py:71 -msgid "I understand that I may need to carry heavy weights for this shift." -msgstr "" - -#: shifts/models.py:74 -msgid "I understand that I may need to work high, for example up a ladder. I do not suffer from fear of heights." -msgstr "" - -#: shifts/models.py:185 -msgid "This determines from which date shifts should be generated from this ABCD shift." -msgstr "" - -#: shifts/models.py:189 shifts/models.py:521 -#: shifts/templates/shifts/shift_block_tag.html:9 -msgid "Flexible time" -msgstr "" - -#: shifts/models.py:191 shifts/models.py:523 -msgid "If enabled, members who register for that shift can choose themselves the time where they come do their shift." -msgstr "" - -#: shifts/models.py:417 shifts/models.py:836 -#: shifts/templates/shifts/shift_detail.html:86 -#: shifts/templates/shifts/shift_template_detail.html:42 -msgid "Chosen time" -msgstr "" - -#: shifts/models.py:419 -msgid "This shift lets you choose at what time you come during the day of the shift. In order to help organising the attendance, please specify when you expect to come.Setting or updating this field will set the time for all individual shifts generated from this ABCD shift.You can update the time of a single shift individually and at any time on the shift page." -msgstr "" - -#: shifts/models.py:503 -msgid "Number of required attendances" -msgstr "" - -#: shifts/models.py:505 -msgid "If there are less members registered to a shift than that number, it will be highlighted in the shift calendar." -msgstr "" - -#: shifts/models.py:514 -msgid "Is shown on the shift page below the title" -msgstr "" - -#: shifts/models.py:534 shifts/models.py:540 -msgid "If 'flexible time' is enabled, then the time component is ignored" -msgstr "" - -#: shifts/models.py:838 -msgid "This shift lets you choose at what time you come during the day of the shift. In order to help organising the attendance, please specify when you expect to come." -msgstr "" - -#: shifts/models.py:946 shifts/templates/shifts/shift_day_printable.html:57 -#: shifts/templates/shifts/shift_detail.html:282 -#: shifts/templates/shifts/shift_detail_printable.html:52 -msgid "Missed" -msgstr "" - -#: shifts/models.py:947 shifts/templates/shifts/shift_day_printable.html:58 -#: shifts/templates/shifts/shift_detail.html:312 -#: shifts/templates/shifts/shift_detail_printable.html:53 -msgid "Excused" -msgstr "" - -#: shifts/models.py:948 shifts/templates/shifts/shift_detail.html:320 -msgid "Cancelled" -msgstr "" - -#: shifts/models.py:949 shifts/templates/shifts/shift_day_printable.html:97 -#: shifts/templates/shifts/shift_detail.html:304 -#: shifts/templates/shifts/shift_detail_printable.html:94 -#: shifts/templates/shifts/shift_filters.html:83 -msgid "Looking for a stand-in" -msgstr "" - -#: shifts/models.py:982 -msgid "🏠 ABCD" -msgstr "" - -#: shifts/models.py:983 -msgid "✈ Flying" -msgstr "" - -#: shifts/models.py:984 -msgid "❄ Frozen" -msgstr "" - -#: shifts/models.py:1015 -msgid "Is frozen" -msgstr "" - -#: shifts/models.py:1189 -msgid "Cycle start date" -msgstr "" - -#: shifts/templates/shifts/cancel_shift.html:7 -#: shifts/templates/shifts/cancel_shift.html:13 -msgid "Cancel shift:" -msgstr "" - -#: shifts/templates/shifts/cancel_shift.html:17 -msgid "" -"\n" -" Cancelling a shift is used for example for holidays. It has the following consequences :\n" -"
    \n" -"
  • It is not possible to register to the shift anymore.
  • \n" -"
  • Members who are registered from their ABCD shift get a shift point.
  • \n" -"
  • Members who registered just to this shift don't get a point.
  • \n" -"
\n" -" " -msgstr "" - -#: shifts/templates/shifts/cancel_shift.html:32 -msgid "Confirm cancellation" -msgstr "" - -#: shifts/templates/shifts/convert_exemption_to_pause_form.html:12 -#: shifts/templates/shifts/convert_exemption_to_pause_form.html:18 -msgid "Convert exemption to pause" -msgstr "" - -#: shifts/templates/shifts/convert_exemption_to_pause_form.html:22 -#, python-format -msgid "" -"\n" -"
    \n" -"
  • Start date: %(start_date)s
  • \n" -"
  • End date: %(end_date)s
  • \n" -"
  • Description: %(description)s
  • \n" -"
\n" -" " -msgstr "" - -#: shifts/templates/shifts/email/flying_member_registration_reminder_email.body.html:6 -#, python-format -msgid "" -"\n" -"

Hi %(display_name_short)s,

\n" -"\n" -"

\n" -" we would like to remind you about your next shift\n" -" registration.\n" -"

\n" -"

\n" -" A new shift cycle has been running for a week and we have noticed\n" -" that you have not yet registered for a suitable shift. As a flying member,\n" -" unlike members with a regular ABCD shift, you have to take care of this yourself every four weeks.\n" -"

\n" -"

\n" -" So take a look at the shift calendar on Tapir\n" -" and register for one of the shifts highlighted in blue - this is where the most support is currently needed.\n" -"

\n" -"

\n" -" Cooperative greetings,
\n" -" The Member Office\n" -"

\n" -" " -msgstr "" - -#: shifts/templates/shifts/email/flying_member_registration_reminder_email.subject.html:2 -msgid "Sign up for your next SuperCoop shift" -msgstr "" - -#: shifts/templates/shifts/email/freeze_warning.body.html:6 -#, python-format -msgid "" -"\n" -"

\n" -" Hi %(display_name_short)s,\n" -"

\n" -"\n" -"

\n" -" This is an automated email from %(coop_name)s. This is to kindly inform you that your shift log has\n" -" reached %(account_balance)s.\n" -"

\n" -"\n" -"

\n" -" You now have %(freeze_after_days)s days to register for the outstanding shifts as make-up shifts. These shifts\n" -" must be within the next %(nb_weeks_in_the_future)s weeks.\n" -"

\n" -"

\n" -" Please note: If you do not sign up for your catch-up shifts within the next %(freeze_after_days)s days,\n" -" your account will be frozen. This means you will not be able to shop or vote in general meetings. To\n" -" unfreeze your account, you will need to sign up for your outstanding shifts.\n" -"

\n" -"

\n" -" If you are an ABCD member and do not complete your catch-up shifts, nor your regular shifts, you will be\n" -" removed from your ABCD shift.\n" -"

\n" -"

\n" -" So please sign up for your outstanding shifts as soon as possible. For more information on the shift system,\n" -" please see the Member Manual.\n" -"

\n" -"

\n" -" Thank you!\n" -"

\n" -"

\n" -" Cooperative greetings,
\n" -" The member office\n" -"

\n" -" " -msgstr "" - -#: shifts/templates/shifts/email/freeze_warning.subject.html:2 -#, python-format -msgid "" -"\n" -" Your shift log has reached %(account_balance)s shifts\n" -msgstr "" - -#: shifts/templates/shifts/email/member_frozen.body.html:6 -#, python-format -msgid "" -"\n" -"

Hi %(display_name_short)s,

\n" -"

\n" -" This is an automated email from %(coop_name)s. This is to kindly inform you that your Tapir account at\n" -" SuperCoop has been temporarily frozen. Unfortunately, your shift log has been below %(freeze_threshold)s\n" -" shifts for more than %(freeze_after_days)s days.\n" -"

\n" -"

\n" -" While your account is frozen, you cannot shop or vote in the General Assembly.\n" -"

\n" -"

\n" -" To unfreeze your account, simply sign up for your outstanding shifts as catch-up shifts. Please note that\n" -" these shifts must be within the next %(nb_weeks_in_the_future_for_make_up_shifts)s weeks.\n" -"

\n" -"

\n" -" So please sign up for your outstanding shifts as soon as possible. For more information on the shift system,\n" -" please see the Member Manual.\n" -"

\n" -"

\n" -" Thank you very much!\n" -"

\n" -"

\n" -" Cooperative greetings,
\n" -" The Member Office\n" -"

\n" -" " -msgstr "" - -#: shifts/templates/shifts/email/member_frozen.subject.html:2 -msgid "" -"\n" -" Your Tapir account has been frozen\n" -msgstr "" - -#: shifts/templates/shifts/email/shift_missed.body.html:6 -#, python-format -msgid "" -"\n" -"

Hi %(display_name_short)s,

\n" -"\n" -"

This is an automated email from SuperCoop.

\n" -"\n" -"

\n" -" It has been registered into Tapir that you did not show up for the following shift even though you were\n" -" registered for it: %(shift_display_name)s\n" -"

\n" -"

\n" -" If this is an error, such as you were excused for the shift, please contact the Member Office as soon as\n" -" possible by simply replying to this email.\n" -"

\n" -"

\n" -" In order for the shift system to work in case of absences, the stand-in system (an internal substitution\n" -" system) has been set up. This is to relieve both you and your team members. You can find out exactly how the\n" -" substitution system works in the\n" -" Member\n" -" Manual. Please note that it is your responsibility to actively look for a shift substitute, this\n" -" includes an entry in Tapir as well as actively searching for a substitute e.g. via Slack.\n" -"

\n" -"

\n" -" As a general rule, a member is required to work two make-up shifts for each missed shift for which a\n" -" replacement could not be found. Your Tapir account now contains a note to that effect.\n" -"

\n" -"

\n" -" We ask that you sign up for your make-up shifts in Tapir within the next four weeks to balance your account. Failure to do so can result in the pausing of your purchasing and voting rights until you begin actively working again (see Member\n" -" Manual (Section III.F)\n" -"

\n" -"

Thank you for your understanding.

\n" -"

\n" -" Cooperative Greetings,
\n" -" The Membership Office\n" -"

\n" -" " -msgstr "" - -#: shifts/templates/shifts/email/shift_missed.subject.html:2 -msgid "" -"\n" -" You missed your shift!\n" -msgstr "" - -#: shifts/templates/shifts/email/shift_reminder.body.html:6 -#, python-format -msgid "" -"\n" -"

Hello %(display_name_short)s,

\n" -"\n" -"

We'll see you on %(shift_display_name)s

\n" -"
\n" -" If you can't make it to your shift:\n" -"
    \n" -"
  1. Please set yourself to ‘Looking for a stand-in’ in Tapir
  2. \n" -"
  3. Register for another shift
  4. \n" -"
  5. Contact your team leader or the Member Office within the next 48 hours. This way you avoid an additional catch-up shift.
  6. \n" -"
\n" -"
\n" -"

\n" -"

If you have already contacted your team leader and/or the Member Office about this matter, you can ignore this message.

\n" -"

If you can't be on time for your shift: Please come anyway, any support is important!

\n" -"

You can find more information about the stand-in system in the member manual.

\n" -"

\n" -" Cooperative greetings,
\n" -" The Member Office\n" -"

\n" -" " -msgstr "" - -#: shifts/templates/shifts/email/shift_reminder.subject.html:2 -#, python-format -msgid "" -"\n" -" Your upcoming %(coop_name)s shift: %(shift_name)s\n" -msgstr "" - -#: shifts/templates/shifts/email/stand_in_found.body.html:6 -#, python-format -msgid "" -"\n" -"

Dear %(display_name_short)s,

\n" -"\n" -"

This is an automated email from %(coop_name)s.

\n" -"

\n" -" You were looking for a stand-in for the following shift: %(shift_display_name)s.
\n" -" Another member has taken over your slot for that shift.\n" -"

\n" -"\n" -"

If you haven't done so already, don't forget to attend another shift to compensate for the one you will miss!

\n" -"

\n" -" Cooperative Greetings,
\n" -" The member office\n" -"

\n" -" " -msgstr "" - -#: shifts/templates/shifts/email/stand_in_found.subject.html:2 -msgid "" -"\n" -" You found a stand-in!\n" -msgstr "" - -#: shifts/templates/shifts/email/unfreeze_notification.body.html:6 -#, python-format -msgid "" -"\n" -"

Hi %(display_name_short)s,

\n" -"

\n" -" This is an automatic email from %(coop_name)s. This is to inform you that your Tapir account at %(coop_name)s\n" -" has been reactivated.\n" -"

\n" -"

\n" -" Your shopping and voting rights have been reactivated and you can now shop at %(coop_name)s and vote at\n" -" general meetings again.\n" -"

\n" -"

\n" -" In the future, please make sure to catch up on your missed shifts in a timely manner.\n" -"

\n" -"

\n" -" For more information on the shift system, please see the Member Manual.\n" -"

\n" -"

\n" -" Cooperative Greetings,
\n" -" The member office\n" -"

\n" -" " -msgstr "" - -#: shifts/templates/shifts/email/unfreeze_notification.subject.html:2 -msgid "" -"\n" -" Your Tapir account has been unfrozen.\n" -msgstr "" - -#: shifts/templates/shifts/log/create_exemption_log_entry.html:2 -msgid "without end date" -msgstr "" - -#: shifts/templates/shifts/log/create_exemption_log_entry.html:4 -#, python-format -msgid "Added exemption starting %(start_date)s until %(ending)s" -msgstr "" - -#: shifts/templates/shifts/log/create_shift_attendance_log_entry.html:2 -msgid "Registered for Shift" -msgstr "" - -#: shifts/templates/shifts/log/create_shift_attendance_template_log_entry.html:2 -msgid "Registered for ABCD Shift" -msgstr "" - -#: shifts/templates/shifts/log/delete_shift_attendance_template_log_entry.html:2 -msgid "Unregistered from ABCD Shift" -msgstr "" - -#: shifts/templates/shifts/log/shift_attendance_taken_over_log_entry.html:2 -msgid "Someone took over your shift" -msgstr "" - -#: shifts/templates/shifts/log/update_exemption_log_entry.html:2 -msgid "Update Exemption" -msgstr "" - -#: shifts/templates/shifts/log/update_shift_attendance_state_log_entry.html:2 -msgid "Set shift attendance to" -msgstr "" - -#: shifts/templates/shifts/log/update_shift_user_data_log_entry.html:2 -msgid "Update Shift User Data" -msgstr "" - -#: shifts/templates/shifts/members_on_alert_list.html:10 -#: shifts/templates/shifts/members_on_alert_list.html:15 -#: shifts/templates/shifts/shift_management.html:33 -msgid "Members on alert" -msgstr "" - -#: shifts/templates/shifts/members_on_alert_list.html:31 -msgid "" -"\n" -" The members in this list are on alert relative to their shift account : the current balance is -2\n" -" or less.\n" -" " -msgstr "" - -#: shifts/templates/shifts/register_user_to_shift_slot.html:7 -#: shifts/templates/shifts/register_user_to_shift_slot.html:16 -#: shifts/templates/shifts/register_user_to_shift_slot_template.html:7 -msgid "Register for shift slot" -msgstr "" - -#: shifts/templates/shifts/register_user_to_shift_slot_template.html:16 -msgid "Register for ABCD Shift" -msgstr "" - -#: shifts/templates/shifts/register_user_to_shift_slot_template.html:34 -msgid "Some shifts are already occupied" -msgstr "" - -#: shifts/templates/shifts/register_user_to_shift_slot_template.html:36 -msgid "" -"\n" -"

A flying member has already registered to the shifts linked below.

\n" -"

It is still possible to sign someone up for this shift using the ABCD system. This would mean that the member is signing up to work this shift on a recurring 4-week basis (i.e. once per shift cycle).

\n" -"

IMPORTANT: If a flying member had previously signed up for the same shift, the ABCD member will have to sign up for a different shift for that particular cycle.

\n" -" " -msgstr "" - -#: shifts/templates/shifts/register_user_to_shift_slot_template.html:42 -msgid "Already occupied shifts :" -msgstr "" - -#: shifts/templates/shifts/shift_calendar_future.html:10 -#, python-format -msgid "" -"\n" -"

Below you can find all the upcoming shifts, and who is registered for each shift.

\n" -"

You can sign yourself up for any shift that has a free slot here on TAPIR. You may also cancel it yourself if done at least %(nb_days_for_self_unregister)s days before the shift.
\n" -" When you sign up for one of these shifts, you only commit to the shift on that specific day.

\n" -"

To register for a regular ABCD shift you must contact the Member Office.

\n" -" " -msgstr "" - -#: shifts/templates/shifts/shift_calendar_past.html:4 -msgid "Past shifts" -msgstr "" - -#: shifts/templates/shifts/shift_calendar_template.html:27 -msgid "Legend & Filters" -msgstr "" - -#: shifts/templates/shifts/shift_calendar_template.html:50 -msgid "starting" -msgstr "" - -#: shifts/templates/shifts/shift_calendar_template.html:51 -#: shifts/templates/shifts/shift_template_overview.html:40 -msgid "Week " -msgstr "" - -#: shifts/templates/shifts/shift_calendar_template.html:55 -msgid "From" -msgstr "" - -#: shifts/templates/shifts/shift_calendar_template.html:62 -msgid "To" -msgstr "" - -#: shifts/templates/shifts/shift_day_printable.html:54 -#: shifts/templates/shifts/shift_detail.html:79 -#: shifts/templates/shifts/shift_detail_printable.html:49 -msgid "Slot" -msgstr "" - -#: shifts/templates/shifts/shift_day_printable.html:72 -#: shifts/templates/shifts/shift_detail_printable.html:68 -msgid "Required qualifications: " -msgstr "" - -#: shifts/templates/shifts/shift_day_printable.html:86 -#: shifts/templates/shifts/shift_detail_printable.html:82 -msgid "Expected to come at" -msgstr "" - -#: shifts/templates/shifts/shift_day_printable.html:88 -#: shifts/templates/shifts/shift_detail_printable.html:84 -msgid "Flexible time not specified" -msgstr "" - -#: shifts/templates/shifts/shift_day_printable.html:94 -#: shifts/templates/shifts/shift_detail.html:113 -#: shifts/templates/shifts/shift_detail_printable.html:90 -#: shifts/templates/shifts/shift_template_detail.html:64 -msgid "Shift partner: " -msgstr "" - -#: shifts/templates/shifts/shift_day_printable.html:104 -#: shifts/templates/shifts/shift_detail_printable.html:100 -msgid "Has qualifications: " -msgstr "" - -#: shifts/templates/shifts/shift_detail.html:7 -#: shifts/templates/shifts/shift_detail.html:29 -msgid "Shift" -msgstr "" - -#: shifts/templates/shifts/shift_detail.html:35 -msgid "Get printable version" -msgstr "" - -#: shifts/templates/shifts/shift_detail.html:39 -#: shifts/templates/shifts/shift_template_detail.html:20 -msgid "Add a slot" -msgstr "" - -#: shifts/templates/shifts/shift_detail.html:49 -msgid "Cancel shift" -msgstr "" - -#: shifts/templates/shifts/shift_detail.html:57 -msgid "Generated from" -msgstr "" - -#: shifts/templates/shifts/shift_detail.html:67 -msgid "This shift has been cancelled: " -msgstr "" - -#: shifts/templates/shifts/shift_detail.html:76 -msgid "List of slots for this shift" -msgstr "" - -#: shifts/templates/shifts/shift_detail.html:79 -msgid "Number" -msgstr "" - -#: shifts/templates/shifts/shift_detail.html:80 -#: shifts/templates/shifts/shift_template_detail.html:38 -msgid "Details" -msgstr "" - -#: shifts/templates/shifts/shift_detail.html:81 -#: shifts/templates/shifts/shift_template_detail.html:40 -msgid "Registered user" -msgstr "" - -#: shifts/templates/shifts/shift_detail.html:82 -msgid "Attendance" -msgstr "" - -#: shifts/templates/shifts/shift_detail.html:84 -msgid "Do you meet the requirements?" -msgstr "" - -#: shifts/templates/shifts/shift_detail.html:89 -#: shifts/templates/shifts/shift_template_detail.html:45 -msgid "Member-Office actions" -msgstr "" - -#: shifts/templates/shifts/shift_detail.html:90 -msgid "Previous attendances" -msgstr "" - -#: shifts/templates/shifts/shift_detail.html:121 -msgid "since" -msgstr "" - -#: shifts/templates/shifts/shift_detail.html:134 -msgid "Vacant" -msgstr "" - -#: shifts/templates/shifts/shift_detail.html:151 -msgid "Cancels the search for a stand-in. Use this if you want to attend the shift." -msgstr "" - -#: shifts/templates/shifts/shift_detail.html:153 -#: shifts/templates/shifts/shift_detail.html:293 -msgid "Cancel looking for a stand-in" -msgstr "" - -#: shifts/templates/shifts/shift_detail.html:161 -#, python-format -msgid "" -"You can only look for\n" -" a\n" -" stand-in\n" -" %(NB_DAYS_FOR_SELF_LOOK_FOR_STAND_IN)s days before the\n" -" shift. If\n" -" you can't\n" -" attend, contact your shift leader as soon as\n" -" possible." -msgstr "" - -#: shifts/templates/shifts/shift_detail.html:174 -msgid "Look for a stand-in" -msgstr "" - -#: shifts/templates/shifts/shift_detail.html:183 -#, python-format -msgid "" -"You can only\n" -" unregister\n" -" yourself\n" -" %(NB_DAYS_FOR_SELF_UNREGISTER)s days before the shift. Also,\n" -" ABCD-Shifts\n" -" can't be unregistered from. If you can't\n" -" attend, look for a stand-in or contact your shift leader as soon\n" -" as\n" -" possible." -msgstr "" - -#: shifts/templates/shifts/shift_detail.html:198 -msgid "Unregister myself" -msgstr "" - -#: shifts/templates/shifts/shift_detail.html:203 -msgid "" -"You can only register\n" -" yourself\n" -" for a shift if:
\n" -" -You are not registered to another slot in that shift
\n" -" -You have the required qualification (if you want to get a\n" -" qualification, contact the member office)
\n" -" -The shift is in the future
\n" -" -The shift is not cancelled (holidays...)
\n" -" " -msgstr "" - -#: shifts/templates/shifts/shift_detail.html:245 -#: shifts/templates/shifts/shift_template_detail.html:95 -msgid "Not specified" -msgstr "" - -#: shifts/templates/shifts/shift_detail.html:328 -msgid "Edit slot" -msgstr "" - -#: shifts/templates/shifts/shift_filters.html:7 -msgid "Use the buttons below to filter the shifts that are relevant to you." -msgstr "" - -#: shifts/templates/shifts/shift_filters.html:16 -msgid "No filter" -msgstr "" - -#: shifts/templates/shifts/shift_filters.html:23 -msgid "Has a free slot" -msgstr "" - -#: shifts/templates/shifts/shift_filters.html:30 -msgid "Most help needed" -msgstr "" - -#: shifts/templates/shifts/shift_filters.html:38 -msgid "Filter for type of slot" -msgstr "" - -#: shifts/templates/shifts/shift_filters.html:60 -msgid "Legend" -msgstr "" - -#: shifts/templates/shifts/shift_filters.html:68 -msgctxt "Filters" -msgid "Other shifts" -msgstr "" - -#: shifts/templates/shifts/shift_filters.html:71 -msgctxt "Filters" -msgid "Cancelled" -msgstr "" - -#: shifts/templates/shifts/shift_filters.html:74 -msgid "Free slot" -msgstr "" - -#: shifts/templates/shifts/shift_filters.html:77 -msgid "ABCD attendance" -msgstr "" - -#: shifts/templates/shifts/shift_filters.html:80 -msgid "Flying attendance" -msgstr "" - -#: shifts/templates/shifts/shift_form.html:20 -msgid "" -"\n" -"

Be careful when creating a shift, as you won't be able to delete it afterwards.

\n" -"

Slots can be added to the shift after it has been created.

\n" -" " -msgstr "" - -#: shifts/templates/shifts/shift_management.html:16 -msgid "Add shifts" -msgstr "" - -#: shifts/templates/shifts/shift_management.html:20 -msgid "Add a shift" -msgstr "" - -#: shifts/templates/shifts/shift_management.html:24 -msgid "Add an ABCD shift" -msgstr "" - -#: shifts/templates/shifts/shift_management.html:37 -#: shifts/templates/shifts/shiftexemption_list.html:10 -#: shifts/templates/shifts/shiftexemption_list.html:19 -msgid "Shift exemptions" -msgstr "" - -#: shifts/templates/shifts/shift_management.html:44 -#: shifts/templates/shifts/shift_management.html:56 -msgid "Update frozen statuses" -msgstr "" - -#: shifts/templates/shifts/shift_management.html:47 -msgid "" -"\n" -"

For every member, checks if they should get the frozen status, if they should get\n" -" unfrozen, or if\n" -" they should get the warning that they will be frozen soon.

\n" -"

This command is run automatically once per day. You can trigger it here manually for test\n" -" purposes or to accelerate a status update.

\n" -" " -msgstr "" - -#: shifts/templates/shifts/shift_management.html:63 -msgid "Generate shifts from ABCD shifts" -msgstr "" - -#: shifts/templates/shifts/shift_management.html:66 -msgid "" -"\n" -"

This generates shifts for the coming 200 days according to the ABCD-shift-calendar.

\n" -"

This command is run automatically once per day. You can trigger it manually to see results\n" -" faster.

\n" -" " -msgstr "" - -#: shifts/templates/shifts/shift_management.html:73 -msgid "Generate shifts" -msgstr "" - -#: shifts/templates/shifts/shift_management.html:79 -#: shifts/templates/shifts/shift_management.html:87 -msgid "Old shift statistics" -msgstr "" - -#: shifts/templates/shifts/shift_management.html:82 -msgid "" -"\n" -"

This statistics have been hidden from members in order to focus on the more important ones.

\n" -"

They may not be relevant or well presented.

\n" -" " -msgstr "" - -#: shifts/templates/shifts/shift_template_detail.html:7 -#: shifts/templates/shifts/shift_template_detail.html:15 -#: shifts/templates/shifts/user_shifts_overview_tag.html:26 -#: shifts/templates/shifts/user_shifts_overview_tag.html:38 -msgid "ABCD Shift" -msgstr "" - -#: shifts/templates/shifts/shift_template_detail.html:34 -msgid "List of slots for this ABCD shifts" -msgstr "" - -#: shifts/templates/shifts/shift_template_detail.html:39 -msgid "Requirements" -msgstr "" - -#: shifts/templates/shifts/shift_template_detail.html:73 -msgid "Unregister" -msgstr "" - -#: shifts/templates/shifts/shift_template_detail.html:122 -msgid "Future generated Shifts" -msgstr "" - -#: shifts/templates/shifts/shift_template_detail.html:132 -msgid "Past generated Shifts" -msgstr "" - -#: shifts/templates/shifts/shift_template_group_calendar.html:9 -#: shifts/templates/shifts/shift_template_group_calendar.html:18 -msgid "ABCD weeks calendar" -msgstr "" - -#: shifts/templates/shifts/shift_template_group_calendar.html:27 -msgid "as pdf" -msgstr "" - -#: shifts/templates/shifts/shift_template_overview.html:12 -msgid "ABCD week-plan" -msgstr "" - -#: shifts/templates/shifts/shift_template_overview.html:19 -msgid "ABCD schedule overview, current week: " -msgstr "" - -#: shifts/templates/shifts/shift_template_overview.html:22 -msgid "" -"\n" -"

This is the ABCD shiftplan. It repeats every four weeks. That's why you see only weekdays\n" -" (Monday, Tuesday...) and the week (A,B,C or D), instead of specific dates (e.g. 23.8.2021).\n" -" To see the date of your next shift, please go to your profile.

\n" -"

If you would like to change your regular ABCD shift, please contact the Member Office.

\n" -" " -msgstr "" - -#: shifts/templates/shifts/shift_template_overview.html:35 -msgid "Calendar of ABCD shifts" -msgstr "" - -#: shifts/templates/shifts/shiftexemption_list.html:38 -msgid "Create shift exemption" -msgstr "" - -#: shifts/templates/shifts/shiftexemption_list.html:58 -#, python-format -msgid "" -"\n" -" Filtered %(filtered_exemption_count)s of %(total_exemption_count)s\n" -" " -msgstr "" - -#: shifts/templates/shifts/shiftslot_form.html:20 -msgid "" -"\n" -" Be careful when creating a shift slot, as you won't be able to delete it afterwards.\n" -" " -msgstr "" - -#: shifts/templates/shifts/solidarity_shifts_overview.html:14 -msgid "Available" -msgstr "" - -#: shifts/templates/shifts/solidarity_shifts_overview.html:17 -#, python-format -msgid "" -"\n" -"

There is %(available_solidarity_shifts)s solidarity shift available at the moment

\n" -" " -msgstr "" - -#: shifts/templates/shifts/solidarity_shifts_overview.html:21 -#, python-format -msgid "" -"\n" -"

There are %(available_solidarity_shifts)s solidarity shifts available at the moment

\n" -" " -msgstr "" - -#: shifts/templates/shifts/solidarity_shifts_overview.html:30 -msgid "Used" -msgstr "" - -#: shifts/templates/shifts/solidarity_shifts_overview.html:33 -#, python-format -msgid "" -"\n" -"

%(used_solidarity_shifts_total)s solidarity shift has been used in total

\n" -" " -msgstr "" - -#: shifts/templates/shifts/solidarity_shifts_overview.html:37 -#, python-format -msgid "" -"\n" -"

%(used_solidarity_shifts_total)s solidarity shifts have been used in total

\n" -" " -msgstr "" - -#: shifts/templates/shifts/solidarity_shifts_overview.html:41 -#: shifts/views/solidarity.py:208 -msgid "Solidarity shifts used" -msgstr "" - -#: shifts/templates/shifts/solidarity_shifts_overview.html:50 -msgid "Gifted" -msgstr "" - -#: shifts/templates/shifts/solidarity_shifts_overview.html:53 -#, python-format -msgid "" -"\n" -"

%(gifted_solidarity_shifts_total)s solidarity shift has been gifted in total

\n" -" " -msgstr "" - -#: shifts/templates/shifts/solidarity_shifts_overview.html:57 -#, python-format -msgid "" -"\n" -"

%(gifted_solidarity_shifts_total)s solidarity shifts have been gifted in total

\n" -" " -msgstr "" - -#: shifts/templates/shifts/solidarity_shifts_overview.html:61 -#: shifts/views/solidarity.py:179 -msgid "Solidarity shifts gifted" -msgstr "" - -#: shifts/templates/shifts/statistics.html:9 -msgid "Shift statistics" -msgstr "" - -#: shifts/templates/shifts/statistics.html:66 -msgid "Statistics on shifts" -msgstr "" - -#: shifts/templates/shifts/statistics.html:81 -msgid "Gives details of the attendance per slot type." -msgstr "" - -#: shifts/templates/shifts/statistics.html:107 -msgid "Gives details of the attendance for the past, current and next week." -msgstr "" - -#: shifts/templates/shifts/statistics.html:131 -msgid "Shift cycle list" -msgstr "" - -#: shifts/templates/shifts/statistics.html:135 -msgid "Gives details of the attendance per shift cycle." -msgstr "" - -#: shifts/templates/shifts/statistics.html:158 -msgid "Data exports" -msgstr "" - -#: shifts/templates/shifts/statistics.html:164 -msgid "Slot data" -msgstr "" - -#: shifts/templates/shifts/statistics.html:169 -msgid "ABCD shift data" -msgstr "" - -#: shifts/templates/shifts/statistics.html:174 -msgid "ABCD shift slot data" -msgstr "" - -#: shifts/templates/shifts/statistics.html:179 -msgid "Shift data" -msgstr "" - -#: shifts/templates/shifts/statistics.html:184 -msgid "Shift slot data" -msgstr "" - -#: shifts/templates/shifts/statistics.html:189 -msgid "ABCD attendance data" -msgstr "" - -#: shifts/templates/shifts/statistics.html:194 -msgid "Attendance data" -msgstr "" - -#: shifts/templates/shifts/statistics.html:199 -msgid "Attendance updates data" -msgstr "" - -#: shifts/templates/shifts/statistics.html:204 -msgid "Attendance takeover data" -msgstr "" - -#: shifts/templates/shifts/statistics.html:212 -msgid "Evolution of shift status" -msgstr "" - -#: shifts/templates/shifts/statistics.html:215 -msgid "Shift status evolution" -msgstr "" - -#: shifts/templates/shifts/user_shift_account_log.html:10 -msgid "Shift account log for: " -msgstr "" - -#: shifts/templates/shifts/user_shift_account_log.html:16 -msgid "Add a manual entry" -msgstr "" - -#: shifts/templates/shifts/user_shift_account_log.html:23 -msgid "List of changes to this members shift account " -msgstr "" - -#: shifts/templates/shifts/user_shift_account_log.html:28 -msgid "Balance at date" -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:45 -msgid "Find an ABCD shift" -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:52 -msgid "Upcoming Shift" -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:59 -msgid "Show more" -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:68 -msgctxt "No upcoming shift" -msgid "None" -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:77 -msgid "OK" -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:79 -msgid "Shift for ongoing cycle pending" -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:81 -#, python-format -msgid "" -"\n" -" %(num_banked_shifts)s banked shifts\n" -" " -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:86 -msgid "On alert" -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:89 -msgid "log" -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:98 -msgctxt "No qualifications" -msgid "None" -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:103 -msgid "Exemption" -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:108 -msgid "until" -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:115 -msgctxt "No shift exemption" -msgid "None" -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:123 -msgid "View all" -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:130 -msgid "Solidarity" -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:137 -msgid "Receive Solidarity" -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:147 -msgid "Give Solidarity" -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:167 -msgid "One of your banked shifts will be donated as a solidarity shift. Do you want to continue?" -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:172 -msgid "Cancel" -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:175 -msgid "Confirm" -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:185 -msgid "Solidarity Status" -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:189 -#, python-format -msgid "" -"\n" -"

You already used %(used_solidarity_shifts_current_year)s out of 2 Solidarity Shifts\n" -" this year

\n" -" " -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:194 -#, python-format -msgid "" -"\n" -"

There are Solidarity Shifts available for you to use. You\n" -" used %(used_solidarity_shifts_current_year)s out of 2 Solidarity Shifts this year

\n" -" " -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:199 -msgid "" -"\n" -"

\n" -" You cannot receive a Solidarity Shift at the moment

\n" -" " -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:204 -msgid "" -"\n" -"

There are no Solidarity Shifts available at the moment

\n" -" " -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:213 -msgid "Shift partner" -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:220 -msgid "" -"\n" -" You can email the member office to ask for a shift partner to be registered.
\n" -" See member manual section III.F.6\n" -" " -msgstr "" - -#: shifts/templates/shifts/user_shifts_overview_tag.html:225 -msgctxt "No shift partner" -msgid "None" -msgstr "" - -#: shifts/templatetags/shifts.py:61 shifts/templatetags/shifts.py:165 -#: shifts/views/views.py:144 -msgid "General" -msgstr "" - -#: shifts/utils.py:166 -#, python-brace-format -msgid "Unknown mode {attendance_mode}" -msgstr "" - -#: shifts/views/attendance.py:194 shifts/views/attendance.py:336 -#, python-format -msgid "Shift attendance: %(name)s" -msgstr "" - -#: shifts/views/attendance.py:200 shifts/views/attendance.py:342 -#, python-format -msgid "Updating shift attendance: %(member_link)s, %(slot_link)s" -msgstr "" - -#: shifts/views/attendance.py:376 -#, python-format -msgid "ABCD attendance: %(name)s" -msgstr "" - -#: shifts/views/attendance.py:383 -#, python-format -msgid "Updating ABCD attendance: %(member_link)s, %(slot_link)s" -msgstr "" - -#: shifts/views/exemptions.py:83 -msgid "Is covered by shift exemption: " -msgstr "" - -#: shifts/views/exemptions.py:108 shifts/views/exemptions.py:158 -#, python-format -msgid "Shift exemption: %(name)s" -msgstr "" - -#: shifts/views/exemptions.py:113 -#, python-format -msgid "Create shift exemption for: %(link)s" -msgstr "" - -#: shifts/views/exemptions.py:163 -#, python-format -msgid "Edit shift exemption for: %(link)s" -msgstr "" - -#: shifts/views/exemptions.py:295 -msgid "Shift exemption converted to membership pause." -msgstr "" - -#: shifts/views/management.py:36 -msgid "Create a shift" -msgstr "" - -#: shifts/views/management.py:76 -msgid "Edit slot: " -msgstr "" - -#: shifts/views/management.py:120 -msgid "Edit shift: " -msgstr "" - -#: shifts/views/management.py:134 -msgid "Edit shift template: " -msgstr "" - -#: shifts/views/management.py:153 -msgid "Create an ABCD shift" -msgstr "" - -#: shifts/views/management.py:155 -msgid "Shifts are generated every day at midnight. After you created the ABCD shift, come back tomorrow to see your shifts!" -msgstr "" - -#: shifts/views/management.py:226 -msgid "Shifts generated." -msgstr "" - -#: shifts/views/solidarity.py:70 -msgid "Solidarity Shift received. Account Balance increased by 1." -msgstr "" - -#: shifts/views/solidarity.py:99 -msgid "Could not find a shift attendance to use as solidarity shift. You have to finish at least one shift, before you can donate one." -msgstr "" - -#: shifts/views/solidarity.py:118 -msgid "Solidarity Shift given. Account Balance debited with -1." -msgstr "" - -#: shifts/views/views.py:124 shifts/views/views.py:129 -#, python-format -msgid "Edit user shift data: %(name)s" -msgstr "" - -#: shifts/views/views.py:197 -#, python-format -msgid "Shift account: %(name)s" -msgstr "" - -#: shifts/views/views.py:203 -#, python-format -msgid "Create manual shift account entry for: %(link)s" -msgstr "" - -#: shifts/views/views.py:354 -msgid "Frozen statuses updated." -msgstr "" - -#: statistics/apps.py:18 -msgid "Statistics" -msgstr "" - -#: statistics/templates/statistics/fancy_graph.html:10 -msgid "Fancy graph" -msgstr "" - -#: statistics/templates/statistics/main_statistics.html:9 -msgid "Main statistics" -msgstr "" - -#: statistics/templates/statistics/main_statistics.html:19 -#: statistics/views/main_view.py:260 -msgid "Total number of members" -msgstr "" - -#: statistics/templates/statistics/main_statistics.html:22 -#, python-format -msgid "" -"\n" -" All members of the cooperative - whether investing or active: %(number_of_members_now)s.\n" -" " -msgstr "" - -#: statistics/templates/statistics/main_statistics.html:31 -msgid "New members per month" -msgstr "" - -#: statistics/templates/statistics/main_statistics.html:39 -msgid "Targets for break-even" -msgstr "" - -#: statistics/templates/statistics/main_statistics.html:42 -msgid "Shopping basket" -msgstr "" - -#: statistics/templates/statistics/main_statistics.html:44 -#, python-format -msgid "" -"\n" -" The current target food basket value per member and per month to reach the break-even is\n" -" %(target_basket)s€. If you have enabled purchase tracking, you can see your average\n" -" basket value on your profile page.\n" -" " -msgstr "" - -#: statistics/templates/statistics/main_statistics.html:52 -msgid "Members eligible to purchase" -msgstr "" - -#: statistics/templates/statistics/main_statistics.html:54 -#, python-format -msgid "" -"\n" -"

\n" -" All working members and all members who have an exemption (such as parental leave,\n" -" over 70, etc.). Members who are frozen (and have not yet signed up for their\n" -" catch-up shifts) or on break (3 shift cycles or longer away) are not eligible to\n" -" purchase.\n" -"

\n" -"

\n" -" Target number of purchasing members for break-even: %(target_count)s.\n" -"

\n" -" " -msgstr "" - -#: statistics/templates/statistics/main_statistics.html:71 -#: statistics/templates/statistics/main_statistics.html:103 -msgid "Current" -msgstr "" - -#: statistics/templates/statistics/main_statistics.html:88 -msgid "Working members" -msgstr "" - -#: statistics/templates/statistics/main_statistics.html:92 -#, python-format -msgid "" -"\n" -" Working members are active members who do not have an exemption. Exemptions are, for\n" -" example, one year of parental leave, prolonged illness or members over 70 years of age.\n" -" Required number of working members to fulfil all shift placements: %(target_count)s.\n" -" " -msgstr "" - -#: statistics/templates/statistics/main_statistics.html:118 -#: statistics/templates/statistics/main_statistics.html:129 -#: statistics/views/main_view.py:382 -msgid "Frozen members" -msgstr "" - -#: statistics/templates/statistics/main_statistics.html:122 -msgid "" -"\n" -" Any member who is registered as an active member but is 4 or more shifts short and\n" -" therefore\n" -" not eligible to purchase again until they sign up for the appropriate make-up shifts.\n" -" " -msgstr "" - -#: statistics/templates/statistics/main_statistics.html:139 -msgid "Co-purchasers" -msgstr "" - -#: statistics/templates/statistics/main_statistics.html:143 -msgid "" -"\n" -" Each member can designate one person (whether in their own household or not) to shop\n" -" under the same membership number. This can be investing members or non-members.\n" -" " -msgstr "" - -#: statistics/templates/statistics/main_statistics.html:149 -msgid "Co-Purchasers" -msgstr "" - -#: statistics/templates/statistics/main_statistics.html:163 -msgid "" -"\n" -" Here you can follow the progress of the funding campaign. Both additional shares\n" -" (all shares that are subscribed to over and above the compulsory share) and\n" -" subordinated loans are counted. The period runs from 12.09.2023 - 09.12.2023.\n" -" What one person can't achieve, many can!\n" -" " -msgstr "" - -#: statistics/templates/statistics/shift_cancelling_rate.html:9 -msgid "Shift attendance rates" -msgstr "" - -#: statistics/templates/statistics/shift_cancelling_rate.html:19 -#: statistics/templates/statistics/shift_cancelling_rate.html:69 -msgid "Shift cancellation rate" -msgstr "" - -#: statistics/templates/statistics/shift_cancelling_rate.html:78 -msgid "Number of shifts by category" -msgstr "" - -#: statistics/templates/statistics/state_distribution.html:9 -#: statistics/templates/statistics/state_distribution.html:19 -#: statistics/templates/statistics/state_distribution.html:36 -msgid "State distribution" -msgstr "" - -#: statistics/templates/statistics/stats_for_marie.html:9 -msgid "Stats for Marie" -msgstr "" - -#: statistics/templates/statistics/stats_for_marie.html:20 -#: statistics/templates/statistics/stats_for_marie.html:26 -msgid "Number of frozen members per month" -msgstr "" - -#: statistics/templates/statistics/stats_for_marie.html:33 -#: statistics/templates/statistics/stats_for_marie.html:39 -msgid "Number of purchasing members per month" -msgstr "" - -#: statistics/templates/statistics/tags/on_demand_chart.html:5 -msgid "Show graph: " -msgstr "" - -#: statistics/templates/statistics/tags/purchase_statistics_card.html:6 -msgid "Purchases" -msgstr "" - -#: statistics/templates/statistics/tags/purchase_statistics_card.html:9 -#, python-format -msgid "" -"\n" -" Your average basket per month is %(average)s€.\n" -" " -msgstr "" - -#: statistics/templates/statistics/tags/purchase_statistics_card.html:15 -msgid "List of the last purchases" -msgstr "" - -#: statistics/templates/statistics/tags/purchase_statistics_card.html:19 -msgid "Gross amount" -msgstr "" - -#: statistics/templates/statistics/tags/purchase_statistics_card.html:35 -msgid "Evolution of total spends per month" -msgstr "" - -#: statistics/views/main_view.py:332 -msgid "Total spends per month" -msgstr "" - -#: statistics/views/main_view.py:382 -msgid "Purchasing members" -msgstr "" - -#: statistics/views/main_view.py:400 -msgid "Percentage of members with a co-purchaser relative to the number of active members" -msgstr "" - -#: statistics/views/main_view.py:552 -msgid "Purchase data updated" -msgstr "" - -#: utils/forms.py:25 -msgid "German phone number don't need a prefix (e.g. (0)1736160646), international always (e.g. +12125552368)" -msgstr "" - -#: utils/models.py:15 -msgid "Andorra" -msgstr "" - -#: utils/models.py:16 -msgid "United Arab Emirates" -msgstr "" - -#: utils/models.py:17 -msgid "Afghanistan" -msgstr "" - -#: utils/models.py:18 -msgid "Antigua & Barbuda" -msgstr "" - -#: utils/models.py:19 -msgid "Anguilla" -msgstr "" - -#: utils/models.py:20 -msgid "Albania" -msgstr "" - -#: utils/models.py:21 -msgid "Armenia" -msgstr "" - -#: utils/models.py:22 -msgid "Netherlands Antilles" -msgstr "" - -#: utils/models.py:23 -msgid "Angola" -msgstr "" - -#: utils/models.py:24 -msgid "Antarctica" -msgstr "" - -#: utils/models.py:25 -msgid "Argentina" -msgstr "" - -#: utils/models.py:26 -msgid "American Samoa" -msgstr "" - -#: utils/models.py:27 -msgid "Austria" -msgstr "" - -#: utils/models.py:28 -msgid "Australia" -msgstr "" - -#: utils/models.py:29 -msgid "Aruba" -msgstr "" - -#: utils/models.py:30 -msgid "Azerbaijan" -msgstr "" - -#: utils/models.py:31 -msgid "Bosnia and Herzegovina" -msgstr "" - -#: utils/models.py:32 -msgid "Barbados" -msgstr "" - -#: utils/models.py:33 -msgid "Bangladesh" -msgstr "" - -#: utils/models.py:34 -msgid "Belgium" -msgstr "" - -#: utils/models.py:35 -msgid "Burkina Faso" -msgstr "" - -#: utils/models.py:36 -msgid "Bulgaria" -msgstr "" - -#: utils/models.py:37 -msgid "Bahrain" -msgstr "" - -#: utils/models.py:38 -msgid "Burundi" -msgstr "" - -#: utils/models.py:39 -msgid "Benin" -msgstr "" - -#: utils/models.py:40 -msgid "Bermuda" -msgstr "" - -#: utils/models.py:41 -msgid "Brunei Darussalam" -msgstr "" - -#: utils/models.py:42 -msgid "Bolivia" -msgstr "" - -#: utils/models.py:43 -msgid "Brazil" -msgstr "" - -#: utils/models.py:44 -msgid "Bahama" -msgstr "" - -#: utils/models.py:45 -msgid "Bhutan" -msgstr "" - -#: utils/models.py:46 -msgid "Bouvet Island" -msgstr "" - -#: utils/models.py:47 -msgid "Botswana" -msgstr "" - -#: utils/models.py:48 -msgid "Belarus" -msgstr "" - -#: utils/models.py:49 -msgid "Belize" -msgstr "" - -#: utils/models.py:50 -msgid "Canada" -msgstr "" - -#: utils/models.py:51 -msgid "Cocos (Keeling) Islands" -msgstr "" - -#: utils/models.py:52 -msgid "Central African Republic" -msgstr "" - -#: utils/models.py:53 -msgid "Congo" -msgstr "" - -#: utils/models.py:54 -msgid "Switzerland" -msgstr "" - -#: utils/models.py:55 -msgid "Ivory Coast" -msgstr "" - -#: utils/models.py:56 -msgid "Cook Iislands" -msgstr "" - -#: utils/models.py:57 -msgid "Chile" -msgstr "" - -#: utils/models.py:58 -msgid "Cameroon" -msgstr "" - -#: utils/models.py:59 -msgid "China" -msgstr "" - -#: utils/models.py:60 -msgid "Colombia" -msgstr "" - -#: utils/models.py:61 -msgid "Costa Rica" -msgstr "" - -#: utils/models.py:62 -msgid "Cuba" -msgstr "" - -#: utils/models.py:63 -msgid "Cape Verde" -msgstr "" - -#: utils/models.py:64 -msgid "Christmas Island" -msgstr "" - -#: utils/models.py:65 -msgid "Cyprus" -msgstr "" - -#: utils/models.py:66 -msgid "Czech Republic" -msgstr "" - -#: utils/models.py:67 -msgid "Germany" -msgstr "" - -#: utils/models.py:68 -msgid "Djibouti" -msgstr "" - -#: utils/models.py:69 -msgid "Denmark" -msgstr "" - -#: utils/models.py:70 -msgid "Dominica" -msgstr "" - -#: utils/models.py:71 -msgid "Dominican Republic" -msgstr "" - -#: utils/models.py:72 -msgid "Algeria" -msgstr "" - -#: utils/models.py:73 -msgid "Ecuador" -msgstr "" - -#: utils/models.py:74 -msgid "Estonia" -msgstr "" - -#: utils/models.py:75 -msgid "Egypt" -msgstr "" - -#: utils/models.py:76 -msgid "Western Sahara" -msgstr "" - -#: utils/models.py:77 -msgid "Eritrea" -msgstr "" - -#: utils/models.py:78 -msgid "Spain" -msgstr "" - -#: utils/models.py:79 -msgid "Ethiopia" -msgstr "" - -#: utils/models.py:80 -msgid "Finland" -msgstr "" - -#: utils/models.py:81 -msgid "Fiji" -msgstr "" - -#: utils/models.py:82 -msgid "Falkland Islands (Malvinas)" -msgstr "" - -#: utils/models.py:83 -msgid "Micronesia" -msgstr "" - -#: utils/models.py:84 -msgid "Faroe Islands" -msgstr "" - -#: utils/models.py:85 -msgid "France" -msgstr "" - -#: utils/models.py:86 -msgid "France, Metropolitan" -msgstr "" - -#: utils/models.py:87 -msgid "Gabon" -msgstr "" - -#: utils/models.py:88 -msgid "United Kingdom (Great Britain)" -msgstr "" - -#: utils/models.py:89 -msgid "Grenada" -msgstr "" - -#: utils/models.py:90 -msgid "Georgia" -msgstr "" - -#: utils/models.py:91 -msgid "French Guiana" -msgstr "" - -#: utils/models.py:92 -msgid "Ghana" -msgstr "" - -#: utils/models.py:93 -msgid "Gibraltar" -msgstr "" - -#: utils/models.py:94 -msgid "Greenland" -msgstr "" - -#: utils/models.py:95 -msgid "Gambia" -msgstr "" - -#: utils/models.py:96 -msgid "Guinea" -msgstr "" - -#: utils/models.py:97 -msgid "Guadeloupe" -msgstr "" - -#: utils/models.py:98 -msgid "Equatorial Guinea" -msgstr "" - -#: utils/models.py:99 -msgid "Greece" -msgstr "" - -#: utils/models.py:100 -msgid "South Georgia and the South Sandwich Islands" -msgstr "" - -#: utils/models.py:101 -msgid "Guatemala" -msgstr "" - -#: utils/models.py:102 -msgid "Guam" -msgstr "" - -#: utils/models.py:103 -msgid "Guinea-Bissau" -msgstr "" - -#: utils/models.py:104 -msgid "Guyana" -msgstr "" - -#: utils/models.py:105 -msgid "Hong Kong" -msgstr "" - -#: utils/models.py:106 -msgid "Heard & McDonald Islands" -msgstr "" - -#: utils/models.py:107 -msgid "Honduras" -msgstr "" - -#: utils/models.py:108 -msgid "Croatia" -msgstr "" - -#: utils/models.py:109 -msgid "Haiti" -msgstr "" - -#: utils/models.py:110 -msgid "Hungary" -msgstr "" - -#: utils/models.py:111 -msgid "Indonesia" -msgstr "" - -#: utils/models.py:112 -msgid "Ireland" -msgstr "" - -#: utils/models.py:113 -msgid "Israel" -msgstr "" - -#: utils/models.py:114 -msgid "India" -msgstr "" - -#: utils/models.py:115 -msgid "British Indian Ocean Territory" -msgstr "" - -#: utils/models.py:116 -msgid "Iraq" -msgstr "" - -#: utils/models.py:117 -msgid "Islamic Republic of Iran" -msgstr "" - -#: utils/models.py:118 -msgid "Iceland" -msgstr "" - -#: utils/models.py:119 -msgid "Italy" -msgstr "" - -#: utils/models.py:120 -msgid "Jamaica" -msgstr "" - -#: utils/models.py:121 -msgid "Jordan" -msgstr "" - -#: utils/models.py:122 -msgid "Japan" -msgstr "" - -#: utils/models.py:123 -msgid "Kenya" -msgstr "" - -#: utils/models.py:124 -msgid "Kyrgyzstan" -msgstr "" - -#: utils/models.py:125 -msgid "Cambodia" -msgstr "" - -#: utils/models.py:126 -msgid "Kiribati" -msgstr "" - -#: utils/models.py:127 -msgid "Comoros" -msgstr "" - -#: utils/models.py:128 -msgid "St. Kitts and Nevis" -msgstr "" - -#: utils/models.py:129 -msgid "Korea, Democratic People's Republic of" -msgstr "" - -#: utils/models.py:130 -msgid "Korea, Republic of" -msgstr "" - -#: utils/models.py:131 -msgid "Kuwait" -msgstr "" - -#: utils/models.py:132 -msgid "Cayman Islands" -msgstr "" - -#: utils/models.py:133 -msgid "Kazakhstan" -msgstr "" - -#: utils/models.py:134 -msgid "Lao People's Democratic Republic" -msgstr "" - -#: utils/models.py:135 -msgid "Lebanon" -msgstr "" - -#: utils/models.py:136 -msgid "Saint Lucia" -msgstr "" - -#: utils/models.py:137 -msgid "Liechtenstein" -msgstr "" - -#: utils/models.py:138 -msgid "Sri Lanka" -msgstr "" - -#: utils/models.py:139 -msgid "Liberia" -msgstr "" - -#: utils/models.py:140 -msgid "Lesotho" -msgstr "" - -#: utils/models.py:141 -msgid "Lithuania" -msgstr "" - -#: utils/models.py:142 -msgid "Luxembourg" -msgstr "" - -#: utils/models.py:143 -msgid "Latvia" -msgstr "" - -#: utils/models.py:144 -msgid "Libyan Arab Jamahiriya" -msgstr "" - -#: utils/models.py:145 -msgid "Morocco" -msgstr "" - -#: utils/models.py:146 -msgid "Monaco" -msgstr "" - -#: utils/models.py:147 -msgid "Moldova, Republic of" -msgstr "" - -#: utils/models.py:148 -msgid "Madagascar" -msgstr "" - -#: utils/models.py:149 -msgid "Marshall Islands" -msgstr "" - -#: utils/models.py:150 -msgid "Mali" -msgstr "" - -#: utils/models.py:151 -msgid "Mongolia" -msgstr "" - -#: utils/models.py:152 -msgid "Myanmar" -msgstr "" - -#: utils/models.py:153 -msgid "Macau" -msgstr "" - -#: utils/models.py:154 -msgid "Northern Mariana Islands" -msgstr "" - -#: utils/models.py:155 -msgid "Martinique" -msgstr "" - -#: utils/models.py:156 -msgid "Mauritania" -msgstr "" - -#: utils/models.py:157 -msgid "Monserrat" -msgstr "" - -#: utils/models.py:158 -msgid "Malta" -msgstr "" - -#: utils/models.py:159 -msgid "Mauritius" -msgstr "" - -#: utils/models.py:160 -msgid "Maldives" -msgstr "" - -#: utils/models.py:161 -msgid "Malawi" -msgstr "" - -#: utils/models.py:162 -msgid "Mexico" -msgstr "" - -#: utils/models.py:163 -msgid "Malaysia" -msgstr "" - -#: utils/models.py:164 -msgid "Mozambique" -msgstr "" - -#: utils/models.py:165 -msgid "Namibia" -msgstr "" - -#: utils/models.py:166 -msgid "New Caledonia" -msgstr "" - -#: utils/models.py:167 -msgid "Niger" -msgstr "" - -#: utils/models.py:168 -msgid "Norfolk Island" -msgstr "" - -#: utils/models.py:169 -msgid "Nigeria" -msgstr "" - -#: utils/models.py:170 -msgid "Nicaragua" -msgstr "" - -#: utils/models.py:171 -msgid "Netherlands" -msgstr "" - -#: utils/models.py:172 -msgid "Norway" -msgstr "" - -#: utils/models.py:173 -msgid "Nepal" -msgstr "" - -#: utils/models.py:174 -msgid "Nauru" -msgstr "" - -#: utils/models.py:175 -msgid "Niue" -msgstr "" - -#: utils/models.py:176 -msgid "New Zealand" -msgstr "" - -#: utils/models.py:177 -msgid "Oman" -msgstr "" - -#: utils/models.py:178 -msgid "Panama" -msgstr "" - -#: utils/models.py:179 -msgid "Peru" -msgstr "" - -#: utils/models.py:180 -msgid "French Polynesia" -msgstr "" - -#: utils/models.py:181 -msgid "Papua New Guinea" -msgstr "" - -#: utils/models.py:182 -msgid "Philippines" -msgstr "" - -#: utils/models.py:183 -msgid "Pakistan" -msgstr "" - -#: utils/models.py:184 -msgid "Poland" -msgstr "" - -#: utils/models.py:185 -msgid "St. Pierre & Miquelon" -msgstr "" - -#: utils/models.py:186 -msgid "Pitcairn" -msgstr "" - -#: utils/models.py:187 -msgid "Puerto Rico" -msgstr "" - -#: utils/models.py:188 -msgid "Portugal" -msgstr "" - -#: utils/models.py:189 -msgid "Palau" -msgstr "" - -#: utils/models.py:190 -msgid "Paraguay" -msgstr "" - -#: utils/models.py:191 -msgid "Qatar" -msgstr "" - -#: utils/models.py:192 -msgid "Reunion" -msgstr "" - -#: utils/models.py:193 -msgid "Romania" -msgstr "" - -#: utils/models.py:194 -msgid "Russian Federation" -msgstr "" - -#: utils/models.py:195 -msgid "Rwanda" -msgstr "" - -#: utils/models.py:196 -msgid "Saudi Arabia" -msgstr "" - -#: utils/models.py:197 -msgid "Solomon Islands" -msgstr "" - -#: utils/models.py:198 -msgid "Seychelles" -msgstr "" - -#: utils/models.py:199 -msgid "Sudan" -msgstr "" - -#: utils/models.py:200 -msgid "Sweden" -msgstr "" - -#: utils/models.py:201 -msgid "Singapore" -msgstr "" - -#: utils/models.py:202 -msgid "St. Helena" -msgstr "" - -#: utils/models.py:203 -msgid "Slovenia" -msgstr "" - -#: utils/models.py:204 -msgid "Svalbard & Jan Mayen Islands" -msgstr "" - -#: utils/models.py:205 -msgid "Slovakia" -msgstr "" - -#: utils/models.py:206 -msgid "Sierra Leone" -msgstr "" - -#: utils/models.py:207 -msgid "San Marino" -msgstr "" - -#: utils/models.py:208 -msgid "Senegal" -msgstr "" - -#: utils/models.py:209 -msgid "Somalia" -msgstr "" - -#: utils/models.py:210 -msgid "Suriname" -msgstr "" - -#: utils/models.py:211 -msgid "Sao Tome & Principe" -msgstr "" - -#: utils/models.py:212 -msgid "El Salvador" -msgstr "" - -#: utils/models.py:213 -msgid "Syrian Arab Republic" -msgstr "" - -#: utils/models.py:214 -msgid "Swaziland" -msgstr "" - -#: utils/models.py:215 -msgid "Turks & Caicos Islands" -msgstr "" - -#: utils/models.py:216 -msgid "Chad" -msgstr "" - -#: utils/models.py:217 -msgid "French Southern Territories" -msgstr "" - -#: utils/models.py:218 -msgid "Togo" -msgstr "" - -#: utils/models.py:219 -msgid "Thailand" -msgstr "" - -#: utils/models.py:220 -msgid "Tajikistan" -msgstr "" - -#: utils/models.py:221 -msgid "Tokelau" -msgstr "" - -#: utils/models.py:222 -msgid "Turkmenistan" -msgstr "" - -#: utils/models.py:223 -msgid "Tunisia" -msgstr "" - -#: utils/models.py:224 -msgid "Tonga" -msgstr "" - -#: utils/models.py:225 -msgid "East Timor" -msgstr "" - -#: utils/models.py:226 -msgid "Turkey" -msgstr "" - -#: utils/models.py:227 -msgid "Trinidad & Tobago" -msgstr "" - -#: utils/models.py:228 -msgid "Tuvalu" -msgstr "" - -#: utils/models.py:229 -msgid "Taiwan, Province of China" -msgstr "" - -#: utils/models.py:230 -msgid "Tanzania, United Republic of" -msgstr "" - -#: utils/models.py:231 -msgid "Ukraine" -msgstr "" - -#: utils/models.py:232 -msgid "Uganda" -msgstr "" - -#: utils/models.py:233 -msgid "United States Minor Outlying Islands" -msgstr "" - -#: utils/models.py:234 -msgid "United States of America" -msgstr "" - -#: utils/models.py:235 -msgid "Uruguay" -msgstr "" - -#: utils/models.py:236 -msgid "Uzbekistan" -msgstr "" - -#: utils/models.py:237 -msgid "Vatican City State (Holy See)" -msgstr "" - -#: utils/models.py:238 -msgid "St. Vincent & the Grenadines" -msgstr "" - -#: utils/models.py:239 -msgid "Venezuela" -msgstr "" - -#: utils/models.py:240 -msgid "British Virgin Islands" -msgstr "" - -#: utils/models.py:241 -msgid "United States Virgin Islands" -msgstr "" - -#: utils/models.py:242 -msgid "Viet Nam" -msgstr "" - -#: utils/models.py:243 -msgid "Vanuatu" -msgstr "" - -#: utils/models.py:244 -msgid "Wallis & Futuna Islands" -msgstr "" - -#: utils/models.py:245 -msgid "Samoa" -msgstr "" - -#: utils/models.py:246 -msgid "Yemen" -msgstr "" - -#: utils/models.py:247 -msgid "Mayotte" -msgstr "" - -#: utils/models.py:248 -msgid "Yugoslavia" -msgstr "" - -#: utils/models.py:249 -msgid "South Africa" -msgstr "" - -#: utils/models.py:250 -msgid "Zambia" -msgstr "" - -#: utils/models.py:251 -msgid "Zaire" -msgstr "" - -#: utils/models.py:252 -msgid "Zimbabwe" -msgstr "" - -#: utils/models.py:253 -msgid "Unknown or unspecified country" -msgstr "" - -#: utils/models.py:257 -msgid "🇩🇪 Deutsch" -msgstr "" - -#: utils/models.py:258 -msgid "🇬🇧 English" -msgstr "" - -#: utils/models.py:366 -msgid "start date must be set" -msgstr "" - -#: utils/models.py:369 -msgid "Start date must be prior to end date" -msgstr "" - -#: utils/models.py:399 -msgid "Must be a positive number." -msgstr "" - -#: utils/user_utils.py:42 -msgid "NO NAME AVAILABLE" -msgstr "" - -#: welcomedesk/apps.py:17 welcomedesk/apps.py:20 -#: welcomedesk/templates/welcomedesk/welcome_desk_search.html:14 -msgid "Welcome Desk" -msgstr "" - -#: welcomedesk/services/welcome_desk_reasons_cannot_shop_service.py:44 -#, python-format -msgid "%(name)s does not have a Tapir account. Contact a member of the management team." -msgstr "" - -#: welcomedesk/services/welcome_desk_reasons_cannot_shop_service.py:47 -#, python-format -msgid "%(name)s is an investing member. If they want to shop, they have to become an active member. Contact a member of the management team." -msgstr "" - -#: welcomedesk/services/welcome_desk_reasons_cannot_shop_service.py:51 -#, python-format -msgid "%(name)s has been frozen because they missed too many shifts.If they want to shop, they must first be re-activated.Contact a member of the management team." -msgstr "" - -#: welcomedesk/services/welcome_desk_reasons_cannot_shop_service.py:56 -#, python-format -msgid "%(name)s has paused their membership. Contact a member of the management team." -msgstr "" - -#: welcomedesk/services/welcome_desk_reasons_cannot_shop_service.py:59 -#, python-format -msgid "%(name)s has is not a member of the cooperative. They may have transferred their shares to another member. Contact a member of the management team." -msgstr "" - -#: welcomedesk/services/welcome_desk_warnings_service.py:34 -#, python-format -msgid "%(name)s has not attended a welcome session yet. Make sure they plan to do it!" -msgstr "" diff --git a/tapir/translations/locale/de/LC_MESSAGES/django.po b/tapir/translations/locale/de/LC_MESSAGES/django.po index 61b060a3..600ac3e8 100644 --- a/tapir/translations/locale/de/LC_MESSAGES/django.po +++ b/tapir/translations/locale/de/LC_MESSAGES/django.po @@ -2,12 +2,12 @@ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. -# +# msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-07 13:51+0100\n" +"POT-Creation-Date: 2024-12-11 16:55+0100\n" "PO-Revision-Date: 2024-11-22 08:33+0100\n" "Last-Translator: \n" "Language-Team: \n" @@ -44,12 +44,12 @@ msgstr "" msgid "Displayed name" msgstr "Angezeigter Name" -#: accounts/models.py:71 coop/models.py:71 coop/models.py:484 +#: accounts/models.py:71 coop/models.py:70 coop/models.py:476 msgid "Pronouns" msgstr "Pronomen" #: accounts/models.py:72 accounts/templates/accounts/user_detail.html:67 -#: coop/models.py:73 coop/models.py:486 +#: coop/models.py:72 coop/models.py:478 #: coop/templates/coop/draftuser_detail.html:71 #: coop/templates/coop/draftuser_detail.html:142 #: coop/templates/coop/shareowner_detail.html:51 @@ -57,29 +57,29 @@ msgid "Phone number" msgstr "Telefonnummer" #: accounts/models.py:73 accounts/templates/accounts/user_detail.html:77 -#: coop/models.py:74 coop/models.py:487 +#: coop/models.py:73 coop/models.py:479 #: coop/templates/coop/draftuser_detail.html:146 #: coop/templates/coop/shareowner_detail.html:55 msgid "Birthdate" msgstr "Geburtsdatum" -#: accounts/models.py:74 coop/models.py:75 coop/models.py:488 +#: accounts/models.py:74 coop/models.py:74 coop/models.py:480 msgid "Street and house number" msgstr "Straße und Hausnummer" -#: accounts/models.py:75 coop/models.py:76 coop/models.py:489 +#: accounts/models.py:75 coop/models.py:75 coop/models.py:481 msgid "Extra address line" msgstr "Adresszusatz" -#: accounts/models.py:76 coop/models.py:77 coop/models.py:490 +#: accounts/models.py:76 coop/models.py:76 coop/models.py:482 msgid "Postcode" msgstr "Postleitzahl" -#: accounts/models.py:77 coop/models.py:78 coop/models.py:491 +#: accounts/models.py:77 coop/models.py:77 coop/models.py:483 msgid "City" msgstr "Ort" -#: accounts/models.py:78 coop/models.py:79 coop/models.py:492 +#: accounts/models.py:78 coop/models.py:78 coop/models.py:484 msgid "Country" msgstr "Land" @@ -92,7 +92,7 @@ msgid "Allow purchase tracking" msgstr "" #: accounts/models.py:95 accounts/templates/accounts/user_detail.html:97 -#: coop/models.py:82 coop/models.py:495 +#: coop/models.py:81 coop/models.py:487 #: coop/templates/coop/shareowner_detail.html:75 msgid "Preferred Language" msgstr "Bevorzugte Sprache" @@ -253,7 +253,7 @@ msgstr "" msgid "Enable" msgstr "" -#: accounts/templates/accounts/user_detail.html:20 coop/models.py:761 +#: accounts/templates/accounts/user_detail.html:20 coop/models.py:753 #: coop/templates/coop/draftuser_detail.html:86 #: coop/templates/coop/shareowner_detail.html:10 log/views.py:94 #: log/views.py:152 shifts/templates/shifts/shift_day_printable.html:55 @@ -579,7 +579,7 @@ msgstr "Anzahl zu erstellender Anteile" msgid "The end date must be later than the start date." msgstr "" -#: coop/forms.py:107 coop/models.py:501 +#: coop/forms.py:107 coop/models.py:493 msgid "Number of Shares" msgstr "Anzahl Anteile" @@ -660,189 +660,189 @@ msgstr "" msgid "Cannot pay out, because shares have been gifted." msgstr "" -#: coop/models.py:55 +#: coop/models.py:54 msgid "Is company" msgstr "Ist eine Firma" -#: coop/models.py:62 coop/models.py:475 +#: coop/models.py:61 coop/models.py:467 msgid "Administrative first name" msgstr "Amtlicher Vorname" -#: coop/models.py:64 coop/models.py:477 +#: coop/models.py:63 coop/models.py:469 msgid "Last name" msgstr "Nachname" -#: coop/models.py:66 coop/models.py:479 +#: coop/models.py:65 coop/models.py:471 msgid "Usage name" msgstr "Angezeigter Name" -#: coop/models.py:72 coop/models.py:485 +#: coop/models.py:71 coop/models.py:477 msgid "Email address" msgstr "E-Mail-Adresse" -#: coop/models.py:89 +#: coop/models.py:88 msgid "Is investing member" msgstr "Ist investierendes Mitglied" -#: coop/models.py:91 coop/models.py:516 +#: coop/models.py:90 coop/models.py:508 #: coop/templates/coop/draftuser_detail.html:176 #: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:48 #: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:167 msgid "Ratenzahlung" msgstr "Ratenzahlung" -#: coop/models.py:93 coop/models.py:508 +#: coop/models.py:92 coop/models.py:500 msgid "Attended Welcome Session" msgstr "An Willkommenstreffen teilgenommen" -#: coop/models.py:95 coop/models.py:513 +#: coop/models.py:94 coop/models.py:505 msgid "Paid Entrance Fee" msgstr "Eintrittsgeld bezahlt" -#: coop/models.py:97 +#: coop/models.py:96 msgid "Is willing to gift a share" msgstr "Ist bereit Anteile zu verschenken" -#: coop/models.py:209 +#: coop/models.py:208 msgid "Cannot be a company and have a Tapir account" msgstr "Kann keine Firma sein und ein Tapir-Konto haben" -#: coop/models.py:225 +#: coop/models.py:224 msgid "User info should be stored in associated Tapir account" msgstr "Benutzer Infos sollen in dem Tapir Konto gespeichert warden" -#: coop/models.py:384 +#: coop/models.py:376 msgid "Not a member" msgstr "" -#: coop/models.py:385 coop/templates/coop/draftuser_detail.html:169 +#: coop/models.py:377 coop/templates/coop/draftuser_detail.html:169 msgid "Investing" msgstr "Investierend" -#: coop/models.py:386 coop/templates/coop/draftuser_detail.html:171 +#: coop/models.py:378 coop/templates/coop/draftuser_detail.html:171 #: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:103 #: coop/views/statistics.py:142 msgid "Active" msgstr "Aktiv" -#: coop/models.py:387 +#: coop/models.py:379 msgid "Paused" msgstr "Pausiert" -#: coop/models.py:442 +#: coop/models.py:434 msgid "Amount paid for a share can't be negative" msgstr "Der Betrag kann nicht negative sein" -#: coop/models.py:446 +#: coop/models.py:438 #, python-brace-format msgid "Amount paid for a share can't more than {COOP_SHARE_PRICE} (the price of a share)" msgstr "Der Betrag kann nicht mehr als {COOP_SHARE_PRICE} sein" -#: coop/models.py:504 +#: coop/models.py:496 msgid "Investing member" msgstr "Investierendes Mitglied" -#: coop/models.py:511 +#: coop/models.py:503 msgid "Signed Beteiligungserklärung" msgstr "Beteiligungserklärung unterschrieben" -#: coop/models.py:514 coop/templates/coop/draftuser_detail.html:234 +#: coop/models.py:506 coop/templates/coop/draftuser_detail.html:234 msgid "Paid Shares" msgstr "Anteil(e) bezahlt" -#: coop/models.py:571 +#: coop/models.py:563 msgid "Email address must be set." msgstr "Email-Adresse muss gesetzt sein." -#: coop/models.py:573 +#: coop/models.py:565 msgid "First name must be set." msgstr "Vorname muss gesetzt sein." -#: coop/models.py:575 +#: coop/models.py:567 msgid "Last name must be set." msgstr "Nachname muss gesetzt sein." -#: coop/models.py:579 +#: coop/models.py:571 msgid "Membership agreement must be signed." msgstr "Mitgliedsantrag muss unterschrieben sein." -#: coop/models.py:581 +#: coop/models.py:573 msgid "Amount of requested shares must be positive." msgstr "Die Anzahl der erwünschten Anteile muss positiv sein." -#: coop/models.py:583 +#: coop/models.py:575 msgid "Member already created." msgstr "Mitglied schon vorhanden." -#: coop/models.py:610 +#: coop/models.py:602 msgid "Paying member" msgstr "Zahlendes Mitglied" -#: coop/models.py:618 +#: coop/models.py:610 msgid "Credited member" msgstr "Empfangendes Mitglied" -#: coop/models.py:625 +#: coop/models.py:617 msgid "Amount" msgstr "Betrag" -#: coop/models.py:631 +#: coop/models.py:623 msgid "Payment date" msgstr "Zahldatum" -#: coop/models.py:634 +#: coop/models.py:626 msgid "Creation date" msgstr "Erstellungsdatum" -#: coop/models.py:639 +#: coop/models.py:631 msgid "Created by" msgstr "Erstellt durch" -#: coop/models.py:811 +#: coop/models.py:803 msgid "The cooperative buys the shares back from the member" msgstr "" -#: coop/models.py:814 +#: coop/models.py:806 #, fuzzy #| msgid "Number of shares to create" msgid "The member gifts the shares to the cooperative" msgstr "Anzahl zu erstellender Anteile" -#: coop/models.py:816 +#: coop/models.py:808 msgid "The shares get transferred to another member" msgstr "" -#: coop/models.py:819 +#: coop/models.py:811 #, fuzzy #| msgid "Confirm cancellation" msgid "Financial reasons" msgstr "Absage bestätigen" -#: coop/models.py:820 +#: coop/models.py:812 #, fuzzy #| msgid "Confirm cancellation" msgid "Health reasons" msgstr "Absage bestätigen" -#: coop/models.py:821 +#: coop/models.py:813 msgid "Distance" msgstr "" -#: coop/models.py:822 +#: coop/models.py:814 msgid "Strategic orientation of SuperCoop" msgstr "" -#: coop/models.py:823 +#: coop/models.py:815 msgid "Other" msgstr "" -#: coop/models.py:828 +#: coop/models.py:820 #, fuzzy #| msgid "Edit shareowner" msgid "Shareowner" msgstr "Mitglied bearbeiten" -#: coop/models.py:847 +#: coop/models.py:839 msgid "Leave this empty if the resignation type is not a transfer to another member" msgstr "" @@ -1069,7 +1069,7 @@ msgid "" msgstr "" #: coop/templates/coop/email/accounting_recap.body.default.html:21 -#: statistics/views/main_view.py:299 +#: statistics/views/main_view.py:301 msgid "New members" msgstr "Neue Mitglieder" @@ -4621,7 +4621,7 @@ msgid "Main statistics" msgstr "Hauptstatistik" #: statistics/templates/statistics/main_statistics.html:19 -#: statistics/views/main_view.py:258 +#: statistics/views/main_view.py:260 msgid "Total number of members" msgstr "Gesamte Mitgliederzahl" @@ -4718,7 +4718,7 @@ msgstr "" #: statistics/templates/statistics/main_statistics.html:118 #: statistics/templates/statistics/main_statistics.html:129 -#: statistics/views/main_view.py:380 +#: statistics/views/main_view.py:382 msgid "Frozen members" msgstr "Eingefrorene Mitglieder" @@ -4844,19 +4844,19 @@ msgstr "" msgid "Evolution of total spends per month" msgstr "Entwicklung der Gesamtausgaben pro Monat" -#: statistics/views/main_view.py:330 +#: statistics/views/main_view.py:332 msgid "Total spends per month" msgstr "Gesamtausgaben pro Monat" -#: statistics/views/main_view.py:380 +#: statistics/views/main_view.py:382 msgid "Purchasing members" msgstr "Einkaufsberechtigten Mitglieder*innen" -#: statistics/views/main_view.py:398 +#: statistics/views/main_view.py:400 msgid "Percentage of members with a co-purchaser relative to the number of active members" msgstr "Prozentualer Anteil der Mitglieder mit einem Miterwerber im Verhältnis zur Zahl der aktiven Mitglieder" -#: statistics/views/main_view.py:550 +#: statistics/views/main_view.py:552 msgid "Purchase data updated" msgstr "Kaufdaten aktualisiert" From 896ba370a8e0acaf6dab9578a070a15d29bce3e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Wed, 11 Dec 2024 16:57:39 +0100 Subject: [PATCH 31/50] Added tests for NumberOfInvestingMembersAtDateView --- .../test_number_of_investing_members_view.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 tapir/statistics/tests/fancy_graph/test_number_of_investing_members_view.py diff --git a/tapir/statistics/tests/fancy_graph/test_number_of_investing_members_view.py b/tapir/statistics/tests/fancy_graph/test_number_of_investing_members_view.py new file mode 100644 index 00000000..60dc5f60 --- /dev/null +++ b/tapir/statistics/tests/fancy_graph/test_number_of_investing_members_view.py @@ -0,0 +1,45 @@ +import datetime + +from django.utils import timezone + +from tapir.coop.models import ShareOwnership +from tapir.coop.tests.factories import ShareOwnerFactory +from tapir.statistics.views.fancy_graph.number_of_investing_members_view import ( + NumberOfInvestingMembersAtDateView, +) +from tapir.utils.tests_utils import ( + TapirFactoryTestBase, + mock_timezone_now, +) + + +class TestNumberOfInvestingMembersView(TapirFactoryTestBase): + NOW = datetime.datetime(year=2023, month=4, day=1, hour=12) + REFERENCE_TIME = timezone.make_aware( + datetime.datetime(year=2022, month=6, day=15, hour=12) + ) + + def setUp(self) -> None: + super().setUp() + self.NOW = mock_timezone_now(self, self.NOW) + + def test_calculateDatapoint_memberIsNotInvesting_notCounted(self): + ShareOwnerFactory.create(is_investing=False) + + result = NumberOfInvestingMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(0, result) + + def test_calculateDatapoint_memberIsInvesting_counted(self): + ShareOwnerFactory.create(is_investing=True) + ShareOwnership.objects.update( + start_date=self.REFERENCE_TIME.date() - datetime.timedelta(days=1) + ) + + result = NumberOfInvestingMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(1, result) From 98b7f92438541af38dd654819bb2ba0a8c1d38d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Wed, 11 Dec 2024 17:14:41 +0100 Subject: [PATCH 32/50] Added tests for NumberOfLongTermFrozenMembersAtDateView --- .../test_number_of_frozen_members_view.py | 2 +- ...number_of_long_term_frozen_members_view.py | 101 ++++++++++++++++++ ...number_of_long_term_frozen_members_view.py | 1 + 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 tapir/statistics/tests/fancy_graph/test_number_of_long_term_frozen_members_view.py diff --git a/tapir/statistics/tests/fancy_graph/test_number_of_frozen_members_view.py b/tapir/statistics/tests/fancy_graph/test_number_of_frozen_members_view.py index 919ab332..88c6aff5 100644 --- a/tapir/statistics/tests/fancy_graph/test_number_of_frozen_members_view.py +++ b/tapir/statistics/tests/fancy_graph/test_number_of_frozen_members_view.py @@ -23,7 +23,7 @@ def setUp(self) -> None: super().setUp() self.NOW = mock_timezone_now(self, self.NOW) - def test_calculateDatapoint_memberIsFrozenButIsNoActive_notCounted(self): + def test_calculateDatapoint_memberIsFrozenButIsNotActive_notCounted(self): TapirUserFactory.create( date_joined=self.REFERENCE_TIME + datetime.timedelta(days=1) ) diff --git a/tapir/statistics/tests/fancy_graph/test_number_of_long_term_frozen_members_view.py b/tapir/statistics/tests/fancy_graph/test_number_of_long_term_frozen_members_view.py new file mode 100644 index 00000000..33482d5d --- /dev/null +++ b/tapir/statistics/tests/fancy_graph/test_number_of_long_term_frozen_members_view.py @@ -0,0 +1,101 @@ +import datetime + +from django.utils import timezone + +from tapir.accounts.tests.factories.factories import TapirUserFactory +from tapir.coop.models import ShareOwnership +from tapir.shifts.models import ShiftUserData, UpdateShiftUserDataLogEntry +from tapir.statistics.views.fancy_graph.number_of_long_term_frozen_members_view import ( + NumberOfLongTermFrozenMembersAtDateView, +) +from tapir.utils.tests_utils import ( + TapirFactoryTestBase, + mock_timezone_now, +) + + +class TestNumberOfLongTermFrozenMembersView(TapirFactoryTestBase): + NOW = datetime.datetime(year=2023, month=4, day=1, hour=12) + REFERENCE_TIME = timezone.make_aware( + datetime.datetime(year=2022, month=6, day=15, hour=12) + ) + + def setUp(self) -> None: + super().setUp() + self.NOW = mock_timezone_now(self, self.NOW) + + def test_calculateDatapoint_memberIsNotFrozen_notCounted(self): + TapirUserFactory.create( + date_joined=self.REFERENCE_TIME + datetime.timedelta(days=1) + ) + ShiftUserData.objects.update(is_frozen=False) + + result = NumberOfLongTermFrozenMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(0, result) + + def test_calculateDatapoint_memberIsFrozenSinceNotLongEnough_notCounted(self): + tapir_user = TapirUserFactory.create( + date_joined=self.REFERENCE_TIME - datetime.timedelta(days=1), + share_owner__is_investing=False, + ) + ShareOwnership.objects.update( + start_date=self.REFERENCE_TIME.date() - datetime.timedelta(days=1) + ) + ShiftUserData.objects.update(is_frozen=True) + + log_entry = UpdateShiftUserDataLogEntry.objects.create( + user=tapir_user, + old_values={"is_frozen": False}, + new_values={"is_frozen": True}, + ) + log_entry.created_date = self.REFERENCE_TIME - datetime.timedelta(days=150) + log_entry.save() + + result = NumberOfLongTermFrozenMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(0, result) + + def test_calculateDatapoint_memberIsFrozenSinceLongEnough_counted(self): + tapir_user = TapirUserFactory.create( + date_joined=self.REFERENCE_TIME + datetime.timedelta(days=1), + share_owner__is_investing=False, + ) + ShareOwnership.objects.update( + start_date=self.REFERENCE_TIME.date() - datetime.timedelta(days=1) + ) + ShiftUserData.objects.update(is_frozen=True) + + log_entry = UpdateShiftUserDataLogEntry.objects.create( + user=tapir_user, + old_values={"is_frozen": False}, + new_values={"is_frozen": True}, + ) + log_entry.created_date = self.REFERENCE_TIME - datetime.timedelta(days=190) + log_entry.save() + + result = NumberOfLongTermFrozenMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(1, result) + + def test_calculateDatapoint_memberIsFrozenAndHasNoLogs_counted(self): + TapirUserFactory.create( + date_joined=self.REFERENCE_TIME + datetime.timedelta(days=1), + share_owner__is_investing=False, + ) + ShareOwnership.objects.update( + start_date=self.REFERENCE_TIME.date() - datetime.timedelta(days=1) + ) + ShiftUserData.objects.update(is_frozen=True) + + result = NumberOfLongTermFrozenMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(1, result) diff --git a/tapir/statistics/views/fancy_graph/number_of_long_term_frozen_members_view.py b/tapir/statistics/views/fancy_graph/number_of_long_term_frozen_members_view.py index 1e6e5a1d..36131c47 100644 --- a/tapir/statistics/views/fancy_graph/number_of_long_term_frozen_members_view.py +++ b/tapir/statistics/views/fancy_graph/number_of_long_term_frozen_members_view.py @@ -31,6 +31,7 @@ def calculate_datapoint(self, reference_time: datetime.datetime) -> int: if not status_change_log_entry: # could not find any log entry, we assume the member is frozen long-term count += 1 + continue if (reference_time - status_change_log_entry.created_date).days > 30 * 6: count += 1 From a3d2ed14265181e3e7215d4b093363933159a92d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Wed, 11 Dec 2024 17:21:15 +0100 Subject: [PATCH 33/50] Added tests for NumberOfMembersAtDateView --- .../test_number_of_members_view.py | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 tapir/statistics/tests/fancy_graph/test_number_of_members_view.py diff --git a/tapir/statistics/tests/fancy_graph/test_number_of_members_view.py b/tapir/statistics/tests/fancy_graph/test_number_of_members_view.py new file mode 100644 index 00000000..6ef0133f --- /dev/null +++ b/tapir/statistics/tests/fancy_graph/test_number_of_members_view.py @@ -0,0 +1,72 @@ +import datetime + +from django.utils import timezone + +from tapir.coop.models import MemberStatus, MembershipPause +from tapir.coop.tests.factories import ShareOwnerFactory +from tapir.statistics.views.fancy_graph.number_of_members_view import ( + NumberOfMembersAtDateView, +) +from tapir.utils.tests_utils import ( + TapirFactoryTestBase, + mock_timezone_now, +) + + +class TestNumberOfActiveMembersView(TapirFactoryTestBase): + NOW = datetime.datetime(year=2023, month=4, day=1, hour=12) + REFERENCE_TIME = timezone.make_aware( + datetime.datetime(year=2022, month=6, day=15, hour=12) + ) + + def setUp(self) -> None: + super().setUp() + self.NOW = mock_timezone_now(self, self.NOW) + + def test_calculateDatapoint_memberStatusSold_notCounted(self): + member_sold = ShareOwnerFactory.create(nb_shares=0) + self.assertEqual( + MemberStatus.SOLD, member_sold.get_member_status(self.REFERENCE_TIME) + ) + + result = NumberOfMembersAtDateView().calculate_datapoint(self.REFERENCE_TIME) + + self.assertEqual(0, result) + + def test_calculateDatapoint_memberStatusInvesting_counted(self): + member_investing = ShareOwnerFactory.create(is_investing=True) + self.assertEqual( + MemberStatus.INVESTING, + member_investing.get_member_status(self.REFERENCE_TIME), + ) + + result = NumberOfMembersAtDateView().calculate_datapoint(self.REFERENCE_TIME) + + self.assertEqual(1, result) + + def test_calculateDatapoint_memberStatusPaused_counted(self): + member_paused = ShareOwnerFactory.create(is_investing=False) + MembershipPause.objects.create( + share_owner=member_paused, + description="Test", + start_date=self.REFERENCE_TIME.date(), + ) + self.assertEqual( + MemberStatus.PAUSED, + member_paused.get_member_status(self.REFERENCE_TIME), + ) + + result = NumberOfMembersAtDateView().calculate_datapoint(self.REFERENCE_TIME) + + self.assertEqual(1, result) + + def test_calculateDatapoint_memberStatusActive_counted(self): + member_active = ShareOwnerFactory.create(is_investing=False) + self.assertEqual( + MemberStatus.ACTIVE, + member_active.get_member_status(self.REFERENCE_TIME), + ) + + result = NumberOfMembersAtDateView().calculate_datapoint(self.REFERENCE_TIME) + + self.assertEqual(1, result) From 059cf58122457ec1431ec585233dce822cfe1c53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Wed, 11 Dec 2024 17:28:39 +0100 Subject: [PATCH 34/50] Added tests for NumberOfPausedMembersAtDateView --- .../test_number_of_paused_members_view.py | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 tapir/statistics/tests/fancy_graph/test_number_of_paused_members_view.py diff --git a/tapir/statistics/tests/fancy_graph/test_number_of_paused_members_view.py b/tapir/statistics/tests/fancy_graph/test_number_of_paused_members_view.py new file mode 100644 index 00000000..6e80ac43 --- /dev/null +++ b/tapir/statistics/tests/fancy_graph/test_number_of_paused_members_view.py @@ -0,0 +1,47 @@ +import datetime + +from django.utils import timezone + +from tapir.coop.models import MembershipPause +from tapir.coop.tests.factories import ShareOwnerFactory +from tapir.statistics.views.fancy_graph.number_of_paused_members_view import ( + NumberOfPausedMembersAtDateView, +) +from tapir.utils.tests_utils import ( + TapirFactoryTestBase, + mock_timezone_now, +) + + +class TestNumberOfPausedMembersView(TapirFactoryTestBase): + NOW = datetime.datetime(year=2023, month=4, day=1, hour=12) + REFERENCE_TIME = timezone.make_aware( + datetime.datetime(year=2022, month=6, day=15, hour=12) + ) + + def setUp(self) -> None: + super().setUp() + self.NOW = mock_timezone_now(self, self.NOW) + + def test_calculateDatapoint_memberIsNotPaused_notCounted(self): + ShareOwnerFactory.create() + + result = NumberOfPausedMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(0, result) + + def test_calculateDatapoint_memberIsPaused_counted(self): + share_owner = ShareOwnerFactory.create(is_investing=False) + MembershipPause.objects.create( + share_owner=share_owner, + description="Test", + start_date=self.REFERENCE_TIME.date(), + ) + + result = NumberOfPausedMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(1, result) From 4301b9254955887a5e594586a48e036fd1e748bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Wed, 11 Dec 2024 17:30:38 +0100 Subject: [PATCH 35/50] Removed old untested stats views. --- .../statistics/shift_cancelling_rate.html | 85 ------ .../statistics/state_distribution.html | 43 --- .../templates/statistics/stats_for_marie.html | 46 --- tapir/statistics/urls.py | 50 ---- .../views/cancellation_rate_view.py | 230 --------------- .../views/state_distribution_view.py | 105 ------- tapir/statistics/views/stats_for_marie.py | 267 ------------------ 7 files changed, 826 deletions(-) delete mode 100644 tapir/statistics/templates/statistics/shift_cancelling_rate.html delete mode 100644 tapir/statistics/templates/statistics/state_distribution.html delete mode 100644 tapir/statistics/templates/statistics/stats_for_marie.html delete mode 100644 tapir/statistics/views/cancellation_rate_view.py delete mode 100644 tapir/statistics/views/state_distribution_view.py delete mode 100644 tapir/statistics/views/stats_for_marie.py diff --git a/tapir/statistics/templates/statistics/shift_cancelling_rate.html b/tapir/statistics/templates/statistics/shift_cancelling_rate.html deleted file mode 100644 index 6e668b3b..00000000 --- a/tapir/statistics/templates/statistics/shift_cancelling_rate.html +++ /dev/null @@ -1,85 +0,0 @@ -{% extends "core/base.html" %} -{% load statistics %} -{% load core %} -{% load django_bootstrap5 %} -{% load static %} -{% load i18n %} -{% load utils %} -{% block title %} - {% translate 'Shift attendance rates' %} -{% endblock title %} -{% block head %} - - -{% endblock head %} -{% block content %} -
-
-
-
{% translate "Shift cancellation rate" %}
-
-
    -
  • - These graphs show the attendance ratio of shifts per month over several category. -
      -
    • - That means, for a given month and category, we get all "attendances". -
      - An attendance is a registration of a member to a shift slot. -
      - We then calculate the ratio of "number of not-attended attendances" against "total number of attendances". -
    • -
    • - For example, if there has been 10 registrations in a given month and category, - and out of those 10 registrations, 8 members actually showed up, then we get a 20% cancellation rate. -
    • -
    -
  • -
  • We only count actual show ups: excused, cancelled, no-shows... are all counted as "not attended".
  • -
  • Cancelled shifts (for example on holidays) are removed from the calculations.
  • -
  • - For each shift slot, only a single attendance is taken into account. -
      -
    • - There can be several attendances for a single slot, for example if a flying member registered, cancelled, then another member registered. -
      - In that case, we only look at the most recent attendance for that slot. -
    • -
    -
  • -
- The following categories are counted separately: -
    -
  • - ABCD members: takes into account only attendances from ABCD members. That means, members that have the status ABCD on their shift profile. -
    - The status at the time of the shift is taken into account, not the status they have now. -
  • -
  • Same for flying and frozen members.
  • -
  • - ABCD shifts: takes into account only attendances from ABCD shifts. That means, the attendance comes from the user being registered to an ABCD slot. -
  • -
  • - Flying shifts: takes into account only attendances from not-ABCD shifts. That means, the user registered to a single shift. -
    - The case of an ABCD member that registers to an extra single shift, separate from their normal ABCD shift, would count as a flying shift. -
  • -
-

- {% translate "Shift cancellation rate" as chart_name %} - {% on_demand_chart chart_name "statistics:shift_cancelling_rate_json" %} -

-

- This second graph shows the number of attendances in each category, following the same criterias as described above. -
- This is useful to put some values in perspective. For example, at 01.05.2024, 100% of the frozen members didn't show up. But that's only 1 attendance. -

-

- {% translate "Number of shifts by category" as chart_name %} - {% on_demand_chart chart_name "statistics:shift_count_by_category_json" %} -

-
-
-
-
-{% endblock content %} diff --git a/tapir/statistics/templates/statistics/state_distribution.html b/tapir/statistics/templates/statistics/state_distribution.html deleted file mode 100644 index 3491707c..00000000 --- a/tapir/statistics/templates/statistics/state_distribution.html +++ /dev/null @@ -1,43 +0,0 @@ -{% extends "core/base.html" %} -{% load statistics %} -{% load core %} -{% load django_bootstrap5 %} -{% load static %} -{% load i18n %} -{% load utils %} -{% block title %} - {% translate 'State distribution' %} -{% endblock title %} -{% block head %} - - -{% endblock head %} -{% block content %} -
-
-
-
{% translate "State distribution" %}
-
-
    -
  • These graphs show the distribution of the attendance states per month
  • -
  • Cancelled shifts (for example on holidays) are removed from the calculations.
  • -
  • - For each shift slot, only a single attendance is taken into account. -
      -
    • - There can be several attendances for a single slot, for example if a flying member registered, cancelled, then another member registered. -
      - In that case, we only look at the most recent attendance for that slot. -
    • -
    -
  • -
-

- {% translate "State distribution" as chart_name %} - {% on_demand_chart chart_name "statistics:state_distribution_json" %} -

-
-
-
-
-{% endblock content %} diff --git a/tapir/statistics/templates/statistics/stats_for_marie.html b/tapir/statistics/templates/statistics/stats_for_marie.html deleted file mode 100644 index 6a8f8b1a..00000000 --- a/tapir/statistics/templates/statistics/stats_for_marie.html +++ /dev/null @@ -1,46 +0,0 @@ -{% extends "core/base.html" %} -{% load statistics %} -{% load core %} -{% load django_bootstrap5 %} -{% load static %} -{% load i18n %} -{% load utils %} -{% block title %} - {% translate 'Stats for Marie' %} -{% endblock title %} -{% block head %} - - -{% endblock head %} -{% block content %} -
-
-
-
-
{% translate "Number of frozen members per month" %}
- downloadDownload CSV -
-
-

- {% translate "Number of frozen members per month" as chart_name %} - {% on_demand_chart chart_name "statistics:number_of_frozen_members_per_month_json" %} -

-
-
-
-
-
{% translate "Number of purchasing members per month" %}
- downloadDownload CSV -
-
-

- {% translate "Number of purchasing members per month" as chart_name %} - {% on_demand_chart chart_name "statistics:number_of_purchasing_members_per_month_json" %} -

-
-
-
-
-{% endblock content %} diff --git a/tapir/statistics/urls.py b/tapir/statistics/urls.py index 54fb6317..8d418d03 100644 --- a/tapir/statistics/urls.py +++ b/tapir/statistics/urls.py @@ -61,56 +61,6 @@ views.BasketSumEvolutionJsonView.as_view(), name="basket_sum_evolution_json", ), - path( - "shift_cancelling_rate", - views.ShiftCancellingRateView.as_view(), - name="shift_cancelling_rate", - ), - path( - "shift_cancelling_rate_json", - views.ShiftCancellingRateJsonView.as_view(), - name="shift_cancelling_rate_json", - ), - path( - "shift_count_by_category_json", - views.ShiftCountByCategoryJsonView.as_view(), - name="shift_count_by_category_json", - ), - path( - "state_distribution", - views.StateDistributionView.as_view(), - name="state_distribution", - ), - path( - "state_distribution_json", - views.StateDistributionJsonView.as_view(), - name="state_distribution_json", - ), - path( - "stats_for_marie", - views.StatsForMarieView.as_view(), - name="stats_for_marie", - ), - path( - "number_of_frozen_members_per_month_json", - views.NumberOfFrozenMembersPerMonthJsonView.as_view(), - name="number_of_frozen_members_per_month_json", - ), - path( - "number_of_frozen_members_per_month_csv", - views.NumberOfFrozenMembersPerMonthCsvView.as_view(), - name="number_of_frozen_members_per_month_csv", - ), - path( - "number_of_purchasing_members_per_month_json", - views.NumberOfPurchasingMembersPerMonthJsonView.as_view(), - name="number_of_purchasing_members_per_month_json", - ), - path( - "number_of_purchasing_members_per_month_csv", - views.NumberOfPurchasingMembersPerMonthCsvView.as_view(), - name="number_of_purchasing_members_per_month_csv", - ), path( "fancy_graph", fancy_graph.base_view.FancyGraphView.as_view(), diff --git a/tapir/statistics/views/cancellation_rate_view.py b/tapir/statistics/views/cancellation_rate_view.py deleted file mode 100644 index ca3ed55e..00000000 --- a/tapir/statistics/views/cancellation_rate_view.py +++ /dev/null @@ -1,230 +0,0 @@ -import datetime - -from chartjs.views import JSONView -from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin -from django.db.models import OuterRef, Subquery, CharField -from django.utils import timezone -from django.views import generic - -from tapir.settings import PERMISSION_SHIFTS_MANAGE -from tapir.shifts.models import ( - ShiftAttendance, - ShiftAttendanceMode, - ShiftUserData, -) -from tapir.shifts.services.is_shift_attendance_from_template_service import ( - IsShiftAttendanceFromTemplateService, -) -from tapir.shifts.services.shift_attendance_mode_service import ( - ShiftAttendanceModeService, -) -from tapir.statistics.utils import ( - build_line_chart_data, - FORMAT_TICKS_PERCENTAGE, -) -from tapir.utils.shortcuts import get_first_of_next_month - - -# The main statistic view is intended for members and is accessible for all. -# The views from this file are intended for deciders. They are not accessible for all to avoid confusion, -# as they may be less well presented or harder to interpret - - -class ShiftCancellingRateView( - LoginRequiredMixin, PermissionRequiredMixin, generic.TemplateView -): - permission_required = PERMISSION_SHIFTS_MANAGE - template_name = "statistics/shift_cancelling_rate.html" - - def get_context_data(self, **kwargs): - context_data = super().get_context_data(**kwargs) - - return context_data - - -class ShiftCountByCategoryJsonView( - LoginRequiredMixin, PermissionRequiredMixin, JSONView -): - permission_required = PERMISSION_SHIFTS_MANAGE - SELECTION_ABCD_MEMBERS = "ABCD members" - SELECTION_FLYING_MEMBERS = "Flying members" - SELECTION_FROZEN_MEMBERS = "Frozen members" - SELECTION_ABCD_SHIFTS = "ABCD shifts" - SELECTION_FLYING_SHIFTS = "Flying shifts" - SELECTIONS = [ - SELECTION_ABCD_MEMBERS, - SELECTION_FLYING_MEMBERS, - SELECTION_FROZEN_MEMBERS, - SELECTION_ABCD_SHIFTS, - SELECTION_FLYING_SHIFTS, - ] - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.dates_from_first_shift_to_today = None - - def get_context_data(self, **kwargs): - return build_line_chart_data( - x_axis_values=[ - date for date in self.get_and_cache_dates_from_first_shift_to_today() - ], - y_axis_values=self.get_data(), - data_labels=self.SELECTIONS, - ) - - def get_and_cache_dates_from_first_shift_to_today(self): - if self.dates_from_first_shift_to_today is None: - self.dates_from_first_shift_to_today = ( - self.get_dates_from_first_shift_to_today() - ) - return self.dates_from_first_shift_to_today - - @staticmethod - def get_dates_from_first_shift_to_today(): - current_date = datetime.date(year=2024, month=1, day=1) - end_date = timezone.now().date() + datetime.timedelta(days=1) - dates = [] - while current_date < end_date: - dates.append(current_date) - current_date = get_first_of_next_month(current_date) - - return dates - - def get_data(self): - return [ - self.get_cancel_count_for_selection(selection) - for selection in self.SELECTIONS - ] - - def get_cancel_count_for_selection(self, selection: str): - return [ - self.get_number_of_attendances_for_selection_at_date(selection, at_date) - for at_date in self.get_and_cache_dates_from_first_shift_to_today() - ] - - def get_number_of_attendances_for_selection_at_date(self, selection: str, at_date): - return self.get_attendances_for_selection_at_date(selection, at_date).count() - - @staticmethod - def filter_attendance_by_attendance_mode_of_member_at_date( - attendances, attendance_mode, at_date - ): - shift_user_datas = ShiftAttendanceModeService.annotate_shift_user_data_queryset_with_attendance_mode_at_datetime( - ShiftUserData.objects.all(), at_date - ) - attendances = attendances.annotate( - attendance_mode=Subquery( - shift_user_datas.filter(user_id=OuterRef("user_id")).values( - ShiftAttendanceModeService.ANNOTATION_SHIFT_ATTENDANCE_MODE_AT_DATE - ), - output_field=CharField(), - ) - ) - - return attendances.filter(attendance_mode=attendance_mode) - - @classmethod - def get_attendances_for_selection_at_date(cls, selection, at_date): - end_date = get_first_of_next_month(at_date) - datetime.timedelta(days=1) - attendances = ShiftAttendance.objects.exclude( - state=ShiftAttendance.State.PENDING - ).filter( - slot__shift__start_time__gte=at_date, - slot__shift__start_time__lte=end_date, - slot__shift__cancelled=False, - ) - - # Only pick one attendance per slot, choosing the most recently updated one - attendances = attendances.order_by("slot", "-last_state_update").distinct( - "slot" - ) - - if selection == cls.SELECTION_ABCD_MEMBERS: - attendances = cls.filter_attendance_by_attendance_mode_of_member_at_date( - attendances, ShiftAttendanceMode.REGULAR, at_date - ) - elif selection == cls.SELECTION_FLYING_MEMBERS: - attendances = cls.filter_attendance_by_attendance_mode_of_member_at_date( - attendances, ShiftAttendanceMode.FLYING, at_date - ) - elif selection == cls.SELECTION_FROZEN_MEMBERS: - attendances = cls.filter_attendance_by_attendance_mode_of_member_at_date( - attendances, ShiftAttendanceMode.FROZEN, at_date - ) - elif selection == cls.SELECTION_ABCD_SHIFTS: - attendances = ( - IsShiftAttendanceFromTemplateService.annotate_shift_attendances( - attendances - ) - ) - filters = { - IsShiftAttendanceFromTemplateService.ANNOTATION_IS_FROM_ATTENDANCE_TEMPLATE: True - } - attendances = attendances.filter(**filters) - elif selection == cls.SELECTION_FLYING_SHIFTS: - attendances = ( - IsShiftAttendanceFromTemplateService.annotate_shift_attendances( - attendances - ) - ) - filters = { - IsShiftAttendanceFromTemplateService.ANNOTATION_IS_FROM_ATTENDANCE_TEMPLATE: False - } - attendances = attendances.filter(**filters) - - return attendances - - -class ShiftCancellingRateJsonView( - LoginRequiredMixin, PermissionRequiredMixin, JSONView -): - permission_required = PERMISSION_SHIFTS_MANAGE - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.dates_from_first_shift_to_today = None - - def get_context_data(self, **kwargs): - return build_line_chart_data( - x_axis_values=[ - date for date in self.get_and_cache_dates_from_first_shift_to_today() - ], - y_axis_values=self.get_data(), - data_labels=ShiftCountByCategoryJsonView.SELECTIONS, - y_axis_min=0, - y_axis_max=1, - format_ticks=FORMAT_TICKS_PERCENTAGE, - ) - - def get_and_cache_dates_from_first_shift_to_today(self): - if self.dates_from_first_shift_to_today is None: - self.dates_from_first_shift_to_today = ( - ShiftCountByCategoryJsonView.get_dates_from_first_shift_to_today() - ) - return self.dates_from_first_shift_to_today - - def get_data(self): - return [ - self.get_cancel_rate_for_selection(selection) - for selection in ShiftCountByCategoryJsonView.SELECTIONS - ] - - def get_cancel_rate_for_selection(self, selection: str): - return [ - self.get_cancel_rate_for_selection_at_date(selection, at_date) - for at_date in self.get_and_cache_dates_from_first_shift_to_today() - ] - - @staticmethod - def get_cancel_rate_for_selection_at_date(selection, at_date): - attendances = ( - ShiftCountByCategoryJsonView.get_attendances_for_selection_at_date( - selection, at_date - ) - ) - - nb_total_attendances = attendances.count() - nb_attended = attendances.filter(state=ShiftAttendance.State.DONE).count() - nb_not_attended = nb_total_attendances - nb_attended - - return nb_not_attended / (nb_total_attendances or 1) diff --git a/tapir/statistics/views/state_distribution_view.py b/tapir/statistics/views/state_distribution_view.py deleted file mode 100644 index a9ffd752..00000000 --- a/tapir/statistics/views/state_distribution_view.py +++ /dev/null @@ -1,105 +0,0 @@ -import datetime - -from chartjs.views import JSONView -from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin -from django.utils import timezone -from django.views import generic - -from tapir.settings import PERMISSION_SHIFTS_MANAGE -from tapir.shifts.models import ( - ShiftAttendance, - Shift, -) -from tapir.statistics.utils import ( - build_line_chart_data, -) -from tapir.utils.shortcuts import get_first_of_next_month - - -# The main statistic view is intended for members and is accessible for all. -# The views from this file are intended for deciders. They are not accessible for all to avoid confusion, -# as they may be less well presented or harder to interpret - - -class StateDistributionView( - LoginRequiredMixin, PermissionRequiredMixin, generic.TemplateView -): - permission_required = PERMISSION_SHIFTS_MANAGE - template_name = "statistics/state_distribution.html" - - def get_context_data(self, **kwargs): - context_data = super().get_context_data(**kwargs) - - return context_data - - -class StateDistributionJsonView(LoginRequiredMixin, PermissionRequiredMixin, JSONView): - permission_required = PERMISSION_SHIFTS_MANAGE - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.dates_from_first_shift_to_today = None - - def get_context_data(self, **kwargs): - return build_line_chart_data( - x_axis_values=[ - date for date in self.get_and_cache_dates_from_first_shift_to_today() - ], - y_axis_values=self.get_data(), - data_labels=[ - "Pending", - "Attended", - "Cancelled", - "No show", - "Excused", - "Looking for standing", - ], - ) - - def get_and_cache_dates_from_first_shift_to_today(self): - if self.dates_from_first_shift_to_today is None: - self.dates_from_first_shift_to_today = ( - self.get_dates_from_first_shift_to_today() - ) - return self.dates_from_first_shift_to_today - - @staticmethod - def get_dates_from_first_shift_to_today(): - first_shift = Shift.objects.order_by("start_time").first() - if not first_shift: - return [] - - current_date = first_shift.start_time.date().replace(day=1) - end_date = timezone.now().date() + datetime.timedelta(days=1) - dates = [] - while current_date < end_date: - dates.append(current_date) - current_date = get_first_of_next_month(current_date) - - return dates - - def get_data(self): - return [ - self.get_ratio_for_state(state[0]) - for state in ShiftAttendance.State.choices - ] - - def get_ratio_for_state(self, state): - return [ - self.get_ratio_for_state_and_date(state, at_date) - for at_date in self.get_and_cache_dates_from_first_shift_to_today() - ] - - @classmethod - def get_ratio_for_state_and_date(cls, state, at_date): - end_date = get_first_of_next_month(at_date) - datetime.timedelta(days=1) - attendances = ShiftAttendance.objects.filter( - slot__shift__start_time__gte=at_date, - slot__shift__start_time__lte=end_date, - slot__shift__cancelled=False, - ) - - nb_total_attendances = attendances.count() - nb_attendances_of_state = attendances.filter(state=state).count() - - return nb_attendances_of_state diff --git a/tapir/statistics/views/stats_for_marie.py b/tapir/statistics/views/stats_for_marie.py deleted file mode 100644 index 0d5130d5..00000000 --- a/tapir/statistics/views/stats_for_marie.py +++ /dev/null @@ -1,267 +0,0 @@ -import csv -import datetime - -from chartjs.views import JSONView -from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin -from django.http import HttpResponse -from django.utils import timezone -from django.views import View -from django.views.generic import TemplateView - -from tapir.coop.models import ShareOwner, MemberStatus, ShareOwnership -from tapir.coop.services.investing_status_service import InvestingStatusService -from tapir.coop.services.member_can_shop_service import MemberCanShopService -from tapir.coop.services.membership_pause_service import MembershipPauseService -from tapir.coop.services.number_of_shares_service import NumberOfSharesService -from tapir.settings import PERMISSION_COOP_MANAGE -from tapir.shifts.models import ( - UpdateShiftUserDataLogEntry, - ShiftAttendanceMode, - ShiftUserData, -) -from tapir.shifts.services.shift_attendance_mode_service import ( - ShiftAttendanceModeService, -) -from tapir.statistics.utils import build_line_chart_data -from tapir.statistics.views import MainStatisticsView -from tapir.utils.shortcuts import ( - get_first_of_next_month, - get_last_day_of_month, - ensure_datetime, -) - - -class StatsForMarieView(LoginRequiredMixin, PermissionRequiredMixin, TemplateView): - permission_required = PERMISSION_COOP_MANAGE - template_name = "statistics/stats_for_marie.html" - - -class NumberOfFrozenMembersPerMonthJsonView( - LoginRequiredMixin, PermissionRequiredMixin, JSONView -): - permission_required = PERMISSION_COOP_MANAGE - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.dates_from_first_shift_to_today = None - - def get_context_data(self, **kwargs): - return build_line_chart_data( - x_axis_values=[ - date for date in self.get_and_cache_dates_from_first_frozen_to_today() - ], - y_axis_values=[ - self.get_data(self.get_and_cache_dates_from_first_frozen_to_today()) - ], - data_labels=["Number of frozen members"], - ) - - def get_and_cache_dates_from_first_frozen_to_today(self): - if self.dates_from_first_shift_to_today is None: - self.dates_from_first_shift_to_today = ( - self.get_dates_from_first_frozen_to_today() - ) - return self.dates_from_first_shift_to_today - - @staticmethod - def get_dates_from_first_frozen_to_today(): - first_frozen_log = ( - UpdateShiftUserDataLogEntry.objects.filter( - new_values__attendance_mode=ShiftAttendanceMode.FROZEN - ) - .order_by("created_date") - .first() - ) - if not first_frozen_log: - return [] - - current_date = first_frozen_log.created_date.date().replace(day=1) - end_date = timezone.now().date() + datetime.timedelta(days=1) - dates = [] - while current_date < end_date: - dates.append(current_date) - current_date = get_first_of_next_month(current_date) - - dates.append(get_last_day_of_month(timezone.now().date())) - - # we want the end of each month rather than the first of the month - dates = [date - datetime.timedelta(days=1) for date in dates] - - # limit to 6 month to avoid time out - dates = dates[-6:] - - return dates - - @staticmethod - def get_data(dates): - data = [] - for date in dates: - active_members = ShareOwner.objects.with_status( - MemberStatus.ACTIVE, at_datetime=date - ) - shift_user_datas = ShiftUserData.objects.filter( - user__share_owner__in=active_members - ) - shift_user_datas = ShiftAttendanceModeService.annotate_shift_user_data_queryset_with_attendance_mode_at_datetime( - shift_user_datas, date - ) - filter_args = { - ShiftAttendanceModeService.ANNOTATION_SHIFT_ATTENDANCE_MODE_AT_DATE: ShiftAttendanceMode.FROZEN - } - data.append(shift_user_datas.filter(**filter_args).count()) - return data - - -class NumberOfFrozenMembersPerMonthCsvView( - LoginRequiredMixin, PermissionRequiredMixin, View -): - permission_required = PERMISSION_COOP_MANAGE - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.dates_from_first_frozen_to_today = None - - def get_and_cache_dates_from_first_frozen_to_today(self): - if self.dates_from_first_frozen_to_today is None: - self.dates_from_first_frozen_to_today = ( - NumberOfFrozenMembersPerMonthJsonView.get_dates_from_first_frozen_to_today() - ) - return self.dates_from_first_frozen_to_today - - def get(self, *args, **kwargs): - response = HttpResponse( - content_type="text/csv", - headers={ - "Content-Disposition": 'attachment; filename="number_of_frozen_members_per_month.csv"' - }, - ) - - writer = csv.writer(response) - - dates = self.get_and_cache_dates_from_first_frozen_to_today() - data = NumberOfFrozenMembersPerMonthJsonView.get_data(dates) - - for index, date in enumerate(dates): - writer.writerow([date.strftime("%d/%m/%Y"), data[index]]) - - return response - - -class NumberOfPurchasingMembersPerMonthJsonView( - LoginRequiredMixin, PermissionRequiredMixin, JSONView -): - permission_required = PERMISSION_COOP_MANAGE - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.dates_from_first_share_to_today = None - - def get_context_data(self, **kwargs): - return build_line_chart_data( - x_axis_values=[ - date for date in self.get_and_cache_dates_from_first_share_to_today() - ], - y_axis_values=[ - self.get_data(self.get_and_cache_dates_from_first_share_to_today()) - ], - data_labels=["Number of purchasing members"], - ) - - def get_and_cache_dates_from_first_share_to_today(self): - if self.dates_from_first_share_to_today is None: - self.dates_from_first_share_to_today = ( - self.get_dates_from_first_share_to_today() - ) - return self.dates_from_first_share_to_today - - @staticmethod - def get_dates_from_first_share_to_today(): - first_share = ShareOwnership.objects.order_by("start_date").first() - if not first_share: - return [] - - current_date = first_share.start_date.replace(day=1) - end_date = timezone.now().date() + datetime.timedelta(days=1) - dates = [] - while current_date < end_date: - dates.append(current_date) - current_date = get_first_of_next_month(current_date) - - dates.append(get_first_of_next_month(timezone.now().date())) - - # we want the end of each month rather than the first of the month - dates = [date - datetime.timedelta(days=1) for date in dates] - - # limit to 6 month to avoid time out - dates = dates[-6:] - - return dates - - @staticmethod - def get_data(dates): - data = [] - for date in dates: - date_with_time = ensure_datetime(date) - share_owners = ( - ShareOwner.objects.all() - .prefetch_related("user") - .prefetch_related("user__shift_user_data") - .prefetch_related("share_ownerships") - ) - share_owners = NumberOfSharesService.annotate_share_owner_queryset_with_nb_of_active_shares( - share_owners, date - ) - share_owners = MembershipPauseService.annotate_share_owner_queryset_with_has_active_pause( - share_owners, date - ) - share_owners = InvestingStatusService.annotate_share_owner_queryset_with_investing_status_at_datetime( - share_owners, date_with_time - ) - share_owners = MainStatisticsView.annotate_attendance_modes( - share_owners, date - ) - - number_of_purchasing_members_at_date = len( - [ - share_owner - for share_owner in share_owners - if MemberCanShopService.can_shop(date_with_time) - ] - ) - data.append(number_of_purchasing_members_at_date) - return data - - -class NumberOfPurchasingMembersPerMonthCsvView( - LoginRequiredMixin, PermissionRequiredMixin, View -): - permission_required = PERMISSION_COOP_MANAGE - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.dates_from_first_share_to_today = None - - def get_and_cache_dates_from_first_share_to_today(self): - if self.dates_from_first_share_to_today is None: - self.dates_from_first_share_to_today = ( - NumberOfPurchasingMembersPerMonthJsonView.get_dates_from_first_share_to_today() - ) - return self.dates_from_first_share_to_today - - def get(self, *args, **kwargs): - response = HttpResponse( - content_type="text/csv", - headers={ - "Content-Disposition": 'attachment; filename="number_of_purchasing_members_per_month.csv"' - }, - ) - - writer = csv.writer(response) - - dates = self.get_and_cache_dates_from_first_share_to_today() - data = NumberOfPurchasingMembersPerMonthJsonView.get_data(dates) - - for index, date in enumerate(dates): - writer.writerow([date.strftime("%d/%m/%Y"), data[index]]) - - return response From 7a41b95f3b5791febc9012bf0f5bf04332d8555f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Wed, 11 Dec 2024 17:31:25 +0100 Subject: [PATCH 36/50] Removed old untested stats views. --- tapir/statistics/views/__init__.py | 3 - .../locale/de/LC_MESSAGES/django.po | 86 ++++++++----------- 2 files changed, 37 insertions(+), 52 deletions(-) diff --git a/tapir/statistics/views/__init__.py b/tapir/statistics/views/__init__.py index b269632f..e130b74a 100644 --- a/tapir/statistics/views/__init__.py +++ b/tapir/statistics/views/__init__.py @@ -1,4 +1 @@ -from .cancellation_rate_view import * from .main_view import * -from .state_distribution_view import * -from .stats_for_marie import * diff --git a/tapir/translations/locale/de/LC_MESSAGES/django.po b/tapir/translations/locale/de/LC_MESSAGES/django.po index 600ac3e8..e9e953bc 100644 --- a/tapir/translations/locale/de/LC_MESSAGES/django.po +++ b/tapir/translations/locale/de/LC_MESSAGES/django.po @@ -2,12 +2,12 @@ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. -# +# msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-11 16:55+0100\n" +"POT-Creation-Date: 2024-12-11 17:31+0100\n" "PO-Revision-Date: 2024-11-22 08:33+0100\n" "Last-Translator: \n" "Language-Team: \n" @@ -4766,53 +4766,6 @@ msgstr "" "Hier könnt ihr den Fortschritt der Finanzierungskampagne verfolgen. Es werden sowohl weitere Anteile gezählt (alle Anteile, die über den Pflichtanteil hinaus gezeichnet werden) als auch Nachrangdarlehen. Der Zeitraum läuft vom 12.09.2023 - 09.12.2023. Was eine:r nicht schafft, schaffen viele!\n" " " -#: statistics/templates/statistics/shift_cancelling_rate.html:9 -#, fuzzy -#| msgid "Shift attendance: %(name)s" -msgid "Shift attendance rates" -msgstr "Schicht-Anwesenheit: %(name)s" - -#: statistics/templates/statistics/shift_cancelling_rate.html:19 -#: statistics/templates/statistics/shift_cancelling_rate.html:69 -#, fuzzy -#| msgid "Creation date" -msgid "Shift cancellation rate" -msgstr "Erstellungsdatum" - -#: statistics/templates/statistics/shift_cancelling_rate.html:78 -#, fuzzy -#| msgid "Number of shares to create" -msgid "Number of shifts by category" -msgstr "Anzahl zu erstellender Anteile" - -#: statistics/templates/statistics/state_distribution.html:9 -#: statistics/templates/statistics/state_distribution.html:19 -#: statistics/templates/statistics/state_distribution.html:36 -#, fuzzy -#| msgid "Send me instructions!" -msgid "State distribution" -msgstr "Schickt mir eine Anleitung!" - -#: statistics/templates/statistics/stats_for_marie.html:9 -#, fuzzy -#| msgid "Statistics on shares" -msgid "Stats for Marie" -msgstr "Anteile-Statistiken" - -#: statistics/templates/statistics/stats_for_marie.html:20 -#: statistics/templates/statistics/stats_for_marie.html:26 -#, fuzzy -#| msgid "New members per month" -msgid "Number of frozen members per month" -msgstr "Neue Mitglieder*innen pro Monat" - -#: statistics/templates/statistics/stats_for_marie.html:33 -#: statistics/templates/statistics/stats_for_marie.html:39 -#, fuzzy -#| msgid "Current number of purchasing members" -msgid "Number of purchasing members per month" -msgstr "Aktuelle Anzahl von einkaufsberechtigten Mitgliedern" - #: statistics/templates/statistics/tags/on_demand_chart.html:5 msgid "Show graph: " msgstr "Grafik anzeigen: " @@ -5882,6 +5835,41 @@ msgstr "%(name)s ist kein Mitglied der Genossenschaft. Vielleicht haben sie dere msgid "%(name)s has not attended a welcome session yet. Make sure they plan to do it!" msgstr "%(name)s hat an dem Willkommenstreffen noch nicht teilgenommen. Stelle sicher, dass er*sie es entsprechend einplant!" +#, fuzzy +#~| msgid "Shift attendance: %(name)s" +#~ msgid "Shift attendance rates" +#~ msgstr "Schicht-Anwesenheit: %(name)s" + +#, fuzzy +#~| msgid "Creation date" +#~ msgid "Shift cancellation rate" +#~ msgstr "Erstellungsdatum" + +#, fuzzy +#~| msgid "Number of shares to create" +#~ msgid "Number of shifts by category" +#~ msgstr "Anzahl zu erstellender Anteile" + +#, fuzzy +#~| msgid "Send me instructions!" +#~ msgid "State distribution" +#~ msgstr "Schickt mir eine Anleitung!" + +#, fuzzy +#~| msgid "Statistics on shares" +#~ msgid "Stats for Marie" +#~ msgstr "Anteile-Statistiken" + +#, fuzzy +#~| msgid "New members per month" +#~ msgid "Number of frozen members per month" +#~ msgstr "Neue Mitglieder*innen pro Monat" + +#, fuzzy +#~| msgid "Current number of purchasing members" +#~ msgid "Number of purchasing members per month" +#~ msgstr "Aktuelle Anzahl von einkaufsberechtigten Mitgliedern" + #, fuzzy, python-format #~| msgid "" #~| "\n" From d6f1c5a53722eccb469e65ebb0d76557c76310bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Wed, 11 Dec 2024 17:37:59 +0100 Subject: [PATCH 37/50] Added tests for NumberOfPendingResignationsAtDateView --- ...est_number_of_pending_resignations_view.py | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 tapir/statistics/tests/fancy_graph/test_number_of_pending_resignations_view.py diff --git a/tapir/statistics/tests/fancy_graph/test_number_of_pending_resignations_view.py b/tapir/statistics/tests/fancy_graph/test_number_of_pending_resignations_view.py new file mode 100644 index 00000000..0d5101f8 --- /dev/null +++ b/tapir/statistics/tests/fancy_graph/test_number_of_pending_resignations_view.py @@ -0,0 +1,59 @@ +import datetime + +from django.utils import timezone + +from tapir.coop.tests.factories import MembershipResignationFactory +from tapir.statistics.views.fancy_graph.number_of_pending_resignations_view import ( + NumberOfPendingResignationsAtDateView, +) +from tapir.utils.tests_utils import ( + TapirFactoryTestBase, + mock_timezone_now, +) + + +class TestNumberOfPendingResignationsView(TapirFactoryTestBase): + NOW = datetime.datetime(year=2023, month=4, day=1, hour=12) + REFERENCE_TIME = timezone.make_aware( + datetime.datetime(year=2022, month=6, day=15, hour=12) + ) + + def setUp(self) -> None: + super().setUp() + self.NOW = mock_timezone_now(self, self.NOW) + + def test_calculateDatapoint_resignationIsPayedOut_notCounted(self): + MembershipResignationFactory.create( + cancellation_date=datetime.date(year=2022, month=5, day=1), + pay_out_day=datetime.date(year=2022, month=6, day=14), + ) + + result = NumberOfPendingResignationsAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(0, result) + + def test_calculateDatapoint_resignationIsPending_counted(self): + MembershipResignationFactory.create( + cancellation_date=datetime.date(year=2022, month=5, day=1), + pay_out_day=datetime.date(year=2022, month=6, day=16), + ) + + result = NumberOfPendingResignationsAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(1, result) + + def test_calculateDatapoint_resignationIsInTheFuture_counted(self): + MembershipResignationFactory.create( + cancellation_date=datetime.date(year=2022, month=6, day=16), + pay_out_day=datetime.date(year=2028, month=6, day=14), + ) + + result = NumberOfPendingResignationsAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(0, result) From 9554d38a9dcd3593cdfed307a4d6852b9ae58044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Wed, 11 Dec 2024 17:50:29 +0100 Subject: [PATCH 38/50] Added tests for NumberOfPurchasingMembersAtDateView --- .../test_number_of_purchasing_members_view.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tapir/statistics/tests/fancy_graph/test_number_of_purchasing_members_view.py diff --git a/tapir/statistics/tests/fancy_graph/test_number_of_purchasing_members_view.py b/tapir/statistics/tests/fancy_graph/test_number_of_purchasing_members_view.py new file mode 100644 index 00000000..d8b6411e --- /dev/null +++ b/tapir/statistics/tests/fancy_graph/test_number_of_purchasing_members_view.py @@ -0,0 +1,48 @@ +import datetime + +from django.utils import timezone + +from tapir.accounts.tests.factories.factories import TapirUserFactory +from tapir.coop.models import ShareOwner, ShareOwnership +from tapir.coop.services.member_can_shop_service import MemberCanShopService +from tapir.statistics.views.fancy_graph.number_of_purchasing_members_view import ( + NumberOfPurchasingMembersAtDateView, +) +from tapir.utils.tests_utils import TapirFactoryTestBase, mock_timezone_now + + +class TestNumberOfPurchasingMembersView(TapirFactoryTestBase): + NOW = datetime.datetime(year=2023, month=4, day=1, hour=12) + REFERENCE_TIME = timezone.make_aware( + datetime.datetime(year=2022, month=6, day=15, hour=12) + ) + + def setUp(self) -> None: + super().setUp() + self.NOW = mock_timezone_now(self, self.NOW) + + def create_member_that_can_shop(self): + tapir_user = TapirUserFactory.create(share_owner__is_investing=False) + ShareOwnership.objects.update(start_date=self.REFERENCE_TIME.date()) + self.assertTrue( + MemberCanShopService.can_shop(tapir_user.share_owner, self.REFERENCE_TIME) + ) + + def test_calculateDatapoint_memberCantShop_notCounted(self): + self.create_member_that_can_shop() + ShareOwner.objects.update(is_investing=True) + + result = NumberOfPurchasingMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(0, result) + + def test_calculateDatapoint_memberCanShop_counted(self): + self.create_member_that_can_shop() + + result = NumberOfPurchasingMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(1, result) From 7daa2e1832235c1e5b3ebf130716ac0b0bfc9a39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Wed, 11 Dec 2024 17:59:32 +0100 Subject: [PATCH 39/50] Added tests for NumberOfShiftPartnersAtDateView --- .../test_number_of_purchasing_members_view.py | 21 +++--- .../test_number_of_shift_partners_view.py | 67 +++++++++++++++++++ tapir/utils/tests_utils.py | 11 +++ 3 files changed, 86 insertions(+), 13 deletions(-) create mode 100644 tapir/statistics/tests/fancy_graph/test_number_of_shift_partners_view.py diff --git a/tapir/statistics/tests/fancy_graph/test_number_of_purchasing_members_view.py b/tapir/statistics/tests/fancy_graph/test_number_of_purchasing_members_view.py index d8b6411e..215edc64 100644 --- a/tapir/statistics/tests/fancy_graph/test_number_of_purchasing_members_view.py +++ b/tapir/statistics/tests/fancy_graph/test_number_of_purchasing_members_view.py @@ -2,13 +2,15 @@ from django.utils import timezone -from tapir.accounts.tests.factories.factories import TapirUserFactory -from tapir.coop.models import ShareOwner, ShareOwnership -from tapir.coop.services.member_can_shop_service import MemberCanShopService +from tapir.coop.models import ShareOwner from tapir.statistics.views.fancy_graph.number_of_purchasing_members_view import ( NumberOfPurchasingMembersAtDateView, ) -from tapir.utils.tests_utils import TapirFactoryTestBase, mock_timezone_now +from tapir.utils.tests_utils import ( + TapirFactoryTestBase, + mock_timezone_now, + create_member_that_can_shop, +) class TestNumberOfPurchasingMembersView(TapirFactoryTestBase): @@ -21,15 +23,8 @@ def setUp(self) -> None: super().setUp() self.NOW = mock_timezone_now(self, self.NOW) - def create_member_that_can_shop(self): - tapir_user = TapirUserFactory.create(share_owner__is_investing=False) - ShareOwnership.objects.update(start_date=self.REFERENCE_TIME.date()) - self.assertTrue( - MemberCanShopService.can_shop(tapir_user.share_owner, self.REFERENCE_TIME) - ) - def test_calculateDatapoint_memberCantShop_notCounted(self): - self.create_member_that_can_shop() + create_member_that_can_shop(self, self.REFERENCE_TIME) ShareOwner.objects.update(is_investing=True) result = NumberOfPurchasingMembersAtDateView().calculate_datapoint( @@ -39,7 +34,7 @@ def test_calculateDatapoint_memberCantShop_notCounted(self): self.assertEqual(0, result) def test_calculateDatapoint_memberCanShop_counted(self): - self.create_member_that_can_shop() + create_member_that_can_shop(self, self.REFERENCE_TIME) result = NumberOfPurchasingMembersAtDateView().calculate_datapoint( self.REFERENCE_TIME diff --git a/tapir/statistics/tests/fancy_graph/test_number_of_shift_partners_view.py b/tapir/statistics/tests/fancy_graph/test_number_of_shift_partners_view.py new file mode 100644 index 00000000..db69c7e8 --- /dev/null +++ b/tapir/statistics/tests/fancy_graph/test_number_of_shift_partners_view.py @@ -0,0 +1,67 @@ +import datetime + +from django.utils import timezone + +from tapir.statistics.views.fancy_graph.number_of_shift_partners_view import ( + NumberOfShiftPartnersAtDateView, +) +from tapir.utils.tests_utils import ( + TapirFactoryTestBase, + mock_timezone_now, + create_member_that_can_shop, +) + + +class TestNumberOfShiftPartnersView(TapirFactoryTestBase): + NOW = datetime.datetime(year=2023, month=4, day=1, hour=12) + REFERENCE_TIME = timezone.make_aware( + datetime.datetime(year=2022, month=6, day=15, hour=12) + ) + + def setUp(self) -> None: + super().setUp() + self.NOW = mock_timezone_now(self, self.NOW) + + def test_calculateDatapoint_memberHasPartnerButIsNotWorking_notCounted(self): + member_with_partner = create_member_that_can_shop(self, self.REFERENCE_TIME) + member_that_is_partner_of = create_member_that_can_shop( + self, self.REFERENCE_TIME + ) + member_with_partner.shift_user_data.shift_partner = ( + member_that_is_partner_of.shift_user_data + ) + member_with_partner.shift_user_data.save() + + member_with_partner.share_owner.is_investing = True + member_with_partner.share_owner.save() + + result = NumberOfShiftPartnersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(0, result) + + def test_calculateDatapoint_memberIsWorkingButHasNoPartner_notCounted(self): + create_member_that_can_shop(self, self.REFERENCE_TIME) + + result = NumberOfShiftPartnersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(0, result) + + def test_calculateDatapoint_memberHasIsWorkingAndHasAPartner_counted(self): + member_with_partner = create_member_that_can_shop(self, self.REFERENCE_TIME) + member_that_is_partner_of = create_member_that_can_shop( + self, self.REFERENCE_TIME + ) + member_with_partner.shift_user_data.shift_partner = ( + member_that_is_partner_of.shift_user_data + ) + member_with_partner.shift_user_data.save() + + result = NumberOfShiftPartnersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(1, result) diff --git a/tapir/utils/tests_utils.py b/tapir/utils/tests_utils.py index d7582e93..c0d4cb16 100644 --- a/tapir/utils/tests_utils.py +++ b/tapir/utils/tests_utils.py @@ -26,7 +26,9 @@ from tapir import settings from tapir.accounts.models import TapirUser from tapir.accounts.tests.factories.factories import TapirUserFactory +from tapir.coop.models import ShareOwnership from tapir.coop.pdfs import CONTENT_TYPE_PDF +from tapir.coop.services.member_can_shop_service import MemberCanShopService from tapir.core.tapir_email_base import TapirEmailBase from tapir.shifts.models import ( ShiftAttendanceTemplate, @@ -311,3 +313,12 @@ def create_attendance_template_log_entry_in_the_past( log_entry.save() log_entry.created_date = reference_datetime - datetime.timedelta(days=1) log_entry.save() + + +def create_member_that_can_shop(test, reference_time): + tapir_user = TapirUserFactory.create(share_owner__is_investing=False) + ShareOwnership.objects.update(start_date=reference_time.date()) + test.assertTrue( + MemberCanShopService.can_shop(tapir_user.share_owner, reference_time) + ) + return tapir_user From 120443065f8ce4cda6428c049c95053d35e390da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Wed, 11 Dec 2024 19:24:49 +0100 Subject: [PATCH 40/50] WIP ShiftPartnerHistoryService --- .../services/co_purchaser_history_service.py | 8 +- .../test_co_purchaser_history_service.py | 8 +- .../coop/services/investing_status_service.py | 2 +- tapir/coop/views/statistics.py | 9 +- .../services/frozen_status_history_service.py | 2 +- .../services/shift_partner_history_service.py | 44 ++++-- .../test_shift_partner_history_service.py | 145 ++++++++++++++++++ tapir/statistics/views/main_view.py | 4 +- 8 files changed, 197 insertions(+), 25 deletions(-) create mode 100644 tapir/shifts/tests/test_shift_partner_history_service.py diff --git a/tapir/accounts/services/co_purchaser_history_service.py b/tapir/accounts/services/co_purchaser_history_service.py index a90442d4..4274267c 100644 --- a/tapir/accounts/services/co_purchaser_history_service.py +++ b/tapir/accounts/services/co_purchaser_history_service.py @@ -35,11 +35,11 @@ def annotate_tapir_user_queryset_with_has_co_purchaser_at_date( co_purchaser_from_log_entry=Subquery( UpdateTapirUserLogEntry.objects.filter( user_id=OuterRef("id"), - created_date__lte=at_datetime, - new_values__co_purchaser__isnull=False, + created_date__gte=at_datetime, + old_values__has_key="co_purchaser", ) - .order_by("-created_date") - .values("new_values__co_purchaser")[:1], + .order_by("created_date") + .values("old_values__co_purchaser")[:1], output_field=CharField(), ) ) diff --git a/tapir/accounts/tests/test_co_purchaser_history_service.py b/tapir/accounts/tests/test_co_purchaser_history_service.py index 0ce21bd1..f2dfe2a8 100644 --- a/tapir/accounts/tests/test_co_purchaser_history_service.py +++ b/tapir/accounts/tests/test_co_purchaser_history_service.py @@ -48,7 +48,7 @@ def test_annotateTapirUserQuerysetWithHasCoPurchaserAtDate_noRelevantLogEntriesA queryset.get(), CoPurchaserHistoryService.ANNOTATION_HAS_CO_PURCHASER ) ) - self.assertTrue( + self.assertEqual( self.REFERENCE_TIME, getattr( queryset.get(), @@ -72,7 +72,7 @@ def test_annotateTapirUserQuerysetWithHasCoPurchaserAtDate_noRelevantLogEntriesA queryset.get(), CoPurchaserHistoryService.ANNOTATION_HAS_CO_PURCHASER ) ) - self.assertTrue( + self.assertEqual( self.REFERENCE_TIME, getattr( queryset.get(), @@ -116,7 +116,7 @@ def test_annotateTapirUserQuerysetWithHasCoPurchaserAtDate_hasRelevantLogEntries queryset.get(), CoPurchaserHistoryService.ANNOTATION_HAS_CO_PURCHASER ) ) - self.assertTrue( + self.assertEqual( self.REFERENCE_TIME, getattr( queryset.get(), @@ -160,7 +160,7 @@ def test_annotateTapirUserQuerysetWithHasCoPurchaserAtDate_hasRelevantLogEntries queryset.get(), CoPurchaserHistoryService.ANNOTATION_HAS_CO_PURCHASER ) ) - self.assertTrue( + self.assertEqual( self.REFERENCE_TIME, getattr( queryset.get(), diff --git a/tapir/coop/services/investing_status_service.py b/tapir/coop/services/investing_status_service.py index de8aa044..50a5b33c 100644 --- a/tapir/coop/services/investing_status_service.py +++ b/tapir/coop/services/investing_status_service.py @@ -54,7 +54,7 @@ def annotate_share_owner_queryset_with_investing_status_at_datetime( was_investing_as_string=UpdateShareOwnerLogEntry.objects.filter( Q(share_owner_id=OuterRef("id")) | Q(user_id=OuterRef("user_id")), created_date__gte=at_datetime, - old_values__is_investing__isnull=False, + old_values__has_key="is_investing", ) .order_by("created_date") .values("old_values__is_investing")[:1] diff --git a/tapir/coop/views/statistics.py b/tapir/coop/views/statistics.py index 90a312d5..b87ed9e4 100644 --- a/tapir/coop/views/statistics.py +++ b/tapir/coop/views/statistics.py @@ -347,8 +347,9 @@ def get_data(cls): @staticmethod def get_member_status_updates(): - filters = Q(old_values__is_investing=False) | Q(old_values__is_investing=True) - return UpdateShareOwnerLogEntry.objects.filter(filters) + return UpdateShareOwnerLogEntry.objects.filter( + old_values__has_key="is_investing" + ) @staticmethod def filter_status_updates_per_member(updates, member: ShareOwner): @@ -506,7 +507,7 @@ def get_number_of_co_purchasers_per_month(cls) -> dict: all_tapir_users = TapirUser.objects.all() first_update = ( UpdateTapirUserLogEntry.objects.filter( - new_values__co_purchaser__isnull=False, + new_values__has_key="co_purchaser", ) .order_by("created_date") .first() @@ -519,7 +520,7 @@ def get_number_of_co_purchasers_per_month(cls) -> dict: co_purchaser_updates = ( UpdateTapirUserLogEntry.objects.filter( - new_values__co_purchaser__isnull=False, + new_values__has_key="co_purchaser", ) .order_by("created_date") .prefetch_related("user") diff --git a/tapir/shifts/services/frozen_status_history_service.py b/tapir/shifts/services/frozen_status_history_service.py index 878a6f5b..8a1fdd53 100644 --- a/tapir/shifts/services/frozen_status_history_service.py +++ b/tapir/shifts/services/frozen_status_history_service.py @@ -63,7 +63,7 @@ def annotate_shift_user_data_queryset_with_is_frozen_at_datetime( UpdateShiftUserDataLogEntry.objects.filter( user_id=OuterRef("user_id"), created_date__gte=at_datetime, - old_values__is_frozen__isnull=False, + old_values__has_key="is_frozen", ) .order_by("created_date") .values("old_values__is_frozen")[:1], diff --git a/tapir/shifts/services/shift_partner_history_service.py b/tapir/shifts/services/shift_partner_history_service.py index 70a04cf8..b71c6c79 100644 --- a/tapir/shifts/services/shift_partner_history_service.py +++ b/tapir/shifts/services/shift_partner_history_service.py @@ -2,7 +2,16 @@ import datetime -from django.db.models import Value, OuterRef, Case, When, QuerySet, Q +from django.db.models import ( + Value, + OuterRef, + Case, + When, + QuerySet, + Subquery, + Exists, +) +from django.db.models.fields import CharField from django.utils import timezone from tapir.shifts.models import ShiftUserData, UpdateShiftUserDataLogEntry @@ -45,20 +54,37 @@ def annotate_shift_user_data_queryset_with_has_shift_partner_at_date( if at_datetime is None: at_datetime = timezone.now() + relevant_logs = UpdateShiftUserDataLogEntry.objects.filter( + user_id=OuterRef("user_id"), + created_date__gte=at_datetime, + old_values__has_key="shift_partner", + ) + subquery_shift_partner_from_log = Subquery( + relevant_logs.order_by("created_date").values("old_values__shift_partner")[ + :1 + ], + output_field=CharField(), + ) + queryset = queryset.annotate( - shift_partner_at_date=UpdateShiftUserDataLogEntry.objects.filter( - user_id=OuterRef("user_id"), - created_date__lte=at_datetime, - new_values__shift_partner__isnull=False, - ) - .order_by("-created_date") - .values("new_values__shift_partner")[:1] + shift_partner_from_log=subquery_shift_partner_from_log, + shift_partner_log_found=Exists(relevant_logs), ) return queryset.annotate( **{ cls.ANNOTATION_HAS_SHIFT_PARTNER: Case( - When(~Q(shift_partner_at_date="None"), then=True), default=False + When( + shift_partner_log_found=True, + then=Case( + When( + shift_partner_from_log=None, + then=False, + ), + default=True, + ), + ), + default=Case(When(shift_partner=None, then=False), default=True), ), cls.ANNOTATION_HAS_SHIFT_PARTNER_DATE_CHECK: Value(at_datetime), }, diff --git a/tapir/shifts/tests/test_shift_partner_history_service.py b/tapir/shifts/tests/test_shift_partner_history_service.py new file mode 100644 index 00000000..354e72d0 --- /dev/null +++ b/tapir/shifts/tests/test_shift_partner_history_service.py @@ -0,0 +1,145 @@ +import datetime + +from django.utils import timezone + +from tapir.accounts.tests.factories.factories import TapirUserFactory +from tapir.shifts.models import UpdateShiftUserDataLogEntry, ShiftUserData +from tapir.shifts.services.shift_partner_history_service import ( + ShiftPartnerHistoryService, +) +from tapir.utils.tests_utils import TapirFactoryTestBase, mock_timezone_now + + +class TestShiftPartnerHistoryService(TapirFactoryTestBase): + NOW = datetime.datetime(year=2022, month=7, day=13, hour=12) + REFERENCE_TIME = timezone.make_aware( + datetime.datetime(year=2022, month=5, day=21, hour=15) + ) + + def setUp(self) -> None: + super().setUp() + self.NOW = mock_timezone_now(self, self.NOW) + + @staticmethod + def create_irrelevant_log_entry(tapir_user, reference_time): + log_entry = UpdateShiftUserDataLogEntry.objects.create( + user=tapir_user, + old_values={"is_frozen": False}, + new_values={"is_frozen": True}, + ) + log_entry.created_date = reference_time - datetime.timedelta(hours=5) + log_entry.save() + + def test_annotateShiftUserDataQuerysetWithHasShiftPartnerAtDate_noRelevantLogEntriesAndMemberHasShiftPartner_annotatesTrue( + self, + ): + member_with_partner = TapirUserFactory.create() + member_that_is_partner_of = TapirUserFactory.create() + member_with_partner.shift_user_data.shift_partner = ( + member_that_is_partner_of.shift_user_data + ) + member_with_partner.shift_user_data.save() + + self.create_irrelevant_log_entry(member_with_partner, self.REFERENCE_TIME) + + queryset = ShiftPartnerHistoryService.annotate_shift_user_data_queryset_with_has_shift_partner_at_date( + ShiftUserData.objects.all(), + self.REFERENCE_TIME, + ) + + self.assertTrue( + getattr( + queryset.get(user=member_with_partner), + ShiftPartnerHistoryService.ANNOTATION_HAS_SHIFT_PARTNER, + ) + ) + self.assertFalse( + getattr( + queryset.get(user=member_that_is_partner_of), + ShiftPartnerHistoryService.ANNOTATION_HAS_SHIFT_PARTNER, + ) + ) + self.assertEqual( + self.REFERENCE_TIME, + getattr( + queryset.get(user=member_with_partner), + ShiftPartnerHistoryService.ANNOTATION_HAS_SHIFT_PARTNER_DATE_CHECK, + ), + ) + + def test_annotateShiftUserDataQuerysetWithHasShiftPartnerAtDate_noRelevantLogEntriesAndMemberHasNoShiftPartner_annotatesFalse( + self, + ): + member_with_partner = TapirUserFactory.create() + self.create_irrelevant_log_entry(member_with_partner, self.REFERENCE_TIME) + + queryset = ShiftPartnerHistoryService.annotate_shift_user_data_queryset_with_has_shift_partner_at_date( + ShiftUserData.objects.all(), self.REFERENCE_TIME + ) + + self.assertFalse( + getattr( + queryset.get(), ShiftPartnerHistoryService.ANNOTATION_HAS_SHIFT_PARTNER + ) + ) + self.assertEqual( + self.REFERENCE_TIME, + getattr( + queryset.get(), + ShiftPartnerHistoryService.ANNOTATION_HAS_SHIFT_PARTNER_DATE_CHECK, + ), + ) + + def test_annotateShiftUserDataQuerysetWithHasShiftPartnerAtDate_hasRelevantLogEntriesWithShiftPartner_annotatesTrue( + self, + ): + member_with_partner = TapirUserFactory.create() + self.create_irrelevant_log_entry(member_with_partner, self.REFERENCE_TIME) + + log_entry = UpdateShiftUserDataLogEntry.objects.create( + user=member_with_partner, + old_values={"shift_partner": 120}, + new_values={"shift_partner": None}, + ) + log_entry.created_date = self.REFERENCE_TIME + datetime.timedelta(hours=5) + log_entry.save() + + queryset = ShiftPartnerHistoryService.annotate_shift_user_data_queryset_with_has_shift_partner_at_date( + ShiftUserData.objects.all(), self.REFERENCE_TIME + ) + + self.assertTrue( + getattr( + queryset.get(), ShiftPartnerHistoryService.ANNOTATION_HAS_SHIFT_PARTNER + ) + ) + + def test_annotateShiftUserDataQuerysetWithHasShiftPartnerAtDate_hasRelevantLogEntriesWithNoShiftPartner_annotatesFalse( + self, + ): + member_with_partner = TapirUserFactory.create() + member_that_is_partner_of = TapirUserFactory.create() + member_with_partner.shift_user_data.shift_partner = ( + member_that_is_partner_of.shift_user_data + ) + member_with_partner.shift_user_data.save() + self.create_irrelevant_log_entry(member_with_partner, self.REFERENCE_TIME) + + log_entry = UpdateShiftUserDataLogEntry.objects.create( + user=member_with_partner, + old_values={"shift_partner": None}, + new_values={"shift_partner": 120}, + ) + log_entry.created_date = self.REFERENCE_TIME + datetime.timedelta(hours=5) + log_entry.save() + + queryset = ShiftPartnerHistoryService.annotate_shift_user_data_queryset_with_has_shift_partner_at_date( + ShiftUserData.objects.all(), self.REFERENCE_TIME + ) + + self.assertFalse( + getattr( + queryset.get(user=member_with_partner), + ShiftPartnerHistoryService.ANNOTATION_HAS_SHIFT_PARTNER, + ) + ) diff --git a/tapir/statistics/views/main_view.py b/tapir/statistics/views/main_view.py index 5986a7ac..4b9a8249 100644 --- a/tapir/statistics/views/main_view.py +++ b/tapir/statistics/views/main_view.py @@ -406,7 +406,7 @@ def get_context_data(self, **kwargs): def get_dates(self): first_update = ( UpdateTapirUserLogEntry.objects.filter( - new_values__co_purchaser__isnull=False, + new_values__has_key="co_purchaser", ) .order_by("created_date") .first() @@ -433,7 +433,7 @@ def get_percentage_of_co_purchasers_per_month(self): co_purchaser_updates = ( UpdateTapirUserLogEntry.objects.filter( - new_values__co_purchaser__isnull=False, + new_values__has_key="co_purchaser", ) .order_by("created_date") .select_related("user") From 653b43297b7e8929e89cd5b8a39f7436118d0729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Thu, 12 Dec 2024 10:58:52 +0100 Subject: [PATCH 41/50] Finished tests for ShiftPartnerHistoryService --- tapir/coop/services/member_can_shop_service.py | 2 ++ .../tests/fancy_graph/test_number_of_shift_partners_view.py | 2 +- tapir/utils/tests_utils.py | 5 ++++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tapir/coop/services/member_can_shop_service.py b/tapir/coop/services/member_can_shop_service.py index 823be5ee..53b9157b 100644 --- a/tapir/coop/services/member_can_shop_service.py +++ b/tapir/coop/services/member_can_shop_service.py @@ -22,6 +22,8 @@ def can_shop( return False if not share_owner.is_active(at_datetime): return False + if share_owner.user.date_joined > at_datetime: + return False member_object = share_owner if not hasattr( diff --git a/tapir/statistics/tests/fancy_graph/test_number_of_shift_partners_view.py b/tapir/statistics/tests/fancy_graph/test_number_of_shift_partners_view.py index db69c7e8..ce554e8a 100644 --- a/tapir/statistics/tests/fancy_graph/test_number_of_shift_partners_view.py +++ b/tapir/statistics/tests/fancy_graph/test_number_of_shift_partners_view.py @@ -50,7 +50,7 @@ def test_calculateDatapoint_memberIsWorkingButHasNoPartner_notCounted(self): self.assertEqual(0, result) - def test_calculateDatapoint_memberHasIsWorkingAndHasAPartner_counted(self): + def test_calculateDatapoint_memberIsWorkingAndHasAPartner_counted(self): member_with_partner = create_member_that_can_shop(self, self.REFERENCE_TIME) member_that_is_partner_of = create_member_that_can_shop( self, self.REFERENCE_TIME diff --git a/tapir/utils/tests_utils.py b/tapir/utils/tests_utils.py index c0d4cb16..e0392428 100644 --- a/tapir/utils/tests_utils.py +++ b/tapir/utils/tests_utils.py @@ -316,7 +316,10 @@ def create_attendance_template_log_entry_in_the_past( def create_member_that_can_shop(test, reference_time): - tapir_user = TapirUserFactory.create(share_owner__is_investing=False) + tapir_user = TapirUserFactory.create( + share_owner__is_investing=False, + date_joined=reference_time - datetime.timedelta(hours=1), + ) ShareOwnership.objects.update(start_date=reference_time.date()) test.assertTrue( MemberCanShopService.can_shop(tapir_user.share_owner, reference_time) From cea7754292762596ae445daa1bbf87ce3facf2e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Thu, 12 Dec 2024 11:02:58 +0100 Subject: [PATCH 42/50] Added tests for NumberOfWorkingMembersAtDateView --- .../test_number_of_working_members_view.py | 43 ++++++++++++++ .../number_of_working_members_view.py | 57 ++----------------- 2 files changed, 48 insertions(+), 52 deletions(-) create mode 100644 tapir/statistics/tests/fancy_graph/test_number_of_working_members_view.py diff --git a/tapir/statistics/tests/fancy_graph/test_number_of_working_members_view.py b/tapir/statistics/tests/fancy_graph/test_number_of_working_members_view.py new file mode 100644 index 00000000..39716371 --- /dev/null +++ b/tapir/statistics/tests/fancy_graph/test_number_of_working_members_view.py @@ -0,0 +1,43 @@ +import datetime + +from django.utils import timezone + +from tapir.coop.models import ShareOwner +from tapir.statistics.views.fancy_graph.number_of_working_members_view import ( + NumberOfWorkingMembersAtDateView, +) +from tapir.utils.tests_utils import ( + TapirFactoryTestBase, + mock_timezone_now, + create_member_that_can_shop, +) + + +class TestNumberOfWorkingMembersView(TapirFactoryTestBase): + NOW = datetime.datetime(year=2023, month=4, day=1, hour=12) + REFERENCE_TIME = timezone.make_aware( + datetime.datetime(year=2022, month=6, day=15, hour=12) + ) + + def setUp(self) -> None: + super().setUp() + self.NOW = mock_timezone_now(self, self.NOW) + + def test_calculateDatapoint_memberIsWorking_counted(self): + create_member_that_can_shop(self, self.REFERENCE_TIME) + + result = NumberOfWorkingMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(1, result) + + def test_calculateDatapoint_memberIsNotWorking_notCounted(self): + create_member_that_can_shop(self, self.REFERENCE_TIME) + ShareOwner.objects.update(is_investing=True) + + result = NumberOfWorkingMembersAtDateView().calculate_datapoint( + self.REFERENCE_TIME + ) + + self.assertEqual(0, result) diff --git a/tapir/statistics/views/fancy_graph/number_of_working_members_view.py b/tapir/statistics/views/fancy_graph/number_of_working_members_view.py index a16f0423..36dcb389 100644 --- a/tapir/statistics/views/fancy_graph/number_of_working_members_view.py +++ b/tapir/statistics/views/fancy_graph/number_of_working_members_view.py @@ -1,65 +1,18 @@ import datetime -from tapir.coop.models import ShareOwner -from tapir.coop.services.investing_status_service import InvestingStatusService -from tapir.coop.services.membership_pause_service import MembershipPauseService -from tapir.coop.services.number_of_shares_service import NumberOfSharesService from tapir.shifts.models import ShiftUserData -from tapir.shifts.services.frozen_status_history_service import ( - FrozenStatusHistoryService, -) from tapir.shifts.services.shift_expectation_service import ShiftExpectationService from tapir.statistics.views.fancy_graph.base_view import DatapointView -from tapir.utils.shortcuts import transfer_attributes class NumberOfWorkingMembersAtDateView(DatapointView): def calculate_datapoint(self, reference_time: datetime.datetime) -> int: - reference_date = reference_time.date() - - shift_user_datas = ( - ShiftUserData.objects.filter(user__share_owner__isnull=False) - .prefetch_related("user") - .prefetch_related("user__share_owner") - .prefetch_related("user__share_owner__share_ownerships") - .prefetch_related("shift_exemptions") + queryset = ShiftExpectationService.annotate_shift_user_data_queryset_with_working_status_at_datetime( + ShiftUserData.objects.all(), reference_time ) - shift_user_datas = FrozenStatusHistoryService.annotate_shift_user_data_queryset_with_is_frozen_at_datetime( - shift_user_datas, reference_time - ).distinct() - share_owners = NumberOfSharesService.annotate_share_owner_queryset_with_nb_of_active_shares( - ShareOwner.objects.all(), reference_date - ) - share_owners = ( - MembershipPauseService.annotate_share_owner_queryset_with_has_active_pause( - share_owners, reference_date - ) + queryset = queryset.filter( + **{ShiftExpectationService.ANNOTATION_IS_WORKING_AT_DATE: True} ) - share_owners = InvestingStatusService.annotate_share_owner_queryset_with_investing_status_at_datetime( - share_owners, reference_time - ) - share_owners = {share_owner.id: share_owner for share_owner in share_owners} - for shift_user_data in shift_user_datas: - transfer_attributes( - share_owners[shift_user_data.user.share_owner.id], - shift_user_data.user.share_owner, - [ - NumberOfSharesService.ANNOTATION_NUMBER_OF_ACTIVE_SHARES, - NumberOfSharesService.ANNOTATION_SHARES_ACTIVE_AT_DATE, - MembershipPauseService.ANNOTATION_HAS_ACTIVE_PAUSE, - MembershipPauseService.ANNOTATION_HAS_ACTIVE_PAUSE_AT_DATE, - InvestingStatusService.ANNOTATION_WAS_INVESTING, - InvestingStatusService.ANNOTATION_WAS_INVESTING_AT_DATE, - ], - ) - return len( - [ - shift_user_data - for shift_user_data in shift_user_datas - if ShiftExpectationService.is_member_expected_to_do_shifts( - shift_user_data, reference_time - ) - ] - ) + return queryset.count() From fcb84c89e16c83af80d9f0a32c1c55c81b0d183b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Thu, 12 Dec 2024 11:04:32 +0100 Subject: [PATCH 43/50] Removed unused function --- .../services/shift_partner_history_service.py | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/tapir/shifts/services/shift_partner_history_service.py b/tapir/shifts/services/shift_partner_history_service.py index b71c6c79..aa9ed9c0 100644 --- a/tapir/shifts/services/shift_partner_history_service.py +++ b/tapir/shifts/services/shift_partner_history_service.py @@ -21,30 +21,6 @@ class ShiftPartnerHistoryService: ANNOTATION_HAS_SHIFT_PARTNER = "has_shift_partner" ANNOTATION_HAS_SHIFT_PARTNER_DATE_CHECK = "has_shift_partner_date_check" - @classmethod - def has_shift_partner( - cls, shift_user_data: ShiftUserData, at_datetime: datetime.datetime = None - ): - if at_datetime is None: - at_datetime = timezone.now() - - if not hasattr(shift_user_data, cls.ANNOTATION_HAS_SHIFT_PARTNER): - shift_user_data = ( - cls.annotate_shift_user_data_queryset_with_has_shift_partner_at_date( - ShiftUserData.objects.filter(id=shift_user_data.id), at_datetime - ).first() - ) - - annotated_date = getattr( - shift_user_data, cls.ANNOTATION_HAS_SHIFT_PARTNER_DATE_CHECK - ) - if annotated_date != at_datetime: - raise ValueError( - f"Trying to get 'has shift partner' at date {at_datetime}, but the queryset has been " - f"annotated relative to {annotated_date}" - ) - return getattr(shift_user_data, cls.ANNOTATION_HAS_SHIFT_PARTNER) - @classmethod def annotate_shift_user_data_queryset_with_has_shift_partner_at_date( cls, From 4c77d880edcc46d9229bc8bae879787e8d97dcb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Thu, 12 Dec 2024 11:52:14 +0100 Subject: [PATCH 44/50] Used test functions create_member_that_is_working and create_member_that_can_shop in a few more places. --- .../tests/test_member_can_shop_service.py | 4 ++- .../test_number_of_abcd_members_view.py | 16 ++++------ .../test_number_of_co_purchasers_view.py | 29 ++++++++----------- .../test_number_of_exempted_members_view.py | 19 ++++-------- .../test_number_of_flying_members_view.py | 10 ++----- .../test_number_of_frozen_members_view.py | 4 +-- .../test_number_of_shift_partners_view.py | 12 ++++---- .../test_number_of_working_members_view.py | 6 ++-- tapir/utils/tests_utils.py | 15 ++++++++++ 9 files changed, 55 insertions(+), 60 deletions(-) diff --git a/tapir/coop/tests/test_member_can_shop_service.py b/tapir/coop/tests/test_member_can_shop_service.py index 3b7ba34b..989dc7b0 100644 --- a/tapir/coop/tests/test_member_can_shop_service.py +++ b/tapir/coop/tests/test_member_can_shop_service.py @@ -17,7 +17,9 @@ def test_canShop_memberCanShop_annotatedWithTrue( self, ): tapir_user = TapirUserFactory.create( - share_owner__nb_shares=1, share_owner__is_investing=False + share_owner__nb_shares=1, + share_owner__is_investing=False, + date_joined=self.REFERENCE_TIME - datetime.timedelta(hours=1), ) ShareOwnership.objects.update( start_date=self.REFERENCE_TIME.date() - datetime.timedelta(days=1) diff --git a/tapir/statistics/tests/fancy_graph/test_number_of_abcd_members_view.py b/tapir/statistics/tests/fancy_graph/test_number_of_abcd_members_view.py index 711535c9..10e29c1e 100644 --- a/tapir/statistics/tests/fancy_graph/test_number_of_abcd_members_view.py +++ b/tapir/statistics/tests/fancy_graph/test_number_of_abcd_members_view.py @@ -2,7 +2,7 @@ from django.utils import timezone -from tapir.accounts.tests.factories.factories import TapirUserFactory +from tapir.coop.models import ShareOwner from tapir.shifts.models import CreateShiftAttendanceTemplateLogEntry from tapir.statistics.views.fancy_graph.number_of_abcd_members_view import ( NumberOfAbcdMembersAtDateView, @@ -11,6 +11,7 @@ TapirFactoryTestBase, mock_timezone_now, create_attendance_template_log_entry_in_the_past, + create_member_that_is_working, ) @@ -25,9 +26,8 @@ def setUp(self) -> None: self.NOW = mock_timezone_now(self, self.NOW) def test_calculateDatapoint_memberIsAbcdButIsNotWorking_notCounted(self): - tapir_user = TapirUserFactory.create( - date_joined=self.REFERENCE_TIME + datetime.timedelta(days=1) - ) + tapir_user = create_member_that_is_working(self, self.REFERENCE_TIME) + ShareOwner.objects.update(is_investing=True) create_attendance_template_log_entry_in_the_past( CreateShiftAttendanceTemplateLogEntry, tapir_user, self.REFERENCE_TIME ) @@ -39,9 +39,7 @@ def test_calculateDatapoint_memberIsAbcdButIsNotWorking_notCounted(self): self.assertEqual(0, result) def test_calculateDatapoint_memberIsWorkingButIsNotAbcd_notCounted(self): - TapirUserFactory.create( - date_joined=self.REFERENCE_TIME - datetime.timedelta(days=1) - ) + create_member_that_is_working(self, self.REFERENCE_TIME) result = NumberOfAbcdMembersAtDateView().calculate_datapoint( self.REFERENCE_TIME @@ -50,9 +48,7 @@ def test_calculateDatapoint_memberIsWorkingButIsNotAbcd_notCounted(self): self.assertEqual(0, result) def test_calculateDatapoint_memberIsWorkingAndAbcd_counted(self): - tapir_user = TapirUserFactory.create( - date_joined=self.REFERENCE_TIME - datetime.timedelta(days=1) - ) + tapir_user = create_member_that_is_working(self, self.REFERENCE_TIME) create_attendance_template_log_entry_in_the_past( CreateShiftAttendanceTemplateLogEntry, tapir_user, self.REFERENCE_TIME ) diff --git a/tapir/statistics/tests/fancy_graph/test_number_of_co_purchasers_view.py b/tapir/statistics/tests/fancy_graph/test_number_of_co_purchasers_view.py index dbcee24d..c006faca 100644 --- a/tapir/statistics/tests/fancy_graph/test_number_of_co_purchasers_view.py +++ b/tapir/statistics/tests/fancy_graph/test_number_of_co_purchasers_view.py @@ -2,13 +2,15 @@ from django.utils import timezone -from tapir.accounts.tests.factories.factories import TapirUserFactory +from tapir.accounts.models import TapirUser +from tapir.coop.models import ShareOwner from tapir.statistics.views.fancy_graph.number_of_co_purchasers_view import ( NumberOfCoPurchasersAtDateView, ) from tapir.utils.tests_utils import ( TapirFactoryTestBase, mock_timezone_now, + create_member_that_can_shop, ) @@ -22,11 +24,10 @@ def setUp(self) -> None: super().setUp() self.NOW = mock_timezone_now(self, self.NOW) - def test_calculateDatapoint_memberHasCoPurchaserButIsNotWorking_notCounted(self): - TapirUserFactory.create( - date_joined=self.REFERENCE_TIME + datetime.timedelta(days=1), - co_purchaser="A test co-purchaser", - ) + def test_calculateDatapoint_memberHasCoPurchaserButCannotShop_notCounted(self): + create_member_that_can_shop(self, self.REFERENCE_TIME) + ShareOwner.objects.update(is_investing=True) + TapirUser.objects.update(co_purchaser="A test co-purchaser") result = NumberOfCoPurchasersAtDateView().calculate_datapoint( self.REFERENCE_TIME @@ -34,14 +35,11 @@ def test_calculateDatapoint_memberHasCoPurchaserButIsNotWorking_notCounted(self) self.assertEqual(0, result) - def test_calculateDatapoint_memberIsWorkingButDoesntHaveACoPurchaser_notCounted( + def test_calculateDatapoint_memberCanShopButDoesntHaveACoPurchaser_notCounted( self, ): - TapirUserFactory.create( - date_joined=self.REFERENCE_TIME - datetime.timedelta(days=1), - share_owner__is_investing=False, - co_purchaser="", - ) + create_member_that_can_shop(self, self.REFERENCE_TIME) + TapirUser.objects.update(co_purchaser="") result = NumberOfCoPurchasersAtDateView().calculate_datapoint( self.REFERENCE_TIME @@ -50,11 +48,8 @@ def test_calculateDatapoint_memberIsWorkingButDoesntHaveACoPurchaser_notCounted( self.assertEqual(0, result) def test_calculateDatapoint_memberIsWorkingAndHasCoPurchaser_counted(self): - TapirUserFactory.create( - date_joined=self.REFERENCE_TIME - datetime.timedelta(days=1), - co_purchaser="A test co-purchaser", - share_owner__is_investing=False, - ) + create_member_that_can_shop(self, self.REFERENCE_TIME) + TapirUser.objects.update(co_purchaser="A test co-purchaser") result = NumberOfCoPurchasersAtDateView().calculate_datapoint( self.REFERENCE_TIME diff --git a/tapir/statistics/tests/fancy_graph/test_number_of_exempted_members_view.py b/tapir/statistics/tests/fancy_graph/test_number_of_exempted_members_view.py index c42d937b..39b838ef 100644 --- a/tapir/statistics/tests/fancy_graph/test_number_of_exempted_members_view.py +++ b/tapir/statistics/tests/fancy_graph/test_number_of_exempted_members_view.py @@ -3,8 +3,7 @@ from django.utils import timezone from tapir.accounts.models import TapirUser -from tapir.accounts.tests.factories.factories import TapirUserFactory -from tapir.coop.models import ShareOwnership, ShareOwner +from tapir.coop.models import ShareOwner from tapir.shifts.models import ShiftExemption, ShiftUserData from tapir.statistics.views.fancy_graph.number_of_exempted_members_view import ( NumberOfExemptedMembersAtDateView, @@ -12,6 +11,7 @@ from tapir.utils.tests_utils import ( TapirFactoryTestBase, mock_timezone_now, + create_member_that_is_working, ) @@ -25,18 +25,11 @@ def setUp(self) -> None: super().setUp() self.NOW = mock_timezone_now(self, self.NOW) - @classmethod - def create_member_where_the_only_reason_for_not_working_is_an_exemption(cls): - tapir_user = TapirUserFactory.create( - date_joined=cls.REFERENCE_TIME - datetime.timedelta(days=1), - share_owner__is_investing=False, - ) - ShareOwnership.objects.update( - start_date=cls.REFERENCE_TIME.date() - datetime.timedelta(days=1) - ) + def create_member_where_the_only_reason_for_not_working_is_an_exemption(self): + tapir_user = create_member_that_is_working(self, self.REFERENCE_TIME) ShiftExemption.objects.create( - start_date=cls.REFERENCE_TIME.date() - datetime.timedelta(days=1), - end_date=cls.REFERENCE_TIME.date() + datetime.timedelta(days=1), + start_date=self.REFERENCE_TIME.date() - datetime.timedelta(days=1), + end_date=self.REFERENCE_TIME.date() + datetime.timedelta(days=1), shift_user_data=tapir_user.shift_user_data, ) diff --git a/tapir/statistics/tests/fancy_graph/test_number_of_flying_members_view.py b/tapir/statistics/tests/fancy_graph/test_number_of_flying_members_view.py index 2a23b603..605d3700 100644 --- a/tapir/statistics/tests/fancy_graph/test_number_of_flying_members_view.py +++ b/tapir/statistics/tests/fancy_graph/test_number_of_flying_members_view.py @@ -11,6 +11,7 @@ TapirFactoryTestBase, mock_timezone_now, create_attendance_template_log_entry_in_the_past, + create_member_that_is_working, ) @@ -36,9 +37,7 @@ def test_calculateDatapoint_memberIsFlyingButIsNotWorking_notCounted(self): self.assertEqual(0, result) def test_calculateDatapoint_memberIsWorkingButIsNotFlying_notCounted(self): - tapir_user = TapirUserFactory.create( - date_joined=self.REFERENCE_TIME - datetime.timedelta(days=1) - ) + tapir_user = create_member_that_is_working(self, self.REFERENCE_TIME) create_attendance_template_log_entry_in_the_past( CreateShiftAttendanceTemplateLogEntry, tapir_user, self.REFERENCE_TIME ) @@ -50,10 +49,7 @@ def test_calculateDatapoint_memberIsWorkingButIsNotFlying_notCounted(self): self.assertEqual(0, result) def test_calculateDatapoint_memberIsWorkingAndFlying_counted(self): - TapirUserFactory.create( - date_joined=self.REFERENCE_TIME - datetime.timedelta(days=1), - share_owner__is_investing=False, - ) + create_member_that_is_working(self, self.REFERENCE_TIME) result = NumberOfFlyingMembersAtDateView().calculate_datapoint( self.REFERENCE_TIME diff --git a/tapir/statistics/tests/fancy_graph/test_number_of_frozen_members_view.py b/tapir/statistics/tests/fancy_graph/test_number_of_frozen_members_view.py index 88c6aff5..fa594c44 100644 --- a/tapir/statistics/tests/fancy_graph/test_number_of_frozen_members_view.py +++ b/tapir/statistics/tests/fancy_graph/test_number_of_frozen_members_view.py @@ -24,9 +24,7 @@ def setUp(self) -> None: self.NOW = mock_timezone_now(self, self.NOW) def test_calculateDatapoint_memberIsFrozenButIsNotActive_notCounted(self): - TapirUserFactory.create( - date_joined=self.REFERENCE_TIME + datetime.timedelta(days=1) - ) + TapirUserFactory.create(share_owner__is_investing=True) ShiftUserData.objects.update(is_frozen=True) result = NumberOfFrozenMembersAtDateView().calculate_datapoint( diff --git a/tapir/statistics/tests/fancy_graph/test_number_of_shift_partners_view.py b/tapir/statistics/tests/fancy_graph/test_number_of_shift_partners_view.py index ce554e8a..afba0c8a 100644 --- a/tapir/statistics/tests/fancy_graph/test_number_of_shift_partners_view.py +++ b/tapir/statistics/tests/fancy_graph/test_number_of_shift_partners_view.py @@ -8,7 +8,7 @@ from tapir.utils.tests_utils import ( TapirFactoryTestBase, mock_timezone_now, - create_member_that_can_shop, + create_member_that_is_working, ) @@ -23,8 +23,8 @@ def setUp(self) -> None: self.NOW = mock_timezone_now(self, self.NOW) def test_calculateDatapoint_memberHasPartnerButIsNotWorking_notCounted(self): - member_with_partner = create_member_that_can_shop(self, self.REFERENCE_TIME) - member_that_is_partner_of = create_member_that_can_shop( + member_with_partner = create_member_that_is_working(self, self.REFERENCE_TIME) + member_that_is_partner_of = create_member_that_is_working( self, self.REFERENCE_TIME ) member_with_partner.shift_user_data.shift_partner = ( @@ -42,7 +42,7 @@ def test_calculateDatapoint_memberHasPartnerButIsNotWorking_notCounted(self): self.assertEqual(0, result) def test_calculateDatapoint_memberIsWorkingButHasNoPartner_notCounted(self): - create_member_that_can_shop(self, self.REFERENCE_TIME) + create_member_that_is_working(self, self.REFERENCE_TIME) result = NumberOfShiftPartnersAtDateView().calculate_datapoint( self.REFERENCE_TIME @@ -51,8 +51,8 @@ def test_calculateDatapoint_memberIsWorkingButHasNoPartner_notCounted(self): self.assertEqual(0, result) def test_calculateDatapoint_memberIsWorkingAndHasAPartner_counted(self): - member_with_partner = create_member_that_can_shop(self, self.REFERENCE_TIME) - member_that_is_partner_of = create_member_that_can_shop( + member_with_partner = create_member_that_is_working(self, self.REFERENCE_TIME) + member_that_is_partner_of = create_member_that_is_working( self, self.REFERENCE_TIME ) member_with_partner.shift_user_data.shift_partner = ( diff --git a/tapir/statistics/tests/fancy_graph/test_number_of_working_members_view.py b/tapir/statistics/tests/fancy_graph/test_number_of_working_members_view.py index 39716371..ede21ad0 100644 --- a/tapir/statistics/tests/fancy_graph/test_number_of_working_members_view.py +++ b/tapir/statistics/tests/fancy_graph/test_number_of_working_members_view.py @@ -9,7 +9,7 @@ from tapir.utils.tests_utils import ( TapirFactoryTestBase, mock_timezone_now, - create_member_that_can_shop, + create_member_that_is_working, ) @@ -24,7 +24,7 @@ def setUp(self) -> None: self.NOW = mock_timezone_now(self, self.NOW) def test_calculateDatapoint_memberIsWorking_counted(self): - create_member_that_can_shop(self, self.REFERENCE_TIME) + create_member_that_is_working(self, self.REFERENCE_TIME) result = NumberOfWorkingMembersAtDateView().calculate_datapoint( self.REFERENCE_TIME @@ -33,7 +33,7 @@ def test_calculateDatapoint_memberIsWorking_counted(self): self.assertEqual(1, result) def test_calculateDatapoint_memberIsNotWorking_notCounted(self): - create_member_that_can_shop(self, self.REFERENCE_TIME) + create_member_that_is_working(self, self.REFERENCE_TIME) ShareOwner.objects.update(is_investing=True) result = NumberOfWorkingMembersAtDateView().calculate_datapoint( diff --git a/tapir/utils/tests_utils.py b/tapir/utils/tests_utils.py index e0392428..42093728 100644 --- a/tapir/utils/tests_utils.py +++ b/tapir/utils/tests_utils.py @@ -34,6 +34,7 @@ ShiftAttendanceTemplate, DeleteShiftAttendanceTemplateLogEntry, ) +from tapir.shifts.services.shift_expectation_service import ShiftExpectationService from tapir.shifts.tests.factories import ShiftTemplateFactory from tapir.utils.expection_utils import TapirException from tapir.utils.json_user import JsonUser @@ -325,3 +326,17 @@ def create_member_that_can_shop(test, reference_time): MemberCanShopService.can_shop(tapir_user.share_owner, reference_time) ) return tapir_user + + +def create_member_that_is_working(test, reference_time): + tapir_user = TapirUserFactory.create( + share_owner__is_investing=False, + date_joined=reference_time - datetime.timedelta(hours=1), + ) + ShareOwnership.objects.update(start_date=reference_time.date()) + test.assertTrue( + ShiftExpectationService.is_member_expected_to_do_shifts( + tapir_user.shift_user_data, reference_time + ) + ) + return tapir_user From 2b4d9983ff69a9eaf743d36ecad76c61256d032a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Thu, 12 Dec 2024 11:52:37 +0100 Subject: [PATCH 45/50] Translation file update. --- tapir/translations/locale/de/LC_MESSAGES/django.po | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tapir/translations/locale/de/LC_MESSAGES/django.po b/tapir/translations/locale/de/LC_MESSAGES/django.po index e9e953bc..43aae886 100644 --- a/tapir/translations/locale/de/LC_MESSAGES/django.po +++ b/tapir/translations/locale/de/LC_MESSAGES/django.po @@ -2,12 +2,12 @@ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. -# +# msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-11 17:31+0100\n" +"POT-Creation-Date: 2024-12-12 11:52+0100\n" "PO-Revision-Date: 2024-11-22 08:33+0100\n" "Last-Translator: \n" "Language-Team: \n" @@ -2591,7 +2591,7 @@ msgstr "" msgid "Investing to active" msgstr "" -#: coop/views/statistics.py:487 +#: coop/views/statistics.py:488 msgid "Number of members with a co-purchaser (X-axis) by month (Y-axis)" msgstr "" From a2e1a980f66d8ebac42539623a2078e57bb5db32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Thu, 12 Dec 2024 11:56:21 +0100 Subject: [PATCH 46/50] Translation file update post-merge. --- .../locale/de/LC_MESSAGES/django.po | 208 +++++++++--------- 1 file changed, 98 insertions(+), 110 deletions(-) diff --git a/tapir/translations/locale/de/LC_MESSAGES/django.po b/tapir/translations/locale/de/LC_MESSAGES/django.po index 72937768..1d52eb88 100644 --- a/tapir/translations/locale/de/LC_MESSAGES/django.po +++ b/tapir/translations/locale/de/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-10 17:23+0100\n" +"POT-Creation-Date: 2024-12-12 11:55+0100\n" "PO-Revision-Date: 2024-12-06 17:42+0100\n" "Last-Translator: \n" "Language-Team: \n" @@ -44,12 +44,12 @@ msgstr "" msgid "Displayed name" msgstr "Angezeigter Name" -#: accounts/models.py:68 coop/models.py:71 coop/models.py:484 +#: accounts/models.py:68 coop/models.py:70 coop/models.py:476 msgid "Pronouns" msgstr "Pronomen" #: accounts/models.py:69 accounts/templates/accounts/user_detail.html:67 -#: coop/models.py:73 coop/models.py:486 +#: coop/models.py:72 coop/models.py:478 #: coop/templates/coop/draftuser_detail.html:71 #: coop/templates/coop/draftuser_detail.html:142 #: coop/templates/coop/shareowner_detail.html:51 @@ -57,29 +57,29 @@ msgid "Phone number" msgstr "Telefonnummer" #: accounts/models.py:70 accounts/templates/accounts/user_detail.html:77 -#: coop/models.py:74 coop/models.py:487 +#: coop/models.py:73 coop/models.py:479 #: coop/templates/coop/draftuser_detail.html:146 #: coop/templates/coop/shareowner_detail.html:55 msgid "Birthdate" msgstr "Geburtsdatum" -#: accounts/models.py:71 coop/models.py:75 coop/models.py:488 +#: accounts/models.py:71 coop/models.py:74 coop/models.py:480 msgid "Street and house number" msgstr "Straße und Hausnummer" -#: accounts/models.py:72 coop/models.py:76 coop/models.py:489 +#: accounts/models.py:72 coop/models.py:75 coop/models.py:481 msgid "Extra address line" msgstr "Adresszusatz" -#: accounts/models.py:73 coop/models.py:77 coop/models.py:490 +#: accounts/models.py:73 coop/models.py:76 coop/models.py:482 msgid "Postcode" msgstr "Postleitzahl" -#: accounts/models.py:74 coop/models.py:78 coop/models.py:491 +#: accounts/models.py:74 coop/models.py:77 coop/models.py:483 msgid "City" msgstr "Ort" -#: accounts/models.py:75 coop/models.py:79 coop/models.py:492 +#: accounts/models.py:75 coop/models.py:78 coop/models.py:484 msgid "Country" msgstr "Land" @@ -92,7 +92,7 @@ msgid "Allow purchase tracking" msgstr "" #: accounts/models.py:92 accounts/templates/accounts/user_detail.html:97 -#: coop/models.py:82 coop/models.py:495 +#: coop/models.py:81 coop/models.py:487 #: coop/templates/coop/shareowner_detail.html:75 msgid "Preferred Language" msgstr "Bevorzugte Sprache" @@ -253,7 +253,7 @@ msgstr "" msgid "Enable" msgstr "" -#: accounts/templates/accounts/user_detail.html:20 coop/models.py:761 +#: accounts/templates/accounts/user_detail.html:20 coop/models.py:753 #: coop/templates/coop/draftuser_detail.html:86 #: coop/templates/coop/shareowner_detail.html:10 log/views.py:94 #: log/views.py:152 shifts/templates/shifts/shift_day_printable.html:55 @@ -579,7 +579,7 @@ msgstr "Anzahl zu erstellender Anteile" msgid "The end date must be later than the start date." msgstr "" -#: coop/forms.py:107 coop/models.py:501 +#: coop/forms.py:107 coop/models.py:493 msgid "Number of Shares" msgstr "Anzahl Anteile" @@ -654,185 +654,185 @@ msgstr "" msgid "Cannot pay out, because shares have been gifted." msgstr "" -#: coop/models.py:55 +#: coop/models.py:54 msgid "Is company" msgstr "Ist eine Firma" -#: coop/models.py:62 coop/models.py:475 +#: coop/models.py:61 coop/models.py:467 msgid "Administrative first name" msgstr "Amtlicher Vorname" -#: coop/models.py:64 coop/models.py:477 +#: coop/models.py:63 coop/models.py:469 msgid "Last name" msgstr "Nachname" -#: coop/models.py:66 coop/models.py:479 +#: coop/models.py:65 coop/models.py:471 msgid "Usage name" msgstr "Angezeigter Name" -#: coop/models.py:72 coop/models.py:485 +#: coop/models.py:71 coop/models.py:477 msgid "Email address" msgstr "E-Mail-Adresse" -#: coop/models.py:89 +#: coop/models.py:88 msgid "Is investing member" msgstr "Ist investierendes Mitglied" -#: coop/models.py:91 coop/models.py:516 +#: coop/models.py:90 coop/models.py:508 #: coop/templates/coop/draftuser_detail.html:176 #: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:48 #: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:167 msgid "Ratenzahlung" msgstr "Ratenzahlung" -#: coop/models.py:93 coop/models.py:508 +#: coop/models.py:92 coop/models.py:500 msgid "Attended Welcome Session" msgstr "An Willkommenstreffen teilgenommen" -#: coop/models.py:95 coop/models.py:513 +#: coop/models.py:94 coop/models.py:505 msgid "Paid Entrance Fee" msgstr "Eintrittsgeld bezahlt" -#: coop/models.py:97 +#: coop/models.py:96 msgid "Is willing to gift a share" msgstr "Ist bereit Anteile zu verschenken" -#: coop/models.py:209 +#: coop/models.py:208 msgid "Cannot be a company and have a Tapir account" msgstr "Kann keine Firma sein und ein Tapir-Konto haben" -#: coop/models.py:225 +#: coop/models.py:224 msgid "User info should be stored in associated Tapir account" msgstr "Benutzer Infos sollen in dem Tapir Konto gespeichert warden" -#: coop/models.py:384 +#: coop/models.py:376 msgid "Not a member" msgstr "" -#: coop/models.py:385 coop/templates/coop/draftuser_detail.html:169 +#: coop/models.py:377 coop/templates/coop/draftuser_detail.html:169 msgid "Investing" msgstr "Investierend" -#: coop/models.py:386 coop/templates/coop/draftuser_detail.html:171 +#: coop/models.py:378 coop/templates/coop/draftuser_detail.html:171 #: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:103 #: coop/views/statistics.py:142 msgid "Active" msgstr "Aktiv" -#: coop/models.py:387 +#: coop/models.py:379 msgid "Paused" msgstr "Pausiert" -#: coop/models.py:442 +#: coop/models.py:434 msgid "Amount paid for a share can't be negative" msgstr "Der Betrag kann nicht negative sein" -#: coop/models.py:446 +#: coop/models.py:438 #, python-brace-format msgid "Amount paid for a share can't more than {COOP_SHARE_PRICE} (the price of a share)" msgstr "Der Betrag kann nicht mehr als {COOP_SHARE_PRICE} sein" -#: coop/models.py:504 +#: coop/models.py:496 msgid "Investing member" msgstr "Investierendes Mitglied" -#: coop/models.py:511 +#: coop/models.py:503 msgid "Signed Beteiligungserklärung" msgstr "Beteiligungserklärung unterschrieben" -#: coop/models.py:514 coop/templates/coop/draftuser_detail.html:234 +#: coop/models.py:506 coop/templates/coop/draftuser_detail.html:234 msgid "Paid Shares" msgstr "Anteil(e) bezahlt" -#: coop/models.py:571 +#: coop/models.py:563 msgid "Email address must be set." msgstr "Email-Adresse muss gesetzt sein." -#: coop/models.py:573 +#: coop/models.py:565 msgid "First name must be set." msgstr "Vorname muss gesetzt sein." -#: coop/models.py:575 +#: coop/models.py:567 msgid "Last name must be set." msgstr "Nachname muss gesetzt sein." -#: coop/models.py:579 +#: coop/models.py:571 msgid "Membership agreement must be signed." msgstr "Mitgliedsantrag muss unterschrieben sein." -#: coop/models.py:581 +#: coop/models.py:573 msgid "Amount of requested shares must be positive." msgstr "Die Anzahl der erwünschten Anteile muss positiv sein." -#: coop/models.py:583 +#: coop/models.py:575 msgid "Member already created." msgstr "Mitglied schon vorhanden." -#: coop/models.py:610 +#: coop/models.py:602 msgid "Paying member" msgstr "Zahlendes Mitglied" -#: coop/models.py:618 +#: coop/models.py:610 msgid "Credited member" msgstr "Empfangendes Mitglied" -#: coop/models.py:625 +#: coop/models.py:617 msgid "Amount" msgstr "Betrag" -#: coop/models.py:631 +#: coop/models.py:623 msgid "Payment date" msgstr "Zahldatum" -#: coop/models.py:634 +#: coop/models.py:626 msgid "Creation date" msgstr "Erstellungsdatum" -#: coop/models.py:639 +#: coop/models.py:631 msgid "Created by" msgstr "Erstellt durch" -#: coop/models.py:811 +#: coop/models.py:803 msgid "The cooperative buys the shares back from the member" msgstr "" -#: coop/models.py:814 +#: coop/models.py:806 #, fuzzy #| msgid "Number of shares to create" msgid "The member gifts the shares to the cooperative" msgstr "Anzahl zu erstellender Anteile" -#: coop/models.py:816 +#: coop/models.py:808 msgid "The shares get transferred to another member" msgstr "" -#: coop/models.py:819 +#: coop/models.py:811 msgid "Financial reasons" msgstr "Finanzielle Gründe" -#: coop/models.py:820 +#: coop/models.py:812 msgid "Health reasons" msgstr "Gesundheit" -#: coop/models.py:821 +#: coop/models.py:813 msgid "Distance" msgstr "Entfernung" -#: coop/models.py:822 +#: coop/models.py:814 msgid "Strategic orientation of SuperCoop" msgstr "Strategische Ausrichtung von Supercoop" -#: coop/models.py:823 +#: coop/models.py:815 msgid "Other" msgstr "Andere" -#: coop/models.py:828 +#: coop/models.py:820 #, fuzzy #| msgid "Edit shareowner" msgid "Shareowner" msgstr "Mitglied bearbeiten" -#: coop/models.py:847 +#: coop/models.py:839 msgid "Leave this empty if the resignation type is not a transfer to another member" msgstr "Lass das Feld leer, wenn es sich nicht um eine Übertragung auf ein anderes Mitglied handelt" @@ -1059,7 +1059,7 @@ msgid "" msgstr "" #: coop/templates/coop/email/accounting_recap.body.default.html:21 -#: statistics/views/main_view.py:304 +#: statistics/views/main_view.py:301 msgid "New members" msgstr "Neue Mitglieder" @@ -2573,7 +2573,7 @@ msgstr "" msgid "Investing to active" msgstr "" -#: coop/views/statistics.py:487 +#: coop/views/statistics.py:488 msgid "Number of members with a co-purchaser (X-axis) by month (Y-axis)" msgstr "" @@ -4514,7 +4514,7 @@ msgid "Main statistics" msgstr "Hauptstatistik" #: statistics/templates/statistics/main_statistics.html:19 -#: statistics/views/main_view.py:263 +#: statistics/views/main_view.py:260 msgid "Total number of members" msgstr "Gesamte Mitgliederzahl" @@ -4611,7 +4611,7 @@ msgstr "" #: statistics/templates/statistics/main_statistics.html:118 #: statistics/templates/statistics/main_statistics.html:129 -#: statistics/views/main_view.py:385 +#: statistics/views/main_view.py:382 msgid "Frozen members" msgstr "Eingefrorene Mitglieder" @@ -4659,53 +4659,6 @@ msgstr "" "Hier könnt ihr den Fortschritt der Finanzierungskampagne verfolgen. Es werden sowohl weitere Anteile gezählt (alle Anteile, die über den Pflichtanteil hinaus gezeichnet werden) als auch Nachrangdarlehen. Der Zeitraum läuft vom 12.09.2023 - 09.12.2023. Was eine:r nicht schafft, schaffen viele!\n" " " -#: statistics/templates/statistics/shift_cancelling_rate.html:9 -#, fuzzy -#| msgid "Shift attendance: %(name)s" -msgid "Shift attendance rates" -msgstr "Schicht-Anwesenheit: %(name)s" - -#: statistics/templates/statistics/shift_cancelling_rate.html:19 -#: statistics/templates/statistics/shift_cancelling_rate.html:69 -#, fuzzy -#| msgid "Creation date" -msgid "Shift cancellation rate" -msgstr "Erstellungsdatum" - -#: statistics/templates/statistics/shift_cancelling_rate.html:78 -#, fuzzy -#| msgid "Number of shares to create" -msgid "Number of shifts by category" -msgstr "Anzahl zu erstellender Anteile" - -#: statistics/templates/statistics/state_distribution.html:9 -#: statistics/templates/statistics/state_distribution.html:19 -#: statistics/templates/statistics/state_distribution.html:36 -#, fuzzy -#| msgid "Send me instructions!" -msgid "State distribution" -msgstr "Schickt mir eine Anleitung!" - -#: statistics/templates/statistics/stats_for_marie.html:9 -#, fuzzy -#| msgid "Statistics on shares" -msgid "Stats for Marie" -msgstr "Anteile-Statistiken" - -#: statistics/templates/statistics/stats_for_marie.html:20 -#: statistics/templates/statistics/stats_for_marie.html:26 -#, fuzzy -#| msgid "New members per month" -msgid "Number of frozen members per month" -msgstr "Neue Mitglieder*innen pro Monat" - -#: statistics/templates/statistics/stats_for_marie.html:33 -#: statistics/templates/statistics/stats_for_marie.html:39 -#, fuzzy -#| msgid "Current number of purchasing members" -msgid "Number of purchasing members per month" -msgstr "Aktuelle Anzahl von einkaufsberechtigten Mitgliedern" - #: statistics/templates/statistics/tags/on_demand_chart.html:5 msgid "Show graph: " msgstr "Grafik anzeigen: " @@ -4737,19 +4690,19 @@ msgstr "" msgid "Evolution of total spends per month" msgstr "Entwicklung der Gesamtausgaben pro Monat" -#: statistics/views/main_view.py:335 +#: statistics/views/main_view.py:332 msgid "Total spends per month" msgstr "Gesamtausgaben pro Monat" -#: statistics/views/main_view.py:385 +#: statistics/views/main_view.py:382 msgid "Purchasing members" msgstr "Einkaufsberechtigten Mitglieder*innen" -#: statistics/views/main_view.py:403 +#: statistics/views/main_view.py:400 msgid "Percentage of members with a co-purchaser relative to the number of active members" msgstr "Prozentualer Anteil der Mitglieder mit einem Miterwerber im Verhältnis zur Zahl der aktiven Mitglieder" -#: statistics/views/main_view.py:555 +#: statistics/views/main_view.py:552 msgid "Purchase data updated" msgstr "Kaufdaten aktualisiert" @@ -5775,6 +5728,41 @@ msgstr "%(name)s ist kein Mitglied der Genossenschaft. Vielleicht haben sie dere msgid "%(name)s has not attended a welcome session yet. Make sure they plan to do it!" msgstr "%(name)s hat an dem Willkommenstreffen noch nicht teilgenommen. Stelle sicher, dass er*sie es entsprechend einplant!" +#, fuzzy +#~| msgid "Shift attendance: %(name)s" +#~ msgid "Shift attendance rates" +#~ msgstr "Schicht-Anwesenheit: %(name)s" + +#, fuzzy +#~| msgid "Creation date" +#~ msgid "Shift cancellation rate" +#~ msgstr "Erstellungsdatum" + +#, fuzzy +#~| msgid "Number of shares to create" +#~ msgid "Number of shifts by category" +#~ msgstr "Anzahl zu erstellender Anteile" + +#, fuzzy +#~| msgid "Send me instructions!" +#~ msgid "State distribution" +#~ msgstr "Schickt mir eine Anleitung!" + +#, fuzzy +#~| msgid "Statistics on shares" +#~ msgid "Stats for Marie" +#~ msgstr "Anteile-Statistiken" + +#, fuzzy +#~| msgid "New members per month" +#~ msgid "Number of frozen members per month" +#~ msgstr "Neue Mitglieder*innen pro Monat" + +#, fuzzy +#~| msgid "Current number of purchasing members" +#~ msgid "Number of purchasing members per month" +#~ msgstr "Aktuelle Anzahl von einkaufsberechtigten Mitgliedern" + #~ msgid "Shift statistics" #~ msgstr "Schicht-Statistiken" From 7e172bcde50b51c3f84130b91dd0a7e7d6121e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Thu, 12 Dec 2024 12:16:20 +0100 Subject: [PATCH 47/50] Fixed delete_transferred_share_ownerships --- .../membership_resignation_service.py | 2 +- .../test_delete_view.py | 30 +++++++++++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/tapir/coop/services/membership_resignation_service.py b/tapir/coop/services/membership_resignation_service.py index c2d62f1d..8ca33dbc 100644 --- a/tapir/coop/services/membership_resignation_service.py +++ b/tapir/coop/services/membership_resignation_service.py @@ -123,7 +123,7 @@ def delete_transferred_share_ownerships(cls, resignation: MembershipResignation) ) started_ownerships = ShareOwnership.objects.filter( share_owner=resignation.transferring_shares_to, - start_date=resignation.cancellation_date, + start_date=resignation.cancellation_date + datetime.timedelta(days=1), transferred_from__in=ended_ownerships, ) for started_ownership in started_ownerships: diff --git a/tapir/coop/tests/membership_resignation/test_delete_view.py b/tapir/coop/tests/membership_resignation/test_delete_view.py index cdfa7081..96b56678 100644 --- a/tapir/coop/tests/membership_resignation/test_delete_view.py +++ b/tapir/coop/tests/membership_resignation/test_delete_view.py @@ -14,6 +14,7 @@ from tapir.coop.services.membership_resignation_service import ( MembershipResignationService, ) +from tapir.coop.services.number_of_shares_service import NumberOfSharesService from tapir.coop.tests.factories import ( MembershipResignationFactory, ShareOwnerFactory, @@ -89,15 +90,16 @@ def test_membershipResignationDeleteView_default_logEntryCreated(self): def test_membershipResignationDeleteView_sharesWereTransferred_updateTransferredShares( self, ): - actor = self.login_as_vorstand() + self.login_as_vorstand() member_that_gifts_shares = ShareOwnerFactory.create(nb_shares=3) member_that_receives_shares = ShareOwnerFactory.create(nb_shares=1) + cancellation_date = timezone.now().date() data = { "share_owner": member_that_gifts_shares.id, "cancellation_reason": "Test resignation", "cancellation_reason_category": MembershipResignation.CancellationReasons.OTHER, - "cancellation_date": timezone.now().date(), + "cancellation_date": cancellation_date, "resignation_type": MembershipResignation.ResignationType.TRANSFER, "transferring_shares_to": member_that_receives_shares.id, } @@ -108,13 +110,31 @@ def test_membershipResignationDeleteView_sharesWereTransferred_updateTransferred follow=True, ) self.assertStatusCode(response, HTTPStatus.OK) - resignation = MembershipResignation.objects.get() + self.assertNumberOfSharesAtDate( + member_that_receives_shares, 1, cancellation_date + ) + self.assertNumberOfSharesAtDate(member_that_gifts_shares, 3, cancellation_date) + new_shares_date = cancellation_date + datetime.timedelta(days=1) + self.assertNumberOfSharesAtDate(member_that_receives_shares, 4, new_shares_date) + self.assertNumberOfSharesAtDate(member_that_gifts_shares, 0, new_shares_date) + + resignation = MembershipResignation.objects.get() response = self.client.post( reverse("coop:membership_resignation_delete", args=[resignation.id]), follow=True, ) self.assertStatusCode(response, HTTPStatus.OK) - self.assertEqual(1, member_that_receives_shares.share_ownerships.count()) - self.assertEqual(3, member_that_gifts_shares.share_ownerships.count()) + self.assertNumberOfSharesAtDate( + member_that_receives_shares, 1, cancellation_date + ) + self.assertNumberOfSharesAtDate(member_that_gifts_shares, 3, cancellation_date) + self.assertNumberOfSharesAtDate(member_that_receives_shares, 1, new_shares_date) + self.assertNumberOfSharesAtDate(member_that_gifts_shares, 3, new_shares_date) + + def assertNumberOfSharesAtDate(self, member, number_of_shares, date): + self.assertEqual( + number_of_shares, + NumberOfSharesService.get_number_of_active_shares(member, date), + ) From 155aa3199e1f202d269866d2958ff66eb045468d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Thu, 12 Dec 2024 12:23:14 +0100 Subject: [PATCH 48/50] Fixed test_deleteTransferredShareOwnerships_default_deletesAllOwnershipsOfChain --- tapir/coop/tests/membership_resignation/test_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tapir/coop/tests/membership_resignation/test_service.py b/tapir/coop/tests/membership_resignation/test_service.py index 97d59c4a..13a0b6f9 100644 --- a/tapir/coop/tests/membership_resignation/test_service.py +++ b/tapir/coop/tests/membership_resignation/test_service.py @@ -335,7 +335,7 @@ def test_deleteTransferredShareOwnerships_default_deletesAllOwnershipsOfChain(se # set start date for only one share in order to test that the second share doesn't get affected transferred_share = first_recipient.share_ownerships.first() transferred_share.transferred_from = resigned_member.share_ownerships.first() - transferred_share.start_date = cancellation_date + transferred_share.start_date = cancellation_date + datetime.timedelta(days=1) transferred_share.save() second_recipient: ShareOwner = ShareOwnerFactory.create(nb_shares=1) From 827fa6f35d85bffa670832505b252142ec79fc79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Thu, 12 Dec 2024 12:27:27 +0100 Subject: [PATCH 49/50] Ignore start date in delete_transferred_share_ownerships --- tapir/coop/services/membership_resignation_service.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tapir/coop/services/membership_resignation_service.py b/tapir/coop/services/membership_resignation_service.py index 8ca33dbc..20e510d6 100644 --- a/tapir/coop/services/membership_resignation_service.py +++ b/tapir/coop/services/membership_resignation_service.py @@ -123,7 +123,6 @@ def delete_transferred_share_ownerships(cls, resignation: MembershipResignation) ) started_ownerships = ShareOwnership.objects.filter( share_owner=resignation.transferring_shares_to, - start_date=resignation.cancellation_date + datetime.timedelta(days=1), transferred_from__in=ended_ownerships, ) for started_ownership in started_ownerships: From 363d1c3b40c89967881151304bb9428492c32060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Madet?= Date: Thu, 12 Dec 2024 12:37:44 +0100 Subject: [PATCH 50/50] Clear FancyGraphCache when resetting test data --- .../management/commands/generate_test_data_functions.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tapir/utils/management/commands/generate_test_data_functions.py b/tapir/utils/management/commands/generate_test_data_functions.py index a0570c6b..967ca0f5 100644 --- a/tapir/utils/management/commands/generate_test_data_functions.py +++ b/tapir/utils/management/commands/generate_test_data_functions.py @@ -43,7 +43,11 @@ ShiftAttendanceMode, CreateShiftAttendanceTemplateLogEntry, ) -from tapir.statistics.models import ProcessedPurchaseFiles, PurchaseBasket +from tapir.statistics.models import ( + ProcessedPurchaseFiles, + PurchaseBasket, + FancyGraphCache, +) from tapir.utils.json_user import JsonUser from tapir.utils.models import copy_user_info from tapir.utils.shortcuts import ( @@ -468,6 +472,7 @@ def clear_django_db(): DraftUser, ProcessedPurchaseFiles, PurchaseBasket, + FancyGraphCache, ] ShareOwnership.objects.update(transferred_from=None) for cls in classes: