diff --git a/frontend/__pycache__/main.cpython-312.pyc b/frontend/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..123ae2a Binary files /dev/null and b/frontend/__pycache__/main.cpython-312.pyc differ diff --git a/frontend/main.py b/frontend/main.py new file mode 100644 index 0000000..ee16ecf --- /dev/null +++ b/frontend/main.py @@ -0,0 +1,35 @@ +import flet as ft +from urllib.parse import urlparse, parse_qs +from pages.main_page import main_page # Import main page function +from pages.login_page import login_page # Import login page function +from pages.register_teacher_page import register_teacher_page # Import register page function +from pages.register_student_page import register_student_page # Import search page function +from pages.create_page import create_page # Import create page function7 +from pages.join_page import join_page + +# Define your routes +ROUTES = { + "/": main_page, + "/login": login_page, + "/register_teacher": register_teacher_page, + "/register_student": register_student_page, + "/create": create_page, + "/join": join_page +} + +def main(page: ft.Page): + def route_change(e: ft.RouteChangeEvent): + page.clean() + parsed_url = urlparse(e.route) # Parse the route + query_params = parse_qs(parsed_url.query) # Extract query parameters + + + + render_function = ROUTES.get(parsed_url.path, main_page) + render_function(page) + + page.on_route_change = route_change # Set up route change handling + page.go(page.route) # Navigate to the current route + +if __name__ == "__main__": + ft.app(target=main, view=ft.AppView.WEB_BROWSER, port=9000) \ No newline at end of file diff --git a/frontend/pages/__pycache__/all_rooms_page.cpython-312.pyc b/frontend/pages/__pycache__/all_rooms_page.cpython-312.pyc new file mode 100644 index 0000000..8b14122 Binary files /dev/null and b/frontend/pages/__pycache__/all_rooms_page.cpython-312.pyc differ diff --git a/frontend/pages/__pycache__/create_page.cpython-312.pyc b/frontend/pages/__pycache__/create_page.cpython-312.pyc new file mode 100644 index 0000000..04b7da4 Binary files /dev/null and b/frontend/pages/__pycache__/create_page.cpython-312.pyc differ diff --git a/frontend/pages/__pycache__/join_page.cpython-312.pyc b/frontend/pages/__pycache__/join_page.cpython-312.pyc new file mode 100644 index 0000000..d37fd85 Binary files /dev/null and b/frontend/pages/__pycache__/join_page.cpython-312.pyc differ diff --git a/frontend/pages/__pycache__/login_page.cpython-312.pyc b/frontend/pages/__pycache__/login_page.cpython-312.pyc new file mode 100644 index 0000000..d9209e3 Binary files /dev/null and b/frontend/pages/__pycache__/login_page.cpython-312.pyc differ diff --git a/frontend/pages/__pycache__/main_page.cpython-312.pyc b/frontend/pages/__pycache__/main_page.cpython-312.pyc new file mode 100644 index 0000000..6d9f144 Binary files /dev/null and b/frontend/pages/__pycache__/main_page.cpython-312.pyc differ diff --git a/frontend/pages/__pycache__/register_student_page.cpython-312.pyc b/frontend/pages/__pycache__/register_student_page.cpython-312.pyc new file mode 100644 index 0000000..b033d7a Binary files /dev/null and b/frontend/pages/__pycache__/register_student_page.cpython-312.pyc differ diff --git a/frontend/pages/__pycache__/register_teacher_page.cpython-312.pyc b/frontend/pages/__pycache__/register_teacher_page.cpython-312.pyc new file mode 100644 index 0000000..50e521a Binary files /dev/null and b/frontend/pages/__pycache__/register_teacher_page.cpython-312.pyc differ diff --git a/frontend/pages/__pycache__/student_main_page.cpython-312.pyc b/frontend/pages/__pycache__/student_main_page.cpython-312.pyc new file mode 100644 index 0000000..9e45241 Binary files /dev/null and b/frontend/pages/__pycache__/student_main_page.cpython-312.pyc differ diff --git a/frontend/pages/all_rooms_page.py b/frontend/pages/all_rooms_page.py new file mode 100644 index 0000000..00474e0 --- /dev/null +++ b/frontend/pages/all_rooms_page.py @@ -0,0 +1,199 @@ +import flet as ft +import requests +import threading +import asyncio +import websockets +from websockets import connect # Ensure this is imported +import json +from datetime import datetime + +def student_main_page(page: ft.Page): + page.clean() + page.theme_mode = ft.ThemeMode.LIGHT # Set the theme to light + page.title = "Room Information" + page.vertical_alignment = ft.MainAxisAlignment.START # Align all content to the top + page.scroll = "adaptive" + page.padding = 20 + + # Container for lessons + lessons_container = ft.Column(alignment=ft.MainAxisAlignment.CENTER, spacing=20) + + # Buttons + test_button = ft.TextButton( + text="Auch Räume an anderen Tagen anzeigen", + on_click=lambda e: page.go("/all_rooms"), + style=ft.ButtonStyle(padding=20), + visible=False, # Initially hidden + ) + + join_more_rooms_button = ft.TextButton( + text="Tritt noch mehr Räume bei", + on_click=lambda e: page.go("/join"), + style=ft.ButtonStyle(padding=25), + ) + def leave_current_room(e=None, unique_id=None): + if not unique_id: + page.snack_bar = ft.SnackBar(ft.Text("No room selected to leave.")) + page.snack_bar.open = True + page.update() + return + + url = f"http://awesom-o.org:8000/student/leave_room/?unique_id={unique_id}" + headers = {"accept": "application/json", "Content-Type": "application/json"} + data = {"session_id": page.session.get("access_token")} + try: + response = requests.post(url, json=data, headers=headers) + response.raise_for_status() + if e: + page.snack_bar = ft.SnackBar(ft.Text(f"Left Room {unique_id}")) + page.snack_bar.open = True + page.update() + except requests.RequestException as error: + # Handle all bad responses + error_detail = "Something went wrong." + if hasattr(error, "response") and error.response: + try: + error_detail = error.response.json().get("detail", "Something went wrong.") + except json.JSONDecodeError: + error_detail = error.response.text or "Something went wrong." + page.snack_bar = ft.SnackBar(ft.Text(f"Error: {error_detail}")) + page.snack_bar.open = True + page.update() + + async def get_rooms(): + uri = "ws://localhost:8000/ws/student/my_room" + headers = {"session-id": page.session.get("access_token")} + + try: + async with websockets.connect(uri, extra_headers=headers) as websocket: + while True: + message = await websocket.recv() + data = json.loads(message) + + # Clear previous lessons + lessons_container.controls.clear() + + if "error" in data: + # Show "No Rooms for Today" message + lessons_container.controls.append( + ft.Text( + "Für heute hast du noch keine Räume ausgewählt!", + size=50, + weight="bold", + text_align="center", + ) + ) + else: + rooms = data.get("rooms", []) + # Filter and sort lessons for today + today = datetime.now().date() + today_lessons = [room for room in rooms if room["lesson_date"] == str(today)] + today_lessons.sort(key=lambda x: x["lesson_time"]) + + # Check if there are rooms on other dates + other_date_rooms = [room for room in rooms if room["lesson_date"] != str(today)] + test_button.visible = bool(other_date_rooms) # Show button if there are rooms on other dates + + if today_lessons: + for lesson in today_lessons: + # Create lesson card + room_card = ft.Card( + content=ft.Container( + content=ft.Row( + [ + # Room details + ft.Column( + [ + # Top line: room_number, info, location + ft.Row( + [ + ft.Text( + f"{lesson['room_number']} {lesson['info']}", + size=20, + weight="bold", + ), + ft.Text( + f"| {lesson['location']}", + size=20, + weight="bold", + ), + ], + spacing=5, + ), + # Second line: first_name and last_name + ft.Text( + f"Teacher: {lesson['first_name']} {lesson['last_name']}", + size=16, + ), + # Third line: lesson_time + ft.Text( + f"Time: {lesson['lesson_time']}", + size=16, + ), + ], + expand=True, + alignment=ft.MainAxisAlignment.CENTER, + ), + # IconButton on the right + ft.IconButton( + icon=ft.icons.EXIT_TO_APP, + on_click=lambda e, lesson_id=lesson['unique_id']: leave_current_room(e, lesson_id), + tooltip="View Room Details", + ), + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + ), + padding=20, # Add padding inside the card + ), + margin=10, + elevation=5, # Add shadow for better visual appeal + ) + lessons_container.controls.append(room_card) + else: + # Show "No Rooms for Today" message + lessons_container.controls.append( + ft.Text( + "No Rooms for Today", + size=50, + weight="bold", + text_align="center", + ) + ) + + # Update page + page.update() + except Exception as e: + lessons_container.controls.clear() + lessons_container.controls.append(ft.Text(f"Error: {e}", size=50, weight="bold", text_align="center")) + page.update() + + def start_get_rooms(): + # Ensure a new event loop in the thread + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(get_rooms()) + + # Start the WebSocket client in a thread + import threading + threading.Thread(target=start_get_rooms, daemon=True).start() + + # Add components to page + page.add( + ft.Column( + [ + # Lessons container (centered) + ft.Column( + [lessons_container], + alignment=ft.MainAxisAlignment.CENTER, + expand=True, + ), + # Buttons in one row at the bottom + ft.Row( + [test_button, join_more_rooms_button], + alignment=ft.MainAxisAlignment.CENTER, + spacing=20, # Add spacing between buttons + ), + ], + expand=True, + ) + ) \ No newline at end of file diff --git a/frontend/pages/create_page.py b/frontend/pages/create_page.py new file mode 100644 index 0000000..f2a5078 --- /dev/null +++ b/frontend/pages/create_page.py @@ -0,0 +1,288 @@ +import flet as ft +import requests +import datetime + +def create_page(page: ft.Page): + + page.clean() + page.theme_mode = ft.ThemeMode.LIGHT # Set the theme to light + def is_student(): + url = 'http://127.0.0.1:8000/check_role' + headers = { + 'accept': 'application/json', + 'Content-Type': 'application/json' + } + session_id = page.session.get("access_token") + if not session_id: + print("Session ID is missing.") + return None + + data = {'session_id': session_id} + + try: + response = requests.post(url, json=data, headers=headers) + response.raise_for_status() + role = response.json() + + if 'role' in role and 'role' in role['role']: + if role['role']['role'] == "student": + role = "s" + page.go("/login") + return role + elif role['role']['role'] == "teacher": + role = "t" + return role + print("Unexpected response structure:", role) + return None + except requests.exceptions.RequestException as error: + print("Error during role check:", error) + return None + + page.title = "Create Room Page" + + if is_student == "s": + page.gp("/") + session = page.session.get("access_token") + if not session: + page.go("/login") + + # Initialize selected room state (use a simple variable instead of ft.State) + selected_room = "" + selected_date = "" + selected_time = "" + + SEARCH_RESULT = "" + + date_text = ft.Text( + "", # Initially empty + size=18, # Font size + weight=ft.FontWeight.BOLD, # Make the text bold + color="black", # Text colo + visible=False # Initially hidden +) + + + # Function to fetch room data + def fetch_rooms(): + try: + response = requests.get("http://awesom-o.org:8000/teacher/all_room_information") + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + print("Error fetching room data:", e) + return [] + + # Fetch rooms initially (don't display them immediately) + room_data = fetch_rooms() + + # Function to filter room data + def search_rooms(query): + query = query.lower() + return [ + room for room in room_data + if query in room["room_number"].lower() + or query in room["info"].lower() + or query in room["location"].lower() + ] + + # Search results container + search_results = ft.Column( + visible=False, # Initially hidden + scroll=ft.ScrollMode.AUTO, + expand=True + ) + + # Update search results + def update_results(query): + if query.strip() == "": # When the search field is empty, show all rooms + search_results.controls.clear() + for room in room_data: + search_results.controls.append( + ft.ListTile( + title=ft.Text(f"Room {room['room_number']}: {room['info']}"), + subtitle=ft.Text(room['location']), + on_click=lambda e, r=room: select_room(r), + ) + ) + search_results.visible = True + else: + search_results.controls.clear() + filtered_rooms = search_rooms(query) + for room in filtered_rooms: + search_results.controls.append( + ft.ListTile( + title=ft.Text(f"Room {room['room_number']}: {room['info']}"), + subtitle=ft.Text(room['location']), + on_click=lambda e, r=room: select_room(r), + ) + ) + search_results.visible = True + + close_button.visible = search_results.visible # Show close button when results are visible + page.update() + + # Select a room + def select_room(room): + nonlocal selected_room + nonlocal SEARCH_RESULT + selected_room = f"Room {room['room_number']}: {room['info']} - {room['location']}" + search_bar.value = selected_room + SEARCH_RESULT = room['number'] + + search_results.visible = False + close_button.visible = False + page.update() + + # Close button to hide search results + def close_search_results(e): + search_results.visible = False + close_button.visible = False + page.update() + + def handle_change_date_picker(e): + # Once a date is selected, update the Text and make it visible + nonlocal selected_date + selected_date = e.control.value.strftime('%Y-%m-%d') + if selected_time != "": + date_text.value = f"Your Room will be started on: {selected_date} at {selected_time}" + date_text.visible = True # Make the Text visible after date selection + page.update() + else: + date_text.value = f"Your Room will be started on: {selected_date}" + date_text.visible = True # Make the Text visible after date selection + page.update() # Update the page to reflect the changes + + def handle_change_dropdown(e): + nonlocal selected_time + selected_time1 = e.control.value + selected_time = selected_time1.replace("Start time: ", "") + if selected_date != "": + date_text.value = f"Your Room will be started on: {selected_date} at {selected_time}" + date_text.visible = True # Make the Text visible after date selection + page.update() + else: + date_text.value = f"Your Room will be started at {selected_time}" + date_text.visible = True # Make the Text visible after date selection + page.update() # Update the page to reflect the changes + + def go_to_main(e): + page.go("/") # Replace with the appropriate route for your register page + + def create(e): + page.update() + search_results = SEARCH_RESULT.strip() + max_students = max_students_field.value.strip() + selected_date + selected_time + + # Validate input + if not search_results or not max_students or not selected_date or not selected_time: + info_label.value = "Please enter Room Number, Max Students, Lesson Date and Lesson Time." + info_label.color = "red" + page.update() + return + + try: + max_students = int(max_students) + except ValueError: + info_label.value = "Max Students must be a number." + info_label.color = "red" + page.update() + return + + if max_students < 1: + info_label.value = "Please don't enter a negative or zero value for Max Students." + info_label.color = "red" + page.update() + return + + try: + url = 'http://127.0.0.1:8000/teacher/create_room' + headers = { + 'accept': 'application/json', + 'Content-Type': 'application/json' + } + data = { + 'room_number': search_results, + 'max_students': max_students, + 'session_id': page.session.get("access_token"), + 'lesson_time': selected_time, + 'lesson_date': selected_date + } + + response = requests.post(url, headers=headers, json=data) + + if response.status_code == 200: + success_message = "The Room was created Successfully!" + info_label.value = success_message + info_label.color = "green" + else: + error_message = "Something went wrong..." + info_label.value = error_message + info_label.color = "red" + except requests.exceptions.RequestException as e: + info_label.value = f"An error occurred: {str(e)}" + info_label.color = "red" + + page.update() + + # Search bar + search_bar = ft.TextField( + label="Search for a room", + on_focus=lambda e: update_results(""), # Show all rooms when search bar is focused + on_change=lambda e: update_results(e.control.value), + expand=True # Make the search bar take up available space + ) + + # Close button + close_button = ft.IconButton( + icon=ft.icons.CLOSE, + on_click=close_search_results, + visible=False # Initially hidden + ) + + max_students_field = ft.TextField(label="Max Students") + info_label = ft.Text("", theme_style=ft.TextThemeStyle.TITLE_SMALL) + + + # Add components to the page + page.add( + ft.Row([search_bar, close_button]), # Add search bar and close button in a row + search_results, + max_students_field, + ft.Row([ + ft.OutlinedButton( + "Pick the date when your lesson begins", + icon=ft.icons.CALENDAR_MONTH, + height=40, + on_click=lambda e: page.open( + ft.DatePicker( + first_date=datetime.datetime(year=2024, month=11, day=20), + on_change=handle_change_date_picker, + ) + ), + ), + date_text, # Add the styled Text below the button + ], spacing=20), + ft.Dropdown( + hint_text="Choose the Start of your Lesson", + options=[ + ft.dropdown.Option("Start time: 8:00"), + ft.dropdown.Option("Start time: 9:20"), + ft.dropdown.Option("Start time: 10:40"), + ft.dropdown.Option("Start time: 11:50"), + ft.dropdown.Option("Start time: 12:50"), + ft.dropdown.Option("Start time: 13:55"), + ft.dropdown.Option("Start time: 15:00"), + ],on_change=handle_change_dropdown, + label_style=ft.TextStyle(size=16), # Correct way to set font size for Dropdown + ), + ft.ElevatedButton("Submit", on_click=create), + info_label, + ft.Row([ + ft.TextButton("Enough? Click here to go to the Main Page", on_click=go_to_main), + ]), + ) +# Example usage +if __name__ == "__main__": + ft.app(target=create_page, view=ft.AppView.WEB_BROWSER) diff --git a/frontend/pages/join_page.py b/frontend/pages/join_page.py new file mode 100644 index 0000000..d8adcd3 --- /dev/null +++ b/frontend/pages/join_page.py @@ -0,0 +1,425 @@ +import flet as ft +import asyncio +import websockets +import json +import requests +import threading +from datetime import datetime + +def get_initials(user_name: str): + if user_name: + return user_name[0].capitalize() + else: + return "E" + + +def get_avatar_color(user_name: str): + colors_lookup = [ + ft.colors.AMBER, + ft.colors.BLUE, + ft.colors.BROWN, + ft.colors.CYAN, + ft.colors.GREEN, + ft.colors.INDIGO, + ft.colors.LIME, + ft.colors.ORANGE, + ft.colors.PINK, + ft.colors.PURPLE, + ft.colors.RED, + ft.colors.TEAL, + ] + return colors_lookup[hash(user_name) % len(colors_lookup)] + +def join_page(page: ft.Page): + page.clean() + page.theme_mode = ft.ThemeMode.LIGHT # Set the theme to light + page.title = "Join Rooms" + page.vertical_alignment = ft.MainAxisAlignment.START # Align all content to the top + page.scroll = "adaptive" + page.padding = 20 + + # Check if the user is logged in + session = page.session.get("access_token") + if not session: + page.go("/login") # Redirect to login if no session + + def test(e): + print(e) + page.go("/test") + print("test") + + # Search field + search_field = ft.TextField(label="Lehrkraft / Raumnummer ", expand=True) + + # Dropdown for time filter + time_filter = ft.Dropdown( + label="Stunde wählen", + options=[ + ft.dropdown.Option("8:00", "8:00"), + ft.dropdown.Option("9:20", "9:20"), + ft.dropdown.Option("10:40", "10:40"), + ft.dropdown.Option("11:50", "11:50"), + ft.dropdown.Option("12:50", "12:50"), + ft.dropdown.Option("13:55", "13:55"), + ft.dropdown.Option("15:00", "15:00"), + ], + width=170, # Shorter width for time filter + ) + + # Cross icon button to clear time filter + clear_time_filter = ft.IconButton( + icon=ft.icons.CLOSE, + on_click=lambda e: clear_filter("time"), + tooltip="Clear Time Filter", + ) + + # Container for the room list + room_container = ft.Column(scroll=ft.ScrollMode.AUTO, expand=True) + + def ausloggen(e = None): + page.go("/login") + print(page.session.set("access_token", None)) + + # Function to clear filters + def clear_filter(filter_type): + if filter_type == "time": + time_filter.value = None + if filter_type == "lesson": + lesson_filter.value = None + + update_room_list(json.loads(page.session.get("rooms_data", "[]"))) + def update_room_list(rooms): + # Ensure rooms is a list + if not isinstance(rooms, list): + rooms = [] + + # Apply search filter + search_query = search_field.value.lower() + filtered_rooms = [ + room + for room in rooms + if (search_query in str(room.get("room_name", "")).lower() # Search by room_name + or search_query in room.get("info", "").lower() # Search by room_info + or search_query in room.get("location", "").lower() # Search by location + or search_query in room.get("subjects", "").lower() # Search by subjects + or search_query in room.get("first_name", "").lower() # Search by teacher_first_name + or search_query in room.get("last_name", "").lower()) # Search by teacher_last_name + ] + + if time_filter.value: # Only apply the filter if a time is selected + filtered_rooms = [ + room + for room in filtered_rooms + if room.get("lesson_time", "") == time_filter.value + ] + + if lesson_filter.value: # Only apply the filter if a time is selected + filtered_rooms = [ + room + for room in filtered_rooms + if lesson_filter.value.lower() in room.get("subjects", "").lower().split(", ") + ] + + if show_full_rooms.value == True: + filtered_rooms = [ + room + for room in filtered_rooms + if room.get("is_open", "") == True + ] + + # Apply date filter (only if a date is selected) + room_container.controls.clear() + + def page_dialog_click(e, unique_id): + dialog_join = ft.AlertDialog( + modal=True, + title=ft.Text("Bitte bestätige"), + content=ft.Text("Möchtest du diesem Raum wirklich beitreten?"), + actions=[ + ft.TextButton("Ja", on_click=lambda e: (page.close(dialog_join), room_clicked(e, unique_id))), + ft.TextButton("Nein", on_click=lambda e: page.close(dialog_join)), + ], + actions_alignment=ft.MainAxisAlignment.END, + ) + page.dialog = dialog_join + dialog_join.open = True + page.update() + + def room_clicked(e, unique_id): + try: + url = f"http://awesom-o.org:8000/student/join_room/?unique_id={unique_id}" + headers = {"accept": "application/json", "Content-Type": "application/json"} + data = {"session_id": page.session.get("access_token")} + + response = requests.post(url, json=data, headers=headers) + + if response.status_code == 200: + try: + page.snack_bar = ft.SnackBar(ft.Text(f"Raum erfolgreich beigetreten: {unique_id}")) + page.snack_bar.open = True + page.update() + page.go("/") + except: + page.snack_bar = ft.SnackBar(ft.Text(f"Ein fehler ist aufgetreten, bitte melde dich bei der Administration")) + page.snack_bar.open = True + page.update() + else: + dialog_join = ft.AlertDialog( + modal=True, + title=ft.Text("Fehler"), + content=ft.Text(response.json().get("detail")), + actions=[ + ft.TextButton("Schließen", on_click=lambda e: page.close(dialog_join)), + ], + actions_alignment=ft.MainAxisAlignment.END, + ) + page.dialog = dialog_join + dialog_join.open = True + page.update() + + except requests.exceptions.RequestException as e: + dialog_join = ft.AlertDialog( + modal=True, + title=ft.Text("Fehler"), + content=ft.Text("Ein Fehler ist aufgetreten"), + actions=[ + ft.TextButton("Schließen", on_click=lambda e: page.close(dialog_join)), + ], + actions_alignment=ft.MainAxisAlignment.END, + ) + page.dialog = dialog_join + dialog_join.open = True + page.update() + + + for room in filtered_rooms: + room_name = room.get("room_name", "Unknown Room") + room_info = room.get("info", "") + location = room.get("location", "") + first_name = room.get("first_name", "") + last_name = room.get("last_name", "") + subjects = room.get("subjects", "") + lesson_time = room.get("lesson_time", "") + lesson_date = room.get("lesson_date", "") + max_students = room.get("max_students", 0) + current_students = room.get("current_students", 0) + unique_id = room.get("unique_id", "") + is_open = room.get("is_open", True) + joined = room.get("joined", False) + + if current_students == max_students: + BACKROUND_COLOR = "#f10d0c" + TEXT_COLOR = ft.colors.WHITE + user_interface_button_text = ft.Text("Raum bereits voll") + else: + BACKROUND_COLOR = "#729fcf" + TEXT_COLOR = ft.colors.BLACK + if joined == True: + user_interface_button_text = ft.Text("Raum bereits gebucht") + else: + user_interface_button_text = ft.TextButton( + "Jetzt Platz reservieren", + on_click=lambda e, unique_id=unique_id: page_dialog_click(e, unique_id), + style=ft.ButtonStyle( + bgcolor=ft.colors.WHITE, + color=ft.colors.BLACK, + shape=ft.RoundedRectangleBorder(radius=5), + padding=20, + )) + room_card = ft.Card( + content=ft.Container( + content=ft.Row( + [ + # Room details + ft.Column( + [ + ft.Text(f"Heute, {lesson_time} Uhr", size=16, color=TEXT_COLOR), + ft.Text(f"Lehrkraft: {first_name} {last_name} - {subjects}", size=16, color=TEXT_COLOR), + ft.Text(f"Raum {room_name} {room_info} | {location}", size=16, color=TEXT_COLOR), + ft.Text(f"Belegte Plätze: {current_students}/{max_students}", size=16, color=TEXT_COLOR), + ], + expand=True, + alignment=ft.MainAxisAlignment.CENTER, + ), + # IconButton on the right + user_interface_button_text, + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + ), + padding=20, # Add padding inside the card + ), + color=BACKROUND_COLOR, + margin=10, + ) + room_container.controls.append(room_card) + + + if not filtered_rooms: + room_container.controls.append(ft.Text("Keine offenen Räume verfügbar für Heute.", size=18)) + + page.update() + + # WebSocket listener for real-time updates + async def listen_for_updates(): + uri = "ws://localhost:8000/ws/open_rooms" + headers = {"session-id": page.session.get("access_token")} + + async with websockets.connect(uri, extra_headers=headers) as websocket: + print("Connected to WebSocket server") + + try: + while True: + # Receive data from the server + data = await websocket.recv() + rooms = json.loads(data) + # Ensure rooms is a list + if isinstance(rooms, dict): + rooms = rooms.get("rooms", []) + elif not isinstance(rooms, list): + rooms = [] + + # Store rooms data in session for filtering + page.session.set("rooms_data", json.dumps(rooms)) + + # Extract unique dates and sort them from nearest to longest + unique_dates = list(set(room.get("lesson_date", "") for room in rooms)) + unique_dates.sort(key=lambda x: datetime.strptime(x, "%Y-%m-%d")) # Sort dates + + + # Update the room list + update_room_list(rooms) + + except websockets.ConnectionClosed: + print("WebSocket connection closed") + + # Start the WebSocket listener in a separate thread + def start_websocket_listener(): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(listen_for_updates()) + + threading.Thread(target=start_websocket_listener, daemon=True).start() + + top_bar = ft.Row( + [ + search_field, + ft.Row([time_filter, clear_time_filter], spacing=5), + ], + alignment=ft.MainAxisAlignment.START, + height=50, + ) + + lesson_filter = ft.Dropdown( + label="Fach wählen", + options=[ + ft.dropdown.Option("De", "De"), + ft.dropdown.Option("Ma", "Ma"), + ft.dropdown.Option("NW", "NW"), + ft.dropdown.Option("En", "En"), + ft.dropdown.Option("Gs", "Gs"), + ft.dropdown.Option("Re", "Re"), + ft.dropdown.Option("WN", "WN"), + ft.dropdown.Option("Fr", "Fr"), + ft.dropdown.Option("Sp", "Sp"), + ft.dropdown.Option("Sn", "Sn"), + ], + width=170, + ) + + clear_lesson_filter = ft.IconButton( + icon=ft.icons.CLOSE, + on_click=lambda e: clear_filter("lesson"), + tooltip="Clear Lesson Filter", + ) + + + show_full_rooms = ft.Checkbox(label="Volle Räume ausblenden ", value=False, label_style=ft.TextStyle(size=20)) + + middle_bar = ft.Row( + [ + show_full_rooms, + ft.Row( + [ + ft.Column( + [ + ft.Text( + "Nur Räume anzeigen, in denen\nfolgendes Fach angeboten wird:", + size=20, + ), + ], + ), + lesson_filter, + clear_lesson_filter, + ], + spacing=5, + alignment=ft.MainAxisAlignment.END, + ), + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + height=50, + ) + + go_home_button = ft.Container( + content=ft.Text("Home", size=24, color=ft.colors.BLUE_900), + on_click=lambda e: page.go("/"), + alignment=ft.alignment.center, + width=200, + height=60, + bgcolor='transparent', + ink=True, + ) + + navigation_bar = ft.Row( + [go_home_button, ft.Text("Räume suchen", size=23, weight=ft.FontWeight.BOLD, width=170, no_wrap=True)], + ) + + page.add( + ft.Column( + [ + ft.Row( + [ + ft.Text( + "Daltonraum-Buchungssystem der IGS Garbsen", size=30), + ft.Column([ + ft.PopupMenuButton( + items=[ + ft.PopupMenuItem(content=ft.Text(f"Eingeloggt als: {page.session.get("username")}", weight=ft.FontWeight.BOLD)), + ft.PopupMenuItem(text="Profil anzeigen", on_click=test), + ft.PopupMenuItem(text="Ausloggen", on_click=ausloggen), + ], + content=ft.CircleAvatar( + content=ft.Text(get_initials( + page.session.get("username"))), + color=ft.colors.WHITE, + bgcolor=get_avatar_color( + page.session.get("username")), + ), + menu_position=ft.PopupMenuPosition.UNDER, + tooltip="", + + ) + + ], + # Align column content to the end (optional) + alignment=ft.MainAxisAlignment.END, + ), + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, # Space out the two elements + vertical_alignment=ft.CrossAxisAlignment.CENTER, # Align items vertically + ), + + navigation_bar, + top_bar, + middle_bar, + ft.Column( + [ + room_container, + ], + alignment=ft.MainAxisAlignment.CENTER, + expand=True, + ), + ], + expand=True, + alignment=ft.MainAxisAlignment.CENTER, + ) + ) diff --git a/frontend/pages/login_page.py b/frontend/pages/login_page.py new file mode 100644 index 0000000..bf9308d --- /dev/null +++ b/frontend/pages/login_page.py @@ -0,0 +1,79 @@ +import flet as ft +import requests + +def login_page(page: ft.Page): + page.clean() + page.title = "Login Page" + page.theme_mode = ft.ThemeMode.LIGHT # Set the theme to light + + # UI components + username_field = ft.TextField(label="Accountname") + password_field = ft.TextField(label="Passwort", password=True, can_reveal_password=True) + + # Create an info text label + info_label = ft.Text("", theme_style=ft.TextThemeStyle.TITLE_SMALL) + + # Function to handle login + + def login(e): + username = username_field.value.strip() # Strip whitespace + password = password_field.value.strip() + + # Validate input + if not username or not password: + info_label.value = "Bitte Benutzername und Passwort eingeben" + info_label.color = "red" + page.update() # Update the page to reflect the changes + return + + # Send request over HTTP + try: + url = 'http://awesom-o.org:8000/login' + headers = { + 'accept': 'application/json', + 'Content-Type': 'application/json' + } + data = { + 'username': username, + 'password': password + } + + response = requests.post(url, headers=headers, json=data) + + if response.status_code == 200: + token_data = response.json() + page.session.set("username", token_data['username']) + page.session.set("access_token", token_data['session_id']) + page.go("/") + else: + error_message = response.json().get("detail") + info_label.value = error_message + info_label.color = "red" + except requests.exceptions.RequestException as e: + info_label.value = f"An error occurred: {str(e)}" + info_label.color = "red" + + # Update the page to reflect the changes + page.update() + + # Function to navigate to the register page + def go_to_register(e): + page.go("/register_student") # Replace with the appropriate route for your register page + + # Add components to the page + page.add( + ft.Text("Daltonraum-Buchungssystem der IGS Garbsen", size=30), + ft.Text("Login", size=23, weight=ft.FontWeight.BOLD), + username_field, + password_field, + ft.ElevatedButton("Anmelden", on_click=login), + info_label, # Add the info label to the page + ft.Row([ + ft.TextButton("Noch nicht registriert? Registriere dich hier!", on_click=go_to_register), + ]) + + ) + +# You can run the login page separately or as part of your main app. +if __name__ == "__main__": + ft.app(target=login_page, view=ft.AppView.WEB_BROWSER) diff --git a/frontend/pages/main_page.py b/frontend/pages/main_page.py new file mode 100644 index 0000000..6fd30d0 --- /dev/null +++ b/frontend/pages/main_page.py @@ -0,0 +1,272 @@ +import flet as ft +import requests +import threading +import asyncio +import websockets +from websockets import connect # Ensure this is imported +import json +from datetime import datetime +from pages.student_main_page import student_main_page + +def main_page(page: ft.Page): + page.clean() + page.theme_mode = ft.ThemeMode.LIGHT + + session = page.session.get("access_token") + if not session: + page.go("/login") # Redirect to login if no session + + def is_student(): + url = 'http://awesom-o.org:8000/check_role' + headers = { + 'accept': 'application/json', + 'Content-Type': 'application/json' + } + session_id = page.session.get("access_token") + if not session_id: + return None + + data = {'session_id': session_id} + try: + response = requests.post(url, json=data, headers=headers) + response.raise_for_status() + role = response.json() + + if role and 'role' in role and 'role' in role['role']: + if role['role']['role'] == "student": + role = "s" + return role + elif role['role']['role'] == "teacher": + role = "t" + return role + print("Unexpected response structure:", role) + return None + except requests.exceptions.RequestExceptin as error: + print("Error during role check:", error) + return None + + + + page.title = "Room Information" + page.vertical_alignment = ft.MainAxisAlignment.START # Align all content to the top + + + if is_student() == "s": + student_main_page(page) + elif is_student() == "t": + async def fetch_room_data(session_id: str, update_room_list): + uri = "ws://localhost:8000/ws/teacher/open_rooms" + headers = {"session-id": session_id} # Add session ID to headers + + async with websockets.connect(uri, extra_headers=headers) as websocket: + print("WebSocket connected") + try: + while True: + # Receive data from the WebSocket + data = await websocket.recv() + room_data = json.loads(data) + open_rooms = room_data.get("open_rooms", []) + + # Update the room list + update_room_list(open_rooms) + + except websockets.ConnectionClosed: + print("WebSocket connection closed") + except Exception as e: + print(f"WebSocket error: {e}") + + # Function to fetch students in a room + def fetch_students_in_room(unique_id: str, session_id: str): + url = f"http://awesom-o.org:8000/teacher/room_students/?unique_id={unique_id}" + headers = {"accept": "application/json", "Content-Type": "application/json"} + data = {"session_id": session_id} + try: + response = requests.post(url, json=data, headers=headers) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + print(f"Error fetching students: {e}") + return None + + # Function to delete a room + def delete_room(unique_id: str, session_id: str): + url = f"http://awesom-o.org:8000/teacher/delete_room/?unique_id={unique_id}" + headers = {"accept": "application/json", "Content-Type": "application/json"} + data = {"session_id": session_id} + try: + response = requests.delete(url, json=data, headers=headers) + response.raise_for_status() + return True + except requests.RequestException as e: + print(f"Error deleting room: {e}") + return False + + # Main Flet app + page.title = "Room Management" + page.vertical_alignment = ft.MainAxisAlignment.START # Align content to the top + page.horizontal_alignment = ft.CrossAxisAlignment.CENTER + page.padding = 20 + page.scroll = "adaptive" + + # Check if the user is logged in + session_id = page.session.get("access_token") + if not session_id: + page.go("/login") # Redirect to login if no session + + # Search field + search_field = ft.TextField(label="Search", expand=True) + + # Icon button to navigate to "/create" page + create_button = ft.IconButton( + icon=ft.icons.ADD, + on_click=lambda _: page.go("/create"), + ) + + # Container for the room list + room_container = ft.Column(scroll=ft.ScrollMode.AUTO, expand=True) + + # Function to update the room list + def update_room_list(rooms): + room_container.controls.clear() + + for room in rooms: + room_name = room.get("room_name", "Unknown Room") + room_info = room.get("info", "") + location = room.get("location", "") + teacher_name = room.get("teacher_name", "") + max_students = room.get("max_students", 0) + current_students = room.get("current_students", 0) + unique_id = room.get("unique_id", "") + + # Create a clickable room card + room_card = ft.GestureDetector( + content=ft.Card( + content=ft.Container( + content=ft.Column( + [ + # Top line: room_name, room_info, location + ft.Row( + [ + ft.Text(f"{room_name}", size=20), + ft.Text(f"{room_info}", size=16), + ft.Text(f"| {location}", size=16), + ], + spacing=5, + ), + # Teacher line + ft.Text(f"Teacher: {teacher_name}", size=16), + # Capacity line + ft.Text(f"Capacity: {current_students}/{max_students}", size=16), + ], + spacing=10, + ), + padding=20, + ), + margin=10, + elevation=5, + ), + on_tap=lambda e, unique_id=unique_id: open_room_dialog(unique_id), + ) + + room_container.controls.append(room_card) + + if not rooms: + room_container.controls.append(ft.Text("No open rooms available.", size=18)) + + page.update() + + # Function to open the room dialog + def open_room_dialog(unique_id: str): + # Fetch students in the room + students_data = fetch_students_in_room(unique_id, session_id) + if not students_data: + page.snack_bar = ft.SnackBar(ft.Text("Failed to fetch students.")) + page.snack_bar.open = True + page.update() + return + + students = students_data.get("students", []) + + if students != None: + + students_list = ft.Column( + [ft.Text(f"Student: {student}") for student in students], + scroll=ft.ScrollMode.AUTO, + expand=True, + ) + else: + + students_list = ft.Column(ft.Text(f"There are no students who joined your Room"), + scroll=ft.ScrollMode.AUTO, + expand=True, + ) + # Confirmation dialog for deleting the room + def open_delete_confirmation_dialog(): + page.close(dialog) + def confirm_delete(e): + if delete_room(unique_id, session_id): + page.snack_bar = ft.SnackBar(ft.Text("Room deleted successfully.")) + page.snack_bar.open = True + page.update() + page.close(confirm_dialog) + page.update() + else: + page.snack_bar = ft.SnackBar(ft.Text("Failed to delete room.")) + page.snack_bar.open = True + page.update() + + confirm_dialog = ft.AlertDialog( + title=ft.Text("Are you sure you want to delete this room?"), + actions=[ + ft.TextButton("No", on_click=lambda e: page.close(confirm_dialog)), + ft.TextButton("Yes", on_click=confirm_delete), + ], + ) + + page.dialog = confirm_dialog + confirm_dialog.open = True + page.update() + + # Room dialog + dialog = ft.AlertDialog( + title=ft.Text("Room Details"), + content=students_list, + actions=[ + ft.TextButton("Close", on_click=lambda e: page.close(dialog)), + ft.TextButton("Delete Room", on_click=lambda e: open_delete_confirmation_dialog()), + ], + ) + + page.dialog = dialog + dialog.open = True + page.update() + + # WebSocket listener for real-time updates + async def listen_for_updates(): + while True: + try: + await fetch_room_data(session_id, update_room_list) + except Exception as e: + print(f"WebSocket connection error: {e}. Reconnecting in 5 seconds...") + await asyncio.sleep(5) # Wait before reconnecting + + # Top bar layout + top_bar = ft.Row( + [ + search_field, + create_button, + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + vertical_alignment=ft.CrossAxisAlignment.CENTER, + ) + + # Add components to the page + page.add( + top_bar, + room_container, + ) + + # Start the WebSocket listener + page.run_task(listen_for_updates) + else: + page.go("/login") \ No newline at end of file diff --git a/frontend/pages/register_student_page.py b/frontend/pages/register_student_page.py new file mode 100644 index 0000000..7b3e83d --- /dev/null +++ b/frontend/pages/register_student_page.py @@ -0,0 +1,94 @@ +import flet as ft +import requests +import json + +def register_student_page(page: ft.Page): + page.clean() + page.title = "Register Page" + page.theme_mode = ft.ThemeMode.LIGHT + + # UI components + username_field = ft.TextField(label="Accountname (darfst du dir ausdenken)") + first_name_field = ft.TextField(label="Vorname") + last_name_field = ft.TextField(label="Nachname") + user_class_field = ft.TextField(label="Klasse") + password_field0 = ft.TextField(label="Passwort", password=True, can_reveal_password=True) + password_field1 = ft.TextField(label="Passwort bestätigen", password=True, can_reveal_password=True) + + # Create an info text label + info_label = ft.Text("", theme_style=ft.TextThemeStyle.TITLE_SMALL) + + # Function to handle registration + def register(e): + username = username_field.value.strip() + password0 = password_field0.value.strip() + first_name = first_name_field.value.strip() + last_name = last_name_field.value.strip() + user_class = user_class_field.value.strip() + password1 = password_field1.value.strip() + + # Validate input + if not username or not password0 or not password1 or not first_name or not last_name or not user_class : + info_label.value = "Bitte fülle alle lücken aus" + info_label.color = "red" + page.update() + return + + if not password0 == password1: + info_label.value = "Passwörter stimmen nicht überein" + info_label.color = "red" + page.update() + return + # Send request over HTTP + try: + url = 'http://awesom-o.org:8000/student/register' + headers = { + 'accept': 'application/json', + 'Content-Type': 'application/json' + } + data = { + 'username': username, + 'first_name': first_name, + 'last_name': last_name, + 'user_class': user_class, + 'password': password0, + } + + response = requests.post(url, headers=headers, data=json.dumps(data)) + + + if response.status_code == 200: + session_id = response.json().get("session_id") + username = response.json().get("username") + page.session.set("access_token", session_id) + page.session.set("username", username) + page.go("/") + + + else: + error_message = response.json().get("detail", "Registrierung fehlgeschlagen") + info_label.value = error_message + info_label.color = "red" + except requests.exceptions.RequestException as e: + info_label.value = f"An error occurred: {str(e)}" + info_label.color = "red" + + page.update() + + page.add( + ft.Text("Daltonraum-Buchungssystem der IGS Garbsen", size=30), + ft.Text("Registrieren", size=23, weight=ft.FontWeight.BOLD), + username_field, + first_name_field, + last_name_field, + user_class_field, + password_field0, + password_field1, + ft.ElevatedButton("Registrieren", on_click=register), + info_label, # Add the info label to the page + ft.TextButton("Bereits registriert? Zurück zur Login-Seite.", on_click=lambda _: page.go("/login")) # Link to login + ) + +# You can run the registration page separately or as part of your main app. +if __name__ == "__main__": + ft.app(target=register_student_page, view=ft.AppView.WEB_BROWSER) diff --git a/frontend/pages/register_teacher_page.py b/frontend/pages/register_teacher_page.py new file mode 100644 index 0000000..7ae6943 --- /dev/null +++ b/frontend/pages/register_teacher_page.py @@ -0,0 +1,180 @@ +import flet as ft +import requests + +def register_teacher_page(page: ft.Page): + page.clean() + page.title = "Register Page" + page.theme_mode = ft.ThemeMode.LIGHT + + subjects_list = [] + row = ft.Row() + + # UI components + username_field = ft.TextField(label="Benutzername") + first_name_field = ft.TextField(label="Vorname") + last_name_field = ft.TextField(label="Nachname") + password_field0 = ft.TextField(label="Passwort", password=True, can_reveal_password=True) + password_field1 = ft.TextField(label="Passwort bestätigen", password=True, can_reveal_password=True) + secret_teacher_password_field = ft.TextField(label="Secret Teacher Password", password=True, can_reveal_password=True) + info_label = ft.Text("", theme_style=ft.TextThemeStyle.TITLE_SMALL) + + def delete_subject(dropdown, delete_button): + if dropdown.value in subjects_list: + subjects_list.remove(dropdown.value) + row.controls.remove(dropdown) + row.controls.remove(delete_button) + page.update() + refresh_dropdowns() + + def append_subject(e=None, dropdown=None, plus_button=None): + if not dropdown.value or dropdown.value in subjects_list or dropdown.value == "Wähle deine Fächer aus": + dropdown.value = "" # Reset if empty or duplicate + page.update() + return + + subjects_list.append(dropdown.value) + + # Disable current dropdown and button after selection + dropdown.disabled = True + plus_button.visible = False + + # Add new dropdown to the same row + add_new_dropdown() + + page.update() + + def on_dropdown_change(e, dropdown, plus_button): + if dropdown.value and dropdown.value not in subjects_list and dropdown.value != "Wähle deine Fächer aus": + subjects_list.append(dropdown.value) + dropdown.disabled = True + plus_button.visible = False + add_new_dropdown() + page.update() + + def refresh_dropdowns(): + for control in row.controls: + if isinstance(control, ft.Dropdown) and not control.disabled: + available_subjects = get_available_subjects() + control.options = [ + ft.dropdown.Option("Wähle deine Fächer aus", disabled=True) + ] + [ + ft.dropdown.Option(subj) for subj in available_subjects + ] + page.update() + + def get_available_subjects(): + available_subjects = [ + "De", "En", "Fr", "Gs", "Ma", "NW", "Re", "Sn", "Sp", "Wn" + ] + return [subj for subj in available_subjects if subj not in subjects_list] + + def add_new_dropdown(): + options = [ + ft.dropdown.Option("Wähle deine Fächer aus", disabled=True) + ] + [ + ft.dropdown.Option(subj) for subj in get_available_subjects() + ] + + dropdown = ft.Dropdown( + width=250, + options=options, + value="Wähle deine Fächer aus", + on_change=lambda e: on_dropdown_change(e, dropdown, plus_button) + ) + + plus_button = ft.IconButton( + icon=ft.icons.ADD, + visible=False # Initially hidden + ) + + delete_button = ft.IconButton( + icon=ft.icons.CLOSE_OUTLINED, + on_click=lambda e: delete_subject(dropdown, delete_button) + ) + + plus_button.on_click = lambda e: append_subject(e, dropdown, plus_button) + + # Add dropdown and buttons to the row + row.controls.append(dropdown) + row.controls.append(plus_button) + row.controls.append(delete_button) + page.update() + + def register(e): + print(subjects_list) + username = username_field.value.strip() + password0 = password_field0.value.strip() + password1 = password_field1.value.strip() + secret_teacher_password = secret_teacher_password_field.value.strip() + last_name = last_name_field.value.strip() + first_name = first_name_field.value.strip() + + subjects_string = ", ".join(subjects_list) + + if not username or not password0 or not secret_teacher_password or not password1 or not last_name or not first_name or not subjects_string: + info_label.value = "Bitte fülle alle Felder aus" + info_label.color = "red" + page.update() + #page.clean() + #register_teacher_page(page=page) + return + + if not password0 == password1: + info_label.value = "Passwörter stimmen nicht überein" + info_label.color = "red" + page.update() + return + + try: + url = 'http://awesom-o.org:8000/teacher/register' + headers = { + 'accept': 'application/json', + 'Content-Type': 'application/json' + } + data = { + 'username': username, + 'password': password0, + 'teacher_secret_password': secret_teacher_password, + 'first_name': first_name, + 'last_name': last_name, + 'subjects': subjects_string + } + + response = requests.post(url, headers=headers, json=data) + + if response.status_code == 200: + session_id = response.json().get("session_id") + username = response.json().get("username") + page.session.set("access_token", session_id) + page.session.set("username", username) + page.go("/") + + else: + error_message = response.json().get("detail", "Registrierung fehlgeschlagen") + info_label.value = error_message + info_label.color = "red" + except requests.exceptions.RequestException as e: + info_label.value = f"An error occurred: {str(e)}" + info_label.color = "red" + + page.update() + + page.add( + ft.Text("Daltonraum-Buchungssystem der IGS Garbsen", size=30), + ft.Text("Registrieren", size=23, weight=ft.FontWeight.BOLD), + username_field, + ft.Row([ + first_name_field, + last_name_field + ]), + row, + password_field0, + password_field1, + secret_teacher_password_field, + ft.ElevatedButton("Registrieren", on_click=register), + info_label, + ft.TextButton("Kein Lehrer? Klicke hier!", on_click=lambda _: page.go("/register_student")) + ) + + add_new_dropdown() + diff --git a/frontend/pages/student_main_page.py b/frontend/pages/student_main_page.py new file mode 100644 index 0000000..48537e1 --- /dev/null +++ b/frontend/pages/student_main_page.py @@ -0,0 +1,343 @@ +import flet as ft +import requests +import threading +import asyncio +import websockets +from websockets import connect # Ensure this is imported +import json +from datetime import datetime + + + +def get_initials(user_name: str): + if user_name: + return user_name[0].capitalize() + else: + return "E" # or any default value you prefer + + +def get_avatar_color(user_name: str): + colors_lookup = [ + ft.colors.AMBER, + ft.colors.BLUE, + ft.colors.BROWN, + ft.colors.CYAN, + ft.colors.GREEN, + ft.colors.INDIGO, + ft.colors.LIME, + ft.colors.ORANGE, + ft.colors.PINK, + ft.colors.PURPLE, + ft.colors.RED, + ft.colors.TEAL, + ] + return colors_lookup[hash(user_name) % len(colors_lookup)] + + +def student_main_page(page: ft.Page): + page.clean() + page.title = "Room Information" + page.theme_mode = ft.ThemeMode.LIGHT + + page.vertical_alignment = ft.MainAxisAlignment.START # Align all content to the top + page.scroll = "adaptive" + page.padding = 20 + + # Container for lessons + lessons_container = ft.Column( + alignment=ft.MainAxisAlignment.CENTER, spacing=20) + + def test(e): + print(e) + page.go("/test") + print("test") + + def ausloggen(e=None): + page.go("/login") + print(page.session.get("access_token")) + + def page_dialog_click(e, lesson_id): + dialog_leave = ft.AlertDialog( + modal=True, + title=ft.Text("Bitte bestätige"), + content=ft.Text("Möchtest du diesem Raum wirklich verlassen?"), + actions=[ + ft.TextButton("Ja", on_click=lambda e: (page.close( + dialog_leave), leave_current_room(e, lesson_id))), + ft.TextButton( + "Nein", on_click=lambda e: page.close(dialog_leave)), + ], + actions_alignment=ft.MainAxisAlignment.END, + ) + page.dialog = dialog_leave + dialog_leave.open = True + page.update() + + def leave_current_room(e=None, unique_id=None): + if not unique_id: + page.snack_bar = ft.SnackBar( + ft.Text("Kein Raum zum verlassen ausgewählt")) + page.snack_bar.open = True + page.update() + return + + url = f"http://awesom-o.org:8000/student/leave_room/?unique_id={ + unique_id}" + headers = {"accept": "application/json", + "Content-Type": "application/json"} + data = {"session_id": page.session.get("access_token")} + try: + response = requests.post(url, json=data, headers=headers) + response.raise_for_status() + if response: + get_rooms() + page.snack_bar = ft.SnackBar( + ft.Text("Raum erfolgreich verlassen")) + page.snack_bar.open = True + page.update() + get_rooms() + except requests.RequestException as error: + # Handle all bad responses + error_detail = "Something went wrong." + if hasattr(error, "response") and error.response: + try: + error_detail = error.response.json().get("detail", "Something went wrong.") + except json.JSONDecodeError: + error_detail = error.response.text or "Etwas ist schiefgelaufen" + page.snack_bar = ft.SnackBar(ft.Text(f"Error: {error_detail}")) + page.snack_bar.open = True + page.update() + + async def get_rooms(): + uri = "ws://localhost:8000/ws/student/my_room" + headers = {"session-id": page.session.get("access_token")} + + try: + async with websockets.connect(uri, extra_headers=headers) as websocket: + while True: + message = await websocket.recv() + data = json.loads(message) + # Clear previous lessons + lessons_container.controls.clear() + + if "error" in data: + # Show "No Rooms for Today" message + lessons_container.controls.append( + ft.Container( + content=ft.Text( + "Für heute wurden noch keine Räume ausgewählt.", + size=25, + text_align="center", + ), + border=ft.Border( + top=ft.BorderSide(1, ft.colors.BLACK), + bottom=ft.BorderSide(1, ft.colors.BLACK), + left=ft.BorderSide(1, ft.colors.BLACK), + right=ft.BorderSide(1, ft.colors.BLACK) + ), + border_radius=20, + padding=10, + height=150, + bgcolor=ft.colors.WHITE, + alignment=ft.alignment.center, + ) + ) + else: + rooms = data.get("rooms", []) + # Filter and sort lessons for today + today = datetime.now().date() + today_lessons = [ + room for room in rooms if room["lesson_date"] == str(today)] + today_lessons.sort(key=lambda x: x["lesson_time"]) + + # Check if there are rooms on other dates + other_date_rooms = [ + room for room in rooms if room["lesson_date"] != str(today)] + if today_lessons: + for lesson in today_lessons: + if lesson['current_students'] == lesson['max_students']: + BACKROUND_COLOR = "#f10d0c" + TEXT_COLOR = ft.colors.WHITE + else: + BACKROUND_COLOR = "#729fcf" + TEXT_COLOR = ft.colors.BLACK + # Create lesson card + room_card = ft.Card( + content=ft.Container( + content=ft.Row( + [ + # Room details + ft.Column( + [ + # Top line: + # room_number, info, location + ft.Text( + f"Heute, { + lesson['lesson_time']} Uhr", + size=16, + color=TEXT_COLOR + ), + + # Second line: first_name and last_name + ft.Text( + f"Lehrkraft: {lesson['first_name']} { + lesson['last_name']} - {lesson['subjects']}", + size=16, + color=TEXT_COLOR + ), + # Third line: lesson_time + ft.Text( + f"Raum {lesson['room_number']} { + lesson['info']} | {lesson['location']}", + size=16, + color=TEXT_COLOR + ), + + ft.Text( + f"Belegte Plätze: { + lesson['current_students']}/{lesson['max_students']}", + size=16, + color=TEXT_COLOR + ), + ], + expand=True, + alignment=ft.MainAxisAlignment.CENTER, + ), + # IconButton on the right + ft.Column( + [ + ft.TextButton("Aus Raum abmelden", on_click=lambda e, lesson_id=lesson['unique_id']: page_dialog_click(e, lesson_id), + style=ft.ButtonStyle( + bgcolor=ft.colors.WHITE, + color=ft.colors.BLACK, + shape=ft.RoundedRectangleBorder( + radius=5), + padding=20, + ) + ), + ft.Text("ACHTUNG: Hierdurch wird", size=12), ft.Text( + "dein reservierter Platz für", size=12), ft.Text("jemand anderes freigegeben.", size=12), + ] + ) + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + ), + padding=20, # Add padding inside the card + ), + color=BACKROUND_COLOR, + margin=10, + ) + lessons_container.controls.append(room_card) + else: + # Show "No Rooms for Today" message + lessons_container.controls.append( + ft.Container( + content=ft.Text( + "Für heute wurden noch keine Räume ausgewählt.", + size=25, + text_align="center", + ), + border=ft.Border( + top=ft.BorderSide(1, ft.colors.BLACK), + bottom=ft.BorderSide( + 1, ft.colors.BLACK), + left=ft.BorderSide(1, ft.colors.BLACK), + right=ft.BorderSide(1, ft.colors.BLACK) + ), + border_radius=20, + padding=10, + height=150, + bgcolor=ft.colors.WHITE, + alignment=ft.alignment.center, + ) + ) + + # Update page + page.update() + except Exception as e: + lessons_container.controls.clear() + lessons_container.controls.append( + ft.Text(f"Error: {e}", size=50, weight="bold", text_align="center")) + page.update() + + def start_get_rooms(): + # Ensure a new event loop in the thread + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(get_rooms()) + + # Start the WebSocket client in a thread + import threading + threading.Thread(target=start_get_rooms, daemon=True).start() + + join_more_rooms_button = ft.Container( + content=ft.Text("Räume suchen", size=24, color=ft.colors.BLUE_900), + on_click=lambda e: page.go("/join"), + alignment=ft.alignment.center, + width=200, + height=60, + bgcolor='transparent', + ink=True, + ) + + navigation_bar = ft.Row( + [ft.Text("Home", size=23, weight=ft.FontWeight.BOLD, + width=100), join_more_rooms_button], + ) + + # Add components to page + page.add( + ft.Column( + [ + ft.Row( + [ + ft.Text( + "Daltonraum-Buchungssystem der IGS Garbsen", size=30), + ft.Column([ + ft.PopupMenuButton( + items=[ + ft.PopupMenuItem(content=ft.Text(f"Eingeloggt als: {page.session.get("username")}", weight=ft.FontWeight.BOLD)), + ft.PopupMenuItem(text="Profil anzeigen", on_click=test), + ft.PopupMenuItem(text="Ausloggen", on_click=ausloggen), + ], + content=ft.CircleAvatar( + content=ft.Text(get_initials( + page.session.get("username"))), + color=ft.colors.WHITE, + bgcolor=get_avatar_color( + page.session.get("username")), + ), + menu_position=ft.PopupMenuPosition.UNDER, + tooltip="", + + ) + + ], + # Align column content to the end (optional) + alignment=ft.MainAxisAlignment.END, + ), + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, # Space out the two elements + vertical_alignment=ft.CrossAxisAlignment.CENTER, # Align items vertically + ), + + navigation_bar, + ft.Row( # Wrap the Text in a Row + [ + ft.Text( + "Du hast dich heute in folgende Daltonräume eingetragen:", size=25) + ], + alignment=ft.MainAxisAlignment.CENTER # Center the Text horizontally + ), + ft.Column( + [ + lessons_container, + ], + alignment=ft.MainAxisAlignment.CENTER, + expand=True, # Make it take available space vertically + ), + ], + expand=True, # This makes the outer column take available space as well + alignment=ft.MainAxisAlignment.CENTER, # Center the whole column vertically + ) + ) diff --git a/frontend/pages/temp/attendance_sheet.pdf b/frontend/pages/temp/attendance_sheet.pdf new file mode 100644 index 0000000..82d7797 Binary files /dev/null and b/frontend/pages/temp/attendance_sheet.pdf differ diff --git a/frontend/pages/test.py b/frontend/pages/test.py new file mode 100644 index 0000000..c0da7fd --- /dev/null +++ b/frontend/pages/test.py @@ -0,0 +1,15 @@ +import flet as ft + +def main(page: ft.Page): + # Button click handler to launch the download URL + def on_download_click(e): + # Launch the URL to trigger file download + page.launch_url("http://awesom-o.org:8000/download/tedt.pdf") + + # Create an ElevatedButton that will trigger the download + download_button = ft.ElevatedButton("Download myfile", on_click=on_download_click) + + # Add the button to the page + page.add(download_button) + +ft.app(target=main) diff --git a/main.py b/main.py new file mode 100644 index 0000000..5d0ab7c --- /dev/null +++ b/main.py @@ -0,0 +1,996 @@ +import sqlite3 +from datetime import datetime, timedelta, time +from fastapi import FastAPI, HTTPException, Depends, WebSocket, WebSocketDisconnect, Header +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +from fastapi.responses import FileResponse +from typing import List, Dict, Optional +import uuid +import os +import json +import asyncio +from starlette.websockets import WebSocketState +from fpdf import FPDF +import tempfile + +app = FastAPI() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# SQLite database setup +DATABASE_NAME = "school.db" + +SUBJECTS = ["De", "Ma", "NW", "En", "Gs", "Re", "WN", "Fr", "Sp", "Sn"] + +ROOM_NAMES = [ + {"number": "1", "room_number": "0201", "info": "(Hörsaal)", "location": "Osttrakt"}, + {"number": "2", "room_number": "0202", "info": "(NW)", "location": "Osttrakt"}, + {"number": "3", "room_number": "0203", "info": "(NW)", "location": "Osttrakt"}, + {"number": "4", "room_number": "0204", "info": "(NW)", "location": "Osttrakt"}, + {"number": "5", "room_number": "0205", "info": "(NW)", "location": "Osttrakt"}, + {"number": "6", "room_number": "0214", "info": "(NW)", "location": "Lichthof"}, + {"number": "7", "room_number": "0215", "info": "(NW)", "location": "Lichthof"}, + {"number": "8", "room_number": "0216", "info": "(NW)", "location": "Flur zum Lichthof"}, + {"number": "9", "room_number": "0217", "info": "(NW)", "location": "Flur zum Lichthof"}, + {"number": "10", "room_number": "0265", "info": "(NW)", "location": "Lichthof"}, + {"number": "11", "room_number": "0266", "info": "(NW)", "location": "Flur zum Lichthof"}, + {"number": "12", "room_number": "0267", "info": "(NW)", "location": "Flur zum Lichthof"}, + {"number": "13", "room_number": "1860", "info": "Jahrgang Südwest", "location": "1. OG"}, + {"number": "14", "room_number": "1861", "info": "Jahrgang Südwest", "location": "1. OG"}, + {"number": "15", "room_number": "1862", "info": "Jahrgang Südwest", "location": "1. OG"}, + {"number": "16", "room_number": "1863", "info": "Jahrgang Südwest", "location": "1. OG"}, + {"number": "17", "room_number": "1864", "info": "Jahrgang Südwest", "location": "1. OG"}, + {"number": "18", "room_number": "1865", "info": "Jahrgang Südwest", "location": "1. OG"}, + {"number": "19", "room_number": "1866", "info": "Jahrgang Südwest", "location": "1. OG"}, + {"number": "20", "room_number": "1867", "info": "Jahrgang Südwest", "location": "1. OG"}, + {"number": "21", "room_number": "1868", "info": "Jahrgang Südwest", "location": "1. OG"}, + {"number": "22", "room_number": "1869", "info": "Jahrgang Südwest", "location": "1. OG"}, + {"number": "23", "room_number": "1870", "info": "Jahrgang Südwest", "location": "1. OG"}, + {"number": "24", "room_number": "2860", "info": "Jahrgang Südwest", "location": "2. OG"}, + {"number": "25", "room_number": "2861", "info": "Jahrgang Südwest", "location": "2. OG"}, + {"number": "26", "room_number": "2862", "info": "Jahrgang Südwest", "location": "2. OG"}, + {"number": "27", "room_number": "2863", "info": "Jahrgang Südwest", "location": "2. OG"}, + {"number": "28", "room_number": "2864", "info": "Jahrgang Südwest", "location": "2. OG"}, + {"number": "29", "room_number": "2865", "info": "Jahrgang Südwest", "location": "2. OG"}, + {"number": "30", "room_number": "2866", "info": "Jahrgang Südwest", "location": "2. OG"}, + {"number": "31", "room_number": "2867", "info": "Jahrgang Südwest", "location": "2. OG"}, + {"number": "32", "room_number": "2868", "info": "Jahrgang Südwest", "location": "2. OG"}, + {"number": "33", "room_number": "2869", "info": "Jahrgang Südwest", "location": "2. OG"}, + {"number": "34", "room_number": "2870", "info": "Jahrgang Südwest", "location": "2. OG"} +] +room_name_dict = {room['number']: room for room in ROOM_NAMES} + +active_connections: Dict[str, WebSocket] = {} + +#CURRENT_TIME_HOUR_MINUTE = datetime.now().strftime("%H:%M") +CURRENT_DATE = datetime.now().strftime("%Y-%m-%d") + +CURRENT_TIME_HOUR_MINUTE = "9:10" +# Helper function to connect to the database +def get_db_connection(): + conn = sqlite3.connect(DATABASE_NAME) + conn.row_factory = sqlite3.Row + return conn + +# Database tables creation on startup +def create_tables(): + conn = get_db_connection() + cursor = conn.cursor() + + cursor.execute(''' + CREATE TABLE IF NOT EXISTS rooms ( + room_number TEXT NOT NULL, + max_students INTEGER NOT NULL, + teacher_name TEXT NOT NULL, + is_open BOOLEAN NOT NULL, + lesson_date TEXT NOT NULL, -- Can also be DATETIME if you need both date and time + lesson_time TEXT NOT NULL, + unique_id TEXT PRIMARY KEY, -- Ensure unique_id is the primary key + UNIQUE(room_number, lesson_date, lesson_time) -- Composite unique constraint + ) + ''') + cursor.execute(''' + CREATE TABLE IF NOT EXISTS student_joins ( + student_username TEXT NOT NULL, + room_unique_id TEXT NOT NULL, + join_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (student_username, room_unique_id), + FOREIGN KEY (student_username) REFERENCES students(username) ON DELETE CASCADE, + FOREIGN KEY (room_unique_id) REFERENCES rooms(unique_id) ON DELETE CASCADE + ) + ''') + + cursor.execute(''' + CREATE TABLE IF NOT EXISTS students ( + username TEXT PRIMARY KEY NOT NULL, + password TEXT NOT NULL, + first_name TEXT, + last_name TEXT, + user_class TEXT, + unique_id TEXT + ) + ''') + + + cursor.execute(''' + CREATE TABLE IF NOT EXISTS teachers ( + username TEXT PRIMARY KEY, + password TEXT, + first_name TEXT, + last_name TEXT, + subjects TEXT + ) + ''') + + + cursor.execute(''' + CREATE TABLE IF NOT EXISTS sessions ( + session_id TEXT PRIMARY KEY, + username TEXT, + role TEXT + ) + ''') + + # Clear all rooms and sessions on startup + cursor.execute("DELETE FROM rooms") + cursor.execute("DELETE FROM sessions") + cursor.execute("DELETE FROM student_joins") + conn.commit() + conn.close() + +# Models +class RoomCreate(BaseModel): + room_number: str + max_students: int + session_id: str + lesson_time: str + lesson_date: str + +class Student(BaseModel): + username: str + +class RoomInfo(BaseModel): + room_number: str + max_students: int + current_students: List[str] + teacher_name: str +class User_register(BaseModel): + username: str + first_name: str + last_name: str + user_class: str + password: str + +class User(BaseModel): + username: str + password: str + +class Session(BaseModel): + session_id: str + +class Teacher_User(BaseModel): + username: str + password: str + teacher_secret_password: str + first_name: str + last_name: str + subjects: str + + + +def current_role(session_id: str): + conn = get_db_connection() + cursor = conn.cursor() + + # Check if the session ID exists in the sessions table + cursor.execute("SELECT role FROM sessions WHERE session_id = ?", (session_id,)) + role: list[sqlite3.Row] = cursor.fetchone() + + conn.close() + return role +# Helper functions +def get_current_user(session_id: str): + conn = get_db_connection() + cursor = conn.cursor() + + cursor.execute("SELECT * FROM sessions WHERE session_id = ?", (session_id,)) + session = cursor.fetchone() + + if session is None: + raise HTTPException(status_code=404, detail="Session not found") + + conn.close() + return session + +def subjects_avaible(liste, worte): + for fach in worte: + if fach not in liste: + return False, fach # Gibt False und das nicht gefundene Fach zurück + return True, None + +async def check_lesson_time(): + print(f"Checking for the event at {datetime.now()}...") + try: + conn = get_db_connection() + cursor = conn.cursor() + + cursor.execute( + """ + UPDATE rooms + SET is_open = ? + WHERE lesson_date = ? AND lesson_time = ? + """, + (False, CURRENT_DATE, CURRENT_TIME_HOUR_MINUTE) + ) + conn.commit() + conn.close() + event_happened = False # Replace with your actual condition + if event_happened: + print("Event occurred!") + return True + return False + except: + print("no rooms") + +# Calculate seconds until the next target time +def seconds_until_next_interval(): + now = datetime.now() + next_minute = (now.minute // 5 + 1) * 5 # Next multiple of 5 minutes + if next_minute >= 60: # Handle hour overflow + next_hour = now.hour + 1 if now.hour < 23 else 0 + next_time = datetime(now.year, now.month, now.day, next_hour, 0, 0) + else: + next_time = datetime(now.year, now.month, now.day, now.hour, next_minute, 0) + return (next_time - now).total_seconds() + +# Background task to check the event based on real-world time +async def periodic_event_checker(): + while True: + try: + event_detected = await check_lesson_time() + if event_detected: + print("Event detected! Taking action.") + # Add any action you want to take when the event occurs + # You can also exit the loop if needed + break + sleep_time = seconds_until_next_interval() + print(f"Sleeping for {sleep_time:.2f} seconds until the next check.") + await asyncio.sleep(sleep_time) + except Exception as e: + print(f"Error in background task: {e}") + await asyncio.sleep(5 * 60) # Retry after 5 minutes in case of an error + + +import logging + +logging.basicConfig(level=logging.DEBUG) + +def is_open_update(unique_id: str): + conn = get_db_connection() + cursor = conn.cursor() + + # Check the current number of students in the room + cursor.execute(""" + SELECT COUNT(s.username) AS current_students, + COALESCE(r.max_students, 100) AS max_students, + COALESCE(r.is_open, 0) AS is_open + FROM students s + JOIN student_joins sj ON s.username = sj.student_username + JOIN rooms r ON r.unique_id = sj.room_unique_id + WHERE sj.room_unique_id = ? + """, (unique_id,)) + + result = cursor.fetchone() + if result is None: + logging.debug(f"No room found with unique_id: {unique_id}") + conn.close() + return + + current_students = int(result['current_students']) + max_students = int(result['max_students']) + current_is_open = int(result['is_open']) + + logging.debug(f"current_students: {current_students}, max_students: {max_students}, current_is_open: {current_is_open}") + + # If the number of students exceeds or equals the max students, close the room + if current_students >= max_students: + if current_is_open != 0: # Check if the room is not already closed + cursor.execute(""" + UPDATE rooms + SET is_open = 0 + WHERE unique_id = ? + """, (unique_id,)) + conn.commit() + else: + if current_is_open != 1: # Check if the room is not already + cursor.execute(""" + UPDATE rooms + SET is_open = 1 + WHERE unique_id = ? + """, (unique_id,)) + conn.commit() + + conn.close() + +# Clear all data when the application starts +@app.on_event("startup") +def startup_event(): + create_tables() + asyncio.create_task(periodic_event_checker()) + +@app.post("/teacher/register") +def teacher_register(user: Teacher_User): + conn = get_db_connection() + cursor = conn.cursor() + + # Check if username exists in either teachers or students + cursor.execute("SELECT * FROM teachers WHERE username = ?", (user.username,)) + if cursor.fetchone(): + conn.close() + raise HTTPException(status_code=400, detail="Username already exists as a teacher") + + cursor.execute("SELECT * FROM students WHERE username = ?", (user.username,)) + if cursor.fetchone(): + conn.close() + raise HTTPException(status_code=400, detail="Username already exists as a student") + + if user.teacher_secret_password != "chrissi": + conn.close() + raise HTTPException(status_code=403, detail="Incorrect teacher secret password") + + + teacher_subjects = [fach.strip() for fach in user.subjects.split(",")] + + result, error_subject = subjects_avaible(SUBJECTS, teacher_subjects) + if result == False: + return{"message": "Unavaible Subject", "subject": error_subject} + + cursor.execute("INSERT INTO teachers (username, password, first_name, last_name, subjects) VALUES (?, ?, ?, ?, ?)", (user.username, user.password, user.first_name, user.last_name, user.subjects)) + conn.commit() + + # Create session for teacher + session_id = str(uuid.uuid4()) + cursor.execute("INSERT INTO sessions (session_id, username, role) VALUES (?, ?, ?)", (session_id, user.username, "teacher")) + conn.commit() + + conn.close() + return {"message": "Teacher registered successfully", "session_id": session_id, "username": user.username} + +@app.post("/student/register") +def student_register(user: User_register): + conn = get_db_connection() + cursor = conn.cursor() + + # Check if username exists in either students or teachers + cursor.execute("SELECT * FROM students WHERE username = ?", (user.username,)) + if cursor.fetchone(): + conn.close() + raise HTTPException(status_code=400, detail="Benutzername bereits vergeben") + + cursor.execute("SELECT * FROM teachers WHERE username = ?", (user.username,)) + if cursor.fetchone(): + conn.close() + raise HTTPException(status_code=400, detail="Benutzername bereits vergeben") + + cursor.execute("INSERT INTO students (username, password, first_name, last_name, user_class) VALUES (?, ?, ?, ?, ?)", (user.username, user.password, user.first_name, user.last_name, user.user_class)) + conn.commit() + + # Create session for student + session_id = str(uuid.uuid4()) + cursor.execute("INSERT INTO sessions (session_id, username, role) VALUES (?, ?, ?)", (session_id, user.username, "student")) + conn.commit() + + conn.close() + return {"message": "Schüler erfolgreich registriert", "session_id": session_id, "username": user.username} + +@app.post("/login") +def login(user: User): + conn = get_db_connection() + cursor = conn.cursor() + + cursor.execute("SELECT * FROM teachers WHERE username = ?", (user.username,)) + teacher = cursor.fetchone() + if teacher: + if teacher["password"] != user.password: + conn.close() + raise HTTPException(status_code=403, detail="Anmeldung Fehlgeschlagen! Falscher Benutzername oder falsches Passwort") + role = "teacher" + else: + cursor.execute("SELECT * FROM students WHERE username = ?", (user.username,)) + student = cursor.fetchone() + if student: + if student["password"] != user.password: + conn.close() + raise HTTPException(status_code=403, detail="Anmeldung Fehlgeschlagen! Falscher Benutzername oder falsches Passwort") + role = "student" + else: + conn.close() + raise HTTPException(status_code=404, detail="Anmeldung Fehlgeschlagen! Falscher Benutzername oder falsches Passwort") + + session_id = str(uuid.uuid4()) + cursor.execute("INSERT INTO sessions (session_id, username, role) VALUES (?, ?, ?)", (session_id, user.username, role)) + conn.commit() + + conn.close() + return {"session_id": session_id, "username": user.username} + +@app.post("/teacher/create_room") +def create_room(room: RoomCreate): + lesson_date = datetime.strptime(room.lesson_date, "%Y-%m-%d") + today = datetime.today().date() + + if lesson_date.date() < today: + raise HTTPException(status_code=400, detail="Lesson date cannot be in the past") + + # Check if the lesson date is today, but the lesson time has already passed + if lesson_date.date() == today: + lesson_time = datetime.strptime(room.lesson_time, "%H:%M").time() + current_time = datetime.now().time() + + #if lesson_time < current_time: + # raise HTTPException(status_code=400, detail="Lesson time has already passed today") + + user = get_current_user(room.session_id) + if user['role'] != "teacher": + raise HTTPException(status_code=403, detail="Only teachers can create rooms") + + unique_id = str(uuid.uuid4()) + + conn = get_db_connection() + cursor = conn.cursor() + + cursor.execute("SELECT * FROM rooms WHERE room_number = ? AND lesson_date = ? AND lesson_time = ?", (room.room_number, room.lesson_date, room.lesson_time,)) + if cursor.fetchone(): + conn.close() + raise HTTPException(status_code=400, detail="Room already exists") + cursor.execute( + "INSERT INTO rooms (room_number, max_students, teacher_name, is_open, lesson_date, lesson_time, unique_id) VALUES (?, ?, ?, ?, ?, ?, ?)", + ( + room.room_number, + room.max_students, + user['username'], + True, + room.lesson_date, + room.lesson_time, + unique_id, + ) + ) + + conn.commit() + conn.close() + return {"message": "Room created successfully", "unique_id": unique_id} + +@app.delete("/teacher/delete_room/") +def delete_room(unique_id: str, session: Session): + user = get_current_user(session.session_id) + if user['role'] != "teacher": + raise HTTPException(status_code=403, detail="Only teachers can delete rooms") + + conn = get_db_connection() + cursor = conn.cursor() + + cursor.execute("SELECT * FROM rooms WHERE unique_id = ?", (unique_id,)) + room = cursor.fetchone() + if room is None: + conn.close() + raise HTTPException(status_code=404, detail="Room not found") + + if room["teacher_name"] != user['username']: + conn.close() + raise HTTPException(status_code=403, detail="You do not have permission to delete this room") + + cursor.execute("DELETE FROM rooms WHERE unique_id = ?", (unique_id,)) + conn.commit() + + conn.close() + return {"message": f"Room {unique_id} deleted successfully by {user['username']}"} + +@app.post("/teacher/room_students/") +def get_room_students(unique_id: str, session: Session): + user = get_current_user(session.session_id) + + if user['role'] != "teacher": + raise HTTPException(status_code=403, detail="Only teachers can view students in their room") + + conn = get_db_connection() + cursor = conn.cursor() + + # Check if the room exists and if it's owned by the current teacher + cursor.execute("SELECT * FROM rooms WHERE unique_id = ? AND teacher_name = ?", (unique_id, user['username'])) + room = cursor.fetchone() + + if room is None: + conn.close() + raise HTTPException(status_code=404, detail="Room not found or not owned by this teacher") + + # Fetch the students who are currently joined to the room + cursor.execute(""" + SELECT s.username + FROM students s + JOIN student_joins sj ON s.username = sj.student_username + WHERE sj.room_unique_id = ? + """, (unique_id,)) + + students = [row['username'] for row in cursor.fetchall()] + + conn.close() + return {"unique_id": unique_id, "students": students} + +@app.websocket("/ws/student/my_room") +async def websocket_get_my_room(websocket: WebSocket, session_id: Optional[str] = Header(None)): + await websocket.accept() + # Validate session ID and get user + user = get_current_user(session_id) + + if not user or user['role'] != "student": + await websocket.close(code=403) + return + + # Store active connection + active_connections[user['username']] = websocket + + try: + while True: + # Simulate periodic room and teacher subjects fetch + conn = get_db_connection() + cursor = conn.cursor() + + # Fetch rooms the student has joined + cursor.execute( + """ + SELECT r.unique_id, r.room_number, r.teacher_name, r.max_students, r.lesson_date, r.lesson_time, + t.first_name, t.last_name + FROM student_joins sj + JOIN rooms r ON sj.room_unique_id = r.unique_id + JOIN teachers t ON r.teacher_name = t.username + WHERE sj.student_username = ? + """, + (user['username'],) + ) + + rooms = cursor.fetchall() + + # Fetch teacher subjects + cursor.execute("SELECT username, subjects FROM teachers") + teacher_subjects_raw = cursor.fetchall() + teacher_subjects = {row[0]: row[1] for row in teacher_subjects_raw} + + rooms_list = [] + for room in rooms: + room_dict = dict(room) + room_number = room_dict.get("room_number") + teacher_name = room_dict.get("teacher_name") + + # Add additional room details from room_name_dict + if room_number and room_number in room_name_dict: + room_dict.update(room_name_dict[room_number]) + + # Add teacher subjects if available + if teacher_name and teacher_name in teacher_subjects: + room_dict["subjects"] = teacher_subjects[teacher_name] + + # Fetch current student count for the room + cursor.execute( + """ + SELECT COUNT(s.username) as current_students + FROM students s + JOIN student_joins sj ON s.username = sj.student_username + WHERE sj.room_unique_id = ? + """, + (room_dict["unique_id"],) + ) + current_students = cursor.fetchone()["current_students"] + room_dict["current_students"] = current_students + + rooms_list.append(room_dict) + + conn.close() + + if rooms_list: + await websocket.send_json({"rooms": rooms_list}) + else: + await websocket.send_json({"error": "Student has not joined any rooms"}) + + await asyncio.sleep(2) # Poll every 2 seconds + except WebSocketDisconnect: + del active_connections[user['username']] + + +@app.websocket("/ws/open_rooms") +async def websocket_open_rooms(websocket: WebSocket, session_id: Optional[str] = Header(None)): + await websocket.accept() + + # Validate session ID and get user + user = get_current_user(session_id) + + if not user or user['role'] != "student": + await websocket.close(code=403) + return + + # Store active connection + active_connections[user['username']] = websocket + + try: + while True: + # Simulate periodic room fetch + conn = sqlite3.connect('school.db') + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + # Fetch all rooms, both open and closed + cursor.execute(""" + SELECT r.unique_id, r.room_number, r.teacher_name, r.max_students, + COUNT(s.username) AS current_students, + r.is_open, + r.lesson_date, r.lesson_time, + t.first_name, t.last_name, t.subjects, + CASE + WHEN EXISTS ( + SELECT 1 + FROM student_joins sj + JOIN sessions ss ON sj.student_username = ss.username + WHERE sj.room_unique_id = r.unique_id AND ss.session_id = ? + ) THEN 1 + ELSE 0 + END AS joined + FROM rooms r + LEFT JOIN student_joins sj ON r.unique_id = sj.room_unique_id + LEFT JOIN students s ON sj.student_username = s.username + LEFT JOIN teachers t ON r.teacher_name = t.username + WHERE r.lesson_date = ? + GROUP BY r.unique_id + """, (session_id, CURRENT_DATE,)) + + all_rooms = [] + for row in cursor.fetchall(): + room = dict(row) + room_number = str(room['room_number']) + + room_info = room_name_dict.get(room_number, {}) + room.update({ + 'room_number': int(room_number), + 'room_name': room_info.get('room_number', ''), + 'info': room_info.get('info', ''), + 'location': room_info.get('location', ''), + 'joined': bool(room['joined']), # Whether the user joined + 'max_students': room['max_students'], # Maximum capacity + 'current_students': room['current_students'], # Current count + 'is_open': bool(room['is_open']), # Room open status + }) + + all_rooms.append(room) + + conn.close() + + if all_rooms: + await websocket.send_json({"rooms": all_rooms}) + else: + await websocket.send_json({"message": "No rooms available"}) + await asyncio.sleep(1) + except HTTPException: + await websocket.send_json({"error": "Session not found"}) + await websocket.close(code=404) + return + + except WebSocketDisconnect: + del active_connections[user['username']] + + + +@app.post("/student/join_room/") +def join_room(unique_id: str, session: Session): + user = get_current_user(session.session_id) + + if user['role'] != "student": + raise HTTPException(status_code=403, detail="Nur Schüler können diesem Raum beitreten") + + conn = get_db_connection() + cursor = conn.cursor() + + # Check if the room exists + cursor.execute("SELECT * FROM rooms WHERE unique_id = ?", (unique_id,)) + room = cursor.fetchone() + if room is None: + conn.close() + raise HTTPException(status_code=404, detail="Raum nicht gefunden") + + # Check if the room is open + if not room["is_open"]: + conn.close() + raise HTTPException(status_code=400, detail="Raum ist geschlossen") + + # Check if the student is already in a room at the same time (same lesson_date and lesson_time) + cursor.execute(""" + SELECT r.unique_id + FROM student_joins sj + JOIN rooms r ON sj.room_unique_id = r.unique_id + WHERE sj.student_username = ? AND r.lesson_date = ? AND r.lesson_time = ? + """, (user['username'], room["lesson_date"], room["lesson_time"])) + conflicting_room = cursor.fetchone() + + if conflicting_room: + conn.close() + raise HTTPException(status_code=400, detail="Du bist bereits einem Raum für die gleiche Stunde zugewiesen") + + # Check the current number of students in the room + cursor.execute(""" + SELECT COUNT(sj.student_username) as current_students + FROM student_joins sj + WHERE sj.room_unique_id = ? + """, (unique_id,)) + current_students = cursor.fetchone()["current_students"] + + if current_students >= room["max_students"]: + conn.close() + raise HTTPException(status_code=400, detail="Raum ist voll") + + # Add the student to the room by inserting into student_joins table + cursor.execute(""" + INSERT INTO student_joins (student_username, room_unique_id) + VALUES (?, ?) + """, (user['username'], unique_id)) + + conn.commit() + conn.close() + + is_open_update(unique_id) + return {"message": f"Joined room {unique_id}"} + + + +@app.post("/student/leave_room/") +def leave_room(unique_id: str, session: Session): + user = get_current_user(session.session_id) + + if user['role'] != "student": + raise HTTPException(status_code=403, detail="Only students can leave rooms") + + conn = get_db_connection() + cursor = conn.cursor() + + # Check if the student is in the specified room by checking the student_joins table + cursor.execute("SELECT * FROM student_joins WHERE student_username = ? AND room_unique_id = ?", + (user['username'], unique_id)) + student_join = cursor.fetchone() + + if not student_join: + conn.close() + raise HTTPException(status_code=400, detail="You are not in the specified room") + + # Remove the student from the room by deleting the entry from student_joins + cursor.execute("DELETE FROM student_joins WHERE student_username = ? AND room_unique_id = ?", + (user['username'], unique_id)) + conn.commit() + + conn.close() + is_open_update(unique_id) + return {"message": f"You have left room {unique_id}"} + + +@app.post("/check_session") +def check_session(session: Session): + conn = get_db_connection() + cursor = conn.cursor() + + # Check if the session ID exists in the sessions table + cursor.execute("SELECT * FROM sessions WHERE session_id = ?", (session.session_id,)) + session_data = cursor.fetchone() + + conn.close() + + if session_data: + return {"available": True} + else: + return {"available": False} + +@app.post("/check_role") +def check_role(session: Session): + session_data = current_role(session.session_id) + + if session_data: + return {"role": session_data} + else: + return {"role": None} + +@app.websocket("/ws/teacher/open_rooms") +async def websocket_list_open_rooms_teacher(websocket: WebSocket, session_id: Optional[str] = Header(None, alias="session-id")): + await websocket.accept() + + try: + # Get the current user and their role based on session_id + info = get_current_user(session_id) + # Convert info to a dictionary if it's a sqlite3.Row + if isinstance(info, sqlite3.Row): + info = dict(info) # Convert sqlite3.Row to dict + role = current_role(session_id) + teacher_username = info.get("username", "") # Access username from the info dictionary + except Exception as e: + print(f"Session validation error: {e}") + try: + await websocket.send_json({"error": "Session not found"}) + except WebSocketDisconnect: + pass # Ignore if the client has disconnected + await websocket.close(code=4004) + return + + role_final = ''.join([elt for elt in role]) + # Check if the role is 'teacher' + if role_final != "teacher": + try: + await websocket.send_json({"message": "You are not a Teacher"}) + except WebSocketDisconnect: + pass # Ignore if the client has disconnected + await websocket.close(code=1008) + return + + try: + # Fetch and send room data for the teacher + await send_room_data(websocket, teacher_username) + while True: + await asyncio.sleep(2) + await send_room_data(websocket, teacher_username) + except WebSocketDisconnect: + print("WebSocket disconnected") + except Exception as e: + print(f"Unexpected error: {e}") + try: + await websocket.send_json({"error": "An internal server error occurred."}) + except WebSocketDisconnect: + pass # Ignore if the client has disconnected + await websocket.close(code=1011) + +async def send_room_data(websocket: WebSocket, teacher_username: str): + + try: + conn = get_db_connection() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + # Update the query to filter rooms by the teacher's username + cursor.execute(""" + SELECT r.unique_id, r.room_number, r.teacher_name, r.max_students, + COUNT(s.username) AS current_students + FROM rooms r + LEFT JOIN student_joins sj ON r.unique_id = sj.room_unique_id + LEFT JOIN students s ON sj.student_username = s.username + WHERE r.is_open = 1 AND r.teacher_name = ? + GROUP BY r.unique_id + ORDER BY r.room_number + """, (teacher_username,)) + + open_rooms = [] + for row in cursor.fetchall(): + room = dict(row) # Convert the row to a dictionary + room_number = str(room['room_number']) + room_info = room_name_dict.get(room_number, {}) + room.update({ + 'room_number': int(room_number), + 'room_name': room_info.get('room_number', ''), + 'info': room_info.get('info', ''), + 'location': room_info.get('location', ''), + }) + open_rooms.append(room) + + conn.close() + + if open_rooms: + try: + await websocket.send_json({"open_rooms": open_rooms}) + except WebSocketDisconnect: + pass # Ignore if the client has disconnected + else: + try: + await websocket.send_json({"message": "No open rooms available"}) + except WebSocketDisconnect: + pass # Ignore if the client has disconnected + except Exception as e: + print(f"Error fetching or sending room data: {e}") + try: + await websocket.send_json({"error": "Failed to fetch room data"}) + except WebSocketDisconnect: + pass # Ignore if the client has disconnected + +@app.get("/teacher/all_room_information") +def all_room_information(): + return ROOM_NAMES + + +@app.get("/test") +def test(): + conn = get_db_connection() + cursor = conn.cursor() + + # Check if the session ID exists in the sessions table + cursor.execute("SELECT * FROM students WHERE username = ?", ("hi",)) + session_data = cursor.fetchone() + print(session_data) + conn.close() + + if session_data: + print(session_data) # Liste der Fächer ausgeben + return {"subjects": session_data} # Liste der Fächer zurückgeben + else: + return {"error": "Benutzer nicht gefunden oder keine Fächer vorhanden"} + +@app.websocket("/ws/student/test") +async def websocket_get_my_room(websocket: WebSocket, session_id: Optional[str] = Header(None)): + await websocket.accept() + + # Validate session ID + user = get_current_user(session_id) + for i in user: + print(i) + if not user: + await websocket.close(code=403) + return + + # Store connection + active_connections[user['username']] = websocket + + counter = 0 # Initialize counter to 0 + + try: + while True: + # Send incrementing counter value every second + counter += 1 + await websocket.send_text(f"Counter: {counter}") + await asyncio.sleep(1) # Wait 1 second before sending the next message + except WebSocketDisconnect: + del active_connections[user['username']] + print(f"Connection closed for user: {user['username']}") + + +def generate_pdf(file_path): + class PDF(FPDF): + def header(self): + self.set_font("Arial", "B", 16) + today = "01.01.2025" + self.cell(0, 10, f"Attendance Sheet - {today}", ln=True, align="C") + self.ln(10) + + pdf = PDF() + pdf.add_page() + pdf.set_font("Arial", size=12) + + # Column headers + pdf.cell(80, 10, "Namen", border=1, align="C") + pdf.cell(50, 10, "Anwesend", border=1, align="C") + pdf.cell(50, 10, "Abwesend", border=1, align="C") + pdf.ln() + + # Names list + names = [ + "Jasper Grevsmühl", + "Papa Grevsmühl", + "Clara Müller", + "Anna Schmidt", + "Max Mustermann", + ] + + for name in names: + pdf.cell(80, 10, name, border=1) + pdf.cell(50, 10, "", border=1) + pdf.cell(50, 10, "", border=1) + pdf.ln() + + # Save the PDF to the provided file path + pdf.output(file_path) + +# FastAPI route for downloading the PDF +@app.get("/download/{filename}") +def download(filename: str): + # Prepare file path + temp_dir = tempfile.mkdtemp() + pdf_file_path = os.path.join(temp_dir, filename) + + # Generate the PDF file + generate_pdf(pdf_file_path) + + # Return the generated file as a response + return FileResponse(pdf_file_path) \ No newline at end of file