Substitution-Requests/substitutionApprovalWindow.py

272 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
import sectionViewWindow as SVW
#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'), sg.Button('Section Viewer')])
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 a "column" of checkboxes instead of a "row" of checkboxes
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 == "Section Viewer":
SVW.secViewWin(defaultFont) #open section viewer
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()