Coverage for website/members/models/member.py: 79.31%
96 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
1import logging
2import operator
3from datetime import timedelta
4from functools import reduce
6from django.contrib.auth.models import User, UserManager
7from django.db.models import Q
8from django.urls import reverse
9from django.utils import timezone
10from django.utils.functional import cached_property
11from django.utils.translation import gettext_lazy as _
13from activemembers.models import MemberGroup, MemberGroupMembership
14from members.models.membership import Membership
16logger = logging.getLogger(__name__)
19class MemberManager(UserManager):
20 """Get all members, i.e. all users with a profile."""
22 def get_queryset(self):
23 return super().get_queryset().exclude(profile=None)
26class ActiveMemberManager(MemberManager):
27 """Get all active members, i.e. who have a committee membership."""
29 def get_queryset(self):
30 """Select all committee members."""
31 active_memberships = MemberGroupMembership.active_objects.filter(
32 group__board=None
33 ).filter(group__society=None)
35 return (
36 super()
37 .get_queryset()
38 .filter(membergroupmembership__in=active_memberships)
39 .distinct()
40 )
43class CurrentMemberManager(MemberManager):
44 """Get all members with an active membership."""
46 def get_queryset(self):
47 """Select all members who have a current membership."""
48 return (
49 super()
50 .get_queryset()
51 .exclude(membership=None)
52 .filter(
53 Q(membership__until__isnull=True)
54 | Q(membership__until__gt=timezone.now().date())
55 )
56 .distinct()
57 )
59 def with_birthdays_in_range(self, from_date, to_date):
60 """Select all who are currently a Thalia member and have a birthday within the specified range.
62 :param from_date: the start of the range (inclusive)
63 :param to_date: the end of the range (inclusive)
64 :paramtype from_date: datetime
65 :paramtype to_date: datetime
67 :return: the filtered queryset
68 :rtype: Queryset
69 """
70 queryset = self.get_queryset().filter(profile__birthday__lte=to_date)
72 if (to_date - from_date).days >= 366:
73 # 366 is important to also account for leap years
74 # Everyone that's born before to_date has a birthday
75 return queryset
77 delta = to_date - from_date
78 dates = [from_date + timedelta(days=i) for i in range(delta.days + 1)]
79 monthdays = [
80 {"profile__birthday__month": d.month, "profile__birthday__day": d.day}
81 for d in dates
82 ]
83 # Don't get me started (basically, we are making a giant OR query with
84 # all days and months that are in the range)
85 query = reduce(operator.or_, [Q(**d) for d in monthdays])
86 return queryset.filter(query)
89class Member(User):
90 class Meta:
91 proxy = True
92 ordering = ("first_name", "last_name")
94 objects = MemberManager()
95 current_members = CurrentMemberManager()
96 active_members = ActiveMemberManager()
98 def __str__(self):
99 return f"{self.get_full_name()} ({self.username})"
101 def refresh_from_db(self, **kwargs):
102 # Clear the cached current- and latest_membership
103 if hasattr(self, "_current_membership"): 103 ↛ 104line 103 didn't jump to line 104 because the condition on line 103 was never true
104 del self._current_membership
105 if hasattr(self, "current_membership"): 105 ↛ 107line 105 didn't jump to line 107 because the condition on line 105 was always true
106 del self.current_membership
107 if hasattr(self, "latest_membership"): 107 ↛ 110line 107 didn't jump to line 110 because the condition on line 107 was always true
108 del self.latest_membership
110 return super().refresh_from_db(**kwargs)
112 @cached_property
113 def current_membership(self) -> Membership | None:
114 """Return the currently active membership of the user, None if not active.
116 This might not be the latest membership, as a latest membership may be in the future.
118 Warning: this property is cached.
119 You can use `refresh_from_db` to clear it.
120 """
121 # Use membership from a Prefetch object if available.
122 if hasattr(self, "_current_membership"): 122 ↛ 123line 122 didn't jump to line 123 because the condition on line 122 was never true
123 if not self._current_membership:
124 return None
125 return self._current_membership[0]
127 today = timezone.now().date()
128 try:
129 return self.membership_set.filter(
130 Q(until__isnull=True) | Q(until__gt=today),
131 since__lte=today,
132 ).latest("since")
133 except Membership.DoesNotExist:
134 return None
136 @cached_property
137 def latest_membership(self) -> Membership | None:
138 """Get the most recent (potentially future) membership of this user.
140 Warning: this property is cached.
141 You can use `refresh_from_db` to clear it.
142 """
143 if not self.membership_set.exists():
144 return None
145 return self.membership_set.latest("since")
147 @property
148 def earliest_membership(self) -> Membership | None:
149 """Get the earliest membership of this user."""
150 if not self.membership_set.exists():
151 return None
152 return self.membership_set.earliest("since")
154 def has_been_member(self) -> bool:
155 """Has this user ever been a member?."""
156 return self.membership_set.filter(type="member").exists()
158 def has_been_honorary_member(self) -> bool:
159 """Has this user ever been an honorary member?."""
160 return self.membership_set.filter(type="honorary").exists()
162 def has_active_membership(self) -> bool:
163 """Is this member currently active.
165 Tested by checking if the expiration date has passed.
166 """
167 return self.current_membership is not None
169 # Special properties for admin site
170 has_active_membership.boolean = True
171 has_active_membership.short_description = _("Is this user currently active")
173 @classmethod
174 def all_with_membership(cls, membership_type):
175 """Get all users who have a specific membership.
177 :param membership_type: The membership to select by
178 :return: List of users
179 :rtype: [Member]
180 """
181 return [
182 x
183 for x in cls.objects.all()
184 if x.current_membership and x.current_membership.type == membership_type
185 ]
187 @property
188 def can_attend_events(self):
189 """May this user attend events."""
190 if not self.profile: 190 ↛ 191line 190 didn't jump to line 191 because the condition on line 190 was never true
191 return False
193 return (
194 self.profile.event_permissions in ("all", "no_drinks")
195 and self.has_active_membership()
196 )
198 @property
199 def can_attend_events_without_membership(self):
200 if not self.profile:
201 return False
203 return self.profile.event_permissions in ("all", "no_drinks")
205 def get_member_groups(self):
206 """Get the groups this user is a member of."""
207 now = timezone.now()
208 return MemberGroup.objects.filter(
209 Q(membergroupmembership__member=self),
210 Q(membergroupmembership__until=None)
211 | Q(
212 membergroupmembership__since__lte=now,
213 membergroupmembership__until__gte=now,
214 ),
215 active=True,
216 )
218 def get_absolute_url(self):
219 return reverse("members:profile", args=[str(self.pk)])