diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e725a4e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.git +__pycache__ +*.pyc +.env diff --git a/.github/workflows/Actions.yaml b/.github/workflows/Actions.yaml new file mode 100644 index 0000000..b7b550d --- /dev/null +++ b/.github/workflows/Actions.yaml @@ -0,0 +1,70 @@ + +# name of the workflow. +# this is optional. +name: Interview Cloud Actions + +# events that will trigger this workflow. +# here, we only have "pull_request", so the workflow will run +# whenever we create a pull request. +# other examples: [push] and [pull_request, push] +on: + pull_request: + + push: + branches: + - main + +# each workflow must have at least one job. +# jobs run in parallel by default (we can change that). +# each job groups together a series of steps to accomplish a purpose. +jobs: + # name of the job + ruffLint: + # the platform or OS that the workflow will run on. + runs-on: ubuntu-latest + + # series of steps to finish the job. + steps: + # name of the step. + # steps run sequentially. + # this is optionale + - name: checkout + # each step can either have "uses" or "run". + # "uses" run an action written somewhere other than this workflow . + # usually from the community. + # this action checks out the repo code to the runner (instance) + # running the action + uses: actions/checkout@v3 + + # another step. + # this step runs a bash (Ubuntu's default shell) command + - name: install ruff + run: pip install ruff + + - name: Lint + run: ruff check ./*/*.py --ignore E402 + + Dockerhub: + runs-on: ubuntu-latest + needs: ruffLint # will only run if linter is successful + if: ${{ github.ref == 'refs/heads/main' || github.event.pull_request.merged == true }} # Runs if it's a push to 'main' or a merged PR to 'main' + steps: + - name: checkout + uses: actions/checkout@v3 + + - name: Login to Dockerhub # log into docker hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} # Using secret for Docker username + password: ${{ secrets.DOCKER_PASSWORD }} # Using secret for Docker password + id: docker-login + + - name: build container image # build the container + run: docker compose build --no-cache + id: docker-build + + - name: Upload to Dockerhub + run: docker push darkicewolf50/uofcbajacloud:latest + if: ${{ steps.docker-login.outcome == 'success' && steps.docker-build.outcome == 'success' }} + + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..12f5ae0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +# Use an official Python runtime as a parent image +FROM python:3.10-slim + +# Set the working directory inside the container +WORKDIR /BajaCloudBackend + +# Copy the current directory contents into the container at /app +COPY ./ContainerContents /BajaCloudBackend + +# Install any necessary dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Expose port 8080 for the container to listen on +EXPOSE 8000 + +# Command to run the Python server when the container starts +#CMD ["fastapi", "run" "main.py"] +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--reload"] + diff --git a/InterviewBooking/ContainerContents/NoSheet.py b/InterviewBooking/ContainerContents/NoSheet.py new file mode 100644 index 0000000..0d1d118 --- /dev/null +++ b/InterviewBooking/ContainerContents/NoSheet.py @@ -0,0 +1,141 @@ +import openpyxl +import yaml +import datetime +from openpyxl.styles import Font, Border, Side, PatternFill +from openpyxl.formatting.rule import FormulaRule + + +def NoSheet(file_path): + """ + 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 + - 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: "No" #add condiftional formatting for no to make whole line red +Interview TimeTable: + - Date: 2024-09-16 + - 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 And Info: + - Status Dropdown: + - Unknown + - Done + - No Show + - Cancelled/Moved + - First time Startup: Move docker volume pointer to new drive and start up container + - Weird Date: Add more space and it will change from ### to a date + - 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) + + 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 = file_path + + # border style + border = Border( # defualt behaviour is thin + left=Side(style='thin'), + right=Side(style='thin'), + top=Side(style='thin'), + bottom=Side(style='thin') + ) + + # create workbook in memory + work_book = openpyxl.Workbook() + + # remove default sheet + 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 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) + # 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" + + elif isinstance(data, datetime.date): + cell.value = data + # Convert the example date '2024-09-16' to a datetime object + # Set the number format to 'YYYY-MM-DD' + cell.number_format = 'yyyy-mmm-d' + 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__": + 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 + file_name = f"./Interviews/OR{year_donation}-L-Interview Data.xlsx" + NoSheet(file_name) \ No newline at end of file diff --git a/InterviewBooking/ContainerContents/ReadDB.py b/InterviewBooking/ContainerContents/ReadDB.py new file mode 100644 index 0000000..214d025 --- /dev/null +++ b/InterviewBooking/ContainerContents/ReadDB.py @@ -0,0 +1,90 @@ +import pandas as pd +import json +from filelock import FileLock, Timeout +import time + +import datetime + +""" +TODO change to use new tempate +TODO change names to be more clear +""" + +def ReadDatabase(file_path): + """ + Reads the database for which slots are available + + ``REQUIRES``: ``File_Path`` where the file is + + ``PROMISES``: ``JSON`` Interview Available Slots + + ``Developed in part by``: Ahmad, Brock + + ``Contact``: ahmad.ahmad1@ucalgary.ca, darkicewolf50@gmail.com + + """ + + + # Define the path to the Excel file and the lock file + excel_file_path = file_path + lock_file_path = file_path + ".lock" + + # 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, sheet_name="Interview TimeTable") + + # Initialize the dictionary to store the structured data + interview_data = {} + + # Group the DataFrame by Date, Start Time, and Slot for organization + for _, row in df.iterrows(): + date = str(row['Date']).split(" ")[0] + start_time = str(row['Start Time Slot']) + slot = int(row['Slot']) if not pd.isna(row['Slot']) else 0 + + # Returns the number of interviewees in the slot; returns 0 if empty + interviewee_amount = len(str(row['Interviewee Name (What to call them)']).split()) if str(row['Interviewee Name (What to call them)']) != "nan" else 0 + + # 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__": + import datetime + 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 + file_name = f"./Interviews/OR{year_donation}-L-Interview Data.xlsx" + try: + data = ReadDatabase(file_name) + print(json.dumps(data, indent=4)) + except RuntimeError as e: + print(e) diff --git a/InterviewBooking/ContainerContents/WriteDB.py b/InterviewBooking/ContainerContents/WriteDB.py new file mode 100644 index 0000000..e9329a8 --- /dev/null +++ b/InterviewBooking/ContainerContents/WriteDB.py @@ -0,0 +1,157 @@ +import pandas as pd +import json +from openpyxl import load_workbook +from send_email import send_email +from filelock import FileLock + + +""" +TODO make it work with the new template +TODO update names to be more clear +TODO try to remove pandas +""" +def ReadDatabase(file_path, lock_path): + """ + Reads the Database to retrieve available interview slots + + ``REQUIRES``: ``File_Path`` ``Lock_Path`` where the file and lock are located + + ``PROMISES``: JSON (Available interview slots) + + ``Developed by``: Ahmad, Brock + + ``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', 'Slot', 'Interviewee Name (What to call them)', 'Interviewee Email', 'Meeting Duration'], sheet_name="Interview TimeTable") + + # Initialize the dictionary to store structured data for available slots + interview_data = {} + + # 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 Slot']) + + # 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 (What to call them)']).split(',') if name.strip()] + interviewee_count = len(interviewee_names) if interviewee_names != ["nan"] else 0 + + # 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(file_path, date, start_time, interviewee_name, interviewee_email): + """ + Appends a new appointment with the interviewee's name and email if the slot is available. + + ``REQUIRES``: ``File_Path`` ``str`` date, ``str`` start_time, ``str`` interviewee_name, ``str`` interviewee_email + + ``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, Brock + + ``Contact``: ahmad.ahmad1@ucalgary.ca, darkicewolf50@gmail.com + """ + + lock_path = file_path + ".lock" + + available_slots = ReadDatabase(file_path, lock_path) + + # 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 "Interview TimeTable" for updating appointments + workbook = load_workbook(file_path) + sheet = workbook["Interview TimeTable"] + df = pd.read_excel(file_path, sheet_name="Interview TimeTable") + + # 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 Slot']) + + 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 (What to call them)']).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 (What to call them)') + 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 + + status_cell = sheet.cell(row=index + 2, column=df.columns.get_loc('Status') + 1) + status_cell.value = "Unknown" + + 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(): + """ + Executes test cases to verify appointment scheduling and slot availability. + + ``REQUIRES``: None + + ``PROMISES``: Prints test outcomes to validate successful booking or slot unavailability. + """ + import datetime + 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 + file_name = f"./Interviews/OR{year_donation}-L-Interview Data.xlsx" + lock = file_name + ".lock" + + print("Available Slots:") + available_slots = ReadDatabase(file_path=file_name, lock_path=lock) + print(json.dumps(available_slots, indent=4)) + + # Test Case 1: Append to an available slot on 2024-09-16 at 10:30:00 + date_1 = "9/16/2024" + start_time_1 = "10:30:00" + interviewee_name_1 = "Alice Johnson" + interviewee_email_1 = "ahmadmuhammadofficial@gmail.com" + print(f"\nTest Case 1: Trying to book {date_1} at {start_time_1} for {interviewee_name_1} ({interviewee_email_1})") + AppendAppointment(file_name, date_1, start_time_1, interviewee_name_1, interviewee_email_1) + + # Test Case 2: Append to an available slot on 2024-09-17 at 13:30:00 + date_2 = "9/17/2024" + start_time_2 = "13:30:00" + interviewee_name_2 = "Bob Smith" + interviewee_email_2 = "bob.smith@example.com" + print(f"\nTest Case 2: Trying to book {date_2} at {start_time_2} for {interviewee_name_2} ({interviewee_email_2})") + AppendAppointment(file_name, date_2, start_time_2, interviewee_name_2, interviewee_email_2) + + # Test Case 3: Attempting to book at 10:30:00 on 2024-09-16 for a different interviewee + date_3 = "9/16/2024" + start_time_3 = "10:30:00" + interviewee_name_3 = "Charlie Brown" + interviewee_email_3 = "charlie.brown@example.com" + print(f"\nTest Case 3: Trying to book {date_3} at {start_time_3} for {interviewee_name_3} ({interviewee_email_3})") + AppendAppointment(file_name, date_3, start_time_3, interviewee_name_3, interviewee_email_3) + +# Run tests +if __name__ == "__main__": + run_tests() diff --git a/InterviewBooking/ContainerContents/interviewPackagers.py b/InterviewBooking/ContainerContents/interviewPackagers.py new file mode 100644 index 0000000..a2bb7df --- /dev/null +++ b/InterviewBooking/ContainerContents/interviewPackagers.py @@ -0,0 +1,55 @@ +from ReadDB import ReadDatabase + + +def getSchedulePackager(file_name): + """ + Packages up the response for a http response + + ``REQUIRES``: ``File_Path`` where the file is + + ``PROMISES``: ``JSON`` http response ready + + ``Develop in part by``: Brock T + + ``Contact``: darkicewolf50@gmail.ocm + + """ + return { + "interviewDates": ReadDatabase(file_path=file_name) + } + +from WriteDB import AppendAppointment +from email_validator import validate_email, EmailNotValidError + + +def SelectAppointment (file_name, appointmentJson): + """ + Packages up a response for a http request + + ``REQUIRES``: ``File_Path`` ``JSON`` where the file is, json has the data of interviewee name, date, starttime and interviewee email + + ``PROMISES``: ``JSON`` Returns if the booking was a success + + ``Developed in part by``: Brock + + ``Contact``: darkicewolf50@gmail.com + + """ + + try: + validEmail = validate_email(appointmentJson["intervieweeEmail"], check_deliverability=True) + if validEmail: + status = AppendAppointment(file_path=file_name, 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: + print(e) + return {"Success": False, "validEmail": "false"} + diff --git a/InterviewBooking/ContainerContents/main.py b/InterviewBooking/ContainerContents/main.py new file mode 100644 index 0000000..dcd8ad6 --- /dev/null +++ b/InterviewBooking/ContainerContents/main.py @@ -0,0 +1,128 @@ +from fastapi import FastAPI +from fastapi.responses import JSONResponse +from pydantic import BaseModel + +from NoSheet import NoSheet +import datetime +import os + +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 +file_name = f"/Interviews/OR{year_donation}-L-Interview Data.xlsx" +if not os.path.isfile(file_name): + os.makedirs(os.path.dirname(file_name), exist_ok=True) + NoSheet(file_name) + +app = FastAPI() + +@app.get("/") +def get_root(): + """ + This does nothing, allows for pings to check for life + + ``REQUIRES``: ```None`` Nothing + + ``PROMISES``: ``JSON`` returns a short message in the body + + ``Develop in part by``: Brock + + ``Contact``: darkicewolf50@gmail.com + + """ + + res = {"message": "Hello I am alive, this does nothing"} + + # Return the response with the custom header + return JSONResponse( + headers={ + "isBase64Encoded": "false", # Header Modification + }, + content={ + "body": res # Ensure res is a dict or do json.dumps to enusre it is stringified + }, + + # status_code=200 commented out just to show how to change it if you wanted + ) + + +from interviewPackagers import getSchedulePackager + +@app.get("/getAppointments") +async def getAppointments(): + """ + checks for all available slots in the database + + ``REQUIRES``: ``None`` Nothing + + ``PROMISES``: ``JSON`` returns all of the avaialbe slots by date then time + + ``Develop in part by``: Brock + + ``Contact``: darkicewolf50@gmail.com + + """ + + res = getSchedulePackager(file_name) + + return JSONResponse( + headers={ + "isBase64Encoded": "false", # Header Modification + }, + content={ + "body": res # Ensure res is a dict or do json.dumps to enusre it is stringified + }, + + # status_code=200 commented out just to show how to change it if you wanted + ) + +from interviewPackagers import SelectAppointment + +class Appointment(BaseModel): + """ + The formatted + + ``REQUIRES``: Correct Format + + ``PROMISES``: Formatted class, needs to be converted into dict os that it can be used in another file + + ``Develop in part by``: Brock + + ``Contact``: darkicewolf50@gmail.com + + """ + intervieweeName: str + date: str + startTime: str + intervieweeEmail: str + + +@app.post("/SelectInterview") +async def postSelectInterview(rawRequest: Appointment): + """ + Books an interview, first checks if the slot is valid + + ``REQUIRES``: ``Appointment`` A specifically formatted request + + ``PROMISES``: ``JSON`` returns if the booking was successful or not + + ``Develop in part by``: Brock + + ``Contact``: darkicewolf50@gmail.com + + """ + + requestDict = {key: str(value) for key, value in rawRequest.dict().items()} + res = SelectAppointment(file_name, requestDict) + + return JSONResponse( + headers={ + "isBase64Encoded": "false", # Header Modification + }, + content={ + "body": res # Ensure res is a dict or do json.dumps to enusre it is stringified + }, + + # status_code=200 commented out just to show how to change it if you wanted + ) + diff --git a/InterviewBooking/ContainerContents/requirements.txt b/InterviewBooking/ContainerContents/requirements.txt new file mode 100644 index 0000000..682708d --- /dev/null +++ b/InterviewBooking/ContainerContents/requirements.txt @@ -0,0 +1,5 @@ +fastapi[standard] +pandas +openpyxl +PyYAML +filelock \ No newline at end of file diff --git a/InterviewBooking/ContainerContents/send_email.py b/InterviewBooking/ContainerContents/send_email.py new file mode 100644 index 0000000..eb209d5 --- /dev/null +++ b/InterviewBooking/ContainerContents/send_email.py @@ -0,0 +1,94 @@ +import smtplib +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from datetime import datetime, timedelta +import pytz # For timezone handling + +""" +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 to the uofcbaja account + + ``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@gmail.com" + gmail_user = "uofcbaja.noreply@gmail.com" + gmail_apppassword = "pver lpnt upjd zvld" + + # Define Mountain Standard Time + mst = pytz.timezone("America/Edmonton") # MST with Daylight Savings considered + + # Parse the input date and time, localize to MST + start_datetime = mst.localize(datetime.strptime(f"{date} {start_time}", "%Y-%m-%d %H:%M:%S")) + end_datetime = start_datetime + timedelta(minutes=30) + + # Format date and time for the calendar URLs + start_time_google = start_datetime.strftime("%Y%m%dT%H%M%S") # Google Calendar (Local time without Z) + end_time_google = end_datetime.strftime("%Y%m%dT%H%M%S") # Google Calendar (Local time without Z) + outlook_start = start_datetime.isoformat() # Outlook Calendar (ISO local time) + outlook_end = end_datetime.isoformat() # Outlook Calendar (ISO local time) + + # Create message object + msg = MIMEMultipart() + msg['From'] = gmail_user + msg['To'] = f"{interviewee_email}, {static_email}" + msg['Subject'] = "Interview Appointment Confirmation" + + # Message body + body = f''' + + + Interview Invitation + + +

