diff --git a/GetSchedulePackager.py b/GetSchedulePackager.py deleted file mode 100644 index 6e85fd7..0000000 --- a/GetSchedulePackager.py +++ /dev/null @@ -1,25 +0,0 @@ -import yaml -import json - -from ReadDB import ReadDatabase - -with open("./MockDB/schedule.yaml", "r") as scheduleyml: - ymlschedule = yaml.safe_load(scheduleyml) - -def getSchedulePackager(): - """ - Formats and allows for the - - ``REQUIRES``: None - - ``PROMISES``: ``JSON`` http response ready - - ``Develop in part by``: Brock T - - ``Contact``: darkicewolf50@gmail.ocm - - """ - return { - "interviewDates": ReadDatabase() - } - diff --git a/MockDB/schedule.yaml b/MockDB/schedule.yaml deleted file mode 100644 index 2a85139..0000000 --- a/MockDB/schedule.yaml +++ /dev/null @@ -1,11 +0,0 @@ -Date: - Sept 16: - Meeting Duration: 30 min - Meeting Start Times: - - 10:00 am - - 10:30 am - - 11:00 am - - 11:30 am - - 1:00 pm - - 1:30 pm - - 2:00 pm diff --git a/NoSheet.py b/NoSheet.py index e5d9aab..f8fd33b 100644 --- a/NoSheet.py +++ b/NoSheet.py @@ -1,13 +1,24 @@ import openpyxl import yaml -import json import datetime from openpyxl.styles import Font, Border, Side, PatternFill from openpyxl.formatting.rule import FormulaRule def NoSheet(): - # sheet with an example properites + """ + Creates the Template for more data to be added + + ``REQUIRES``: ``None`` Ensure no other sheets are present, will overwrite them + + ``PROMISES``: ``XLSX File`` give the template for recuitment for the year + + ``Develop in part by``: Brock + + ``Contact``: darkicewolf50@gmail.com + + """ + yamlraw = """ Recruitment Responses: - Frist Name (What we should call them): Steve @@ -39,21 +50,20 @@ Interview TimeTable: - Category (if not general): Test - Interviewer(s) Name(s): Example - Status: Dropdown (Options in datahelp) #default is Unknown -Data Helper: +Data Helper And Info: - Status Dropdown: - Unknown - Done - No Show - Cancelled/Moved + - First time Startup: Call getschedule for the year - How to Add Dropdown: Go into data, click data validation, select list then select the area you want to get values from in the formula spot """ - + # uses the base above "yaml file" to create the base template yamlsheet = yaml.safe_load(yamlraw) - # print(json.dumps(yamlsheet, indent=4)) - year_donation = int(str(datetime.datetime.now().year)[2:]) # gets the last two digits of the current year then adds 1 for the current season - file_name = f"OR{year_donation + 1}-L-Interview Data.xlsx" # name based off the 2025 naming system + file_name = f"./OR{year_donation + 1}-L-Interview Data.xlsx" # name based off the 2025 naming system # border style border = Border( # defualt behaviour is thin @@ -63,9 +73,6 @@ Data Helper: bottom=Side(style='thin') ) - # for conditional formatting - red_fill = PatternFill(start_color="FF0000", end_color="FF0000", fill_type="solid") - # create workbook in memory work_book = openpyxl.Workbook() @@ -73,45 +80,50 @@ Data Helper: default_sheet = work_book.active work_book.remove(default_sheet) + # decomposes the yaml file at the top and convertss it into a standard template + # does one sheet at a time for sheet_name, title_list in yamlsheet.items(): - # add standard sheets + # add 1 standard sheet, by the outermost name sheet = work_book.create_sheet(sheet_name) + # gets header titles for each sheet, from the inner list titles = [list(title.keys())[0] for title in title_list] + # makes the header titles to bold, have a border and have text for col_num, title in enumerate(titles, start=1): cell = sheet.cell(row=1, column=col_num) cell.value = title - cell.font = Font(bold=True) cell.border = border - + # example data to show on what it will look like or to copy formatting down example_data = [list(data.values())[0] for data in title_list] for col_num, data in enumerate(example_data, start=1): + # for special case Data Helper where there a list in a dictionary if isinstance(data, list): row_num = 2 for item in data: cell = sheet.cell(row=row_num, column=col_num) cell.value = item row_num += 1 + # adds data to the cells else: cell = sheet.cell(row=2, column=col_num) - cell.value = data + # changes the Dropdown data in status to unknown instead of the other option, only there for prep for a dropdown if data == "Dropdown (Options in datahelp)": cell.value = "Unknown" + else: + cell.value = data + if sheet.title == "Recruitment Responses": sheet.conditional_formatting.add("A2:I2", FormulaRule(formula=['=$I2="No"'], fill=PatternFill(start_color="FF0000", end_color="FF0000", fill_type="solid"))) - - - - # save to storage work_book.save(file_name) + print(f"Created {file_name} for {year_donation}") if __name__ == "__main__": NoSheet() \ No newline at end of file diff --git a/OR25-L-Interview Data.xlsx b/OR25-L-Interview Data.xlsx index cfa75ff..6d0e3f3 100644 Binary files a/OR25-L-Interview Data.xlsx and b/OR25-L-Interview Data.xlsx differ diff --git a/ReadDB.py b/ReadDB.py index 68cef6e..1e7d54c 100644 --- a/ReadDB.py +++ b/ReadDB.py @@ -3,74 +3,84 @@ import json from filelock import FileLock, Timeout import time -# Define the path to the Excel file and the lock file -excel_file_path = "./OR-L-Interview Data.xlsx" -lock_file_path = "./OR-L-Interview Data.xlsx.lock" +from NoSheet import NoSheet +import datetime +import os + """ -TODO change to dynamic file name - year_donation = int(str(datetime.datetime.now().year)[2:]) + 1 # gets the last two digits of the current year then adds 1 for the current season - file_name = f"OR{year_donation}-L-Interview Data.xlsx" # name based off the 2025 naming system +TODO update to possibly not use pandas and update to use the new template +TODO update name of function to be more clear """ def ReadDatabase(): """ Reads the database for which slots are available - ``REQUIRES``: None + ``REQUIRES``: ``None`` - ``PROMISES``: JSON (Interview Available Slots) + ``PROMISES``: ``JSON`` Interview Available Slots - ``Developed in part by``: Ahmad + ``Developed in part by``: Ahmad, Brock - ``Contact``: ahmad.ahmad1@ucalgary.ca + ``Contact``: ahmad.ahmad1@ucalgary.ca, darkicewolf50@gmail.com """ - # Retry parameters - max_retries = 60 # Maximum number of retries if the file is locked - retry_interval = 0.5 # Wait time (in seconds) between retries - retries = 0 - while retries < max_retries: - try: - # Attempt to acquire a shared read (non-blocking) access - with FileLock(lock_file_path, timeout=0): # Non-blocking, checks if the lock exists - # Load the Excel file into a pandas DataFrame - df = pd.read_excel(excel_file_path) + year_donation = int(str(datetime.datetime.now().year)[2:]) + 1 # gets the last two digits of the current year then adds 1 for the current season + # name based off the 2025 naming system + # Define the path to the Excel file and the lock file + excel_file_path = f"OR{year_donation}-L-Interview Data.xlsx" + lock_file_path = f"OR{year_donation}-L-Interview Data.xlsx.lock" - # Initialize the dictionary to store the structured data - interview_data = {} + if not (os.path.isfile(excel_file_path) or os.path.isfile(lock_file_path)): + NoSheet() + else: + # Retry parameters + max_retries = 60 # Maximum number of retries if the file is locked + retry_interval = 0.5 # Wait time (in seconds) between retries - # Group the DataFrame by Date, Start Time, and Slot for organization - for _, row in df.iterrows(): - date = str(row['Date']) - start_time = str(row['Start Time']) - slot = int(row['Slot']) if not pd.isna(row['Slot']) else 0 + retries = 0 + while retries < max_retries: + try: + # Attempt to acquire a shared read (non-blocking) access + with FileLock(lock_file_path, timeout=0): # Non-blocking, checks if the lock exists + # Load the Excel file into a pandas DataFrame + df = pd.read_excel(excel_file_path) - # Returns the number of interviewees in the slot; returns 0 if empty - interviewee_amount = len(str(row['Interviewee Name']).split()) if str(row['Interviewee Name']) != "nan" else 0 + # Initialize the dictionary to store the structured data + interview_data = {} - # Check if the slot is available for an interviewee to attend - available_slots = interviewee_amount != slot - if available_slots: - # Initialize nested structure if not present - if date not in interview_data: - interview_data[date] = {} - # Add the start time and duration if not present - if start_time not in interview_data[date]: - interview_data[date][start_time] = { - 'Meeting Duration': row['Meeting Duration'], - } - return interview_data # Successfully read the database + # Group the DataFrame by Date, Start Time, and Slot for organization + for _, row in df.iterrows(): + date = str(row['Date']) + start_time = str(row['Start Time']) + slot = int(row['Slot']) if not pd.isna(row['Slot']) else 0 - except Timeout: - # File is locked; wait and retry - retries += 1 - print(f"File is locked, retrying ({retries}/{max_retries})...") - time.sleep(retry_interval) + # Returns the number of interviewees in the slot; returns 0 if empty + interviewee_amount = len(str(row['Interviewee Name']).split()) if str(row['Interviewee Name']) != "nan" else 0 - # If max retries are exceeded, raise an error - raise RuntimeError("Unable to access the database after multiple attempts due to a file lock.") + # Check if the slot is available for an interviewee to attend + available_slots = interviewee_amount != slot + if available_slots: + # Initialize nested structure if not present + if date not in interview_data: + interview_data[date] = {} + # Add the start time and duration if not present + if start_time not in interview_data[date]: + interview_data[date][start_time] = { + 'Meeting Duration': row['Meeting Duration'], + } + return interview_data # Successfully read the database + + except Timeout: + # File is locked; wait and retry + retries += 1 + print(f"File is locked, retrying ({retries}/{max_retries})...") + time.sleep(retry_interval) + + # If max retries are exceeded, raise an error + raise RuntimeError("Unable to access the database after multiple attempts due to a file lock.") # Example usage of the ReadDatabase function if __name__ == "__main__": diff --git a/WriteDB.py b/WriteDB.py index 3750a24..c5e9a25 100644 --- a/WriteDB.py +++ b/WriteDB.py @@ -4,105 +4,126 @@ from openpyxl import load_workbook from send_email import send_email from filelock import FileLock -# Define the path to the Excel file and the lock file -file_path = "./interview_database.xlsx" -lock_path = "./interview_database.xlsx.lock" # Lock file for synchronization +from NoSheet import NoSheet +import datetime +import os """ -TODO chnage to dynamic file name - year_donation = int(str(datetime.datetime.now().year)[2:]) + 1 # gets the last two digits of the current year then adds 1 for the current season - file_name = f"OR{year_donation}-L-Interview Data.xlsx" # name based off the 2025 naming system +TODO update to possibly not use pandas and update to use the new template +TODO update name of functions to be more clear """ + def ReadDatabase(): """ - Reads the Database to retrieve available interview slots. + Reads the Database to retrieve available interview slots ``REQUIRES``: None ``PROMISES``: JSON (Available interview slots) - ``Developed by``: Ahmad + ``Developed by``: Ahmad, Brock - ``Contact``: ahmad.ahmad1@ucalgary.ca + ``Contact``: ahmad.ahmad1@ucalgary.ca, darkicewolf50@gmail.com """ - # Use a file-based lock for thread-safe and process-safe access - with FileLock(lock_path): - # Load the Excel file into a pandas DataFrame with specific columns - df = pd.read_excel(file_path, usecols=['Date', 'Start Time', 'Slot', 'Interviewee Name', 'Interviewee Email', 'Meeting Duration']) + + year_donation = int(str(datetime.datetime.now().year)[2:]) + 1 # gets the last two digits of the current year then adds 1 for the current season + # name based off the 2025 naming system + file_path = f"OR{year_donation}-L-Interview Data.xlsx" + lock_path = f"OR{year_donation}-L-Interview Data.xlsx.lock" - # Initialize the dictionary to store structured data for available slots - interview_data = {} + # checks for if the file exisits for the year otherwise it will create one + if not (os.path.isfile(file_path) or os.path.isfile(lock_path)): + NoSheet() + else: + # Use a file-based lock for thread-safe and process-safe access + with FileLock(lock_path): + # Load the Excel file into a pandas DataFrame with specific columns + df = pd.read_excel(file_path, usecols=['Date', 'Start Time', 'Slot', 'Interviewee Name', 'Interviewee Email', 'Meeting Duration']) - # Process each row in the DataFrame to structure data by date and time - for _, row in df.iterrows(): - # Convert Date and Start Time to string format for easier comparison - date = str(row['Date']).split(" ")[0] # Format date to YYYY-MM-DD - start_time = str(row['Start Time']) - - # Calculate the slot capacity and current number of interviewees - slot_capacity = int(row['Slot']) if not pd.isna(row['Slot']) else 0 - interviewee_names = [name.strip() for name in str(row['Interviewee Name']).split(',') if name.strip()] - interviewee_count = len(interviewee_names) if interviewee_names != ["nan"] else 0 + # Initialize the dictionary to store structured data for available slots + interview_data = {} - # Check if there are available slots for more interviewees - if interviewee_count < slot_capacity: - # Organize data by date and time, keeping track of available slots and meeting duration - if date not in interview_data: - interview_data[date] = {} - interview_data[date][start_time] = { - 'Meeting Duration': row['Meeting Duration'], - 'Available Slots': slot_capacity - interviewee_count - } + # Process each row in the DataFrame to structure data by date and time + for _, row in df.iterrows(): + # Convert Date and Start Time to string format for easier comparison + date = str(row['Date']).split(" ")[0] # Format date to YYYY-MM-DD + start_time = str(row['Start Time']) + + # Calculate the slot capacity and current number of interviewees + slot_capacity = int(row['Slot']) if not pd.isna(row['Slot']) else 0 + interviewee_names = [name.strip() for name in str(row['Interviewee Name']).split(',') if name.strip()] + interviewee_count = len(interviewee_names) if interviewee_names != ["nan"] else 0 - return interview_data + # Check if there are available slots for more interviewees + if interviewee_count < slot_capacity: + # Organize data by date and time, keeping track of available slots and meeting duration + if date not in interview_data: + interview_data[date] = {} + interview_data[date][start_time] = { + 'Meeting Duration': row['Meeting Duration'], + 'Available Slots': slot_capacity - interviewee_count + } + + return interview_data def AppendAppointment(date, start_time, interviewee_name, interviewee_email): """ Appends a new appointment with the interviewee's name and email if the slot is available. - ``REQUIRES``: date (str), start_time (str), interviewee_name (str), interviewee_email (str) + ``REQUIRES``: ``str`` date, ``str`` start_time, ``str`` interviewee_name, ``str`` interviewee_email - ``PROMISES``: Updates the Excel file with the new interviewee's name and email if there is an available slot. Returns Bool. + ``PROMISES``: ``None`` Updates the Excel file with the new interviewee's name and email if there is an available slot. Returns Bool. - ``Developed by``: Ahmad + ``Developed by``: Ahmad, Brock - ``Contact``: ahmad.ahmad1@ucalgary.ca + ``Contact``: ahmad.ahmad1@ucalgary.ca, darkicewolf50@gmail.com """ - available_slots = ReadDatabase() - - # Check if the requested slot is available in the `available_slots` structure - if date in available_slots and start_time in available_slots[date]: - with FileLock(lock_path): # Ensure process-safe access to the file - # Load workbook and select "Sheet1" for updating appointments - workbook = load_workbook(file_path) - sheet = workbook["Interview Timetable"] - df = pd.read_excel(file_path) - # Find and update the row that matches the provided date and start time - for index, row in df.iterrows(): - row_date = str(row['Date']).split(" ")[0] - row_start_time = str(row['Start Time']) + year_donation = int(str(datetime.datetime.now().year)[2:]) + 1 # gets the last two digits of the current year then adds 1 for the current season + # name based off the 2025 naming system + file_path = f"OR{year_donation}-L-Interview Data.xlsx" + lock_path = f"OR{year_donation}-L-Interview Data.xlsx.lock" - if row_date == date and row_start_time == start_time: - # Current entries for names and emails, and append new data with comma and space - current_names = str(row['Interviewee Name']).strip() - current_emails = str(row['Interviewee Email']).strip() - - updated_names = f"{current_names}, {interviewee_name}" if current_names != "nan" else interviewee_name - updated_emails = f"{current_emails}, {interviewee_email}" if current_emails != "nan" else interviewee_email + # checks for if the file exisits for the year otherwise it will create one + if not (os.path.isfile(file_path) or os.path.isfile(lock_path)): + NoSheet() + else: - # Update the cells with new names and emails - name_cell = sheet.cell(row=index + 2, column=df.columns.get_loc('Interviewee Name') + 1) - email_cell = sheet.cell(row=index + 2, column=df.columns.get_loc('Interviewee Email') + 1) - name_cell.value = updated_names - email_cell.value = updated_emails + available_slots = ReadDatabase() + + # Check if the requested slot is available in the `available_slots` structure + if date in available_slots and start_time in available_slots[date]: + with FileLock(lock_path): # Ensure process-safe access to the file + # Load workbook and select "Sheet1" for updating appointments + workbook = load_workbook(file_path) + sheet = workbook["Interview Timetable"] + df = pd.read_excel(file_path) - workbook.save(file_path) - send_email(interviewee_email, interviewee_name, date, start_time) - return True + # Find and update the row that matches the provided date and start time + for index, row in df.iterrows(): + row_date = str(row['Date']).split(" ")[0] + row_start_time = str(row['Start Time']) - # If no slots available, return that the slot is unavailable - return False + if row_date == date and row_start_time == start_time: + # Current entries for names and emails, and append new data with comma and space + current_names = str(row['Interviewee Name']).strip() + current_emails = str(row['Interviewee Email']).strip() + + updated_names = f"{current_names}, {interviewee_name}" if current_names != "nan" else interviewee_name + updated_emails = f"{current_emails}, {interviewee_email}" if current_emails != "nan" else interviewee_email + + # Update the cells with new names and emails + name_cell = sheet.cell(row=index + 2, column=df.columns.get_loc('Interviewee Name') + 1) + email_cell = sheet.cell(row=index + 2, column=df.columns.get_loc('Interviewee Email') + 1) + name_cell.value = updated_names + email_cell.value = updated_emails + + workbook.save(file_path) + send_email(interviewee_email, interviewee_name, date, start_time) + return True + + # If no slots available, return that the slot is unavailable + return False def run_tests(): diff --git a/__pycache__/NoSheet.cpython-313.pyc b/__pycache__/NoSheet.cpython-313.pyc new file mode 100644 index 0000000..c77c12d Binary files /dev/null and b/__pycache__/NoSheet.cpython-313.pyc differ diff --git a/interviewPackagers.py b/interviewPackagers.py new file mode 100644 index 0000000..e69de29 diff --git a/main.py b/main.py index f052938..307f85c 100644 --- a/main.py +++ b/main.py @@ -35,7 +35,7 @@ def get_root(): ) -from GetSchedulePackager import getSchedulePackager +from interviewPackagers import getSchedulePackager @app.get("/getAppointments") async def getAppointments(): @@ -65,7 +65,7 @@ async def getAppointments(): # status_code=200 commented out just to show how to change it if you wanted ) -from postSelectAppointment import SelectAppointment +from interviewPackagers import SelectAppointment class Appointment(BaseModel): """ diff --git a/postSelectAppointment.py b/postSelectAppointment.py deleted file mode 100644 index f48b30b..0000000 --- a/postSelectAppointment.py +++ /dev/null @@ -1,44 +0,0 @@ -from WriteDB import AppendAppointment -from email_validator import validate_email, EmailNotValidError - - -def SelectAppointment (appointmentJson): - """ - packages up a response for a http request - - ``appointmentJSON``: ``JSON`` - The appointment date and time details - - ``returns``: ``json`` - Returns the status of the booking confirmation - - ``Develop in part by``: Brock T - - ``Contact``: darkicewolf50@gmail.com - """ - """ - { - "intervieweeName": "Alice Johnson", - "date": "2024-09-16", - "startTime": "10:30:00", - "intervieweeEmail": "darkicewolf50@gmail.com" - } - """ - try: - validEmail = validate_email(appointmentJson["intervieweeEmail"], check_deliverability=True) - if validEmail: - status = AppendAppointment(date=appointmentJson["date"], start_time=appointmentJson["startTime"], interviewee_name=appointmentJson["intervieweeName"], interviewee_email=appointmentJson["intervieweeEmail"]) - - if status: - resBody = {"Success": True, "validEmail": "true"} - else: - resBody = {"Success": False, "validEmail": "true"} - - # resBody["message"] = appointmentJson for testing - return resBody - - except EmailNotValidError as e: - return {"Success": False, "validEmail": "false"} - -if __name__ == "__main__": - print(SelectAppointment("10:00 AM")) diff --git a/send_email.py b/send_email.py index 888ce36..a506b71 100644 --- a/send_email.py +++ b/send_email.py @@ -4,12 +4,20 @@ from email.mime.text import MIMEText from datetime import datetime, timedelta import pytz # For timezone handling -def send_email(interviewee_email="darkicewolf50@gmail.com", interviewee_name="brock", date="10-1-2024", start_time="10:00 AM", location ="ENC25"): +""" +TODO add +""" +def send_email(interviewee_email="darkicewolf50@gmail.com", interviewee_name="brock", date="2024-1-10", start_time="10:00:00", location ="ENC25"): """ - Sends an email notification to the interviewee and a static Gmail account. + Sends an email notification to the interviewee and to the uofcbaja account - ``REQUIRES``: interviewee_email (str), interviewee_name (str), date (str), start_time (str) - ``PROMISES``: Sends an email to interviewee and static email on successful appointment booking. + ``REQUIRES``: ``str`` interviewee_email, ``str`` interviewee_name, ``str`` date, ``str`` start_time + + ``PROMISES``: ``EMAIL`` Sends an email to interviewee and static email on successful appointment booking. + + ``Developed by``: Ahmad + + ``Contact``: ahmad.ahmad1@ucalgary.ca """ # Define static email for notifications and Gmail credentials static_email = "uofcbaja.noreply@gmail.com" diff --git a/temp.yml b/temp.yml deleted file mode 100644 index a4d7d76..0000000 --- a/temp.yml +++ /dev/null @@ -1,37 +0,0 @@ -Recruitment Responses: - - Frist Name (What we should call them): Steve - - Last Name: the Bug - - Ucalgary Email: steve.the.bug@ucalgary.ca - - What Subsystem/SubTeam are you interested in?: | - Chassis - Ergonomics - Suspension - Steering - Powertrain - Final Drive - Any Mechanical - Business - Content Creation - Business - Business Relations - Software - - Major: General (1st Year) - - Academic Year: 1st - - Why are you interested in joining UCalgary BAJA?: Example Interest - - Where did you hear about us?: Testing - - Are you available for team meetings/work days? Saturdays 10 am - 4 pm: "Yes" #add condiftional formatting for no to make whole line red -Interview TimeTable: - - Date: 9/16/2024 - - Meeting Duration: 30 min - - Start Time Slot: 10:00:00 AM - - Slot: 1 - - Interviewee Name (What to call them): Steve - - Interviewee Email: steve.the.bug@ucalgary.ca - - Category (if not general): Test - - Interviewer(s) Name(s): Example - - Status: Dropdown (Options in datahelp) #default is Unknown -Data Helper: - - Status Dropdown: - - Unknown - - Done - - No Show - - Cancelled/Moved - - How to Add Dropdown: Go into data, click data validation, select list then select the area you want to get values from in the formula spot