Quality-of-life improvements to email reminders.

-Tweaked email body wording
-Added To: field
-Added librarian@ as a recipient of reminder emails
-Added a confirmation that emails were sent
-Added ability to cancel email sending (by using cancel button)
-Added a nice confirmation dialog at the end of sending
 reminder emails to keep the librarian's hopes up
email_reminders
Patrick Melanson 5 years ago
parent bff0abf3aa
commit c812a6634e
  1. 1
      TODO
  2. 7
      debian/changelog
  3. 23
      library/emails.py
  4. 21
      library/interface/form.py
  5. 88
      library/interface/sendemails.py
  6. 1
      run_emails_test.exp

@ -36,7 +36,6 @@ Support UTF-8 for everything
Search ignores Case (for lowercase search strings)
Text entry supports longer string
Home and End navigate to top and bottom of catalogue respectively.
Email reminders for signed-out books
Support for multiple copies
- books will have their book_id written in pencil on inside cover

7
debian/changelog vendored

@ -1,3 +1,10 @@
library (1.0-4stretch0) UNRELEASED; urgency=medium
* New menu option to send out mass email for overdue books
* Community effort by Connor Murphy, Charlie Wang, Patrick Melanson
-- Patrick James Melanson <pj2melan@csclub.uwaterloo.ca> Sat, 29 Jul 2017 15:12:12 -0400
library (1.0-3stretch0) stretch; urgency=medium
* Package for stretch

