Compare commits
1 Commits
Author | SHA1 | Date |
---|---|---|
Felix Bauckholt | b4bfc05823 |
|
@ -1,3 +1,2 @@
|
|||
*.pyc
|
||||
/build-library/
|
||||
/test_emails/
|
||||
|
|
5
TODO
5
TODO
|
@ -1,12 +1,14 @@
|
|||
_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_
|
||||
|
@ -31,7 +33,6 @@ 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)
|
||||
|
|
|
@ -1,50 +1,4 @@
|
|||
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
|
||||
library (1.0-2jessie) jessie; urgency=medium
|
||||
|
||||
* Resolved crashes
|
||||
* Prepared for release
|
||||
|
|
|
@ -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 --install-layout=deb --root=debian/library
|
||||
python3 setup.py -q build --build-base=build-library install --no-compile -O0 --prefix=/usr --root=debian/library
|
||||
|
||||
binary-arch: build install
|
||||
dh_testdir
|
||||
|
|
58
librarian
58
librarian
|
@ -6,13 +6,10 @@ 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
|
||||
|
@ -23,7 +20,6 @@ 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
|
||||
|
@ -38,16 +34,11 @@ 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.
|
||||
Email the librarian (librarian@csclub.uwaterloo.ca)
|
||||
with python's output after this program quits.
|
||||
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.
|
||||
The program will now quit."""
|
||||
form.error_form(text, stdscr, hb)
|
||||
raise
|
||||
|
@ -108,10 +99,7 @@ def addForm():
|
|||
(my,mx)=stdscr.getmaxyx()
|
||||
bf = form.BookForm(w,hb,width=mx-20)
|
||||
(r,c)=w.getmaxyx()
|
||||
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
|
||||
w.mvwin((my-r)//2,(mx-c)//2)
|
||||
bf.lookup_isbn=book_data.openLibrary_isbn
|
||||
bf.lookup_lccn=book_data.openLibrary_lccn
|
||||
bf.caption='Add a Book'
|
||||
|
@ -126,10 +114,7 @@ def browseMenu():
|
|||
b = browser.bookBrowser(w,hb)
|
||||
(r,c) = w.getmaxyx()
|
||||
(my,mx)=stdscr.getmaxyx()
|
||||
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
|
||||
w.mvwin((my-r)//2 -2, (mx-c)//2)
|
||||
b.refreshBooks()
|
||||
b.eventLoop()
|
||||
b.clear()
|
||||
|
@ -139,10 +124,7 @@ def trashMenu():
|
|||
b = browser.trashBrowser(w,hb)
|
||||
(r,c) = w.getmaxyx()
|
||||
(my,mx)=stdscr.getmaxyx()
|
||||
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
|
||||
w.mvwin((my-r)//2 -2, (mx-c)//2)
|
||||
b.refreshBooks()
|
||||
b.eventLoop()
|
||||
b.clear()
|
||||
|
@ -152,10 +134,7 @@ def uncategorizedMenu():
|
|||
b = browser.bookBrowser(w,hb)
|
||||
(r,c) = w.getmaxyx()
|
||||
(my,mx)=stdscr.getmaxyx()
|
||||
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
|
||||
w.mvwin((my-r)//2 -2, (mx-c)//2)
|
||||
b.refreshBooksUncategorized()
|
||||
b.eventLoop()
|
||||
b.clear()
|
||||
|
@ -165,10 +144,7 @@ def checkedout_menu():
|
|||
b = browser.bookBrowser(w,hb)
|
||||
(r,c) = w.getmaxyx()
|
||||
(my,mx)=stdscr.getmaxyx()
|
||||
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
|
||||
w.mvwin((my-r)//2 -2, (mx-c)//2)
|
||||
b.refreshBooksCheckedout()
|
||||
b.columnDefs = [("id",0,3),
|
||||
("uwid",0,8),
|
||||
|
@ -183,10 +159,7 @@ def onshelf_menu():
|
|||
b = browser.bookBrowser(w,hb)
|
||||
(r,c) = w.getmaxyx()
|
||||
(my,mx)=stdscr.getmaxyx()
|
||||
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
|
||||
w.mvwin((my-r)//2 -2, (mx-c)//2)
|
||||
b.refreshBooksOnshelf()
|
||||
b.eventLoop()
|
||||
b.clear()
|
||||
|
@ -206,20 +179,12 @@ def catMenu():
|
|||
w=curses.newwin(3,5)
|
||||
cat = browser.categoryBrowser(w,hb)
|
||||
(r,c) = w.getmaxyx()
|
||||
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
|
||||
w.mvwin((my-r)//2 -2, (mx-c)//2)
|
||||
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()
|
||||
|
@ -231,7 +196,6 @@ 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),
|
||||
|
|
|
@ -268,22 +268,6 @@ 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)
|
||||
|
@ -295,17 +279,6 @@ 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()
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
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()
|
||||
|
|
@ -336,7 +336,7 @@ class bookBrowser(browserWindow):
|
|||
('Authors',30,None),
|
||||
('Title',60,None)]
|
||||
|
||||
cs = [(' u', 'update'), (' d', 'delete selected'), (' c', 'categorize'), (' a', 'add selected to category'), (' r', 'refresh')]
|
||||
cs = [(' u', 'update'), (' d', 'delete selected'), (' c', 'categorize')]
|
||||
|
||||
|
||||
# redefinable functions
|
||||
|
@ -380,28 +380,12 @@ 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())
|
||||
|
@ -431,12 +415,6 @@ 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]:
|
||||
|
@ -449,30 +427,6 @@ 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')]
|
||||
|
@ -492,7 +446,7 @@ class categoryBrowser(browserWindow):
|
|||
|
||||
def viewCategory(self):
|
||||
w = curses.newwin(3,5)
|
||||
b = bookBrowserInCategory(w,self.hb)
|
||||
b = bookBrowser(w,self.hb)
|
||||
self.centreChild(w)
|
||||
b.refreshBooksInCategory(self.highlightedEntry())
|
||||
b.eventLoop()
|
||||
|
@ -591,27 +545,6 @@ 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)]
|
||||
|
|
|
@ -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,13 +107,6 @@ 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:
|
||||
|
@ -283,6 +276,9 @@ class FormWindow:
|
|||
if self.bt==-1:
|
||||
self.entries[self.hl].handle_input(ch)
|
||||
|
||||
|
||||
|
||||
|
||||
class BookForm(FormWindow):
|
||||
caption = "Add a Book"
|
||||
blabel = "Add"
|
||||
|
@ -309,13 +305,13 @@ class BookForm(FormWindow):
|
|||
if book != {}:
|
||||
self._set_entries(book)
|
||||
self.refresh()
|
||||
self._mv_focus(+12)
|
||||
self._mv_focus(+7)
|
||||
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(+11)
|
||||
self._mv_focus(+6)
|
||||
else:
|
||||
FormWindow.handle_input(self,ch)
|
||||
|
||||
|
@ -345,13 +341,12 @@ class CategoryForm(FormWindow):
|
|||
def _return_values(self):
|
||||
return self.entries[0].value
|
||||
|
||||
class InfoForm(FormWindow):
|
||||
caption = "Info"
|
||||
class ErrorForm(FormWindow):
|
||||
caption = "Error"
|
||||
blabel = "OK"
|
||||
buttononly = True
|
||||
def __init__(self,caption,window,helpbar,errortext,width=50):
|
||||
def __init__(self,window,helpbar,errortext,width=50):
|
||||
self.labels = errortext.split("\n")
|
||||
self.caption = caption
|
||||
super().__init__(window, helpbar, width=width)
|
||||
|
||||
def redraw(self):
|
||||
|
@ -368,16 +363,6 @@ class InfoForm(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)
|
||||
|
|
|
@ -1,190 +0,0 @@
|
|||
#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 ""
|
|
@ -1,93 +0,0 @@
|
|||
#!/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"
|
Loading…
Reference in New Issue