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

1import logging 

2from datetime import timedelta 

3 

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 

9 

10from members.models import Member, Membership 

11from utils.snippets import send_email 

12 

13logger = logging.getLogger(__name__) 

14 

15 

16def send_information_request(dry_run=False): 

17 """Send an email to all members to have them check their personal information. 

18 

19 :param dry_run: does not really send emails if True 

20 """ 

21 members = Member.current_members.all().exclude(email="") 

22 

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 ) 

54 

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 ) 

64 

65 

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. 

68 

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 ) 

80 

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 ) 

91 

92 members = Member.current_members.filter( 

93 has_expiring_membership, ~has_future_membership 

94 ).exclude(email="") 

95 

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 ) 

113 

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 ) 

123 

124 

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 ) 

137 

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 ) 

147 

148 members = Member.current_members.filter( 

149 has_expiring_study_long_membership, ~has_future_membership 

150 ).exclude(email="") 

151 

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 ) 

168 

169 

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 ) 

210 

211 

212def send_welcome_message(user, password): 

213 """Send an email to a new user welcoming them. 

214 

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 ) 

231 

232 

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. 

235 

236 :param change_request the email change request entered by the user 

237 """ 

238 member = change_request.member 

239 

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 ) 

254 

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 ) 

269 

270 

271def send_email_change_completion_message(change_request): 

272 """Send email to the member to confirm the email change. 

273 

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 )