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'#reddish, contrasts well with the dark grey-blue background 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 emailCheckCol=[[sg.Text("Send Emails?")],[sg.Checkbox('',key='-EMAILCHECK-',size=(1,1))]] layout.append([sg.Button('Ok'), sg.Button('Cancel'), sg.Button('See Sub History'), sg.Button('Make Manual Changes'), sg.Button('Section Viewer'),sg.Column(emailCheckCol,element_justification='center')]) 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() #This happens when multiple windows are closed via the red X. Just abort the program at that point, to avoid running into attribute errors if values==None: break #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())[:-1:5] acceptValues=list(values.values())[1:-1:5] rejectValues=list(values.values())[2:-1:5] cancelValues=list(values.values())[3:-1: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': actuallySendEmails=values['-EMAILCHECK-'] valid=True dialogShown = False #Do not show multiple error dialogues even if there are multiple problems (to avoid spamming popup boxes) #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:#"outer product" of the dates and sections, every permutation of the selected options is counted separately for tallying 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]:# this could be unindented one level, but it's technically more performant to not check if we already know its false HF.incrementSubCount(oldID,0,amount=amount) if acceptValues[i]:# this could be unindented one level, but it's technically more performant to not check if we already know its false HF.incrementSubCount(oldID,1,amount=amount) if rejectValues[i]: HF.incrementSubCount(oldID,2,amount=amount) if cancelValues[i]: HF.incrementSubCount(oldID,3,amount=amount) #Adding to either trimmed or resolved only happens once per request, unlike the earlier steps which are once per date*section 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,actuallySendEmails) #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()