From 4cf4d44d7e481a7defa14a0559464d454eb06d8c Mon Sep 17 00:00:00 2001 From: 20111596 <20111596@tafe.wa.edu.au> Date: Thu, 27 Nov 2025 15:27:37 +0800 Subject: [PATCH 01/23] Feat: Added main.py (runs Travel Itinerary Planner app) and README.md template. --- Travel Itinerary Planner/README.md | 37 ++++++++++++ Travel Itinerary Planner/main.py | 93 ++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 Travel Itinerary Planner/README.md create mode 100644 Travel Itinerary Planner/main.py diff --git a/Travel Itinerary Planner/README.md b/Travel Itinerary Planner/README.md new file mode 100644 index 0000000..0f3546c --- /dev/null +++ b/Travel Itinerary Planner/README.md @@ -0,0 +1,37 @@ +# Script Name +Short description of package/script +- If package, list of functionalities/scripts it can perform +- If standalone script, short description of script explaining what it achieves + +# Description +- If code is not explainable using comments, use this sections to explain your script + +# Prerequisites +- Python 3.14 +- pip (Python package installer) +- Git Bash +- (List out the libraries imported in the script) + +# 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 using + ```bash + python3 main.py + ``` + +# Screenshot +- Display images/gifs/videos of output/result of your script so that users can visualize it. + +# Author +Gabrielle Allan diff --git a/Travel Itinerary Planner/main.py b/Travel Itinerary Planner/main.py new file mode 100644 index 0000000..08b5bae --- /dev/null +++ b/Travel Itinerary Planner/main.py @@ -0,0 +1,93 @@ +from src.manage_itineraries import add_itinerary, edit_itinerary, view_itineraries, delete_itinerary, export_itinerary +from pick import pick + + +def user_task_request(request, itinerary_list): + ''' + Executes a specific request based on user's choice. + ''' + # Add a new itinerary + if request == "1": + flights_list_done = False + attractions_list_done = False + print("How exciting! Please provide us information about the trip: \n") + name = input("Title of the itinerary: ") + location = input("Location (NA if not applicable): ") + summary = input("Brief description of trip: ") + start_date = input("Start date in DD-MM-YYYY: ") + end_date = input("End date in DD-MM-YYYY: ") + while not flights_list_done: + # Answer format: Perth-Sydney 12-12-2012, Sydney-Perth 21-12-2012 + flights = input("") # dictionary - add loop here + if flights == "DONE": + flights_list_done = True + while not attractions_list_done: + attractions = input("") # dictionary - add loop here + if attractions == "DONE": + attractions_list_done = True + add_itinerary(itinerary_list, name, location, summary, start_date, end_date, flights, attractions) + pass + + # Edit existing itinerary + elif request == "2": + # Use pickpack module (https://github.com/anafvana/pickpack#map-function-for-nested-lists) + pass + + # View itinerary + elif request == "3": + view_itineraries(itinerary_list) + pass + + # Delete itinerary + elif request == "4": + delete_itinerary(itinerary_list) + pass + + elif request == "5": + # Export to .csv file? + # Use pick (not pickpack) library to list itineraries by name and location + export_itinerary(itinerary_list) + pass + + else: + print("Invalid answer, please type a number between 1-5.") + pass + + +def run_app(): + """ + Main loop to run the travel planner. + """ + 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") + print("3. View existing Itineraries") + print("4. Delete an Itinerary") + print("5. Export an Itinerary") + print("6. Log out\n") + + user_choice = input("Please select one of the above options (1-5): ") + + # Check user_choice was a number + if user_choice.isdigit(): + choice = int(user_choice) + if 1 <= choice <= 6: + if user_choice == "6": + app_running = False + else: + user_task_request(user_choice, itineraries) + else: + print("Enter a number between 1-6.") + else: + ("Invalid input: Please enter a number between 1-6.") + + print("See you next time! 👋") + pass + + +if __name__ == "__main__": + run_app() From 55d26c9a3c783ad0fbab8492a554948340f4a6d9 Mon Sep 17 00:00:00 2001 From: 20111596 <20111596@tafe.wa.edu.au> Date: Thu, 27 Nov 2025 20:39:02 +0800 Subject: [PATCH 02/23] Feat: Added Itinerary class (itinerary.py) and itinerary serialization (file_handler.py). --- Travel Itinerary Planner/src/file_handler.py | 35 +++++++++ Travel Itinerary Planner/src/itinerary.py | 75 ++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 Travel Itinerary Planner/src/file_handler.py create mode 100644 Travel Itinerary Planner/src/itinerary.py diff --git a/Travel Itinerary Planner/src/file_handler.py b/Travel Itinerary Planner/src/file_handler.py new file mode 100644 index 0000000..a971cca --- /dev/null +++ b/Travel Itinerary Planner/src/file_handler.py @@ -0,0 +1,35 @@ +import pickle +import os +from pprint import pprint +from src.itinerary import Itinerary + +ITINERARY_FILE = "itineraries.bin" + + +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(ITINERARY_FILE): + 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(ITINERARY_FILE, "wb") as file: + pickle.dump(itineraries, file) diff --git a/Travel Itinerary Planner/src/itinerary.py b/Travel Itinerary Planner/src/itinerary.py new file mode 100644 index 0000000..d6ebc5c --- /dev/null +++ b/Travel Itinerary Planner/src/itinerary.py @@ -0,0 +1,75 @@ +class Itinerary: + """ + This class represents a single itinerary object that users can create. + + Args: + name (str): The name assigned to the itinerary. + location (str): The main city/country the holiday takes place. + summary (str, optional): A brief summary of the travel plan. + start_date (str): The date the holiday begins in 'DD-MM-YYYY' format. + end_date (str): The date the holiday ends in 'DD-MM-YYYY' format. + flights (dict): A nested dictionary type containing flights and flight details: + flight_name (key, dict type): Contains the flight name (departure-arrival location format, e.g. 'Perth to Sydney') + departure_airport (str): Departure airport name. + departure_date (datetime): Date & time of flight departure in 'DD-MM-YYYY HH:MM' format. + arrival_airport (str): Arrival airport name. + arrival_date (datetime): Date & time of flight arrival in 'DD-MM-YYYY HH:MM' format. + attractions (dict): Nested dictionary of attractions. Each dictionary key (name of attraction) contains: + attraction_name (str, dict): Name of attraction (key) and a dictionary containing attraction details: + address (str): Address of attraction. + attraction_summary(str): Short description of attraction. + attraction_tags (list): List of tags (str type) that catergorise the attraction. + """ + + def __init__(self, name, location, summary, start_date, end_date, flights, attractions): + self.name = name + self.location = location + self.summary = summary + self.start_date = start_date + self.end_date = end_date + self.flights = flights + self.attractions = attractions + pass + + def to_dict(self): + """ + Converts the Itinerary object into a dictionary. + + Returns: + dict: A dictionary containing itinerary details with these keys: + 'name', 'location', 'summary', 'start-date', 'end-date', 'flights', and 'attractions'. + """ + return { + "name": self.name, + "location": self.location, + "summary": self.summary, + "start_date": self.start_date, + "end_date": self.end_date, + "flights": self.flights, + "attractions": self.attractions + } + + @staticmethod + def from_dict(itinerary_info): + """ + Creates an Itinerary object from the dictionary representation. + + Args: + itinerary_info (dict): A dictionary containing itinerary info with these keys: + 'name', 'location', 'summary', 'start-date', 'end-date', 'flights', and 'attractions'. + + Returns: + Itinerary: A new Itinerary object created from the dictionary data. + """ + # TODO: Re-write to accommodate the dictionary type, and load its attributes according to their type. + # Reference that might help: https://stackoverflow.com/questions/56640436/how-to-generically-serialize-and-de-serialize-objects-from-dictionaries + + return Itinerary( + itinerary_info["name"], + itinerary_info["location"], + itinerary_info["summary"], + itinerary_info["start-date"], + itinerary_info["end_date"], + itinerary_info["flights"], + itinerary_info["attractions"] + ) From f3354be8921fdb6080ac80118bcf85787d3d26f0 Mon Sep 17 00:00:00 2001 From: 20111596 <20111596@tafe.wa.edu.au> Date: Thu, 27 Nov 2025 20:42:17 +0800 Subject: [PATCH 03/23] Feat: Added add_itinerary functionality to main program (main.py) and function handler (manage_itineraries.py). Also added requirements.txt for pip install. --- Travel Itinerary Planner/main.py | 101 +++++++++++++++--- Travel Itinerary Planner/requirements.txt | 3 + .../src/manage_itineraries.py | 98 +++++++++++++++++ 3 files changed, 185 insertions(+), 17 deletions(-) create mode 100644 Travel Itinerary Planner/requirements.txt create mode 100644 Travel Itinerary Planner/src/manage_itineraries.py diff --git a/Travel Itinerary Planner/main.py b/Travel Itinerary Planner/main.py index 08b5bae..c6b9a9b 100644 --- a/Travel Itinerary Planner/main.py +++ b/Travel Itinerary Planner/main.py @@ -10,48 +10,115 @@ def user_task_request(request, itinerary_list): if request == "1": flights_list_done = False attractions_list_done = False + print("How exciting! Please provide us information about the trip: \n") name = input("Title of the itinerary: ") location = input("Location (NA if not applicable): ") summary = input("Brief description of trip: ") start_date = input("Start date in DD-MM-YYYY: ") end_date = input("End date in DD-MM-YYYY: ") - while not flights_list_done: - # Answer format: Perth-Sydney 12-12-2012, Sydney-Perth 21-12-2012 - flights = input("") # dictionary - add loop here - if flights == "DONE": - flights_list_done = True - while not attractions_list_done: - attractions = input("") # dictionary - add loop here - if attractions == "DONE": - attractions_list_done = True + 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: + flight_details = {} + + 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}" + + flight_details.update({ + "departure airport": departure_airport, + "departure date": departure_date, + "arrival airport": arrival_airport, + "arrival date": arrival_date + }) + flights[flight_name] = flight_details + + add_another_flight = input("Would you like to add another flight? Type Y (yes) or N (no): ") + while True: + if add_another_flight == "Y": + flights_list_done = True + break + elif add_another_flight == "N": + flights_list_done = False + 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_details = {} + + attraction_name = input("Name of attraction: ") + attraction_address = input("Address of attraction: ") + attraction_summary = input("Short description of attraction: ") + attraction_type = input("(Optional) Provide some tags that categorise what kind of activity this involves.\nExample of format required: hike, exciting, views\n") + attraction_tags = attraction_type.split(", ") + + attraction_details.update({ + "address": attraction_address, + "summary": attraction_summary, + "tag(s)": attraction_tags + }) + attractions[attraction_name] = attraction_details + + add_another_attraction = input("Would you like to add another attraction? Type Y or N:") + while True: + if add_another_attraction == "Y": + attractions_list_done = True + break + elif add_another_attraction == "N": + attractions_list_done = False + break + else: + print("Invalid answer: Please type Y or N only.") + add_itinerary(itinerary_list, name, location, summary, start_date, end_date, flights, attractions) - pass # Edit existing itinerary elif request == "2": # Use pickpack module (https://github.com/anafvana/pickpack#map-function-for-nested-lists) + chosen_itinerary = input("") + edit_itinerary(itinerary_list, chosen_itinerary) pass - + # View itinerary elif request == "3": - view_itineraries(itinerary_list) + chosen_itinerary = input("") + view_itineraries(itinerary_list, chosen_itinerary) pass # Delete itinerary elif request == "4": - delete_itinerary(itinerary_list) + chosen_itinerary = input("") + delete_itinerary(itinerary_list, chosen_itinerary) pass + # Export itinerary elif request == "5": - # Export to .csv file? - # Use pick (not pickpack) library to list itineraries by name and location - export_itinerary(itinerary_list) + chosen_itinerary = input("") + export_itinerary(itinerary_list, chosen_itinerary) pass else: print("Invalid answer, please type a number between 1-5.") - pass + return def run_app(): diff --git a/Travel Itinerary Planner/requirements.txt b/Travel Itinerary Planner/requirements.txt new file mode 100644 index 0000000..d80dc8e --- /dev/null +++ b/Travel Itinerary Planner/requirements.txt @@ -0,0 +1,3 @@ +flake8==7.3.0 +pick==v2.4.0 +pickpack==2.0.0 diff --git a/Travel Itinerary Planner/src/manage_itineraries.py b/Travel Itinerary Planner/src/manage_itineraries.py new file mode 100644 index 0000000..a654f82 --- /dev/null +++ b/Travel Itinerary Planner/src/manage_itineraries.py @@ -0,0 +1,98 @@ +from src.itinerary import Itinerary +from datetime import datetime + +""" +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, summary, start_date, end_date, flights, attractions): + """ + Add a new itinerary to the list of itineraries. + + Args: + itinerary_list (list): The list of existing Itinerary objects. + location (str): The main city/country the holiday takes place. + summary (str, optional): A brief summary of the travel plan. + start_date (str): The date the holiday begins in 'DD-MM-YYYY' format. + end_date (str): The date the holiday ends in 'DD-MM-YYYY' format. + flights (nested dict): A nested dictionary type. Flight name (before-after location format, e.g. Perth-Sydney) is tied to a date in 'DD-MM-YYYY' format. + attractions (nested dict): Dictionary of attractions. Each dictionary key (name of attraction) contains a short description of the attraction (object). + + Returns: + bool: True if Itinerary is added without issue, otherwise False. + + Raises: + ValueError: If the start or end date is not in the correct format. + + Side Effects: + - Saves the updated itinerary list to a file using `update_itinerary`. + """ + # Prevent duplicate itineraries + if any(trip.name == name for trip in itinerary_list): + print("Error: A trip with this name already exists!") + return False + + if not validate_dates(start_date, end_date, flights): + return False + + # Add the new itinerary after validation checks + itinerary_list.append(Itinerary(name, location, summary, start_date, end_date, flights, attractions)) + + +def edit_itinerary(itinerary_list, chosen_itinerary): + # Use pickpack module (https://github.com/anafvana/pickpack#map-function-for-nested-lists) + pass + + +def view_itineraries(itinerary_list, chosen_itinerary): + if not itinerary_list: + print("No itineraries planned!") + else: + # Use pick module: Ask if they would like to view all itineraries, or a specific one + pass + return + + +def delete_itinerary(itinerary_list, chosen_itinerary): + print("What would you like to delete?") + # Use pickpack module (https://github.com/anafvana/pickpack#map-function-for-nested-lists) + pass + + +def export_itinerary(itinerary, chosen_itinerary): + # Export to .pdf or .csv file + # Use pick (not pickpack) library to list itineraries by name and location + pass + + +# Validation functions + +def validate_dates(start_date, end_date, flights): + # Validate dates given for start date, end date and flight datetimes + 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_name, flight_info in flights.items(): + try: + datetime.strptime(flight_info["departure_time"], "%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_time"], "%d-%m-%Y %H:%M") + except ValueError: + print("Error: Invalid date/time for flight arrival. Use 'DD-MM-YYYY HH:MM' format.") + return False From bf66474fd953a1cfe8d6799dc46b5fe7b8e4f303 Mon Sep 17 00:00:00 2001 From: 20111596 <20111596@tafe.wa.edu.au> Date: Thu, 27 Nov 2025 20:44:40 +0800 Subject: [PATCH 04/23] Feat: Added empty unittest file for manage_itineraries.py file. --- Travel Itinerary Planner/tests/test_manage_itineraries.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 Travel Itinerary Planner/tests/test_manage_itineraries.py 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..20f944d --- /dev/null +++ b/Travel Itinerary Planner/tests/test_manage_itineraries.py @@ -0,0 +1,6 @@ +# import unittest +""" +Tests the functions in manage_itineraries.py + +""" +pass From f836acd75e8e059943762b28f592379b69eda432 Mon Sep 17 00:00:00 2001 From: 20111596 <20111596@tafe.wa.edu.au> Date: Thu, 27 Nov 2025 20:59:12 +0800 Subject: [PATCH 05/23] Chore: Added docstring for validation function & minor editing. --- .../src/manage_itineraries.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/Travel Itinerary Planner/src/manage_itineraries.py b/Travel Itinerary Planner/src/manage_itineraries.py index a654f82..56ed93f 100644 --- a/Travel Itinerary Planner/src/manage_itineraries.py +++ b/Travel Itinerary Planner/src/manage_itineraries.py @@ -8,7 +8,7 @@ def add_itinerary(itinerary_list, name, location, summary, start_date, end_date, flights, attractions): - """ + ''' Add a new itinerary to the list of itineraries. Args: @@ -23,12 +23,10 @@ def add_itinerary(itinerary_list, name, location, summary, start_date, end_date, Returns: bool: True if Itinerary is added without issue, otherwise False. - Raises: - ValueError: If the start or end date is not in the correct format. - Side Effects: - Saves the updated itinerary list to a file using `update_itinerary`. - """ + ''' + # Prevent duplicate itineraries if any(trip.name == name for trip in itinerary_list): print("Error: A trip with this name already exists!") @@ -70,7 +68,17 @@ def export_itinerary(itinerary, chosen_itinerary): # Validation functions def validate_dates(start_date, end_date, flights): - # Validate dates given for start date, end date and flight datetimes + ''' + Validate dates given for start date, end date and flight datetimes + + Raises: + ValueError: If the start or end date is not in the correct format. + + :param start_date (DD-MM-YYYY): Itinerary start date. + :param end_date (DD-MM-YYYY): Itinerary start date. + :param flights (nested dict): 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: From 00864d29c8d17ca30a795a194fbe5240724e7b5e Mon Sep 17 00:00:00 2001 From: 20111596 <20111596@tafe.wa.edu.au> Date: Sun, 30 Nov 2025 13:03:32 +0800 Subject: [PATCH 06/23] Feat: add_itinerary feature completed --- Travel Itinerary Planner/main.py | 256 ++++++++---------- Travel Itinerary Planner/requirements.txt | 3 +- Travel Itinerary Planner/src/file_handler.py | 13 +- .../src/manage_itineraries.py | 61 +++-- 4 files changed, 169 insertions(+), 164 deletions(-) diff --git a/Travel Itinerary Planner/main.py b/Travel Itinerary Planner/main.py index c6b9a9b..0b6f30c 100644 --- a/Travel Itinerary Planner/main.py +++ b/Travel Itinerary Planner/main.py @@ -1,131 +1,12 @@ -from src.manage_itineraries import add_itinerary, edit_itinerary, view_itineraries, delete_itinerary, export_itinerary -from pick import pick - - -def user_task_request(request, itinerary_list): - ''' - Executes a specific request based on user's choice. - ''' - # Add a new itinerary - if request == "1": - flights_list_done = False - attractions_list_done = False - - print("How exciting! Please provide us information about the trip: \n") - name = input("Title of the itinerary: ") - location = input("Location (NA if not applicable): ") - summary = 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: - flight_details = {} - - 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}" - - flight_details.update({ - "departure airport": departure_airport, - "departure date": departure_date, - "arrival airport": arrival_airport, - "arrival date": arrival_date - }) - flights[flight_name] = flight_details - - add_another_flight = input("Would you like to add another flight? Type Y (yes) or N (no): ") - while True: - if add_another_flight == "Y": - flights_list_done = True - break - elif add_another_flight == "N": - flights_list_done = False - 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_details = {} - - attraction_name = input("Name of attraction: ") - attraction_address = input("Address of attraction: ") - attraction_summary = input("Short description of attraction: ") - attraction_type = input("(Optional) Provide some tags that categorise what kind of activity this involves.\nExample of format required: hike, exciting, views\n") - attraction_tags = attraction_type.split(", ") - - attraction_details.update({ - "address": attraction_address, - "summary": attraction_summary, - "tag(s)": attraction_tags - }) - attractions[attraction_name] = attraction_details - - add_another_attraction = input("Would you like to add another attraction? Type Y or N:") - while True: - if add_another_attraction == "Y": - attractions_list_done = True - break - elif add_another_attraction == "N": - attractions_list_done = False - break - else: - print("Invalid answer: Please type Y or N only.") - - add_itinerary(itinerary_list, name, location, summary, start_date, end_date, flights, attractions) - - # Edit existing itinerary - elif request == "2": - # Use pickpack module (https://github.com/anafvana/pickpack#map-function-for-nested-lists) - chosen_itinerary = input("") - edit_itinerary(itinerary_list, chosen_itinerary) - pass - - # View itinerary - elif request == "3": - chosen_itinerary = input("") - view_itineraries(itinerary_list, chosen_itinerary) - pass - - # Delete itinerary - elif request == "4": - chosen_itinerary = input("") - delete_itinerary(itinerary_list, chosen_itinerary) - pass - - # Export itinerary - elif request == "5": - chosen_itinerary = input("") - export_itinerary(itinerary_list, chosen_itinerary) - pass - - else: - print("Invalid answer, please type a number between 1-5.") - return +from src.file_handler import load_itineraries +from src.manage_itineraries import add_itinerary, edit_itinerary, view_itineraries, delete_itinerary def run_app(): """ Main loop to run the travel planner. """ - itineraries = [] + itineraries = load_itineraries() app_running = True while app_running: @@ -134,26 +15,129 @@ def run_app(): print("2. Edit an existing Itinerary") print("3. View existing Itineraries") print("4. Delete an Itinerary") - print("5. Export an Itinerary") - print("6. Log out\n") - - user_choice = input("Please select one of the above options (1-5): ") + print("5. Log out\n") # Check user_choice was a number - if user_choice.isdigit(): - choice = int(user_choice) - if 1 <= choice <= 6: - if user_choice == "6": - app_running = False + while True: + user_choice = input("Please select one of the above options (1-5): ") + if user_choice.isdigit(): + choice = int(user_choice) + if 1 <= choice <= 5: + break else: - user_task_request(user_choice, itineraries) + print("Enter a number between 1-5.") + else: + ("Invalid input: Please enter a number between 1-5.") + + # Add a new itinerary + if user_choice == "1": + flights_list_done = False + attractions_list_done = False + + print("How exciting! Please provide us information about the trip: \n") + name = input("Title of the itinerary: ") + location = input("Location (NA if not applicable): ") + summary = 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: + flight_details = {} + + 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}" + + flight_details.update({ + "departure airport": departure_airport, + "departure date": departure_date, + "arrival airport": arrival_airport, + "arrival date": arrival_date + }) + flights[flight_name] = flight_details + + 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: - print("Enter a number between 1-6.") - else: - ("Invalid input: Please enter a number between 1-6.") + while not attractions_list_done: + attraction_details = {} + + attraction_name = input("Name of attraction: ") + attraction_address = input("Address of attraction: ") + attraction_summary = input("Short description of attraction: ") + attraction_type = input("(Optional) Provide some tags that categorise what kind of activity this involves.\nExample of format required: hike, exciting, views\n") + attraction_tags = attraction_type.split(", ") + + attraction_details.update({ + "address": attraction_address, + "summary": attraction_summary, + "tag(s)": attraction_tags + }) + attractions[attraction_name] = attraction_details + + 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, summary, start_date, end_date, flights, attractions) + + # Edit existing itinerary + elif user_choice == "2": + # Use pickpack module (https://github.com/anafvana/pickpack#map-function-for-nested-lists) + chosen_itinerary = input("") + edit_itinerary(chosen_itinerary) + pass + + # View itinerary + elif user_choice == "3": + chosen_itinerary = input("Please choose a trip") + view_itineraries(itineraries, chosen_itinerary) + pass + + # Delete itinerary + elif user_choice == "4": + chosen_itinerary = input("") + delete_itinerary(chosen_itinerary) + pass + + # Log out + elif user_choice == "5": + app_running = False print("See you next time! 👋") - pass if __name__ == "__main__": diff --git a/Travel Itinerary Planner/requirements.txt b/Travel Itinerary Planner/requirements.txt index d80dc8e..b0839cb 100644 --- a/Travel Itinerary Planner/requirements.txt +++ b/Travel Itinerary Planner/requirements.txt @@ -1,3 +1,4 @@ flake8==7.3.0 +anytree==2.13.0 pick==v2.4.0 -pickpack==2.0.0 +pickpack==2.0.0 \ No newline at end of file diff --git a/Travel Itinerary Planner/src/file_handler.py b/Travel Itinerary Planner/src/file_handler.py index a971cca..48715c4 100644 --- a/Travel Itinerary Planner/src/file_handler.py +++ b/Travel Itinerary Planner/src/file_handler.py @@ -1,10 +1,11 @@ -import pickle import os +import pickle from pprint import pprint -from src.itinerary import Itinerary 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(): """ @@ -14,10 +15,10 @@ def load_itineraries(): 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(ITINERARY_FILE): + if os.path.exists(current_directory): with open(ITINERARY_FILE, "rb") as file: return pickle.load(file) - return {} + return [] def save_itineraries(itineraries): @@ -31,5 +32,5 @@ def save_itineraries(itineraries): - Writes the serialized itinerary dictionary to ITINERARY_FILE. - Overwrites the file if it already exists. """ - with open(ITINERARY_FILE, "wb") as file: + 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 index 56ed93f..5ae1a90 100644 --- a/Travel Itinerary Planner/src/manage_itineraries.py +++ b/Travel Itinerary Planner/src/manage_itineraries.py @@ -1,5 +1,10 @@ -from src.itinerary import Itinerary +from src.file_handler import load_itineraries, save_itineraries from datetime import datetime +from pick import pick +from anytree import Node, RenderTree +from pickpack import pickpack +from rich.console import Console +from rich.table import Table """ This file contains all of the functions needed for the Travel Itinerary Planner. @@ -9,30 +14,42 @@ def add_itinerary(itinerary_list, name, location, summary, start_date, end_date, flights, attractions): ''' - Add a new itinerary to the list of itineraries. - - Args: - itinerary_list (list): The list of existing Itinerary objects. - location (str): The main city/country the holiday takes place. - summary (str, optional): A brief summary of the travel plan. - start_date (str): The date the holiday begins in 'DD-MM-YYYY' format. - end_date (str): The date the holiday ends in 'DD-MM-YYYY' format. - flights (nested dict): A nested dictionary type. Flight name (before-after location format, e.g. Perth-Sydney) is tied to a date in 'DD-MM-YYYY' format. - attractions (nested dict): Dictionary of attractions. Each dictionary key (name of attraction) contains a short description of the attraction (object). + 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 `update_itinerary`. + - Saves the updated itinerary list to a file using `save_itineraries`. ''' - - # Prevent duplicate itineraries - if any(trip.name == name for trip in itinerary_list): - print("Error: A trip with this name already exists!") - return False - - if not validate_dates(start_date, end_date, flights): + # TEST: check that all items have been transferred correctly + print(name) + print(location) + print(summary) + print(start_date) + print(end_date) + print(flights.items()) + print(attractions.items()) + + if validate_dates(start_date, end_date, flights): + # Add the new itinerary after validation checks + new_itinerary = { + "name": name, + "location": location, + "summary": summary, + "start_date": start_date, + "end_date": end_date, + "flights": flights, + "attractions": attractions + } + print(new_itinerary) + + itinerary_list.append(new_itinerary) + print(itinerary_list) + + save_itineraries(itinerary_list) + return True + else: return False # Add the new itinerary after validation checks @@ -94,13 +111,15 @@ def validate_dates(start_date, end_date, flights): # Resource used for following code: https://stackoverflow.com/questions/17322208/multiple-try-codes-in-one-block for flight_name, flight_info in flights.items(): try: - datetime.strptime(flight_info["departure_time"], "%d-%m-%Y %H:%M") + 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_time"], "%d-%m-%Y %H:%M") + 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 From 5dcea41f7469e358896314917d2d29e85d307218 Mon Sep 17 00:00:00 2001 From: 20111596 <20111596@tafe.wa.edu.au> Date: Sun, 30 Nov 2025 16:40:08 +0800 Subject: [PATCH 07/23] Feat: view_itinerary feature added and works successfully. --- Travel Itinerary Planner/main.py | 11 +- Travel Itinerary Planner/src/itinerary.py | 2 +- .../src/manage_itineraries.py | 101 +++++++++++++++--- 3 files changed, 89 insertions(+), 25 deletions(-) diff --git a/Travel Itinerary Planner/main.py b/Travel Itinerary Planner/main.py index 0b6f30c..ca9f952 100644 --- a/Travel Itinerary Planner/main.py +++ b/Travel Itinerary Planner/main.py @@ -28,14 +28,11 @@ def run_app(): print("Enter a number between 1-5.") else: ("Invalid input: Please enter a number between 1-5.") - + # Add a new itinerary if user_choice == "1": - flights_list_done = False - attractions_list_done = False - print("How exciting! Please provide us information about the trip: \n") - name = input("Title of the itinerary: ") + name = input("Name of the itinerary (please choose a name you will remember for later): ") location = input("Location (NA if not applicable): ") summary = input("Brief description of trip: ") start_date = input("Start date in DD-MM-YYYY: ") @@ -123,9 +120,7 @@ def run_app(): # View itinerary elif user_choice == "3": - chosen_itinerary = input("Please choose a trip") - view_itineraries(itineraries, chosen_itinerary) - pass + view_itineraries(itineraries) # Delete itinerary elif user_choice == "4": diff --git a/Travel Itinerary Planner/src/itinerary.py b/Travel Itinerary Planner/src/itinerary.py index d6ebc5c..7dd8625 100644 --- a/Travel Itinerary Planner/src/itinerary.py +++ b/Travel Itinerary Planner/src/itinerary.py @@ -27,7 +27,7 @@ def __init__(self, name, location, summary, start_date, end_date, flights, attra self.summary = summary self.start_date = start_date self.end_date = end_date - self.flights = flights + self.flights = flights # https://docs.python.org/3/tutorial/classes.html#class-and-instance-variables self.attractions = attractions pass diff --git a/Travel Itinerary Planner/src/manage_itineraries.py b/Travel Itinerary Planner/src/manage_itineraries.py index 5ae1a90..9fc6ff3 100644 --- a/Travel Itinerary Planner/src/manage_itineraries.py +++ b/Travel Itinerary Planner/src/manage_itineraries.py @@ -5,7 +5,7 @@ from pickpack import pickpack 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. @@ -52,34 +52,90 @@ def add_itinerary(itinerary_list, name, location, summary, start_date, end_date, else: return False - # Add the new itinerary after validation checks - itinerary_list.append(Itinerary(name, location, summary, start_date, end_date, flights, attractions)) - -def edit_itinerary(itinerary_list, chosen_itinerary): +def edit_itinerary(chosen_itinerary): # Use pickpack module (https://github.com/anafvana/pickpack#map-function-for-nested-lists) pass -def view_itineraries(itinerary_list, chosen_itinerary): - if not itinerary_list: - print("No itineraries planned!") +def view_itineraries(itinerary_list): + # 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}") + # Use rich to print table + print_table(itinerary_list) + 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 itinerary_list: + itinerary_options.append(trip["name"]) + filter_option, filter_index = pick(itinerary_options, itinerary_choice) + + # 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 pick module: Ask if they would like to view all itineraries, or a specific one - pass - return + print("Error: Valid filter not selected, returning to Task Manager menu.\n") + return False + return True + +def delete_itinerary(chosen_itinerary): + itinerary_list = load_itineraries() -def delete_itinerary(itinerary_list, chosen_itinerary): print("What would you like to delete?") # Use pickpack module (https://github.com/anafvana/pickpack#map-function-for-nested-lists) pass -def export_itinerary(itinerary, chosen_itinerary): - # Export to .pdf or .csv file - # Use pick (not pickpack) library to list itineraries by name and location - pass +# Print functions +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") + trip_table.add_column("Summary", justify="center") + 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") + + for trip in trips: + for key in trip["flights"]: + if len(key) == 1: + departure_flight_name = key + flight_departure = trip["flights"][f"{departure_flight_name}"]["departure date"] + flight_list = f'[bold red]{departure_flight_name}[/bold red]: {flight_departure}\n' + elif len(key) >= 2: + flight_list = '' + departure_flight_name = key + flight_departure = trip["flights"][f"{departure_flight_name}"]["departure date"] + flight_item = f'[bold red]{departure_flight_name}[/bold red]: {flight_departure}\n' + flight_list = flight_list + f'{flight_item}' + for key in trip["attractions"]: + if len(key) == 1: + attraction = key + attraction_list = f'[bold red]{attraction}[/bold red] \nAddress: {trip["attractions"][f"{attraction}"]["address"]} \nSummary: {trip["attractions"][f"{attraction}"]["summary"]} \nTag(s): {trip["attractions"][f"{attraction}"]["tag(s)"]}\n' + elif len(key) >= 2: + attraction_list = '' + attraction = key + # Printing python text with colour using ANSI codes: https://vascosim.medium.com/how-to-print-colored-text-in-python-52f6244e2e30 + attraction_item = f'[bold red]{attraction}[/bold red] \nAddress: {trip["attractions"][f"{attraction}"]["address"]} \nSummary: {trip["attractions"][f"{attraction}"]["summary"]} \nTag(s): {trip["attractions"][f"{attraction}"]["tag(s)"]}\n' + attraction_list = attraction_list + f'{attraction_item}' + trip_table.add_row(trip["name"], trip["location"], trip["summary"], trip["start_date"], trip["end_date"], flight_list, attraction_list) + trip_console.print(trip_table) + return # Validation functions @@ -123,3 +179,16 @@ def validate_dates(start_date, end_date, flights): return False return True + + +def find_itinerary(itinerary_list, trip_name): + ''' + + Args: + itinerary_list: List of all itineraries saved to itinerary.bin file. + trip_name: Name of the trip user wants to view. + + Returns: + itinerary: The itinerary the user wants to view. + ''' + return [itinerary for itinerary in itinerary_list if itinerary["name"] == trip_name] From aa82bf99e13d675d8962bbf343e6c5051ac23e75 Mon Sep 17 00:00:00 2001 From: 20111596 <20111596@tafe.wa.edu.au> Date: Sat, 6 Dec 2025 15:15:54 +0800 Subject: [PATCH 08/23] Feat: Added itinerary editing functionality (edit_itinerary(), add_new_flight(), AND add_new_attraction) to main.py and manage_itineraries_py, + tidied up itinerary nesting. --- Travel Itinerary Planner/main.py | 166 +++++++++++--- .../src/manage_itineraries.py | 203 ++++++++++++++---- 2 files changed, 294 insertions(+), 75 deletions(-) diff --git a/Travel Itinerary Planner/main.py b/Travel Itinerary Planner/main.py index ca9f952..f742cf2 100644 --- a/Travel Itinerary Planner/main.py +++ b/Travel Itinerary Planner/main.py @@ -1,5 +1,6 @@ from src.file_handler import load_itineraries -from src.manage_itineraries import add_itinerary, edit_itinerary, view_itineraries, delete_itinerary +from src.manage_itineraries import add_itinerary, edit_itinerary, add_new_flight, add_new_attraction, view_itineraries, delete_itinerary, print_table +from pick import pick def run_app(): @@ -12,43 +13,43 @@ def run_app(): while app_running: print("Welcome to the Travel Itinerary Planner app!\n") print("1. Add a new Itinerary") - print("2. Edit an existing Itinerary") - print("3. View existing Itineraries") - print("4. Delete an Itinerary") - print("5. Log out\n") + 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-5): ") + user_choice = input("Please select one of the above options (1-6): ") if user_choice.isdigit(): choice = int(user_choice) - if 1 <= choice <= 5: + if 1 <= choice <= 6: break else: - print("Enter a number between 1-5.") + print("Enter a number between 1-6.") else: - ("Invalid input: Please enter a number between 1-5.") + 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): ") - summary = input("Brief description of trip: ") + 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 = {} + 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 = {} + flights = [] else: while not flights_list_done: - flight_details = {} 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): ") @@ -56,13 +57,13 @@ def run_app(): arrival_date = input("Date & time of flight arrival (Format: DD-MM-YYYY HH:MM): ") flight_name = f"{departure_airport} to {arrival_airport}" - flight_details.update({ + flights.append({ + "flight name": flight_name, "departure airport": departure_airport, "departure date": departure_date, "arrival airport": arrival_airport, "arrival date": arrival_date }) - flights[flight_name] = flight_details while True: add_another_flight = input("Would you like to add another flight? Type Y (yes) or N (no): ") @@ -80,23 +81,21 @@ def run_app(): attractions_list_done = False if user_attractions_choice == "SKIP": - attractions = {} + attractions = [] else: while not attractions_list_done: - attraction_details = {} attraction_name = input("Name of attraction: ") attraction_address = input("Address of attraction: ") attraction_summary = input("Short description of attraction: ") - attraction_type = input("(Optional) Provide some tags that categorise what kind of activity this involves.\nExample of format required: hike, exciting, views\n") - attraction_tags = attraction_type.split(", ") + attraction_tags = input("(Optional) Provide some tags that categorise what kind of activity this involves.\nExample of format required: hike, exciting, views\n") - attraction_details.update({ + attractions.append({ + "attraction name": attraction_name, "address": attraction_address, "summary": attraction_summary, "tag(s)": attraction_tags }) - attractions[attraction_name] = attraction_details while True: add_another_attraction = input("Would you like to add another attraction? Type Y or N:") @@ -109,27 +108,134 @@ def run_app(): else: print("Invalid answer: Please type Y or N only.") - add_itinerary(itineraries, name, location, summary, start_date, end_date, flights, attractions) + add_itinerary(itineraries, name, location, description, start_date, end_date, flights, attractions) # Edit existing itinerary elif user_choice == "2": - # Use pickpack module (https://github.com/anafvana/pickpack#map-function-for-nested-lists) - chosen_itinerary = input("") - edit_itinerary(chosen_itinerary) - pass + # 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': + flight_name_prompt = 'Which flight would you like to edit?' + flight_name_options = [] + for item in itineraries: + for flight in item['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': + attraction_name_prompt = 'Which attraction would you like to edit?' + attractions_available = [] + for item in itineraries: + for attraction in item['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" + print(flight_choice) + + edit_itinerary(itineraries, itinerary_option, edit_option, flight_choice, attraction_choice) + + # 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 + }) + + 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 + }) + + 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 == "3": + elif user_choice == "4": view_itineraries(itineraries) # Delete itinerary - elif user_choice == "4": + elif user_choice == "5": chosen_itinerary = input("") delete_itinerary(chosen_itinerary) pass # Log out - elif user_choice == "5": + elif user_choice == "6": app_running = False print("See you next time! 👋") diff --git a/Travel Itinerary Planner/src/manage_itineraries.py b/Travel Itinerary Planner/src/manage_itineraries.py index 9fc6ff3..762684a 100644 --- a/Travel Itinerary Planner/src/manage_itineraries.py +++ b/Travel Itinerary Planner/src/manage_itineraries.py @@ -2,7 +2,7 @@ from datetime import datetime from pick import pick from anytree import Node, RenderTree -from pickpack import pickpack +from pickpack import pickpack, PickPacker, AnyNode from rich.console import Console from rich.table import Table from rich import print @@ -12,8 +12,8 @@ """ -def add_itinerary(itinerary_list, name, location, summary, start_date, end_date, flights, attractions): - ''' +def add_itinerary(itinerary_list, name, location, description, start_date, end_date, flights, attractions): + """ Docstring for add_itinerary Returns: @@ -21,22 +21,22 @@ def add_itinerary(itinerary_list, name, location, summary, start_date, end_date, Side Effects: - Saves the updated itinerary list to a file using `save_itineraries`. - ''' + """ # TEST: check that all items have been transferred correctly print(name) print(location) - print(summary) + print(description) print(start_date) print(end_date) - print(flights.items()) - print(attractions.items()) + print(flights) + print(attractions) if validate_dates(start_date, end_date, flights): # Add the new itinerary after validation checks new_itinerary = { "name": name, "location": location, - "summary": summary, + "description": description, "start_date": start_date, "end_date": end_date, "flights": flights, @@ -53,9 +53,128 @@ def add_itinerary(itinerary_list, name, location, summary, start_date, end_date, return False -def edit_itinerary(chosen_itinerary): - # Use pickpack module (https://github.com/anafvana/pickpack#map-function-for-nested-lists) - pass +def edit_itinerary(itinerary_list, itinerary_option, edit_option, flight_choice, attraction_choice): + # Edit the itinerary list + for itinerary in itinerary_list: + if itinerary["name"] == itinerary_option: + # Edit DateTime-adjacent data + if edit_option == "start_date": + while True: + start_date = input("Start date in DD-MM-YYYY: ") + if not validate_dates(start_date, itinerary["end_date"], itinerary["flights"]): + print("Invalid date. Please ensure it is in DD-MM-YYYY format (e.g., 12-12-2026)") + else: + itinerary["start_date"] = start_date + break + elif edit_option == "end_date": + while True: + end_date = input("End date in DD-MM-YYYY: ") + if not validate_dates(itinerary["start_date"], end_date, itinerary["flights"]): + print("Invalid date. Please ensure it is in DD-MM-YYYY format (e.g., 12-12-2026)") + else: + itinerary["end_date"] = end_date + break + elif edit_option == "departure date": + for flight_id in itinerary["flights"]: + if flight_id["flight name"] == flight_choice: + while True: + departure = input("Date & time of flight departure (Format: DD-MM-YYYY HH:MM): ") + if not validate_dates(itinerary["start_date"], itinerary["end_date"], itinerary["flights"]): + print("Invalid date. Please ensure it is in DD-MM-YYYY HH:MM format (e.g., 12-12-2026 08:00)") + else: + flight_id["departure date"] = departure + flight_id["flight name"] = f"{flight_id["departure airport"]} to {flight_id["arrival airport"]}" + break + break + elif edit_option == "arrival date": + for flight_id in itinerary["flights"]: + if flight_id["flight name"] == flight_choice: + while True: + arrival = input("Date & time of flight arrival (Format: DD-MM-YYYY HH:MM): ") + if not validate_dates(itinerary["start_date"], itinerary["end_date"], itinerary["flights"]): + print("Invalid date & time. Please ensure it is in DD-MM-YYYY HH:MM format (e.g., 12-12-2026 08:00") + else: + flight_id["arrival date"] = arrival + 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] = input(f"Enter a new {edit_option} for {itinerary['name']}: ") + + # Flights: string type options + elif edit_option == 'departure airport': + airport_leaving = input("Name of airport you are departing from: ") + for flight_id in itinerary["flights"]: + if flight_id["flight name"] == flight_choice: + flight_id["departure airport"] = airport_leaving + flight_id["flight name"] = f"{airport_leaving} to {flight_id["arrival airport"]}" + break + + elif edit_option == 'arrival airport': + arrival_airport = input("Name of airport you are arriving at: ") + for flight_id in itinerary["flights"]: + if flight_id["flight name"] == flight_choice: + flight_id.update({"arrival airport": arrival_airport}) + flight_id["flight name"] = f"{flight_id["departure airport"]} to {arrival_airport}" + break + + # Attractions: string type options + elif edit_option == 'attraction_name': + attraction_id_name = input("Name of the attraction: ") + for attraction in itinerary["attractions"]: + if attraction["attraction name"] == attraction_choice: + attraction.update({"attraction name": attraction_id_name}) + break + elif edit_option == 'address': + attraction_address = input("New attraction address: ") + for attraction in itinerary["attractions"]: + if attraction["attraction name"] == attraction_choice: + attraction.update({"address": attraction_address}) + break + elif edit_option == 'summary': + attraction_summary = input("Short summary of the attraction: ") + for attraction in itinerary["attractions"]: + if attraction["attraction name"] == attraction_choice: + attraction.update({"summary": attraction_summary}) + break + elif edit_option == 'tag(s)': + attraction_tags = input("Provide some tags that categorise what kind of activity this involves.\nExample of format required: hike, exciting, views: ") + for attraction in itinerary["attractions"]: + if attraction["attraction name"] == attraction_choice: + attraction.update({"tag(s)": attraction_tags}) + break + save_itineraries(itinerary_list) + 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.") + save_itineraries(itinerary_list) + 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.") + save_itineraries(itinerary_list) + print(itinerary_list) + return True def view_itineraries(itinerary_list): @@ -72,7 +191,6 @@ def view_itineraries(itinerary_list): print(f"\nFilter selected: {option}") itinerary_choice = 'Which itinerary would you like to view?: ' itinerary_options = [] - for trip in itinerary_list: itinerary_options.append(trip["name"]) filter_option, filter_index = pick(itinerary_options, itinerary_choice) @@ -105,52 +223,47 @@ def print_table(trips): trip_table.add_column("Trip Name", justify="center", no_wrap=True) trip_table.add_column("Location", justify="center") - trip_table.add_column("Summary", justify="center") + trip_table.add_column("Description", justify="center") 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") for trip in trips: - for key in trip["flights"]: - if len(key) == 1: - departure_flight_name = key - flight_departure = trip["flights"][f"{departure_flight_name}"]["departure date"] - flight_list = f'[bold red]{departure_flight_name}[/bold red]: {flight_departure}\n' - elif len(key) >= 2: - flight_list = '' - departure_flight_name = key - flight_departure = trip["flights"][f"{departure_flight_name}"]["departure date"] - flight_item = f'[bold red]{departure_flight_name}[/bold red]: {flight_departure}\n' + flight_list = '' + attraction_list = '' + if len(trip["flights"]) == 1: + flight_list = f'[bold red]{flight["flight name"]}[/bold red] \nDepart: {flight["departure date"]}\nArrive: {flight["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}' - for key in trip["attractions"]: - if len(key) == 1: - attraction = key - attraction_list = f'[bold red]{attraction}[/bold red] \nAddress: {trip["attractions"][f"{attraction}"]["address"]} \nSummary: {trip["attractions"][f"{attraction}"]["summary"]} \nTag(s): {trip["attractions"][f"{attraction}"]["tag(s)"]}\n' - elif len(key) >= 2: - attraction_list = '' - attraction = key + + if len(trip["attractions"]) == 1: + attraction_list = f'[bold red]{attraction["attraction name"]}[/bold red] \nAddress: {attraction["address"]} \nSummary: {attraction["summary"]} \nTag(s): {attraction["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_item = f'[bold red]{attraction}[/bold red] \nAddress: {trip["attractions"][f"{attraction}"]["address"]} \nSummary: {trip["attractions"][f"{attraction}"]["summary"]} \nTag(s): {trip["attractions"][f"{attraction}"]["tag(s)"]}\n' - attraction_list = attraction_list + f'{attraction_item}' - trip_table.add_row(trip["name"], trip["location"], trip["summary"], trip["start_date"], trip["end_date"], flight_list, attraction_list) + 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 # Validation functions - def validate_dates(start_date, end_date, flights): - ''' - Validate dates given for start date, end date and flight datetimes + """ + 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 (DD-MM-YYYY): Itinerary start date. - :param end_date (DD-MM-YYYY): Itinerary start date. - :param flights (nested dict): Nested dictionary containing flight information. This function will be testing the departure_date and arrival_date items. - ''' + :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") @@ -165,7 +278,7 @@ def validate_dates(start_date, end_date, flights): return False # Resource used for following code: https://stackoverflow.com/questions/17322208/multiple-try-codes-in-one-block - for flight_name, flight_info in flights.items(): + for flight_info in flights: try: datetime.strptime(flight_info["departure date"], "%d-%m-%Y %H:%M") except ValueError: @@ -177,12 +290,11 @@ def validate_dates(start_date, end_date, flights): except ValueError: print("Error: Invalid date/time for flight arrival. Use 'DD-MM-YYYY HH:MM' format.") return False - return True def find_itinerary(itinerary_list, trip_name): - ''' + """ Args: itinerary_list: List of all itineraries saved to itinerary.bin file. @@ -190,5 +302,6 @@ def find_itinerary(itinerary_list, trip_name): Returns: itinerary: The itinerary the user wants to view. - ''' - return [itinerary for itinerary in itinerary_list if itinerary["name"] == trip_name] + """ + trip = [itinerary for itinerary in itinerary_list if itinerary["name"] == trip_name] + return trip From 312fb13b8cf0c3751f0d252e29bd692fc6adddb8 Mon Sep 17 00:00:00 2001 From: 20111596 <20111596@tafe.wa.edu.au> Date: Sat, 6 Dec 2025 17:25:36 +0800 Subject: [PATCH 09/23] Feat: Added delete functionality (delete itinerary, itinerary flight, or itinerary attraction) to main.py and manage_itineraries.py + code cleanup. --- Travel Itinerary Planner/main.py | 95 ++++++++++++++++++- .../src/manage_itineraries.py | 89 +++++++++-------- 2 files changed, 140 insertions(+), 44 deletions(-) diff --git a/Travel Itinerary Planner/main.py b/Travel Itinerary Planner/main.py index f742cf2..3514160 100644 --- a/Travel Itinerary Planner/main.py +++ b/Travel Itinerary Planner/main.py @@ -1,5 +1,5 @@ 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, print_table +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 @@ -226,13 +226,98 @@ def run_app(): # View itinerary elif user_choice == "4": - view_itineraries(itineraries) + if not itineraries: + print("No itineraries available to view!") + else: + view_itineraries(itineraries) # Delete itinerary elif user_choice == "5": - chosen_itinerary = input("") - delete_itinerary(chosen_itinerary) - pass + # 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": diff --git a/Travel Itinerary Planner/src/manage_itineraries.py b/Travel Itinerary Planner/src/manage_itineraries.py index 762684a..af0e80e 100644 --- a/Travel Itinerary Planner/src/manage_itineraries.py +++ b/Travel Itinerary Planner/src/manage_itineraries.py @@ -1,8 +1,6 @@ from src.file_handler import load_itineraries, save_itineraries from datetime import datetime from pick import pick -from anytree import Node, RenderTree -from pickpack import pickpack, PickPacker, AnyNode from rich.console import Console from rich.table import Table from rich import print @@ -22,14 +20,15 @@ def add_itinerary(itinerary_list, name, location, description, start_date, end_d Side Effects: - Saves the updated itinerary list to a file using `save_itineraries`. """ - # TEST: 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: 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 @@ -42,10 +41,12 @@ def add_itinerary(itinerary_list, name, location, description, start_date, end_d "flights": flights, "attractions": attractions } - print(new_itinerary) + # Uncomment to print for itinerary validation: + # print(new_itinerary) itinerary_list.append(new_itinerary) - print(itinerary_list) + # Uncomment to print for itinerary_list validation: + # print(itinerary_list) save_itineraries(itinerary_list) return True @@ -145,7 +146,8 @@ def edit_itinerary(itinerary_list, itinerary_option, edit_option, flight_choice, attraction.update({"tag(s)": attraction_tags}) break save_itineraries(itinerary_list) - print(itinerary_list) + # Uncomment to print for itinerary_list validation: + # print(itinerary_list) return True @@ -159,7 +161,8 @@ def add_new_flight(itinerary_list, itinerary_name, new_flights): print(f"Duplicate flight detected: {flight["flight name"]}!") print("This flight will not be added.") save_itineraries(itinerary_list) - print(itinerary_list) + # Uncomment to print for itinerary_list validation: + # print(itinerary_list) return True @@ -173,7 +176,8 @@ def add_new_attraction(itinerary_list, itinerary_name, new_attractions): print(f"Duplicate attraction detected: {attraction["attraction name"]}!") print("This attraction will not be added.") save_itineraries(itinerary_list) - print(itinerary_list) + # Uncomment to print for itinerary_list validation: + # print(itinerary_list) return True @@ -208,15 +212,36 @@ def view_itineraries(itinerary_list): return True -def delete_itinerary(chosen_itinerary): - itinerary_list = load_itineraries() - - print("What would you like to delete?") - # Use pickpack module (https://github.com/anafvana/pickpack#map-function-for-nested-lists) - pass +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 -# Print functions +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) @@ -233,14 +258,14 @@ def print_table(trips): flight_list = '' attraction_list = '' if len(trip["flights"]) == 1: - flight_list = f'[bold red]{flight["flight name"]}[/bold red] \nDepart: {flight["departure date"]}\nArrive: {flight["arrival date"]} \n' + 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]{attraction["attraction name"]}[/bold red] \nAddress: {attraction["address"]} \nSummary: {attraction["summary"]} \nTag(s): {attraction["tag(s)"]}\n' + 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 @@ -252,7 +277,7 @@ def print_table(trips): return -# Validation functions +# Validate dates function def validate_dates(start_date, end_date, flights): """ Validate dates given for start date, end date and flight datetime @@ -291,17 +316,3 @@ def validate_dates(start_date, end_date, flights): print("Error: Invalid date/time for flight arrival. Use 'DD-MM-YYYY HH:MM' format.") return False return True - - -def find_itinerary(itinerary_list, trip_name): - """ - - Args: - itinerary_list: List of all itineraries saved to itinerary.bin file. - trip_name: Name of the trip user wants to view. - - Returns: - itinerary: The itinerary the user wants to view. - """ - trip = [itinerary for itinerary in itinerary_list if itinerary["name"] == trip_name] - return trip From eda89c51a1f3054f470cc90aa0bee65ac9245b75 Mon Sep 17 00:00:00 2001 From: 20111596 <20111596@tafe.wa.edu.au> Date: Sat, 6 Dec 2025 18:15:07 +0800 Subject: [PATCH 10/23] Fix/chore: moved all uses of pick module to main.py to successfully unittest manage_itineraries.py file + fixed issues with all itinerary flights showing in "edit an existing itinerary" when only one itinerary's flights should show --- Travel Itinerary Planner/main.py | 54 ++++++++++++++++--- .../src/manage_itineraries.py | 24 ++------- 2 files changed, 50 insertions(+), 28 deletions(-) diff --git a/Travel Itinerary Planner/main.py b/Travel Itinerary Planner/main.py index 3514160..9a83f11 100644 --- a/Travel Itinerary Planner/main.py +++ b/Travel Itinerary Planner/main.py @@ -127,21 +127,25 @@ def run_app(): 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 item in itineraries: - for flight in item['flights']: - flight_name_options.append(flight["flight name"]) + 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 item in itineraries: - for attraction in item['attractions']: - attractions_available.append(attraction["attraction name"]) + 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)'] @@ -149,7 +153,6 @@ def run_app(): elif edit_option != 'flights' or edit_option != 'attractions': flight_choice = "N/A" attraction_choice = "N/A" - print(flight_choice) edit_itinerary(itineraries, itinerary_option, edit_option, flight_choice, attraction_choice) @@ -182,6 +185,16 @@ def run_app(): "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): ") @@ -211,6 +224,16 @@ def run_app(): "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:") @@ -229,7 +252,22 @@ def run_app(): if not itineraries: print("No itineraries available to view!") else: - view_itineraries(itineraries) + # 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": diff --git a/Travel Itinerary Planner/src/manage_itineraries.py b/Travel Itinerary Planner/src/manage_itineraries.py index af0e80e..297e480 100644 --- a/Travel Itinerary Planner/src/manage_itineraries.py +++ b/Travel Itinerary Planner/src/manage_itineraries.py @@ -181,24 +181,8 @@ def add_new_attraction(itinerary_list, itinerary_name, new_attractions): return True -def view_itineraries(itinerary_list): - # 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}") - # Use rich to print table - print_table(itinerary_list) - 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 itinerary_list: - itinerary_options.append(trip["name"]) - filter_option, filter_index = pick(itinerary_options, itinerary_choice) - +def view_itineraries(itinerary_list, filter_option): + if filter_option != "All": # Filter itinerary list filtered_itineraries = [] for itinerary in itinerary_list: @@ -207,8 +191,8 @@ def view_itineraries(itinerary_list): # Use rich to print table print_table(filtered_itineraries) else: - print("Error: Valid filter not selected, returning to Task Manager menu.\n") - return False + # Use rich to print table + print_table(itinerary_list) return True From 709924f63a9b407471a9098af31cbc82a9d9eaec Mon Sep 17 00:00:00 2001 From: 20111596 <20111596@tafe.wa.edu.au> Date: Sat, 6 Dec 2025 18:21:10 +0800 Subject: [PATCH 11/23] Chore: removed unnecessary requirements and files --- Travel Itinerary Planner/requirements.txt | 3 +- Travel Itinerary Planner/src/itinerary.py | 75 ------------------- .../src/manage_itineraries.py | 1 - 3 files changed, 1 insertion(+), 78 deletions(-) delete mode 100644 Travel Itinerary Planner/src/itinerary.py diff --git a/Travel Itinerary Planner/requirements.txt b/Travel Itinerary Planner/requirements.txt index b0839cb..697059d 100644 --- a/Travel Itinerary Planner/requirements.txt +++ b/Travel Itinerary Planner/requirements.txt @@ -1,4 +1,3 @@ flake8==7.3.0 -anytree==2.13.0 pick==v2.4.0 -pickpack==2.0.0 \ No newline at end of file +rich==14.0.0 \ No newline at end of file diff --git a/Travel Itinerary Planner/src/itinerary.py b/Travel Itinerary Planner/src/itinerary.py deleted file mode 100644 index 7dd8625..0000000 --- a/Travel Itinerary Planner/src/itinerary.py +++ /dev/null @@ -1,75 +0,0 @@ -class Itinerary: - """ - This class represents a single itinerary object that users can create. - - Args: - name (str): The name assigned to the itinerary. - location (str): The main city/country the holiday takes place. - summary (str, optional): A brief summary of the travel plan. - start_date (str): The date the holiday begins in 'DD-MM-YYYY' format. - end_date (str): The date the holiday ends in 'DD-MM-YYYY' format. - flights (dict): A nested dictionary type containing flights and flight details: - flight_name (key, dict type): Contains the flight name (departure-arrival location format, e.g. 'Perth to Sydney') - departure_airport (str): Departure airport name. - departure_date (datetime): Date & time of flight departure in 'DD-MM-YYYY HH:MM' format. - arrival_airport (str): Arrival airport name. - arrival_date (datetime): Date & time of flight arrival in 'DD-MM-YYYY HH:MM' format. - attractions (dict): Nested dictionary of attractions. Each dictionary key (name of attraction) contains: - attraction_name (str, dict): Name of attraction (key) and a dictionary containing attraction details: - address (str): Address of attraction. - attraction_summary(str): Short description of attraction. - attraction_tags (list): List of tags (str type) that catergorise the attraction. - """ - - def __init__(self, name, location, summary, start_date, end_date, flights, attractions): - self.name = name - self.location = location - self.summary = summary - self.start_date = start_date - self.end_date = end_date - self.flights = flights # https://docs.python.org/3/tutorial/classes.html#class-and-instance-variables - self.attractions = attractions - pass - - def to_dict(self): - """ - Converts the Itinerary object into a dictionary. - - Returns: - dict: A dictionary containing itinerary details with these keys: - 'name', 'location', 'summary', 'start-date', 'end-date', 'flights', and 'attractions'. - """ - return { - "name": self.name, - "location": self.location, - "summary": self.summary, - "start_date": self.start_date, - "end_date": self.end_date, - "flights": self.flights, - "attractions": self.attractions - } - - @staticmethod - def from_dict(itinerary_info): - """ - Creates an Itinerary object from the dictionary representation. - - Args: - itinerary_info (dict): A dictionary containing itinerary info with these keys: - 'name', 'location', 'summary', 'start-date', 'end-date', 'flights', and 'attractions'. - - Returns: - Itinerary: A new Itinerary object created from the dictionary data. - """ - # TODO: Re-write to accommodate the dictionary type, and load its attributes according to their type. - # Reference that might help: https://stackoverflow.com/questions/56640436/how-to-generically-serialize-and-de-serialize-objects-from-dictionaries - - return Itinerary( - itinerary_info["name"], - itinerary_info["location"], - itinerary_info["summary"], - itinerary_info["start-date"], - itinerary_info["end_date"], - itinerary_info["flights"], - itinerary_info["attractions"] - ) diff --git a/Travel Itinerary Planner/src/manage_itineraries.py b/Travel Itinerary Planner/src/manage_itineraries.py index 297e480..87b276a 100644 --- a/Travel Itinerary Planner/src/manage_itineraries.py +++ b/Travel Itinerary Planner/src/manage_itineraries.py @@ -1,6 +1,5 @@ from src.file_handler import load_itineraries, save_itineraries from datetime import datetime -from pick import pick from rich.console import Console from rich.table import Table from rich import print From 1c62ba0f370feebae07415c5abbbdde0af4e7931 Mon Sep 17 00:00:00 2001 From: 20111596 <20111596@tafe.wa.edu.au> Date: Sat, 6 Dec 2025 18:22:34 +0800 Subject: [PATCH 12/23] Beginning testing stage using unittest module (test_manage_itineraries.py) --- Travel Itinerary Planner/tests/test_manage_itineraries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Travel Itinerary Planner/tests/test_manage_itineraries.py b/Travel Itinerary Planner/tests/test_manage_itineraries.py index 20f944d..b53cb32 100644 --- a/Travel Itinerary Planner/tests/test_manage_itineraries.py +++ b/Travel Itinerary Planner/tests/test_manage_itineraries.py @@ -1,4 +1,4 @@ -# import unittest +import unittest """ Tests the functions in manage_itineraries.py From a503f084e44caff3b2094652029014b1e729b82d Mon Sep 17 00:00:00 2001 From: 20111596 <20111596@tafe.wa.edu.au> Date: Sat, 6 Dec 2025 18:46:03 +0800 Subject: [PATCH 13/23] Fix: Renamed folder to remove whitespaces --- Travel Itinerary Planner/tests/test_manage_itineraries.py | 6 ------ .../README.md | 0 .../main.py | 0 .../requirements.txt | 0 .../src/file_handler.py | 1 + .../src/manage_itineraries.py | 2 +- Travel-Itinerary-Planner/tests/test_file_handler.py | 7 +++++++ Travel-Itinerary-Planner/tests/test_manage_itineraries.py | 7 +++++++ 8 files changed, 16 insertions(+), 7 deletions(-) delete mode 100644 Travel Itinerary Planner/tests/test_manage_itineraries.py rename {Travel Itinerary Planner => Travel-Itinerary-Planner}/README.md (100%) rename {Travel Itinerary Planner => Travel-Itinerary-Planner}/main.py (100%) rename {Travel Itinerary Planner => Travel-Itinerary-Planner}/requirements.txt (100%) rename {Travel Itinerary Planner => Travel-Itinerary-Planner}/src/file_handler.py (99%) rename {Travel Itinerary Planner => Travel-Itinerary-Planner}/src/manage_itineraries.py (99%) create mode 100644 Travel-Itinerary-Planner/tests/test_file_handler.py create mode 100644 Travel-Itinerary-Planner/tests/test_manage_itineraries.py diff --git a/Travel Itinerary Planner/tests/test_manage_itineraries.py b/Travel Itinerary Planner/tests/test_manage_itineraries.py deleted file mode 100644 index b53cb32..0000000 --- a/Travel Itinerary Planner/tests/test_manage_itineraries.py +++ /dev/null @@ -1,6 +0,0 @@ -import unittest -""" -Tests the functions in manage_itineraries.py - -""" -pass diff --git a/Travel Itinerary Planner/README.md b/Travel-Itinerary-Planner/README.md similarity index 100% rename from Travel Itinerary Planner/README.md rename to Travel-Itinerary-Planner/README.md diff --git a/Travel Itinerary Planner/main.py b/Travel-Itinerary-Planner/main.py similarity index 100% rename from Travel Itinerary Planner/main.py rename to Travel-Itinerary-Planner/main.py diff --git a/Travel Itinerary Planner/requirements.txt b/Travel-Itinerary-Planner/requirements.txt similarity index 100% rename from Travel Itinerary Planner/requirements.txt rename to Travel-Itinerary-Planner/requirements.txt diff --git a/Travel Itinerary Planner/src/file_handler.py b/Travel-Itinerary-Planner/src/file_handler.py similarity index 99% rename from Travel Itinerary Planner/src/file_handler.py rename to Travel-Itinerary-Planner/src/file_handler.py index 48715c4..02dcec1 100644 --- a/Travel Itinerary Planner/src/file_handler.py +++ b/Travel-Itinerary-Planner/src/file_handler.py @@ -7,6 +7,7 @@ current_directory = f"{os.getcwd()}\\{ITINERARY_FILE}" print(current_directory) + def load_itineraries(): """ Load tasks from a binary file using the pickle module. diff --git a/Travel Itinerary Planner/src/manage_itineraries.py b/Travel-Itinerary-Planner/src/manage_itineraries.py similarity index 99% rename from Travel Itinerary Planner/src/manage_itineraries.py rename to Travel-Itinerary-Planner/src/manage_itineraries.py index 87b276a..1f2e4cc 100644 --- a/Travel Itinerary Planner/src/manage_itineraries.py +++ b/Travel-Itinerary-Planner/src/manage_itineraries.py @@ -1,4 +1,4 @@ -from src.file_handler import load_itineraries, save_itineraries +from src.file_handler import save_itineraries from datetime import datetime from rich.console import Console from rich.table import Table diff --git a/Travel-Itinerary-Planner/tests/test_file_handler.py b/Travel-Itinerary-Planner/tests/test_file_handler.py new file mode 100644 index 0000000..407c8ca --- /dev/null +++ b/Travel-Itinerary-Planner/tests/test_file_handler.py @@ -0,0 +1,7 @@ +import unittest +from src.file_handler import load_itineraries, save_itineraries + +""" +Tests the pickle.dump() and pickle.load() in file_handler.py + +""" 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..92f9462 --- /dev/null +++ b/Travel-Itinerary-Planner/tests/test_manage_itineraries.py @@ -0,0 +1,7 @@ +import unittest +from src.manage_itineraries import * +""" +Tests all of the functions in manage_itineraries.py + +""" +pass From 0dc9377099e1eda3602528b8bdbb68a09bf267be Mon Sep 17 00:00:00 2001 From: 20111596 <20111596@tafe.wa.edu.au> Date: Sat, 6 Dec 2025 23:00:42 +0800 Subject: [PATCH 14/23] Chore: changed directory name again, plus added template to test_manage_itineraries. --- .../tests/test_file_handler.py | 7 - .../tests/test_manage_itineraries.py | 7 - Travel_Itinerary_Planner/.flake8 | 4 + .../README.md | 0 .../main.py | 0 .../requirements.txt | 2 +- Travel_Itinerary_Planner/setup.py | 27 ++ Travel_Itinerary_Planner/src/__init__.py | 0 .../src/file_handler.py | 0 .../src/manage_itineraries.py | 9 +- .../tests/test_manage_itineraries.py | 235 ++++++++++++++++++ 11 files changed, 274 insertions(+), 17 deletions(-) delete mode 100644 Travel-Itinerary-Planner/tests/test_file_handler.py delete mode 100644 Travel-Itinerary-Planner/tests/test_manage_itineraries.py create mode 100644 Travel_Itinerary_Planner/.flake8 rename {Travel-Itinerary-Planner => Travel_Itinerary_Planner}/README.md (100%) rename {Travel-Itinerary-Planner => Travel_Itinerary_Planner}/main.py (100%) rename {Travel-Itinerary-Planner => Travel_Itinerary_Planner}/requirements.txt (69%) create mode 100644 Travel_Itinerary_Planner/setup.py create mode 100644 Travel_Itinerary_Planner/src/__init__.py rename {Travel-Itinerary-Planner => Travel_Itinerary_Planner}/src/file_handler.py (100%) rename {Travel-Itinerary-Planner => Travel_Itinerary_Planner}/src/manage_itineraries.py (97%) create mode 100644 Travel_Itinerary_Planner/tests/test_manage_itineraries.py diff --git a/Travel-Itinerary-Planner/tests/test_file_handler.py b/Travel-Itinerary-Planner/tests/test_file_handler.py deleted file mode 100644 index 407c8ca..0000000 --- a/Travel-Itinerary-Planner/tests/test_file_handler.py +++ /dev/null @@ -1,7 +0,0 @@ -import unittest -from src.file_handler import load_itineraries, save_itineraries - -""" -Tests the pickle.dump() and pickle.load() in file_handler.py - -""" diff --git a/Travel-Itinerary-Planner/tests/test_manage_itineraries.py b/Travel-Itinerary-Planner/tests/test_manage_itineraries.py deleted file mode 100644 index 92f9462..0000000 --- a/Travel-Itinerary-Planner/tests/test_manage_itineraries.py +++ /dev/null @@ -1,7 +0,0 @@ -import unittest -from src.manage_itineraries import * -""" -Tests all of the functions in manage_itineraries.py - -""" -pass 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 similarity index 100% rename from Travel-Itinerary-Planner/README.md rename to Travel_Itinerary_Planner/README.md diff --git a/Travel-Itinerary-Planner/main.py b/Travel_Itinerary_Planner/main.py similarity index 100% rename from Travel-Itinerary-Planner/main.py rename to Travel_Itinerary_Planner/main.py diff --git a/Travel-Itinerary-Planner/requirements.txt b/Travel_Itinerary_Planner/requirements.txt similarity index 69% rename from Travel-Itinerary-Planner/requirements.txt rename to Travel_Itinerary_Planner/requirements.txt index 697059d..f5430d1 100644 --- a/Travel-Itinerary-Planner/requirements.txt +++ b/Travel_Itinerary_Planner/requirements.txt @@ -1,3 +1,3 @@ flake8==7.3.0 pick==v2.4.0 -rich==14.0.0 \ No newline at end of file +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 similarity index 100% rename from Travel-Itinerary-Planner/src/file_handler.py rename to Travel_Itinerary_Planner/src/file_handler.py diff --git a/Travel-Itinerary-Planner/src/manage_itineraries.py b/Travel_Itinerary_Planner/src/manage_itineraries.py similarity index 97% rename from Travel-Itinerary-Planner/src/manage_itineraries.py rename to Travel_Itinerary_Planner/src/manage_itineraries.py index 1f2e4cc..d171860 100644 --- a/Travel-Itinerary-Planner/src/manage_itineraries.py +++ b/Travel_Itinerary_Planner/src/manage_itineraries.py @@ -42,8 +42,13 @@ def add_itinerary(itinerary_list, name, location, description, start_date, end_d } # Uncomment to print for itinerary validation: # print(new_itinerary) - - itinerary_list.append(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 + else: + itinerary_list.append(new_itinerary) # Uncomment to print for itinerary_list validation: # print(itinerary_list) 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..0f275dc --- /dev/null +++ b/Travel_Itinerary_Planner/tests/test_manage_itineraries.py @@ -0,0 +1,235 @@ +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 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 task with the same title. + Verify that duplicates 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, "Test Task", "Description", "01-12-2021", "Pending") + result = add_itinerary(self.itineraries, "Test Task", "New Description", "02-12-2024", "Pending") + self.assertFalse(result) + + def test_add_invalid_itinerary(self): + """ + Test adding a task with an invalid due date format. + Verify that the function handles invalid input gracefully and returns False. + """ + result = add_itinerary(self.itineraries, "Test Task", "Description", "2024-12-01", "Pending") + self.assertFalse(result) + + def test_delete_itinerary(self): + """ + Test deleting a task by its title. + Verify that the task is removed from the list and the list size decreases. + """ + add_itinerary(self.itineraries, "Task to Delete", "Description", "01-12-2024", "Pending") + result = delete_itinerary(self.itineraries, "Task to Delete") + self.assertTrue(result) + self.assertEqual(len(self.itineraries), 0) + + def test_view_itineraries(self): + """ + Docstring for test_view_itineraries + + + """ + pass + + 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. + """ + add_itinerary(self.itineraries, "Persistent Task", "Description", "01-12-2024", "Pending") + save_itineraries(self.itineraries) + loaded_itineraries = load_itineraries() + self.assertEqual(len(loaded_tasks), 1) + self.assertEqual(loaded_tasks[0].title, "Persistent Task") + + def test_rich_builtin_table(self): + """ + Test saves itineraries to the task list and displays new formatted table using "rich" Table component. + 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="Test List") + test_table.add_column("Task", justify="center") + test_table.add_column("Description", justify="center") + test_table.add_column("Due Date", justify="center", no_wrap=True) + test_table.add_column("Status", justify="left", no_wrap=True) + add_itinerary(self.itineraries, "Test Task", "N/A", "01-12-2024", "Pending") + + save_itineraries(self.itineraries) + view_itineraries(self.itineraries) + + #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) + test_output = test_console.file.getvalue() + print(test_output) + + 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) + + def test_pick_filter_tasks_by_status(self): + """ + Test the "pick" library's ability to correctly filter tasks based on their status (e.g., 'completed'). + Verify that only tasks matching the specified status are returned. + """ + task1 = Task("Task 1", "Desc", "01-12-2024", "pending") + task2 = Task("Task 2", "Desc", "02-12-2024", "complete") + self.tasks.extend([task1, task2]) + save_tasks(self.tasks) + task_filter_title = "Please choose a task: " + status_options = ["TBA", "pending", "complete"] + task_picker = Picker(status_options, task_filter_title, default_index=2) + assert task_picker.get_selected() == ("complete", 2) + + filter_option = status_options[task_picker.default_index] + print("Filter option selected:", filter_option) + + pick_filter = filter_tasks_by_status(self.tasks, filter_option) + self.assertEqual(pick_filter[0].title, "Task 2") + + +if __name__ == "__main__": + unittest.main() + + + +pass From 2ce14da057638bb3fa00d5e26ba91099dfb140b0 Mon Sep 17 00:00:00 2001 From: 20111596 <20111596@tafe.wa.edu.au> Date: Sat, 6 Dec 2025 23:48:05 +0800 Subject: [PATCH 15/23] Feat(unittest): unittests for adding duplicate itineraries finished, and passed after adding duplication checks in manage_itineraries.py --- .../src/manage_itineraries.py | 7 +++++- .../tests/test_manage_itineraries.py | 23 +++++++++++-------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/Travel_Itinerary_Planner/src/manage_itineraries.py b/Travel_Itinerary_Planner/src/manage_itineraries.py index d171860..1dfc987 100644 --- a/Travel_Itinerary_Planner/src/manage_itineraries.py +++ b/Travel_Itinerary_Planner/src/manage_itineraries.py @@ -44,11 +44,16 @@ def add_itinerary(itinerary_list, name, location, description, start_date, end_d # print(new_itinerary) if itinerary_list: for itinerary in itinerary_list: - if new_itinerary== itinerary: + 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) diff --git a/Travel_Itinerary_Planner/tests/test_manage_itineraries.py b/Travel_Itinerary_Planner/tests/test_manage_itineraries.py index 0f275dc..3fe4b01 100644 --- a/Travel_Itinerary_Planner/tests/test_manage_itineraries.py +++ b/Travel_Itinerary_Planner/tests/test_manage_itineraries.py @@ -48,7 +48,7 @@ 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 = [{ + 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", @@ -76,7 +76,7 @@ def test_add_itinerary(self): "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"]) @@ -86,10 +86,10 @@ def test_add_itinerary(self): def test_add_duplicate_itinerary(self): """ - Test adding a duplicate task with the same title. - Verify that duplicates are not allowed and the function returns False. + 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 = [{ + 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", @@ -117,11 +117,16 @@ def test_add_duplicate_itinerary(self): "summary": "A lovely dinner spot", "tag(s)": "dinner, romantic" } - ]}] + ]} - add_itinerary(self.itineraries, "Test Task", "Description", "01-12-2021", "Pending") - result = add_itinerary(self.itineraries, "Test Task", "New Description", "02-12-2024", "Pending") - self.assertFalse(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"]) + + 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) def test_add_invalid_itinerary(self): """ From b28a1eb936a2a7adbd969c6ca963c38fb6df1c9e Mon Sep 17 00:00:00 2001 From: 20111596 <20111596@tafe.wa.edu.au> Date: Sat, 6 Dec 2025 23:49:47 +0800 Subject: [PATCH 16/23] Fix: Fixed validate_dates() function after datetime related unittests failed. --- Travel_Itinerary_Planner/src/manage_itineraries.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Travel_Itinerary_Planner/src/manage_itineraries.py b/Travel_Itinerary_Planner/src/manage_itineraries.py index 1dfc987..edeb2e3 100644 --- a/Travel_Itinerary_Planner/src/manage_itineraries.py +++ b/Travel_Itinerary_Planner/src/manage_itineraries.py @@ -284,13 +284,13 @@ def validate_dates(start_date, end_date, flights): """ try: - datetime.strptime(start_date, "%d-%m-%Y") + datetime.strptime(start_date, "dd-mm-YYYY") except ValueError: print("Error: Invalid start date. Use 'DD-MM-YYYY' format.") return False try: - datetime.strptime(end_date, "%d-%m-%Y") + datetime.strptime(end_date, "dd-mm-YYYY") except ValueError: print("Error: Invalid end date. Use 'DD-MM-YYYY' format.") return False @@ -298,13 +298,13 @@ def validate_dates(start_date, end_date, flights): # 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") + datetime.strptime(flight_info["departure date"], "dd-mm-YYYY HH:MM") 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") + datetime.strptime(flight_info["arrival date"], "dd-mm-YYYY HH:MM") except ValueError: print("Error: Invalid date/time for flight arrival. Use 'DD-MM-YYYY HH:MM' format.") return False From 0c24707f5102330c71da95391d102b4db8da89ce Mon Sep 17 00:00:00 2001 From: 20111596 <20111596@tafe.wa.edu.au> Date: Sun, 7 Dec 2025 00:01:09 +0800 Subject: [PATCH 17/23] Feat(unittest): Completed test_add_itinerary_with_invalid_dates unittest. --- .../tests/test_manage_itineraries.py | 70 +++++++++++++++++-- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/Travel_Itinerary_Planner/tests/test_manage_itineraries.py b/Travel_Itinerary_Planner/tests/test_manage_itineraries.py index 3fe4b01..53ca1aa 100644 --- a/Travel_Itinerary_Planner/tests/test_manage_itineraries.py +++ b/Travel_Itinerary_Planner/tests/test_manage_itineraries.py @@ -128,13 +128,73 @@ def test_add_duplicate_itinerary(self): self.assertFalse(same_name_result) self.assertFalse(duplicate_result) - def test_add_invalid_itinerary(self): + def test_add_itinerary_with_invalid_dates(self): """ - Test adding a task with an invalid due date format. - Verify that the function handles invalid input gracefully and returns False. + Test adding a task with invalid date formats (start_date, end_date, & flight departure and arrival datetimes). + Verify that the function handles invalid input as expected and returns False. """ - result = add_itinerary(self.itineraries, "Test Task", "Description", "2024-12-01", "Pending") - self.assertFalse(result) + 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 + 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) def test_delete_itinerary(self): """ From bdb9b6cbd9c490e6c7cbc013496a88fe53feef5e Mon Sep 17 00:00:00 2001 From: 20111596 <20111596@tafe.wa.edu.au> Date: Sun, 7 Dec 2025 14:54:31 +0800 Subject: [PATCH 18/23] Bugfix(improve unittesting): Moved functionality to prompt user for new/editied value from manage_itineraries.py to main.py, then passed this value as a parameter into edit_itinerary() function. --- Travel_Itinerary_Planner/main.py | 7 +- .../src/manage_itineraries.py | 103 +++++++++--------- 2 files changed, 56 insertions(+), 54 deletions(-) diff --git a/Travel_Itinerary_Planner/main.py b/Travel_Itinerary_Planner/main.py index 9a83f11..a0b420b 100644 --- a/Travel_Itinerary_Planner/main.py +++ b/Travel_Itinerary_Planner/main.py @@ -148,13 +148,16 @@ def run_app(): 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)'] + 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) + 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": diff --git a/Travel_Itinerary_Planner/src/manage_itineraries.py b/Travel_Itinerary_Planner/src/manage_itineraries.py index edeb2e3..91e4570 100644 --- a/Travel_Itinerary_Planner/src/manage_itineraries.py +++ b/Travel_Itinerary_Planner/src/manage_itineraries.py @@ -59,101 +59,100 @@ def add_itinerary(itinerary_list, name, location, description, start_date, end_d save_itineraries(itinerary_list) return True - else: - return False + return False -def edit_itinerary(itinerary_list, itinerary_option, edit_option, flight_choice, attraction_choice): +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": - while True: - start_date = input("Start date in DD-MM-YYYY: ") - if not validate_dates(start_date, itinerary["end_date"], itinerary["flights"]): - print("Invalid date. Please ensure it is in DD-MM-YYYY format (e.g., 12-12-2026)") - else: - itinerary["start_date"] = start_date - break + 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": - while True: - end_date = input("End date in DD-MM-YYYY: ") - if not validate_dates(itinerary["start_date"], end_date, itinerary["flights"]): - print("Invalid date. Please ensure it is in DD-MM-YYYY format (e.g., 12-12-2026)") - else: - itinerary["end_date"] = end_date - break + 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: - while True: - departure = input("Date & time of flight departure (Format: DD-MM-YYYY HH:MM): ") - if not validate_dates(itinerary["start_date"], itinerary["end_date"], itinerary["flights"]): - print("Invalid date. Please ensure it is in DD-MM-YYYY HH:MM format (e.g., 12-12-2026 08:00)") - else: - flight_id["departure date"] = departure - flight_id["flight name"] = f"{flight_id["departure airport"]} to {flight_id["arrival airport"]}" - break - break + test_updated_flight = [flight_id] + test_updated_flight["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: - while True: - arrival = input("Date & time of flight arrival (Format: DD-MM-YYYY HH:MM): ") - if not validate_dates(itinerary["start_date"], itinerary["end_date"], itinerary["flights"]): - print("Invalid date & time. Please ensure it is in DD-MM-YYYY HH:MM format (e.g., 12-12-2026 08:00") - else: - flight_id["arrival date"] = arrival - break - break + test_updated_flight = [flight_id] + test_updated_flight["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] = input(f"Enter a new {edit_option} for {itinerary['name']}: ") + break # Flights: string type options elif edit_option == 'departure airport': - airport_leaving = input("Name of airport you are departing from: ") for flight_id in itinerary["flights"]: if flight_id["flight name"] == flight_choice: - flight_id["departure airport"] = airport_leaving - flight_id["flight name"] = f"{airport_leaving} to {flight_id["arrival airport"]}" + 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': - arrival_airport = input("Name of airport you are arriving at: ") for flight_id in itinerary["flights"]: if flight_id["flight name"] == flight_choice: - flight_id.update({"arrival airport": arrival_airport}) - flight_id["flight name"] = f"{flight_id["departure airport"]} to {arrival_airport}" + 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': - attraction_id_name = input("Name of the attraction: ") for attraction in itinerary["attractions"]: if attraction["attraction name"] == attraction_choice: - attraction.update({"attraction name": attraction_id_name}) + attraction.update({"attraction name": new_value}) break + break elif edit_option == 'address': - attraction_address = input("New attraction address: ") for attraction in itinerary["attractions"]: if attraction["attraction name"] == attraction_choice: - attraction.update({"address": attraction_address}) + attraction.update({"address": new_value}) break + break elif edit_option == 'summary': - attraction_summary = input("Short summary of the attraction: ") for attraction in itinerary["attractions"]: if attraction["attraction name"] == attraction_choice: - attraction.update({"summary": attraction_summary}) + attraction.update({"summary": new_value}) break + break elif edit_option == 'tag(s)': - attraction_tags = input("Provide some tags that categorise what kind of activity this involves.\nExample of format required: hike, exciting, views: ") for attraction in itinerary["attractions"]: if attraction["attraction name"] == attraction_choice: - attraction.update({"tag(s)": attraction_tags}) + attraction.update({"tag(s)": new_value}) break + break save_itineraries(itinerary_list) # Uncomment to print for itinerary_list validation: # print(itinerary_list) @@ -284,13 +283,13 @@ def validate_dates(start_date, end_date, flights): """ try: - datetime.strptime(start_date, "dd-mm-YYYY") + 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, "dd-mm-YYYY") + datetime.strptime(end_date, "%d-%m-%Y") except ValueError: print("Error: Invalid end date. Use 'DD-MM-YYYY' format.") return False @@ -298,13 +297,13 @@ def validate_dates(start_date, end_date, flights): # 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"], "dd-mm-YYYY HH:MM") + 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"], "dd-mm-YYYY HH:MM") + 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 From 79764c238f902887fa3d1497d1d13e2bf993a319 Mon Sep 17 00:00:00 2001 From: 20111596 <20111596@tafe.wa.edu.au> Date: Sun, 7 Dec 2025 14:57:32 +0800 Subject: [PATCH 19/23] Chore: Replaced the input()'s overlooked in edit_itinerary() with "new_value" parameter. --- Travel_Itinerary_Planner/src/manage_itineraries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Travel_Itinerary_Planner/src/manage_itineraries.py b/Travel_Itinerary_Planner/src/manage_itineraries.py index 91e4570..9bcb404 100644 --- a/Travel_Itinerary_Planner/src/manage_itineraries.py +++ b/Travel_Itinerary_Planner/src/manage_itineraries.py @@ -108,7 +108,7 @@ def edit_itinerary(itinerary_list, itinerary_option, edit_option, flight_choice, 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] = input(f"Enter a new {edit_option} for {itinerary['name']}: ") + itinerary[edit_option] = new_value break # Flights: string type options From 8015e2c5ffb41af054eaae7a864894e47b87c4c6 Mon Sep 17 00:00:00 2001 From: 20111596 <20111596@tafe.wa.edu.au> Date: Sun, 7 Dec 2025 16:13:41 +0800 Subject: [PATCH 20/23] Feat(unittest): Fixed code issues in edit_itinerary() and passed test_edit_itinerary() unittests. --- .../src/manage_itineraries.py | 6 +- .../tests/test_manage_itineraries.py | 114 +++++++++++++++++- 2 files changed, 114 insertions(+), 6 deletions(-) diff --git a/Travel_Itinerary_Planner/src/manage_itineraries.py b/Travel_Itinerary_Planner/src/manage_itineraries.py index 9bcb404..9ee7d38 100644 --- a/Travel_Itinerary_Planner/src/manage_itineraries.py +++ b/Travel_Itinerary_Planner/src/manage_itineraries.py @@ -85,7 +85,7 @@ def edit_itinerary(itinerary_list, itinerary_option, edit_option, flight_choice, for flight_id in itinerary["flights"]: if flight_id["flight name"] == flight_choice: test_updated_flight = [flight_id] - test_updated_flight["departure date"] = new_value + 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 @@ -97,7 +97,7 @@ def edit_itinerary(itinerary_list, itinerary_option, edit_option, flight_choice, for flight_id in itinerary["flights"]: if flight_id["flight name"] == flight_choice: test_updated_flight = [flight_id] - test_updated_flight["arrival date"] = new_value + 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 @@ -129,7 +129,7 @@ def edit_itinerary(itinerary_list, itinerary_option, edit_option, flight_choice, break # Attractions: string type options - elif edit_option == 'attraction_name': + elif edit_option == 'attraction name': for attraction in itinerary["attractions"]: if attraction["attraction name"] == attraction_choice: attraction.update({"attraction name": new_value}) diff --git a/Travel_Itinerary_Planner/tests/test_manage_itineraries.py b/Travel_Itinerary_Planner/tests/test_manage_itineraries.py index 53ca1aa..ca40287 100644 --- a/Travel_Itinerary_Planner/tests/test_manage_itineraries.py +++ b/Travel_Itinerary_Planner/tests/test_manage_itineraries.py @@ -127,11 +127,12 @@ def test_add_duplicate_itinerary(self): 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 a task with invalid date formats (start_date, end_date, & flight departure and arrival datetimes). - Verify that the function handles invalid input as expected and returns False. + 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": [{ @@ -186,7 +187,7 @@ def test_add_itinerary_with_invalid_dates(self): # 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 + # 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"]) @@ -195,6 +196,113 @@ def test_add_itinerary_with_invalid_dates(self): 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_delete_itinerary(self): """ From a84fd77e095de224529bee2b6352bad65147faf7 Mon Sep 17 00:00:00 2001 From: 20111596 <20111596@tafe.wa.edu.au> Date: Mon, 8 Dec 2025 00:31:35 +0800 Subject: [PATCH 21/23] Chore: minor code cleanup for readability and functions return boolean (True/False) where needed for unittesting. --- Travel_Itinerary_Planner/main.py | 3 +++ Travel_Itinerary_Planner/src/manage_itineraries.py | 8 +++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Travel_Itinerary_Planner/main.py b/Travel_Itinerary_Planner/main.py index a0b420b..3fac5f6 100644 --- a/Travel_Itinerary_Planner/main.py +++ b/Travel_Itinerary_Planner/main.py @@ -313,11 +313,13 @@ def run_app(): 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: @@ -351,6 +353,7 @@ def run_app(): 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: diff --git a/Travel_Itinerary_Planner/src/manage_itineraries.py b/Travel_Itinerary_Planner/src/manage_itineraries.py index 9ee7d38..5f1801e 100644 --- a/Travel_Itinerary_Planner/src/manage_itineraries.py +++ b/Travel_Itinerary_Planner/src/manage_itineraries.py @@ -168,6 +168,7 @@ def add_new_flight(itinerary_list, itinerary_name, new_flights): 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) @@ -183,6 +184,7 @@ def add_new_attraction(itinerary_list, itinerary_name, new_attractions): 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) @@ -239,12 +241,12 @@ def print_table(trips): 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") - trip_table.add_column("Description", justify="center") + 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") + trip_table.add_column("Attractions", justify="left", style="bold", no_wrap=False) for trip in trips: flight_list = '' From 200828ac573e6a8ea30fc1843738f21ee1a4426a Mon Sep 17 00:00:00 2001 From: 20111596 <20111596@tafe.wa.edu.au> Date: Mon, 8 Dec 2025 00:35:27 +0800 Subject: [PATCH 22/23] Feat: Completed all unittests! --- .../tests/test_manage_itineraries.py | 484 ++++++++++++++++-- 1 file changed, 435 insertions(+), 49 deletions(-) diff --git a/Travel_Itinerary_Planner/tests/test_manage_itineraries.py b/Travel_Itinerary_Planner/tests/test_manage_itineraries.py index ca40287..1668bf4 100644 --- a/Travel_Itinerary_Planner/tests/test_manage_itineraries.py +++ b/Travel_Itinerary_Planner/tests/test_manage_itineraries.py @@ -6,6 +6,7 @@ import io from rich.console import Console from rich.table import Table +from rich import print from pick import Picker @@ -302,40 +303,382 @@ def test_edit_itinerary(self): 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 task by its title. - Verify that the task is removed from the list and the list size decreases. + 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. """ - add_itinerary(self.itineraries, "Task to Delete", "Description", "01-12-2024", "Pending") - result = delete_itinerary(self.itineraries, "Task to Delete") + 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), 0) + self.assertEqual(len(self.itineraries), 1) + self.assertEqual(self.itineraries, [test_England_itinerary]) + self.assertNotEqual(self.itineraries, [test_Japan_itinerary]) + - def test_view_itineraries(self): + 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. """ - Docstring for test_view_itineraries + 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" + } + ]) - """ - pass + 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. + Verify that the saved itineraries are correctly loaded with the same data and format. """ - add_itinerary(self.itineraries, "Persistent Task", "Description", "01-12-2024", "Pending") + 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_tasks), 1) - self.assertEqual(loaded_tasks[0].title, "Persistent Task") + + self.assertEqual(len(loaded_itineraries), 2) + self.assertEqual(loaded_itineraries, itineraries_list) def test_rich_builtin_table(self): """ - Test saves itineraries to the task list and displays new formatted table using "rich" Table component. + 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 @@ -343,22 +686,89 @@ def test_rich_builtin_table(self): - [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="Test List") - test_table.add_column("Task", justify="center") - test_table.add_column("Description", justify="center") - test_table.add_column("Due Date", justify="center", no_wrap=True) - test_table.add_column("Status", justify="left", no_wrap=True) - add_itinerary(self.itineraries, "Test Task", "N/A", "01-12-2024", "Pending") + # Test view_itineraries function to verify itineraries are saved + test_table = Table(title="Itineraries", show_lines=True) - save_itineraries(self.itineraries) - view_itineraries(self.itineraries) + 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 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) + print(test_output) # prints table template for itinerary list self.assertIs(type(test_table), type(Table())) self.assertIs(type(test_output), str) @@ -379,30 +789,6 @@ def test_pick_move_up_down_function(self): itinerary_picker.move_down() assert itinerary_picker.get_selected() == ("this_itinerary", 1) - def test_pick_filter_tasks_by_status(self): - """ - Test the "pick" library's ability to correctly filter tasks based on their status (e.g., 'completed'). - Verify that only tasks matching the specified status are returned. - """ - task1 = Task("Task 1", "Desc", "01-12-2024", "pending") - task2 = Task("Task 2", "Desc", "02-12-2024", "complete") - self.tasks.extend([task1, task2]) - save_tasks(self.tasks) - task_filter_title = "Please choose a task: " - status_options = ["TBA", "pending", "complete"] - task_picker = Picker(status_options, task_filter_title, default_index=2) - assert task_picker.get_selected() == ("complete", 2) - - filter_option = status_options[task_picker.default_index] - print("Filter option selected:", filter_option) - - pick_filter = filter_tasks_by_status(self.tasks, filter_option) - self.assertEqual(pick_filter[0].title, "Task 2") - if __name__ == "__main__": unittest.main() - - - -pass From 28ae0d8f3973807602121b2b9a1321a00ec9d3d1 Mon Sep 17 00:00:00 2001 From: 20111596 <20111596@tafe.wa.edu.au> Date: Mon, 8 Dec 2025 01:34:16 +0800 Subject: [PATCH 23/23] Feat: Updated README.md and added images used in it. --- Travel_Itinerary_Planner/README.md | 41 +++++++++++++----- .../assets/choose-item-key-to-edit.png | Bin 0 -> 5552 bytes .../assets/choose-itinerary-to-edit.png | Bin 0 -> 4605 bytes .../assets/view-itineraries.png | Bin 0 -> 53589 bytes .../assets/welcome-to-the-app.png | Bin 0 -> 30626 bytes 5 files changed, 29 insertions(+), 12 deletions(-) create mode 100644 Travel_Itinerary_Planner/assets/choose-item-key-to-edit.png create mode 100644 Travel_Itinerary_Planner/assets/choose-itinerary-to-edit.png create mode 100644 Travel_Itinerary_Planner/assets/view-itineraries.png create mode 100644 Travel_Itinerary_Planner/assets/welcome-to-the-app.png diff --git a/Travel_Itinerary_Planner/README.md b/Travel_Itinerary_Planner/README.md index 0f3546c..1497391 100644 --- a/Travel_Itinerary_Planner/README.md +++ b/Travel_Itinerary_Planner/README.md @@ -1,16 +1,17 @@ -# Script Name -Short description of package/script -- If package, list of functionalities/scripts it can perform -- If standalone script, short description of script explaining what it achieves - -# Description -- If code is not explainable using comments, use this sections to explain your script +# 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) -- Git Bash -- (List out the libraries imported in the script) +- 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 @@ -19,19 +20,35 @@ Short description of package/script ``` 2. Change directory into the cloned repository ```bash - cd All-In-One-Python-Projects/'Travel Itinerary Planner'/ + cd All-In-One-Python-Projects/'Travel_Itinerary_Planner'/ ``` 3. Install the required libraries ```bash pip install -r requirements.txt ``` -4. Run the program using +4. Run the program in Command Line or Terminal using ```bash python3 main.py ``` # Screenshot -- Display images/gifs/videos of output/result of your script so that users can visualize it. +### 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 0000000000000000000000000000000000000000..ecadb55c64ad06945f4443578ab1377096275fec GIT binary patch literal 5552 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 K`bD}<5B>{syAQDd literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..60f7c759639808e005e3709364057764372c9721 GIT binary patch literal 4605 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 QzjZ)pl)7Sxg4wJ80jzTyLjV8( literal 0 HcmV?d00001 diff --git a/Travel_Itinerary_Planner/assets/view-itineraries.png b/Travel_Itinerary_Planner/assets/view-itineraries.png new file mode 100644 index 0000000000000000000000000000000000000000..f0b2b10b8530836e18c50f8f369fbd7c623d2bef GIT binary patch literal 53589 zcmdqIby$>b*EfoYfk>!yE7B$1pnxC^B3%N)NHa7;35e7HN=gombPU}g-QC?VbPhAX z9=z}8eY}0Y_uKnD_P3Ay?af~t*Id_mt#hq){?>2B6{xHzjs1Z10U8<_w#*yJw`gb> zHE3w)X!p@kS5~Xw)M#kbXfl$o-nra@)hUH>ONVE*2UAnz?6Ptb=Eh8af2!R*`AvC& zEpFiL02-F~2zdb19Qm!@}|LEx#J-B^~&zrseym|Iik<4d9@pgmfl!5y} zYvFm7HhSLi31dTQd!g6mcCG?8c>~L>=(!+2N!9xaVT}NYsK-St)%}-f=y;#dFsRXP z{q=8SLL-rZySGsS%_70Sul{v|B)Zp`vzNcobE5ya{qHA}U!k>5d=mdpPau&;{R~n= zf4lT~{>h@?0Xk&fBl|F2w||x0&0#E-r02Tj!Bi z5RmVV>UI0Pr~3JE`@AVYjG29BdFh^1KA@s&0>5(p3aHlJ+SX*=f}N4?ti zJvx?=<>C-6-SO4cmBHoswZc=<_^7KvUnAtU*HQNP;*GrYCk{Cy>R=TIdC)S%^BTcr z9+ouEyk()lGpegUC2yQ>3zt;27>-tQu+Y0&rlQrn^%K8EU2f}lTiIq|5Pn!}&cJG7 zIyOA8dq0p&e0`GD&Dgdq7ox2?NvcEH=vLBA3)6`&cOQ!NyX%Ey4FcZ6jrnbj-S3S) zEV`8r0`GGkm+gNmR=lQ2OJM0DK8-wBhj^8<-zct)WZCEa!d7Pve>fz?w_swYy zfe8B`=JC6J_krkGCAN!0?d^I+)Y`?K;*KeDdvBG&5XD4}9R5h3OD{r?#rblhcLzN7 zPXjX2H+?|gq*Pw1wKDLh?{wZ3l!|;N6S49wlOyYGq@rK&6yH{t1BXNBPUddr>mPaR zgHE2jtCngWdxSlB=1m`~bQ6$a;XNQ8L(3K${cD!nmO`eroUgfnJDH)A;=Gg>bXj)y zjIGV;s|2opI{-5zx0t224ozSDGEi`JMZ8g|;a8HWxN;*Flb0|ejK=4FNCbJIY>A+t&ocb2riFx|wzmG;@o~{DK@!9)G z$*_iCVGRlqCB53Kj#um7)Yx+&*#o4{bU9C+3_fGX46&LovJ50RVx?H(`TZP~KQE%j zB4X3)lkgx`!=fUwK!R21yu>-$xGKfZFr3E&2}cREzhgZnAdAo<@XKcdaa0S`PC0AhHb?#<#BM#ZQpAAw7%gj2{kaMIU@Vr zBlNW&yX(hV*^48LjGZ!A$H%^77AbhdRif8IUYhUJ{PmNhaD8L408>4OrBSNRG#WC^wMB$^fOV9ntcvDQgn7Bzn+b&= z@ms&kB!s72^)9T=HrpC`5SJ-xseaLZ$kZmz%xl1#BugNj$|9%tZd^V}JL7HB);%53 zNoul0{C8P$cr=UaBlPnOG8TY~1#&f8dj%6R3jzEk!1`z4$qbJ`YN2DJREpIrGzG#m zX4i)lvqlaG2vFOfxj|4u-oIi}mWH9XUYq0rPcf)IO77k7F#cW$T}9L3x+0P4AWbHi z-8gb^etzD+Y@03!DEFqFLe6iXC|D2cQd-KtT~NH;Eq?FO_vPEao7@ybk-4qm+TZK= znh)9=OyXT|6BV1hT)_>H7vBFVa8{b3i(Mm+AnI2bt>=Z;mj<)k zgmm}*8D7xPaT%n-|BQM6D}v!Cqv!X2x5!O!NmfYCBEDX;xyn3yl;f^$PDcA@47CQo zROPr*xH1cp!e0(qMH}}1r$q+nl$qP|2!|WWK0XXO=)YbXw{rVWF}pDzj{!fet5xiB zQ>on2(HI4*q8`-*=smZ8KP5NOdkP~iNaZ9YK+1#h4!-gKP@H^XEDudD44j~YHoH%o zzdJQbYagF5;2S?Bif2~=TSfjUMhw=OJFgNSx5Lg=SPCd1^YZHSuym}$7<&)zR!W=1O z6~pPgrhuqx;8CJ*6f|a_?p(leN$`w|0`Z|waGB&o{)zC$L@E1EWKc0*AuLOu>g;H7 z(J)S<&;Jgh*OdTMt2O~v*avw69E2-1^#dVnt8o}&f-%#OqU-(r7Z4k?5qUQqt#&?A zOk;T1J&RrevS-9`cRcJyZ)wTjk+fk~>Kxj1)w;(Z@o%H+`ZOjRA#JW_ufI9(n`vmk zI;bcqo7ZgWxp1bBb&b1GCa>PcK5_NZ(!s+hFJwJK3z^Jzn=G~zNE|J_3bk;Ju%aML zx=1%57P0F%q~2-G*BHB}Q*K%|1EqQxEhba?alTr)pBWL0(NhEo7jP8CKUG^lUK`C- zvMY`9KjBt8I$kzfi$7BTGQj4iCD&%y@aU{r+^_*|ml7Oq=`_;ygJt#Q+aX$kOzb$Q zq6*gsOGIkk+1ES~74V&>`u~B7-W2z5IQTWNw;ytKoYjD+X+Y$ugI^W*ocXoV@$B$J z^Kx)SKAbv1iCI=?=uDPN({>`9^laMqyjUR)$9{5CQV;InC1%2^)Lt=CSq;GSiE+Cm z*CHUtI@5J8H2b%8Jqd5e!ZR_`;ah%JwaroABgQJqp6mgkylMMSp3AI^xI5@C71ke5 zDXsZlzE9}_@MseK98L1UrluHsR^cc?RJbH_>pg55J`XN{FG$)lTLXe6k>3LYKjBreOp~iimdOZjj`UVoNK>6lO+$F24Dx&peH#<+# z(*UX>sPF@g-}vU(7%?+Vvc2S8O z*TE?!+dA)f4NULE&=m}`lq0&N$++`JqeKuxC6(IF?B-jpiM{hf^M;;W*Y<+T=zzym$1*k)sSIzpUTf>;;~NjY z)R$^Ump>|T&&vF+J*-&B>7sJEVOys^^1HjoH97?qpCdRDzt0BBR=HHGX;1b3ump}a z=lINrS}{r-w_&rcXK`=6t^5!wUslhCKYj5+9j3|jB#j(&HPUvnuVCUk71QR&X0BZn zbyC~6o+Z!B@FX7B&m++-OeaobB&ffT%PP%d2Mk(Gx=a!?%34c1ZK1V)u~7@2m=Vbh=FP+o-7_SJ;N<)p@e>%T&w53`n0vcgsy)BeP5d{pn9WjJ1}I z-!1=&@s{qX!66&raZ#&vCZH_@GM358w5)~- z&Ewjw`jOuRPl#!(G!ibP()4X~e8hl1dyrU&i`Kt=)XcHwdas%E-(jjT8kHpL44LLl zKecrXQc>hQDY~frc0bFfURtMLh;8h*c+aNrUYXxQ;0 zuwf<>?{UK9N&g*5ZeaP7FIv#xtw6f}h+ltU^=>#?&;eUZU;2Mq{08^DwBarE^Zb=V z82? z|E0J8$?`F8!C_%H+++_G;=;nVT=PJI+O-s&pbQ5+vQ9!)=r=U8IiQj>}u80~uU0jH#xB1+#nRC0F%rB~(Di>X`ncD^q zE>8rgc0||4Ekvj+u8$8-PY=nj&bSjdD-8G0 zWO`wTBqe5Fd7R1@URxG@4LBHJ$9zLOCcK)xBQ0o zAI2);@D65>ifv)?8+b?aO4KCEq+13qiA_SFvMkwc2Xi&DnzHNnnr@3u)3rntnd@gL z(1c$G9V{+3O=Yj^k58Ox<3w=c1ZXjrvl=qy3g#om_V7QRO;OcJ{v*Z2n}})jCrA^J zVqTF{!sOnzFd^vgq9e&Pe{L4WVj5(s5N9ZPl$=@0D5#AXO5V3m@z|-YRx#L`WZY(M z%H&ldO!dKD4+3ofYWUX)K1p`0&7%#*F?E?&eGldy&aKmbEyDf}fA^lnbl2oq<7x!a z<>HG=X!%{WDn0%x(p`qD%|+6UBON$V&R!R!0@{hsk#9fg&;6R$Yb?l9qZ{DED*GW; z;wru^>ImeC&6}jJZ-DwsD)E6~>)HOG*WZ69R0pzp`N!sh8@&mP$K-k93`4#p?SNB@ zU2#m?nWQ$$h~iHmJAv8S{Ej;vE?TpG8gTMa&?-xJOGYbxx{b=@3H`p8hbPF@!xiM| zfpwkRXO_=}{aWO#fmj1^x>a`T&Oc7jdvq>_}iw$5*lgAqdG*vEWvbb4_?NNMBYiJf8otZQp= z@hKLPTNnRpif?&#dz4%qXy?s;+8CU`Sg;-1@0Q6`M~!VN2ER3!%m;gf2*3P^fGtO$RCZ}+ab zQ`>r;yqaGjYB9ATT7mo=e?8NK>7VA?!E}GaVZFyj^Uo8A_o>koh3xSurqusX3>vzI z6bji*ZngZGW5+*w$zlB^&ijuDp-XZLe>v*TL+5|I_SSt%shcn~*?{cs)&212JMbp_ zZW#4e&9yO+GYS4*cs%g@YrRCrC99+Y4IocAP7IJhn)z1@DnJE?J*F9XLe!0oj(7p{ z_blq0#@(qb4|WO#A!&@IY4EgUhQH&gI!exh=GVr*wlxv@Qk9&Sx#B4B+uw3K-)s+X z3s8N7ZUz1?pW;CD=X#2bS=ONKn{NwTU0lAZ){g^3iXJNEZNbV;XN5a1e&J90*qnY6 z>CU_XgeoFY->q!l8u>`I7dY(wA!%;=XeCSVU#?t)h0+My#NXReL?x+Sw$L{X#J+zwyQHFxNJ zT?Z(~$@R80@&Dd>4Jyv*6&J~78MV$hFB>-IkNc=%lbcuQ4 z-2(-tF0QtvJ)y4SM@o@LFkk~&+0!k8FH4M}_c{g_Q07ZjruD1$DmWI3I0ne?J48dp# zeq`irhyMD;7#)0-A&-c+U)P5K)5du*I{K~nxgKDYKVQ%tM?JUgbyDcS+4rqf#E%2r zmeAUkxSI+T@`!qVfmiB%9ub_kEku7Bk6Nr^v4;U~BK(uR9xjtqA8`dZ1H1~}I1RND zen!rF*K@t)n~gCh+Rdwy$G>3~6@-yfxcfE|-&(Jh;s{-wO^M-36>=%x`D!~6eO4DH zRUcx4HiLS_Cw*zU#%fVJhka~xV~m7t(pLU2&i?v+0lN#6D9fM7L_rZ^B3oP)LiRUOl0yNdWfN2~|e=FB{#K)Cs&AW}**2WkcjvvtT zwrQmA%~QXSYOE?y0UNtGZ=i(6NwN}pPF}w1v6qGtn&kh!&>$52PB$Rs9J>X=%d=ic zq4hOm;~2vw3OUeM$Y$E^gHN8#w^a^WkfE=v^CpnoLDzX~Td<8X?&dIkBG~@FVX*NW zVk#;Wiq6f?>nBlCRs5`8)B72jfJoi8ZSu09Zd{k&)2ETA;Z_vk5u7y21ckw)B+0)!JAQBSn9Kfq2N11KT zfFtNQxMBX->(za>H2C=grRBBx>@e5hV&UCTjl@UTB|C>Tv?M8+2M3iSyuqYm{;A9L z?A^lsffXSd7Rx2Z?iII|s9GU5n0ECcb#=m<3?fNWjavKDB;9-smk~IKsuOvIF8XI> z!ftw{S1Tg6&Z1l9D*N_2YflQ)`ADukUTUA>FOwXle-~xPRcwSn44Iz^XxBmKyCM3Q zRo{VxKT zIjtx~wm?ZjekZqLxG(BfUjSPI2VF||dAe($o1t%%7p!3XUDoLn8}QG6l&A)t@-d8%SMNg*^ZhA}{>Rb$=ds}I-w67~l|ngcKI^_CWi zy81)jZ5g31&`1m^p`NpX@|=73S%nEA0iK6V+}#KR(QC*lA{!ArIio8V6Jn)z$r^NaV)S zD4{Q(Ap_;jML_y^|1}FhGMHq9FA61dUVD>JkXlCl;E7NxZ@13S-Q4T_Eo;n>+WKO-00N8l=RXXpwDF~g| z!dU}1^`6Gt)B#@uH{Ys3dFidR$hfgQt$TM%R7VgwGzNTt$;Rf?My^XZ_@<~k0F>o}vI999g>5ltrfZ-MOY7SV;x>5SG^>5JY5;3#& z-RdUIUhQImBp3yACXFh?8XTlbbBUqzOM#9x&WLi+?c)r)6gl8L%cqS>blad9O$YbT zZxG)TuQR*Wr?x?piKc9nQ>H1c$oyfGb8`;L)}Ed*#9p`;&i)}Dw$S4}^2)Gw=d(8i zIC)(aFI7Abb>7gs%KC!EYEz2v+Wj0gDy^O{u1S<=^Trp}Ox54zh6~_28}`=gw)Q%W z*b#RjJL*z6}uW+ zkNwD~i`c}Lt%@|`vAXi(H7-4psmsp#Gas3i+sbf>kwlJLcow#v@Yl&W-!3+Na3+C* z0o+33buxCc20OogucJ{u#%VsJRFCuL0nVAQ71PH~mIvXJIX`RE>I*K#uWf1P=;gPM ztu;Aj+6@G0NRdjav6M!6%#O<$ep4-W=J0|#+jbk+o3*|TTkWsFc+qjTz{?Bb^vOfz zs5m~e`h$~BWNbJy`)8l{#Iz5k`NgYC=7w3C?986fB%%7Tb(+Y_ZN<8f7XYQ#ysoEX z8jl!xYHD{aq3%!O%h?}WYw_0s<8NxIibO-@#08!LjrlKzR+QJQ6dN|OVr>f8Ew{Q$ zJlyMOsZP=v&Z~IfKg#Att34ONz$l}hRE{goo-^hX|7I&Z+ciM3VldhJgSVaB+uPu% z)75;&U8Nx4XbFDY>FQe!+2W@vQ0+IO0j(@7zK`Hfud$A6g?Uj3U$587!1t*(eEa<* zg@ijN54#7{u}EBQk9yMfoNq4eZ96F7XpEo6+@l<40{(?p5CV5E3!WiSi#M}3D_ZJ{ zca>>3nwblgoc)32iQ_m=VIg}JCmIX$v3ZOoz`?L&ojM|yd%~()Cz29H$q%XjUz&N33&d|3Xjzeks zPDJ6keZP{A-`bR@mAUt_EKKNSyH zhs@4mt{ee+tZ0PnM>@PA2ziR^JJ8d&DoL~ROCqcd%hV~MXCm2r-UXMDzU|(#*Tj56 z!3*;GG};C$`E$`b*jkyRa_>3Hgs2i#P!ZGOP{pDy_q9hYkpciZRoAXXia|qXRT@JW zq*9vHT=v9vEsIS))@JTby7K@CKAdN>Hq?CeytMF?Ye{PO%As?{3U>K?_O@{|dm$FK z^)R4L_7=J{4T>I)XAzsU_RZA>ZP(`U1viky!$Y6WeOTISOs_n)Dn1?@IJiq-Lt@8X z#1~m6!FMU3wF%ysx#kZ+=89kv#A+8r0!^+!ubGD1m`CdCKHzM={Yk%bq&PAapka4E zZuvo}kSZ8c^8n7!i)3V`I0)}yd7NDu5%#F-f;?|_S?9a~A-7%)#S&NVi4a(b8|?ex zww+7TuCEJnf8gbi(z}P7HSUOCag>pGxrfbqqG*jJwU+j!1}c~~Tue4>)`hB_!C}P! zFY|<5H8b`a5=fFU3aEu+P2y&3jA}tAn-oEteUYIg86FW(si%(SQ;_*WP(;Sn)_wjX zD1UE+!w_E{VDjTGeeZ>75__Sl+xEaKJ=b_Iv9`8@5WCL9g#J25jwAQ0(~D6*V*kCb zC~8dghQss4n^xJY{0V;Ry@fGQ_{BapQsUwZEUW%-IU;xNKbP{cBLW3pW&?iD{d>$8d- zTsSYGL1mK_7_BZ;{*$A}%Ekg2M%yo8fe-g^>JlE!HbJfYKev?E?s;$@TCv<-xLd)2 zek{m&YwYA(ny(EW@r_$Cv~ko59otDc-0Lr=c#JcBSbsLyQBnNx(+yW1*zUm49G4Eo zseknzj8bVDar^%6KsU)Ch48e~8-B}f#GEwEd>bd4wJjOV%R-tb@Aar|@Qplm(w!~JJMdh5O}fp~XBGk$q8+^%B)e=ara>Z#L6UzDyGz`P#atkf&V{X>XxVX5zt|G$1 zAFctw%{$GZI`ZXa4U_c;Za>dFQzLYy?7a||N}CNfo*h6~uSH7zTwWMs+f-k3a<5!i zgUiLi^q$wn7FN~8`FSf<{pGH61M$pb?x;>wTHAhUDd>e8HHUl%Ot32nFy>;zSfxDl z7od!|0bFqorl#{;XF%(ZzGL3rsrSo1T&a|WIIWY*MOOAwIAgH3)ewT^sYwb}`1pla z+v$~iaP$7o>gtI{0Wrj&Wjk4?+4p|dp3Qp6Fd1J*(hvRcP>=_qZr$lK&kqkgi%89Nhhx6zy14&{r<6q5-pSTxF_Y)ox*`(`@trv;#1X{7I54^X#lZP|lbI!W} z!8QT6t6<#e>-kt4V7=k43Z%5Z`xJ$;>!XjNRcq^u!qdtdpi_R{vYp<~bX=C%rxqIw>2HA2+p7wfTedLE*3P zlbHF-^HuhSt3x4y=tz3B6BE%~`E|ERPZwSueL>*BIP^(Nj+A2e6#q9H&vPUhuuvI1 zTniSq?%PIYndOqq0n~2=`}EU-XwO{Q-Ay2)k~`mDjl2d*HYR3 zn?30u7ok$6eq}FivF(-dL99ZD=Vr;S;YVI4)x+L6Z#Cm>f`e3=g-}CiYc6a;z;4kH zi{7rql)1DKlH9SOx@Y7GT}lsr@a__f*)b#K&rThNaj)2O$0CkOutB0kCtNs~h5?EG z@jPlQByQPkLRBWOW9&Ma+KTG>m^z||6-9QdtlVm}G`tQAL@pVPH{z|#v&m8erK2Q^+8a%Z7xpn= z%E}5q;f!x7ckfquzl+Duj$*>1rADQa&3lL+4^8NEWJ~bGrZT2Qrw60!#Out)W^LzY zdU%yu%NsWVHpkU-B!kt)dc(eiYf*6i%f0e`r>#LTP1L!gI{u(`=2Nv8{U(sdE&4Gh z6IK3n@m5b{E}*A>4$q7V;%GA`qU0|o*vsj~7L6O-L)&F>#41@%z=P0CYwx$8WNcFi zB(i7Dc~NE4D``0dIM`bcJz8}0+9q}T{ClQ)6%rLlAv z$L7CW%P%eB#&i;X55C=80?YoJ0%8yxxi=&cKv}x^`~d%k=6eouP-i7Fi#u zX5&SBa}~2nJaBA7_gIt&eJZ1MIIJIkRY{D_;oR}lFl+c&_(9yG zHqE@(uBnriqIDxv*rsGTy1Gua@N8pCMq;y9jCnLWnbc`*C>Jkv>l5L-#~9goijV01 zv=n<$y^GCU3RZ4_9)e`I)32lRbQJ#%T%^Q(ianA$bTcC`ZDa(9xd-~}o+NUudpE=U zr8277ANIudYEGcQC^8>B7k^lvF%|g5pu=!@!D?@=YAoKAB!?(|$-J1L8*$XaD8*h8 z4BS0{jS6wqjC&MbJiCS7m)@gf(?lzEC~5JCT`2L{?VXI_AdYqo&}e7jSddDP>t_5T zldGEXL>z~&%~S5VQ8GSM%R7(}E!gMxz3l|C+;z)@+VXP;!CmY>IvU`K)iqi21+T{ia8{0%t>at;=-L_2Ol6;$X!Py3=OkLUU64=TQYGdf{ zx>`I+CvK(ks2;t5zF@yb4d8oZNG=Wq9kjE1kvy}IToxC7f`gi_(8Wk-a6Z)l8v@{Q z^=&U~9{c2lpGBDfBIb-0g8=^ zr{*5*?U9Dvtw(41TJbhoh6;Pwt#Pzv!wC!RJI8IK!~|9AW({A7TUn4Zam!sI21hxW zDi1Y;N3`c{LP}@`oyJsl8bh)-t{Ewxh-m3c(t&OUJS;ZR8TPmy&TV#IN_{I-nABEJp z(~+YZj8*sfz4l;*#J&?)(|GV$kvIacCvun5-ACQ=lYyCQYi4ywHoKZSrk?l1XUsF% z#3t(^@CZGoZ6z6>2ybT;f$8HAVCSEyJqF?^K*`=IB2iUwc}iZ@jJyQ;ol>LJmrvFu zOrlIPC=5LNK(FJWq%vp1>L3aF!rH9kGn*yF$BI{u6|bn(u^0RKT&Wso9;{6I=Tv8$ z=F6%1KN5l_KZgv>Iudr7;t@^t9}QINMJCQfBTKccam0hYk4^0LWj14faL-$DNHHM= z^c4{Fm*WN^f z4u2-dm|DQ&d7C#?J#7z;vZ$chnMOSxN0nAOAk zPvBFw(`2Ea4v+WgG>`b_?5V#CVf4MZseYvpYbfY=S`}XM)S3rCxkYOreE~_WaF<}n z&UwGd>9P~=+CI=f%xn<`ckMCroMAq+0zdt)QiwB$Lg-!xik|Mo7Pd}ao?*LHkG7F3 z3OOXQ;*ia!Ek2)bdxrjBHGD*7w_!qd07)gcH4C8AwMR>mbs2~MFpeG|xTWpvBY$T_ zalrquOH-(XY8O>{qdGuyjq)*BX#OUmx+3hiP=eC`SAzb()h|kL);p_r%+~Pwz)5ya z2N6|v7t_5Iu_be9#^d{|;iU1z86a6`Dw-DBY#2#?DRuv=4b2E8<${xn^>9p0mxjXH z-Y7YlgH@ru)!Shley`<>O2dYw!=2U6pmyl>K(yiTWTC4)EU8?V0vs-Z+)JVY``Q|+k^ zS+G@4AQpP{$zABy4|@`8iWl@bi8DEgCgGfBHPL60h7v^Gu6(hfdnNmr4^}o;6`V&? zJmz%2@E*QfO*>BF^{Wf9A*I#f=NNTyRpAIi$lbFL{8r0CV zy5=sQ&MUnI>6AOM=#=v^Qic4g=Y$bf7Kov$sEGNlJNz@U&kKzyQ9av)n?o_$#|fKU zDi^k*HgT|`6B3RRJW=E>>!0o~jX1Sh)55t zIoJq+g#ez7T`-!z>enNx4QEWZ0=0X><_3Q3Y&O&ht2ssQj5{fgosXA&4FXwqbw2wR z?iOY@max+PStKd@u8;tBX$|3HDH8eV^|Tj4i5;09aaQpH-5lIj7QCqD&5uAa^;5ok zL0G&zeb@4u7eNOCwROfTOX!fb$~#99R$U00dYgyfX|mu8R?M5~9k4su{UZxNQjEKe z$Kgv0kL0G4I|?z4Fe`%XR2do4I_|PrvM$&2@A0h8SI+iP*ttOi*{fG(t1O%2p%X7T zq_RxiR^cOEy&#)=4P%OfqHzlJ`6|f!Q8FCSJJ7p10(>%T6JeY*NrN&pY4^-kMTF`u zrhK4nRRvhDQ!pcjU)*}Q+dBl!voTM{3+SlpvyW|c0OI6p>?PD#5jI83e6I~rEeR)UW;5|Svf&KJxs3m z(O>1s`%QN61@MpVF&;jj79Q!n3wL_d{_djo*G*Pl+k!(TGuf3oC+ z@!vT*uiQmdkfH3pB!7g7VoP@XA(*yH|(rR`aO2I3zsS~@zV z!zT^p_9duYwL)SYz8**Bb$NXy(PtfVCRk=HV%o#LDL#K`^|7peZn;sFc;&{%FMBSljlxg7RrpTU;TJ>z=|oJ zI6mDSp+c%E+rRDR${%kdA7NJ_vIhdWrvLywZ=XcE<0QWt{{2I+FVW zLciDNh}u@>*y`0mQ9yHl(TTyj700HB5A^DjqXN#$h10+M*<5@NyfNAX7*XK|ho{Zo zM{$nNaR|3N?cjOaLabG?po1^=rrp_k3}O`t=*Fl|hi>{tQXGUcp$HUyFDov{Cp9g~ zQ*$gods$!gLM>!L{V7mQH-Ic?@HIp#>%K07m=bokBPtj0`8DU_(nOpvjw;WuQyhO9 zTd-|Ro+*+;iD$d`ze~wn?-3uIS^Cl)$*h>I%9yS;Y3C;+R@N2JVirp|k+oX_a) zo$3e%ovwo>$t%o|)Lzb34Ls?a>vkDl_1#Xqm0LHperzLP($j=TBK`00m81LLzPEl? z4^_YSc~F5}nPDT^J^z`PH+7f2LIaGNi%6=6DzyQUP6FW2ymS=DJ>K|#)+rZtK1jhnDrHPHli<*pMiD((6G0k-`2UR%0g zeznnmd*oyrf3#n(-O9@A>FJ6k*e29yIMr%%*UQb-7281SiXVQBqC)rT@SWPTvYgn2 z>VUovgT*;yl4}k2%7_FO=V7Nv0}DV?f9ADu;6jg*;i^ivu{CVkqTSJ%uyrC`qr2P` zD)(+oV*ZO!Ek-WZq{B_i#_t?5pNH}Zs`K}Ri?&6dW-5hHKJjVM$;`Ssn@PD_PQU>Q z(8W60BxbNk(atAW%6E>A;*Jss*nSnj>Z=R}nPn1}1#9Z4Q>>Sy`EGS4fypl8-&r)h z0ugwQO&eI$GO(XFyqA{3r}*yibxp=47eaZB6%%bh*g0mA2V^A;SZB(TIDQ>+@AC5S zkdX7>MC*`Qe`6$gk`cEz9;a7WgLmqM1P2|8N)^niLVJN(T|b|GHW**nhU!BS7^OyHCariP z=Uu`V3KoTXHLQ~bxCNsLyCff6(P@mH_}Cxyh;j2)7??%ayo$)E6wKrs9|;K~O6UX| zA^EpLSomqGzwsfkZ~I?P%rAGK6g6_AC?GWabx^pc{tq+KdnrT}e z+(+fAuJ*>=)&0ZKl<25ZT~CeEoAr_>01gyE+Uiri6DB{9la@Zv+?0(>-&vNycS?r& zk~i|g(tA-~A$^4C;^gH)>MkO#j!DnQ9rqb1sdQAAs?ZoI-&y6Tn#+h*+)qd+2@Tb|CVALtX8DE{N&ef4sa1i-kj!k-X-@n!7wlDfU(zc~C&}!$%WqG3A!|W?Hv%ctH5^iD-M8)4h0L^AO<(udgVg%Rr^XM_E2ex%#f+P7X5IiFJhfE&0`$iMZ)^Iu-14!N zWUhO}oX-9!9`Q#2M(Y_=Q^ zrL||>QbTin<`W}l#ywDu#iGCteW0PJDVqpD9lu-!#@u&pvG+oWt z$1)12X{y4|0-jzUpU7O2mdJ{-#Eo_PH^=08= zwuI2ju2(B3p+=FqJF9u#ctZ5;WpMQu?7}j4p{>W9N4g7yaSpqdMk_4W&Nqil>!Xd~ zPm)1a-ATuX{IHnyJd!tjhtsX-?b~DJ$meu_nw2C)0<>}48y3`ln8 zf-Kn2U2K#}%5L|?c{@_9b0TyGv@uuUn>b>&*ozhP22a%Rr5{9swBdNw*+W#3gk z*<20nE!8cBY#r66OT}-x2ZMGP;92Z$cX+#q-?nx64_1mB!*+)2J|vR+N83`TuER49r#-i)|dA(xp}C8j|GEZ+4mMy8At z-#WaRu51Gd9OV}u@s}C1owE74_2!lYf9nO8j$1hodYqfQ&}VF`l!)8WRhkkDP6X*L z3Nt1Q7}@2aYHr1Z8oMzSIqb6t5b2JJm{KD#C1n=tK z+-}X?u@J&E)C^Pol50E#toph#JlwtAJ$$sSb3X6xIKG^;3g!?YP4@2W%4r|gqN>#c z7Y_W`i;&I!ZWFdHW1aJP2eT;j@W97jC7D<(6hwp77ht{8>)Fz1D0X_r&xuNp9Z~7A zPVpTuS#oJV^C`oYRG~g8ci|V6=Jl`gLLH@C-DI3RYRomhw1EyS3 z_JODlEy zbmXVpPruVbR#G11f1*Wa+2$&&jKHCbf|gn;N=J90jAfLF4b?J_DbWh3`jBUe&*F`PE@ui>KeV5|h?&VPlWK+BW4Y z{yr^05d#C5wU6*6qQ*>g$7Qh1pNCwpy+yM0xMb%(go;PNq5_q0$(a(53AnT}oHJu9 z)imOT;~5CY^q_U`?yet&<-}+`&=Qr&{)tm1>iRw8vsbQ`=Dl=}{7ORDM0Q}yP#vs< z-&%B=%R-=>J*4LnqQ|pialJzxT;gX5&s!oN3nmSx86x*F;zw}8ZM6GLpR07wbjekT z&fQZ4a!F|36V-RRL6>$Fn-5JSgCVt(HpKj}_A5$hy7{(LoPQO{bWf_Id#ks;(%83E zs9~h`VA;un0%e~Mcz(I5cpTHrR%M_jrQKZNi;K3dZQvKENo+bo3t3wo2@7+qioiP= z%a1CzkPow@iG(r)^z(K*(p=aq@Qp){DW0BBy!&Mjjvlg8RCiY*?34sG5Nz8pu7a{h zN6WuvqBW`ujv~IS8oZz+;i=Dm_EmzV|C8btFzQ>46)GJ`3pxn)O8JmTW9Ih27-NBD zX5mG&*fA;1%X-w@N24Y?st`w3SN`;+r*%uL3Uv`JiUU)=17FYDTkj4yxX>fiUrC=wsdPUZKu`?jog?15gQ zGC(9M1FUE_5WYO$K`nja2R{6Fa~fY*r1ku!>x=4dw$mB8dCVvt?xT9xvezC*E%$y} z?cfFtwppa0^f??e)p_sR=WDMwt@#)ApU*pEh1@$rE@fNjB=C)&U`5rSdY34#+5L7X zh)Sl%XGY6?TSmX^GEtynQu9qR9sG9>`M@`*c-=fd55yX?EoPmi>>%cTa;Y>Q8r(Jo zX}&si*oALRIeNU66fi6(dm<{p<;7QR9}vbTwD~;fVEJpYE#|NJm|6^E@PvcZ@iSwp zP6qN&{E+E0KX(}u{w(_R9XkM@Y?C{ z#@lS;Z1e=LGKmDg!CcdNyAje_^C^QLM2CU{)7`2`*<-!Qv+`XW-?0B0LGsm10Ne*i z_H#{m=Ql1kHdVbEM~xLFH=}?k+~^^0+TDDUzuTf=MCtDcRb*INWBit69}j*M2iA)Q zM?@tC9$6$bAMRf-mG|lT4a_D+_?Th&&F)^Uv-%l!~B<>@&+kN+eij|L0+O23$~)snt}?b zs$lPrQFLoormXFRnGz%S$O!IS;vM2Jh!>YCSSxCDC!RE0U46`D;uaaAl@?J_o^~Y% zouZl+p{)Hp+@PIJNRdDJBCXGz?CNL#l?tNI_5Wb+J-nj+zAZr$iXaF|jw&h{gp!;D z1Oz1KR7fmxjztoXT!2c>l5++NkRUl0Mb5d%8Hya|gWqSbUi13*R=?@>W_o7L9{_bf zH=MKYKKtxTC_B?E4e?|M&I%mJ_w!MFWvueGi)QPUU*BsA(Oy}6OL9iIMP0UbqFGx* z8bK#r(ULPu*&Tv}L&M7Sx3S9!J%)VHRHc+iy^)#`i`CMOMv(5r6wf~qA*KF=xNG!Z z^+VV#CrG6~i}PWL7d(9p9*_%+OJO@gA^@Wah?ibHjvr@Cd@H`br4-p>cwHPWp){oD zZYF{>XvtrQu8jNh;5KNLfUI3Lt@XQcmrCl|w08VK|5m%5f$5IsRP=^DPhp4s-M`4kRTU!!ow$ zVfdfRZG4;6ulmekw@z;^;|6t!r&+-YS9!aSQ)pY&`L|4wT|@+t*SSL&z=pA;tJu{j z9-L(%lJKj{P0r9>t$H*2-;Wbd;f41>t~6t!kNyyKx_dB-nFjX2kE_H4jG#;pHw+b< zfp^G*f-mKAaZ=^_WvSlW-yVxJ=Z`Bab%DfvGmmQ9Q>`R%?qU7e7ED!?$3T8)Px>9l z@7o;rPQOj;v%9@s9$90d$U>ftU3oCu-vV6D!9iB!VP_8p22g=ZmG&%As?{xNcvBah zv>TawMVWr-)}575bx|MXMK9hnZ=S~cN6)pSp*{m$ctn#x~NY@%K zEj&&iwqkI$mpre{=J9*Yx#9d`t)1yK9!n}x-HJNJh-2N{iDUG5leMSNl9pdGAtf0h zdjw;mYm2i*Q$`_9gD*8=xQm3jO0W!8R_Y~{gvN(r1CY%%( zOac4tw-2JO9?yO|sE~0hCu)T~HmhE9yGaFeiun(R1eWpVrh#&xNgWAr(-&8{L_WKd zrxFRFr_qt!v3um(V)fHDHrAiah`W$3MMStm_Y|H#L+yRyj<@iRr97Bx17BvbXsh%L=SmG3onzX&;n`B`XvG z9n!c@YMMMSbe4ukJ!Ij)8h_&^Bmd{?l=UT8fsq}Ux{nEQyOTV>uZs6ubv;=& z(T+*=(~CoDc@%MX>xJi{|Bcx5+5UIL9$88huQ&o>dG;NC8q*Mp`>SYnqL|l26Fw`E zO`WgR+kH%Ee?)$jydQ&-nQa)oHgnlfAEY+ztC+PAn)4czJv%zOkbKkC&Z`}h;Mx0; zN029r)7(7qxcS5*^|YH45n~vftEB?F$v^;O@11=O0uUi56jA45sN3wq?XwS^~py(CQorFgHHe4!tNA(Y@=kaAUq_9P)h zaS3G<=Ux915N7jnE4*hrdm0oS@B$bR&;sanLVPU(+4xuBjR%cjXtRkWs*nHlL`Rsv zLoWJTS{69vuxrT0>Aq1Is^#fr?_Hkdff(1%yOgfHgKS$2b?XKdIg!Sm>+dw_Cg=i8 zKy>Tg#mnVa2b=kKq2GCy8`{I}bxaiV9A9h>Pui+Q)t#P(WK%cD1hYNEVr@8|X7#bU zKI`7`JmaJi@;vmzWzG)sI=KiD>d2g`Qe&m`I$@wTAmcgvH-VXBWJG}H+HX+k`6adB zV_6m$pLNP|PIG3iN_!r!5*8l!vwu>O(8YMlTH?P%wvhU$U^eN@m)B*%gP@`ewteRx z5N}e>cEHI2>Y2HstVnIBHu>1 z_};RD%GM;Kx|hXq)@2UM0EwS)`1Q{aN@rD8{~iLf-Wz^oO9;F5gd~i#&;|QR zz9LM>C^bi|oF8NcI>xdqRgqm_Hv5pT>^3-5YT?TdsCQCBNfPDWZ)CDUVA0Q)82@eU z%zH_=kQk8nT*u}cx93YZ_Rguo0X!#kgXi*M4wTIZnDcInAzqngBi^$tcfh(=`Vt<0}PjeQ=G;x>SNjXGT;v#s*UITrv$}Vm7|k z@Y8zYVR8$#+i#O!CeoJ7O0X66cgftM?B;+$FZIbWjUZBkw z$w|^!jH69xbxF1o0%fZZ7Zi_(ImAdhohGIQ;P@PYb)7QD$z(a^Q>eXR7GRWgDpyeaFYyfWJ zudkdzP!#f~yiokQ0N~XS?&6ew&2&6xp+IC5w_H0n9QIksEY$|dog3%tYl;fwz+ zT}GsnS}y$u`<;Udvz^pFd-}HIkLv;{8tpoZDN$CUGn<%%4#y*(pG1kZMFq78|_4TGV4=QNRd{i%vG0-Ynm zW=5Wm$S9z76K-?3g1ylWKC6gLhYi#!SM+3ifgH0fp+}Y8%wMVvAr9b)XXfw&m(-C_ zzp=7;IizQ?2d_&H%kW)74s53kqck)9{>3SDxtea(ygP7HSJh~h5Y2AjiS!1>%TVDS zIhya~;!@((Zj!{xG$~EnJpm_CYyS?t^zD%sy6&Sq-(R$O`}u#^1EKADtGLGlxSD;s zDbDfm4M5}$zsjT}4oRn-eQ)d2WEAqr_?iNbS&4x&IT8_|dg3`=Ss7tt-}}ysxZCbk zehc?5GWkNgM5%X_8zjRRTIMiNwHR)cw8dJfi}FJD!fcdVh(wgwdTR`Yh)>jgugdIo zBwD!zICw9g>t8I!uH6+wLxPF0wiZbZ64fpSa6LFVgo5(&kRWSdtW&5#=iYii-Psnq$c6(Kn}39n&+{6omyEU02gQ93{sFp7p#3LVbl&^-0+JIxKmkFF^4F)p!MrrSx4q~YsGlg7L=2bxbDmJw z2l=}6?^ZrcBsY=V=EmmYhAlm=YXh_=K*z+Jkqwbk;4iTs)M4EL;DmnUv~C)S^W)pN z7Lxz`b+%l}f36?=KPaL8*Y#|}85g4KV;lDhNem*=MiF@2J2kkoV&h#k;7i0O!xwnm z1b=`P6@!^{FHj~hTzFm7IRCoIg(Kn_!1ZGMDL!FYY*nj;)`#d<=!%^j?PuUlo}I{i z35VvY#nwY@!(Jd5-K$ZiG)~$ZNT73e(6hYma=Yz-Dmi-K^(UlI>yusb)FqC8`g4Zw zjs80vP#NMO-4Y8@8!}g^Z*4=eS_?;N_fxbEuZ;d!Y&uR3D6W-L`f6FRt%{Dy9QV_) zQZ_QhPHw0bwDOt-gTb>N!KG_*-$R%I*&ZieBYZQw5WbXrZIqzEM|JL0u(a=rF3*1C zcGz8S>7>HGb?C_ZtkTuITOfwt66$ezm``A!ZthAd_=WsrgJVYJ!p6k(e*#Z-A(<_; zQhetWg9zc!d84UoKlGitmx%boHSI=|C;1Cl3Zs1X*oei&SL=*BU+h`;lyJA-OD@~S z$YL6tXK>oPeRBp)k7Fe{38)oL4mzqF=Wa^S@y_fZYQr2P^LTGALz@k zJQSU1EAG(QBBSlwEc0d0?6q)xs2`S9%rX&|r3OJ9rQrlq8`-bYjp2}#R@F**;dwKv z7Z!5ptdZz!gpt+SOZiRJ^ppjUH^q}nSsaX*D)>1cFj;~^G$P)H5=4zqPu7_V_40-) zjM0B3Qd`Pw_q5DTB|*F`OrbiLX@^E#IZ{rWEM~j#$_PQEiv^cO1;ue$MFL(DYq_eU zzL!D|&C#5_jJ%eF>GRZ&x;}+y5JOu~)*-wtjCa351-gE^^Dl{5PAII~A?Ig2eyBCz z#X#lF!fu*m6=3ZT#~;c8HCer-GuGz$GXobH5O6HgFyPYmgOoPP+@|CLl%HFs^XdpD zRuDOb#6rXIHHBM7&auhQ5L%k1BaDvr}XD zi}Z%usrH1V-s8v@-D_2Hw8QzM_j`TbTLe#6^zAE+*s8QFI=+|b-7Y)8(J^3&^FoOZ z#|3idZdiFY^S08FReZ1Fl_wjm!gmo@WA*T|Y4bCtzENq@wS~W%sVD}eC7e~Xf3g|# zJ-ZB(tPgFavLaqGb~bGtk>^AnuGT?KHFQH0`d!3djsk^s>MEJt()Nr9_{Tg{~bRF=c! zVpQ)GmeRKCs@{vqYX{P3Tu_XGBFGgzG&qWw0P5QOf0vUzX3z!<{A&@tfZz#<+SvG4!qifah znf#b&pl;(9B~$R0vG?t|g6eR~)>1_yVsbn8wX8iY)q2b5-BeIiG*@R7LQc!4Dp#uPd1bxsF{3D~ z*4WXYLx$pZNlBt+owF->?&j|9e?DzV#KyQA(6zb_q2hd-_w?<$sQ`Yt-)~Z^dI`sH z!gV989NrAhAr@+$shbdeu!`LIs|Dz816=Fk z;ytB=WL;5ugG~fnt>LSX3`GBJ)*)R=lo?88F%Qe&`ZAS=B}(F-%OcIa?j>JP8jIHG z6ejK{McoEE2|)Lx05mS-7APU-TVc_dl6Et4Gt+l4u&32=;u)^*6qZG9mYg~3D)6mF znPN{?JQXvJwLN3&ZK1R&dVDP_@v1WSl~;i%;C^tv8b(DvVnE0NN*?!&Rh#Wyt5WA< zzQ(4$U`^V9J{O{OCZpyeLva-EETLgqp3v%o9=m)s)~D9{{GA!{Fs1q>VPd9_crQg| zVoW5Egb1Z^u@s60JF{v}J<|gU5O!y+`O{f4N$k!Oo^fl{EZm6eI^kTQJO5@fkK1gj| zvVUfj*+$1J5r6BbAqv~3D(VBcogd&`xEufSE?15^?s9WL-{H5F$}ac;A`rdvT>EF= zxoYPU_$q&;(u)>wA{~(z+L|;6;ub(uJgfr&J{2hn67z4Q!aQm2b-?k>9zXtT!~?+E z5bfGmQ6|1mo&($(MyJUJao389!{NJ?N?p1(Xw_tnFq;|0O+_lFB@7G*c~=KdO1x}N z-iSOZc=oQo@Uu~T6XLhJ!k0-b^|#ZBpFu*@zRNCN9xND9bdrT?Mo+McS#L}fQ`J__ zuc*Rzc@NpGO^GM`QwP)+^yKCDB$Unh2@~&uPZe=-JRttO7_?Znm8uSC(Ac8an))hd zwEzNcS6R|vksZSkaS6MAUVlvcau%UfN$PX-LH209+lBNxVQ<~_1n??eiMF%Qn{qvG z(HNwWVK(k-`oZPZ8P@|$!v6?>a~n{k--jz$9ZdRvzV@Wk7imqcig7-YoJJ5BM0%9o zNZaV1;U^0d5*(%(4!Kmti+y-Z zwuz$LxO^$ss6MaE%y?GYlGnm=M9KV&wJ#zMc2v%fEjxJ@5b(f!HrjWINgln~%|%ww z>_=-yAD(b8!_K@#08J1>@Qrc-xBDOE^whssPU9?r#Br?Br&i%O-YUnU{jS1~4Xa=9 zxu`(%K2|(~zT-NpJJTBeF}fmp=NJ|poy zL)&m?g16Z@e^j2!|MaH#{mxUo@@wxc?8+>1H|9l3&k=kLJAy+E4w=V<>l%p_1&ui- z4;NlYch!vB$;>zC+7W=)+>N-+InPL242gv2e=|oT4AHrXP^~fedIw0CPjxk+^msGh zzVmvjUW@vI(ewNFVu4X%H@{Bxog~bpcTP;FaVzw!m-8F0cA&$5rZSN4-*K~T8sj7! zRiSq2vz+(ZwgYU_sZ=K1yNiG^272PnXg2hdqYNW=Zom&)w zQ5)+eL|EOLUS-b&JN6vc!by_yt@6`axq}s+^ATeND+E#2&B0dtv-k9_=XUkJkf;L^ za;@9lZ~YRyOVcpnge4|5Jl>@j6uYJ;l5J783M~>xvL49#$E&6o5tm(g^{kNp|BhgmXB>Ps@KGV*R;x}!~m}S zDL)n?U3WSWmYexDboTR;{@#mRN0;0__Gqxjo-=I3J8R`x zgs$C9&2!igXx9Bd2bjdDPzB#fI<7NI(ci8@AI;vRG>>~!S09`)EmIw0GJqfXuvjmb z6X09^I9%)OG`!w=hTVh-@gqm>=y-^JpzkZsCUl)H0#6dZVcir}SzUU@$@#>aht-4Xgrv_2Gr!B5pFe>cJsojyX z{;F}WnqyCHLgW`n|BR?T)$K&8F5Ijy_;>S=vM zojlAusR4du?(1mnj?1lkv(Qv;oGfD!@hzR}Q#HA#4-S zj}PRbOCG!mFxqG(>3O9J)-Y;RoZ+d(`UHptl}+)@U$pz8m9SALG*aEuV}#0d6BpwA*6m9*D0y~lV=CZnO1UoJ&{?`)#7!b zkfz-BUH4%!KcI>7#w+8~FF9h!`(scX|RZJrHxxlS|&`^3%g7I{o6al6)*po|*<4yLm(dq(!pfeaYvedz!JM zO-i|v)<{v(0E$FZ(?NAb@E_t#O<}sc6qM#xQ`I?UGj>J0y_cg%d$k$G}+ zQ#mN;j)Kt(sF)_v3yEJ6V#-wf>%%W)(VhR zA;Z^Nd+6{0W}F9_J_nfzBJQ{Loo$+=UYeOtuv3-= zMGNHXGhK!CpYL133HN+Kk$X{e!D`S$Cr z4d@oT%ya7ek$5Q`DPB+>o7Onf@uuN8sYEZ`(ELZ<3%;j4siy?Rey&kUA?S76r^x0{ zr|q5#3ie=Su+KmH=Jxdq&LsT0jxh;#tNDmnZi|3<2X`#t*6^ww%;FW#|kUzvQ|IU0> zUkxrqdwI10@9W+eLq%V6jnVu5iNel2_hnaev?lH=HTXZIh-H(lzXCu3HGgR5yd7ZB zzA{n^$nsxc{kQR0=}byIErcqQ39~O{(^ZzSv^;p0jq0ko&4Ks%o$#LwfdNO=1G8WC zlsf{D&OiOoQw5OBEFV>9s#HE`6WZA8rS;1KexpghK}`2pZW>e-V8pOSR~Z0L%?Lj> z!NGm-s0-)Udm#T2{~iDT>)m|*3oVFlO0}oD*P7FI|4OIQ0cW#1nDD=KT8#q{_Xd3A zfeZ+ElO6(Z@U0mp{wE}T;%ru=k&i85&+b4?%)1l}6k)lbmZE@k#vhda?vwPNbT-9KM@i;)LXa0BeV6KOmod%&!bUSBHDh(duVIV zv*opoyVst&97&YqI|(T*w(WhO1DqQRAsIqcg?%V=XKQJV<~HWYPZe0WuxeIH)9JY1 zg}JHaxe-}!AfSHL=36PZ!CQ-Iesz8Sd6mV5HkQPUmpawP;xKt2O2ef{N5XuP^+8j^ zHc^`l1>m5CM(DZr^abMS49LSQ-W1enh$E!e_^QlBD3OPRS@T;qw90}%EI$< z$=Ma(Z&;b>3pqCRp!d5QcP#575=!!2EkZy^G`0sAbhXIzycBA3dTC`c;Q0z^ zYk3Y0`w0k5Gf2uGk2I;fy7hK@p78`Gl?TF&V%GB^qzAQ&NOQ|>&}rp{iRC%3`_Hx% z!2T9RF7w9_K(KYJHl?FZY$gY=(l=AsJ{_!JEwdcHEF>v%sSegcUy*KC?5UogtwV2R zv{!S@HtBh|68?3}I@qc*DTo5=>CQ6|oAGsTC8)8cVzKCzuU#P2&PESk#p*U0%foa% zY|-8ZK6vUkjQBZYTLrBPZo>!)nbESSm=|{4;V+&wJ#{;ua5|BBETs;9nXEi}N2%^5 zCwiM%0Lh?+%@)UCZ#i{0$*TD7IKVduv{nJ4w8X80%FUH$I%a#XiA1gzzeGf98X${^ zt8%4(z9H>zL9Bc9o*vjLftrag;wB$A@B9M4CW!l;aa(g^THhkt$R|L@c^y54%#MNM zZR9{?9|-&e9sj`Ead7erOD10j?@Zg~= zZ<)6LNu4Yc8uJ<@;{|nE_^w97qA{t2v7mSa>O^g>5PN-SF9ZqE;?8;&%Vpw@naZ9D zGA3>Cw$!0t?%ui%X)Dbo3BgdQO}t>+*bqc}Ys}sUsAIukeS&J$oTO*h3*f=4*CV?f zxKg@Oy3;RWb{N}r)R3N4zj!!b@kVc7DYm;i;Q8rQD}^_(8!_y;vlAW`l=XujlmQB) z5S}~?Jr|wCVOOnP`>$Y`R+1mxr8YC)Ht@v2Mplv@OUu#;YOb_tV@HHMo25!FFQ|;Y zzUP!!hE;$WY<4Zt(wBQMB2JdN zT$?<|h9Ptu5ru*V1g<~i&M^40Imcx(`^zJiB%TD1$niN>@_fopUKSYT9(Ames3bt$ zx|VM5UxtbQoQIgpXv|iyVqj`h0+V&q?6BS4ApfHG+xPDewGIy=l0SF3QL~YFHg>Ce zf^|3f+-g45t&g)Enr7&0SC+_Qx_w_YtYQu(9lswiQ4v)>J^B;wQRyC#=(zoJZsl3s zOF=(BdUu}Z6~f<4)A|mQ0;_^Pn#uec@`Lsz_DY&TH!~7z_7ClRAew%(1XXz8UtuFZ zi&1pPaUvI73FA_HMI!<4yRDf`jdWJ0<&4)~y^d&)AAI+E`yR!C0_V^3%g!n-yNI|AsmongcSZajG~T2t+^D%y-s-7jZ!1cWZG2!mO}mgrvP-;|6V|v8_JWPTP(=W1aa-xAj4UTVE$D zJkl_(L*gc48c#WZujM1w2IGY1wCE3Grmg%4UB%ga=m%zx&ku#}X*e_xYnNo$?pXXH# zX_oiNCnuFR8awA&gsCw6R)YA7oxeF=vz=;-qYbuw{^IC6Na$B^X~biJEAp1T#hR}| zw`4oxo+8;(A9pUj|_bur=X9C{u0_ z!lyq?n1m@GA>RuwwA77d2J_Nr?(|2XZ8tKsisrNyXsB`2Wu+u$lgn|ZnND~<9BbAR zf*8c0d) zslk>740vYM$AtasvMz(9K2A)oSBpe2?&TI{1R8e76MUO?@0diUHUrhbT$v!cf$=I= zU3PoPCH*OhlgX4G5y<-p{kL~@iB~{&cuKOZ+}LS-=F3`u@k|QHytfozRA=liE+=^obFh0aC4Fob;d<1Q2XLJ_Wr-mjFM<#Td_z!Y_5 ztJ&CL1BMgg{B_gDh>3)sg{)lyy|i0eeC9j_me%cN(SW69rlCw{=9%|!W|Hkxdp3(L zm-d)5L(X`gs~Tmv0NS_gbWL!>MnU$D+;s($v+0D!F39Q_RW-}+VM!&fW1elZ)2qvi z2GNV-=&Nqp)@?kPjHdpib$w0}BmY+mAYf)f9rlWqq`ia%mXc?jp1)_{Y5!V!6(V`H zbwID=oJ{iZI`5`6Bnx;)qOU3hlaFTYRYR;}dUe;3Qu4z`1~%M&B2BoC0=ZaIi=!Bv?F058VhvG3EZ6S68#?JlZU_bMQa86 zrX?K29ST!gX;iMg+#oTGGFFME%s+bTjNfu)r|wlem?oRF&uaI*KlNSj5!ovCwKHj1 zYKqJdZiSELmm+gQ5A>EOyzZXwBX$zfm*p?g^Vs85cD(zjBP8$p%H0b;p)ytpL!Js# zK&PD2NlL-c=e32NOYXlAj{6VW95D?D@YGX2+v+?nBpJ=-+z)IE4CD9vb#foqxe=2_{7wfK4c+jgDFQsy zt143A)bZE%zNRF6YX9+USN=|$BTnl{AKlbo66q#Mo8SM6My~L zQNYe&jvVs@lVp^GApP;q{W=)_^zU-VfO`Rst_^1<9bCHiKa*FTV*VKIxVQbQ$c?pk zZ8-ena%#Qhxd+zUn#;^8X$Ag?D@8AGNu8|@A0Y~PeL>ta#Qg*=(I{~%F^HV|WdA{( z`@OH$bIUYy8%90wlgyC?=9E*9jC$diqdZ-^Uqd49<2tUsuj?$LLo+B1JyC3<$5>BA ztuMq`_L8jb3+@lcccp|~N>(0N+R>cTkmuNy*X;`4)2z;5>N6Vb9Z;+0WVX*_6Uz1I z8n`+)aO)~jLt0s=cXY<7ro3W-yGi?69Yz;RFxDhr{<=u6ro3WQe^X?ht$(p#eeL(0 zOk&}P?gaBQ$oB37-K!rl;kWj2rrhtj*TGJSju%!8lxgZNqmj3zi+^n{hOPX-^mTYN zk>f7mERUVSl0A4DA;YGK4u`{Ao(GFE3|fJsA;zQVY||7&^}%cK`AlEkQQOB)<$wnf zbh|32q$7Xq#HA*3F0T{{jIy|<*vqsh^B12u$|{p&N02hT5f~s_*x>>KwlR@U;&q7Z z8U5EY$(2iD-cR`c&gCNTIv{G1FG7ZYRO*{}@~$LtE^B?y-7i%9N@PZBgXVU`RNcZ4 zC8lFvvP~9am+Hh}hsYrUj*|UR^)4rIFD(QyEyrS#KVHhwDBL}g_!$ETBM*LN%$%wq zsv+5cWK3O|bD7qFpZe}y4*YM{d%JJjGc{&=ME9#JkgmNbJI}H%slI)jB37L*->4!w zK$n@Km2neDFT_Ky0PZ9fy&?3s3+6TDb82=uCc3lc_Xi={yJw8zP)LV#b?(~ktH<<| z>7C1@{rE1tlPh74&so+9Zpj8^T`I<*#M1LIGFdk86%1a=vkn+b!1XU^IUy85lj(Cb zw|ZM}DBMqd!Gge{u|ebS9~?0%Pv`nE2sLH(XnqWFBMvNEfYRvRMcHlye;Oeczbq$D8Z*G!dg zB1O_LAJ5M@x-ZPuRDr+RFP9ucV<=dD8BDlFgQQA-)mep##{eL?JjFC2`4V0c;{ln- z=IJMu{>ggYWrvh41|R7F`ypEypMBhr=ixqgCD@}ZA{&XQde>{8oPAW-fcrjqgd!I5 zBxgV_nsMNEHTa1a*b}JkAPj|VkL3g5tqr#9)nf71>oTrXycIl5^PECPnrc)2v>6xk z_Z{`FV*JbGrW15Xa`#iI@Hw1-u(zcOyZ&87@~=;7G}l+xY?9*UE1NXXmx&I_4#w*F z5uJUO^6G%}ZA4O{TYUzx%5fEa%H~eCXO@=hPBJIL?kW9*u57zq8{PU^fr;HJ{^=`o z$$@*pIOmJI14o6Xz2n(OiY*M-4&$i4d~32&Wlc0lak)X>rfG3K5$-_aaj)_VUzI>Y zz~HlJ{%rb4v9E;F?sY~BjtNzP`_F4_Vy{2oFGp;cawvHE#}N{Dx(Xt)L9O9~r|yDE zzc63;(r}GgAj6b52hedxWg0rKudBf{$%6XHQ>DX&n_M9q ztSmioyPQ2J2FJH?LYbeA&q3oHWbI{o4()S2pSofI-XR4GXn{4=zo!)V>3a`=o-ua4 z(MF6M?|(IFhFwQaI!?JAg0i6S7c<=6YqetnVEI!)WEEW2Aj>ncH#;14`s7Mf0%n`> zeI*wzXb|Z+5LhNo<(YSuAJY|o%@K)zVts&BKgC-?==ozq9OhHJ4^G1^=uwwoyfM3L(CVWaw2aE zE!Y)|7pWy)(|?me(6wb(+c&?MaokQnf@l(}`96*N}Vh-j09 zL-DvKlJ{}t!h_B6O7LCeJtyuB7RK0jb=;Z3M~!johu2T z69^9n02Nc?4Z!F#H@u@h3OvGB@BKL(INzddXS-H?-6Z`u{94W*Y}8|p^TYk!(MGqb zyc5E2K|?>@+JG*u{^r8l#s4?-5~5q8^{>)EvCMv`;1ZoK;ha5HU2^j8*QckE%4muG z*Eu6RJ)E^Jd91YSy?#mU?t4HZ|b%rs1N{vS0qZigK)H zZvWEJOfK$%&8tBSJsdLr;VcVADz`cU6QVqiFf;YCLv6{-)mGcZL?V2V~Wabt5{!<7qg-K6I~!hIejD6>44|Oz0X}DZ}b)izm1^ZO*-h z1vQF-bhgIK@f^~5>erdGwvPjWTjI7YfJQ~x*h~Ad0e~$Sns9kHAOXMW_nkW(aEy}o zN;Xg55D^gDhJ=P^OOG+IkQl(S+U@1P=&2DnsTg7DshzFa@g{}N27W*Y&CCo4vTq}E zJ~D_!L$YRWz}-y6%t6CwJ(q*^)GwP4)?dOkMfTTt>?>ztQ^c3m0?5l7DMzPYh?Pc) zYio=MajCo*SLdl%~X;y6R3>1CQ@SRV+!U{Id8JB`67Lpz1yA` zZm#%dVF;Ba1Xga~GG2D%@hE}sACuS@puSgUHi&oLFVi-V+u*Ne31Dtypvc059v*=V zslqR_3pzU6j+=k;e`B}m^|H#|w21*X#`eg=1U@$plhg1f&K^5qQf$i*94%u$n_XT# z*zwj%K}=Edam_^U{Y==*k`bL6Y~eJG=+9@ZYr*~Q#1NsQIrGxbQp$?H+o(K2s~4d( zWu6!ECbt&niKU|gXGjDNUuKC~k`qL=#%m*ak#qVcHlP+c-6<)Kot&5V?kz_r_gWI>E`RHX%rg z#haX66z6rKOUbrOA%XLO(9A6(U5#j`DnVpA1=B9G#_H*KevAik)Yd6!M*w|m3X_;2 zOXhtx+K3eQgiKLYBc7u9StwV}G-N+za`#!t>)z1Lh*-5wo_ZDHA@!*ktvs^IAB%R< zpm1}UkqCOD0EQ?Is)`Y{u4m3ehddMm$~w#2?$VRbol)9Q^!jiF0$QKiw#)T;or)h8 zM0*vQRKAg`k!+2;1`Ej59z|FWW01*=BUa38n+LYS5M_TtU8_XJ9cmf3o_NR$efSQ`dx@TaRtvErGn(|k;VmNM?_ z3+b&aC0I;Ui$Bk2`cl{LaLq-@8`L?*B)~w-wfFY%()yAEOoFFk5?AI#B4og&yvL-? zbN=;3!vt6+f_1n`Ti1S0tE`V4uEu)K!a$Bznt%mG_P*ne%MrFg=96Tf%J-_%-Oadb z%0vZQo;w2t?CTc&g1*m7eZRb@Y^UbhEohvw=Gg%mR>T>tWmMaCcf$b{gcszruq$uV zC%9{bPnHXTikD$7fy>kKl0v7f1XEzbeOCA%6q+LF;}scb$bBh-Iz77B8AjX42%Q!& z46SAChhCaqFobPKG){z(<rhjk%xqAKba_UaNdFhwT4IuuHl2&To_){G6 z(g58vuXaeFZjoSt)1lggVfcH{YKk;U`{e0mJC$m+^dQ|_k!nz!_^HZmX&)uSVoOjcR(6>z9pb3fxwW}O#e z+p0N@1AQcmOChdI+*68MxQIOT@KU79u{~nBoFSKGyINwkX`^ed5kHOAwKqS3kC4oj z5ShZB0&&RQ^_JD+rL3Qrwjt4xiq_!+`Lsd_&dqf5ZXaHg!H^%&e9_YfG>! z|1#Rp4sYdXOySobgulTKIqC5qV<)t891E2^@8A5=RUsSjF4ixdUXSMy6XXe*L$B1l;-$q9g zWChnE3KIdi_NE23OF+VEO`6qBy<_6egjm4cc45f6smtO+y30n!i(bHh_`c|)Gtr;nBxb%^|zZ^32 z6l&o4nxg~#DusMt(d{@I)2J9L7Mi2)|dJTeTnPi zj&mH{AiVe{WP2d_y>EJR_+{l6X@Hs?dl<;hab%fQSKoTIdvBFawyMQgC6!8TC6(LC zvNu2btRiAOtcZ)7bddJKO7gduc6_kH<3{)QA^{(;_}DkmkmP$d>|aU-kFq1K$sCWA zT0iKJ-H>bxoZFdfX@D!PY_fNA(z_;D(5-#Mmt3=ZR)kNKFskC-r1p#7b4p67pjo** z#==aG$K3a8dtqCab(gURUzGnga9!}%pOPsE#h*sF*M|A{#!#HO0{f1CX!mV#I9Ziz zb?PkdJ*Ug_Hxv+3*_7=M>{MTXrZIsk6y6(*JMgCfdZcTcR46uVB9&#VG&sf+d5FNht-j`(MyB9V@A8_^oMF)NDi+K7WuZA0bWsX&Z5r7L=3*+Ku;X z6&v>#=HMWAkUQG%$>0C*-!qFqkX`VA)#pqN0Pt4Je@sl*W=t$LxlIxvE+hZfiA&($ zd+xQejxnP?Oi^J_-?n*eH8j&+^mDJl;$kFzf){IH*Wnn^Vg%3A8*Lp8n;C0=sGUFpSu--p_{##He z_>H*3f$7vHHTM(@?>-UUyL#r&ui||YK9~M5SvkUVJjP52Xr4dT^|)@15z=~>Y9$ClVPVwwe1&#@g-bBeym=+%#@k?5ppatZxgxQ>h+A7S{7GPNxcX z+Fl(MY(bKnjE{8LhprYmvB+4|X#QM+^TifNykYotjURrB`^zgV(V(s<-_Im;zJ8VX zIyU}cc|kNvVIV`JIVIO)xevW!bhB^_UmaeyRzam+V}HFMz8y{Q7Q9LHIMZi1jZ?Ig zLT`_*PDC!XGE#0p-B7A}v_h9W2GL=yF?gY!sq7pawcZ^Q!aZo;y5L^?5i}MQh=NKU znk`sU#JKxT(G5?Zv42f8*oWv*xNg%;g#edfppI~U&c|o=ubAGl6)mOV94Vq;?PtYR zajO0WFYTXKB7O285YI9kzmd|}a#2*CgkupBZarVzdY zA-cP_*Ww?`F-#E-FwHlV_e7i!!h>Q?6vk-tQ)_uQe(TJ6#35SrC`7A_(~f(j3ruo7gn=KzFqQ#_cPcLhskRsm1@c?5tGK}&_Q z4H*v<1vM|kcZ+@Kta|CZGs&A0!zpb3T+nTPEI~WTOJ!4qQdg z#i#TPdDG1Wj}FM6r<(!F=<$8K=#2?Y@Ie_^n2a`KA+6ay<{}m=7n*&j<1@ovTQ{4W zSq2h`f6?c-84z^z-EW7kdyLx`UA>Vt7vJkT-hLvP)K9L4MITyjJs~ehC4N#?|IvIQ zCnxkI@d$=RDlC{GUaUsQPA`L4ibpT#I|ji|NZ$aeV7GDIWtLg74bX}tAbk-8Vu~M8<7YpjYDvtX z8`!F_Y%@2JVpRhB>a=#51OI81@QwG@924}_|FBSBAxjZo$XCO>MHhZK-7rU4r`-31 zzRJCw6Bac(*Z4UbR3|R}L#8;Sjwy_zO>l^% z!n=cPrS5Q^Kw2>i`%WAzmdar4^nkT@J{Biohd4^pqP zLMv2>Clb5Rg8>{$p`Z2xirS-A26(`Ka4)j(y~o-I`q8c?C|)myV63NQ&gnuYVnP}b zR^N#LY-2i5Ahqr4QLx4DzFM(XSbg>+AHZmC1I{yfB{eW5D1EoV6}EEs?~uQY46x%9yJ-JtI38?nhy8U7PZ;0?0la4W&s379V+f{snleo=L4Sj2+>yRs*_jl^OS>{p5&0qM_^bl()U<;3E5tE(j4vj2sNu03tCh> zI}G|d9@e+8gbt#`L49{9^M|H7^rgzW(A`-Bz!v%}K~?P3SU*TA33#s6H05`UU9(Emt)=62JUx>GGM{%yM6WehKgC#%yUK^ie`2te4B$PXa5G@H2>V zgb`7@C9h3=E#9nOk?E6Ls`d-#xLwoa7u6U}Eh;shE>L0F>TeE9!UKjvDQ`Qq`wTx# zc-lnuBUuZ3U3x@1`$djX$UIq{8`FbIv&K)TkVgJ-YAa9u?pp@P|N2sETB%pH9|tT> zyZcl)jkVU1wDw}3UA#c5L^P&A({SymtwHO_u+imn9=EX|*3jqUNw4x9*QktlgW&=5 zIwg*AEm8z}4l(L^dVBdmx4c{{CWwyWb{#kxr0``m}({KLO zQ+zKm_KqchW!9??+;Wf|<9dhGI=c_oT{cW#+e|ZrFqxg91RPYUOo;Y6kU#x=DmR^yO!rrJt#|j_IYOmBc6B7RIaoo^v?zO0Q6kWzjt< z?aNvc7R|`)J*EN)d7PjIh{@X)@On}NMegB9^5^EKwfGv!^bl9BLr1>MA;_q-MWWNf zHzaO#N;Pl$xn%vu2oVW&TQT)q_V~46k@*P9rofM{f1VcYQ}6ZIq-y73VOAZ>Vg3yP z#Q>rqdZ#YLDPmTLBIaW-v)$Dpmv&@@n|m$%7ju44V1if3K-CPTT)=zO7&j)+K*1((4HtVA9qr`#D<8D*+l1Wrw{X<6B)V;+O!mA4jIw}1&T<#CAKdStkU{TQk zuQB*rT3BH{GP?4^?sW9ZS`L1WzBub-AM3mK|- zDOxhkVM$|y7gv%R`C~LtU39vQ!u0LPw^L2?@kh;jolXn9O=(`-Bhm048p_{I z&~DwCrbDaV8RhhF)mQ`yzYR^`;l8cdmSa_;y|Wfy=ZIIOi_qp|NsU`kgE^!7`>_rmt;lLvr2QOM~tcNSgFiFUh zY3jX%mc!au0WiO0qa!a#zx>F!zPge*TOg0r^E`YnDR=UPU?;^IOxG`QWOyTnNSo3; z(;TsjdfX;DC_t;C*nl*# zSemORkJ!K)SaWh6Rj*XIG#jAbpKxpMsTt}L^y49C77^V;* ze1%18e%)nR75WL(d!6Tv?zb1i%Rv*=C;HQwCO>H03ueukMB8mf=cx!iP#nvcZfL|xxn1P!Da3(>QxNx{>r3`q6MVQeM7&Gz8F(E&nydEkM6J)NX0IQQW_?vDv_z z1DMy{_Wykrl2(dr0S!Qb8vV?-(NWO14}DI)0@e@}u8zbpSDANaQ5}*>=qU*AM>Kgd zJ3Dl}KXB@MY1830a(NAOV?R4IZ<3O(xgO;vGiYNttLD3FGU_vw z0_w^kthF?^yjZ9gyh(JTRnq)!&sb-tgnQAb#1{7HOP%ZqFHS!;ZFlGL{>3|ma64*Q zu?!GY5i2B_rVgX=VfY)ztjd@lBVk@^)N?kLsB?D6f74+|HH=RUFe)%C_&LobJPNW+ zq9@*+wo?qBlW1SY>JQkT>!VHl<1Dd4;!J#=qqwEY9+NK(u zk_oaGu6JNA%=aWu?za+Lpq6N5X=8Fn0RG*2eU*Kuujed6kCJO|jgqI%d`|x=Z~DNC zOZyk!o)P9AL7_0RHd_Jj6kKDVfjMa+E=oN5%1E-w8*xy#jQ7p4?IsrbUQvtt5<9ni z-8Yme0lR%GJ6*#<$Pmzq`xL;*R?S88>Fv%(DW{KXX3q-AghRt72duhy-ej6*Z`RZYh1BYvKd<>gDR@%MvI%^Bq_Z;TA7@Ca)IW6Gk#4~`tDlOqxOmr-kgM2Pjb z@yaUFi*G6AQr?hqyJc2Us>=92_m<@byXFlPWLEnz9%Z?>>yX6-BgHDmR3qMLa-&4v z#OfQ80OG^>@PkbJx4+$}N%m@8B>PH*IFsP3O`O_v9+~8?U=l)4OE1^wxbZhz z6>C{`oGjuONq4ob>|Ha9IW`JwV)osF}zYP8tPH^!~FGNmt>`iN_^!IV~{tdlzr^GD2YJ`N4wUVqPt7U2I zJpm=kWDWbOvJUHpxcuhpD#ojwSB9_0vRrjMaAz-u@V)G^uC)Q_^LriE>$?H$zo&B| zso*0YCy1zD45SIh2AREk+6QwNQ073&G#JNLxI<-?a9&aJ24bd|%IK!ivte3ZUooTQ zniJHkP%JtGxDr-8ue2`!Pz(Ji?b$`fjg7BrM^a`VHQ5`>bxj;&SOKvIwXA`#xEve) z!Mb!{W3D1T~{jFDFhXej2E~~*U-%(U%*61x5HU8)~oMecQxI z;o^-9@i2vp-twI`mzS#3Ntx$#1DAn$YIcW=tns6Fa{wz}6HVsy=(=L;7(MI~w`|iQ zDw8g9$YiU}Mq;ubnJ2ftPdrgs9(uW1+>}gV^|8?rT4P6Nonx_PZqfNL=fhF?zOav; zZHm^1ce6x1GRXg7RaC&4zSl72>m8Qsl#buZ2V?Ao?7qE6_e5C?PW z0f5lB(gNDF)6C|!HuihJV^|&?4)mb4Oi)ygUIM2Ujo{NhC1B40sv)}->wf~D@D|^+ z8ytME*-vSSv42N!+VaJ-5QfrKpNJ5UnTq*?o>I(NB^bk3jo#Edfe+COvOXPrya=K| zxRv>eynBQ4QZJgAhm9ABhA|HB>?RQQgh8T(+U_}Aplp)fu zi7>vH+>A_OQj~Gtq07|2A~!i?U>;2s9@X_9-Gt~HoR0(2Q!=3NuFej*en%w1&j_^B zp-cx5HLV}tZH5m)776NQhF{ZpG_Zr_klHocf?}t~QO^qJCkZ1UB2!KzCrE8QjaPox zYH70Gw=`SWzv*&)Y?*@}4RP@1AfQ`j9sm|WFPTc zmef9yI)s18w?~QlC{UO0VC-c5?bDG5q8wO(Mq`oKU7z(V$Tw*g)d1p%N+>STcpvEe zZ5l@tQheqsq;`?-C_s!J6n#(8d9EO&s202Ek!1}F&=#maGy$0Uw{t!3J0VRgfmIBE z2tf<(m0f@i@|OfFho(FC>bR+14h_)`SJz|lUz6Xh4v#T6Z8?7;mV@X4qM7Y?vdG+( z*8rv){swK3UdpM^yQ~jSNa>MgO}4ny6A0TqGN?A5nLF1Ieqy((Q_Om@W4;$H%i|wZ zOGJ^~^c4PACOutK8;9qJ>`1b}(_1y*eyt|(O1|%`dgu;|tET3EZ_MVcejIBnRx?KL zg5p}8vSOwy<7!dqu1OF+O380ET6+35LlvxcO%QnmI%#GDHcl|e%-t0?mp96@HTaBB zHCOu`11b+*ySRAKusAo{ZZxVxpxgx4SxawcE>lP!`F}!y0QoqxEnJ10B zPCzJ_--{g05tb7l_{4&?d+UqCc_5wOon#41EL9EeU3^KUX4Y~y%w4)}9*F~!?&=IN z4fTta(bef^U|d%;3IrN|+5)~y{$^~dCk4F4_EYb zn(8%ZJ14mX-mb>Ik-MWj0*M=m{N7xLg zGf!(;1N_x4N%`HQ$vFELo&ays(Yb=f<%Su+6#4`4SGpk8!g30TZleMGcdQ z;pX*aFH5*=@*ef*3%c!Gaf)(@Ow0V3exyL1y&3o$G)iddu7ms2mr0ob^Q-8P#L_+h zwi>~lqvmm7#fLZcDN!#7BS#&fxYku78)Nplt|XRM+V+y8>=p2_U@ei;OMWH)F5_E4 zQ^SFP(@28=qqiAbMp#&*O4_&H88fWz?Dh3{eNimoEnd>h;3KLRjuYxG*fZL@EaApD z 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 E3z!F`N&o-= literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5be2c5426d4dee551b5daabd26d870982eb9314e GIT binary patch literal 30626 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