Coverage for website/members/emails.py: 84.62%
58 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
2from datetime import timedelta
4from django.conf import settings
5from django.core import mail
6from django.db.models import Exists, OuterRef, Q, Subquery
7from django.urls import reverse
8from django.utils import timezone
10from members.models import Member, Membership
11from utils.snippets import send_email
13logger = logging.getLogger(__name__)
16def send_information_request(dry_run=False):
17 """Send an email to all members to have them check their personal information.
19 :param dry_run: does not really send emails if True
20 """
21 members = Member.current_members.all().exclude(email="")
23 with mail.get_connection() as connection:
24 for member in members:
25 logger.info("Sent email to %s (%s)", member.get_full_name(), member.email)
26 if not dry_run: 26 ↛ 24line 26 didn't jump to line 24 because the condition on line 26 was always true
27 send_email(
28 to=[member.email],
29 subject="Membership information check",
30 txt_template="members/email/information_check.txt",
31 html_template="members/email/information_check.html",
32 connection=connection,
33 context={
34 k: x if x else ""
35 for k, x in {
36 "name": member.first_name,
37 "username": member.username,
38 "full_name": member.get_full_name(),
39 "address_street": member.profile.address_street,
40 "address_street2": member.profile.address_street2,
41 "address_postal_code": member.profile.address_postal_code,
42 "address_city": member.profile.address_city,
43 "address_country": member.profile.get_address_country_display(),
44 "phone_number": member.profile.phone_number,
45 "birthday": member.profile.birthday,
46 "email": member.email,
47 "student_number": member.profile.student_number,
48 "starting_year": member.profile.starting_year,
49 "programme": member.profile.get_programme_display(),
50 "base_url": settings.BASE_URL,
51 }.items()
52 },
53 )
55 if not dry_run: 55 ↛ exitline 55 didn't jump to the function exit
56 send_email(
57 to=[settings.BOARD_NOTIFICATION_ADDRESS],
58 subject="Membership information check sent",
59 txt_template="members/email/information_check_notification.txt",
60 html_template="members/email/information_check_notification.html",
61 context={"members": members},
62 connection=connection,
63 )
66def send_expiration_announcement(dry_run=False):
67 """Send an email to all members whose membership will end in the next 31 days to warn them about this.
69 :param dry_run: does not really send emails if True
70 """
71 has_future_membership = Exists(
72 Subquery(
73 Membership.objects.filter(
74 user=OuterRef("pk"),
75 since__lte=timezone.now() + timedelta(days=31),
76 until__gte=timezone.now() + timedelta(days=31),
77 )
78 )
79 )
81 has_expiring_membership = Exists(
82 Subquery(
83 Membership.objects.filter(
84 user=OuterRef("pk"),
85 until__gte=timezone.now(),
86 until__lt=timezone.now() + timedelta(days=31),
87 study_long=False,
88 )
89 )
90 )
92 members = Member.current_members.filter(
93 has_expiring_membership, ~has_future_membership
94 ).exclude(email="")
96 with mail.get_connection() as connection:
97 for member in members:
98 logger.info("Sent email to %s (%s)", member.get_full_name(), member.email)
99 if not dry_run: 99 ↛ 97line 99 didn't jump to line 97 because the condition on line 99 was always true
100 send_email(
101 to=[member.email],
102 subject="Membership expiration announcement",
103 txt_template="members/email/expiration_announcement.txt",
104 html_template="members/email/expiration_announcement.html",
105 connection=connection,
106 context={
107 "name": member.get_full_name(),
108 "renewal_url": settings.BASE_URL
109 + reverse("registrations:renew"),
110 "membership_discount": settings.MEMBERSHIP_PRICES["year"],
111 },
112 )
114 if not dry_run: 114 ↛ exitline 114 didn't jump to the function exit
115 send_email(
116 to=[settings.BOARD_NOTIFICATION_ADDRESS],
117 subject="Membership expiration announcement sent",
118 txt_template="members/email/expiration_announcement_notification.txt",
119 html_template="members/email/expiration_announcement_notification.html",
120 connection=connection,
121 context={"members": members},
122 )
125def send_expiration_study_long(dry_run=False):
126 """Send an email to members whose current study-long membership will end in the next 31 days."""
127 has_expiring_study_long_membership = Exists(
128 Subquery(
129 Membership.objects.filter(
130 user=OuterRef("pk"),
131 study_long=True,
132 until__gte=timezone.now(),
133 until__lt=timezone.now() + timedelta(days=31),
134 )
135 )
136 )
138 has_future_membership = Exists(
139 Subquery(
140 Membership.objects.filter(
141 user=OuterRef("pk"),
142 since__lte=timezone.now() + timedelta(days=31),
143 until__gte=timezone.now() + timedelta(days=31),
144 )
145 )
146 )
148 members = Member.current_members.filter(
149 has_expiring_study_long_membership, ~has_future_membership
150 ).exclude(email="")
152 with mail.get_connection() as connection:
153 for member in members:
154 logger.info("Sent email to %s (%s)", member.get_full_name(), member.email)
155 if not dry_run: 155 ↛ 153line 155 didn't jump to line 153 because the condition on line 155 was always true
156 send_email(
157 to=[member.email],
158 subject="Membership expiration warning",
159 txt_template="members/email/yearly_study_check.txt",
160 html_template="members/email/yearly_study_check.html",
161 connection=connection,
162 context={
163 "name": member.get_full_name(),
164 "renewal_url": settings.BASE_URL
165 + reverse("registrations:renew"),
166 },
167 )
170def send_expiration_study_long_reminder(dry_run=False):
171 """Send an email to members whose current study-long membership has expired in the last 31 days."""
172 has_expired_study_long_membership = Exists(
173 Subquery(
174 Membership.objects.filter(
175 user=OuterRef("pk"),
176 study_long=True,
177 until__gte=timezone.now() - timedelta(days=31),
178 until__lte=timezone.now(),
179 )
180 )
181 )
182 has_current_membership = Exists(
183 Subquery(
184 Membership.objects.filter(
185 Q(until__gte=timezone.now()) | Q(until__isnull=True),
186 since__lte=timezone.now(),
187 user=OuterRef("pk"),
188 )
189 )
190 )
191 members = Member.objects.filter(
192 has_expired_study_long_membership, ~has_current_membership
193 ).exclude(email="")
194 with mail.get_connection() as connection:
195 for member in members:
196 logger.info("Sent email to %s (%s)", member.get_full_name(), member.email)
197 if not dry_run: 197 ↛ 195line 197 didn't jump to line 195 because the condition on line 197 was always true
198 send_email(
199 to=[member.email],
200 subject="Membership expiration warning",
201 txt_template="members/email/yearly_study_check_reminder.txt",
202 html_template="members/email/yearly_study_check_reminder.html",
203 connection=connection,
204 context={
205 "name": member.get_full_name(),
206 "renewal_url": settings.BASE_URL
207 + reverse("registrations:renew"),
208 },
209 )
212def send_welcome_message(user, password):
213 """Send an email to a new user welcoming them.
215 :param user: the new user
216 :param password: randomly generated password
217 """
218 send_email(
219 to=[user.email],
220 subject="Welcome to Study Association Thalia",
221 txt_template="members/email/welcome.txt",
222 html_template="members/email/welcome.html",
223 context={
224 "full_name": user.get_full_name(),
225 "username": user.username,
226 "password": password,
227 "url": settings.BASE_URL,
228 "base_url": settings.BASE_URL,
229 },
230 )
233def send_email_change_confirmation_messages(change_request):
234 """Send emails to the old and new email address of a member to confirm the email change.
236 :param change_request the email change request entered by the user
237 """
238 member = change_request.member
240 confirm_link = settings.BASE_URL + reverse(
241 "members:email-change-confirm",
242 args=[change_request.confirm_key],
243 )
244 send_email(
245 to=[member.email],
246 subject="Please confirm your email change",
247 txt_template="members/email/email_change_confirm.txt",
248 html_template="members/email/email_change_confirm.html",
249 context={
250 "confirm_link": confirm_link,
251 "name": member.first_name,
252 },
253 )
255 confirm_link = settings.BASE_URL + reverse(
256 "members:email-change-verify",
257 args=[change_request.verify_key],
258 )
259 send_email(
260 to=[change_request.email],
261 subject="Please verify your email address",
262 txt_template="members/email/email_change_verify.txt",
263 html_template="members/email/email_change_verify.html",
264 context={
265 "confirm_link": confirm_link,
266 "name": member.first_name,
267 },
268 )
271def send_email_change_completion_message(change_request):
272 """Send email to the member to confirm the email change.
274 :param change_request the email change request entered by the user
275 """
276 send_email(
277 to=[change_request.member.email],
278 subject="Your email address has been changed",
279 txt_template="members/email/email_change_completed.txt",
280 html_template="members/email/email_change_completed.html",
281 context={
282 "name": change_request.member.first_name,
283 },
284 )