Compare commits
4 Commits
email_remi
...
master
Author | SHA1 | Date |
---|---|---|
Charlie Wang | 358decd75d | |
Zachary Seguin | 292f6fd8af | |
Patrick Melanson | d18ff562fc | |
Patrick Melanson | 4ce53141e0 |
1
TODO
1
TODO
|
@ -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_
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
50
librarian
50
librarian
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 = """
|
||||||
|
|
Loading…
Reference in New Issue