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)
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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…
Reference in New Issue