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