445 lines
16 KiB
Python
445 lines
16 KiB
Python
#commonly-used helper functions for the other scripts
|
|
#By Drew Murray murraydr@msu.edu
|
|
|
|
import pandas as pd
|
|
import copy as cp
|
|
import smtplib
|
|
import os.path
|
|
from datetime import datetime, timedelta
|
|
|
|
#libraries for google forms
|
|
from apiclient import discovery
|
|
from httplib2 import Http
|
|
from oauth2client import client, file, tools
|
|
from google.oauth2.credentials import Credentials
|
|
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
from googleapiclient.discovery import build
|
|
from google.auth.transport.requests import Request
|
|
|
|
sectionDatabaseFilename="sectionsDatabase.csv"
|
|
staffDatabaseFilename="staffDatabase.csv"
|
|
|
|
|
|
#Converts full names to nedIDs and then back again based on the staff database
|
|
def nameToID(searchName,getAllMatches=False):
|
|
if searchName == "":
|
|
return ""
|
|
stfD = pd.read_csv(staffDatabaseFilename,dtype=str,index_col=False)
|
|
try:
|
|
ret = stfD[stfD['Name'].str.contains(searchName.strip("*"),case=False,regex=False)]["NetID"].values
|
|
except IndexError:
|
|
print ('ERROR: name "'+searchName+'" does not appear in the staff database')
|
|
ret = -1
|
|
if getAllMatches:
|
|
return ret
|
|
else:
|
|
return ret[0]
|
|
def IDToName(searchID):
|
|
if searchID=="":
|
|
return ""
|
|
stfD = pd.read_csv(staffDatabaseFilename,dtype=str,index_col=False)
|
|
name = stfD.loc[stfD['NetID']==searchID].values[0][0]
|
|
if stfD.loc[stfD['NetID']==searchID].values[0][3]=="1":#if returning staff
|
|
name+="*"#append asterisk to name
|
|
return name
|
|
|
|
def isRequestOverlap(req1,req2):
|
|
print(req1)
|
|
print(req2)
|
|
sections1=req1[2].split(';')
|
|
sections2=req2[2].split(';')
|
|
dates1=req1[3].split(';')
|
|
dates2=req2[3].split(';')
|
|
overlap=False
|
|
for s1 in sections1:
|
|
for s2 in sections2:
|
|
for d1 in dates1:
|
|
for d2 in dates2:
|
|
if s1==s2 and d1==d2:
|
|
overlap=True
|
|
return overlap
|
|
|
|
def getTimeFromSection(section):
|
|
secD = pd.read_csv(sectionDatabaseFilename,dtype=str,index_col=False)
|
|
return(secD.loc[secD['Section']==section]['Time'].iloc[0])
|
|
|
|
def getLocationFromSection(section):
|
|
secD = pd.read_csv(sectionDatabaseFilename,dtype=str,index_col=False)
|
|
return(secD.loc[secD['Section']==section]['Location'].iloc[0])
|
|
|
|
def isAssigned(netID,period,date):
|
|
secD = pd.read_csv(sectionDatabaseFilename,dtype=str,index_col=False)
|
|
ret= False
|
|
filt = (secD.loc[(secD['Section']==period) & (secD['Date']==date)].apply(lambda r: r.astype('string').str.contains(netID).any(), axis=1))
|
|
if filt.any(): #if the requestor is not assigned to that section
|
|
ret=True
|
|
return ret
|
|
|
|
def reassign(date,period,oldID,newID):
|
|
secD = pd.read_csv(sectionDatabaseFilename,dtype=str,index_col=False)
|
|
secD.loc[(secD['Section']==period) & (secD['Date']==date)]=secD.loc[(secD['Section']==period) & (secD['Date']==date)].replace(oldID,newID)
|
|
secD.to_csv(sectionDatabaseFilename,index=False,index_label=False)
|
|
|
|
def getSectionTitles():
|
|
secD = pd.read_csv(sectionDatabaseFilename,dtype=str,index_col=False)
|
|
sections=list(secD.drop_duplicates(subset=["Section"])["Section"].values)
|
|
return sections
|
|
|
|
def getDatesFromSection(sec):
|
|
secD = pd.read_csv(sectionDatabaseFilename,dtype=str,index_col=False)
|
|
dates = list(secD.loc[secD["Section"]==sec]["Date"])
|
|
return dates
|
|
|
|
def incrementSubCount(netID,category,amount=1):
|
|
stfD = pd.read_csv(staffDatabaseFilename,dtype=str,index_col=False)
|
|
if category==0 or category=="APP":
|
|
columnName="Approved Substitutions"
|
|
elif category==1 or category=="ACC":
|
|
columnName="Accepted Substitutions"
|
|
elif category==2 or category=="REJ":
|
|
columnName="Rejected Substitutions"
|
|
elif category==3 or category=="CAN":
|
|
columnName="Cancelled Substitutions"
|
|
elif category==4 or category=="FUL":
|
|
columnName="Fulfilled Substitutions"
|
|
else:
|
|
print("ERROR: Invalid category to incrementSubCount")
|
|
return
|
|
stfD.loc[stfD['NetID']==netID,columnName]=str(float(stfD.loc[stfD['NetID']==netID,columnName])+amount)
|
|
stfD.to_csv(staffDatabaseFilename,index=False,index_label=False)
|
|
|
|
def getSubCount(netID):
|
|
|
|
stfD = pd.read_csv(staffDatabaseFilename,dtype=str,index_col=False)
|
|
APP = stfD.loc[(stfD["NetID"]==netID)]["Approved Substitutions"].values[0]
|
|
ACC = stfD.loc[(stfD["NetID"]==netID)]["Accepted Substitutions"].values[0]
|
|
REJ = stfD.loc[(stfD["NetID"]==netID)]["Rejected Substitutions"].values[0]
|
|
CAN = stfD.loc[(stfD["NetID"]==netID)]["Cancelled Substitutions"].values[0]
|
|
FUL = stfD.loc[(stfD["NetID"]==netID)]["Fulfilled Substitutions"].values[0]
|
|
return (APP,ACC,REJ,CAN,FUL)
|
|
|
|
def getTopSubs(categoryStr,fewest=False,num=1):
|
|
if categoryStr=="":
|
|
return ""
|
|
stfD = pd.read_csv(staffDatabaseFilename,dtype=str,index_col=False)
|
|
columns=[]
|
|
if "0" in categoryStr:
|
|
columns+=["Approved Substitutions"]
|
|
if "1" in categoryStr:
|
|
columns+=["Accepted Substitutions"]
|
|
if "2" in categoryStr:
|
|
columns+=["Rejected Substitutions"]
|
|
if "3" in categoryStr:
|
|
columns+=["Cancelled Substitutions"]
|
|
if "4" in categoryStr:
|
|
columns+=["Fulfilled Substitutions"]
|
|
header=""
|
|
for name in columns:
|
|
header+=name[:3].upper()+"+"
|
|
header=header[:-1]
|
|
stfD[header]=0
|
|
for name in columns:
|
|
stfD[header]+=(stfD[name].astype(float))
|
|
return stfD.sort_values(header,axis=0,ascending = fewest).head(num)[["Name",header]].to_string(index=False)
|
|
|
|
|
|
def shiftExists(period,date):
|
|
secD = pd.read_csv(sectionDatabaseFilename,dtype=str,index_col=False)
|
|
try:
|
|
temp=secD[(secD['Section']==period) & (secD['Date']==date)]
|
|
temp.values[0]
|
|
except IndexError:
|
|
return False
|
|
return True
|
|
|
|
def getAllNamesFromSection(periods,dates):
|
|
secD = pd.read_csv(sectionDatabaseFilename,dtype=str,index_col=False)
|
|
others=[]
|
|
for period in periods.split(';'):
|
|
for date in dates.split(';'):
|
|
try:
|
|
temp=secD[(secD['Section']==period) & (secD['Date']==date)]
|
|
netIDs=list(temp.values[0][5:16])
|
|
netIDs = [IDToName(x) for x in netIDs if pd.notna(x)]
|
|
others.append(cp.deepcopy(netIDs))
|
|
except IndexError:
|
|
others.append("NoShift")
|
|
return(others)
|
|
|
|
#finds which date is earlier in string MM/DD format
|
|
def dateBeforeOrEqual(date1, date2):
|
|
month1,day1 = date1.split('/')
|
|
month2,day2 = date2.split('/')
|
|
month1=int(month1)
|
|
month2=int(month2)
|
|
day1=int(day1)
|
|
day2=int(day2)
|
|
if month1<month2:
|
|
return True
|
|
elif month1>month2:
|
|
return False
|
|
elif day1<=day2:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
#create string files for easy pasting into google forms
|
|
def createStrings():
|
|
stfD = pd.read_csv(staffDatabaseFilename,dtype=str,index_col=False)
|
|
secD = pd.read_csv(sectionDatabaseFilename,dtype=str,index_col=False)
|
|
with open("stringOutputs/staffIDs.txt",'w') as f:
|
|
netIDs=stfD['NetID'].values
|
|
netIDs.sort()
|
|
f.write('\n'.join(netIDs))
|
|
|
|
with open("stringOutputs/emails.txt",'w') as f:
|
|
vals=cp.deepcopy(stfD['NetID'].values)
|
|
for i in range(len(vals)):
|
|
vals[i]=vals[i]+"@msu.edu"
|
|
f.write('; '.join(vals))
|
|
|
|
with open("stringOutputs/sectionTitles.txt",'w') as f:
|
|
vals=secD['Section'].values
|
|
seen=set()
|
|
f.write('\n'.join([x for x in vals if not (x in seen or seen.add(x))]))
|
|
|
|
with open("stringOutputs/periodDates.txt",'w') as f:
|
|
vals=secD['Date'].values
|
|
seen=set()
|
|
f.write('\n'.join([x for x in vals if not (x in seen or seen.add(x))]))
|
|
|
|
|
|
def generateEmails(requestList):
|
|
emails=[]
|
|
for req in requestList:
|
|
timestamp = req[0]
|
|
requestor = IDToName(req[1]).strip("*")
|
|
section = req[2]
|
|
date = req[3]
|
|
replacement = IDToName(req[4]).strip("*")
|
|
reason = req[5]
|
|
status = req[6]
|
|
statusReason = req[7]
|
|
|
|
if replacement!="":
|
|
replaced=True
|
|
else:
|
|
replaced=False
|
|
|
|
if ";" in section:
|
|
sections = section.split(";")
|
|
else:
|
|
sections=[section]
|
|
|
|
if ";" in date:
|
|
dates = date.split(";")
|
|
else:
|
|
dates=[date]
|
|
|
|
|
|
|
|
times=[]
|
|
locations=[]
|
|
if status=="APP" or status=="ACC": #For now, don't tell the ULAs that their acceptance was begrudging. We can change this later if desired.
|
|
status="approved"
|
|
changed=True
|
|
for s in sections:
|
|
times.append(getTimeFromSection(s))
|
|
locations.append(getLocationFromSection(s))
|
|
elif status=="REJ" or status=="CAN":
|
|
if status == "REJ":
|
|
status="rejected"
|
|
else:
|
|
status="cancelled"
|
|
changed=False
|
|
for s in sections:
|
|
try:
|
|
times.append(getTimeFromSection(s))
|
|
except:
|
|
times=["INVALID DATE/SECTION"]
|
|
try:
|
|
locations.append(getLocationFromSection(s))
|
|
except:
|
|
locations=["INVALID DATE/SECTION"]
|
|
else:
|
|
print ('ERROR: Status of request is "'+str(status)+'" instead of APP/ACC/REJ/CAN')
|
|
return
|
|
|
|
recipient = req[1]+"@msu.edu"
|
|
subject = "Your request for substitution on "+date+" has been "+status+"."
|
|
|
|
sectionStrings=[]
|
|
for i in range(len(sections)):
|
|
if "HR" in sections[i]:#helproom
|
|
sectionStrings.append(sections[i].replace("_HR"," helproom (hour ")+")")
|
|
sectionStrings[i]=sectionStrings[i].replace("M","Monday")
|
|
sectionStrings[i]=sectionStrings[i].replace("T","Tuesday")
|
|
sectionStrings[i]=sectionStrings[i].replace("W","Wednesday")
|
|
sectionStrings[i]=sectionStrings[i].replace("R","Thursday")
|
|
sectionStrings[i]=sectionStrings[i].replace("F","Friday")
|
|
sectionStrings[i]=sectionStrings[i].replace("S","Sunday")
|
|
else:
|
|
sectionStrings.append(sections[i].replace("Sec","Section"))
|
|
|
|
message = "Hello "+requestor+","
|
|
message+="\n\nYour request to be substituted out of ["+" and ".join(sectionStrings)+"] at ["+" and ".join(times)+"] on ["+" and ".join(dates)+'] because of "'+reason+'" was recieved at ['+timestamp+"]."
|
|
if replaced:
|
|
message+="\nYou specified "+replacement+" as your replacement"
|
|
if changed:
|
|
message+=" and they have also recieved an automatically generated email notifying them of the switch."
|
|
else:
|
|
message+="."
|
|
else:
|
|
message+="\nYou did not specify a replacement."
|
|
message+="\n\nThis request has been reviewed by the professors and *"+status+"*"
|
|
if statusReason != "INST. REASON":
|
|
message+=' for the given reason of: "'+statusReason+'"'
|
|
if changed:
|
|
message+=" and the corresponding changes have been made in our calander.\nIf all of the above information is correct, no further action is necessary. If any of this information is incorrect, please contact us IMMEDIATELY on Slack so we can correct it."
|
|
else:
|
|
message+= " and no changes have been made in our calander. If you did not submit this request, please contact us on Slack as there may be an error in our system."
|
|
message+="\n\nThis email was automatically generated. Do not reply or send email to this address as it will not be seen. All communication should occur in Slack or by emailing cse102@msu.edu"
|
|
|
|
emails.append([recipient,subject,message])
|
|
|
|
#Also send email to new ULA if their subbing IN was approved/accepted
|
|
if changed and replaced:
|
|
recipient = req[4]+"@msu.edu"
|
|
subject = "You have been scheduled to substitute on "+date+"."
|
|
|
|
message = "Hello "+replacement+","
|
|
message+="\n\nYou have been scheduled to substitute for "+requestor+" in ["+" and ".join(sectionStrings)+"] at ["+" and ".join(times)+"] on ["+" and ".join(dates)+"] at ["+" and ".join(locations)+"]"
|
|
message+="\n\n"+requestor+" has specified that you have already agreed to this by submitting the request with you as a replacement and thus our calander has been changed."
|
|
message+="\nIf all of the above information is correct, no further action is necessary. If any of this information is incorrect, please contact us IMMEDIATELY on Slack so we can correct it."
|
|
message+="\n\nThis email was automatically generated. Do not reply or send email to this address as it will not be seen. All communication should occur in Slack or by emailing cse102@msu.edu"
|
|
|
|
emails.append([recipient,subject,message])
|
|
|
|
#Also send email to GA if their helproom ULAs have been changed.
|
|
if changed:
|
|
for s in sections:
|
|
if s in ["M_HR1","M_HR2","T_HR1","T_HR2","W_HR1","W_HR2","R_HR1","R_HR2","F_HR1","F_HR2","S_HR1","S_HR2"]:
|
|
for date in dates:
|
|
names=getAllNamesFromSection(s,date)[0]
|
|
print(names,names!="NoShift")
|
|
if names!="NoShift":
|
|
recipient = nameToID(names[0])
|
|
subject="Change in helproom staff on "+date
|
|
message="There has been a substitution in your helproom section: "+s+".\nYour ULAs on "+date+" are "+", ".join(names[1:])
|
|
|
|
emails.append([recipient,subject,message])
|
|
|
|
sendEmails(emails,actuallySend=False)
|
|
|
|
|
|
|
|
|
|
|
|
#duumyParam is to make really sure that the author of the code that calls this functions really intends to actually send emails (by forcing them to use the keyword)
|
|
def sendEmails(emails,dummyParam=False,actuallySend=False):
|
|
|
|
if actuallySend:
|
|
#https://www.geeksforgeeks.org/send-mail-gmail-account-using-python/
|
|
# creates SMTP session
|
|
s = smtplib.SMTP('smtp.gmail.com', 587)
|
|
|
|
# start TLS for security
|
|
s.starttls()
|
|
|
|
# Authentication
|
|
s.login("cse102msu@gmail.com", "whol ydag otqa hxps")
|
|
|
|
print("BEGINNING EMAIL SENDING!")
|
|
for email in emails:
|
|
address = email[0]
|
|
subject = email[1]
|
|
message = email[2]
|
|
messageObj = 'Subject: {}\n\n{}'.format(subject, message)
|
|
|
|
# sending the mail
|
|
s.sendmail("cse102msu@gmail.com", address, messageObj)
|
|
|
|
# terminating the session
|
|
print("Emails sent!")
|
|
s.quit()
|
|
else:
|
|
print('\n!!!PRINTING TO TERMINAL INSTEAD TO PREVENT ACCIDENTALY EMAIL SPAM!!!:\n\t(Use the argument "acutallySend=True" in "sendEmails" function to disable this.)')
|
|
for email in emails:
|
|
print("\n"+"-"*80+"\nEmail would be sent to "+email[0]+":\nSubject: "+email[1]+"\nMessage:\n"+email[2]+"\n"+"-"*80)
|
|
|
|
|
|
|
|
def getForms(subRequestsFilename):
|
|
#Requires some installation/setup https://developers.google.com/forms/api/quickstart/python
|
|
|
|
SCOPES = ["https://www.googleapis.com/auth/forms.responses.readonly"]
|
|
DISCOVERY_DOC = "https://forms.googleapis.com/$discovery/rest?version=v1"
|
|
creds = None
|
|
|
|
# The file token.json stores the user's access and refresh tokens, and is
|
|
# created automatically when the authorization flow completes for the first
|
|
# time.
|
|
if os.path.exists('token.json'):
|
|
creds = Credentials.from_authorized_user_file('token.json', SCOPES)
|
|
|
|
store = file.Storage('token.json')
|
|
if not creds or not creds.valid:
|
|
if creds and creds.expired and creds.refresh_token:
|
|
creds.refresh(Request())
|
|
else:
|
|
flow = InstalledAppFlow.from_client_secrets_file('credentials.json', SCOPES)
|
|
creds = flow.run_local_server(port=0)
|
|
#Save the credentials for the next run
|
|
with open('token.json', 'w') as token:
|
|
token.write(creds.to_json())
|
|
|
|
#service = discovery.build('forms', 'v1', http=creds.authorize(Http()), discoveryServiceUrl=DISCOVERY_DOC, static_discovery=False)
|
|
|
|
service = build('forms', 'v1', credentials=creds)
|
|
|
|
# gets the responses of your specified form:
|
|
form_id = '1x-8fkuMAcQlTl36SdsbCG0tfClKAcvNshnV8L_Hl904'
|
|
result = service.forms().responses().list(formId=form_id).execute()
|
|
|
|
|
|
subs = pd.read_csv(subRequestsFilename,dtype=str)
|
|
|
|
|
|
with open("lastUpdatedToken.txt",'r') as f:
|
|
line = f.readline()
|
|
if line != "":
|
|
prevTime=datetime.strptime(line,"%Y-%m-%d %H:%M:%S.%f")
|
|
else:
|
|
prevTime=datetime.strptime("1975-01-01 01:01:01.000000","%Y-%m-%d %H:%M:%S.%f")
|
|
data=result["responses"]
|
|
for req in data:
|
|
try:
|
|
reason=req["answers"]["22a5ae9b"]["textAnswers"]["answers"][0]["value"]
|
|
except KeyError:
|
|
reason = ""
|
|
requestor=req["answers"]["7bb6a9dd"]["textAnswers"]["answers"][0]["value"]
|
|
|
|
dates=""
|
|
for i in req["answers"]["11d3b4f8"]["textAnswers"]["answers"]:
|
|
dates+=i["value"]+";"
|
|
dates=dates[:-1]
|
|
|
|
try:
|
|
replacement=req["answers"]["30178530"]["textAnswers"]["answers"][0]["value"]
|
|
except KeyError:#no replacement specified
|
|
replacement=""
|
|
|
|
sections=""
|
|
for i in req["answers"]["5684403f"]["textAnswers"]["answers"]:
|
|
sections+=i["value"]+";"
|
|
sections=sections[:-1]
|
|
|
|
timeStr = req["createTime"][:-1].replace("T"," ")+"000"
|
|
timeStamp=datetime.strptime(timeStr, '%Y-%m-%d %H:%M:%S.%f')-timedelta(hours=4,minutes=0)
|
|
if timeStamp>prevTime:
|
|
reqDict={"Timestamp": [timeStr], "Requestor": [requestor], "Section": [sections], "Dates": [dates], "Replacement": [replacement], "Reason": [reason]}
|
|
subs=pd.concat([subs,pd.DataFrame(reqDict)],ignore_index=True)
|
|
subs.sort_values(by=["Timestamp"],inplace=True)
|
|
subs.to_csv(subRequestsFilename,index=False,index_label=False)
|
|
with open("lastUpdatedToken.txt",'w') as f:
|
|
f.write(str(datetime.now())) |