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
This commit is contained in:
parent
bff0abf3aa
commit
c812a6634e
1
TODO
1
TODO
|
@ -36,7 +36,6 @@ Support UTF-8 for everything
|
||||||
Search ignores Case (for lowercase search strings)
|
Search ignores Case (for lowercase search strings)
|
||||||
Text entry supports longer string
|
Text entry supports longer string
|
||||||
Home and End navigate to top and bottom of catalogue respectively.
|
Home and End navigate to top and bottom of catalogue respectively.
|
||||||
Email reminders for signed-out books
|
|
||||||
|
|
||||||
Support for multiple copies
|
Support for multiple copies
|
||||||
- books will have their book_id written in pencil on inside cover
|
- books will have their book_id written in pencil on inside cover
|
||||||
|
|
|
@ -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
|
library (1.0-3stretch0) stretch; urgency=medium
|
||||||
|
|
||||||
* Package for stretch
|
* 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")
|
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 = \
|
email_message_body = \
|
||||||
"""Hi {},
|
"""Hi {},
|
||||||
|
|
||||||
Our records indicate that you have had the book {} signed out for {} days.
|
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
|
Computer Science Club | University of Waterloo
|
||||||
librarian@csclub.uwaterloo.ca""".format(
|
librarian@csclub.uwaterloo.ca""".format(
|
||||||
quest_id,
|
quest_id,
|
||||||
|
@ -28,7 +35,13 @@ librarian@csclub.uwaterloo.ca""".format(
|
||||||
)
|
)
|
||||||
|
|
||||||
email_message_subject = "Overdue book: {}".format(book_name)
|
email_message_subject = "Overdue book: {}".format(book_name)
|
||||||
email_message = "Subject: {}\n\n{}".format(email_message_subject,
|
email_message = (
|
||||||
email_message_body)
|
"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
|
return email_message
|
||||||
|
|
||||||
|
|
|
@ -358,12 +358,13 @@ class CategoryForm(FormWindow):
|
||||||
def _return_values(self):
|
def _return_values(self):
|
||||||
return self.entries[0].value
|
return self.entries[0].value
|
||||||
|
|
||||||
class ErrorForm(FormWindow):
|
class InfoForm(FormWindow):
|
||||||
caption = "Error"
|
caption = "Info"
|
||||||
blabel = "OK"
|
blabel = "OK"
|
||||||
buttononly = True
|
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.labels = errortext.split("\n")
|
||||||
|
self.caption = caption
|
||||||
super().__init__(window, helpbar, width=width)
|
super().__init__(window, helpbar, width=width)
|
||||||
|
|
||||||
def redraw(self):
|
def redraw(self):
|
||||||
|
@ -380,6 +381,16 @@ class ErrorForm(FormWindow):
|
||||||
|
|
||||||
def _mv_focus(self,delta): pass
|
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):
|
def error_form(text, w, hb):
|
||||||
width = max([len(l) for l in text.split("\n")]) + 4
|
width = max([len(l) for l in text.split("\n")]) + 4
|
||||||
child=curses.newwin(1,1)
|
child=curses.newwin(1,1)
|
||||||
|
|
|
@ -21,7 +21,7 @@ import subprocess
|
||||||
from library import permissions
|
from library import permissions
|
||||||
from library.exceptions import *
|
from library.exceptions import *
|
||||||
import library.database as db
|
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
|
from library.emails import format_reminder_email
|
||||||
|
|
||||||
# email testing folder creation
|
# email testing folder creation
|
||||||
|
@ -35,10 +35,17 @@ MAX_LOGIN_ATTEMPTS = 3
|
||||||
class DaysForm(FormWindow):
|
class DaysForm(FormWindow):
|
||||||
caption = "Enter the max number of days a book can be signed out for"
|
caption = "Enter the max number of days a book can be signed out for"
|
||||||
blabel = "Enter"
|
blabel = "Enter"
|
||||||
labels = ["Days"]
|
labels = ["Days (default " + str(DEFAULT_DAY_VALUE) + ")"]
|
||||||
|
|
||||||
def _return_values(self):
|
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):
|
class NameForm(FormWindow):
|
||||||
|
@ -50,6 +57,7 @@ class NameForm(FormWindow):
|
||||||
if self.entries[0].value is "":
|
if self.entries[0].value is "":
|
||||||
return "Librarian"
|
return "Librarian"
|
||||||
else:
|
else:
|
||||||
|
assert self.entries[0].value.isprintable()
|
||||||
return self.entries[0].value
|
return self.entries[0].value
|
||||||
|
|
||||||
|
|
||||||
|
@ -58,13 +66,15 @@ def _send_email(quest_id: str,
|
||||||
signed_out_date: str,
|
signed_out_date: str,
|
||||||
max_days_can_be_signed_out: int,
|
max_days_can_be_signed_out: int,
|
||||||
librarian_name: str,
|
librarian_name: str,
|
||||||
book_name_list,
|
book_name: str,
|
||||||
testing=False) -> None:
|
testing=False) -> bool:
|
||||||
"""Sends an email to quest_id@csclub.uwaterloo.ca if the date the book
|
"""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.
|
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
|
Set testing to true to output emails to the current directory, instead
|
||||||
of actually sending them.
|
of actually sending them.
|
||||||
|
|
||||||
|
Returns whether an email was sent.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Determine the days the book has been signed out
|
# 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
|
days_signed_out = (d.today() - d).days
|
||||||
|
|
||||||
if days_signed_out > max_days_can_be_signed_out:
|
if days_signed_out > max_days_can_be_signed_out:
|
||||||
email_address = quest_id + "@csclub.uwaterloo.ca"
|
#Librarian also gets a copy, so that unreplied emails are more apparent.
|
||||||
book_name = "".join(book_name_list)
|
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)
|
email_message = format_reminder_email(quest_id, days_signed_out, librarian_name, book_name)
|
||||||
if testing:
|
if testing:
|
||||||
|
assert book_name.isprintable()
|
||||||
os.makedirs("test_emails", exist_ok=True)
|
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)
|
print(email_message, file=f)
|
||||||
else:
|
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
|
#Public functions
|
||||||
|
@ -100,17 +117,17 @@ def sendemails_procedure(w, hb, cy, cx, mx):
|
||||||
w.mvwin(cy - r // 2, cx - c // 2)
|
w.mvwin(cy - r // 2, cx - c // 2)
|
||||||
days = step1.event_loop()
|
days = step1.event_loop()
|
||||||
step1.clear()
|
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
|
#Get the name of the librarian
|
||||||
if not isinstance(days, int) or days < 0:
|
|
||||||
days = DEFAULT_DAY_VALUE
|
|
||||||
|
|
||||||
#Get the name of the librarain
|
|
||||||
step2 = NameForm(w, hb, width=mx - 20)
|
step2 = NameForm(w, hb, width=mx - 20)
|
||||||
(r, c) = w.getmaxyx()
|
(r, c) = w.getmaxyx()
|
||||||
w.mvwin(cy - r // 2, cx - c // 2)
|
w.mvwin(cy - r // 2, cx - c // 2)
|
||||||
librarianName = step2.event_loop()
|
librarianName = step2.event_loop()
|
||||||
step2.clear()
|
step2.clear()
|
||||||
|
if librarianName == {}:
|
||||||
|
return "" #User cancelled the dialog box
|
||||||
|
|
||||||
#Set up email
|
#Set up email
|
||||||
global server
|
global server
|
||||||
|
@ -149,18 +166,53 @@ def sendemails_procedure(w, hb, cy, cx, mx):
|
||||||
|
|
||||||
#Get the books that are signed out
|
#Get the books that are signed out
|
||||||
signed_out_books = db.get_checkedout_books()
|
signed_out_books = db.get_checkedout_books()
|
||||||
|
earliest_checkout = datetime.now()
|
||||||
|
emails_sent = 0
|
||||||
|
|
||||||
for data in signed_out_books:
|
for data in signed_out_books:
|
||||||
quest_id = data["uwid"]
|
quest_id = data["uwid"]
|
||||||
date = str(data["date"]).split(" ")[0]
|
date = str(data["date"]).split(" ")[0]
|
||||||
book_name_with_spaces = data["title"]
|
book_name_with_spaces = data["title"]
|
||||||
|
|
||||||
_send_email(
|
if _send_email(
|
||||||
str(quest_id),
|
str(quest_id),
|
||||||
str(date), days, str(librarianName), book_name_with_spaces,
|
str(date), days, str(librarianName), book_name_with_spaces,
|
||||||
testing)
|
testing):
|
||||||
|
emails_sent += 1
|
||||||
|
earliest_checkout = min(earliest_checkout,
|
||||||
|
datetime.strptime(date, "%Y-%m-%d"))
|
||||||
|
|
||||||
#Exit from the email server
|
#Exit from the email server
|
||||||
server.quit()
|
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 ""
|
return ""
|
||||||
|
|
|
@ -86,6 +86,7 @@ send -- "5"
|
||||||
send -- "\r"
|
send -- "\r"
|
||||||
send -- "\r"
|
send -- "\r"
|
||||||
send -- "\r"
|
send -- "\r"
|
||||||
|
send -- "\r"
|
||||||
send -- "q"
|
send -- "q"
|
||||||
expect eof
|
expect eof
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue