Compare commits

...

12 Commits

Author SHA1 Message Date
Charlie Wang 358decd75d Package for bionic 2019-04-26 17:06:11 -04:00
Zachary Seguin 292f6fd8af Package for stretch, buster and xenial 2018-04-15 16:20:52 -04:00
Patrick Melanson d18ff562fc Tweaked exception handling, specifically small screen failures
Changes the dialog window that pops up for any exception to tell
the user to read Python's output (i.e. the stacktrace).
Also, handles the common failure of having too small a window and
trying to create a new ncurses dialog box. There is now a specific
failure message, so people know to make their terminal bigger.

Useful!
2017-12-27 11:48:46 -05:00
Patrick Melanson 4ce53141e0 Use currently-logged-in user to authenticate for mass email sending.
Basically, this commit removes email sending code that asked the
user for a CSC username and email to authenticate to an SMTP server
to send a mass email. Now, we just use the system utility 'sendmail'
to do it, which means you don't have to enter in your email creds
to send a mass email once you're already logged in.

Magic!
2017-09-23 01:24:23 -04:00
Patrick Melanson c812a6634e 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
2017-07-29 16:46:41 -04:00
Charlie Wang bff0abf3aa Refactor email reminder code
- Pull out email generation into a new module so that the email
message itself can be tested.
- Add a "backdoor" for the email login screen that writes emails to
text files under test_emails/ instead of sending them.
- Add a GNU expect script to automate email reminder testing using
the above features.
2017-02-04 12:21:05 -05:00
Connor Murphy 6ed3c35554 Added ability for libcom to send emails to those with overdue books 2017-02-04 00:24:15 -05:00
Charlie Wang 8e592f3adc use default colour values for better integration
librarian now looks slightly better when launched from ceo
because it won't set a different background colour
2017-02-03 23:39:35 -05:00
Zachary Seguin 7717dc3636 Build for stretch 2017-01-20 23:08:59 -05:00
Felix Bauckholt 1abaec4b0a Slight changes to make adding books more pleasant 2016-03-19 19:06:35 -04:00
Felix Bauckholt 3487b66393 Categorizing and uncategorizing based on selection
Now, in any book browser window, you can add every
selected book to one category.

When you are browsing the books in a category, you
can remove every selected book from that category.
2016-03-12 21:02:43 -05:00
Zachary Seguin f299412d56 Fix issue where python packages are installed in the incorrect location 2016-02-20 16:12:06 -05:00
11 changed files with 551 additions and 31 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
*.pyc
/build-library/
/test_emails/

5
TODO
View File

@ -1,14 +1,12 @@
_List of Desired Features_
Categories work based on selection, not just highlight
- i.e. assign categories to multiple books at once
- this may involve extra logic if books don't have the same categories beforehand
Regex Search
Choose shown columns in browser
Support for multiple copies
- (better support, that is)
Search function in db_layer
- eventually something which takes things like "title:foo author:bar some other keywords"
Don't let patrons with overdue books checkout more books
_Code Quality Improvements_
@ -33,6 +31,7 @@ Error checking out an already checked out book
_Implemented Features_
Categories work based on selection, not just highlight
Sort by column in browser
Support UTF-8 for everything
Search ignores Case (for lowercase search strings)

46
debian/changelog vendored
View File