@ -8,17 +8,24 @@ def format_reminder_email(quest_id: str,
Example: _send_email("s455wang", "2017-02-04", 30, "csfmurph", "How to Design Programs")
"""
assert len(quest_id) <= 8
assert quest_id.isalnum()
assert days_signed_out > 0
assert librarian_name != ""
assert book_name != ""
email_message_body = \
"""Hi {},
Our records indicate that you have had the book {} signed out for {} days.
Please return the book to the CS Club office (MC 3036) at your earliest convenience.
If you would like to keep this book checked out, tell us when in the next month you will return this book.
Thanks,
Otherwise, please return the book to the CS Club office (MC 3036) at your earliest convenience.
{}
Thank you for using the CS Club library!
{} | Librarian
Computer Science Club | University of Waterloo
librarian@csclub.uwaterloo.ca""".format(
quest_id,
@ -28,7 +35,13 @@ librarian@csclub.uwaterloo.ca""".format(
)
email_message_subject = "Overdue book: {}".format(book_name)
email_message = "Subject: {}\n\n{}".format(email_message_subject,
email_message_body)
email_message = (
"To: \"{0}@csclub.uwaterloo.ca\" <{0}@csclub.uwaterloo.ca>\n".format(quest_id) +
"Subject: {}\n".format(email_message_subject) +
"\n" +
email_message_body
)
assert email_message.replace("\n", "").isprintable(), \
"Our email should not have characters apart from normal characters and newline"
return email_message

@ -48,7 +48,7 @@ class TextEntry:
def gain_focus(self):
self.focus = True
self._mv_cursor(+len(self.value))
self.start = max(0,self.cursor-self.width)
self.start = max(0,self.cursor-self.width)
self.redraw()
def lose_focus(self):
@ -85,7 +85,7 @@ class TextEntry:
def _set_cursor(self, new_c):
self.cursor = max(0, min(len(self.value), new_c))
self.start = max(0,self.cursor-self.width+1)
self.start = max(0,self.cursor-self.width+1)
self.redraw()
# Place the drawn cursor in the correct spot
col = self.x + self.cursor - self.start
@ -358,12 +358,13 @@ class CategoryForm(FormWindow):
def _return_values(self):
return self.entries[0].value
class ErrorForm(FormWindow):
caption = "Error"
class InfoForm(FormWindow):
caption = "Info"
blabel = "OK"
buttononly = True
def __init__(self,window,helpbar,errortext,width=50):
def __init__(self,caption,window,helpbar,errortext,width=50):
self.labels = errortext.split("\n")
self.caption = caption
super().__init__(window, helpbar, width=width)
def redraw(self):
@ -380,6 +381,16 @@ class ErrorForm(FormWindow):
def _mv_focus(self,delta): pass
class ErrorForm(InfoForm):
def __init__(self,window,helpbar,errortext,width=50):
super().__init__("Error", window, helpbar, errortext, width)
class SuccessForm(InfoForm):
def __init__(self,window,helpbar,errortext,width=50):
super().__init__("Success", window, helpbar, errortext, width)
# Any good news is great news in the world of the librarian.
self.blabel = "Great News!"
def error_form(text, w, hb):
width = max([len(l) for l in text.split("\n")]) + 4
child=curses.newwin(1,1)

@ -21,7 +21,7 @@ import subprocess
from library import permissions
from library.exceptions import *
import library.database as db
from library.interface.form import FormWindow, BookForm, LoginForm, catch_error_with, error_form
from library.interface.form import FormWindow, BookForm, LoginForm, SuccessForm, catch_error_with, error_form
from library.emails import format_reminder_email
# email testing folder creation
@ -35,10 +35,17 @@ MAX_LOGIN_ATTEMPTS = 3
class DaysForm(FormWindow):
caption = "Enter the max number of days a book can be signed out for"
blabel = "Enter"
labels = ["Days"]
labels = ["Days (default " + str(DEFAULT_DAY_VALUE) + ")"]
def _return_values(self):
return self.entries[0].value
ret = self.entries[0].value
if ret is "":
return DEFAULT_DAY_VALUE
else:
#If we didn't get valid input, noisily fail
assert ret.isdigit() and int(ret) > 0, \
"Max signed out days is not positive: " + ret.__repr__()
return int(ret)
class NameForm(FormWindow):
@ -50,6 +57,7 @@ class NameForm(FormWindow):
if self.entries[0].value is "":
return "Librarian"
else:
assert self.entries[0].value.isprintable()
return self.entries[0].value
@ -58,13 +66,15 @@ def _send_email(quest_id: str,
signed_out_date: str,
max_days_can_be_signed_out: int,
librarian_name: str,
book_name_list,
testing=False) -> None:
book_name: str,
testing=False) -> bool:
"""Sends an email to quest_id@csclub.uwaterloo.ca if the date the book
was signed out exceeds the days it is supposed to be signed out for.
Set testing to true to output emails to the current directory, instead
of actually sending them.
Returns whether an email was sent.
"""
# Determine the days the book has been signed out
@ -72,15 +82,22 @@ def _send_email(quest_id: str,
days_signed_out = (d.today() - d).days
if days_signed_out > max_days_can_be_signed_out:
email_address = quest_id + "@csclub.uwaterloo.ca"
book_name = "".join(book_name_list)
#Librarian also gets a copy, so that unreplied emails are more apparent.
email_addresses = [quest_id + "@csclub.uwaterloo.ca",
"librarian@csclub.uwaterloo.ca"]
email_message = format_reminder_email(quest_id, days_signed_out, librarian_name, book_name)
if testing:
assert book_name.isprintable()
os.makedirs("test_emails", exist_ok=True)
with open("test_emails/{}_{}.txt".format(quest_id, book_name), 'w') as f:
#Know that book titles such as "FORTRAN/77" exist, sanitize slashes.
filename = ("test_emails/{}_{}.txt"
.format(quest_id, book_name.replace('/', '-')))
with open(filename, 'w') as f:
print(email_message, file=f)
else:
server.sendmail("librarian@csclub.uwaterloo.ca", email_address, email_message)
server.sendmail("librarian@csclub.uwaterloo.ca", email_addresses, email_message)
return True
return False
#Public functions
@ -100,17 +117,17 @@ def sendemails_procedure(w, hb, cy, cx, mx):
w.mvwin(cy - r // 2, cx - c // 2)
days = step1.event_loop()
step1.clear()
if days == {}:
return "" #User cancelled the dialog box
#Set the days to a default value if the value given is less than 0
if not isinstance(days, int) or days < 0:
days = DEFAULT_DAY_VALUE
#Get the name of the librarain
#Get the name of the librarian
step2 = NameForm(w, hb, width=mx - 20)
(r, c) = w.getmaxyx()
w.mvwin(cy - r // 2, cx - c // 2)
librarianName = step2.event_loop()
step2.clear()
if librarianName == {}:
return "" #User cancelled the dialog box
#Set up email
global server
@ -149,18 +166,53 @@ def sendemails_procedure(w, hb, cy, cx, mx):
#Get the books that are signed out
signed_out_books = db.get_checkedout_books()
earliest_checkout = datetime.now()
emails_sent = 0
for data in signed_out_books:
quest_id = data["uwid"]
date = str(data["date"]).split(" ")[0]
book_name_with_spaces = data["title"]
_send_email(
str(quest_id),
str(date), days, str(librarianName), book_name_with_spaces,
testing)
if _send_email(
str(quest_id),
str(date), days, str(librarianName), book_name_with_spaces,
testing):
emails_sent += 1
earliest_checkout = min(earliest_checkout,
datetime.strptime(date, "%Y-%m-%d"))
#Exit from the email server
server.quit()
#A success message about what emails we just sent
assert earliest_checkout > datetime(2010,1,1)
time_since_earliest_checkout = ''
days_since_earliest_checkout = (datetime.now().toordinal()
- earliest_checkout.toordinal())
assert days >= 0
month_length = 30 #It's wrong because it's an approximation
if days_since_earliest_checkout < month_length:
time_since_earliest_checkout = '{} days ago'.format(days_since_earliest_checkout)
else:
time_since_earliest_checkout = '{:0.1f} months ago'.format(days_since_earliest_checkout / month_length)
if emails_sent == 0:
success_text = """
No books are overdue, so all zero reminder emails were vacuously sent successfully.
Either members are (hopefully) good at returning books or members don't take books out."""
else:
success_text = """
Automatically sent out {} individual reminder emails, one per overdue book.
The most overdue book was checked out approximately {}.""".format(
emails_sent,
time_since_earliest_checkout
)
step4 = SuccessForm(w, hb, success_text, width=mx - 20)
(r, c) = w.getmaxyx()
w.mvwin(cy - r // 2, cx - c // 2)
step4.event_loop()
step4.clear()
return ""

@ -86,6 +86,7 @@ send -- "5"
send -- "\r"
send -- "\r"
send -- "\r"
send -- "\r"
send -- "q"
expect eof

Loading…
Cancel
Save