Skip to content

Commit 8bb0709

Browse files
committed
Switch archivable member selection from delta to year-based cutoff
Fixes an oversight in the previous implementation of #470 Also, expand on the test suite substantially while retaining a decent runtime. Refs #67
1 parent 5850441 commit 8bb0709

File tree

2 files changed

+78
-34
lines changed

2 files changed

+78
-34
lines changed

pycroft/lib/user_deletion.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
This module contains methods concerning user archival and deletion.
66
"""
77
from __future__ import annotations
8-
from datetime import timedelta, datetime
8+
from datetime import datetime
99
from typing import Protocol, Sequence, cast
1010

11+
from sqlalchemy import func
1112
from sqlalchemy.future import select
1213
from sqlalchemy.orm import joinedload, Session
1314
from sqlalchemy.sql import Select
1415
from sqlalchemy.sql.elements import and_, not_
15-
from sqlalchemy.sql.functions import current_timestamp
1616

1717
from pycroft.model.property import CurrentProperty
1818
from pycroft.model.user import User
@@ -25,7 +25,9 @@ class ArchivableMemberInfo(Protocol):
2525
mem_end: datetime
2626

2727

28-
def select_archivable_members(delta: timedelta) -> Select: # Select[Tuple[User, int, datetime]]
28+
def select_archivable_members(
29+
current_year: int,
30+
) -> Select: # Select[Tuple[User, int, datetime]]
2931
# last_mem: (user_id, mem_id, mem_end)
3032
last_mem = select_user_and_last_mem().cte("last_mem")
3133
return (
@@ -40,7 +42,7 @@ def select_archivable_members(delta: timedelta) -> Select: # Select[Tuple[User,
4042
# …and use that to filter out the `do-not-archive` occurrences.
4143
.filter(CurrentProperty.property_name.is_(None))
4244
.join(User, User.id == last_mem.c.user_id)
43-
.filter(last_mem.c.mem_end < current_timestamp() - delta)
45+
.filter(func.extract("year", last_mem.c.mem_end) + 2 <= current_year)
4446
.order_by(last_mem.c.mem_end)
4547
.add_columns(
4648
User,
@@ -51,7 +53,8 @@ def select_archivable_members(delta: timedelta) -> Select: # Select[Tuple[User,
5153

5254

5355
def get_archivable_members(
54-
session: Session, delta: timedelta = timedelta(days=14)
56+
session: Session,
57+
current_year: int | None = None,
5558
) -> Sequence[ArchivableMemberInfo]:
5659
"""Return all the users that qualify for being archived right now.
5760
@@ -66,13 +69,18 @@ def get_archivable_members(
6669
- current_properties_maybe_denied
6770
6871
:param session:
69-
:param delta: how far back the end of membership has to lie (positive timedelta).
72+
:param current_year: dependency injection of the current year.
73+
defaults to the current year.
7074
"""
7175
return cast(
7276
list[ArchivableMemberInfo],
7377
session.execute(
74-
select_archivable_members(delta)
75-
.options(
78+
select_archivable_members(
79+
# I know we're sloppy with time zones,
80+
# but ±2h around new year's eve don't matter.
81+
current_year=current_year
82+
or datetime.now().year
83+
).options(
7684
joinedload(User.hosts),
7785
# joinedload(User.current_memberships),
7886
joinedload(User.account, innerjoin=True),

tests/lib/user/test_deletion.py

Lines changed: 62 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,18 @@
22
# This file is part of the Pycroft project and licensed under the terms of
33
# the Apache License, Version 2.0. See the LICENSE file for details
44
from datetime import datetime, date
5+
from typing import Sequence
56

67
import pytest
78

89
from pycroft.helpers.interval import closed, closedopen
9-
from pycroft.lib.user_deletion import get_archivable_members, archive_users
10+
from pycroft.helpers.utc import with_min_time
11+
from pycroft.lib.user_deletion import (
12+
get_archivable_members,
13+
archive_users,
14+
ArchivableMemberInfo,
15+
)
16+
from pycroft.model.user import User
1017
from tests.factories import UserFactory, ConfigFactory, MembershipFactory, \
1118
PropertyGroupFactory, \
1219
HostFactory
@@ -26,27 +33,49 @@ def test_users_without_membership_not_in_list(session):
2633
assert get_archivable_members(session) == []
2734

2835

29-
def assert_archivable_members(members, expected_user, expected_end_date):
30-
match members:
31-
case [(user, mem_id, mem_end)]:
32-
assert user == expected_user
33-
assert mem_id is not None
34-
assert mem_end.date() == expected_end_date
35-
case _:
36-
pytest.fail()
36+
def filter_members(members, user):
37+
return [(u, id, end) for u, id, end in members if u == user]
3738

3839

39-
class TestUserDeletion:
40+
def assert_member_present(
41+
members: Sequence[ArchivableMemberInfo],
42+
expected_user: User,
43+
expected_end_date: date,
44+
):
45+
relevant_members = filter_members(members, expected_user)
46+
assert len(relevant_members) == 1
47+
[(_, mem_id, mem_end)] = relevant_members
48+
assert mem_id is not None
49+
assert mem_end.date() == expected_end_date
50+
51+
52+
def assert_member_absent(
53+
members: Sequence[ArchivableMemberInfo],
54+
expected_absent_user: User,
55+
):
56+
assert not filter_members(members, expected_absent_user)
57+
58+
59+
class TestArchivableUserSelection:
60+
@pytest.fixture(
61+
scope="class",
62+
params=[date(2020, 3, 1), date(2020, 1, 1), date(2020, 12, 15)],
63+
)
64+
def end_date(self, request):
65+
return request.param
66+
4067
@pytest.fixture(scope='class')
4168
def do_not_archive_group(self, class_session):
4269
return PropertyGroupFactory(granted={'do-not-archive'})
4370

44-
@pytest.fixture(scope='class')
45-
def old_user(self, class_session, config, do_not_archive_group):
71+
@pytest.fixture(scope="class")
72+
def old_user(self, class_session, config, do_not_archive_group, end_date) -> User:
4673
user = UserFactory.create(
4774
registered_at=datetime(2000, 1, 1),
4875
with_membership=True,
49-
membership__active_during=closed(datetime(2020, 1, 1), datetime(2020, 3, 1)),
76+
membership__active_during=closed(
77+
with_min_time(date(2020, 1, 1)), with_min_time(end_date)
78+
),
5079
membership__group=config.member_group,
5180
)
5281
MembershipFactory.create(
@@ -66,31 +95,38 @@ def do_not_archive_membership(self, session, old_user, do_not_archive_group):
6695
active_during=closedopen(datetime(2020, 1, 1), None),
6796
)
6897

69-
def test_old_users_in_deletion_list(self, session, old_user):
70-
members = get_archivable_members(session)
71-
assert_archivable_members(members, old_user, date(2020, 3, 1))
98+
@pytest.mark.parametrize("year", [2022, 2023, 2024])
99+
def test_old_users_in_deletion_list_after(self, session, old_user, year, end_date):
100+
members = get_archivable_members(session, current_year=year)
101+
assert_member_present(members, old_user, end_date)
72102

73-
def test_old_user_not_in_list_with_long_delta(self, session, old_user):
74-
delta = date.today() - date(2020, 1, 1) # before 2020-03-01
75-
assert get_archivable_members(session, delta) == []
103+
@pytest.mark.parametrize("year", [2019, 2020, 2021])
104+
def test_old_user_not_in_list_before(self, session, old_user, year):
105+
assert_member_absent(
106+
get_archivable_members(session, current_year=year), old_user
107+
)
76108

77-
def test_user_with_do_not_archive_not_in_list(self, session, old_user,
78-
do_not_archive_membership):
79-
assert get_archivable_members(session) == []
109+
@pytest.mark.parametrize("year", list(range(2018, 2023)))
110+
def test_user_with_do_not_archive_not_in_list(
111+
self, session, old_user, do_not_archive_membership, year
112+
):
113+
assert_member_absent(
114+
get_archivable_members(session, current_year=year), old_user
115+
)
80116

81117
@pytest.mark.parametrize('num_hosts', [0, 2])
82-
def test_user_with_host_in_list(self, session, old_user, num_hosts):
118+
def test_user_with_host_in_list(self, session, old_user, num_hosts, end_date):
83119
if num_hosts:
84120
HostFactory.create_batch(num_hosts, owner=old_user)
85121
members = get_archivable_members(session)
86-
assert_archivable_members(members, old_user, date(2020, 3, 1))
122+
assert_member_present(members, old_user, end_date)
87123

88-
def test_user_with_room_in_list(self, session, old_user):
124+
def test_user_with_room_in_list(self, session, old_user, end_date):
89125
with session.begin_nested():
90126
old_user.room = None
91127
session.add(old_user)
92128
members = get_archivable_members(session)
93-
assert_archivable_members(members, old_user, date(2020, 3, 1))
129+
assert_member_present(members, old_user, end_date)
94130

95131

96132
class TestUserArchival:

0 commit comments

Comments
 (0)