Coverage for app/logic/emailHandler.py: 82%
166 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-11-19 23:53 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2024-11-19 23:53 +0000
1from datetime import datetime
2from peewee import DoesNotExist, JOIN
3from flask_mail import Mail, Message, Attachment
4import os
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
18class EmailHandler:
19 def __init__(self, raw_form_data, url_domain, attachment_file=[]):
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
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']
47 if 'subject' in self.raw_form_data:
48 self.subject = self.raw_form_data['subject']
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
55 # Program
56 if self.event:
57 self.program = self.event.program
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()
63 if 'body' in self.raw_form_data:
64 self.body = self.raw_form_data['body']
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)
71 # Service-Learning Course
72 if 'slCourseId' in self.raw_form_data:
73 self.sl_course_id = self.raw_form_data['slCourseId']
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]]
90 return senderInfo # If the email is not being sent from a program or user, use default values.
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
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))
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()
121 sameYearTerms = Term.select().join(Term2, on=(Term.academicYear == Term2.academicYear)).where(Term2.isCurrentTerm == True)
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)
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
136 def retrieve_and_modify_email_template(self):
137 """ Retrieves email template based on idenitifer and calls replace_general_template_placeholders"""
139 email_template = EmailTemplate.get(EmailTemplate.purpose==self.template_identifier) # --Q: should we keep purpose as the identifier?
140 template_id = email_template.id
142 body = EmailHandler.replaceStaticPlaceholders(self.event.id, self.body)
144 self.reply_to = email_template.replyToAddress
145 return (template_id, self.subject, body)
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)
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
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)
176 except AttributeError: # will pass if there is no attachment to save
177 pass
179 def store_sent_email(self, subject, template_id):
180 """ Stores sent email in the email log """
181 date_sent = datetime.now()
183 attachmentNames = []
184 for file in self.attachment_file:
185 attachmentNames.append(file.filename)
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)
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)
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()
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()))
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
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
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
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 ]
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)
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