Coverage for app/logic/emailHandler.py: 82%

166 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-06-26 15:36 +0000

1from datetime import datetime 

2from peewee import DoesNotExist, JOIN 

3from flask_mail import Mail, Message, Attachment 

4import os 

5 

6from app import app 

7from app.models.interest import Interest 

8from app.models.user import User 

9from app.models.program import Program 

10from app.models.eventRsvp import EventRsvp 

11from app.models.emailTemplate import EmailTemplate 

12from app.models.emailLog import EmailLog 

13from app.models.event import Event 

14from app.models.eventParticipant import EventParticipant 

15from app.models.programBan import ProgramBan 

16from app.models.term import Term 

17 

18class EmailHandler: 

19 def __init__(self, raw_form_data, url_domain, attachment_file=[]): 

20 

21 self.mail = Mail(app) 

22 self.raw_form_data = raw_form_data 

23 self.url_domain = url_domain 

24 self.override_all_mail = app.config['MAIL_OVERRIDE_ALL'] 

25 self.sender_username = None 

26 self.sender_name = None 

27 self.sender_address = None 

28 self.reply_to = app.config['MAIL_REPLY_TO_ADDRESS'] 

29 self.template_identifier = None 

30 self.subject = None 

31 self.body = None 

32 self.event = None 

33 self.program = None 

34 self.recipients = None 

35 self.sl_course_id = None 

36 self.attachment_path = app.config['files']['base_path'] + app.config['files']['email_attachment_path'] 

37 self.attachment_filepaths = [] 

38 self.attachment_file = attachment_file 

39 

40 def process_data(self): 

41 """ Processes raw data and stores it in class variables to be used by other methods """ 

42 # Email Template Data 

43 # Template Identifier 

44 if 'templateIdentifier' in self.raw_form_data: 

45 self.template_identifier = self.raw_form_data['templateIdentifier'] 

46 

47 if 'subject' in self.raw_form_data: 

48 self.subject = self.raw_form_data['subject'] 

49 

50 # Event 

51 if 'eventID' in self.raw_form_data: 

52 event = Event.get_by_id(self.raw_form_data['eventID']) 

53 self.event = event 

54 

55 # Program 

56 if self.event: 

57 self.program = self.event.program 

58 

59 if 'emailSender' in self.raw_form_data: 

60 self.sender_username = self.raw_form_data['emailSender'] 

61 self.sender_name, self.sender_address, self.reply_to = self.getSenderInfo() 

62 

63 if 'body' in self.raw_form_data: 

64 self.body = self.raw_form_data['body'] 

65 

66 # Recipients 

67 if 'recipientsCategory' in self.raw_form_data: 

68 self.recipients_category = self.raw_form_data['recipientsCategory'] 

69 self.recipients = self.retrieve_recipients(self.recipients_category) 

70 

71 # Service-Learning Course 

72 if 'slCourseId' in self.raw_form_data: 

73 self.sl_course_id = self.raw_form_data['slCourseId'] 

74 

75 def getSenderInfo(self): 

76 programObject = Program.get_or_none(Program.programName == self.sender_username) 

77 userObj = User.get_or_none(User.username == self.sender_username) 

78 senderInfo = [None, None, None] 

79 if programObject: 

80 programEmail = programObject.contactEmail 

81 senderInfo = [programObject.programName, programEmail, programEmail] 

82 elif self.sender_username.upper() == "CELTS": 

83 senderInfo = ["CELTS", "celts@berea.edu", "celts@berea.edu"] 

84 elif userObj: 

85 senderInfo = [f"{userObj.fullName}", userObj.email, userObj.email] 

86 # overwrite the sender info with intentional keys in the raw form data. 

87 get = self.raw_form_data.get 

88 senderInfo = [get('sender_name') or senderInfo[0], get('sender_address') or senderInfo[1], get('reply_to') or senderInfo[2]] 

89 

90 return senderInfo # If the email is not being sent from a program or user, use default values. 

91 

92 def update_sender_config(self): 

93 # We might need this. 

94 # This functionality should be moved somewhere else. 

95 # The function in another file would receive email_info[sender] 

