Substitution-Requests/substitutionApprovalWindow.py

269 lines
12 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)
#This is the main window for the substitution requests interface
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]
#--------------------------------------------------------------------------
#Basic error checking before opening display (determines which requests are in red)
datetimeFreq={} #track how many requests are for a specific timeslot
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" #Add to the list of errors with this request
else:
if not HF.isAssigned(oldID,period,date):
subRequests[i][6]+=oldID+" not assigned\n" #Add to the list of errors with this request
if newID != "" and HF.isAssigned(newID,period,date):
subRequests[i][6]+=newID+" already assigned\n" #Add to the list of errors with this request
#Test if replacement is already busy with a different section
if newID != "":
elsewhere = HF.isBusy(newID,HF.getTimeFromSection(period),date)
if elsewhere != False:
subRequests[i][6]+=newID+" assigned elsewhere ("+elsewhere+")\n" #Add to the list of errors with this request
#--------------------------------------------------------------------------
#Formatting the text for the main window
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("'","")
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))])
#Adding buttons
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()
#values.values() is an array of the input from all window elements, we use strides to get the checkboxes as "columns" instead of "rows"
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])
#event is the most recent "click" from the user
if event == "See Sub History":
SHW.subHisWin(subRequestsArchiveFilename,defaultFont) #open history window
if event == "Make Manual Changes":
window['Ok'].Update(disabled=True) #This is to prevent undefined behavior as a result of manual changes not being represented in the main window
SMW.subManualWin(subRequestsArchiveFilename,defaultFont) #open manual change window
event = "Cancel" #after we return from the manual change window, just close the whole program
if event == 'Ok':
valid=True
dialogShown = False #Do not show multiple error dialogues even if there are multiple problems.
#If two responses are both 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 any of the app/acc lines have an error, then it is not valid input
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) #Actually make the changes in the database
#HF.incrementSubCount is for tracking how many subs this person requested and what their outcome was
if newID !="":
HF.incrementSubCount(newID,4,amount=amount)#Also track how many sub requests were filled by each person
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])#Trimmed requests is the list of requests that had no action taken and thus must be re-added to the list of pending requests
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)+"-"]])#Resolved requests is kept to be appended to the archived requests database
#If the program crashes during email sending (such as with invalid login) it will deliberately NOT reach the code where it writes to databases
#KEEP THIS LINE BEFORE THE WRITING STEPS
HF.generateEmails(resolvedSubrequests)
#Write to the respective databases
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()