Compare commits

...

4 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
6 changed files with 88 additions and 82 deletions

1
TODO
View File

@ -6,6 +6,7 @@ Support for multiple copies
- (better support, that is) - (better support, that is)
Search function in db_layer Search function in db_layer
- eventually something which takes things like "title:foo author:bar some other keywords" - 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_ _Code Quality Improvements_

23
debian/changelog vendored
View File

@ -1,9 +1,28 @@
library (1.0-4stretch0) UNRELEASED; urgency=medium 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 * New menu option to send out mass email for overdue books
* Community effort by Connor Murphy, Charlie Wang, Patrick Melanson * Community effort by Connor Murphy, Charlie Wang, Patrick Melanson
-- Patrick James Melanson <pj2melan@csclub.uwaterloo.ca> Sat, 29 Jul 2017 15:12:12 -0400 -- Zachary Seguin <ztseguin@csclub.uwaterloo.ca> Sun, 15 Apr 2018 16:12:58 -0400
library (1.0-3stretch0) stretch; urgency=medium library (1.0-3stretch0) stretch; urgency=medium

View File

@ -10,7 +10,9 @@ import library.interface.sendemails as sendemails
from library import book_data from library import book_data
class SmallScreenException(Exception):
def __init__(self, *args, **kwargs):
Exception.__init__(self, *args, **kwargs)
stdscr=0 stdscr=0
hb=0 hb=0
@ -36,11 +38,16 @@ def menutest(s, l):
try: try:
menu(w, l) menu(w, l)
except SystemExit: pass 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: except:
text = """An unexpected error occured. text = """An unexpected error occured.
You can contact the librarian (librarian@csclub.uwaterloo.ca), Email the librarian (librarian@csclub.uwaterloo.ca)
but given the history of the library system, it seems unlikely with python's output after this program quits.
that somebody will be around to care.
The program will now quit.""" The program will now quit."""
form.error_form(text, stdscr, hb) form.error_form(text, stdscr, hb)
raise raise
@ -101,7 +108,10 @@ def addForm():
(my,mx)=stdscr.getmaxyx() (my,mx)=stdscr.getmaxyx()
bf = form.BookForm(w,hb,width=mx-20) bf = form.BookForm(w,hb,width=mx-20)
(r,c)=w.getmaxyx() (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_isbn=book_data.openLibrary_isbn
bf.lookup_lccn=book_data.openLibrary_lccn bf.lookup_lccn=book_data.openLibrary_lccn
bf.caption='Add a Book' bf.caption='Add a Book'
@ -116,7 +126,10 @@ def browseMenu():
b = browser.bookBrowser(w,hb) b = browser.bookBrowser(w,hb)
(r,c) = w.getmaxyx() (r,c) = w.getmaxyx()
(my,mx)=stdscr.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.refreshBooks()
b.eventLoop() b.eventLoop()
b.clear() b.clear()
@ -126,7 +139,10 @@ def trashMenu():
b = browser.trashBrowser(w,hb) b = browser.trashBrowser(w,hb)
(r,c) = w.getmaxyx() (r,c) = w.getmaxyx()
(my,mx)=stdscr.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.refreshBooks()
b.eventLoop() b.eventLoop()
b.clear() b.clear()
@ -136,7 +152,10 @@ def uncategorizedMenu():
b = browser.bookBrowser(w,hb) b = browser.bookBrowser(w,hb)
(r,c) = w.getmaxyx() (r,c) = w.getmaxyx()
(my,mx)=stdscr.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.refreshBooksUncategorized()
b.eventLoop() b.eventLoop()
b.clear() b.clear()
@ -146,7 +165,10 @@ def checkedout_menu():
b = browser.bookBrowser(w,hb) b = browser.bookBrowser(w,hb)
(r,c) = w.getmaxyx() (r,c) = w.getmaxyx()
(my,mx)=stdscr.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.refreshBooksCheckedout()
b.columnDefs = [("id",0,3), b.columnDefs = [("id",0,3),
("uwid",0,8), ("uwid",0,8),
@ -161,7 +183,10 @@ def onshelf_menu():
b = browser.bookBrowser(w,hb) b = browser.bookBrowser(w,hb)
(r,c) = w.getmaxyx() (r,c) = w.getmaxyx()
(my,mx)=stdscr.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.refreshBooksOnshelf()
b.eventLoop() b.eventLoop()
b.clear() b.clear()
@ -181,7 +206,10 @@ def catMenu():
w=curses.newwin(3,5) w=curses.newwin(3,5)
cat = browser.categoryBrowser(w,hb) cat = browser.categoryBrowser(w,hb)
(r,c) = w.getmaxyx() (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.refreshCategories()
cat.sortByColumn('category') cat.sortByColumn('category')
cat.eventLoop() cat.eventLoop()

View File

@ -1,3 +1,5 @@
from email.mime.text import MIMEText
def format_reminder_email(quest_id: str, def format_reminder_email(quest_id: str,
days_signed_out: int, days_signed_out: int,
librarian_name: str, librarian_name: str,
@ -6,7 +8,7 @@ def format_reminder_email(quest_id: str,
Formats an email as a plain string for sending out email reminders Formats an email as a plain string for sending out email reminders
for signed out books. for signed out books.
Example: _send_email("s455wang", "2017-02-04", 30, "csfmurph", "How to Design Programs") Example: format_reminder_email("s455wang", 30, "Connor Murphy", "How to Design Programs")
""" """
assert len(quest_id) <= 8 assert len(quest_id) <= 8
assert quest_id.isalnum() assert quest_id.isalnum()
@ -14,13 +16,15 @@ def format_reminder_email(quest_id: str,
assert librarian_name != "" assert librarian_name != ""
assert book_name != "" assert book_name != ""
email_message_body = \ email_message = MIMEText(
"""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.
If you would like to keep this book checked out, tell us when in the next month you will return this book. 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. Otherwise, please return the book to the CS Club office (MC 3036) at your earliest convenience.
Thank you for using the CS Club library! Thank you for using the CS Club library!
@ -32,16 +36,11 @@ librarian@csclub.uwaterloo.ca""".format(
book_name, book_name,
days_signed_out, days_signed_out,
librarian_name librarian_name
) ))
email_message_subject = "Overdue book: {}".format(book_name) email_message["Subject"] = "Overdue book: {}".format(book_name)
email_message = ( email_message["To"] = "\"{0}@csclub.uwaterloo.ca\" <{0}@csclub.uwaterloo.ca>\n".format(quest_id)
"To: \"{0}@csclub.uwaterloo.ca\" <{0}@csclub.uwaterloo.ca>\n".format(quest_id) + assert email_message.as_string().replace("\n", "").isprintable(), \
"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" "Our email should not have characters apart from normal characters and newline"
return email_message return email_message.as_string()

View File

@ -283,19 +283,6 @@ class FormWindow:
if self.bt==-1: if self.bt==-1:
self.entries[self.hl].handle_input(ch) self.entries[self.hl].handle_input(ch)
class LoginForm(FormWindow):
caption = "Enter your csclub login to access the mail server"
blabel = "Login"
labels = ["ID", "Password"]
def _make_entries(self):
self.entries = []
self.entries.append(TextEntry(self.w))
self.entries.append(PasswordEntry(self.w))
def _return_values(self):
return [self.entries[0].value, self.entries[1].value]
class BookForm(FormWindow): class BookForm(FormWindow):
caption = "Add a Book" caption = "Add a Book"
blabel = "Add" blabel = "Add"

View File

@ -14,14 +14,15 @@ import time
#Import getpass for password input #Import getpass for password input
import getpass import getpass
#Import subprocess for validation of if a user is in a group #Import subprocess for validation of if a user is in a group, and running
#a sendmail process
import subprocess import subprocess
#Import librarian permissions #Import librarian permissions
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, SuccessForm, catch_error_with, error_form from library.interface.form import FormWindow, BookForm, 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
@ -91,11 +92,15 @@ def _send_email(quest_id: str,
os.makedirs("test_emails", exist_ok=True) os.makedirs("test_emails", exist_ok=True)
#Know that book titles such as "FORTRAN/77" exist, sanitize slashes. #Know that book titles such as "FORTRAN/77" exist, sanitize slashes.
filename = ("test_emails/{}_{}.txt" filename = ("test_emails/{}_{}.txt"
.format(quest_id, book_name.replace('/', '-'))) .format(quest_id, book_name.replace("/", "-")))
with open(filename, 'w') as f: 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_addresses, email_message) #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 True
return False return False
@ -129,40 +134,10 @@ def sendemails_procedure(w, hb, cy, cx, mx):
if librarianName == {}: if librarianName == {}:
return "" #User cancelled the dialog box return "" #User cancelled the dialog box
#Set up email #Don't send out emails if this gets set to True
global server
server = smtplib.SMTP_SSL("mail.csclub.uwaterloo.ca", 465)
# don't send out emails if this is true
testing = False testing = False
if librarianName == "TESTING12345":
#Attempt to login 3 times testing = True
loginAttempts = 0
while loginAttempts < MAX_LOGIN_ATTEMPTS:
#Get the user's login info to login to the mail server
step3 = LoginForm(w, hb, width=mx - 20)
(r, c) = w.getmaxyx()
w.mvwin(cy - r // 2, cx - c // 2)
loginInfo = step3.event_loop()
step3.clear()
csclubID = loginInfo[0]
csclubPwd = loginInfo[1]
if csclubID == "TESTING12345":
testing = True
break
try:
server.login(csclubID, csclubPwd)
break
except smtplib.SMTPAuthenticationError:
loginAttempts += 1
#Check to see if login failed
if loginAttempts >= MAX_LOGIN_ATTEMPTS:
return ""
#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()
@ -182,20 +157,17 @@ def sendemails_procedure(w, hb, cy, cx, mx):
earliest_checkout = min(earliest_checkout, earliest_checkout = min(earliest_checkout,
datetime.strptime(date, "%Y-%m-%d")) datetime.strptime(date, "%Y-%m-%d"))
#Exit from the email server
server.quit()
#A success message about what emails we just sent #A success message about what emails we just sent
assert earliest_checkout > datetime(2010,1,1) assert earliest_checkout > datetime(2010,1,1)
time_since_earliest_checkout = '' time_since_earliest_checkout = ""
days_since_earliest_checkout = (datetime.now().toordinal() days_since_earliest_checkout = (datetime.now().toordinal()
- earliest_checkout.toordinal()) - earliest_checkout.toordinal())
assert days >= 0 assert days >= 0
month_length = 30 #It's wrong because it's an approximation month_length = 30 #It's wrong because it's an approximation
if days_since_earliest_checkout < month_length: if days_since_earliest_checkout < month_length:
time_since_earliest_checkout = '{} days ago'.format(days_since_earliest_checkout) time_since_earliest_checkout = "{} days ago".format(days_since_earliest_checkout)
else: else:
time_since_earliest_checkout = '{:0.1f} months ago'.format(days_since_earliest_checkout / month_length) time_since_earliest_checkout = "{:0.1f} months ago".format(days_since_earliest_checkout / month_length)
if emails_sent == 0: if emails_sent == 0:
success_text = """ success_text = """