96 # and update the config based on that and wherever we will end up saving emails and passwords 

97 #The sender information should be saved like so: {"name": [email, password], } or in the database 

98 pass 

99 

100 def retrieve_recipients(self, recipients_category): 

101 """ Retrieves recipient based on which category is chosen in the 'To' section of the email modal """ 

102 # Other potential recipients: 

103 # - course instructors 

104 # - course Participants 

105 # - outside participants' 

106 if recipients_category == "Interested": 

107 recipients = (User.select() 

108 .join(Interest) 

109 .join(Program, on=(Program.id==Interest.program)) 

110 .where(Interest.program == self.program)) 

111 if recipients_category == "RSVP'd": 

112 recipients = (User.select() 

113 .join(EventRsvp) 

114 .where(EventRsvp.event==self.event.id)) 

115 

116 if recipients_category == "Eligible Students": 

117 # all terms with the same accademic year as the current term, 

118 # the allVolunteer training term then needs to be in that query 

119 Term2 = Term.alias() 

120 

121 sameYearTerms = Term.select().join(Term2, on=(Term.academicYear == Term2.academicYear)).where(Term2.isCurrentTerm == True) 

122 

123 bannedUsers = ProgramBan.select(ProgramBan.user).where((ProgramBan.endDate > datetime.now()) | (ProgramBan.endDate is None), ProgramBan.program == (self.program if self.program else ProgramBan.program)) 

124 allVolunteer = Event.select().where(Event.isAllVolunteerTraining == True, Event.term.in_(sameYearTerms)) 

125 recipients = User.select().join(EventParticipant).where(User.username.not_in(bannedUsers), EventParticipant.event.in_(allVolunteer)) 

126 return list(recipients) 

127 

128 

129 

130 def replaceDynamicPlaceholders(self, email_body, *, name): 

131 """ Replaces placeholders that cannot be predetermined on the front-end """ 

132 event_link = f"{self.url_domain}/event/{self.event.id}/view" 

133 new_body = email_body.format(recipient_name=name, event_link=event_link) 

134 return new_body 

135 

136 def retrieve_and_modify_email_template(self): 

137 """ Retrieves email template based on idenitifer and calls replace_general_template_placeholders""" 

138 

139 email_template = EmailTemplate.get(EmailTemplate.purpose==self.template_identifier) # --Q: should we keep purpose as the identifier? 

140 template_id = email_template.id 

141 

142 body = EmailHandler.replaceStaticPlaceholders(self.event.id, self.body) 

143 

144 self.reply_to = email_template.replyToAddress 

145 return (template_id, self.subject, body) 

146 

147 def getAttachmentFullPath(self, newfile=None): 

148 """ 

149 This creates the directory/path for the object from the "Choose File" input in the emailModal.html file. 

150 :returns: directory path for attachment 

151 """ 

152 attachmentFullPath = None 

153 try: 

154 # tries to create the full path of the files location and passes if 

155 # the directories already exist or there is no attachment 

156 attachmentFullPath = os.path.join(self.attachment_path, newfile.filename) 

157 if attachmentFullPath[:-1] == self.attachment_path: 

158 return None 

159 os.mkdir(self.attachment_path) 

160 

161 except AttributeError: # will pass if there is no attachment to save 

162 pass 

163 except FileExistsError: # will pass if the file already exists 

164 pass 

165 return attachmentFullPath 

166 

167 def saveAttachment(self): 

168 """ Saves the attachment in the app/static/files/attachments/ directory """ 

169 try: 

170 for file in self.attachment_file: 

171 attachmentFullPath = self.getAttachmentFullPath(newfile = file) 

172 if attachmentFullPath: 

173 file.save(attachmentFullPath) # saves attachment in directory 

174 self.attachment_filepaths.append(attachmentFullPath) 

175 

176 except AttributeError: # will pass if there is no attachment to save 

177 pass 

178 

179 def store_sent_email(self, subject, template_id): 

180 """ Stores sent email in the email log """ 

181 date_sent = datetime.now() 

182 

183 attachmentNames = [] 

184 for file in self.attachment_file: 

185 attachmentNames.append(file.filename) 

186 

