diff --git a/Travel_Itinerary_Planner/.flake8 b/Travel_Itinerary_Planner/.flake8 new file mode 100644 index 0000000..68fad0e --- /dev/null +++ b/Travel_Itinerary_Planner/.flake8 @@ -0,0 +1,4 @@ +[flake8] +max-line-length = 127 +max-complexity = 10 +select = E9,F63,F7,F82 diff --git a/Travel_Itinerary_Planner/README.md b/Travel_Itinerary_Planner/README.md new file mode 100644 index 0000000..1497391 --- /dev/null +++ b/Travel_Itinerary_Planner/README.md @@ -0,0 +1,54 @@ +# Travel Itinerary Planner +This application lets you add, edit, view and delete itineraries from a CLI/terminal. +- Add and save an itinerary to an itineraries binary file that can be loaded and updated. +- Edit a saved itinerary item (e.g., name, location, flight departure date & time). +- Add multiple flights and/or attractions to one of the existing itineraries. +- View your itineraries (or a specific itinerary) in a formatted table. +- Delete a full itinerary, or just a flight or attraction from a specific itinerary (note: can only delete flights/attractions if there are more than 1 associated with that itinerary). + +# Prerequisites +- Python 3.14 +- pip (Python package installer) +- Command Line/Terminal (to run the application) +- rich 14.2.0 +- pick 2.4.0 + +# Installing instructions +1. Clone the repository to your local machine + ```bash + git clone https://github.com/king04aman/All-In-One-Python-Projects.git + ``` +2. Change directory into the cloned repository + ```bash + cd All-In-One-Python-Projects/'Travel_Itinerary_Planner'/ + ``` +3. Install the required libraries + ```bash + pip install -r requirements.txt + ``` +4. Run the program in Command Line or Terminal using + ```bash + python3 main.py + ``` + +# Screenshot +### Welcome to the Travel Itinerary Planner! +![Welcome to the Travel Itinerary Planner!](assets/welcome-to-the-app.png) + +### Choose what items to edit +![Choose itinerary to edit](assets/choose-itinerary-to-edit.png) +![Choose itinerary key to edit](assets/choose-item-key-to-edit.png) + +### View your itineraries, formatted into a neat table! +![View itineraries in a table format](assets/view-itineraries.png) + +### And other neat features listed at the top of this document :) + +# Author +Gabrielle Allan + +# Library credits + +[pick library](https://pypi.org/project/pick/) + +[rich library](https://github.com/Textualize/rich) \ No newline at end of file diff --git a/Travel_Itinerary_Planner/assets/choose-item-key-to-edit.png b/Travel_Itinerary_Planner/assets/choose-item-key-to-edit.png new file mode 100644 index 0000000..ecadb55 Binary files /dev/null and b/Travel_Itinerary_Planner/assets/choose-item-key-to-edit.png differ diff --git a/Travel_Itinerary_Planner/assets/choose-itinerary-to-edit.png b/Travel_Itinerary_Planner/assets/choose-itinerary-to-edit.png new file mode 100644 index 0000000..60f7c75 Binary files /dev/null and b/Travel_Itinerary_Planner/assets/choose-itinerary-to-edit.png differ diff --git a/Travel_Itinerary_Planner/assets/view-itineraries.png b/Travel_Itinerary_Planner/assets/view-itineraries.png new file mode 100644 index 0000000..f0b2b10 Binary files /dev/null and b/Travel_Itinerary_Planner/assets/view-itineraries.png differ diff --git a/Travel_Itinerary_Planner/assets/welcome-to-the-app.png b/Travel_Itinerary_Planner/assets/welcome-to-the-app.png new file mode 100644 index 0000000..5be2c54 Binary files /dev/null and b/Travel_Itinerary_Planner/assets/welcome-to-the-app.png differ diff --git a/Travel_Itinerary_Planner/main.py b/Travel_Itinerary_Planner/main.py new file mode 100644 index 0000000..3fac5f6 --- /dev/null +++ b/Travel_Itinerary_Planner/main.py @@ -0,0 +1,374 @@ +from src.file_handler import load_itineraries +from src.manage_itineraries import add_itinerary, edit_itinerary, add_new_flight, add_new_attraction, view_itineraries, delete_itinerary, delete_itinerary_item, print_table +from pick import pick + + +def run_app(): + """ + Main loop to run the travel planner. + """ + itineraries = load_itineraries() + app_running = True + + while app_running: + print("Welcome to the Travel Itinerary Planner app!\n") + print("1. Add a new Itinerary") + print("2. Edit an existing Itinerary item") + print("3. Add a new flight/attraction to an existing Itinerary") + print("4. View existing Itineraries") + print("5. Delete an Itinerary/Itinerary item") + print("6. Log out\n") + + # Check user_choice was a number + while True: + user_choice = input("Please select one of the above options (1-6): ") + if user_choice.isdigit(): + choice = int(user_choice) + if 1 <= choice <= 6: + break + else: + print("Enter a number between 1-6.") + else: + print("Invalid input: Please enter a number between 1-6.") + + # Add a new itinerary + if user_choice == "1": + print("How exciting! Please provide us information about the trip: \n") + name = input("Name of the itinerary (please choose a name you will remember for later): ") + location = input("Location (NA if not applicable): ") + description = input("Brief description of trip: ") + start_date = input("Start date in DD-MM-YYYY: ") + end_date = input("End date in DD-MM-YYYY: ") + flights = [] + attractions = [] + + print("\nNow it is time to add flights!") + user_flight_choice = input("If you want to skip this step, type SKIP and press 'Enter'. Otherwise, press 'Enter'. ") + flights_list_done = False + + if user_flight_choice == "SKIP": + flights = [] + else: + while not flights_list_done: + + departure_airport = input("Name of the airport you will depart from: ") + departure_date = input("Date & time of flight departure (Format: DD-MM-YYYY HH:MM): ") + arrival_airport = input("Name of the airport you will arrive at: ") + arrival_date = input("Date & time of flight arrival (Format: DD-MM-YYYY HH:MM): ") + flight_name = f"{departure_airport} to {arrival_airport}" + + flights.append({ + "flight name": flight_name, + "departure airport": departure_airport, + "departure date": departure_date, + "arrival airport": arrival_airport, + "arrival date": arrival_date + }) + + while True: + add_another_flight = input("Would you like to add another flight? Type Y (yes) or N (no): ") + if add_another_flight == "Y": + flights_list_done = False + break + elif add_another_flight == "N": + flights_list_done = True + break + else: + print("Invalid answer: Please type Y or N only.") + + print("\nFinally: ATTRACTIONS!") + user_attractions_choice = input("If you want to skip this step, type SKIP and press 'Enter'. Otherwise, press 'Enter'. ") + attractions_list_done = False + + if user_attractions_choice == "SKIP": + attractions = [] + else: + while not attractions_list_done: + + attraction_name = input("Name of attraction: ") + attraction_address = input("Address of attraction: ") + attraction_summary = input("Short description of attraction: ") + attraction_tags = input("(Optional) Provide some tags that categorise what kind of activity this involves.\nExample of format required: hike, exciting, views\n") + + attractions.append({ + "attraction name": attraction_name, + "address": attraction_address, + "summary": attraction_summary, + "tag(s)": attraction_tags + }) + + while True: + add_another_attraction = input("Would you like to add another attraction? Type Y or N:") + if add_another_attraction == "Y": + attractions_list_done = False + break + elif add_another_attraction == "N": + attractions_list_done = True + break + else: + print("Invalid answer: Please type Y or N only.") + + add_itinerary(itineraries, name, location, description, start_date, end_date, flights, attractions) + + # Edit existing itinerary + elif user_choice == "2": + # Use rich module to print all the itineraries available, THEN pick module to pick an itinerary to modify + print_table(itineraries) + + itinerary_prompt = 'Here are all the available itineraries. Which would you like to edit?:' + itinerary_options = [] + for itinerary in itineraries: + itinerary_options.append(itinerary["name"]) + itinerary_option, itinerary_index = pick(itinerary_options, itinerary_prompt) + + # Use pick module to select value to modify + edit_prompt = 'Please select the item you want to edit: ' + edit_options = ['name', 'location', 'description', 'start_date', 'end_date', 'flights', 'attractions'] + edit_option, edit_index = pick(edit_options, edit_prompt) + + if edit_option == 'flights': + attraction_choice = "N/A" + flight_name_prompt = 'Which flight would you like to edit?' + flight_name_options = [] + for itinerary in itineraries: + if itinerary["name"] == itinerary_option: + for flight in itinerary['flights']: + flight_name_options.append(flight["flight name"]) + flight_choice, flight_name_index = pick(flight_name_options, flight_name_prompt) + flight_prompt = 'Finally, what about the flight would you like to edit?' + flight_options = ['departure airport', 'departure date', 'arrival airport', 'arrival date'] + edit_option, flight_index = pick(flight_options, flight_prompt) + elif edit_option == 'attractions': + flight_choice = "N/A" + attraction_name_prompt = 'Which attraction would you like to edit?' + attractions_available = [] + for itinerary in itineraries: + if itinerary["name"] == itinerary_option: + for attraction in itinerary['attractions']: + attractions_available.append(attraction["attraction name"]) + attraction_choice, attraction_choice_index = pick(attractions_available, attraction_name_prompt) + attractions_prompt = 'Which attraction property would you like to edit?' + attraction_options = ['attraction name', 'address', 'summary', 'tag(s)'] + edit_option, attraction_index = pick(attraction_options, attractions_prompt) + elif edit_option != 'flights' or edit_option != 'attractions': + flight_choice = "N/A" + attraction_choice = "N/A" + + # Set new value + new_edit_value = input(f"What would you like to change {edit_option} to?\n(If a start/end date use DD-MM-YYYY format. If a flight date/time, use DD-MM-YYYY HH:MM)") + + edit_itinerary(itineraries, itinerary_option, edit_option, flight_choice, attraction_choice, new_edit_value) + + # Add flight or attraction to existing itinerary + elif user_choice == "3": + itinerary_prompt = 'Which itinerary would you like to add to?: ' + itinerary_options = [] + for itinerary in itineraries: + itinerary_options.append(itinerary["name"]) + itinerary_option, itinerary_index = pick(itinerary_options, itinerary_prompt) + + edit_prompt = 'Please select which item you want to add: ' + edit_options = ['flights', 'attractions'] + option, edit_index = pick(edit_options, edit_prompt) + + # Add a flight + if option == 'flights': + flights = [] + flights_list_done = False + while not flights_list_done: + departure_airport = input("Name of the airport you will depart from: ") + departure_date = input("Date & time of flight departure (Format: DD-MM-YYYY HH:MM): ") + arrival_airport = input("Name of the airport you will arrive at: ") + arrival_date = input("Date & time of flight arrival (Format: DD-MM-YYYY HH:MM): ") + flight_name = f"{departure_airport} to {arrival_airport}" + flights.append({ + "flight name": flight_name, + "departure airport": departure_airport, + "departure date": departure_date, + "arrival airport": arrival_airport, + "arrival date": arrival_date + }) + # Check if flight already exists + for itinerary in itineraries: + for flight in itinerary['flights']: + if flight_name == flight['flight name']: + print(f"Flight '{flight_name}' already exists!") + flights_list_done = True + break + if flights_list_done: + print("Returning to main menu...") + break + + while True: + add_another_flight = input("Would you like to add another flight? Type Y (yes) or N (no): ") + if add_another_flight == "Y": + flights_list_done = False + break + elif add_another_flight == "N": + flights_list_done = True + break + else: + print("Invalid answer: Please type Y or N only.") + add_new_flight(itineraries, itinerary_option, flights) + + # Add an attraction + elif option == 'attractions': + attractions = [] + attractions_list_done = False + while not attractions_list_done: + attraction_name = input("Name of attraction: ") + attraction_address = input("Address of attraction: ") + attraction_summary = input("Short description of attraction: ") + attraction_tags = input( + "(Optional) Provide some tags that categorise what kind of activity this involves.\nExample of format required: hike, exciting, views\n") + attractions.append({ + "attraction name": attraction_name, + "address": attraction_address, + "summary": attraction_summary, + "tag(s)": attraction_tags + }) + # Check if attraction already exists + for itinerary in itineraries: + for attraction in itinerary['attractions']: + if attraction_name == attraction['attraction name']: + print(f"Attraction '{attraction_name}' already exists!") + attractions_list_done = True + break + if attractions_list_done: + print("Returning to main menu...") + break + + while True: + add_another_attraction = input("Would you like to add another attraction? Type Y or N:") + if add_another_attraction == "Y": + attractions_list_done = False + break + elif add_another_attraction == "N": + attractions_list_done = True + break + else: + print("Invalid answer: Please type Y or N only.") + add_new_attraction(itineraries, itinerary_option, attractions) + + # View itinerary + elif user_choice == "4": + if not itineraries: + print("No itineraries available to view!") + else: + # Use pick module: Ask if they would like to view all itineraries, or a specific one + view_prompt = 'Would you like to view all the existing itineraries?: ' + view_options = ['View All', 'View One'] + option, index = pick(view_options, view_prompt) + if option == 'View All': + print(f"\nFilter selected: {option}") + filter_option = "All" + elif option == 'View One': + # Use pick module to select an itinerary by name and location + print(f"\nFilter selected: {option}") + itinerary_choice = 'Which itinerary would you like to view?: ' + itinerary_options = [] + for trip in itineraries: + itinerary_options.append(trip["name"]) + filter_option, filter_index = pick(itinerary_options, itinerary_choice) + view_itineraries(itineraries, filter_option) + + # Delete itinerary + elif user_choice == "5": + # Use pick to choose delete options + delete_prompt = 'Would you like to delete a full itinerary, or a flight/attraction? \n(Note: You can only delete a flight or attraction if there is MORE THAN ONE available in the itinerary. \nIf there is only one, please select return.)' + delete_options = ['Entire itinerary', 'Itinerary flight', 'Itinerary attraction', 'Return'] + delete_option, delete_index = pick(delete_options, delete_prompt) + print_table(itineraries) + + if delete_option == 'Entire itinerary': + itinerary_prompt = 'Which itinerary would you like to delete? ' + itinerary_options = [] + for itinerary in itineraries: + itinerary_options.append(itinerary["name"]) + itinerary_option, itinerary_index = pick(itinerary_options, itinerary_prompt) + if delete_itinerary(itineraries, itinerary_option): + print("Itinerary has been deleted.") + else: + print("Itinerary could not be found.") + + elif delete_option == 'Itinerary flight': + while True: + multiple_flights = True + selected_type = "flights" + # Choose itinerary + itinerary_prompt = 'Which itinerary would you like to change? ' + itinerary_options = [] + for itinerary in itineraries: + itinerary_options.append(itinerary["name"]) + itinerary_option, itinerary_index = pick(itinerary_options, itinerary_prompt) + # Choose flight + flight_prompt = 'Which flight would you like to delete? ' + flight_options = [] + for itinerary in itineraries: + if itinerary["name"] == itinerary_option: + if len(itinerary["flights"]) <= 1: + print("There is only one flight available, therefore you cannot delete it.") + multiple_flights = False + break + else: + for flight in itinerary["flights"]: + flight_options.append(flight["flight name"]) + + # Checks if 'while True' statement should be broken + if not multiple_flights: + print("Returning to main menu...") + break + flight_id, itinerary_index = pick(flight_options, flight_prompt) + + if delete_itinerary_item(itineraries, selected_type, itinerary_option, flight_id): + print(f"Flight '{flight_id}' has been deleted.") + else: + print("Flight could not be found.") + break + + elif delete_option == 'Itinerary attraction': + while True: + multiple_attractions = True + selected_type = "attractions" + # Choose itinerary + itinerary_prompt = 'Which itinerary would you like to change? ' + itinerary_options = [] + for itinerary in itineraries: + itinerary_options.append(itinerary["name"]) + itinerary_option, itinerary_index = pick(itinerary_options, itinerary_prompt) + # Choose attraction + attraction_prompt = 'Which attraction would you like to delete? ' + attraction_options = [] + for itinerary in itineraries: + if itinerary["name"] == itinerary_option: + if len(itinerary["attractions"]) <= 1: + print("There is only one attraction available, therefore you cannot delete it.") + multiple_attractions = False + break + else: + for attraction in itinerary["attractions"]: + attraction_options.append(attraction["attraction name"]) + # Checks if 'while True' statement should be broken + if not multiple_attractions: + print("Returning to main menu...") + break + attraction_id, itinerary_index = pick(attraction_options, attraction_prompt) + + if delete_itinerary_item(itineraries, selected_type, itinerary_option, attraction_id): + print(f"Attraction '{attraction_id}' has been deleted.") + else: + print("Attraction could not be found.") + break + + elif delete_option == 'Return': + print("Returning to previous menu...\n") + + # Log out + elif user_choice == "6": + app_running = False + + print("See you next time! 👋") + + +if __name__ == "__main__": + run_app() diff --git a/Travel_Itinerary_Planner/requirements.txt b/Travel_Itinerary_Planner/requirements.txt new file mode 100644 index 0000000..f5430d1 --- /dev/null +++ b/Travel_Itinerary_Planner/requirements.txt @@ -0,0 +1,3 @@ +flake8==7.3.0 +pick==v2.4.0 +rich==14.2.0 \ No newline at end of file diff --git a/Travel_Itinerary_Planner/setup.py b/Travel_Itinerary_Planner/setup.py new file mode 100644 index 0000000..16212d4 --- /dev/null +++ b/Travel_Itinerary_Planner/setup.py @@ -0,0 +1,27 @@ +from setuptools import setup, find_packages + +setup( + name="task_manager_cli", # Updated project name + version="0.2", # Updated version to reflect the new project + packages=find_packages(where='src'), # Automatically locate packages under 'src' + package_dir={'': 'src'}, # Packages are located in the 'src' directory + + # Dependencies required for the updated functionality + install_requires=[ + "pick==2.4.0", + "python-dateutil>=2.8", # For robust date validation + "rich==14.2.0", # FIRST ISSUE: For formatted table + "colorama==0.4.6", + "markdown-it-py==3.0.0", + "mdurl==0.1.2", + "Pygments==2.19.1", + "six==1.17.0", + "coverage", + "flake8==7.3.0", + "pick==v2.4.0" + ], + + # Project metadata + author="Gabrielle", + author_email="your-email@example.com", +) diff --git a/Travel_Itinerary_Planner/src/__init__.py b/Travel_Itinerary_Planner/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Travel_Itinerary_Planner/src/file_handler.py b/Travel_Itinerary_Planner/src/file_handler.py new file mode 100644 index 0000000..02dcec1 --- /dev/null +++ b/Travel_Itinerary_Planner/src/file_handler.py @@ -0,0 +1,37 @@ +import os +import pickle +from pprint import pprint + +ITINERARY_FILE = "itineraries.bin" +# Used \\ because of this warning: https://stackoverflow.com/questions/52335970/how-to-fix-syntaxwarning-invalid-escape-sequence-in-python +current_directory = f"{os.getcwd()}\\{ITINERARY_FILE}" +print(current_directory) + + +def load_itineraries(): + """ + Load tasks from a binary file using the pickle module. + + Returns: + dictionary: A dictionary of Itinerary objects loaded from the binary file. + If the file does not exist, an empty dictionary is returned. + """ + if os.path.exists(current_directory): + with open(ITINERARY_FILE, "rb") as file: + return pickle.load(file) + return [] + + +def save_itineraries(itineraries): + """ + Save a list of itineraries to a binary file using the pickle module. + + Args: + itineraries (dict): A dictionary containing Itinerary objects to save. + + Side Effects: + - Writes the serialized itinerary dictionary to ITINERARY_FILE. + - Overwrites the file if it already exists. + """ + with open(current_directory, "wb") as file: + pickle.dump(itineraries, file) diff --git a/Travel_Itinerary_Planner/src/manage_itineraries.py b/Travel_Itinerary_Planner/src/manage_itineraries.py new file mode 100644 index 0000000..5f1801e --- /dev/null +++ b/Travel_Itinerary_Planner/src/manage_itineraries.py @@ -0,0 +1,312 @@ +from src.file_handler import save_itineraries +from datetime import datetime +from rich.console import Console +from rich.table import Table +from rich import print +""" +This file contains all of the functions needed for the Travel Itinerary Planner. +It allows the user to add, edit, view, delete, and export an itinerary while checking for errors in input. +""" + + +def add_itinerary(itinerary_list, name, location, description, start_date, end_date, flights, attractions): + """ + Docstring for add_itinerary + + Returns: + bool: True if Itinerary is added without issue, otherwise False. + + Side Effects: + - Saves the updated itinerary list to a file using `save_itineraries`. + """ + # Print TEST: Uncomment to check that all items have been transferred correctly + # print(name) + # print(location) + # print(description) + # print(start_date) + # print(end_date) + # print(flights) + # print(attractions) + # print("TEST ENDS HERE") + + if validate_dates(start_date, end_date, flights): + # Add the new itinerary after validation checks + new_itinerary = { + "name": name, + "location": location, + "description": description, + "start_date": start_date, + "end_date": end_date, + "flights": flights, + "attractions": attractions + } + # Uncomment to print for itinerary validation: + # print(new_itinerary) + if itinerary_list: + for itinerary in itinerary_list: + if new_itinerary == itinerary: + print("This itinerary already exists!\nReturning to main menu...") + return False + elif new_itinerary["name"] == itinerary["name"]: + print(f"'{new_itinerary["name"]}' already exists!\nReturning to main menu...") + return False + else: + itinerary_list.append(new_itinerary) + else: + itinerary_list.append(new_itinerary) + # Uncomment to print for itinerary_list validation: + # print(itinerary_list) + + save_itineraries(itinerary_list) + return True + return False + + +def edit_itinerary(itinerary_list, itinerary_option, edit_option, flight_choice, attraction_choice, new_value): + # Edit the itinerary list + for itinerary in itinerary_list: + if itinerary["name"] == itinerary_option: + # Edit DateTime-adjacent data + if edit_option == "start_date": + if not validate_dates(new_value, itinerary["end_date"], itinerary["flights"]): + print("Invalid date. Format required: DD-MM-YYYY (e.g., 12-12-2026)") + return False + else: + itinerary["start_date"] = new_value + break + elif edit_option == "end_date": + if not validate_dates(itinerary["start_date"], new_value, itinerary["flights"]): + print("Invalid date. Format required: DD-MM-YYYY (e.g., 12-12-2026)") + return False + else: + itinerary["end_date"] = new_value + break + elif edit_option == "departure date": + for flight_id in itinerary["flights"]: + if flight_id["flight name"] == flight_choice: + test_updated_flight = [flight_id] + test_updated_flight[0]["departure date"] = new_value + if not validate_dates(itinerary["start_date"], itinerary["end_date"], test_updated_flight): + print("Invalid date. Format required: DD-MM-YYYY HH:MM (e.g., 12-12-2026 08:00)") + return False + else: + flight_id["departure date"] = new_value + break + break + elif edit_option == "arrival date": + for flight_id in itinerary["flights"]: + if flight_id["flight name"] == flight_choice: + test_updated_flight = [flight_id] + test_updated_flight[0]["arrival date"] = new_value + if not validate_dates(itinerary["start_date"], itinerary["end_date"], test_updated_flight): + print("Invalid date & time. Format required: DD-MM-YYYY HH:MM (e.g., 12-12-2026 08:00") + return False + else: + flight_id["arrival date"] = new_value + break + break + else: + # Edit trip's string data (outside of flights & attractions) + if edit_option == 'name' or edit_option == 'location' or edit_option == 'description': + itinerary[edit_option] = new_value + break + + # Flights: string type options + elif edit_option == 'departure airport': + for flight_id in itinerary["flights"]: + if flight_id["flight name"] == flight_choice: + flight_id["departure airport"] = new_value + flight_id["flight name"] = f"{new_value} to {flight_id["arrival airport"]}" + break + break + + elif edit_option == 'arrival airport': + for flight_id in itinerary["flights"]: + if flight_id["flight name"] == flight_choice: + flight_id.update({"arrival airport": new_value}) + flight_id["flight name"] = f"{flight_id["departure airport"]} to {new_value}" + break + break + + # Attractions: string type options + elif edit_option == 'attraction name': + for attraction in itinerary["attractions"]: + if attraction["attraction name"] == attraction_choice: + attraction.update({"attraction name": new_value}) + break + break + elif edit_option == 'address': + for attraction in itinerary["attractions"]: + if attraction["attraction name"] == attraction_choice: + attraction.update({"address": new_value}) + break + break + elif edit_option == 'summary': + for attraction in itinerary["attractions"]: + if attraction["attraction name"] == attraction_choice: + attraction.update({"summary": new_value}) + break + break + elif edit_option == 'tag(s)': + for attraction in itinerary["attractions"]: + if attraction["attraction name"] == attraction_choice: + attraction.update({"tag(s)": new_value}) + break + break + save_itineraries(itinerary_list) + # Uncomment to print for itinerary_list validation: + # print(itinerary_list) + return True + + +def add_new_flight(itinerary_list, itinerary_name, new_flights): + for itinerary in itinerary_list: + if itinerary["name"] == itinerary_name: + for flight in new_flights: + if flight not in itinerary["flights"]: + itinerary["flights"].append(flight) + else: + print(f"Duplicate flight detected: {flight["flight name"]}!") + print("This flight will not be added.") + return False + save_itineraries(itinerary_list) + # Uncomment to print for itinerary_list validation: + # print(itinerary_list) + return True + + +def add_new_attraction(itinerary_list, itinerary_name, new_attractions): + for itinerary in itinerary_list: + if itinerary["name"] == itinerary_name: + for attraction in new_attractions: + if attraction not in itinerary["attractions"]: + itinerary["attractions"].append(attraction) + else: + print(f"Duplicate attraction detected: {attraction["attraction name"]}!") + print("This attraction will not be added.") + return False + save_itineraries(itinerary_list) + # Uncomment to print for itinerary_list validation: + # print(itinerary_list) + return True + + +def view_itineraries(itinerary_list, filter_option): + if filter_option != "All": + # Filter itinerary list + filtered_itineraries = [] + for itinerary in itinerary_list: + if filter_option == itinerary["name"]: + filtered_itineraries.append(itinerary) + # Use rich to print table + print_table(filtered_itineraries) + else: + # Use rich to print table + print_table(itinerary_list) + return True + + +def delete_itinerary(itinerary_list, itinerary_to_delete): + for itinerary in itinerary_list: + if itinerary["name"] == itinerary_to_delete: + itinerary_list.remove(itinerary) + save_itineraries(itinerary_list) + # Uncomment to print for itinerary_list validation: + # print(itinerary_list) + return True + return False + + +def delete_itinerary_item(itinerary_list, flights_or_attractions_type, itinerary_name, item_to_delete): + for itinerary in itinerary_list: + if itinerary["name"] == itinerary_name: + if flights_or_attractions_type == "flights": + for item in itinerary["flights"]: + if item["flight name"] == item_to_delete: + itinerary["flights"].remove(item) + save_itineraries(itinerary_list) + return True + elif flights_or_attractions_type == "attractions": + for item in itinerary["attractions"]: + if item["attraction name"] == item_to_delete: + itinerary["attractions"].remove(item) + save_itineraries(itinerary_list) + return True + return False + + +# Print rich table function +def print_table(trips): + trip_console = Console() + trip_table = Table(title="Itineraries", show_lines=True) + + trip_table.add_column("Trip Name", justify="center", no_wrap=True) + trip_table.add_column("Location", justify="center", no_wrap=True) + trip_table.add_column("Description", justify="center", no_wrap=False) + trip_table.add_column("Start Date", justify="center", no_wrap=True) + trip_table.add_column("End Date", justify="center", no_wrap=True) + trip_table.add_column("Flights", justify="left", no_wrap=True) + trip_table.add_column("Attractions", justify="left", style="bold", no_wrap=False) + + for trip in trips: + flight_list = '' + attraction_list = '' + if len(trip["flights"]) == 1: + flight_list = f'[bold red]{trip["flights"][0]["flight name"]}[/bold red] \nDepart: {trip["flights"][0]["departure date"]}\nArrive: {trip["flights"][0]["arrival date"]} \n' + else: + for flight in trip["flights"]: + flight_item = f'[bold red]{flight["flight name"]}[/bold red] \nDepart: {flight["departure date"]}\nArrive: {flight["arrival date"]} \n' + flight_list = flight_list + f'{flight_item}' + + if len(trip["attractions"]) == 1: + attraction_list = f'[bold red]{trip["attractions"][0]["attraction name"]}[/bold red] \nAddress: {trip["attractions"][0]["address"]} \nSummary: {trip["attractions"][0]["summary"]} \nTag(s): {trip["attractions"][0]["tag(s)"]}\n' + else: + for attraction in trip["attractions"]: + # Printing python text with colour using ANSI codes: https://vascosim.medium.com/how-to-print-colored-text-in-python-52f6244e2e30 + attraction_string = f'[bold red]{attraction["attraction name"]}[/bold red] \nAddress: {attraction["address"]} \nSummary: {attraction["summary"]} \nTag(s): {attraction["tag(s)"]}\n' + attraction_list = attraction_list + f'{attraction_string}' + + trip_table.add_row(trip["name"], trip["location"], trip["description"], trip["start_date"], trip["end_date"], flight_list, attraction_list) + trip_console.print(trip_table) + return + + +# Validate dates function +def validate_dates(start_date, end_date, flights): + """ + Validate dates given for start date, end date and flight datetime + + Raises: + ValueError: If the start or end date is not in the correct format. + + :param start_date: Itinerary start date. + :param end_date: Itinerary end date. + :param flights: Nested dictionary containing flight information. This function will be testing the departure_date and arrival_date items. + """ + + try: + datetime.strptime(start_date, "%d-%m-%Y") + except ValueError: + print("Error: Invalid start date. Use 'DD-MM-YYYY' format.") + return False + + try: + datetime.strptime(end_date, "%d-%m-%Y") + except ValueError: + print("Error: Invalid end date. Use 'DD-MM-YYYY' format.") + return False + + # Resource used for following code: https://stackoverflow.com/questions/17322208/multiple-try-codes-in-one-block + for flight_info in flights: + try: + datetime.strptime(flight_info["departure date"], "%d-%m-%Y %H:%M") + except ValueError: + print("Error: Invalid date/time for flight departure. Use 'DD-MM-YYYY HH:MM' format.") + return False + + try: + datetime.strptime(flight_info["arrival date"], "%d-%m-%Y %H:%M") + except ValueError: + print("Error: Invalid date/time for flight arrival. Use 'DD-MM-YYYY HH:MM' format.") + return False + return True diff --git a/Travel_Itinerary_Planner/tests/test_manage_itineraries.py b/Travel_Itinerary_Planner/tests/test_manage_itineraries.py new file mode 100644 index 0000000..1668bf4 --- /dev/null +++ b/Travel_Itinerary_Planner/tests/test_manage_itineraries.py @@ -0,0 +1,794 @@ +import unittest +from src.manage_itineraries import add_itinerary, edit_itinerary, add_new_flight, add_new_attraction, view_itineraries, delete_itinerary, delete_itinerary_item, print_table +from src.file_handler import load_itineraries, save_itineraries + +import os +import io +from rich.console import Console +from rich.table import Table +from rich import print + +from pick import Picker + +TEST_FILE = "test_itineraries.bin" + +""" +Tests all of the functions in manage_itineraries.py + +""" + + +class TestManageItineraries(unittest.TestCase): + """ + Unit tests for manage_itineraries.py functionalities including adding, editing, viewing, deleting, filtering, + and persisting itineraries to and from a binary file. + """ + + def setUp(self): + """ + Set up the test environment by initialising an empty task list + and backing up the original itinerary binary file. + """ + self.itineraries = [] + self.original_file = "itineraries.bin" + if os.path.exists(TEST_FILE): + os.remove(TEST_FILE) + os.rename("itineraries.bin", TEST_FILE) if os.path.exists("itineraries.bin") else None + + def tearDown(self): + """ + Clean up the test environment by removing any test-created binary files + and restoring the original task binary file. + """ + if os.path.exists("itineraries.bin"): + os.remove("itineraries.bin") + os.rename(TEST_FILE, "itineraries.bin") if os.path.exists(TEST_FILE) else None + + def test_add_itinerary(self): + """ + Test adding a new task to the task list. + Verify that the task is successfully added and the list size increases. + """ + test_itinerary = { + "name": "trip", "location": "Japan", "description": "description", "start_date": "12-12-2026", "end_date": "28-01-2027", "flights": [{ + "flight name": "Perth to Narita", + "departure airport": "Perth", + "departure date": "12-12-2026 08:00", + "arrival airport": "Narita", + "arrival date": "12-12-2026 16:00" + }, + { + "flight name": "Narita to Perth", + "departure airport": "Narita", + "departure date": "27-01-2027 23:00", + "arrival airport": "Perth", + "arrival date": "28-01-2027 07:00" + } + ], + "attractions": [{ + "attraction name": "Hike", + "address": "123 Hike Lane", + "summary": "A cool hike with good views", + "tag(s)": "outdoors" + }, + { + "attraction name": "Dinner spot", + "address": "49 Sweet Cove", + "summary": "A lovely dinner spot", + "tag(s)": "dinner, romantic" + } + ]} + + result = add_itinerary(self.itineraries, name=test_itinerary["name"], location=test_itinerary["location"], description=test_itinerary["description"], start_date=test_itinerary["start_date"], end_date=test_itinerary["end_date"], flights=test_itinerary["flights"], attractions=test_itinerary["attractions"]) + + print(self.itineraries) + self.assertTrue(result) + self.assertEqual(len(self.itineraries), 1) + + def test_add_duplicate_itinerary(self): + """ + Test adding a duplicate itinerary with the same title but different values, then a complete copy of the itinerary. + Verify that these itineraries are not allowed and the function returns False. + """ + test_itinerary = { + "name": "trip", "location": "Japan", "description": "description", "start_date": "12-12-2026", "end_date": "28-01-2027", "flights": [{ + "flight name": "Perth to Narita", + "departure airport": "Perth", + "departure date": "12-12-2026 08:00", + "arrival airport": "Narita", + "arrival date": "12-12-2026 16:00" + }, + { + "flight name": "Narita to Perth", + "departure airport": "Narita", + "departure date": "27-01-2027 23:00", + "arrival airport": "Perth", + "arrival date": "28-01-2027 07:00" + } + ], + "attractions": [{ + "attraction name": "Hike", + "address": "123 Hike Lane", + "summary": "A cool hike with good views", + "tag(s)": "outdoors" + }, + { + "attraction name": "Dinner spot", + "address": "49 Sweet Cove", + "summary": "A lovely dinner spot", + "tag(s)": "dinner, romantic" + } + ]} + + add_itinerary(self.itineraries, name=test_itinerary["name"], location=test_itinerary["location"], description=test_itinerary["description"], start_date=test_itinerary["start_date"], end_date=test_itinerary["end_date"], flights=test_itinerary["flights"], attractions=test_itinerary["attractions"]) + + same_name_result = add_itinerary(self.itineraries, name=test_itinerary["name"], location="Another location", description="Another description", start_date=test_itinerary["start_date"], end_date=test_itinerary["end_date"], flights=test_itinerary["flights"], attractions=test_itinerary["attractions"]) + + duplicate_result = add_itinerary(self.itineraries, name=test_itinerary["name"], location=test_itinerary["location"], description=test_itinerary["description"], start_date=test_itinerary["start_date"], end_date=test_itinerary["end_date"], flights=test_itinerary["flights"], attractions=test_itinerary["attractions"]) + + self.assertFalse(same_name_result) + self.assertFalse(duplicate_result) + self.assertEqual(len(self.itineraries), 1) + + def test_add_itinerary_with_invalid_dates(self): + """ + Test adding itineraries with invalid date formats (start_date, end_date, & flight departure and arrival datetimes). + Verify that the add_itinerary() AND validate_dates() functions handle invalid input as expected and returns False. + """ + test_itinerary = { + "name": "trip", "location": "Japan", "description": "description", "start_date": "12-12-2026", "end_date": "28-01-2027", "flights": [{ + "flight name": "Perth to Narita", + "departure airport": "Perth", + "departure date": "12-12-2026 08:00", + "arrival airport": "Narita", + "arrival date": "12-12-2026 16:00" + }, + { + "flight name": "Narita to Perth", + "departure airport": "Narita", + "departure date": "27-01-2027 23:00", + "arrival airport": "Perth", + "arrival date": "28-01-2027 07:00" + } + ], + "attractions": [{ + "attraction name": "Hike", + "address": "123 Hike Lane", + "summary": "A cool hike with good views", + "tag(s)": "outdoors" + }, + { + "attraction name": "Dinner spot", + "address": "49 Sweet Cove", + "summary": "A lovely dinner spot", + "tag(s)": "dinner, romantic" + } + ]} + + # Flight item to use for departure_date validation (expected to assertFalse) + invalid_departure_date = [{ + "flight name": "Perth to Narita", + "departure airport": "Perth", + "departure date": "08:00 12-12-2026", + "arrival airport": "Narita", + "arrival date": "12-12-2026 16:00" + }] + + invalid_arrival_date = [{ + "flight name": "Perth to Narita", + "departure airport": "Perth", + "departure date": "12-12-2026 08:00", + "arrival airport": "Narita", + "arrival date": "2026-12-12 16:00" + }] + + # Test start_date with reversed format ("YYYY-MM-DD") -> asserts False + invalid_start_date_result = add_itinerary(self.itineraries, name=test_itinerary["name"], location=test_itinerary["location"], description=test_itinerary["description"], start_date="2026-12-12", end_date=test_itinerary["end_date"], flights=test_itinerary["flights"], attractions=test_itinerary["attractions"]) + + # Test end_date with American format ("MM-DD-YYYY") -> asserts False + invalid_end_date_result = add_itinerary(self.itineraries, name=test_itinerary["name"], location=test_itinerary["location"], description=test_itinerary["description"], start_date=test_itinerary["start_date"], end_date="01-28-2027", flights=test_itinerary["flights"], attractions=test_itinerary["attractions"]) + + # Test invalid flight dates -> asserts False + invalid_departure_date_result = add_itinerary(self.itineraries, name=test_itinerary["name"], location=test_itinerary["location"], description=test_itinerary["description"], start_date=test_itinerary["start_date"], end_date=test_itinerary["end_date"], flights=invalid_departure_date, attractions=test_itinerary["attractions"]) + + invalid_arrival_date_result = add_itinerary(self.itineraries, name=test_itinerary["name"], location=test_itinerary["location"], description=test_itinerary["description"], start_date=test_itinerary["start_date"], end_date=test_itinerary["end_date"], flights=invalid_arrival_date, attractions=test_itinerary["attractions"]) + + self.assertFalse(invalid_start_date_result) + self.assertFalse(invalid_end_date_result) + self.assertFalse(invalid_departure_date_result) + self.assertFalse(invalid_arrival_date_result) + self.assertEqual(len(self.itineraries), 0) + + def test_edit_itinerary(self): + """ + Test editing an itinerary's items (uses two itineraries). + Verify that ALL types of items in a specific itinerary can be edited smoothly, which includes: + + new_name (edits name of trip) - covers string types that can be accessed in the first level of the itinerary dictionary, i.e. name, location & description. + new_start_date (edits trip start_date) - covers validation of a trip's start & end dates. + new_departure_airport (in flights list) - validates that departure & arrival airport is changed, and for the correct flight. + Note: the "flight name" value should ALSO change to reflect the new departure airport. + new_departure_date (in 'flights') - validates that departure & arrival date is validated by the validate_dates() function, then changed for the correct flight. + new_attraction_name (in 'attractions') - since each item in an attraction dictionary is a type of string and tested the same way, only one needs to be tested. + """ + test_Japan_itinerary = { + "name": "trip", "location": "Japan", "description": "description", "start_date": "12-12-2026", "end_date": "28-01-2027", "flights": [{ + "flight name": "Perth to Narita", + "departure airport": "Perth", + "departure date": "12-12-2026 08:00", + "arrival airport": "Narita", + "arrival date": "12-12-2026 16:00" + }, + { + "flight name": "Narita to Perth", + "departure airport": "Narita", + "departure date": "27-01-2027 23:00", + "arrival airport": "Perth", + "arrival date": "28-01-2027 07:00" + } + ], + "attractions": [{ + "attraction name": "Hike", + "address": "123 Hike Lane", + "summary": "A cool hike with good views", + "tag(s)": "outdoors" + }, + { + "attraction name": "Dinner spot", + "address": "49 Sweet Cove", + "summary": "A lovely dinner spot", + "tag(s)": "dinner, romantic" + } + ]} + + test_England_itinerary = { + "name": "England 2020", "location": "England", "description": "Trip to England in 2020", "start_date": "21-09-2020", "end_date": "04-10-2020", "flights": [{ + "flight name": "Perth to London", + "departure airport": "Perth", + "departure date": "21-09-2020 05:00", + "arrival airport": "London", + "arrival date": "21-09-2020 21:00" + }, + { + "flight name": "London to Perth", + "departure airport": "London", + "departure date": "04-10-2020 23:00", + "arrival airport": "Perth", + "arrival date": "05-10-2020 15:30" + } + ], + "attractions": [{ + "attraction name": "London Eye", + "address": "Somewhere in city", + "summary": "A glorified ferris wheel that shows the city surrounds", + "tag(s)": "view, relaxing" + }, + { + "attraction name": "Shakespeare's Globe", + "address": "12 address strees", + "summary": "A reconstructed theatre", + "tag(s)": "entertainment, history" + } + ]} + + new_name = "Japan 2026-27" + new_start_date = "27-12-2026" + new_departure_airport = "Sydney" + new_departure_date = "23-09-2020 13:30" + new_attraction_name = "London Theatre" + add_itinerary(self.itineraries, name=test_Japan_itinerary["name"], location=test_Japan_itinerary["location"], description=test_Japan_itinerary["description"], start_date=test_Japan_itinerary["start_date"], end_date=test_Japan_itinerary["end_date"], flights=test_Japan_itinerary["flights"], attractions=test_Japan_itinerary["attractions"]) + add_itinerary(self.itineraries, name=test_England_itinerary["name"], location=test_England_itinerary["location"], description=test_England_itinerary["description"], start_date=test_England_itinerary["start_date"], end_date=test_England_itinerary["end_date"], flights=test_England_itinerary["flights"], attractions=test_England_itinerary["attractions"]) + + edit_itinerary(self.itineraries, test_Japan_itinerary["name"], "name", "N/A", "N/A", new_name) + self.assertNotEqual(self.itineraries[0]["name"], test_Japan_itinerary["name"]) + self.assertEqual(self.itineraries[0]["name"], new_name) + + edit_itinerary(self.itineraries, self.itineraries[0]["name"], "start_date", "N/A", "N/A", new_start_date) + self.assertNotEqual(self.itineraries[0]["start_date"], test_Japan_itinerary["start_date"]) + self.assertEqual(self.itineraries[0]["start_date"], new_start_date) + + old_departure_airport = test_England_itinerary["flights"][0]["departure airport"] + old_flight_name = test_England_itinerary["flights"][0]["flight name"] + edit_itinerary(self.itineraries, self.itineraries[1]["name"], "departure airport", test_England_itinerary["flights"][0]["flight name"], "N/A", new_departure_airport) + self.assertNotEqual(self.itineraries[1]["flights"][0]["departure airport"], old_departure_airport) + self.assertNotEqual(self.itineraries[1]["flights"][0]["flight name"], old_flight_name) + self.assertEqual(self.itineraries[1]["flights"][0]["departure airport"], new_departure_airport) + + old_departure_date = test_England_itinerary["flights"][0]["departure date"] + edit_itinerary(self.itineraries, self.itineraries[1]["name"], "departure date", test_England_itinerary["flights"][0]["flight name"], "N/A", new_departure_date) + self.assertNotEqual(self.itineraries[1]["flights"][0]["departure date"], old_departure_date) + self.assertEqual(self.itineraries[1]["flights"][0]["departure date"], new_departure_date) + + old_attraction_name = test_England_itinerary["attractions"][1]["attraction name"] + edit_itinerary(self.itineraries, self.itineraries[1]["name"], "attraction name", "N/A", test_England_itinerary["attractions"][1]["attraction name"], new_attraction_name) + self.assertNotEqual(self.itineraries[1]["attractions"][1]["attraction name"], old_attraction_name) + self.assertEqual(self.itineraries[1]["attractions"][1]["attraction name"], new_attraction_name) + + + def test_add_new_flight(self): + """ + Docstring for test_add_new_flight. + + Verifies that: + 1. A new flight is added successfully. + 2. Number of flights has increased by 1 + 3. The last flight in the flight list is the added flight (function uses append() to add a new flight) + """ + test_Japan_itinerary = { + "name": "trip", "location": "Japan", "description": "description", "start_date": "12-12-2026", "end_date": "28-01-2027", "flights": [{ + "flight name": "Perth to Narita", + "departure airport": "Perth", + "departure date": "12-12-2026 08:00", + "arrival airport": "Narita", + "arrival date": "12-12-2026 16:00" + }, + { + "flight name": "Narita to Singapore", + "departure airport": "Narita", + "departure date": "27-01-2027 23:00", + "arrival airport": "Singapore", + "arrival date": "28-01-2027 03:00" + } + ], + "attractions": [{ + "attraction name": "Hike", + "address": "123 Hike Lane", + "summary": "A cool hike with good views", + "tag(s)": "outdoors" + }, + { + "attraction name": "Dinner spot", + "address": "49 Sweet Cove", + "summary": "A lovely dinner spot", + "tag(s)": "dinner, romantic" + } + ]} + add_itinerary(self.itineraries, name=test_Japan_itinerary["name"], location=test_Japan_itinerary["location"], description=test_Japan_itinerary["description"], start_date=test_Japan_itinerary["start_date"], end_date=test_Japan_itinerary["end_date"], flights=test_Japan_itinerary["flights"], attractions=test_Japan_itinerary["attractions"]) + initial_number_of_flights = len(test_Japan_itinerary["flights"]) + new_flight = [{ + "flight name": "Singapore to Perth", + "departure airport": "Singapore", + "departure date": "28-01-2027 04:30", + "arrival airport": "Perth", + "arrival date": "28-01-2027 09:45" + }] + + test_new_flight = add_new_flight(self.itineraries, test_Japan_itinerary["name"], new_flight) + added_flight = [self.itineraries[0]["flights"][-1]] + self.assertTrue(test_new_flight) + self.assertEqual(len(self.itineraries[0]["flights"]), (initial_number_of_flights + 1)) + self.assertEqual(added_flight, new_flight) + + + def test_add_new_attraction(self): + """ + Docstring for test_add_new_attraction. + + Verifies that: + 1. A new attraction is added successfully. + 2. Number of attractions has increased by 1 + 3. The last attraction in the attractions list is the added attraction (function uses append() to add a new attraction) + """ + test_Japan_itinerary = { + "name": "trip", "location": "Japan", "description": "description", "start_date": "12-12-2026", "end_date": "28-01-2027", "flights": [{ + "flight name": "Perth to Narita", + "departure airport": "Perth", + "departure date": "12-12-2026 08:00", + "arrival airport": "Narita", + "arrival date": "12-12-2026 16:00" + }, + { + "flight name": "Narita to Singapore", + "departure airport": "Narita", + "departure date": "27-01-2027 23:00", + "arrival airport": "Singapore", + "arrival date": "28-01-2027 03:00" + } + ], + "attractions": [{ + "attraction name": "Hike", + "address": "123 Hike Lane", + "summary": "A cool hike with good views", + "tag(s)": "outdoors" + }, + { + "attraction name": "Dinner spot", + "address": "49 Sweet Cove", + "summary": "A lovely dinner spot", + "tag(s)": "dinner, romantic" + } + ]} + add_itinerary(self.itineraries, name=test_Japan_itinerary["name"], location=test_Japan_itinerary["location"], description=test_Japan_itinerary["description"], start_date=test_Japan_itinerary["start_date"], end_date=test_Japan_itinerary["end_date"], flights=test_Japan_itinerary["flights"], attractions=test_Japan_itinerary["attractions"]) + initial_number_of_attractions = len(test_Japan_itinerary["attractions"]) + new_attraction = [{ + "attraction name": "Singapore to Perth", + "address": "Singapore", + "summary": "28-01-2027 04:30", + "tag(s)": "Perth" + }] + + test_new_attraction = add_new_attraction(self.itineraries, test_Japan_itinerary["name"], new_attraction) + added_attraction = [self.itineraries[0]["attractions"][-1]] + self.assertTrue(test_new_attraction) + self.assertEqual(len(self.itineraries[0]["attractions"]), (initial_number_of_attractions + 1)) + self.assertEqual(added_attraction, new_attraction) + + + def test_delete_itinerary(self): + """ + Test deleting a full itinerary by name. + Verify that: + 1. The itinerary is removed from the list of itineraries. + 2. The list size decreases. + 3. The itinerary left in the itineraries list is the one not chosen for deletion. + 4. The itinerary chosen for deletion is no longer in the saved itinerary list. + """ + test_Japan_itinerary = { + "name": "trip", "location": "Japan", "description": "description", "start_date": "12-12-2026", "end_date": "28-01-2027", "flights": [{ + "flight name": "Perth to Narita", + "departure airport": "Perth", + "departure date": "12-12-2026 08:00", + "arrival airport": "Narita", + "arrival date": "12-12-2026 16:00" + }, + { + "flight name": "Narita to Perth", + "departure airport": "Narita", + "departure date": "27-01-2027 23:00", + "arrival airport": "Perth", + "arrival date": "28-01-2027 07:00" + } + ], + "attractions": [{ + "attraction name": "Hike", + "address": "123 Hike Lane", + "summary": "A cool hike with good views", + "tag(s)": "outdoors" + }, + { + "attraction name": "Dinner spot", + "address": "49 Sweet Cove", + "summary": "A lovely dinner spot", + "tag(s)": "dinner, romantic" + } + ]} + + test_England_itinerary = { + "name": "England 2020", "location": "England", "description": "Trip to England in 2020", "start_date": "21-09-2020", "end_date": "04-10-2020", "flights": [{ + "flight name": "Perth to London", + "departure airport": "Perth", + "departure date": "21-09-2020 05:00", + "arrival airport": "London", + "arrival date": "21-09-2020 21:00" + }, + { + "flight name": "London to Perth", + "departure airport": "London", + "departure date": "04-10-2020 23:00", + "arrival airport": "Perth", + "arrival date": "05-10-2020 15:30" + } + ], + "attractions": [{ + "attraction name": "London Eye", + "address": "Somewhere in city", + "summary": "A glorified ferris wheel that shows the city surrounds", + "tag(s)": "view, relaxing" + }, + { + "attraction name": "Shakespeare's Globe", + "address": "12 address strees", + "summary": "A reconstructed theatre", + "tag(s)": "entertainment, history" + } + ]} + add_itinerary(self.itineraries, name=test_Japan_itinerary["name"], location=test_Japan_itinerary["location"], description=test_Japan_itinerary["description"], start_date=test_Japan_itinerary["start_date"], end_date=test_Japan_itinerary["end_date"], flights=test_Japan_itinerary["flights"], attractions=test_Japan_itinerary["attractions"]) + add_itinerary(self.itineraries, name=test_England_itinerary["name"], location=test_England_itinerary["location"], description=test_England_itinerary["description"], start_date=test_England_itinerary["start_date"], end_date=test_England_itinerary["end_date"], flights=test_England_itinerary["flights"], attractions=test_England_itinerary["attractions"]) + result = delete_itinerary(self.itineraries, test_Japan_itinerary["name"]) + + self.assertTrue(result) + self.assertEqual(len(self.itineraries), 1) + self.assertEqual(self.itineraries, [test_England_itinerary]) + self.assertNotEqual(self.itineraries, [test_Japan_itinerary]) + + + def test_delete_itinerary_items(self): + """ + Test deleting a full itinerary by name. + Verify that the itinerary is removed from the list of itineraries and the list size decreases. + """ + test_Japan_itinerary = { + "name": "trip", "location": "Japan", "description": "description", "start_date": "12-12-2026", "end_date": "28-01-2027", "flights": [{ + "flight name": "Perth to Narita", + "departure airport": "Perth", + "departure date": "12-12-2026 08:00", + "arrival airport": "Narita", + "arrival date": "12-12-2026 16:00" + }, + { + "flight name": "Narita to Perth", + "departure airport": "Narita", + "departure date": "27-01-2027 23:00", + "arrival airport": "Perth", + "arrival date": "28-01-2027 07:00" + } + ], + "attractions": [{ + "attraction name": "Hike", + "address": "123 Hike Lane", + "summary": "A cool hike with good views", + "tag(s)": "outdoors" + }, + { + "attraction name": "Dinner spot", + "address": "49 Sweet Cove", + "summary": "A lovely dinner spot", + "tag(s)": "dinner, romantic" + } + ]} + test_England_itinerary = { + "name": "England 2020", "location": "England", "description": "Trip to England in 2020", "start_date": "21-09-2020", "end_date": "04-10-2020", "flights": [{ + "flight name": "Perth to London", + "departure airport": "Perth", + "departure date": "21-09-2020 05:00", + "arrival airport": "London", + "arrival date": "21-09-2020 21:00" + }, + { + "flight name": "London to Perth", + "departure airport": "London", + "departure date": "04-10-2020 23:00", + "arrival airport": "Perth", + "arrival date": "05-10-2020 15:30" + } + ], + "attractions": [{ + "attraction name": "London Eye", + "address": "Somewhere in city", + "summary": "A glorified ferris wheel that shows the city surrounds", + "tag(s)": "view, relaxing" + }, + { + "attraction name": "Shakespeare's Globe", + "address": "12 address strees", + "summary": "A reconstructed theatre", + "tag(s)": "entertainment, history" + } + ]} + + original_itinerary_items = [] + original_itinerary_items.append([{ + "flight name": "Perth to Narita", + "departure airport": "Perth", + "departure date": "12-12-2026 08:00", + "arrival airport": "Narita", + "arrival date": "12-12-2026 16:00" + }, + { + "flight name": "Narita to Perth", + "departure airport": "Narita", + "departure date": "27-01-2027 23:00", + "arrival airport": "Perth", + "arrival date": "28-01-2027 07:00" + } + ]) + original_itinerary_items.append([{ + "attraction name": "London Eye", + "address": "Somewhere in city", + "summary": "A glorified ferris wheel that shows the city surrounds", + "tag(s)": "view, relaxing" + }, + { + "attraction name": "Shakespeare's Globe", + "address": "12 address strees", + "summary": "A reconstructed theatre", + "tag(s)": "entertainment, history" + } + ]) + + add_itinerary(self.itineraries, name=test_Japan_itinerary["name"], location=test_Japan_itinerary["location"], description=test_Japan_itinerary["description"], start_date=test_Japan_itinerary["start_date"], end_date=test_Japan_itinerary["end_date"], flights=test_Japan_itinerary["flights"], attractions=test_Japan_itinerary["attractions"]) + add_itinerary(self.itineraries, name=test_England_itinerary["name"], location=test_England_itinerary["location"], description=test_England_itinerary["description"], start_date=test_England_itinerary["start_date"], end_date=test_England_itinerary["end_date"], flights=test_England_itinerary["flights"], attractions=test_England_itinerary["attractions"]) + test_delete_flight = delete_itinerary_item(self.itineraries, 'flights', test_Japan_itinerary["name"], test_Japan_itinerary["flights"][1]["flight name"]) + test_delete_attraction = delete_itinerary_item(self.itineraries, 'attractions', test_England_itinerary["name"], test_England_itinerary["attractions"][0]["attraction name"]) + + self.assertTrue(test_delete_flight) + self.assertTrue(test_delete_attraction) + + self.assertNotEqual(self.itineraries[0]["flights"], original_itinerary_items[0]) + self.assertNotEqual(self.itineraries[1]["attractions"], original_itinerary_items[1]) + self.assertEqual(len(self.itineraries[0]["flights"]), len(original_itinerary_items[0]) - 1) + self.assertEqual(len(self.itineraries[1]["attractions"]), len(original_itinerary_items[1]) - 1) + + + def test_save_and_load_itineraries(self): + """ + + Test saving itineraries to a file and loading them back. + Verify that the saved itineraries are correctly loaded with the same data and format. + """ + test_Japan_itinerary = { + "name": "trip", "location": "Japan", "description": "description", "start_date": "12-12-2026", "end_date": "28-01-2027", "flights": [{ + "flight name": "Perth to Narita", + "departure airport": "Perth", + "departure date": "12-12-2026 08:00", + "arrival airport": "Narita", + "arrival date": "12-12-2026 16:00" + }, + { + "flight name": "Narita to Perth", + "departure airport": "Narita", + "departure date": "27-01-2027 23:00", + "arrival airport": "Perth", + "arrival date": "28-01-2027 07:00" + } + ], + "attractions": [{ + "attraction name": "Hike", + "address": "123 Hike Lane", + "summary": "A cool hike with good views", + "tag(s)": "outdoors" + }, + { + "attraction name": "Dinner spot", + "address": "49 Sweet Cove", + "summary": "A lovely dinner spot", + "tag(s)": "dinner, romantic" + } + ]} + + test_England_itinerary = { + "name": "England 2020", "location": "England", "description": "Trip to England in 2020", "start_date": "21-09-2020", "end_date": "04-10-2020", "flights": [{ + "flight name": "Perth to London", + "departure airport": "Perth", + "departure date": "21-09-2020 05:00", + "arrival airport": "London", + "arrival date": "21-09-2020 21:00" + }, + { + "flight name": "London to Perth", + "departure airport": "London", + "departure date": "04-10-2020 23:00", + "arrival airport": "Perth", + "arrival date": "05-10-2020 15:30" + } + ], + "attractions": [{ + "attraction name": "London Eye", + "address": "Somewhere in city", + "summary": "A glorified ferris wheel that shows the city surrounds", + "tag(s)": "view, relaxing" + }, + { + "attraction name": "Shakespeare's Globe", + "address": "12 address strees", + "summary": "A reconstructed theatre", + "tag(s)": "entertainment, history" + } + ]} + + itineraries_list = [test_Japan_itinerary, test_England_itinerary] + add_itinerary(self.itineraries, name=test_Japan_itinerary["name"], location=test_Japan_itinerary["location"], description=test_Japan_itinerary["description"], start_date=test_Japan_itinerary["start_date"], end_date=test_Japan_itinerary["end_date"], flights=test_Japan_itinerary["flights"], attractions=test_Japan_itinerary["attractions"]) + add_itinerary(self.itineraries, name=test_England_itinerary["name"], location=test_England_itinerary["location"], description=test_England_itinerary["description"], start_date=test_England_itinerary["start_date"], end_date=test_England_itinerary["end_date"], flights=test_England_itinerary["flights"], attractions=test_England_itinerary["attractions"]) + save_itineraries(self.itineraries) + loaded_itineraries = load_itineraries() + + self.assertEqual(len(loaded_itineraries), 2) + self.assertEqual(loaded_itineraries, itineraries_list) + + def test_rich_builtin_table(self): + """ + Test saves itineraries to the itinerary list and displays new formatted table using "rich" Table component. + Verify that using the view_itineraries() function prints a "rich" Table. + Verify that the terminal's output matches the itineraries saved in the list of itineraries. + + Documentation for capturing output made by rich: https://rich.readthedocs.io/en/latest/console.html#capturing-output + Extra resources: + - [QUESTION] How to test output of rich.Table? #247 https://github.com/Textualize/rich/issues/247 + """ + + # Test view_itineraries function to verify itineraries are saved + test_table = Table(title="Itineraries", show_lines=True) + + test_table.add_column("Trip Name", justify="center", no_wrap=True) + test_table.add_column("Location", justify="center", no_wrap=True) + test_table.add_column("Description", justify="left", no_wrap=False) + test_table.add_column("Start Date", justify="center", no_wrap=True) + test_table.add_column("End Date", justify="center", no_wrap=True) + test_table.add_column("Flights", justify="left", no_wrap=True) + test_table.add_column("Attractions", justify="left", no_wrap=True) + + test_Japan_itinerary = { + "name": "trip", "location": "Japan", "description": "description", "start_date": "12-12-2026", "end_date": "28-01-2027", "flights": [{ + "flight name": "Perth to Narita", + "departure airport": "Perth", + "departure date": "12-12-2026 08:00", + "arrival airport": "Narita", + "arrival date": "12-12-2026 16:00" + }, + { + "flight name": "Narita to Perth", + "departure airport": "Narita", + "departure date": "27-01-2027 23:00", + "arrival airport": "Perth", + "arrival date": "28-01-2027 07:00" + } + ], + "attractions": [{ + "attraction name": "Hike", + "address": "123 Hike Lane", + "summary": "A cool hike with good views", + "tag(s)": "outdoors" + }, + { + "attraction name": "Dinner spot", + "address": "49 Sweet Cove", + "summary": "A lovely dinner spot", + "tag(s)": "dinner, romantic" + } + ]} + + test_England_itinerary = { + "name": "England 2020", "location": "England", "description": "Trip to England in 2020", "start_date": "21-09-2020", "end_date": "04-10-2020", "flights": [{ + "flight name": "Perth to London", + "departure airport": "Perth", + "departure date": "21-09-2020 05:00", + "arrival airport": "London", + "arrival date": "21-09-2020 21:00" + }, + { + "flight name": "London to Perth", + "departure airport": "London", + "departure date": "04-10-2020 23:00", + "arrival airport": "Perth", + "arrival date": "05-10-2020 15:30" + } + ], + "attractions": [{ + "attraction name": "London Eye", + "address": "Somewhere in city", + "summary": "A glorified ferris wheel that shows the city surrounds", + "tag(s)": "view, relaxing" + }, + { + "attraction name": "Shakespeare's Globe", + "address": "12 address strees", + "summary": "A reconstructed theatre", + "tag(s)": "entertainment, history" + } + ]} + add_itinerary(self.itineraries, name=test_Japan_itinerary["name"], location=test_Japan_itinerary["location"], description=test_Japan_itinerary["description"], start_date=test_Japan_itinerary["start_date"], end_date=test_Japan_itinerary["end_date"], flights=test_Japan_itinerary["flights"], attractions=test_Japan_itinerary["attractions"]) + add_itinerary(self.itineraries, name=test_England_itinerary["name"], location=test_England_itinerary["location"], description=test_England_itinerary["description"], start_date=test_England_itinerary["start_date"], end_date=test_England_itinerary["end_date"], flights=test_England_itinerary["flights"], attractions=test_England_itinerary["attractions"]) + view_itineraries(self.itineraries, "All") + + # Test that the output printed in the terminal has the same format as the rich component's "Table" class + test_console = Console(file=io.StringIO()) + test_console.print(test_table) + + # Prints the same table as test_console.print(test_table) + print_table(self.itineraries) + + test_output = test_console.file.getvalue() + print(test_output) # prints table template for itinerary list + + self.assertIs(type(test_table), type(Table())) + self.assertIs(type(test_output), str) + + def test_pick_move_up_down_function(self): + """ + Test creates a variety of itinerary options and asserts that the current option highlighted (*not* entered) matches the picker's position in the "options" list. + Verifies moving up and down the list in a "Picker" screen works. + Documentation for capturing output made by pick: https://github.com/aisk/pick/blob/master/tests/test_pick.py + """ + title = "Please choose an itinerary: " + options = ["TBA", "this_itinerary", "that_itinerary"] + itinerary_picker = Picker(options, title) + assert itinerary_picker.get_selected() == ("TBA", 0) + itinerary_picker.move_up() + assert itinerary_picker.get_selected() == ("that_itinerary", 2) + itinerary_picker.move_down() + itinerary_picker.move_down() + assert itinerary_picker.get_selected() == ("this_itinerary", 1) + + +if __name__ == "__main__": + unittest.main()