Coverage for website/members/services.py: 53.68%
100 statements
« prev ^ index » next coverage.py v7.6.7, created at 2025-08-14 10:31 +0000
« prev ^ index » next coverage.py v7.6.7, created at 2025-08-14 10:31 +0000
1from collections.abc import Callable
2from datetime import date
3from typing import Any
5from django.conf import settings
6from django.db.models import Count, Exists, OuterRef, Q
7from django.utils import timezone
8from members import emails
9from members.models import Member, Membership
10from registrations.models import Renewal
11from utils.snippets import datetime_to_lectureyear
14def _member_group_memberships(
15 member: Member, condition: Callable[[Membership], bool]
16) -> dict[str, dict[str, Any]]:
17 """Determine the group membership of a user based on a condition.
19 :return: Object with group memberships
20 """
21 memberships = member.membergroupmembership_set.all()
22 data = {}
24 for membership in memberships:
25 if not condition(membership):
26 continue
27 period = {
28 "since": membership.since,
29 "until": membership.until,
30 "chair": membership.chair,
31 }
33 if hasattr(membership.group, "board"):
34 period["role"] = membership.role
36 if membership.until is None and hasattr(membership.group, "board"):
37 period["until"] = membership.group.board.until
39 name = membership.group.name
40 if data.get(name):
41 data[name]["periods"].append(period)
42 if data[name]["earliest"] > period["since"]:
43 data[name]["earliest"] = period["since"]
44 if period["until"] is None or (
45 data[name]["latest"] is not None
46 and data[name]["latest"] < period["until"]
47 ):
48 data[name]["latest"] = period["until"]
49 data[name]["periods"].sort(key=lambda x: x["since"])
50 else:
51 data[name] = {
52 "pk": membership.group.pk,
53 "active": membership.group.active,
54 "name": name,
55 "periods": [period],
56 "url": settings.BASE_URL + membership.group.get_absolute_url(),
57 "earliest": period["since"],
58 "latest": period["until"],
59 }
60 return data
63def member_achievements(member) -> list:
64 """Derive a list of achievements of a member.
66 Committee and board memberships + mentorships
67 """
68 achievements = _member_group_memberships(
69 member,
70 lambda membership: (
71 hasattr(membership.group, "board") or hasattr(membership.group, "committee")
72 ),
73 )
75 mentor_years = member.mentorship_set.all()
76 for mentor_year in mentor_years:
77 name = f"Mentor in {mentor_year.year}"
78 # Ensure mentorships appear last but are sorted
79 earliest = date.today()
80 # Making sure it does not crash in leap years
81 if earliest.month == 2 and earliest.day == 29:
82 earliest = earliest.replace(day=28)
84 earliest = earliest.replace(year=earliest.year + mentor_year.year)
86 if not achievements.get(name):
87 achievements[name] = {
88 "name": name,
89 "earliest": earliest,
90 }
91 return sorted(achievements.values(), key=lambda x: x["earliest"])
94def member_societies(member) -> list:
95 """Derive a list of societies a member was part of."""
96 societies = _member_group_memberships(
97 member, lambda membership: (hasattr(membership.group, "society"))
98 )
99 return sorted(societies.values(), key=lambda x: x["earliest"])
102def gen_stats_member_type() -> dict[str, list]:
103 """Generate statistics about membership types."""
104 data = {
105 "labels": [],
106 "datasets": [
107 {"data": []},
108 ],
109 }
111 for key, display in Membership.MEMBERSHIP_TYPES:
112 data["labels"].append(str(display))
113 data["datasets"][0]["data"].append(
114 Membership.objects.filter(since__lte=date.today())
115 .filter(Q(until__isnull=True) | Q(until__gt=date.today()))
116 .filter(type=key)
117 .count()
118 )
120 return data
123def gen_stats_year() -> dict[str, list]:
124 """Generate statistics on how many members (and other membership types) there were in each year."""
125 years = range(2015, datetime_to_lectureyear(date.today()) + 1)
127 data = {
128 "labels": list(years),
129 "datasets": [
130 {"label": str(display), "data": []}
131 for _, display in Membership.MEMBERSHIP_TYPES
132 ],
133 }
135 for index, (key, _) in enumerate(Membership.MEMBERSHIP_TYPES):
136 for year in years:
137 data["datasets"][index]["data"].append(
138 Membership.objects.filter(since__lte=date(year=year, month=10, day=1))
139 .filter(
140 Q(until__isnull=True)
141 | Q(until__gt=date(year=year, month=10, day=1))
142 )
143 .filter(type=key)
144 .count()
145 )
147 return data
150def gen_stats_active_members() -> dict[str, list]:
151 """Generate statistics about active members."""
152 return {
153 "labels": ["Active Members", "Non-active Members"],
154 "datasets": [
155 {
156 "data": [
157 Member.active_members.count(),
158 Member.current_members.count() - Member.active_members.count(),
159 ]
160 }
161 ],
162 }
165def verify_email_change(change_request) -> None:
166 """Mark the email change request as verified.
168 :param change_request: the email change request
169 """
170 change_request.verified = True
171 change_request.save()
173 process_email_change(change_request)
176def confirm_email_change(change_request) -> None:
177 """Mark the email change request as verified.
179 :param change_request: the email change request
180 """
181 change_request.confirmed = True
182 change_request.save()
184 process_email_change(change_request)
187def process_email_change(change_request) -> None:
188 """Change the user's email address if the request was completed and send the completion email.
190 :param change_request: the email change request
191 """
192 if not change_request.completed:
193 return
195 member = change_request.member
196 member.email = change_request.email
197 member.save()
199 emails.send_email_change_completion_message(change_request)
202def execute_data_minimisation(dry_run=False, members=None) -> list[Member]:
203 """Clean the profiles of members/users of whom the last membership ended at least 90 days ago.
205 :param dry_run: does not really remove data if True
206 :param members: queryset of members to process, optional
207 :return: list of processed members
208 """
209 if not members:
210 members = Member.objects
211 members = (
212 members.annotate(membership_count=Count("membership"))
213 .exclude(
214 (
215 Q(membership__until__isnull=True)
216 | Q(membership__until__gt=timezone.now().date())
217 )
218 & Q(membership_count__gt=0)
219 )
220 .exclude(
221 Exists(
222 Renewal.objects.filter(member__id=OuterRef("pk")).exclude(
223 status__in=(
224 Renewal.STATUS_ACCEPTED,
225 Renewal.STATUS_REJECTED,
226 )
227 )
228 )
229 )
230 .distinct()
231 .prefetch_related("membership_set", "profile")
232 )
233 deletion_period = timezone.now().date() - timezone.timedelta(days=90)
234 processed_members = []
235 for member in members:
236 if (
237 member.latest_membership is None
238 or member.latest_membership.until <= deletion_period
239 ):
240 processed_members.append(member)
241 profile = member.profile
242 profile.student_number = None
243 profile.phone_number = None
244 profile.address_street = None
245 profile.address_street2 = None
246 profile.address_postal_code = None
247 profile.address_city = None
248 profile.address_country = None
249 profile.birthday = None
250 profile.emergency_contact_phone_number = None
251 profile.emergency_contact = None
252 profile.is_minimized = True
253 if not dry_run: 253 ↛ 254line 253 didn't jump to line 254 because the condition on line 253 was never true
254 profile.save()
256 return processed_members