187 EmailLog.create( 

188 event = self.event.id, 

189 subject = subject, 

190 templateUsed = template_id, 

191 recipientsCategory = self.recipients_category, 

192 recipients = ", ".join(recipient.email for recipient in self.recipients), 

193 dateSent = date_sent, 

194 sender = self.sender_username, 

195 attachmentNames = attachmentNames) 

196 

197 def build_email(self): 

198 # Most General Scenario 

199 self.saveAttachment() 

200 self.process_data() 

201 template_id, subject, body = self.retrieve_and_modify_email_template() 

202 return (template_id, subject, body) 

203 

204 def send_email(self): 

205 defaultEmailInfo = {"senderName":"CELTS", "replyTo":app.config['celts_admin_contact'], "senderAddress":app.config['celts_admin_contact']} 

206 template_id, subject, body = self.build_email() 

207 

208 attachmentList = [] 

209 for i, filepath in enumerate(self.attachment_filepaths): 

210 with app.open_resource(filepath[4:]) as file: 

211 attachmentList.append(Attachment(filename=filepath.split('/')[-1], content_type=self.attachment_file[i].content_type, data=file.read())) 

212 

213 try: 

214 with self.mail.connect() as conn: 

215 for recipient in self.recipients: 

216 full_name = f'{recipient.firstName} {recipient.lastName}' 

217 email_body = self.replaceDynamicPlaceholders(body, name=full_name) 

218 conn.send(Message( 

219 subject, 

220 # [recipient.email], 

221 [self.override_all_mail], 

222 email_body, 

223 attachments = attachmentList, 

224 reply_to = self.reply_to or defaultEmailInfo["replyTo"], 

225 sender = (self.sender_name or defaultEmailInfo["senderName"], self.sender_address or defaultEmailInfo["senderAddress"]) 

226 )) 

227 self.store_sent_email(subject, template_id) 

228 return True 

229 except Exception as e: 

230 print("Error on sending email: ", e) 

231 return False 

232 

233 def update_email_template(self): 

234 try: 

235 self.process_data() 

236 (EmailTemplate.update({ 

237 EmailTemplate.subject: self.subject, 

238 EmailTemplate.body: self.body, 

239 EmailTemplate.replyToAddress: self.reply_to 

240 }).where(EmailTemplate.purpose==self.template_identifier)).execute() 

241 return True 

242 except Exception as e: 

243 print("Error updating email template record: ", e) 

244 return False 

245 

246 def retrieve_last_email(event_id): 

247 try: 

248 last_email = EmailLog.select().where(EmailLog.event==event_id).order_by(EmailLog.dateSent.desc()).get() 

249 return last_email 

250 except DoesNotExist: 

251 return None 

252 

253 

254 @staticmethod 

255 def retrievePlaceholderList(eventId): 

256 event = Event.get_by_id(eventId) 

257 return [ 

258 ["Recipient Name", "{recipient_name}"], 

259 ["Event Name", event.name], 

260 ["Start Date", (event.startDate).strftime('%m/%d/%Y')], 

261 ["End Date", (event.endDate).strftime('%m/%d/%Y')], 

262 ["Start Time", (event.timeStart).strftime('%I:%M')], 

263 ["End Time", (event.timeEnd).strftime('%I:%M')], 

264 ["Location", event.location], 

265 ["Event Link", "{event_link}"], 

266 ["Relative Time", event.relativeTime] 

267 ] 

268 

269 @staticmethod 

270 def replaceStaticPlaceholders(eventId, email_body): 

271 """ Replaces all template placeholders except for those that can't be known until just before Send-time """ 

272 event = Event.get_by_id(eventId) 

273 

274 new_body = email_body.format(event_name=event.name, 

275 location=event.location, 

276 start_date=(event.startDate).strftime('%m/%d/%Y'), 

277 end_date=(event.endDate).strftime('%m/%d/%Y'), 

278 start_time=(event.timeStart).strftime('%I:%M'), 

279 end_time=(event.timeEnd).strftime('%I:%M'), 

280 event_link="{event_link}", 

281 recipient_name="{recipient_name}", 

282 relative_time=event.relativeTime) 

283 return new_body