Dear {interviewee_name},

+

Your interview has been scheduled on {date} at {start_time} MST.

+

Your interview location is at {location} or will be emailed to you.

+

Please ensure to be available at the designated time.

+ + + + + + +

Best regards,

+

UCalgary Baja Interview Team

+ UCalgary Baja Team + + + ''' + + msg.attach(MIMEText(body, 'html')) + + try: + # Setup the server and send the email + server = smtplib.SMTP('smtp.gmail.com', 587) + server.starttls() + server.login(gmail_user, gmail_apppassword) + server.sendmail(gmail_user, [interviewee_email, static_email], msg.as_string()) + server.quit() + # print(f"Email sent successfully to {interviewee_email} and {static_email}.") + except Exception as e: + print(f"Failed to send email: {e}") + +if __name__ == "__main__": + send_email() diff --git a/InterviewBooking/Other Items/AfterMergeCodeapceTestResults.png b/InterviewBooking/Other Items/AfterMergeCodeapceTestResults.png new file mode 100644 index 0000000..b4c5a58 Binary files /dev/null and b/InterviewBooking/Other Items/AfterMergeCodeapceTestResults.png differ diff --git a/InterviewBooking/Other Items/ConversionToExcel.py b/InterviewBooking/Other Items/ConversionToExcel.py new file mode 100644 index 0000000..2b6e9e9 --- /dev/null +++ b/InterviewBooking/Other Items/ConversionToExcel.py @@ -0,0 +1,44 @@ +import pandas as pd +import yaml + +# Load the YAML file +with open("./interview_database.yaml", 'r') as file: + interview_data = yaml.safe_load(file) + +# Access the nested structure within 'Data' +flattened_data = [] +for date, date_info in interview_data['Data'].items(): + meeting_duration = date_info.get('Meeting Duration') + + for start_time_info in date_info.get('Meeting Start Times', []): + for start_time, meeting_info in start_time_info.items(): + interviewer_name = meeting_info['Interviewer'][0].get('Name') + interviewer_email = meeting_info['Interviewer'][1].get('Email') + interviewee_name = meeting_info['Interviewee'][0].get('Name') + interviewee_email = meeting_info['Interviewee'][1].get('Email') + category = meeting_info.get('Category') + status = meeting_info.get('Status') + slot = meeting_info.get('Slot') + + # Add flattened row to list + flattened_data.append({ + 'Date': date, + 'Meeting Duration': meeting_duration, + 'Start Time': start_time, + 'Interviewer Name': interviewer_name, + 'Interviewer Email': interviewer_email, + 'Interviewee Name': interviewee_name, + 'Interviewee Email': interviewee_email, + 'Category': category, + 'Status': status, + 'Slot': slot + }) + +# Convert to DataFrame if flattened data is not empty +if flattened_data: + df = pd.DataFrame(flattened_data) + # Write the DataFrame to an Excel file + df.to_excel("interview_database.xlsx", index=False) + print("Data has been written to interview_database.xlsx") +else: + print("No data found to write.") diff --git a/InterviewBooking/Other Items/Docker Test/Dockerfile b/InterviewBooking/Other Items/Docker Test/Dockerfile new file mode 100644 index 0000000..d924b8a --- /dev/null +++ b/InterviewBooking/Other Items/Docker Test/Dockerfile @@ -0,0 +1,17 @@ +# Use an official Python runtime as a parent image +FROM python:3.10-slim + +# Set the working directory inside the container +WORKDIR /app + +# Copy the current directory contents into the container at /app +COPY ./mock /app + +# Install any necessary dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Expose port 8080 for the container to listen on +EXPOSE 8080 + +# Command to run the Python server when the container starts +CMD ["python", "server.py"] diff --git a/InterviewBooking/Other Items/Docker Test/docker-compose.yaml b/InterviewBooking/Other Items/Docker Test/docker-compose.yaml new file mode 100644 index 0000000..a24300e --- /dev/null +++ b/InterviewBooking/Other Items/Docker Test/docker-compose.yaml @@ -0,0 +1,10 @@ +services: + test-http-container: + container_name: test-http-container + ports: + - 8080:8080 + volumes: + - ./mock:/app + build: . + # networks: + # - testnet diff --git a/InterviewBooking/Other Items/Docker Test/dockertest-test-http-container_latest.tar.gz b/InterviewBooking/Other Items/Docker Test/dockertest-test-http-container_latest.tar.gz new file mode 100644 index 0000000..292086d Binary files /dev/null and b/InterviewBooking/Other Items/Docker Test/dockertest-test-http-container_latest.tar.gz differ diff --git a/InterviewBooking/Other Items/Docker Test/mock/requirements.txt b/InterviewBooking/Other Items/Docker Test/mock/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/InterviewBooking/Other Items/Docker Test/mock/server.py b/InterviewBooking/Other Items/Docker Test/mock/server.py new file mode 100644 index 0000000..024f1fb --- /dev/null +++ b/InterviewBooking/Other Items/Docker Test/mock/server.py @@ -0,0 +1,49 @@ +import json +import signal +import sys +from http.server import BaseHTTPRequestHandler, HTTPServer +from django.http import HttpResponse + + +# Function to generate the response +def send_funct(): + ymlschedule = {"message": False} + to_send = { + "statusCode": 200, + "isBase64ENcoded": "false", + "body": json.dumps(ymlschedule) + } + return json.dumps(to_send) + +# Define request handler +class RequestHandler(BaseHTTPRequestHandler): + def do_GET(self): + # Send response headers + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.end_headers() + + # Send the response body (the JSON data) + self.wfile.write(send_funct().encode('utf-8')) + +# Graceful shutdown handler +def signal_handler(sig, frame): + print("\nShutting down server gracefully...") + sys.exit(0) + +# Set up and start the server +if __name__ == "__main__": + # Register signal handler for graceful shutdown (e.g., Ctrl+C) + signal.signal(signal.SIGINT, signal_handler) + + # Set the server address (localhost) and port (8080) + server_address = ('', 8080) + httpd = HTTPServer(server_address, RequestHandler) + + print("Server started on port 8080") + try: + # Start the server and listen for requests + httpd.serve_forever() + except KeyboardInterrupt: + # Server shutdown is handled in signal_handler + pass diff --git a/InterviewBooking/Other Items/FastAPI test 1000 requests.png b/InterviewBooking/Other Items/FastAPI test 1000 requests.png new file mode 100644 index 0000000..a86b656 Binary files /dev/null and b/InterviewBooking/Other Items/FastAPI test 1000 requests.png differ diff --git a/InterviewBooking/Other Items/OR-L-Interview Data.xlsx b/InterviewBooking/Other Items/OR-L-Interview Data.xlsx new file mode 100644 index 0000000..d44d9a8 Binary files /dev/null and b/InterviewBooking/Other Items/OR-L-Interview Data.xlsx differ diff --git a/InterviewBooking/Other Items/README.md b/InterviewBooking/Other Items/README.md new file mode 100644 index 0000000..c40a406 --- /dev/null +++ b/InterviewBooking/Other Items/README.md @@ -0,0 +1 @@ +# Interview-Backend \ No newline at end of file diff --git a/InterviewBooking/Other Items/TestBookAppointment.py b/InterviewBooking/Other Items/TestBookAppointment.py new file mode 100644 index 0000000..f076d51 --- /dev/null +++ b/InterviewBooking/Other Items/TestBookAppointment.py @@ -0,0 +1,11 @@ +from WriteDB import AppendAppointment + +def TestBookAppointment(): + date_1 = "2024-09-16" + start_time_1 = "10:30:00" + interviewee_name_1 = "Alice Johnson" + interviewee_email_1 = "ahmadmuhammadofficial@gmail.com" + print(f"\nTest Case 1: Trying to book {date_1} at {start_time_1} for {interviewee_name_1} ({interviewee_email_1})") + AppendAppointment(date_1, start_time_1, interviewee_name_1, interviewee_email_1) + +TestBookAppointment() \ No newline at end of file diff --git a/InterviewBooking/Other Items/TestResultsCodeSpace.png b/InterviewBooking/Other Items/TestResultsCodeSpace.png new file mode 100644 index 0000000..070d1bc Binary files /dev/null and b/InterviewBooking/Other Items/TestResultsCodeSpace.png differ diff --git a/InterviewBooking/Other Items/composetest/Dockerfile b/InterviewBooking/Other Items/composetest/Dockerfile new file mode 100644 index 0000000..8505363 --- /dev/null +++ b/InterviewBooking/Other Items/composetest/Dockerfile @@ -0,0 +1,8 @@ +# syntax=docker/dockerfile:1 +FROM python:3.10-slim +WORKDIR /code +COPY requirements.txt requirements.txt +RUN pip install -r requirements.txt +EXPOSE 5000 +COPY . . +CMD ["python", "app.py"] \ No newline at end of file diff --git a/InterviewBooking/Other Items/composetest/app.py b/InterviewBooking/Other Items/composetest/app.py new file mode 100644 index 0000000..7383f11 --- /dev/null +++ b/InterviewBooking/Other Items/composetest/app.py @@ -0,0 +1,32 @@ +from http.server import BaseHTTPRequestHandler, HTTPServer + +hit_count = 0 # In-memory counter + +def get_hit_count(): + global hit_count + hit_count += 1 + return hit_count + +class RequestHandler(BaseHTTPRequestHandler): + def do_GET(self): + if self.path == '/': + count = get_hit_count() + response = f'Hello World! I have been seen {count} times.\n' + self.send_response(200) + self.send_header('Content-type', 'text/plain') + self.end_headers() + self.wfile.write(response.encode('utf-8')) + else: + self.send_response(404) + self.send_header('Content-type', 'text/plain') + self.end_headers() + self.wfile.write(b'Not Found\n') + +def run(server_class=HTTPServer, handler_class=RequestHandler, port=8000): + server_address = ('', port) + httpd = server_class(server_address, handler_class) + print(f'Starting server on port {port}...') + httpd.serve_forever() + +if __name__ == '__main__': + run() diff --git a/InterviewBooking/Other Items/composetest/docker-compose.yaml b/InterviewBooking/Other Items/composetest/docker-compose.yaml new file mode 100644 index 0000000..c24417b --- /dev/null +++ b/InterviewBooking/Other Items/composetest/docker-compose.yaml @@ -0,0 +1,8 @@ +services: + web: + container_name: test + build: . + ports: + - "8000:8000" + volumes: + - ./:/code diff --git a/InterviewBooking/Other Items/composetest/requirements.txt b/InterviewBooking/Other Items/composetest/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/InterviewBooking/Other Items/interview_database.yaml b/InterviewBooking/Other Items/interview_database.yaml new file mode 100644 index 0000000..9a677ee --- /dev/null +++ b/InterviewBooking/Other Items/interview_database.yaml @@ -0,0 +1,64 @@ +Data: + September 16: + Meeting Duration: 30 min + Meeting Start Times: + - 10:00 AM: + Interviewer: + - Name: John + - Email: john@example.com + Interviewee: + - Name: Jane + - Email: jane@example.com + Category: PowerDrive + Status: Done + Slot: 2 + - 10:30 AM: + Interviewer: + - Name: Jay + - Email: jay@example.com + Interviewee: + - Name: Zoz + - Email: Zoz@example.com + Category: PowerTrain + Status: Pending + Slot: 3 + - 11:00 AM: + Interviewer: + - Name: Ali + - Email: Ali@example.com + Interviewee: + - Name: Bob + - Email: Bob@example.com + Category: Software + Status: Rescheduled + Slot: 1 + - 11:30 AM: + Interviewer: + - Name: + - Email: + Interviewee: + - Name: + - Email: + Category: + Status: + Slot: 1 + - 12:00 PM: + Interviewer: + - Name: Ali, John + - Email: Ali@example.com, Jhon@example.com + Interviewee: + - Name: Bob + - Email: Bob@example.com + Category: Software + Status: Cancelled + Slot: 1 + - 12:30 PM: + Interviewer: + - Name: Ali + - Email: Ali@example.com + Interviewee: + - Name: Bob, Jish + - Email: Bob@example.com, jish@example.com + Category: Software + Status: Cancelled + Slot: 2 diff --git a/InterviewBooking/Other Items/mock/OR25-L-Interview Data.xlsx b/InterviewBooking/Other Items/mock/OR25-L-Interview Data.xlsx new file mode 100644 index 0000000..9628e08 Binary files /dev/null and b/InterviewBooking/Other Items/mock/OR25-L-Interview Data.xlsx differ diff --git a/InterviewBooking/Other Items/mock/OR25-L-Interview Data.xlsx.lock b/InterviewBooking/Other Items/mock/OR25-L-Interview Data.xlsx.lock new file mode 100644 index 0000000..e69de29 diff --git a/InterviewBooking/Other Items/testcontainer.py b/InterviewBooking/Other Items/testcontainer.py new file mode 100644 index 0000000..150a54e --- /dev/null +++ b/InterviewBooking/Other Items/testcontainer.py @@ -0,0 +1,17 @@ +import requests +import json + +if __name__ == "__main__": + getres = requests.get("http://bajacloud.ddnsking.com:43443/getAppointments") + print(getres) + print(json.dumps(json.loads(getres.text), indent=4)) + # example of a request + # postdata = { + # "intervieweeName": "Brock", + # "date": "2024-09-16", + # "startTime": "11:00:00", + # "intervieweeEmail": "darkicewolf50@gmail.com" + # } + # res = requests.post("http://bajacloud.ddnsking.com:43443/SelectInterview", json.dumps(postdata)) + # print(res) + # print(res.text) \ No newline at end of file diff --git a/InterviewBooking/Other Items/testhttp.py b/InterviewBooking/Other Items/testhttp.py new file mode 100644 index 0000000..26babdd --- /dev/null +++ b/InterviewBooking/Other Items/testhttp.py @@ -0,0 +1,25 @@ +import json +import requests +import timeit + +def BenchMarkServer(): + rawRes = requests.get("http://localhost:8080/getAppointments") + res = json.loads(rawRes.text) + print(json.dumps(res, indent=1)) + +def BenchMarkDjango(): + rawRes = requests.get("http://127.0.0.1:8000/getAppointments") + res = json.loads(rawRes.text) + print(json.dumps(res, indent=1)) + +if __name__ == "__main__": + test = 1 + if test: + djangoTime = timeit.timeit(stmt=BenchMarkDjango, number=1000) + # pythonTime = timeit.timeit(stmt=BenchMarkServer, number=10) + print(f"FastAPI: {djangoTime}\nPython: ") + + # reqbody = { + # "body": {"message": "hello"} + # } + # rawRes = requests.post("http://localhost:8000/SelectInterview", reqbody) diff --git a/InterviewBooking/Other Items/web1.html b/InterviewBooking/Other Items/web1.html new file mode 100644 index 0000000..5049285 --- /dev/null +++ b/InterviewBooking/Other Items/web1.html @@ -0,0 +1,29 @@ + + + Interview Invitation + + +

Dear {interviewee_name},

+

Your interview has been scheduled on {date} at {start_time}

+

Please ensure to be available at the designated time.

+ + + + + + +

Best regards,

+

UCalgary Baja Interview Team

+ UCalgary Baja Team + + diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..2a0814e --- /dev/null +++ b/README.MD @@ -0,0 +1,5 @@ +# Deprecaited Moved to Central Repo + +Found [Here](https://github.com/UofCBaja/BajaCloud) + +Reason in "Baja Data 2024 - 2025/W - Software/Brock's Learning.docx" \ No newline at end of file diff --git a/README.md b/README.md index e1f5aad..2a0814e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ -# BajaCloud -The Docker Project that runs on the Baja Nas +# Deprecaited Moved to Central Repo + +Found [Here](https://github.com/UofCBaja/BajaCloud) + +Reason in "Baja Data 2024 - 2025/W - Software/Brock's Learning.docx" \ No newline at end of file diff --git a/bajacloud.tar.gz b/bajacloud.tar.gz new file mode 100644 index 0000000..5ad7021 Binary files /dev/null and b/bajacloud.tar.gz differ diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..e2d18d1 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,11 @@ +services: + bajacloud: + container_name: uofcbajacloud + image: darkicewolf50/uofcbajacloud:latest + ports: + - 43443:8000 + volumes: + - ./L - Logistics/Interviews:/Interviews + # networks: + # - testnet + build: . # do not include in delpoyment version \ No newline at end of file