From 957b3244aa89a589ac7da7eb788791f6216c80ef Mon Sep 17 00:00:00 2001 From: RACHKANC Date: Wed, 27 Aug 2025 21:08:22 +0530 Subject: [PATCH 1/4] created database.py This file include database with tables -> users and groups --- database.py | 521 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 521 insertions(+) create mode 100644 database.py diff --git a/database.py b/database.py new file mode 100644 index 0000000..2f039e1 --- /dev/null +++ b/database.py @@ -0,0 +1,521 @@ +import sqlite3 +import uuid +import re + +def generate_id(): + """Generates a unique ID using UUID4.""" + return uuid.uuid4().hex + +def validate_password(password): + """ + Validates password strength. + Requirements: At least 6 characters, contains numbers, alphabets, and special characters. + """ + if len(password) < 6: + return False, "Password must be at least 6 characters long." + + # Check for at least one letter + if not re.search(r'[a-zA-Z]', password): + return False, "Password must contain at least one letter." + + # Check for at least one number + if not re.search(r'\d', password): + return False, "Password must contain at least one number." + + # Check for at least one special character + if not re.search(r'[!@#$%^&*()_+\-=\[\]{};\':"\\|,.<>\/?]', password): + return False, "Password must contain at least one special character." + + return True, "Password is strong." + +def get_db_connection(): + """Establishes and returns a database connection.""" + conn = sqlite3.connect('travel_together.db') + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA foreign_keys = ON;") + return conn + +def setup_database(): + """Creates the database tables if they don't exist.""" + conn = get_db_connection() + cursor = conn.cursor() + + # Users table with current_group_id column + create_users_table_query = """ + CREATE TABLE IF NOT EXISTS Users ( + userid TEXT PRIMARY KEY, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + email TEXT UNIQUE NOT NULL, + phone_no TEXT UNIQUE, + gender TEXT CHECK(gender IN ('Male', 'Female', 'Other')), + marital_status TEXT CHECK(marital_status IN ('Single', 'Married', 'Divorced', 'Widowed')), + profile_picture TEXT, + bio TEXT, + current_group_id TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (current_group_id) REFERENCES Groups (group_id) ON DELETE SET NULL + ); + """ + cursor.execute(create_users_table_query) + + # Groups table with member_count and member_list + create_groups_table_query = """ + CREATE TABLE IF NOT EXISTS Groups ( + group_id TEXT PRIMARY KEY, + group_name TEXT NOT NULL, + group_description TEXT, + group_type TEXT NOT NULL CHECK(group_type IN ('Public', 'Private')), + owner_id TEXT NOT NULL, + member_count INTEGER DEFAULT 1, + member_list TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (owner_id) REFERENCES Users (userid) ON DELETE CASCADE + ); + """ + cursor.execute(create_groups_table_query) + + # GroupMembers table for detailed membership tracking + create_group_members_table_query = """ + CREATE TABLE IF NOT EXISTS GroupMembers ( + member_id TEXT PRIMARY KEY, + group_id TEXT NOT NULL, + user_id TEXT NOT NULL, + username TEXT NOT NULL, + joined_at TEXT DEFAULT CURRENT_TIMESTAMP, + role TEXT DEFAULT 'Member' CHECK(role IN ('Owner', 'Admin', 'Member')), + FOREIGN KEY (group_id) REFERENCES Groups (group_id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES Users (userid) ON DELETE CASCADE, + UNIQUE(group_id, user_id) + ); + """ + cursor.execute(create_group_members_table_query) + + print("Database and tables are ready.") + conn.commit() + conn.close() + +def add_user(username, password, email, **kwargs): + """Adds a new user to the Users table with a generated ID.""" + # Validate password strength + is_valid, message = validate_password(password) + if not is_valid: + print(f"ERROR: {message}") + return None + + user_id = generate_id() + conn = get_db_connection() + + try: + conn.execute( + """INSERT INTO Users (userid, username, password, email, phone_no, gender, + marital_status, profile_picture, bio, current_group_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", + (user_id, username, password, email, kwargs.get('phone_no'), + kwargs.get('gender'), kwargs.get('marital_status'), + kwargs.get('profile_picture'), kwargs.get('bio'), None) + ) + conn.commit() + print(f"SUCCESS: User '{username}' added with ID: {user_id}") + return user_id + except sqlite3.IntegrityError as e: + if "username" in str(e): + print("ERROR: Username already exists.") + elif "email" in str(e): + print("ERROR: Email already exists.") + else: + print(f"ERROR: Could not add user. {e}") + return None + finally: + conn.close() + +def delete_user(user_id): + """Deletes a user from the Users table by their ID.""" + conn = get_db_connection() + try: + cursor = conn.cursor() + + # First, remove user from any groups they're in + cursor.execute("SELECT current_group_id FROM Users WHERE userid = ?", (user_id,)) + user_data = cursor.fetchone() + + if user_data and user_data['current_group_id']: + leave_group(user_id) + + # Delete the user + cursor.execute("DELETE FROM Users WHERE userid = ?", (user_id,)) + if cursor.rowcount == 0: + print(f"ERROR: No user found with ID '{user_id}'.") + else: + conn.commit() + print(f"SUCCESS: User with ID '{user_id}' deleted.") + except sqlite3.Error as e: + print(f"ERROR: Could not delete user. {e}") + finally: + conn.close() + +def add_group(group_name, group_type, owner_id, group_description=None): + """Adds a new group to the Groups table with a generated ID.""" + if group_type not in ['Public', 'Private']: + print("ERROR: Group type must be 'Public' or 'Private'.") + return None + + group_id = generate_id() + conn = get_db_connection() + + try: + cursor = conn.cursor() + + # Check if owner exists and get username + cursor.execute("SELECT userid, username FROM Users WHERE userid = ?", (owner_id,)) + owner_data = cursor.fetchone() + if not owner_data: + print(f"ERROR: Owner with ID '{owner_id}' does not exist.") + return None + + owner_username = owner_data['username'] + + # Create the group + conn.execute( + """INSERT INTO Groups (group_id, group_name, group_description, group_type, + owner_id, member_count, member_list) VALUES (?, ?, ?, ?, ?, ?, ?)""", + (group_id, group_name, group_description, group_type, owner_id, 1, owner_username) + ) + + # Add owner to GroupMembers + member_id = generate_id() + conn.execute( + """INSERT INTO GroupMembers (member_id, group_id, user_id, username, role) + VALUES (?, ?, ?, ?, 'Owner')""", + (member_id, group_id, owner_id, owner_username) + ) + + # Update owner's current_group_id + conn.execute("UPDATE Users SET current_group_id = ? WHERE userid = ?", (group_id, owner_id)) + + conn.commit() + print(f"SUCCESS: Group '{group_name}' added with ID: {group_id}") + print(f"Owner '{owner_username}' automatically joined the group.") + return group_id + except sqlite3.Error as e: + print(f"ERROR: Could not add group. {e}") + return None + finally: + conn.close() + +def delete_group(group_id): + """Deletes a group from the Groups table by its ID.""" + conn = get_db_connection() + try: + cursor = conn.cursor() + + # Update all users in this group to have no current group + cursor.execute("UPDATE Users SET current_group_id = NULL WHERE current_group_id = ?", (group_id,)) + + # Delete the group (GroupMembers will be deleted due to CASCADE) + cursor.execute("DELETE FROM Groups WHERE group_id = ?", (group_id,)) + if cursor.rowcount == 0: + print(f"ERROR: No group found with ID '{group_id}'.") + else: + conn.commit() + print(f"SUCCESS: Group with ID '{group_id}' deleted.") + except sqlite3.Error as e: + print(f"ERROR: Could not delete group. {e}") + finally: + conn.close() + +def join_group(user_id, group_id): + """Adds a user to a group and updates all related tables.""" + conn = get_db_connection() + try: + cursor = conn.cursor() + + # Check if user exists + cursor.execute("SELECT userid, username, current_group_id FROM Users WHERE userid = ?", (user_id,)) + user_data = cursor.fetchone() + if not user_data: + print(f"ERROR: User with ID '{user_id}' does not exist.") + return False + + username = user_data['username'] + current_group = user_data['current_group_id'] + + # Check if group exists + cursor.execute("SELECT group_id, group_name, member_count, member_list FROM Groups WHERE group_id = ?", (group_id,)) + group_data = cursor.fetchone() + if not group_data: + print(f"ERROR: Group with ID '{group_id}' does not exist.") + return False + + # Check if user is already in this group + if current_group == group_id: + print("ERROR: User is already a member of this group.") + return False + + # If user is in another group, leave it first + if current_group: + leave_group(user_id) + + # Add user to GroupMembers + member_id = generate_id() + conn.execute( + "INSERT INTO GroupMembers (member_id, group_id, user_id, username) VALUES (?, ?, ?, ?)", + (member_id, group_id, user_id, username) + ) + + # Update user's current_group_id + conn.execute("UPDATE Users SET current_group_id = ? WHERE userid = ?", (group_id, user_id)) + + # Update group's member_count and member_list + new_count = group_data['member_count'] + 1 + current_members = group_data['member_list'] or "" + new_member_list = f"{current_members}, {username}" if current_members else username + + conn.execute( + "UPDATE Groups SET member_count = ?, member_list = ? WHERE group_id = ?", + (new_count, new_member_list, group_id) + ) + + conn.commit() + print(f"SUCCESS: User '{username}' joined group '{group_data['group_name']}'.") + return True + except sqlite3.Error as e: + print(f"ERROR: Could not join group. {e}") + return False + finally: + conn.close() + +def leave_group(user_id): + """Removes a user from their current group.""" + conn = get_db_connection() + try: + cursor = conn.cursor() + + # Get user and current group info + cursor.execute("SELECT username, current_group_id FROM Users WHERE userid = ?", (user_id,)) + user_data = cursor.fetchone() + if not user_data or not user_data['current_group_id']: + print("ERROR: User is not in any group.") + return False + + username = user_data['username'] + group_id = user_data['current_group_id'] + + # Get group info + cursor.execute("SELECT group_name, member_count, member_list FROM Groups WHERE group_id = ?", (group_id,)) + group_data = cursor.fetchone() + + # Remove from GroupMembers + cursor.execute("DELETE FROM GroupMembers WHERE group_id = ? AND user_id = ?", (group_id, user_id)) + + # Update user's current_group_id to NULL + conn.execute("UPDATE Users SET current_group_id = NULL WHERE userid = ?", (user_id,)) + + # Update group's member_count and member_list + new_count = group_data['member_count'] - 1 + current_members = group_data['member_list'] or "" + member_list = [member.strip() for member in current_members.split(',') if member.strip()] + if username in member_list: + member_list.remove(username) + new_member_list = ', '.join(member_list) + + conn.execute( + "UPDATE Groups SET member_count = ?, member_list = ? WHERE group_id = ?", + (new_count, new_member_list, group_id) + ) + + conn.commit() + print(f"SUCCESS: User '{username}' left group '{group_data['group_name']}'.") + return True + except sqlite3.Error as e: + print(f"ERROR: Could not leave group. {e}") + return False + finally: + conn.close() + +def view_users(): + """Displays all users with their current group information.""" + conn = get_db_connection() + try: + cursor = conn.cursor() + cursor.execute(""" + SELECT u.userid, u.username, u.email, + COALESCE(g.group_name, 'No Group') as current_group + FROM Users u + LEFT JOIN Groups g ON u.current_group_id = g.group_id + """) + users = cursor.fetchall() + + if not users: + print("No users found.") + return + + print("\n--- Users List ---") + print(f"{'ID':<32} {'Username':<15} {'Email':<25} {'Current Group':<20}") + print("-" * 95) + for user in users: + print(f"{user['userid']:<32} {user['username']:<15} {user['email']:<25} {user['current_group']:<20}") + except sqlite3.Error as e: + print(f"ERROR: Could not retrieve users. {e}") + finally: + conn.close() + +def view_groups(): + """Displays all groups with their member information.""" + conn = get_db_connection() + try: + cursor = conn.cursor() + cursor.execute(""" + SELECT g.group_id, g.group_name, g.group_type, g.group_description, + u.username as owner_name, g.member_count, g.member_list, g.created_at + FROM Groups g + JOIN Users u ON g.owner_id = u.userid + """) + groups = cursor.fetchall() + + if not groups: + print("No groups found.") + return + + print("\n--- Groups List ---") + for group in groups: + print(f"ID: {group['group_id']}") + print(f"Name: {group['group_name']}") + print(f"Type: {group['group_type']}") + print(f"Owner: {group['owner_name']}") + print(f"Description: {group['group_description'] or 'No description'}") + print(f"Member Count: {group['member_count']}") + print(f"Members: {group['member_list'] or 'No members'}") + print(f"Created: {group['created_at']}") + print("-" * 60) + except sqlite3.Error as e: + print(f"ERROR: Could not retrieve groups. {e}") + finally: + conn.close() + +def get_group_members(group_id): + """Returns all members of a specific group (for frontend API).""" + conn = get_db_connection() + try: + cursor = conn.cursor() + cursor.execute(""" + SELECT gm.user_id, gm.username, gm.role, gm.joined_at, + u.email, u.phone_no + FROM GroupMembers gm + JOIN Users u ON gm.user_id = u.userid + WHERE gm.group_id = ? + ORDER BY gm.role DESC, gm.joined_at ASC + """, (group_id,)) + return cursor.fetchall() + except sqlite3.Error as e: + print(f"ERROR: Could not get group members. {e}") + return [] + finally: + conn.close() + +def get_user_group(user_id): + """Returns the current group of a user (for frontend API).""" + conn = get_db_connection() + try: + cursor = conn.cursor() + cursor.execute(""" + SELECT g.group_id, g.group_name, g.group_type, g.group_description, + g.member_count, g.member_list + FROM Users u + JOIN Groups g ON u.current_group_id = g.group_id + WHERE u.userid = ? + """, (user_id,)) + return cursor.fetchone() + except sqlite3.Error as e: + print(f"ERROR: Could not get user group. {e}") + return None + finally: + conn.close() + +def main(): + """Main function to run the interactive command-line interface.""" + setup_database() + + while True: + print("\n--- Travel Together DB Manager ---") + print("1. Add User") + print("2. Delete User") + print("3. View Users") + print("4. Add Group") + print("5. Delete Group") + print("6. View Groups") + print("7. Join Group") + print("8. Leave Group") + print("9. Exit") + + choice = input("Enter your choice (1-9): ").strip() + + if choice == '1': + print("\n--- Add New User ---") + username = input("Enter username: ").strip() + + while True: + password = input("Enter password (min 6 chars, letters, numbers, special chars): ").strip() + is_valid, message = validate_password(password) + if is_valid: + print("Password accepted.") + break + else: + print(f"ERROR: {message}") + retry = input("Try again? (y/n): ").strip().lower() + if retry != 'y': + break + else: + continue + + email = input("Enter email: ").strip() + phone_no = input("Enter phone number (optional): ").strip() or None + gender = input("Enter gender (Male/Female/Other, optional): ").strip() or None + marital_status = input("Enter marital status (Single/Married/Divorced/Widowed, optional): ").strip() or None + bio = input("Enter bio (optional): ").strip() or None + + add_user(username, password, email, phone_no=phone_no, gender=gender, + marital_status=marital_status, bio=bio) + + elif choice == '2': + user_id = input("Enter the userid to delete: ").strip() + delete_user(user_id) + + elif choice == '3': + view_users() + + elif choice == '4': + print("\n--- Add New Group ---") + group_name = input("Enter group name: ").strip() + group_type = input("Enter group type (Public/Private): ").strip().capitalize() + owner_id = input("Enter the owner's userid: ").strip() + group_description = input("Enter group description (optional): ").strip() or None + add_group(group_name, group_type, owner_id, group_description) + + elif choice == '5': + group_id = input("Enter the group_id to delete: ").strip() + delete_group(group_id) + + elif choice == '6': + view_groups() + + elif choice == '7': + print("\n--- Join Group ---") + user_id = input("Enter your userid: ").strip() + group_id = input("Enter the group_id to join: ").strip() + join_group(user_id, group_id) + + elif choice == '8': + print("\n--- Leave Group ---") + user_id = input("Enter your userid: ").strip() + leave_group(user_id) + + elif choice == '9': + print("Exiting... Have a great day!") + break + + else: + print("Invalid choice. Please enter a number between 1-9.") + +if __name__ == "__main__": + main() From c511499b4bd48a5c3b983651270f387473238e0e Mon Sep 17 00:00:00 2001 From: RACHKANC Date: Sun, 31 Aug 2025 12:48:34 +0530 Subject: [PATCH 2/4] Group chat added --- database.py | 134 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 128 insertions(+), 6 deletions(-) diff --git a/database.py b/database.py index 2f039e1..c29c080 100644 --- a/database.py +++ b/database.py @@ -91,6 +91,21 @@ def setup_database(): """ cursor.execute(create_group_members_table_query) + # GroupMessages table for storing group chats + create_group_messages_table_query = """ + CREATE TABLE IF NOT EXISTS GroupMessages ( + message_id INTEGER PRIMARY KEY AUTOINCREMENT, + group_id TEXT NOT NULL, + sender_id TEXT NOT NULL, + sender_username TEXT NOT NULL, + message TEXT NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (group_id) REFERENCES Groups (group_id) ON DELETE CASCADE, + FOREIGN KEY (sender_id) REFERENCES Users (userid) ON DELETE CASCADE + ); + """ + cursor.execute(create_group_messages_table_query) + print("Database and tables are ready.") conn.commit() conn.close() @@ -142,7 +157,7 @@ def delete_user(user_id): if user_data and user_data['current_group_id']: leave_group(user_id) - # Delete the user + # Delete the user (messages will be deleted due to CASCADE) cursor.execute("DELETE FROM Users WHERE userid = ?", (user_id,)) if cursor.rowcount == 0: print(f"ERROR: No user found with ID '{user_id}'.") @@ -212,13 +227,13 @@ def delete_group(group_id): # Update all users in this group to have no current group cursor.execute("UPDATE Users SET current_group_id = NULL WHERE current_group_id = ?", (group_id,)) - # Delete the group (GroupMembers will be deleted due to CASCADE) + # Delete the group (GroupMembers and GroupMessages will be deleted due to CASCADE) cursor.execute("DELETE FROM Groups WHERE group_id = ?", (group_id,)) if cursor.rowcount == 0: print(f"ERROR: No group found with ID '{group_id}'.") else: conn.commit() - print(f"SUCCESS: Group with ID '{group_id}' deleted.") + print(f"SUCCESS: Group with ID '{group_id}' deleted along with all messages.") except sqlite3.Error as e: print(f"ERROR: Could not delete group. {e}") finally: @@ -333,6 +348,99 @@ def leave_group(user_id): finally: conn.close() +def send_message(sender_id, group_id, message): + """Sends a message to a group chat.""" + conn = get_db_connection() + try: + cursor = conn.cursor() + + # Check if sender exists and get username + cursor.execute("SELECT username, current_group_id FROM Users WHERE userid = ?", (sender_id,)) + sender_data = cursor.fetchone() + if not sender_data: + print(f"ERROR: Sender with ID '{sender_id}' does not exist.") + return False + + sender_username = sender_data['username'] + + # Check if group exists + cursor.execute("SELECT group_id FROM Groups WHERE group_id = ?", (group_id,)) + if not cursor.fetchone(): + print(f"ERROR: Group with ID '{group_id}' does not exist.") + return False + + # Check if sender is a member of the group + cursor.execute("SELECT * FROM GroupMembers WHERE group_id = ? AND user_id = ?", (group_id, sender_id)) + if not cursor.fetchone(): + print("ERROR: You must be a member of the group to send messages.") + return False + + # Insert the message + conn.execute( + "INSERT INTO GroupMessages (group_id, sender_id, sender_username, message) VALUES (?, ?, ?, ?)", + (group_id, sender_id, sender_username, message) + ) + + conn.commit() + print(f"SUCCESS: Message sent to group.") + return True + except sqlite3.Error as e: + print(f"ERROR: Could not send message. {e}") + return False + finally: + conn.close() + +def get_group_messages(group_id, limit=50): + """Retrieves recent messages from a group chat.""" + conn = get_db_connection() + try: + cursor = conn.cursor() + cursor.execute(""" + SELECT message_id, sender_username, message, timestamp + FROM GroupMessages + WHERE group_id = ? + ORDER BY timestamp DESC + LIMIT ? + """, (group_id, limit)) + + messages = cursor.fetchall() + return list(reversed(messages)) # Return in chronological order + except sqlite3.Error as e: + print(f"ERROR: Could not retrieve messages. {e}") + return [] + finally: + conn.close() + +def view_group_chat(group_id): + """Displays recent messages from a group chat.""" + conn = get_db_connection() + try: + cursor = conn.cursor() + + # Get group name + cursor.execute("SELECT group_name FROM Groups WHERE group_id = ?", (group_id,)) + group_data = cursor.fetchone() + if not group_data: + print(f"ERROR: Group with ID '{group_id}' does not exist.") + return + + print(f"\n--- Chat for Group: {group_data['group_name']} ---") + + messages = get_group_messages(group_id, 20) # Last 20 messages + + if not messages: + print("No messages in this group yet.") + return + + for msg in messages: + timestamp = msg['timestamp'][:19] # Remove microseconds + print(f"[{timestamp}] {msg['sender_username']}: {msg['message']}") + + except sqlite3.Error as e: + print(f"ERROR: Could not view chat. {e}") + finally: + conn.close() + def view_users(): """Displays all users with their current group information.""" conn = get_db_connection() @@ -446,9 +554,11 @@ def main(): print("6. View Groups") print("7. Join Group") print("8. Leave Group") - print("9. Exit") + print("9. Send Message") + print("10. View Group Chat") + print("11. Exit") - choice = input("Enter your choice (1-9): ").strip() + choice = input("Enter your choice (1-11): ").strip() if choice == '1': print("\n--- Add New User ---") @@ -511,11 +621,23 @@ def main(): leave_group(user_id) elif choice == '9': + print("\n--- Send Message ---") + sender_id = input("Enter your userid: ").strip() + group_id = input("Enter the group_id: ").strip() + message = input("Enter your message: ").strip() + send_message(sender_id, group_id, message) + + elif choice == '10': + print("\n--- View Group Chat ---") + group_id = input("Enter the group_id: ").strip() + view_group_chat(group_id) + + elif choice == '11': print("Exiting... Have a great day!") break else: - print("Invalid choice. Please enter a number between 1-9.") + print("Invalid choice. Please enter a number between 1-11.") if __name__ == "__main__": main() From 23dc02a45bbfd302f2c66f41e7329fd338ca8da6 Mon Sep 17 00:00:00 2001 From: RACHKANC Date: Sun, 31 Aug 2025 13:12:37 +0530 Subject: [PATCH 3/4] Updated DATABASE.py Group table updated.. Destination table added .. a user could create a link between destinations to travel while creating a group --- database.py | 702 +++++++++++++++++++++++++--------------------------- 1 file changed, 343 insertions(+), 359 deletions(-) diff --git a/database.py b/database.py index c29c080..cb2f5f1 100644 --- a/database.py +++ b/database.py @@ -14,15 +14,12 @@ def validate_password(password): if len(password) < 6: return False, "Password must be at least 6 characters long." - # Check for at least one letter if not re.search(r'[a-zA-Z]', password): return False, "Password must contain at least one letter." - # Check for at least one number if not re.search(r'\d', password): return False, "Password must contain at least one number." - # Check for at least one special character if not re.search(r'[!@#$%^&*()_+\-=\[\]{};\':"\\|,.<>\/?]', password): return False, "Password must contain at least one special character." @@ -40,7 +37,7 @@ def setup_database(): conn = get_db_connection() cursor = conn.cursor() - # Users table with current_group_id column + # Users table create_users_table_query = """ CREATE TABLE IF NOT EXISTS Users ( userid TEXT PRIMARY KEY, @@ -53,13 +50,14 @@ def setup_database(): profile_picture TEXT, bio TEXT, current_group_id TEXT, + is_group_owner BOOLEAN DEFAULT FALSE, created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (current_group_id) REFERENCES Groups (group_id) ON DELETE SET NULL ); """ cursor.execute(create_users_table_query) - # Groups table with member_count and member_list + # Groups table (main travel group) create_groups_table_query = """ CREATE TABLE IF NOT EXISTS Groups ( group_id TEXT PRIMARY KEY, @@ -68,20 +66,86 @@ def setup_database(): group_type TEXT NOT NULL CHECK(group_type IN ('Public', 'Private')), owner_id TEXT NOT NULL, member_count INTEGER DEFAULT 1, - member_list TEXT, + max_members INTEGER DEFAULT 50, + total_duration_days INTEGER, + estimated_total_cost TEXT, + travel_start_date DATE, + travel_end_date DATE, + status TEXT DEFAULT 'Planning' CHECK(status IN ('Planning', 'Active', 'Completed', 'Cancelled')), created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (owner_id) REFERENCES Users (userid) ON DELETE CASCADE ); """ cursor.execute(create_groups_table_query) - # GroupMembers table for detailed membership tracking + # Destinations table (places that can be visited) + create_destinations_table_query = """ + CREATE TABLE IF NOT EXISTS Destinations ( + destination_id TEXT PRIMARY KEY, + destination_name TEXT UNIQUE NOT NULL, + state_province TEXT, + country TEXT NOT NULL, + description TEXT, + best_time_to_visit TEXT, + difficulty_level TEXT CHECK(difficulty_level IN ('Easy', 'Moderate', 'Hard')), + estimated_budget_per_day TEXT, + popular_activities TEXT, + latitude DECIMAL(10,8), + longitude DECIMAL(11,8), + created_at TEXT DEFAULT CURRENT_TIMESTAMP + ); + """ + cursor.execute(create_destinations_table_query) + + # TravelItinerary table (links groups to multiple destinations in sequence) + create_travel_itinerary_query = """ + CREATE TABLE IF NOT EXISTS TravelItinerary ( + itinerary_id TEXT PRIMARY KEY, + group_id TEXT NOT NULL, + destination_id TEXT NOT NULL, + visit_order INTEGER NOT NULL, + planned_arrival_date DATE, + planned_departure_date DATE, + duration_days INTEGER DEFAULT 1, + estimated_cost TEXT, + accommodation_type TEXT, + transport_to_next TEXT, + notes TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (group_id) REFERENCES Groups (group_id) ON DELETE CASCADE, + FOREIGN KEY (destination_id) REFERENCES Destinations (destination_id) ON DELETE CASCADE, + UNIQUE(group_id, destination_id), + UNIQUE(group_id, visit_order) + ); + """ + cursor.execute(create_travel_itinerary_query) + + # TouristSpots table + create_tourist_spots_table_query = """ + CREATE TABLE IF NOT EXISTS TouristSpots ( + spot_id TEXT PRIMARY KEY, + destination_id TEXT NOT NULL, + spot_name TEXT NOT NULL, + spot_type TEXT CHECK(spot_type IN ('Historical', 'Natural', 'Adventure', 'Religious', 'Cultural', 'Beach', 'Mountain')), + description TEXT, + entry_fee TEXT, + opening_hours TEXT, + rating DECIMAL(2,1) CHECK(rating >= 0 AND rating <= 5), + coordinates TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (destination_id) REFERENCES Destinations (destination_id) ON DELETE CASCADE + ); + """ + cursor.execute(create_tourist_spots_table_query) + + # GroupMembers table create_group_members_table_query = """ CREATE TABLE IF NOT EXISTS GroupMembers ( member_id TEXT PRIMARY KEY, group_id TEXT NOT NULL, user_id TEXT NOT NULL, username TEXT NOT NULL, + join_status TEXT DEFAULT 'Approved' CHECK(join_status IN ('Pending', 'Approved', 'Rejected', 'Blocked')), joined_at TEXT DEFAULT CURRENT_TIMESTAMP, role TEXT DEFAULT 'Member' CHECK(role IN ('Owner', 'Admin', 'Member')), FOREIGN KEY (group_id) REFERENCES Groups (group_id) ON DELETE CASCADE, @@ -91,7 +155,7 @@ def setup_database(): """ cursor.execute(create_group_members_table_query) - # GroupMessages table for storing group chats + # GroupMessages table create_group_messages_table_query = """ CREATE TABLE IF NOT EXISTS GroupMessages ( message_id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -106,6 +170,11 @@ def setup_database(): """ cursor.execute(create_group_messages_table_query) + # Indexes for performance + cursor.execute("CREATE INDEX IF NOT EXISTS idx_travel_itinerary_group ON TravelItinerary(group_id, visit_order);") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_tourist_spots_destination ON TouristSpots(destination_id);") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_group_messages_group_timestamp ON GroupMessages(group_id, timestamp);") + print("Database and tables are ready.") conn.commit() conn.close() @@ -124,11 +193,11 @@ def add_user(username, password, email, **kwargs): try: conn.execute( """INSERT INTO Users (userid, username, password, email, phone_no, gender, - marital_status, profile_picture, bio, current_group_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", + marital_status, profile_picture, bio, current_group_id, is_group_owner) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", (user_id, username, password, email, kwargs.get('phone_no'), kwargs.get('gender'), kwargs.get('marital_status'), - kwargs.get('profile_picture'), kwargs.get('bio'), None) + kwargs.get('profile_picture'), kwargs.get('bio'), None, False) ) conn.commit() print(f"SUCCESS: User '{username}' added with ID: {user_id}") @@ -144,59 +213,85 @@ def add_user(username, password, email, **kwargs): finally: conn.close() -def delete_user(user_id): - """Deletes a user from the Users table by their ID.""" +def add_destination(destination_name, state_province, country, **kwargs): + """Adds a new destination to the Destinations table.""" + destination_id = generate_id() conn = get_db_connection() + try: - cursor = conn.cursor() - - # First, remove user from any groups they're in - cursor.execute("SELECT current_group_id FROM Users WHERE userid = ?", (user_id,)) - user_data = cursor.fetchone() - - if user_data and user_data['current_group_id']: - leave_group(user_id) - - # Delete the user (messages will be deleted due to CASCADE) - cursor.execute("DELETE FROM Users WHERE userid = ?", (user_id,)) - if cursor.rowcount == 0: - print(f"ERROR: No user found with ID '{user_id}'.") - else: - conn.commit() - print(f"SUCCESS: User with ID '{user_id}' deleted.") - except sqlite3.Error as e: - print(f"ERROR: Could not delete user. {e}") + conn.execute( + """INSERT INTO Destinations (destination_id, destination_name, state_province, + country, description, best_time_to_visit, difficulty_level, estimated_budget_per_day, + popular_activities, latitude, longitude) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", + (destination_id, destination_name, state_province, country, + kwargs.get('description'), kwargs.get('best_time_to_visit'), + kwargs.get('difficulty_level'), kwargs.get('estimated_budget_per_day'), + kwargs.get('popular_activities'), kwargs.get('latitude'), kwargs.get('longitude')) + ) + conn.commit() + print(f"SUCCESS: Destination '{destination_name}' added with ID: {destination_id}") + return destination_id + except sqlite3.IntegrityError as e: + print(f"ERROR: Destination name already exists. {e}") + return None finally: conn.close() -def add_group(group_name, group_type, owner_id, group_description=None): - """Adds a new group to the Groups table with a generated ID.""" - if group_type not in ['Public', 'Private']: - print("ERROR: Group type must be 'Public' or 'Private'.") - return None - - group_id = generate_id() +def create_group_with_itinerary(owner_id, group_name, group_type, destination_ids, **kwargs): + """Creates a new group with a travel itinerary (chain of destinations).""" conn = get_db_connection() try: cursor = conn.cursor() - # Check if owner exists and get username - cursor.execute("SELECT userid, username FROM Users WHERE userid = ?", (owner_id,)) - owner_data = cursor.fetchone() - if not owner_data: - print(f"ERROR: Owner with ID '{owner_id}' does not exist.") + # Check if user exists and can create group + cursor.execute("SELECT userid, username, current_group_id, is_group_owner FROM Users WHERE userid = ?", (owner_id,)) + user_data = cursor.fetchone() + if not user_data: + print(f"ERROR: User with ID '{owner_id}' does not exist.") + return None + + if user_data['current_group_id']: + print("ERROR: You are already a member of a group. Leave your current group first.") + return None + + if user_data['is_group_owner']: + print("ERROR: You already own a group. Delete your existing group first.") return None - owner_username = owner_data['username'] + # Validate all destination IDs exist + for dest_id in destination_ids: + cursor.execute("SELECT destination_id FROM Destinations WHERE destination_id = ?", (dest_id,)) + if not cursor.fetchone(): + print(f"ERROR: Destination with ID '{dest_id}' does not exist.") + return None + + group_id = generate_id() + owner_username = user_data['username'] # Create the group conn.execute( """INSERT INTO Groups (group_id, group_name, group_description, group_type, - owner_id, member_count, member_list) VALUES (?, ?, ?, ?, ?, ?, ?)""", - (group_id, group_name, group_description, group_type, owner_id, 1, owner_username) + owner_id, max_members, total_duration_days, estimated_total_cost, + travel_start_date, travel_end_date) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", + (group_id, group_name, kwargs.get('group_description'), group_type, + owner_id, kwargs.get('max_members', 50), kwargs.get('total_duration_days'), + kwargs.get('estimated_total_cost'), kwargs.get('travel_start_date'), + kwargs.get('travel_end_date')) ) + # Add destinations to itinerary in order + for order, dest_id in enumerate(destination_ids, 1): + itinerary_id = generate_id() + conn.execute( + """INSERT INTO TravelItinerary (itinerary_id, group_id, destination_id, visit_order, + duration_days, notes) VALUES (?, ?, ?, ?, ?, ?)""", + (itinerary_id, group_id, dest_id, order, + kwargs.get('duration_per_destination', 2), + f"Stop {order} in the travel chain") + ) + # Add owner to GroupMembers member_id = generate_id() conn.execute( @@ -205,338 +300,216 @@ def add_group(group_name, group_type, owner_id, group_description=None): (member_id, group_id, owner_id, owner_username) ) - # Update owner's current_group_id - conn.execute("UPDATE Users SET current_group_id = ? WHERE userid = ?", (group_id, owner_id)) + # Update user's status + conn.execute( + "UPDATE Users SET current_group_id = ?, is_group_owner = TRUE WHERE userid = ?", + (group_id, owner_id) + ) conn.commit() - print(f"SUCCESS: Group '{group_name}' added with ID: {group_id}") - print(f"Owner '{owner_username}' automatically joined the group.") + print(f"SUCCESS: Group '{group_name}' created with {len(destination_ids)}-destination itinerary.") + print(f"Group ID: {group_id}") return group_id except sqlite3.Error as e: - print(f"ERROR: Could not add group. {e}") + print(f"ERROR: Could not create group. {e}") return None finally: conn.close() -def delete_group(group_id): - """Deletes a group from the Groups table by its ID.""" - conn = get_db_connection() - try: - cursor = conn.cursor() - - # Update all users in this group to have no current group - cursor.execute("UPDATE Users SET current_group_id = NULL WHERE current_group_id = ?", (group_id,)) - - # Delete the group (GroupMembers and GroupMessages will be deleted due to CASCADE) - cursor.execute("DELETE FROM Groups WHERE group_id = ?", (group_id,)) - if cursor.rowcount == 0: - print(f"ERROR: No group found with ID '{group_id}'.") - else: - conn.commit() - print(f"SUCCESS: Group with ID '{group_id}' deleted along with all messages.") - except sqlite3.Error as e: - print(f"ERROR: Could not delete group. {e}") - finally: - conn.close() - -def join_group(user_id, group_id): - """Adds a user to a group and updates all related tables.""" +def view_group_itinerary(group_id): + """Display the travel itinerary for a group.""" conn = get_db_connection() try: cursor = conn.cursor() - # Check if user exists - cursor.execute("SELECT userid, username, current_group_id FROM Users WHERE userid = ?", (user_id,)) - user_data = cursor.fetchone() - if not user_data: - print(f"ERROR: User with ID '{user_id}' does not exist.") - return False - - username = user_data['username'] - current_group = user_data['current_group_id'] - - # Check if group exists - cursor.execute("SELECT group_id, group_name, member_count, member_list FROM Groups WHERE group_id = ?", (group_id,)) + # Get group info + cursor.execute("SELECT group_name, group_description FROM Groups WHERE group_id = ?", (group_id,)) group_data = cursor.fetchone() if not group_data: print(f"ERROR: Group with ID '{group_id}' does not exist.") - return False - - # Check if user is already in this group - if current_group == group_id: - print("ERROR: User is already a member of this group.") - return False - - # If user is in another group, leave it first - if current_group: - leave_group(user_id) + return - # Add user to GroupMembers - member_id = generate_id() - conn.execute( - "INSERT INTO GroupMembers (member_id, group_id, user_id, username) VALUES (?, ?, ?, ?)", - (member_id, group_id, user_id, username) - ) + print(f"\n--- Travel Itinerary for '{group_data['group_name']}' ---") + if group_data['group_description']: + print(f"Description: {group_data['group_description']}") - # Update user's current_group_id - conn.execute("UPDATE Users SET current_group_id = ? WHERE userid = ?", (group_id, user_id)) - - # Update group's member_count and member_list - new_count = group_data['member_count'] + 1 - current_members = group_data['member_list'] or "" - new_member_list = f"{current_members}, {username}" if current_members else username + # Get itinerary + cursor.execute(""" + SELECT ti.visit_order, d.destination_name, d.state_province, d.country, + ti.duration_days, ti.planned_arrival_date, ti.planned_departure_date, + ti.estimated_cost, ti.accommodation_type, ti.transport_to_next, ti.notes + FROM TravelItinerary ti + JOIN Destinations d ON ti.destination_id = d.destination_id + WHERE ti.group_id = ? + ORDER BY ti.visit_order + """, (group_id,)) - conn.execute( - "UPDATE Groups SET member_count = ?, member_list = ? WHERE group_id = ?", - (new_count, new_member_list, group_id) - ) + itinerary = cursor.fetchall() - conn.commit() - print(f"SUCCESS: User '{username}' joined group '{group_data['group_name']}'.") - return True + if not itinerary: + print("No itinerary found for this group.") + return + + print(f"\nTravel Chain ({len(itinerary)} destinations):") + print("=" * 60) + + for stop in itinerary: + print(f"Stop {stop['visit_order']}: {stop['destination_name']}") + print(f" Location: {stop['state_province']}, {stop['country']}") + print(f" Duration: {stop['duration_days']} day(s)") + if stop['planned_arrival_date']: + print(f" Arrival: {stop['planned_arrival_date']}") + if stop['planned_departure_date']: + print(f" Departure: {stop['planned_departure_date']}") + if stop['estimated_cost']: + print(f" Estimated Cost: {stop['estimated_cost']}") + if stop['accommodation_type']: + print(f" Accommodation: {stop['accommodation_type']}") + if stop['transport_to_next']: + print(f" Transport to Next: {stop['transport_to_next']}") + if stop['notes']: + print(f" Notes: {stop['notes']}") + + if stop['visit_order'] < len(itinerary): + print(" ↓") + print("-" * 40) + except sqlite3.Error as e: - print(f"ERROR: Could not join group. {e}") - return False + print(f"ERROR: Could not retrieve itinerary. {e}") finally: conn.close() -def leave_group(user_id): - """Removes a user from their current group.""" +def add_destination_to_itinerary(group_id, destination_id, owner_id, **kwargs): + """Add a new destination to existing group's itinerary.""" conn = get_db_connection() try: cursor = conn.cursor() - # Get user and current group info - cursor.execute("SELECT username, current_group_id FROM Users WHERE userid = ?", (user_id,)) - user_data = cursor.fetchone() - if not user_data or not user_data['current_group_id']: - print("ERROR: User is not in any group.") + # Verify group ownership + cursor.execute("SELECT owner_id FROM Groups WHERE group_id = ?", (group_id,)) + group_data = cursor.fetchone() + if not group_data: + print(f"ERROR: Group with ID '{group_id}' does not exist.") return False - username = user_data['username'] - group_id = user_data['current_group_id'] - - # Get group info - cursor.execute("SELECT group_name, member_count, member_list FROM Groups WHERE group_id = ?", (group_id,)) - group_data = cursor.fetchone() - - # Remove from GroupMembers - cursor.execute("DELETE FROM GroupMembers WHERE group_id = ? AND user_id = ?", (group_id, user_id)) - - # Update user's current_group_id to NULL - conn.execute("UPDATE Users SET current_group_id = NULL WHERE userid = ?", (user_id,)) - - # Update group's member_count and member_list - new_count = group_data['member_count'] - 1 - current_members = group_data['member_list'] or "" - member_list = [member.strip() for member in current_members.split(',') if member.strip()] - if username in member_list: - member_list.remove(username) - new_member_list = ', '.join(member_list) - - conn.execute( - "UPDATE Groups SET member_count = ?, member_list = ? WHERE group_id = ?", - (new_count, new_member_list, group_id) - ) - - conn.commit() - print(f"SUCCESS: User '{username}' left group '{group_data['group_name']}'.") - return True - except sqlite3.Error as e: - print(f"ERROR: Could not leave group. {e}") - return False - finally: - conn.close() - -def send_message(sender_id, group_id, message): - """Sends a message to a group chat.""" - conn = get_db_connection() - try: - cursor = conn.cursor() - - # Check if sender exists and get username - cursor.execute("SELECT username, current_group_id FROM Users WHERE userid = ?", (sender_id,)) - sender_data = cursor.fetchone() - if not sender_data: - print(f"ERROR: Sender with ID '{sender_id}' does not exist.") + if group_data['owner_id'] != owner_id: + print("ERROR: Only group owner can modify the itinerary.") return False - sender_username = sender_data['username'] - - # Check if group exists - cursor.execute("SELECT group_id FROM Groups WHERE group_id = ?", (group_id,)) - if not cursor.fetchone(): - print(f"ERROR: Group with ID '{group_id}' does not exist.") + # Check if destination exists + cursor.execute("SELECT destination_name FROM Destinations WHERE destination_id = ?", (destination_id,)) + dest_data = cursor.fetchone() + if not dest_data: + print(f"ERROR: Destination with ID '{destination_id}' does not exist.") return False - # Check if sender is a member of the group - cursor.execute("SELECT * FROM GroupMembers WHERE group_id = ? AND user_id = ?", (group_id, sender_id)) - if not cursor.fetchone(): - print("ERROR: You must be a member of the group to send messages.") + # Check if destination is already in itinerary + cursor.execute("SELECT * FROM TravelItinerary WHERE group_id = ? AND destination_id = ?", + (group_id, destination_id)) + if cursor.fetchone(): + print("ERROR: This destination is already in the itinerary.") return False - # Insert the message + # Get next order number + cursor.execute("SELECT COALESCE(MAX(visit_order), 0) + 1 FROM TravelItinerary WHERE group_id = ?", (group_id,)) + next_order = cursor.fetchone()[0] + + # Add to itinerary + itinerary_id = generate_id() conn.execute( - "INSERT INTO GroupMessages (group_id, sender_id, sender_username, message) VALUES (?, ?, ?, ?)", - (group_id, sender_id, sender_username, message) + """INSERT INTO TravelItinerary (itinerary_id, group_id, destination_id, visit_order, + duration_days, estimated_cost, notes) VALUES (?, ?, ?, ?, ?, ?, ?)""", + (itinerary_id, group_id, destination_id, next_order, + kwargs.get('duration_days', 2), kwargs.get('estimated_cost'), + kwargs.get('notes', f"Added destination {next_order}")) ) conn.commit() - print(f"SUCCESS: Message sent to group.") + print(f"SUCCESS: '{dest_data['destination_name']}' added to itinerary as stop {next_order}.") return True except sqlite3.Error as e: - print(f"ERROR: Could not send message. {e}") + print(f"ERROR: Could not add destination to itinerary. {e}") return False finally: conn.close() -def get_group_messages(group_id, limit=50): - """Retrieves recent messages from a group chat.""" +def view_destinations(): + """Display all available destinations.""" conn = get_db_connection() try: cursor = conn.cursor() - cursor.execute(""" - SELECT message_id, sender_username, message, timestamp - FROM GroupMessages - WHERE group_id = ? - ORDER BY timestamp DESC - LIMIT ? - """, (group_id, limit)) + cursor.execute("SELECT * FROM Destinations ORDER BY destination_name") + destinations = cursor.fetchall() - messages = cursor.fetchall() - return list(reversed(messages)) # Return in chronological order - except sqlite3.Error as e: - print(f"ERROR: Could not retrieve messages. {e}") - return [] - finally: - conn.close() - -def view_group_chat(group_id): - """Displays recent messages from a group chat.""" - conn = get_db_connection() - try: - cursor = conn.cursor() - - # Get group name - cursor.execute("SELECT group_name FROM Groups WHERE group_id = ?", (group_id,)) - group_data = cursor.fetchone() - if not group_data: - print(f"ERROR: Group with ID '{group_id}' does not exist.") + if not destinations: + print("No destinations found.") return - print(f"\n--- Chat for Group: {group_data['group_name']} ---") - - messages = get_group_messages(group_id, 20) # Last 20 messages - - if not messages: - print("No messages in this group yet.") - return - - for msg in messages: - timestamp = msg['timestamp'][:19] # Remove microseconds - print(f"[{timestamp}] {msg['sender_username']}: {msg['message']}") - + print("\n--- Available Destinations ---") + for dest in destinations: + print(f"ID: {dest['destination_id']}") + print(f"Name: {dest['destination_name']}") + print(f"Location: {dest['state_province']}, {dest['country']}") + print(f"Best Time: {dest['best_time_to_visit'] or 'N/A'}") + print(f"Difficulty: {dest['difficulty_level'] or 'N/A'}") + print(f"Budget/Day: {dest['estimated_budget_per_day'] or 'N/A'}") + print(f"Description: {dest['description'] or 'No description'}") + print("-" * 50) except sqlite3.Error as e: - print(f"ERROR: Could not view chat. {e}") + print(f"ERROR: Could not retrieve destinations. {e}") finally: conn.close() -def view_users(): - """Displays all users with their current group information.""" +def search_groups_by_destination(destination_name=None): + """Search for groups traveling to a specific destination.""" conn = get_db_connection() try: cursor = conn.cursor() - cursor.execute(""" - SELECT u.userid, u.username, u.email, - COALESCE(g.group_name, 'No Group') as current_group - FROM Users u - LEFT JOIN Groups g ON u.current_group_id = g.group_id - """) - users = cursor.fetchall() - if not users: - print("No users found.") - return + if destination_name: + cursor.execute(""" + SELECT DISTINCT g.group_id, g.group_name, g.group_type, g.member_count, g.max_members, + COUNT(ti.destination_id) as destination_count, + u.username as owner_name + FROM Groups g + JOIN TravelItinerary ti ON g.group_id = ti.group_id + JOIN Destinations d ON ti.destination_id = d.destination_id + JOIN Users u ON g.owner_id = u.userid + WHERE d.destination_name LIKE ? + GROUP BY g.group_id + ORDER BY g.created_at DESC + """, (f"%{destination_name}%",)) + else: + cursor.execute(""" + SELECT g.group_id, g.group_name, g.group_type, g.member_count, g.max_members, + COUNT(ti.destination_id) as destination_count, + u.username as owner_name + FROM Groups g + LEFT JOIN TravelItinerary ti ON g.group_id = ti.group_id + JOIN Users u ON g.owner_id = u.userid + GROUP BY g.group_id + ORDER BY g.created_at DESC + """) - print("\n--- Users List ---") - print(f"{'ID':<32} {'Username':<15} {'Email':<25} {'Current Group':<20}") - print("-" * 95) - for user in users: - print(f"{user['userid']:<32} {user['username']:<15} {user['email']:<25} {user['current_group']:<20}") - except sqlite3.Error as e: - print(f"ERROR: Could not retrieve users. {e}") - finally: - conn.close() - -def view_groups(): - """Displays all groups with their member information.""" - conn = get_db_connection() - try: - cursor = conn.cursor() - cursor.execute(""" - SELECT g.group_id, g.group_name, g.group_type, g.group_description, - u.username as owner_name, g.member_count, g.member_list, g.created_at - FROM Groups g - JOIN Users u ON g.owner_id = u.userid - """) groups = cursor.fetchall() if not groups: print("No groups found.") return - print("\n--- Groups List ---") + search_text = f" for '{destination_name}'" if destination_name else "" + print(f"\n--- Groups{search_text} ---") + for group in groups: - print(f"ID: {group['group_id']}") - print(f"Name: {group['group_name']}") + print(f"Group: {group['group_name']} (ID: {group['group_id']})") print(f"Type: {group['group_type']}") print(f"Owner: {group['owner_name']}") - print(f"Description: {group['group_description'] or 'No description'}") - print(f"Member Count: {group['member_count']}") - print(f"Members: {group['member_list'] or 'No members'}") - print(f"Created: {group['created_at']}") - print("-" * 60) - except sqlite3.Error as e: - print(f"ERROR: Could not retrieve groups. {e}") - finally: - conn.close() - -def get_group_members(group_id): - """Returns all members of a specific group (for frontend API).""" - conn = get_db_connection() - try: - cursor = conn.cursor() - cursor.execute(""" - SELECT gm.user_id, gm.username, gm.role, gm.joined_at, - u.email, u.phone_no - FROM GroupMembers gm - JOIN Users u ON gm.user_id = u.userid - WHERE gm.group_id = ? - ORDER BY gm.role DESC, gm.joined_at ASC - """, (group_id,)) - return cursor.fetchall() - except sqlite3.Error as e: - print(f"ERROR: Could not get group members. {e}") - return [] - finally: - conn.close() - -def get_user_group(user_id): - """Returns the current group of a user (for frontend API).""" - conn = get_db_connection() - try: - cursor = conn.cursor() - cursor.execute(""" - SELECT g.group_id, g.group_name, g.group_type, g.group_description, - g.member_count, g.member_list - FROM Users u - JOIN Groups g ON u.current_group_id = g.group_id - WHERE u.userid = ? - """, (user_id,)) - return cursor.fetchone() + print(f"Members: {group['member_count']}/{group['max_members']}") + print(f"Destinations in Itinerary: {group['destination_count']}") + print("-" * 50) + except sqlite3.Error as e: - print(f"ERROR: Could not get user group. {e}") - return None + print(f"ERROR: Could not search groups. {e}") finally: conn.close() @@ -547,18 +520,15 @@ def main(): while True: print("\n--- Travel Together DB Manager ---") print("1. Add User") - print("2. Delete User") - print("3. View Users") - print("4. Add Group") - print("5. Delete Group") - print("6. View Groups") - print("7. Join Group") - print("8. Leave Group") - print("9. Send Message") - print("10. View Group Chat") - print("11. Exit") + print("2. Add Destination") + print("3. Create Group with Travel Chain") + print("4. View Group Itinerary") + print("5. Add Destination to Itinerary") + print("6. View All Destinations") + print("7. Search Groups by Destination") + print("8. Exit") - choice = input("Enter your choice (1-11): ").strip() + choice = input("Enter your choice (1-8): ").strip() if choice == '1': print("\n--- Add New User ---") @@ -579,65 +549,79 @@ def main(): continue email = input("Enter email: ").strip() - phone_no = input("Enter phone number (optional): ").strip() or None - gender = input("Enter gender (Male/Female/Other, optional): ").strip() or None - marital_status = input("Enter marital status (Single/Married/Divorced/Widowed, optional): ").strip() or None - bio = input("Enter bio (optional): ").strip() or None + add_user(username, password, email) - add_user(username, password, email, phone_no=phone_no, gender=gender, - marital_status=marital_status, bio=bio) - elif choice == '2': - user_id = input("Enter the userid to delete: ").strip() - delete_user(user_id) + print("\n--- Add Destination ---") + destination_name = input("Enter destination name: ").strip() + state_province = input("Enter state/province: ").strip() + country = input("Enter country: ").strip() + description = input("Enter description (optional): ").strip() or None - elif choice == '3': - view_users() + add_destination(destination_name, state_province, country, description=description) - elif choice == '4': - print("\n--- Add New Group ---") + elif choice == '3': + print("\n--- Create Group with Travel Chain ---") + owner_id = input("Enter your userid: ").strip() group_name = input("Enter group name: ").strip() group_type = input("Enter group type (Public/Private): ").strip().capitalize() - owner_id = input("Enter the owner's userid: ").strip() + + # Get destinations for the travel chain + print("\nFirst, let's see available destinations:") + view_destinations() + + print("\nNow enter the destinations for your travel chain:") + destination_ids = [] + while True: + dest_id = input(f"Enter destination ID for stop {len(destination_ids) + 1} (or 'done' to finish): ").strip() + if dest_id.lower() == 'done': + break + if dest_id: + destination_ids.append(dest_id) + + if not destination_ids: + print("ERROR: At least one destination is required.") + continue + group_description = input("Enter group description (optional): ").strip() or None - add_group(group_name, group_type, owner_id, group_description) + total_days = input("Enter total trip duration in days (optional): ").strip() + total_days = int(total_days) if total_days.isdigit() else None + + create_group_with_itinerary( + owner_id, group_name, group_type, destination_ids, + group_description=group_description, total_duration_days=total_days + ) + + elif choice == '4': + group_id = input("Enter group ID to view itinerary: ").strip() + view_group_itinerary(group_id) elif choice == '5': - group_id = input("Enter the group_id to delete: ").strip() - delete_group(group_id) + print("\n--- Add Destination to Existing Itinerary ---") + owner_id = input("Enter your userid: ").strip() + group_id = input("Enter your group ID: ").strip() + destination_id = input("Enter destination ID to add: ").strip() + duration = input("Enter duration in days (default 2): ").strip() + duration = int(duration) if duration.isdigit() else 2 + notes = input("Enter notes (optional): ").strip() or None + + add_destination_to_itinerary(group_id, destination_id, owner_id, + duration_days=duration, notes=notes) elif choice == '6': - view_groups() + view_destinations() elif choice == '7': - print("\n--- Join Group ---") - user_id = input("Enter your userid: ").strip() - group_id = input("Enter the group_id to join: ").strip() - join_group(user_id, group_id) + destination_name = input("Enter destination name to search (or press Enter for all groups): ").strip() + search_groups_by_destination(destination_name if destination_name else None) elif choice == '8': - print("\n--- Leave Group ---") - user_id = input("Enter your userid: ").strip() - leave_group(user_id) - - elif choice == '9': - print("\n--- Send Message ---") - sender_id = input("Enter your userid: ").strip() - group_id = input("Enter the group_id: ").strip() - message = input("Enter your message: ").strip() - send_message(sender_id, group_id, message) - - elif choice == '10': - print("\n--- View Group Chat ---") - group_id = input("Enter the group_id: ").strip() - view_group_chat(group_id) - - elif choice == '11': print("Exiting... Have a great day!") break else: - print("Invalid choice. Please enter a number between 1-11.") + print("Invalid choice. Please enter a number between 1-8.") if __name__ == "__main__": main() + From 20d08b55a90a00fb1cb7653facb150564950fd55 Mon Sep 17 00:00:00 2001 From: RACHKANC Date: Tue, 2 Sep 2025 20:28:58 +0530 Subject: [PATCH 4/4] Update database.py final database .. each and every required feature added --- database.py | 1030 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 848 insertions(+), 182 deletions(-) diff --git a/database.py b/database.py index cb2f5f1..7783459 100644 --- a/database.py +++ b/database.py @@ -7,24 +7,21 @@ def generate_id(): return uuid.uuid4().hex def validate_password(password): - """ - Validates password strength. - Requirements: At least 6 characters, contains numbers, alphabets, and special characters. - """ + """Validates password strength requirements.""" if len(password) < 6: return False, "Password must be at least 6 characters long." - if not re.search(r'[a-zA-Z]', password): return False, "Password must contain at least one letter." - if not re.search(r'\d', password): return False, "Password must contain at least one number." - if not re.search(r'[!@#$%^&*()_+\-=\[\]{};\':"\\|,.<>\/?]', password): return False, "Password must contain at least one special character." - return True, "Password is strong." +def validate_email(email): + """Basic email validation.""" + return "@" in email and "." in email.split("@")[1] + def get_db_connection(): """Establishes and returns a database connection.""" conn = sqlite3.connect('travel_together.db') @@ -57,7 +54,7 @@ def setup_database(): """ cursor.execute(create_users_table_query) - # Groups table (main travel group) + # Groups table create_groups_table_query = """ CREATE TABLE IF NOT EXISTS Groups ( group_id TEXT PRIMARY KEY, @@ -78,7 +75,7 @@ def setup_database(): """ cursor.execute(create_groups_table_query) - # Destinations table (places that can be visited) + # Destinations table create_destinations_table_query = """ CREATE TABLE IF NOT EXISTS Destinations ( destination_id TEXT PRIMARY KEY, @@ -97,7 +94,7 @@ def setup_database(): """ cursor.execute(create_destinations_table_query) - # TravelItinerary table (links groups to multiple destinations in sequence) + # TravelItinerary table create_travel_itinerary_query = """ CREATE TABLE IF NOT EXISTS TravelItinerary ( itinerary_id TEXT PRIMARY KEY, @@ -170,23 +167,279 @@ def setup_database(): """ cursor.execute(create_group_messages_table_query) - # Indexes for performance + # Notifications table + create_notifications_table_query = """ + CREATE TABLE IF NOT EXISTS Notifications ( + notification_id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + group_id TEXT, + notification_type TEXT CHECK(notification_type IN ('join_request', 'member_joined', 'member_left', 'status_update', 'group_update')) DEFAULT 'group_update', + message TEXT NOT NULL, + is_read BOOLEAN DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES Users (userid) ON DELETE CASCADE, + FOREIGN KEY (group_id) REFERENCES Groups (group_id) ON DELETE CASCADE + ); + """ + cursor.execute(create_notifications_table_query) + + # Performance indexes cursor.execute("CREATE INDEX IF NOT EXISTS idx_travel_itinerary_group ON TravelItinerary(group_id, visit_order);") cursor.execute("CREATE INDEX IF NOT EXISTS idx_tourist_spots_destination ON TouristSpots(destination_id);") cursor.execute("CREATE INDEX IF NOT EXISTS idx_group_messages_group_timestamp ON GroupMessages(group_id, timestamp);") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_group_members_group ON GroupMembers(group_id);") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_group_members_user ON GroupMembers(user_id);") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_notifications_user ON Notifications(user_id, is_read);") print("Database and tables are ready.") conn.commit() conn.close() +# NOTIFICATION FUNCTIONS + +def add_notification(user_id, group_id, notification_type, message): + """Adds a new notification for a user.""" + conn = get_db_connection() + try: + conn.execute( + "INSERT INTO Notifications (user_id, group_id, notification_type, message) VALUES (?, ?, ?, ?)", + (user_id, group_id, notification_type, message) + ) + conn.commit() + return True + except sqlite3.Error as e: + print(f"ERROR: Could not add notification. {e}") + return False + finally: + conn.close() + +def notify_group_members_join(user_id, group_id, joining_username): + """Notifies existing group members when someone joins (public groups only).""" + conn = get_db_connection() + try: + cursor = conn.cursor() + + # Check if group is public + cursor.execute("SELECT group_name, group_type FROM Groups WHERE group_id = ?", (group_id,)) + group = cursor.fetchone() + if not group or group['group_type'] != 'Public': + return + + # Get all approved members except the one who just joined + cursor.execute(""" + SELECT user_id FROM GroupMembers + WHERE group_id = ? AND user_id != ? AND join_status = 'Approved' + """, (group_id, user_id)) + + members = cursor.fetchall() + message = f"'{joining_username}' has joined your group '{group['group_name']}'." + + for member in members: + add_notification(member['user_id'], group_id, 'member_joined', message) + + except sqlite3.Error as e: + print(f"ERROR: Could not send join notifications. {e}") + finally: + conn.close() + +def notify_group_members_leave(user_id, group_id, leaving_username): + """Notifies existing group members when someone leaves (public groups only).""" + conn = get_db_connection() + try: + cursor = conn.cursor() + + # Check if group is public + cursor.execute("SELECT group_name, group_type FROM Groups WHERE group_id = ?", (group_id,)) + group = cursor.fetchone() + if not group or group['group_type'] != 'Public': + return + + # Get all approved members except the one who left + cursor.execute(""" + SELECT user_id FROM GroupMembers + WHERE group_id = ? AND user_id != ? AND join_status = 'Approved' + """, (group_id, user_id)) + + members = cursor.fetchall() + message = f"'{leaving_username}' has left your group '{group['group_name']}'." + + for member in members: + add_notification(member['user_id'], group_id, 'member_left', message) + + except sqlite3.Error as e: + print(f"ERROR: Could not send leave notifications. {e}") + finally: + conn.close() + +def notify_owner_join_request(requesting_user_id, group_id): + """Notifies group owner when someone requests to join a private group.""" + conn = get_db_connection() + try: + cursor = conn.cursor() + + # Get group owner and requesting user info + cursor.execute("SELECT owner_id, group_name FROM Groups WHERE group_id = ?", (group_id,)) + group = cursor.fetchone() + if not group: + return + + cursor.execute("SELECT username FROM Users WHERE userid = ?", (requesting_user_id,)) + user = cursor.fetchone() + if not user: + return + + message = f"'{user['username']}' has requested to join your private group '{group['group_name']}'." + add_notification(group['owner_id'], group_id, 'join_request', message) + + except sqlite3.Error as e: + print(f"ERROR: Could not send join request notification. {e}") + finally: + conn.close() + +def notify_user_status_update(user_id, group_id, new_status): + """Notifies user when their membership status changes.""" + conn = get_db_connection() + try: + cursor = conn.cursor() + + cursor.execute("SELECT group_name FROM Groups WHERE group_id = ?", (group_id,)) + group = cursor.fetchone() + if not group: + return + + if new_status == 'Approved': + message = f"Your request to join '{group['group_name']}' has been approved!" + elif new_status == 'Rejected': + message = f"Your request to join '{group['group_name']}' has been rejected." + elif new_status == 'Blocked': + message = f"You have been blocked from group '{group['group_name']}'." + else: + return + + add_notification(user_id, group_id, 'status_update', message) + + except sqlite3.Error as e: + print(f"ERROR: Could not send status update notification. {e}") + finally: + conn.close() + +def view_notifications(user_id, unread_only=False): + """Displays notifications for a user.""" + conn = get_db_connection() + try: + cursor = conn.cursor() + + if unread_only: + cursor.execute(""" + SELECT n.notification_id, n.message, n.notification_type, n.is_read, n.created_at, + g.group_name + FROM Notifications n + LEFT JOIN Groups g ON n.group_id = g.group_id + WHERE n.user_id = ? AND n.is_read = 0 + ORDER BY n.created_at DESC + """, (user_id,)) + else: + cursor.execute(""" + SELECT n.notification_id, n.message, n.notification_type, n.is_read, n.created_at, + g.group_name + FROM Notifications n + LEFT JOIN Groups g ON n.group_id = g.group_id + WHERE n.user_id = ? + ORDER BY n.created_at DESC + LIMIT 20 + """, (user_id,)) + + notifications = cursor.fetchall() + + if not notifications: + print("No notifications found.") + return + + title = "Unread Notifications" if unread_only else "Recent Notifications" + print(f"\n--- {title} ---") + + for notif in notifications: + status = "UNREAD" if not notif['is_read'] else "READ" + timestamp = notif['created_at'][:19] # Remove microseconds + print(f"[{status}] {timestamp}") + print(f"Type: {notif['notification_type']}") + print(f"Message: {notif['message']}") + if notif['group_name']: + print(f"Group: {notif['group_name']}") + print(f"ID: {notif['notification_id']}") + print("-" * 50) + + except sqlite3.Error as e: + print(f"ERROR: Could not retrieve notifications. {e}") + finally: + conn.close() + +def mark_notification_read(notification_id, user_id): + """Marks a notification as read.""" + conn = get_db_connection() + try: + cursor = conn.cursor() + cursor.execute(""" + UPDATE Notifications SET is_read = 1 + WHERE notification_id = ? AND user_id = ? + """, (notification_id, user_id)) + + if cursor.rowcount == 0: + print("ERROR: Notification not found or access denied.") + return False + + conn.commit() + print("SUCCESS: Notification marked as read.") + return True + except sqlite3.Error as e: + print(f"ERROR: Could not mark notification as read. {e}") + return False + finally: + conn.close() + +def mark_all_notifications_read(user_id): + """Marks all notifications as read for a user.""" + conn = get_db_connection() + try: + cursor = conn.cursor() + cursor.execute("UPDATE Notifications SET is_read = 1 WHERE user_id = ?", (user_id,)) + + conn.commit() + print(f"SUCCESS: All notifications marked as read. ({cursor.rowcount} notifications updated)") + return True + except sqlite3.Error as e: + print(f"ERROR: Could not mark notifications as read. {e}") + return False + finally: + conn.close() + +def get_unread_count(user_id): + """Gets the count of unread notifications for a user.""" + conn = get_db_connection() + try: + cursor = conn.cursor() + cursor.execute("SELECT COUNT(*) as count FROM Notifications WHERE user_id = ? AND is_read = 0", (user_id,)) + count = cursor.fetchone()['count'] + return count + except sqlite3.Error as e: + print(f"ERROR: Could not get unread count. {e}") + return 0 + finally: + conn.close() + +# USER MANAGEMENT FUNCTIONS + def add_user(username, password, email, **kwargs): - """Adds a new user to the Users table with a generated ID.""" - # Validate password strength + """Adds a new user to the Users table.""" is_valid, message = validate_password(password) if not is_valid: print(f"ERROR: {message}") return None + if not validate_email(email): + print("ERROR: Invalid email format.") + return None + user_id = generate_id() conn = get_db_connection() @@ -213,6 +466,128 @@ def add_user(username, password, email, **kwargs): finally: conn.close() +def update_user_profile(user_id, **kwargs): + """Updates user's profile information.""" + allowed_fields = ["phone_no", "gender", "marital_status", "profile_picture", "bio"] + conn = get_db_connection() + + try: + cursor = conn.cursor() + updates = [] + values = [] + + for field in allowed_fields: + if field in kwargs and kwargs[field] is not None: + updates.append(f"{field} = ?") + values.append(kwargs[field]) + + if not updates: + print("ERROR: No valid fields provided for update.") + return False + + values.append(user_id) + sql = f"UPDATE Users SET {', '.join(updates)} WHERE userid = ?" + + cursor.execute(sql, values) + if cursor.rowcount == 0: + print(f"ERROR: No user found with ID '{user_id}'.") + return False + + conn.commit() + print("SUCCESS: User profile updated.") + return True + except sqlite3.Error as e: + print(f"ERROR: Could not update user profile. {e}") + return False + finally: + conn.close() + +def update_password(user_id, new_password): + """Updates user's password after validation.""" + is_valid, message = validate_password(new_password) + if not is_valid: + print(f"ERROR: {message}") + return False + + conn = get_db_connection() + try: + cursor = conn.cursor() + cursor.execute("UPDATE Users SET password = ? WHERE userid = ?", (new_password, user_id)) + if cursor.rowcount == 0: + print(f"ERROR: No user found with ID '{user_id}'.") + return False + + conn.commit() + print("SUCCESS: Password updated successfully.") + return True + except sqlite3.Error as e: + print(f"ERROR: Could not update password. {e}") + return False + finally: + conn.close() + +def delete_user(user_id): + """Deletes a user from the Users table.""" + conn = get_db_connection() + try: + cursor = conn.cursor() + + # Handle group membership cleanup + cursor.execute("SELECT current_group_id, is_group_owner FROM Users WHERE userid = ?", (user_id,)) + user_data = cursor.fetchone() + + if user_data: + if user_data['current_group_id'] and not user_data['is_group_owner']: + leave_group(user_id) + elif user_data['is_group_owner']: + cursor.execute("SELECT group_id FROM Groups WHERE owner_id = ?", (user_id,)) + group_data = cursor.fetchone() + if group_data: + delete_group(group_data['group_id']) + + cursor.execute("DELETE FROM Users WHERE userid = ?", (user_id,)) + if cursor.rowcount == 0: + print(f"ERROR: No user found with ID '{user_id}'.") + return False + else: + conn.commit() + print(f"SUCCESS: User with ID '{user_id}' deleted.") + return True + except sqlite3.Error as e: + print(f"ERROR: Could not delete user. {e}") + return False + finally: + conn.close() + +def view_users(): + """Displays all users with their current group information.""" + conn = get_db_connection() + try: + cursor = conn.cursor() + cursor.execute(""" + SELECT u.userid, u.username, u.email, + COALESCE(g.group_name, 'No Group') as current_group + FROM Users u + LEFT JOIN Groups g ON u.current_group_id = g.group_id + """) + users = cursor.fetchall() + + if not users: + print("No users found.") + return + + print("\n--- Users List ---") + print(f"{'ID':<32} {'Username':<15} {'Email':<25} {'Current Group':<20}") + print("-" * 95) + for user in users: + print(f"{user['userid']:<32} {user['username']:<15} {user['email']:<25} {user['current_group']:<20}") + except sqlite3.Error as e: + print(f"ERROR: Could not retrieve users. {e}") + finally: + conn.close() + +# DESTINATION MANAGEMENT FUNCTIONS + def add_destination(destination_name, state_province, country, **kwargs): """Adds a new destination to the Destinations table.""" destination_id = generate_id() @@ -237,14 +612,43 @@ def add_destination(destination_name, state_province, country, **kwargs): finally: conn.close() +def view_destinations(): + """Display all available destinations.""" + conn = get_db_connection() + try: + cursor = conn.cursor() + cursor.execute("SELECT * FROM Destinations ORDER BY destination_name") + destinations = cursor.fetchall() + + if not destinations: + print("No destinations found.") + return + + print("\n--- Available Destinations ---") + for dest in destinations: + print(f"ID: {dest['destination_id']}") + print(f"Name: {dest['destination_name']}") + print(f"Location: {dest['state_province']}, {dest['country']}") + print(f"Best Time: {dest['best_time_to_visit'] or 'N/A'}") + print(f"Difficulty: {dest['difficulty_level'] or 'N/A'}") + print(f"Budget/Day: {dest['estimated_budget_per_day'] or 'N/A'}") + print(f"Description: {dest['description'] or 'No description'}") + print("-" * 50) + except sqlite3.Error as e: + print(f"ERROR: Could not retrieve destinations. {e}") + finally: + conn.close() + +# GROUP MANAGEMENT FUNCTIONS + def create_group_with_itinerary(owner_id, group_name, group_type, destination_ids, **kwargs): - """Creates a new group with a travel itinerary (chain of destinations).""" + """Creates a new group with a travel itinerary.""" conn = get_db_connection() try: cursor = conn.cursor() - # Check if user exists and can create group + # Validate user can create group cursor.execute("SELECT userid, username, current_group_id, is_group_owner FROM Users WHERE userid = ?", (owner_id,)) user_data = cursor.fetchone() if not user_data: @@ -316,203 +720,384 @@ def create_group_with_itinerary(owner_id, group_name, group_type, destination_id finally: conn.close() -def view_group_itinerary(group_id): - """Display the travel itinerary for a group.""" +def join_group(user_id, group_id): + """User joins a group with notification system.""" conn = get_db_connection() + try: cursor = conn.cursor() - # Get group info - cursor.execute("SELECT group_name, group_description FROM Groups WHERE group_id = ?", (group_id,)) + # Check user status + cursor.execute("SELECT userid, username, current_group_id, is_group_owner FROM Users WHERE userid = ?", (user_id,)) + user_data = cursor.fetchone() + if not user_data: + print(f"ERROR: User with ID '{user_id}' does not exist.") + return False + + if user_data['current_group_id']: + print("ERROR: You are already a member of a group. Leave your current group first.") + return False + + if user_data['is_group_owner']: + print("ERROR: You own a group. You cannot join another group.") + return False + + username = user_data['username'] + + # Check group availability + cursor.execute("""SELECT group_id, group_name, group_type, member_count, max_members + FROM Groups WHERE group_id = ?""", (group_id,)) group_data = cursor.fetchone() if not group_data: print(f"ERROR: Group with ID '{group_id}' does not exist.") - return + return False + + if group_data['member_count'] >= group_data['max_members']: + print("ERROR: Group is full.") + return False - print(f"\n--- Travel Itinerary for '{group_data['group_name']}' ---") - if group_data['group_description']: - print(f"Description: {group_data['group_description']}") + # Set join status based on group type + join_status = 'Pending' if group_data['group_type'] == 'Private' else 'Approved' - # Get itinerary - cursor.execute(""" - SELECT ti.visit_order, d.destination_name, d.state_province, d.country, - ti.duration_days, ti.planned_arrival_date, ti.planned_departure_date, - ti.estimated_cost, ti.accommodation_type, ti.transport_to_next, ti.notes - FROM TravelItinerary ti - JOIN Destinations d ON ti.destination_id = d.destination_id - WHERE ti.group_id = ? - ORDER BY ti.visit_order - """, (group_id,)) + # Add user to GroupMembers + member_id = generate_id() + conn.execute( + """INSERT INTO GroupMembers (member_id, group_id, user_id, username, join_status) + VALUES (?, ?, ?, ?, ?)""", + (member_id, group_id, user_id, username, join_status) + ) - itinerary = cursor.fetchall() + # Handle notifications and status updates + if join_status == 'Approved': + # Update user and group for public groups + conn.execute("UPDATE Users SET current_group_id = ? WHERE userid = ?", (group_id, user_id)) + conn.execute("UPDATE Groups SET member_count = member_count + 1 WHERE group_id = ?", (group_id,)) + print(f"SUCCESS: Joined group '{group_data['group_name']}'.") + + # Notify existing members about new member (public groups only) + conn.commit() # Commit first to ensure the member is added + notify_group_members_join(user_id, group_id, username) + else: + # For private groups, notify owner about join request + print(f"SUCCESS: Join request sent to group '{group_data['group_name']}'. Waiting for approval.") + conn.commit() # Commit first + notify_owner_join_request(user_id, group_id) + + return True + except sqlite3.Error as e: + print(f"ERROR: Could not join group. {e}") + return False + finally: + conn.close() + +def leave_group(user_id): + """Removes a user from their current group with notifications.""" + conn = get_db_connection() + try: + cursor = conn.cursor() - if not itinerary: - print("No itinerary found for this group.") - return + # Get user info + cursor.execute("SELECT username, current_group_id, is_group_owner FROM Users WHERE userid = ?", (user_id,)) + user_data = cursor.fetchone() + if not user_data or not user_data['current_group_id']: + print("ERROR: You are not in any group.") + return False - print(f"\nTravel Chain ({len(itinerary)} destinations):") - print("=" * 60) - - for stop in itinerary: - print(f"Stop {stop['visit_order']}: {stop['destination_name']}") - print(f" Location: {stop['state_province']}, {stop['country']}") - print(f" Duration: {stop['duration_days']} day(s)") - if stop['planned_arrival_date']: - print(f" Arrival: {stop['planned_arrival_date']}") - if stop['planned_departure_date']: - print(f" Departure: {stop['planned_departure_date']}") - if stop['estimated_cost']: - print(f" Estimated Cost: {stop['estimated_cost']}") - if stop['accommodation_type']: - print(f" Accommodation: {stop['accommodation_type']}") - if stop['transport_to_next']: - print(f" Transport to Next: {stop['transport_to_next']}") - if stop['notes']: - print(f" Notes: {stop['notes']}") - - if stop['visit_order'] < len(itinerary): - print(" ↓") - print("-" * 40) + group_id = user_data['current_group_id'] + username = user_data['username'] + + # Group owners must delete group instead + if user_data['is_group_owner']: + print("ERROR: You are the group owner. Delete the group instead of leaving.") + return False + # Get group info + cursor.execute("SELECT group_name, member_count FROM Groups WHERE group_id = ?", (group_id,)) + group_data = cursor.fetchone() + + # Remove from GroupMembers + cursor.execute("DELETE FROM GroupMembers WHERE group_id = ? AND user_id = ?", (group_id, user_id)) + + # Update user status + cursor.execute("UPDATE Users SET current_group_id = NULL WHERE userid = ?", (user_id,)) + + # Update group member count + cursor.execute("UPDATE Groups SET member_count = member_count - 1 WHERE group_id = ?", (group_id,)) + + conn.commit() + print(f"SUCCESS: You left group '{group_data['group_name']}'.") + + # Notify remaining members (public groups only) + notify_group_members_leave(user_id, group_id, username) + + return True except sqlite3.Error as e: - print(f"ERROR: Could not retrieve itinerary. {e}") + print(f"ERROR: Could not leave group. {e}") + return False finally: conn.close() -def add_destination_to_itinerary(group_id, destination_id, owner_id, **kwargs): - """Add a new destination to existing group's itinerary.""" +def update_member_status(group_id, owner_id, member_user_id, new_status): + """Updates member status in a group with notifications.""" + valid_statuses = ['Pending', 'Approved', 'Rejected', 'Blocked'] + + if new_status not in valid_statuses: + print(f"ERROR: Invalid status. Must be one of: {', '.join(valid_statuses)}") + return False + conn = get_db_connection() try: cursor = conn.cursor() # Verify group ownership - cursor.execute("SELECT owner_id FROM Groups WHERE group_id = ?", (group_id,)) + cursor.execute("SELECT owner_id, group_type FROM Groups WHERE group_id = ?", (group_id,)) group_data = cursor.fetchone() if not group_data: print(f"ERROR: Group with ID '{group_id}' does not exist.") return False - + if group_data['owner_id'] != owner_id: - print("ERROR: Only group owner can modify the itinerary.") + print("ERROR: Only group owner can update member status.") return False - - # Check if destination exists - cursor.execute("SELECT destination_name FROM Destinations WHERE destination_id = ?", (destination_id,)) - dest_data = cursor.fetchone() - if not dest_data: - print(f"ERROR: Destination with ID '{destination_id}' does not exist.") + + # Get member username for notifications + cursor.execute("SELECT username FROM Users WHERE userid = ?", (member_user_id,)) + member_data = cursor.fetchone() + if not member_data: + print("ERROR: Member not found.") return False - - # Check if destination is already in itinerary - cursor.execute("SELECT * FROM TravelItinerary WHERE group_id = ? AND destination_id = ?", - (group_id, destination_id)) - if cursor.fetchone(): - print("ERROR: This destination is already in the itinerary.") + + member_username = member_data['username'] + + # Update member status + cursor.execute("""UPDATE GroupMembers SET join_status = ? + WHERE group_id = ? AND user_id = ?""", + (new_status, group_id, member_user_id)) + + if cursor.rowcount == 0: + print("ERROR: Member not found in this group.") return False - - # Get next order number - cursor.execute("SELECT COALESCE(MAX(visit_order), 0) + 1 FROM TravelItinerary WHERE group_id = ?", (group_id,)) - next_order = cursor.fetchone()[0] - # Add to itinerary - itinerary_id = generate_id() - conn.execute( - """INSERT INTO TravelItinerary (itinerary_id, group_id, destination_id, visit_order, - duration_days, estimated_cost, notes) VALUES (?, ?, ?, ?, ?, ?, ?)""", - (itinerary_id, group_id, destination_id, next_order, - kwargs.get('duration_days', 2), kwargs.get('estimated_cost'), - kwargs.get('notes', f"Added destination {next_order}")) - ) + # Handle status changes + if new_status == 'Approved': + cursor.execute("UPDATE Users SET current_group_id = ? WHERE userid = ?", + (group_id, member_user_id)) + cursor.execute("UPDATE Groups SET member_count = member_count + 1 WHERE group_id = ?", + (group_id,)) + + # Notify existing members about new approved member + conn.commit() # Commit first + notify_group_members_join(member_user_id, group_id, member_username) + + elif new_status in ['Rejected', 'Blocked']: + cursor.execute("SELECT current_group_id FROM Users WHERE userid = ?", (member_user_id,)) + user_data = cursor.fetchone() + if user_data and user_data['current_group_id'] == group_id: + cursor.execute("UPDATE Users SET current_group_id = NULL WHERE userid = ?", + (member_user_id,)) + cursor.execute("UPDATE Groups SET member_count = member_count - 1 WHERE group_id = ?", + (group_id,)) conn.commit() - print(f"SUCCESS: '{dest_data['destination_name']}' added to itinerary as stop {next_order}.") + print(f"SUCCESS: Member status updated to '{new_status}'.") + + # Notify the member about status change + notify_user_status_update(member_user_id, group_id, new_status) + return True except sqlite3.Error as e: - print(f"ERROR: Could not add destination to itinerary. {e}") + print(f"ERROR: Could not update member status. {e}") return False finally: conn.close() -def view_destinations(): - """Display all available destinations.""" +def delete_group(group_id): + """Deletes a group from the Groups table by its ID.""" conn = get_db_connection() try: cursor = conn.cursor() - cursor.execute("SELECT * FROM Destinations ORDER BY destination_name") - destinations = cursor.fetchall() - if not destinations: - print("No destinations found.") - return - - print("\n--- Available Destinations ---") - for dest in destinations: - print(f"ID: {dest['destination_id']}") - print(f"Name: {dest['destination_name']}") - print(f"Location: {dest['state_province']}, {dest['country']}") - print(f"Best Time: {dest['best_time_to_visit'] or 'N/A'}") - print(f"Difficulty: {dest['difficulty_level'] or 'N/A'}") - print(f"Budget/Day: {dest['estimated_budget_per_day'] or 'N/A'}") - print(f"Description: {dest['description'] or 'No description'}") - print("-" * 50) + # Update all users in this group + cursor.execute("UPDATE Users SET current_group_id = NULL, is_group_owner = FALSE WHERE current_group_id = ?", (group_id,)) + + # Delete the group (cascade deletes related records) + cursor.execute("DELETE FROM Groups WHERE group_id = ?", (group_id,)) + if cursor.rowcount == 0: + print(f"ERROR: No group found with ID '{group_id}'.") + return False + else: + conn.commit() + print(f"SUCCESS: Group with ID '{group_id}' deleted along with all related data.") + return True except sqlite3.Error as e: - print(f"ERROR: Could not retrieve destinations. {e}") + print(f"ERROR: Could not delete group. {e}") + return False finally: conn.close() -def search_groups_by_destination(destination_name=None): - """Search for groups traveling to a specific destination.""" +def view_groups(): + """Displays all groups with their member information.""" conn = get_db_connection() try: cursor = conn.cursor() - - if destination_name: - cursor.execute(""" - SELECT DISTINCT g.group_id, g.group_name, g.group_type, g.member_count, g.max_members, - COUNT(ti.destination_id) as destination_count, - u.username as owner_name - FROM Groups g - JOIN TravelItinerary ti ON g.group_id = ti.group_id - JOIN Destinations d ON ti.destination_id = d.destination_id - JOIN Users u ON g.owner_id = u.userid - WHERE d.destination_name LIKE ? - GROUP BY g.group_id - ORDER BY g.created_at DESC - """, (f"%{destination_name}%",)) - else: - cursor.execute(""" - SELECT g.group_id, g.group_name, g.group_type, g.member_count, g.max_members, - COUNT(ti.destination_id) as destination_count, - u.username as owner_name - FROM Groups g - LEFT JOIN TravelItinerary ti ON g.group_id = ti.group_id - JOIN Users u ON g.owner_id = u.userid - GROUP BY g.group_id - ORDER BY g.created_at DESC - """) - + cursor.execute(""" + SELECT g.group_id, g.group_name, g.group_type, g.group_description, + u.username as owner_name, g.member_count, g.max_members, g.created_at + FROM Groups g + JOIN Users u ON g.owner_id = u.userid + """) groups = cursor.fetchall() if not groups: print("No groups found.") return - search_text = f" for '{destination_name}'" if destination_name else "" - print(f"\n--- Groups{search_text} ---") - + print("\n--- Groups List ---") for group in groups: - print(f"Group: {group['group_name']} (ID: {group['group_id']})") + print(f"ID: {group['group_id']}") + print(f"Name: {group['group_name']}") print(f"Type: {group['group_type']}") print(f"Owner: {group['owner_name']}") + print(f"Description: {group['group_description'] or 'No description'}") print(f"Members: {group['member_count']}/{group['max_members']}") - print(f"Destinations in Itinerary: {group['destination_count']}") - print("-" * 50) + print(f"Created: {group['created_at']}") + print("-" * 60) + except sqlite3.Error as e: + print(f"ERROR: Could not retrieve groups. {e}") + finally: + conn.close() + +def view_pending_requests(group_id, owner_id): + """View pending join requests for a group.""" + conn = get_db_connection() + try: + cursor = conn.cursor() + + # Verify ownership + cursor.execute("SELECT owner_id FROM Groups WHERE group_id = ?", (group_id,)) + group_data = cursor.fetchone() + if not group_data: + print(f"ERROR: Group with ID '{group_id}' does not exist.") + return + + if group_data['owner_id'] != owner_id: + print("ERROR: Only group owner can view pending requests.") + return + + # Get pending requests + cursor.execute(""" + SELECT gm.user_id, gm.username, gm.joined_at, u.email + FROM GroupMembers gm + JOIN Users u ON gm.user_id = u.userid + WHERE gm.group_id = ? AND gm.join_status = 'Pending' + ORDER BY gm.joined_at ASC + """, (group_id,)) + + requests = cursor.fetchall() + + if not requests: + print("No pending requests for this group.") + return + + print(f"\n--- Pending Join Requests ---") + for req in requests: + print(f"User: {req['username']} (ID: {req['user_id']})") + print(f"Email: {req['email']}") + print(f"Requested: {req['joined_at']}") + print("-" * 40) + + except sqlite3.Error as e: + print(f"ERROR: Could not retrieve pending requests. {e}") + finally: + conn.close() + +# MESSAGING FUNCTIONS + +def send_message(sender_id, group_id, message): + """Sends a message to a group chat.""" + conn = get_db_connection() + try: + cursor = conn.cursor() + + # Check if sender exists and get username + cursor.execute("SELECT username, current_group_id FROM Users WHERE userid = ?", (sender_id,)) + sender_data = cursor.fetchone() + if not sender_data: + print(f"ERROR: Sender with ID '{sender_id}' does not exist.") + return False + + sender_username = sender_data['username'] + + # Check if group exists + cursor.execute("SELECT group_id FROM Groups WHERE group_id = ?", (group_id,)) + if not cursor.fetchone(): + print(f"ERROR: Group with ID '{group_id}' does not exist.") + return False + + # Check if sender is a member of the group + cursor.execute("SELECT * FROM GroupMembers WHERE group_id = ? AND user_id = ? AND join_status = 'Approved'", + (group_id, sender_id)) + if not cursor.fetchone(): + print("ERROR: You must be an approved member of the group to send messages.") + return False + + # Insert the message + conn.execute( + "INSERT INTO GroupMessages (group_id, sender_id, sender_username, message) VALUES (?, ?, ?, ?)", + (group_id, sender_id, sender_username, message) + ) + + conn.commit() + print("SUCCESS: Message sent to group.") + return True + except sqlite3.Error as e: + print(f"ERROR: Could not send message. {e}") + return False + finally: + conn.close() + +def view_group_chat(group_id): + """Displays recent messages from a group chat.""" + conn = get_db_connection() + try: + cursor = conn.cursor() + + # Get group name + cursor.execute("SELECT group_name FROM Groups WHERE group_id = ?", (group_id,)) + group_data = cursor.fetchone() + if not group_data: + print(f"ERROR: Group with ID '{group_id}' does not exist.") + return + + print(f"\n--- Chat for Group: {group_data['group_name']} ---") + + # Get recent messages + cursor.execute(""" + SELECT sender_username, message, timestamp + FROM GroupMessages + WHERE group_id = ? + ORDER BY timestamp DESC + LIMIT 20 + """, (group_id,)) + + messages = cursor.fetchall() + messages = list(reversed(messages)) # Show in chronological order + + if not messages: + print("No messages in this group yet.") + return + + for msg in messages: + timestamp = msg['timestamp'][:19] # Remove microseconds + print(f"[{timestamp}] {msg['sender_username']}: {msg['message']}") except sqlite3.Error as e: - print(f"ERROR: Could not search groups. {e}") + print(f"ERROR: Could not view chat. {e}") finally: conn.close() +# MAIN FUNCTION + def main(): """Main function to run the interactive command-line interface.""" setup_database() @@ -520,15 +1105,28 @@ def main(): while True: print("\n--- Travel Together DB Manager ---") print("1. Add User") - print("2. Add Destination") - print("3. Create Group with Travel Chain") - print("4. View Group Itinerary") - print("5. Add Destination to Itinerary") - print("6. View All Destinations") - print("7. Search Groups by Destination") - print("8. Exit") + print("2. Update User Profile") + print("3. Change Password") + print("4. Delete User") + print("5. View Users") + print("6. Add Destination") + print("7. View Destinations") + print("8. Create Group with Itinerary") + print("9. Join Group") + print("10. Leave Group") + print("11. Delete Group") + print("12. View Groups") + print("13. View Pending Requests") + print("14. Update Member Status") + print("15. Send Message") + print("16. View Group Chat") + print("17. View Notifications") + print("18. View Unread Notifications") + print("19. Mark Notification as Read") + print("20. Mark All Notifications as Read") + print("21. Exit") - choice = input("Enter your choice (1-8): ").strip() + choice = input("Enter your choice (1-21): ").strip() if choice == '1': print("\n--- Add New User ---") @@ -549,9 +1147,37 @@ def main(): continue email = input("Enter email: ").strip() - add_user(username, password, email) + phone_no = input("Enter phone number (optional): ").strip() or None + gender = input("Enter gender (Male/Female/Other, optional): ").strip() or None + + add_user(username, password, email, phone_no=phone_no, gender=gender) elif choice == '2': + user_id = input("Enter your user ID: ").strip() + print("Enter new values (leave blank to keep current):") + phone_no = input("Phone number: ").strip() or None + gender = input("Gender (Male/Female/Other): ").strip() or None + marital_status = input("Marital status: ").strip() or None + bio = input("Bio: ").strip() or None + + update_user_profile(user_id, phone_no=phone_no, gender=gender, + marital_status=marital_status, bio=bio) + + elif choice == '3': + user_id = input("Enter your user ID: ").strip() + new_password = input("Enter new password: ").strip() + update_password(user_id, new_password) + + elif choice == '4': + user_id = input("Enter user ID to delete: ").strip() + confirm = input("Are you sure? This will delete all user data (y/n): ").strip().lower() + if confirm == 'y': + delete_user(user_id) + + elif choice == '5': + view_users() + + elif choice == '6': print("\n--- Add Destination ---") destination_name = input("Enter destination name: ").strip() state_province = input("Enter state/province: ").strip() @@ -560,13 +1186,15 @@ def main(): add_destination(destination_name, state_province, country, description=description) - elif choice == '3': + elif choice == '7': + view_destinations() + + elif choice == '8': print("\n--- Create Group with Travel Chain ---") owner_id = input("Enter your userid: ").strip() group_name = input("Enter group name: ").strip() group_type = input("Enter group type (Public/Private): ").strip().capitalize() - # Get destinations for the travel chain print("\nFirst, let's see available destinations:") view_destinations() @@ -584,44 +1212,82 @@ def main(): continue group_description = input("Enter group description (optional): ").strip() or None - total_days = input("Enter total trip duration in days (optional): ").strip() - total_days = int(total_days) if total_days.isdigit() else None create_group_with_itinerary( owner_id, group_name, group_type, destination_ids, - group_description=group_description, total_duration_days=total_days + group_description=group_description ) - elif choice == '4': - group_id = input("Enter group ID to view itinerary: ").strip() - view_group_itinerary(group_id) + elif choice == '9': + user_id = input("Enter your userid: ").strip() + group_id = input("Enter group ID to join: ").strip() + join_group(user_id, group_id) - elif choice == '5': - print("\n--- Add Destination to Existing Itinerary ---") - owner_id = input("Enter your userid: ").strip() + elif choice == '10': + user_id = input("Enter your userid: ").strip() + leave_group(user_id) + + elif choice == '11': + group_id = input("Enter group ID to delete: ").strip() + confirm = input("Are you sure? This will delete all group data (y/n): ").strip().lower() + if confirm == 'y': + delete_group(group_id) + + elif choice == '12': + view_groups() + + elif choice == '13': + owner_id = input("Enter your user ID: ").strip() group_id = input("Enter your group ID: ").strip() - destination_id = input("Enter destination ID to add: ").strip() - duration = input("Enter duration in days (default 2): ").strip() - duration = int(duration) if duration.isdigit() else 2 - notes = input("Enter notes (optional): ").strip() or None + view_pending_requests(group_id, owner_id) - add_destination_to_itinerary(group_id, destination_id, owner_id, - duration_days=duration, notes=notes) + elif choice == '14': + owner_id = input("Enter your user ID: ").strip() + group_id = input("Enter your group ID: ").strip() + member_user_id = input("Enter member's user ID: ").strip() + new_status = input("Enter new status (Pending/Approved/Rejected/Blocked): ").strip() + update_member_status(group_id, owner_id, member_user_id, new_status) - elif choice == '6': - view_destinations() + elif choice == '15': + sender_id = input("Enter your userid: ").strip() + group_id = input("Enter the group_id: ").strip() + message = input("Enter your message: ").strip() + send_message(sender_id, group_id, message) - elif choice == '7': - destination_name = input("Enter destination name to search (or press Enter for all groups): ").strip() - search_groups_by_destination(destination_name if destination_name else None) + elif choice == '16': + group_id = input("Enter the group_id: ").strip() + view_group_chat(group_id) - elif choice == '8': + elif choice == '17': + user_id = input("Enter your user ID: ").strip() + view_notifications(user_id) + + elif choice == '18': + user_id = input("Enter your user ID: ").strip() + unread_count = get_unread_count(user_id) + print(f"You have {unread_count} unread notifications.") + if unread_count > 0: + view_notifications(user_id, unread_only=True) + + elif choice == '19': + user_id = input("Enter your user ID: ").strip() + notification_id = input("Enter notification ID to mark as read: ").strip() + try: + notification_id = int(notification_id) + mark_notification_read(notification_id, user_id) + except ValueError: + print("ERROR: Invalid notification ID. Must be a number.") + + elif choice == '20': + user_id = input("Enter your user ID: ").strip() + mark_all_notifications_read(user_id) + + elif choice == '21': print("Exiting... Have a great day!") break else: - print("Invalid choice. Please enter a number between 1-8.") + print("Invalid choice. Please try again.") if __name__ == "__main__": main() -