272 lines
12 KiB
Python
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()
|