191 lines
6.4 KiB
Python
191 lines
6.4 KiB
Python
#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 ""
|