Substitution-Requests/substitutionApprovalWindow.py
2023-10-11 13:21:26 -04:00

256 lines
11 KiB
Python

import PySimpleGUI as sg
import csv
import pandas as pd
import helperFunctions as HF
import substitutionHistoryWindow as SHW
import substitutionManualWindow as SMW
#Printing options for Pandas (for debugging)
pd.set_option('display.min_rows', 20)
pd.set_option('display.expand_frame_repr', False)
pd.set_option('max_colwidth', 20)
def subAppWin(staffDatabaseFilename,secDFilename,subRequestsFilename,subRequestsArchiveFilename,defaultFont=("Courier",11)):
#read in sub requests from the provided csv filepath
subRequests=[]
with open(subRequestsFilename) as f:
headerLine=f.readline()
reader = csv.reader(f)
for row in reader:
subRequests.append(row+[""])
#format fields is Requester, Period, Date, Replacement, Reason, Times, CurrAssigned, Errors
columnWidths=[20,16,16,20,30,50,50,20]
#Set up layout object that governs what appears in the window
headerVals=['Requestor','Period(s)','Date(s)','Replacement','Reason','(Frequency) Times','Currently Assigned','Errors']
row=[sg.Text('APP',size=(4,1)),sg.Text('ACC',size=(4,1)),sg.Text('REJ',size=(4,1)),sg.Text('CAN',size=(4,1))]
for i in range(len(headerVals)):
row.append(sg.Text(headerVals[i],expand_x=True,expand_y=True,size=(columnWidths[i],1)))
layout=[row]
#track how many requests are for a specific timeslot
datetimeFreq={}
for i in range(len(subRequests)):
oldID=subRequests[i][1]
periods=subRequests[i][2].split(';')
dates=subRequests[i][3].split(';')
newID=subRequests[i][4]
for period in periods:
for date in dates:
datetime=date+" "+HF.getTimeFromSection(period)
if datetime in datetimeFreq:
datetimeFreq[datetime]+=1
else:
datetimeFreq[datetime]=1
if not HF.shiftExists(period,date):
subRequests[i][6]+="No such shift\n"
else:
if not HF.isAssigned(oldID,period,date):
subRequests[i][6]+=oldID+" not assigned\n"
if newID != "" and HF.isAssigned(newID,period,date):
subRequests[i][6]+=newID+" already assigned\n"
for i in range(len(subRequests)):
request=subRequests[i]
netID=request[1]
periods=request[2]
if request[4]=='':
newSubName="NONE"
else:
newSubName=HF.IDToName(request[4])
others=HF.getAllNamesFromSection(periods,request[3])
times=[]
for period in periods.split(';'):
time=HF.getTimeFromSection(period)
times.append(time)
timeStr="("
periods=request[2].split(';')
dates=request[3].split(';')
for period in periods:
for date in dates:
datetime=date+" "+HF.getTimeFromSection(period)
timeStr+=str(datetimeFreq[datetime])+","
timeStr=timeStr[:-1]+") "
for period in periods:
for date in dates:
timeStr+=date+" "+str(HF.getTimeFromSection(period))+"; "
timeStr=timeStr[:-2]
others=str(others)[1:-1]
others=others.replace("'","")
#others=others.replace("],","]\n"+" "*(sum(columnWidths[:6])+16)+"|")
textVals=[HF.IDToName(netID),*request[2:4],newSubName,request[5],timeStr,others,request[6].strip("\n")]
row=[sg.Column([[sg.Checkbox('',size=(1,1)),sg.Checkbox('',size=(1,1)),sg.Checkbox('',size=(1,1)),sg.Checkbox('',size=(1,1))],[sg.Input("INST. REASON",size=(12,1),key="-INPUT_REASON_"+str(i)+"-")]])]
if request[6]!="":
color='#FF9999'
else:
color='white'
for i in range(len(textVals)):
row.append(sg.Text(textVals[i],expand_x=True,expand_y=True,size=(columnWidths[i],3),text_color=color))
layout.append(row)
layout.append([sg.Text('-'*(sum(columnWidths)+27))])
layout.append([sg.Button('Ok'), sg.Button('Cancel'), sg.Button('See Sub History'), sg.Button('Make Manual Changes')])
layout=[[sg.Column(layout,scrollable=True,expand_y=True,expand_x=True)]]
# Create the Window
window = sg.Window('Substitution Requests', layout, font=defaultFont,resizable=True,size=(1900,1000))
#Event Loop to process "events" and get the "values" of the inputs
#Cease running if they close the window or press OK or Cancel
completed=False
event=-1
while event != sg.WIN_CLOSED and completed==False:
event, values = window.read()
approveValues=list(values.values())[::5]
acceptValues=list(values.values())[1::5]
rejectValues=list(values.values())[2::5]
cancelValues=list(values.values())[3::5]
#Number of approved changes
nApp = sum([1 for d in approveValues if True == d])
nAcc = sum([1 for d in acceptValues if True == d])
nRej = sum([1 for d in rejectValues if True == d])
nCan = sum([1 for d in cancelValues if True == d])
if event == "See Sub History":
SHW.subHisWin(staffDatabaseFilename,subRequestsArchiveFilename,defaultFont)
if event == "Make Manual Changes":
window['Ok'].Update(disabled=True) #This is to prevent undefined behavior where manual changes are not represented in the error-checking of the main window
SMW.subManualWin(staffDatabaseFilename,secDFilename,subRequestsArchiveFilename,defaultFont)
event = "Cancel"
if event == 'Ok':
valid=True
dialogShown = False #Do not show multiple error dialogues even if there are multiple problems.
#If two responses are checked in ANY single line, then it is not valid input
if True in [True for i, j, k, l in zip(approveValues, acceptValues, rejectValues, cancelValues) if ((i and j) or (i and k) or (j and k) or (i and l) or (j and l) or (k and l))]:
valid=False
if not dialogShown:
dialogShown=True
multiResponseLayout = [[sg.Text('ERROR: You have two different responses checked for at least one substitution')],[sg.Button('Ok')]]
multiResponseWindow = sg.Window('Multiple Responses Selected',multiResponseLayout,font=defaultFont)
multiResponseEvent = -1
while multiResponseEvent != sg.WIN_CLOSED and multiResponseEvent != 'Ok':
multiResponseEvent,multiResponseValues=multiResponseWindow.read()
multiResponseWindow.close()
allNetIDs = []
otherReqCache={}
for i in range(len(subRequests)):
if approveValues[i] or acceptValues[i]:
if subRequests[i][6] != "":
valid=False
if not dialogShown:
dialogShown=True
errorLayout = [[sg.Text('ERROR: You have approved or accepted a request that has an error.\n(See rightmost colum)')],[sg.Button('Ok')]]
errorWindow = sg.Window('Invalid Request App/Acc',errorLayout,font=defaultFont)
errorEvent = -1
while errorEvent != sg.WIN_CLOSED and errorEvent != 'Ok':
errorEvent,errorValues=errorWindow.read()
errorWindow.close()
#Check that the request doesn't overlap another approved request (as that would create a 'race' condition based on request submission order)
netID=subRequests[i][1]
if netID in allNetIDs:#For performance reasons, only check for section/date overlap between requests from the same person (to avoid n^2 searching)
for j in range(len(otherReqCache[netID])):
if HF.isRequestOverlap(subRequests[i],otherReqCache[netID][j]):#this request overlaps with a another request by the same person
valid=False
if not dialogShown:
dialogShown=True
multiRequestLayout = [[sg.Text('WARNING: You have simultaneously approved multiple overlapping requests from '+HF.IDToName(netID)+'.\nThis can produce ambiguous states and is not supported.\nApprove or accept the requests one at a time.')],[sg.Button('Ok')]]
multiRequestWindow = sg.Window('Multiple Requests Approved',multiRequestLayout,font=defaultFont)
multiRequestEvent = -1
while multiRequestEvent != sg.WIN_CLOSED and multiRequestEvent != 'Ok':
multiRequestEvent,multiRequestValues=multiRequestWindow.read()
multiRequestWindow.close()
else:#This request is by the same person but does not overlap
otherReqCache[netID].append(subRequests[i])
else:#there are no other outstanding requests by this person
allNetIDs.append(netID)
otherReqCache[netID]=[subRequests[i]]
#At least one change was approved, check that they didn't click in error:
if nApp+nAcc+nRej+nCan>0 and valid:
verifyLayout = [[sg.Text('Are you sure?\nThis action will make '+str(nApp+nAcc)+' changes to the sectionsDatabase and delete '+str(nApp+nAcc+nRej+nCan)+' sub requests\n***This cannot be undone!***')],[sg.Button('Yes'), sg.Button('No')]]
verifyWindow = sg.Window('Verify Changes', verifyLayout, font=defaultFont)
verifyEvent=-1
while not (verifyEvent == sg.WIN_CLOSED or verifyEvent == 'Yes' or verifyEvent == 'No'):
verifyEvent, verifyValues = verifyWindow.read()
verifyWindow.close()
#Apply changes to the database if they chose "yes"
if verifyEvent == 'Yes':
trimmedSubrequests = [] #the subRequests after all accepted and rejected have been deleted
resolvedSubrequests = [] #the subRequests which were aproved/accepted/rejected
for i in range(len(subRequests)):
oldID=subRequests[i][1]
periods=subRequests[i][2].split(';')
dates=subRequests[i][3].split(';')
newID=subRequests[i][4]
for date in dates:
for period in periods:
if "HR" in period:
amount = 0.5
else:
amount = 1
if approveValues[i] or acceptValues[i]: #If this request was checked "approved" or "accepted"
HF.reassign(date,period,oldID,newID)
if newID !="":
HF.incrementSubCount(newID,4,amount=amount)
if approveValues[i]:
HF.incrementSubCount(oldID,0,amount=amount)
if acceptValues[i]:
HF.incrementSubCount(oldID,1,amount=amount)
if rejectValues[i]:
HF.incrementSubCount(oldID,2,amount=amount)
if cancelValues[i]:
HF.incrementSubCount(oldID,3,amount=amount)
if (not approveValues[i] and not acceptValues[i] and not rejectValues[i] and not cancelValues[i]):
trimmedSubrequests.append(subRequests[i][:-1])
else:
if approveValues[i]:
result="APP"
elif acceptValues[i]:
result="ACC"
elif rejectValues[i]:
result="REJ"
elif cancelValues[i]:
result="CAN"
resolvedSubrequests.append(subRequests[i][:-1]+[result]+[values["-INPUT_REASON_"+str(i)+"-"]])
# If the program crashes during email sending (such as with invalid login) it will deliberately NOT reach the code where it writes to databases
HF.generateEmails(resolvedSubrequests)
with open(subRequestsFilename,'w', newline='') as f:
f.write(headerLine)
writer = csv.writer(f)
writer.writerows(trimmedSubrequests)
with open(subRequestsArchiveFilename,'a', newline='') as f:
writer = csv.writer(f)
writer.writerows(resolvedSubrequests)
completed=True
elif event == 'Cancel':
completed=True
window.close()