@ -1,3 +1,49 @@
library (1.1-0~bionic0) bionic; urgency=medium
* Package for bionic
-- Charlie Wang <s455wang@csclub.uwaterloo.ca> Sun, 17 Feb 2019 21:45:27 -0500
library (1.1-0~xenial0) xenial; urgency=medium
* Package for xenial
-- Zachary Seguin <ztseguin@csclub.uwaterloo.ca> Sun, 15 Apr 2018 16:16:07 -0400
library (1.1-0~buster0) buster; urgency=medium
* Package for buster
-- Zachary Seguin <ztseguin@csclub.uwaterloo.ca> Sun, 15 Apr 2018 16:14:05 -0400
library (1.1-0~stretch0) stretch; urgency=medium
[ Patrick James Melanson ]
* New menu option to send out mass email for overdue books
* Community effort by Connor Murphy, Charlie Wang, Patrick Melanson
-- Zachary Seguin <ztseguin@csclub.uwaterloo.ca> Sun, 15 Apr 2018 16:12:58 -0400
library (1.0-3stretch0) stretch; urgency=medium
* Package for stretch
-- Zachary Seguin <ztseguin@csclub.uwaterloo.ca> Wed, 11 Jan 2017 16:17:51 -0500
library (1.0-3jessie0) jessie; urgency=medium
* Modify build parameters so that library is installed in dist-packages
instead of site-packages
-- Zachary Seguin <ztseguin@csclub.uwaterloo.ca> Sat, 20 Feb 2016 15:49:14 -0500
library (1.0-2trusty0) trusty; urgency=medium
* Modify build parameters so that library is installed in dist-packages
instead of site-packages
-- Zachary Seguin <ztseguin@csclub.uwaterloo.ca> Sat, 20 Feb 2016 15:49:14 -0500
library (1.0-2) trusty; urgency=medium
* Resolved crashes

4
debian/rules vendored
View File

@ -1,6 +1,6 @@
#!/usr/bin/make -f
build:
build:
clean:
dh_testdir
@ -13,7 +13,7 @@ install: build
dh_testdir
dh_testroot
dh_installdirs
python3 setup.py -q build --build-base=build-library install --no-compile -O0 --prefix=/usr --root=debian/library
python3 setup.py -q build --build-base=build-library install --no-compile -O0 --install-layout=deb --root=debian/library
binary-arch: build install
dh_testdir

View File

@ -6,10 +6,13 @@ import library.interface.browser as browser
import library.interface.form as form
import library.interface.help_bar as helpBar
import library.interface.checkout as co
import library.interface.sendemails as sendemails
from library import book_data
class SmallScreenException(Exception):
def __init__(self, *args, **kwargs):
Exception.__init__(self, *args, **kwargs)
stdscr=0
hb=0
@ -20,6 +23,7 @@ def menutest(s, l):
global stdscr
global hb
stdscr=s
curses.use_default_colors() # colours will use terminal defaults
curses.curs_set(0)
(rows,cols)=stdscr.getmaxyx()
# set the default for the browser windows
@ -34,11 +38,16 @@ def menutest(s, l):
try:
menu(w, l)
except SystemExit: pass
except SmallScreenException:
text = """That's a small screen!
This librarian program won't work with a small screen.
Make your terminal window bigger and try again."""
form.error_form(text, stdscr, hb)
raise
except:
text = """An unexpected error occured.
You can contact the librarian (librarian@csclub.uwaterloo.ca),
but given the history of the library system, it seems unlikely
that somebody will be around to care.
Email the librarian (librarian@csclub.uwaterloo.ca)
with python's output after this program quits.
The program will now quit."""
form.error_form(text, stdscr, hb)
raise
@ -99,7 +108,10 @@ def addForm():
(my,mx)=stdscr.getmaxyx()
bf = form.BookForm(w,hb,width=mx-20)
(r,c)=w.getmaxyx()
w.mvwin((my-r)//2,(mx-c)//2)
try:
w.mvwin((my-r)//2,(mx-c)//2)
except curses.error as exc:
raise SmallScreenException("Terminal screen too small. Try again with a bigger terminal.") from exc
bf.lookup_isbn=book_data.openLibrary_isbn
bf.lookup_lccn=book_data.openLibrary_lccn
bf.caption='Add a Book'
@ -114,7 +126,10 @@ def browseMenu():
b = browser.bookBrowser(w,hb)
(r,c) = w.getmaxyx()
(my,mx)=stdscr.getmaxyx()
w.mvwin((my-r)//2 -2, (mx-c)//2)
try:
w.mvwin((my-r)//2 -2, (mx-c)//2)
except curses.error as exc:
raise SmallScreenException("Terminal screen too small. Try again with a bigger terminal.") from exc
b.refreshBooks()
b.eventLoop()
b.clear()
@ -124,7 +139,10 @@ def trashMenu():
b = browser.trashBrowser(w,hb)
(r,c) = w.getmaxyx()
(my,mx)=stdscr.getmaxyx()
w.mvwin((my-r)//2 -2, (mx-c)//2)
try:
w.mvwin((my-r)//2 -2, (mx-c)//2)
except curses.error as exc:
raise SmallScreenException("Terminal screen too small. Try again with a bigger terminal.") from exc
b.refreshBooks()
b.eventLoop()
b.clear()
@ -134,7 +152,10 @@ def uncategorizedMenu():
b = browser.bookBrowser(w,hb)
(r,c) = w.getmaxyx()
(my,mx)=stdscr.getmaxyx()
w.mvwin((my-r)//2 -2, (mx-c)//2)
try:
w.mvwin((my-r)//2 -2, (mx-c)//2)
except curses.error as exc:
raise SmallScreenException("Terminal screen too small. Try again with a bigger terminal.") from exc
b.refreshBooksUncategorized()
b.eventLoop()
b.clear()
@ -144,7 +165,10 @@ def checkedout_menu():
b = browser.bookBrowser(w,hb)
(r,c) = w.getmaxyx()
(my,mx)=stdscr.getmaxyx()
w.mvwin((my-r)//2 -2, (mx-c)//2)
try:
w.mvwin((my-r)//2 -2, (mx-c)//2)
except curses.error as exc:
raise SmallScreenException("Terminal screen too small. Try again with a bigger terminal.") from exc
b.refreshBooksCheckedout()
b.columnDefs = [("id",0,3),
("uwid",0,8),
@ -159,7 +183,10 @@ def onshelf_menu():
b = browser.bookBrowser(w,hb)
(r,c) = w.getmaxyx()
(my,mx)=stdscr.getmaxyx()
w.mvwin((my-r)//2 -2, (mx-c)//2)
try:
w.mvwin((my-r)//2 -2, (mx-c)//2)
except curses.error as exc:
raise SmallScreenException("Terminal screen too small. Try again with a bigger terminal.") from exc
b.refreshBooksOnshelf()
b.eventLoop()
b.clear()
@ -179,12 +206,20 @@ def catMenu():
w=curses.newwin(3,5)
cat = browser.categoryBrowser(w,hb)
(r,c) = w.getmaxyx()
w.mvwin((my-r)//2 -2, (mx-c)//2)
try:
w.mvwin((my-r)//2 -2, (mx-c)//2)
except curses.error as exc:
raise SmallScreenException("Terminal screen too small. Try again with a bigger terminal.") from exc
cat.refreshCategories()
cat.sortByColumn('category')
cat.eventLoop()
cat.clear()
def email_menu():
w=curses.newwin(1,1)
(my,mx)=stdscr.getmaxyx()
sendemails.sendemails_procedure(w,hb,my//2,mx//2,mx)
if __name__ == "__main__":
db.initializeDatabase()
@ -196,6 +231,7 @@ if __name__ == "__main__":
("",exit),
("Check Out a Book", co_menu),
("Return a Book", return_menu),
("Send Overdue Email Reminders", email_menu),
("",exit),
("View Checked Out Books", checkedout_menu),
("View On Shelf Books", onshelf_menu),

View File

@ -268,6 +268,22 @@ def categorizeBook(book, cats):
conn.commit()
c.close()
@permissions.check_permissions(permissions.PERMISSION_LIBCOM)
def categorizeBooks(cat, books):
conn = sqlite3.connect(_catalogue_db_file)
c = conn.cursor()
query = ("INSERT OR IGNORE INTO "+_book_category_table+
" (id,cat_id) VALUES (?, ?);")
exists_query = "SELECT * FROM "+_book_category_table+" WHERE (id = ? AND cat_id = ?);"
for book in books:
args = (book['id'],cat['id'])
c.execute(exists_query, args)
if len(c.fetchall()) == 0:
c.execute(query,args)
conn.commit()
c.close()
@permissions.check_permissions(permissions.PERMISSION_LIBCOM)
def uncategorizeBook(book, cats):
conn = sqlite3.connect(_catalogue_db_file)
@ -279,6 +295,17 @@ def uncategorizeBook(book, cats):
conn.commit()
c.close()
@permissions.check_permissions(permissions.PERMISSION_LIBCOM)
def uncategorizeBooks(books, cat):
conn = sqlite3.connect(_catalogue_db_file)
c = conn.cursor()
query = "DELETE FROM "+_book_category_table+" WHERE (id = ? AND cat_id = ?);"
for book in books:
args = (book['id'],cat['id'])
c.execute(query,args)
conn.commit()
c.close()
def getCategories():
conn = sqlite3.connect(_catalogue_db_file)
c = conn.cursor()

46
library/emails.py Normal file
View File

@ -0,0 +1,46 @@
from email.mime.text import MIMEText
def format_reminder_email(quest_id: str,
days_signed_out: int,
librarian_name: str,
book_name) -> str:
"""
Formats an email as a plain string for sending out email reminders
for signed out books.
Example: format_reminder_email("s455wang", 30, "Connor Murphy", "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 = MIMEText(
"""Hi {},
Our records indicate that you have had the book {} signed out for {} days.
If you would like to keep this book checked out, tell us when in the next month you will return this book.
If you think you have received this message in error, reply back to this email please!
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,
book_name,
days_signed_out,
librarian_name
))
email_message["Subject"] = "Overdue book: {}".format(book_name)
email_message["To"] = "\"{0}@csclub.uwaterloo.ca\" <{0}@csclub.uwaterloo.ca>\n".format(quest_id)
assert email_message.as_string().replace("\n", "").isprintable(), \
"Our email should not have characters apart from normal characters and newline"
return email_message.as_string()

View File

@ -336,7 +336,7 @@ class bookBrowser(browserWindow):
('Authors',30,None),
('Title',60,None)]
cs = [(' u', 'update'), (' d', 'delete selected'), (' c', 'categorize')]
cs = [(' u', 'update'), (' d', 'delete selected'), (' c', 'categorize'), (' a', 'add selected to category'), (' r', 'refresh')]
# redefinable functions
@ -380,12 +380,28 @@ class bookBrowser(browserWindow):
books.append(book)
db.removeBooks(books)
@catch_error
def addBooksToCategory(self):
books = []
for sel,book in zip(self.selected, self.entries):
if sel:
books.append(book)
w = curses.newwin(1,1)
cs = singleCategorySelector(w,self.hb,40,50)
self.centreChild(w)
cs.refreshCategories()
cat = cs.eventLoop()
cs.clear()
self.refreshBooks()
if cat:
db.categorizeBooks(cat, books)
def refreshBooks(self):
self.load_data(db.get_books())
def refreshBooksInCategory(self,cat):
self.refreshBooks = lambda : self.load_data(db.getBooksByCategory(cat))
self.refreshBooks()
#def refreshBooksInCategory(self,cat):
# self.refreshBooks = lambda : self.load_data(db.getBooksByCategory(cat))
# self.refreshBooks()
def refreshBooksUncategorized(self):
self.refreshBooks = lambda : self.load_data(db.getUncategorizedBooks())
@ -415,6 +431,12 @@ class bookBrowser(browserWindow):
book = self.highlightedEntry()
self.categorizeSelection(book)
self.refresh()
elif ch == 97: #a
self.addBooksToCategory()
self.refresh()
elif ch == 114: #r
self.refreshBooks()
self.refresh()
if ch == 100:
count=0
for s in self.selected[0:self.hl-1]:
@ -427,6 +449,30 @@ class bookBrowser(browserWindow):
self.mvHighlight(-count)
return ch
#a pretty ugly hack in order to be able to remove books from this category
class bookBrowserInCategory(bookBrowser):
cs = [(' u', 'update'), (' d', 'delete selected'), (' c', 'categorize'), (' a', 'add selected to category'), (' r', 'uncategorize selected')]
def refreshBooksInCategory(self,cat):
self.cat = cat
self.refreshBooks = lambda : self.load_data(db.getBooksByCategory(cat))
self.refreshBooks()
@catch_error
def uncategorizeBooks(self):
books = []
for sel,book in zip(self.selected, self.entries):
if sel:
books.append(book)
db.uncategorizeBooks(books, self.cat)
def handleInput(self,ch):
if ch == 114: #r
self.uncategorizeBooks()
self.refreshBooks()
self.refresh()
bookBrowser.handleInput(self,ch)
class categoryBrowser(browserWindow):
columnDefs = [('Category',100,None)]
cs = [(' a', 'add category'), (' d', 'delete selected')]
@ -446,7 +492,7 @@ class categoryBrowser(browserWindow):
def viewCategory(self):
w = curses.newwin(3,5)
b = bookBrowser(w,self.hb)
b = bookBrowserInCategory(w,self.hb)
self.centreChild(w)
b.refreshBooksInCategory(self.highlightedEntry())
b.eventLoop()
@ -545,6 +591,27 @@ class categorySelector(browserWindow):
return 113
class singleCategorySelector(categorySelector):
columnDefs = [('Category',100,None)]
cs = []
commands = [(' /', 'search'), (' n', 'find next'), (' N', 'find previous'),
(' q', 'quit'), ('Enter', 'done')]
def handleInput(self,ch):
return browserWindow.handleInput(self,ch,True)
def eventLoop(self):
self.w.keypad(1)
self.refresh()
ch = self.w.getch()
while ch != 27 and ch != 113:
if ch == 10:
return self.highlightedEntry()
self.handleInput(ch)
self.w.refresh()
ch = self.w.getch()
self.hb.refresh()
class columnSelector(browserWindow):
columnDefs = [('Column',100,None)]

View File

@ -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
@ -107,6 +107,13 @@ class TextEntry:
self.value = self.value[:c] + self.value[c+1:]
self._mv_cursor(0)
class PasswordEntry(TextEntry):
def redraw(self):
self.w.addnstr(self.y,self.x, " "*self.width, self.width)
if self.focus:
self.w.chgat(self.y, self.x, self.width, curses.A_UNDERLINE)
curses.curs_set(1)
class FormWindow:
@ -276,9 +283,6 @@ class FormWindow:
if self.bt==-1:
self.entries[self.hl].handle_input(ch)
class BookForm(FormWindow):
caption = "Add a Book"
blabel = "Add"
@ -305,13 +309,13 @@ class BookForm(FormWindow):
if book != {}:
self._set_entries(book)
self.refresh()
self._mv_focus(+7)
self._mv_focus(+12)
if self.hl==1: # lookup by lccn
book = self.lookup_lccn(self.entries[1].value)
if book != {}:
self._set_entries(book)
self.refresh()
self._mv_focus(+6)
self._mv_focus(+11)
else:
FormWindow.handle_input(self,ch)
@ -341,12 +345,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):
@ -363,6 +368,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)

View File

@ -0,0 +1,190 @@
#Import smtp for the email sending function
import smtplib
#Import argparse to parse command line args
import argparse
#Import sys so that we can exit when given bad command line args
import sys
#Import datetime and time to check dates
from datetime import datetime
import time
#Import getpass for password input
import getpass
#Import subprocess for validation of if a user is in a group, and running
#a sendmail process
import subprocess
#Import librarian permissions
from library import permissions
from library.exceptions import *
import library.database as db
from library.interface.form import FormWindow, BookForm, SuccessForm, catch_error_with, error_form
from library.emails import format_reminder_email
# email testing folder creation
import os
#Constants
DEFAULT_DAY_VALUE = 21
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 (default " + str(DEFAULT_DAY_VALUE) + ")"]
def _return_values(self):
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):
caption = "Enter the name you want in the signature line for the email"
blabel = "Enter"
labels = ["Name"]
def _return_values(self):
if self.entries[0].value is "":
return "Librarian"
else:
assert self.entries[0].value.isprintable()
return self.entries[0].value
#Private functions
def _send_email(quest_id: str,
signed_out_date: str,
max_days_can_be_signed_out: int,
librarian_name: str,
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
d = datetime.strptime(signed_out_date, "%Y-%m-%d")
days_signed_out = (d.today() - d).days
if days_signed_out > max_days_can_be_signed_out:
#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)
#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:
#Equivalent to:
#echo <email_message> | sendmail questid@csclub.uwaterloo.ca
sendmail_process = subprocess.Popen(["sendmail"] + email_addresses,
stdin=subprocess.PIPE)
sendmail_process.communicate(input=email_message.encode("utf-8"))
return True
return False
#Public functions
@permissions.check_permissions(permissions.PERMISSION_LIBCOM)
@catch_error_with(lambda w, hb, *args: (w, hb, None))
def sendemails_procedure(w, hb, cy, cx, mx):
"""Procedure to send emails to those with overdue books
w: ncurses window for the routine
cy,cx: centre coordinates of the screen
mx: max width of screen
"""
#Get the max days a book can be signed out for
step1 = DaysForm(w, hb, width=mx - 20)
(r, c) = w.getmaxyx()
w.mvwin(cy - r // 2, cx - c // 2)
days = step1.event_loop()
step1.clear()
if days == {}:
return "" #User cancelled the dialog box
#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
#Don't send out emails if this gets set to True
testing = False
if librarianName == "TESTING12345":
testing = True
#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"]
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"))
#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 ""

93
run_emails_test.exp Executable file
View File

@ -0,0 +1,93 @@
#!/usr/bin/expect -f
# Run this to test out email sending. Writes emails to text files
# under the "test_emails" directory.
# To do this manually, use the UI to send emails normally but put
# "TESTING12345" as the username. The password is thrown away.
#
# This Expect script was generated by autoexpect on Sat Feb 4 11:41:53 2017
# Expect and autoexpect were both written by Don Libes, NIST.
#
# Note that autoexpect does not guarantee a working script. It
# necessarily has to guess about certain things. Two reasons a script
# might fail are:
#
# 1) timing - A surprising number of programs (rn, ksh, zsh, telnet,
# etc.) and devices discard or ignore keystrokes that arrive "too
# quickly" after prompts. If you find your new script hanging up at
# one spot, try adding a short sleep just before the previous send.
# Setting "force_conservative" to 1 (see below) makes Expect do this
# automatically - pausing briefly before sending each character. This
# pacifies every program I know of. The -c flag makes the script do
# this in the first place. The -C flag allows you to define a
# character to toggle this mode off and on.
set force_conservative 0 ;# set to 1 to force conservative mode even if
;# script wasn't run conservatively originally
if {$force_conservative} {
set send_slow {1 .1}
proc send {ignore arg} {
sleep .1
exp_send -s -- $arg
}
}
#
# 2) differing output - Some programs produce different output each time
# they run. The "date" command is an obvious example. Another is
# ftp, if it produces throughput statistics at the end of a file
# transfer. If this causes a problem, delete these patterns or replace
# them with wildcards. An alternative is to use the -p flag (for
# "prompt") which makes Expect only look for the last line of output
# (i.e., the prompt). The -P flag allows you to define a character to
# toggle this mode off and on.
#
# Read the man page for more info.
#
# -Don
set timeout 5
spawn ./librarian
match_max 100
send -- "OB"
send -- "OB"
send -- "OB"
send -- "OB"
send -- "OB"
send -- "OB"
send -- "OB"
send -- "\r"
send -- "0"
send -- ""
send -- "1"
send -- "\r"
send -- "\r"
send -- "T"
send -- "E"
send -- "S"
send -- "T"
send -- "\r"
send -- "\r"
send -- "T"
send -- "E"
send -- "S"
send -- "T"
send -- "I"
send -- "N"
send -- "G"
send -- "1"
send -- "2"
send -- "3"
send -- "4"
send -- "5"
send -- "\r"
send -- "\r"
send -- "\r"
send -- "\r"
send -- "q"
expect eof
send_user -- "Check the test_emails directory\n"