From 4647ba103b4169101857b29ab7976ea00ce25ca2 Mon Sep 17 00:00:00 2001 From: jacobecontreras Date: Wed, 30 Apr 2025 03:56:12 -0700 Subject: [PATCH 1/2] Bug Fixes due to backend refactor --- backend/controller_group.py | 51 ++++++ backend/controller_invite.py | 56 +++++-- backend/controller_task.py | 48 +++++- backend/controller_user.py | 13 +- backend/handler_group.py | 14 ++ backend/handler_invite.py | 29 +++- backend/handler_user.py | 5 +- backend/main.py | 19 ++- roomiebuddy/lib/pages/add_taskpage.dart | 186 ++++++++++++++-------- roomiebuddy/lib/pages/group_page.dart | 32 ++-- roomiebuddy/lib/pages/home_page.dart | 128 +++++++++++---- roomiebuddy/lib/pages/login_screen.dart | 3 +- roomiebuddy/lib/pages/signup_screen.dart | 1 - roomiebuddy/lib/pages/view_taskpage.dart | 2 +- roomiebuddy/lib/services/api_service.dart | 35 ++-- 15 files changed, 454 insertions(+), 168 deletions(-) diff --git a/backend/controller_group.py b/backend/controller_group.py index dc18973..ed715a4 100644 --- a/backend/controller_group.py +++ b/backend/controller_group.py @@ -208,6 +208,57 @@ def get_group_control( } return groups + def get_group_members_control( + self, + user_id: str, + group_id: str, + password: str, + ) -> list[dict]: + """Gets all members of a specific group.""" + if not Validator().check_user_exists(user_id): + raise BackendError("Backend Error: User does not exist", "304") + if not Validator().check_password(user_id, password): + raise BackendError("Backend Error: Password is incorrect", "305") + if not Validator().check_group_exists(group_id): + raise BackendError("Backend Error: Group does not exist", "306") + if not Validator().check_user_in_group(user_id, group_id): + raise BackendError("Backend Error: User not in group", "308") + + with db_operation() as data_cursor: + # Get all users in the group + data_cursor.execute( + """ + SELECT user_id + FROM group_user + WHERE group_id = ? + """, + (group_id,), + ) + user_ids = [row[0] for row in data_cursor.fetchall()] + + if not user_ids: + return [] + + # Get user information for each member + placeholders = ",".join("?" for _ in user_ids) + data_cursor.execute( + f""" + SELECT uuid, username, email + FROM user + WHERE uuid IN ({placeholders}) + """, + user_ids, + ) + members_data = data_cursor.fetchall() + + # Create a list of member dictionaries + members = [ + {"user_id": member_id, "username": member_name, "email": member_email} + for member_id, member_name, member_email in members_data + ] + + return members + if __name__ == "__main__": print("This is a module and should not be run directly.") diff --git a/backend/controller_invite.py b/backend/controller_invite.py index e34eeed..92ed60d 100644 --- a/backend/controller_invite.py +++ b/backend/controller_invite.py @@ -43,7 +43,7 @@ def create_invite_control( with db_operation() as data_cursor: data_cursor.execute( "INSERT INTO group_invites VALUES (?, ?, ?, ?, ?);", - (invite_id, group_id, invitee_id, inviter_id, day_created), + (invite_id, group_id, inviter_id, invitee_id, day_created), ) return invite_id @@ -59,37 +59,59 @@ def get_pending_control( raise BackendError("Backend Error: Password is incorrect", "305") invites: dict[str, dict] = {} with db_operation() as data_cursor: + # 1. Fetch all raw invite data data_cursor.execute( - ( - "SELECT invite_id, inviter_id group_id, created_at " - "FROM group_invites WHERE invitee_id = ?;" - ), + "SELECT invite_id, inviter_id, group_id " + "FROM group_invites WHERE invitee_id = ?;", (user_id,), ) invites_data: list[tuple] = data_cursor.fetchall() - for invite_item in invites_data: - invite_id: str = invite_item[0] - inviter_id: str = invite_item[1] - group_id: str = invite_item[2] - created_at: str = invite_item[3] + + if not invites_data: + return {} # Return early if no invites + + # 2. Collect unique IDs + group_ids = {item[2] for item in invites_data} + inviter_ids = {item[1] for item in invites_data} + + # 3. Fetch group names + group_names: dict[str, str] = {} + if group_ids: + group_placeholders = ",".join("?" * len(group_ids)) data_cursor.execute( - "SELECT name FROM task_group WHERE uuid = ?;", - (group_id), + f"SELECT uuid, name FROM task_group WHERE uuid IN ({group_placeholders});", + list(group_ids), ) - group_name: str = data_cursor.fetchone()[0] + group_names = dict(data_cursor.fetchall()) + + # 4. Fetch inviter names + inviter_names: dict[str, str] = {} + if inviter_ids: + inviter_placeholders = ",".join("?" * len(inviter_ids)) data_cursor.execute( - "SELECT username FROM user WHERE uuid = ?;", - (inviter_id), + f"SELECT uuid, username FROM user WHERE uuid IN ({inviter_placeholders});", + list(inviter_ids), ) - inviter_name: str = data_cursor.fetchone()[0] + inviter_names = dict(data_cursor.fetchall()) + + # 5. Build the final result dictionary + for invite_item in invites_data: + invite_id: str = invite_item[0] + inviter_id: str = invite_item[1] + group_id: str = invite_item[2] + + # Look up names from the fetched dictionaries + group_name: str = group_names.get(group_id, "Unknown Group") + inviter_name: str = inviter_names.get(inviter_id, "Unknown User") + invites[invite_id] = { "invite_id": invite_id, "group_id": group_id, "group_name": group_name, "inviter_id": inviter_id, "inviter_name": inviter_name, - "created_at": created_at, } + return invites def sent_invite_control( diff --git a/backend/controller_task.py b/backend/controller_task.py index 3de37e8..6916700 100644 --- a/backend/controller_task.py +++ b/backend/controller_task.py @@ -93,13 +93,13 @@ def edit_task_control( raise BackendError("Backend Error: User does not exist", "304") if group_id != "0" and not Validator().check_group_exists(group_id): raise BackendError("Backend Error: Group does not exist", "306") - if Validator().check_password(user_id=assigner_id, password=password): + if not Validator().check_password(user_id=assigner_id, password=password): raise BackendError("Backend Error: Password is incorrect", "305") with db_operation() as data_cursor: data_cursor.execute( "UPDATE task SET name = ?, description = ?, due = ?, est_day = ?, " - "est_hour = ?, est_min = ?, assigner_uuid = ?, assign_uuid = ?, group_id = ? " - "recursive = ?, priority = ?, image_path = ? completed = ? " + "est_hour = ?, est_min = ?, assigner_uuid = ?, assign_uuid = ?, group_id = ?, " + "recursive = ?, priority = ?, image_path = ?, completed = ? " "WHERE uuid = ?;", ( task_name, @@ -145,17 +145,29 @@ def get_user_task_control(self, user_id: str, password: str) -> dict[str, dict]: task_list: list[tuple] = data_cursor.fetchall() new_task_list: dict[str, dict] = {} for task in task_list: + # Get assigner username + assigner_username = "Unknown" + with db_operation() as username_cursor: + username_cursor.execute("SELECT username FROM user WHERE uuid = ?;", (task[7],)) + username_result = username_cursor.fetchone() + if username_result: + assigner_username = username_result[0] + new_task_list[task[0]] = { "name": task[1], "description": task[2], - "due": datetime.fromtimestamp(float(task[3])), + "due_timestamp": float(task[3]), "est_day": int(task[4]), "est_hour": int(task[5]), "est_min": int(task[6]), "assigner_id": task[7], + "assigner_username": assigner_username, "assign_id": task[8], "group_id": task[9], "completed": bool(task[10]), + "priority": int(task[11]) if len(task) > 11 else 0, + "recursive": int(task[12]) if len(task) > 12 else 0, + "image_path": task[13] if len(task) > 13 else "", } return new_task_list @@ -182,17 +194,29 @@ def get_group_task_control( task_list: list[tuple] = data_cursor.fetchall() new_task_list: dict[str, dict] = {} for task in task_list: + # Get assigner username + assigner_username = "Unknown" + with db_operation() as username_cursor: + username_cursor.execute("SELECT username FROM user WHERE uuid = ?;", (task[7],)) + username_result = username_cursor.fetchone() + if username_result: + assigner_username = username_result[0] + new_task_list[task[0]] = { "name": task[1], "description": task[2], - "due": datetime.fromtimestamp(float(task[3])), + "due_timestamp": float(task[3]), "est_day": int(task[4]), "est_hour": int(task[5]), "est_min": int(task[6]), "assigner_id": task[7], + "assigner_username": assigner_username, "assign_id": task[8], "group_id": task[9], "completed": bool(task[10]), + "priority": int(task[11]) if len(task) > 11 else 0, + "recursive": int(task[12]) if len(task) > 12 else 0, + "image_path": task[13] if len(task) > 13 else "", } return new_task_list @@ -224,17 +248,29 @@ def get_completed_task_control( task_list: list[tuple] = data_cursor.fetchall() new_task_list: dict[str, dict] = {} for task in task_list: + # Get assigner username + assigner_username = "Unknown" + with db_operation() as username_cursor: + username_cursor.execute("SELECT username FROM user WHERE uuid = ?;", (task[7],)) + username_result = username_cursor.fetchone() + if username_result: + assigner_username = username_result[0] + new_task_list[task[0]] = { "name": task[1], "description": task[2], - "due": datetime.fromtimestamp(float(task[3])), + "due_timestamp": float(task[3]), "est_day": int(task[4]), "est_hour": int(task[5]), "est_min": int(task[6]), "assigner_id": task[7], + "assigner_username": assigner_username, "assign_id": task[8], "group_id": task[9], "completed": bool(task[10]), + "priority": int(task[11]) if len(task) > 11 else 0, + "recursive": int(task[12]) if len(task) > 12 else 0, + "image_path": task[13] if len(task) > 13 else "", } return new_task_list diff --git a/backend/controller_user.py b/backend/controller_user.py index 38a1626..f2a6f11 100644 --- a/backend/controller_user.py +++ b/backend/controller_user.py @@ -36,17 +36,20 @@ def login_user_control( self, email: str, password: str, - ) -> str: + ) -> dict[str, str]: """This will login a user.""" - if Validator().check_login(email=email, password=password): + if not Validator().check_login(email=email, password=password): raise BackendError("Backend Error: Email or Password is incorrect", "303") with db_operation() as data_cursor: data_cursor.execute( - "SELECT uuid FROM user WHERE email = ?;", + "SELECT uuid, username FROM user WHERE email = ?;", (email,), ) - user_id = data_cursor.fetchone()[0] - return user_id + user_data = data_cursor.fetchone() + if user_data: + return {"user_id": user_data[0], "username": user_data[1]} + else: + raise BackendError("Backend Error: User not found after successful check", "500") def edit_user_control( self, diff --git a/backend/handler_group.py b/backend/handler_group.py index 2d59b69..8760328 100644 --- a/backend/handler_group.py +++ b/backend/handler_group.py @@ -76,6 +76,20 @@ def delete_group_request(self) -> None: user_id=user_id, group_id=group_id, password=password ) + @handle_backend_exceptions + def get_group_members_request(self) -> list[dict]: + """Get all members of a specific group.""" + request_data = extract_request_data( + request=self.user_request, + required_fields=["user_id", "group_id", "password"], + ) + user_id = request_data["user_id"] + group_id = request_data["group_id"] + password = request_data["password"] + return GroupController().get_group_members_control( + user_id=user_id, group_id=group_id, password=password + ) + if __name__ == "__main__": print("This is a module and should not be run directly.") diff --git a/backend/handler_invite.py b/backend/handler_invite.py index 81efa70..042f7de 100644 --- a/backend/handler_invite.py +++ b/backend/handler_invite.py @@ -6,7 +6,7 @@ from flask import Request from error import BackendError, handle_backend_exceptions from controller_invite import InviteController -from utils import extract_request_data +from utils import extract_request_data, db_operation class InviteHandle: @@ -51,21 +51,40 @@ def respond_invite_request(self) -> None: request=self.user_request, required_fields=[ "user_id", - "group_id", + "invite_id", + "status", "password", - "accept", ], ) user_id: str = request_data["user_id"] - group_id: str = request_data["group_id"] + invite_id: str = request_data["invite_id"] password: str = request_data["password"] - accept: bool = request_data["accept"] + status: str = request_data["status"] + + # Get group_id from invite_id + group_id = self._get_group_id_from_invite(invite_id, user_id) + + # Convert status to boolean accept + accept = status.lower() == "accepted" + return InviteController().respond_invite_control( user_id=user_id, group_id=group_id, password=password, accept=accept, ) + + def _get_group_id_from_invite(self, invite_id: str, user_id: str) -> str: + """Get group_id from invite_id.""" + with db_operation() as data_cursor: + data_cursor.execute( + "SELECT group_id FROM group_invites WHERE invite_id = ? AND invitee_id = ?;", + (invite_id, user_id), + ) + result = data_cursor.fetchone() + if not result: + raise BackendError("Backend Error: Invite does not exist", "308") + return result[0] @handle_backend_exceptions def get_pending_request(self) -> dict[str, dict[str, Any]]: diff --git a/backend/handler_user.py b/backend/handler_user.py index 6c8fcaf..7d12331 100644 --- a/backend/handler_user.py +++ b/backend/handler_user.py @@ -41,7 +41,7 @@ def add_user_request(self) -> str: return user_id @handle_backend_exceptions - def login_user_request(self) -> str: + def login_user_request(self) -> dict[str, str]: """ "Logs in a user.""" request_data: dict[str, Any] = extract_request_data( request=self.user_request, @@ -52,7 +52,8 @@ def login_user_request(self) -> str: ) email: str = request_data["email"] password: str = request_data["password"] - return UserController().login_user_control(email=email, password=password) + user_info: dict[str, str] = UserController().login_user_control(email=email, password=password) + return user_info @handle_backend_exceptions def edit_user_request(self) -> None: diff --git a/backend/main.py b/backend/main.py index 262a9db..8d874e4 100644 --- a/backend/main.py +++ b/backend/main.py @@ -57,9 +57,14 @@ def handle_signup() -> Response: @error_handling_decorator("login") def handle_login() -> Response: """Login a user.""" - user_id: str = UserHandle(request).login_user_request() + user_info: dict[str, str] = UserHandle(request).login_user_request() + # Extract user_id and username + user_id: str = user_info["user_id"] + username: str = user_info["username"] return jsonify( - [{"error_no": "0", "message": "success", "user_id": user_id}] + # Include both user_id and username in the response + # (Front end expects this and needs it to store user info) + [{"error_no": "0", "message": "success", "user_id": user_id, "username": username}] ) @@ -122,7 +127,7 @@ def handle_get_user_task() -> Response: def handle_get_group_list() -> Response: """Get all groups for a user.""" groups: dict[str, dict[str, Any]] = GroupHandle(request).get_group_list_request() - return jsonify([{"error_no": "0", "message": "success", "group_id": groups}]) + return jsonify([{"error_no": "0", "message": "success", "groups": groups}]) @app.route("/create_group", methods=["POST"]) @@ -149,6 +154,14 @@ def handle_delete_group() -> Response: return jsonify([{"error_no": "0", "message": "success"}]) +@app.route("/get_group_members", methods=["POST"]) +@error_handling_decorator("get_group_members") +def handle_get_group_members() -> Response: + """Get all members of a specific group.""" + members = GroupHandle(request).get_group_members_request() + return jsonify([{"error_no": "0", "message": "success", "members": members}]) + + # ----- Invite Handlers ---- diff --git a/roomiebuddy/lib/pages/add_taskpage.dart b/roomiebuddy/lib/pages/add_taskpage.dart index badda59..f6d464e 100644 --- a/roomiebuddy/lib/pages/add_taskpage.dart +++ b/roomiebuddy/lib/pages/add_taskpage.dart @@ -1,13 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; -import 'dart:convert'; -import 'dart:io'; // Import dart:io for File +import 'dart:io'; import 'package:provider/provider.dart'; import 'package:roomiebuddy/providers/theme_provider.dart'; import 'package:roomiebuddy/utils/data_transformer.dart'; import 'package:roomiebuddy/services/api_service.dart'; import 'package:roomiebuddy/services/auth_storage.dart'; -import 'package:image_picker/image_picker.dart'; // Import image_picker +import 'package:image_picker/image_picker.dart'; class AddTaskpage extends StatefulWidget { const AddTaskpage({super.key}); @@ -17,26 +15,30 @@ class AddTaskpage extends StatefulWidget { } class _AddTaskpageState extends State { + + // TextEditingControllers final TextEditingController _titleController = TextEditingController(); final TextEditingController _descriptionController = TextEditingController(); + final TextEditingController _estDaysController = TextEditingController(); + final TextEditingController _estHoursController = TextEditingController(); + final TextEditingController _estMinsController = TextEditingController(); + + // Task variables String? _selectedGroupId; String? _selectedMemberId; String? _selectedPriority; DateTime? _selectedDate; TimeOfDay? _selectedTime; - final TextEditingController _estDaysController = TextEditingController(); - final TextEditingController _estHoursController = TextEditingController(); - final TextEditingController _estMinsController = TextEditingController(); String? _selectedRecurrence = 'Once'; - - // Add state variable for selected image File? _selectedImage; + // For storing users groups and members in selected group List> _userGroups = []; List> _groupMembers = []; bool _isLoadingGroups = false; bool _isLoadingMembers = false; bool _isSaving = false; + bool _initialDataLoaded = false; String _userId = ""; String _password = ""; @@ -47,10 +49,30 @@ class _AddTaskpageState extends State { @override void initState() { super.initState(); - _loadInitialData(); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (!_initialDataLoaded) { + _loadInitialData(); + _initialDataLoaded = true; + } + } + + @override + void dispose() { + _titleController.dispose(); + _descriptionController.dispose(); + _estDaysController.dispose(); + _estHoursController.dispose(); + _estMinsController.dispose(); + super.dispose(); + } + + // Get the users id and password from the auth storage Future _loadInitialData() async { + setState(() => _isLoadingGroups = true); final userId = await _authStorage.getUserId(); final password = await _authStorage.getPassword(); if (userId != null && password != null) { @@ -58,23 +80,26 @@ class _AddTaskpageState extends State { _userId = userId; _password = password; }); - _loadUserGroups(); + await _loadUserGroups(); + setState(() => _isLoadingGroups = false); } else { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Error: User not logged in.')), - ); - Navigator.of(context).pop(); - } + setState(() => _isLoadingGroups = false); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Error: User not logged in.')), + ); + Navigator.of(context).pop(); + } } } Future _loadUserGroups() async { setState(() => _isLoadingGroups = true); + try { final response = await _apiService.getGroupList(_userId, _password); - if (response['success'] && mounted) { - final groupsMap = response['data']?['message'] as Map? ?? {}; + if (response['success']) { + final groupsMap = response['data']?['groups'] as Map? ?? {}; setState(() { _userGroups = groupsMap.values.map((group) => group as Map).toList(); }); @@ -92,23 +117,37 @@ class _AddTaskpageState extends State { ); } } finally { - if (mounted) { - setState(() => _isLoadingGroups = false); - } + setState(() => _isLoadingGroups = false); } } Future _loadGroupMembers(String groupId) async { setState(() => _isLoadingMembers = true); + try { final response = await _apiService.getGroupMembers(_userId, groupId, _password); - if (response['success'] && mounted) { - setState(() { - _groupMembers = List>.from(response['members']); - }); + if (response['success']) { + final members = response['members']; + if (members is List) { + setState(() { + _groupMembers = List>.from(members.map((member) { + if (member is Map) { + return member; + } else { + return Map.from(member as Map); + } + })); + }); + } else { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Failed to load members: Invalid response format')), + ); + } + } } else { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to load members: ${response['message']}')), ); } @@ -120,12 +159,11 @@ class _AddTaskpageState extends State { ); } } finally { - if (mounted) { - setState(() => _isLoadingMembers = false); - } + setState(() => _isLoadingMembers = false); } } + // For Date Picker Future _selectDate(BuildContext context) async { final themeProvider = Provider.of(context, listen: false); @@ -170,6 +208,7 @@ class _AddTaskpageState extends State { } } + // For Time Picker Future _selectTime(BuildContext context) async { final themeProvider = Provider.of(context, listen: false); @@ -218,10 +257,9 @@ class _AddTaskpageState extends State { } } - // Function to pick an image + // For Image Picker Future _pickImage() async { final ImagePicker picker = ImagePicker(); - // Pick an image from the gallery final XFile? image = await picker.pickImage(source: ImageSource.gallery); if (image != null) { @@ -232,17 +270,19 @@ class _AddTaskpageState extends State { } Future _saveTask() async { + if (!mounted) return; final scaffoldMessenger = ScaffoldMessenger.of(context); - // --- Show SnackBar if image selected but upload not implemented --- + // TODO: Implement image upload if (_selectedImage != null) { scaffoldMessenger.showSnackBar( const SnackBar(content: Text('Image selected, but backend upload is not yet implemented.')), ); - // Optionally, you might want to return here if upload is mandatory - // return; } + // Ensure we have all the required fields + // If the user has not entered certain fields, show a snackbar telling them to enter the next missing field + if (_titleController.text.isEmpty) { scaffoldMessenger.showSnackBar(const SnackBar(content: Text('Please enter a task title.'))); return; @@ -267,28 +307,45 @@ class _AddTaskpageState extends State { scaffoldMessenger.showSnackBar(const SnackBar(content: Text('Please select a due time.'))); return; } + + // Validate that est time inputs contain valid integers + if (_estDaysController.text.isNotEmpty && int.tryParse(_estDaysController.text) == null) { + scaffoldMessenger.showSnackBar(const SnackBar(content: Text('Days must be an integer value.'))); + return; + } + if (_estHoursController.text.isNotEmpty && int.tryParse(_estHoursController.text) == null) { + scaffoldMessenger.showSnackBar(const SnackBar(content: Text('Hours must be an integer value.'))); + return; + } + if (_estMinsController.text.isNotEmpty && int.tryParse(_estMinsController.text) == null) { + scaffoldMessenger.showSnackBar(const SnackBar(content: Text('Minutes must be an integer value.'))); + return; + } + // Convert est time inputs from objects to ints final int estDays = int.tryParse(_estDaysController.text) ?? 0; final int estHours = int.tryParse(_estHoursController.text) ?? 0; final int estMins = int.tryParse(_estMinsController.text) ?? 0; + // Map the recurrence value to an int for backend final Map recurrenceMap = { 'Once': 0, 'Daily': 1, 'Weekly': 2, 'Monthly': 3, }; - final int recurrenceInt = recurrenceMap[_selectedRecurrence ?? 'Once'] ?? 0; + final int recurrenceInt = recurrenceMap[_selectedRecurrence ?? 'Once'] ?? 0; final double? dueTimestamp = dateTimeToTimestamp(_selectedDate, _selectedTime); final int priorityInt = priorityToInt(_selectedPriority); + // If the date/time is invalid, show a snackbar if (dueTimestamp == null) { scaffoldMessenger.showSnackBar(const SnackBar(content: Text('Invalid date/time selected.'))); return; } - setState(() => _isSaving = true); + setState(() => _isSaving = true); // Set saving state to true, shows a loading indicator try { final response = await _apiService.post('/add_task', { @@ -304,13 +361,16 @@ class _AddTaskpageState extends State { 'task_est_hour': estHours, 'task_est_min': estMins, 'recursive': recurrenceInt, - 'image_path': '' + 'image_path': '' // TODO: Implement image upload not via path, but via file }); - if (response['success'] && mounted) { + if (!mounted) return; + + if (response['success']) { scaffoldMessenger.showSnackBar( const SnackBar(content: Text('Task added successfully!'), duration: Duration(seconds: 2)), ); + // Clear all the fields if save was successful setState(() { _titleController.clear(); _descriptionController.clear(); @@ -324,41 +384,26 @@ class _AddTaskpageState extends State { _estHoursController.clear(); _estMinsController.clear(); _selectedRecurrence = 'Once'; - // Clear selected image _selectedImage = null; }); } else { - if (mounted) { - scaffoldMessenger.showSnackBar( - SnackBar(content: Text('Failed to add task: ${response['message']}')), - ); - } + scaffoldMessenger.showSnackBar( + SnackBar(content: Text('Failed to add task: ${response['message']}')), + ); } } catch (e) { - if (mounted) { - scaffoldMessenger.showSnackBar( - SnackBar(content: Text('Error adding task: $e')), - ); - } + if (!mounted) return; + scaffoldMessenger.showSnackBar( + SnackBar(content: Text('Error adding task: $e')), + ); } finally { - if (mounted) { - setState(() => _isSaving = false); - } + setState(() => _isSaving = false); } } - @override - void dispose() { - _titleController.dispose(); - _descriptionController.dispose(); - _estDaysController.dispose(); - _estHoursController.dispose(); - _estMinsController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { + // Theme and general input styling setup final themeProvider = Provider.of(context); final inputDecoration = InputDecoration( border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), @@ -388,6 +433,7 @@ class _AddTaskpageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Task Details Section - Title and Description Text( 'Task Details', style: TextStyle( @@ -409,6 +455,7 @@ class _AddTaskpageState extends State { ), const SizedBox(height: 12), + // Assignment Section - Group and Member Selection Text( 'Assign To', style: TextStyle( @@ -480,6 +527,7 @@ class _AddTaskpageState extends State { ), const SizedBox(height: 12), + // Priority Section - Task Priority Selection Text( 'Priority', style: TextStyle( @@ -510,6 +558,7 @@ class _AddTaskpageState extends State { ), const SizedBox(height: 12), + // Due Date & Time Section - Calendar and Time Pickers Text( 'Due Date & Time', style: TextStyle( @@ -554,6 +603,7 @@ class _AddTaskpageState extends State { ), const SizedBox(height: 12), + // Estimated Duration Section - Days, Hours, Minutes inputs Text( 'Estimated Duration', style: TextStyle( @@ -592,6 +642,7 @@ class _AddTaskpageState extends State { ), const SizedBox(height: 12), + // Recurrence Section - Task repetition pattern Text( 'Recurrence', style: TextStyle( @@ -621,25 +672,26 @@ class _AddTaskpageState extends State { }); }, ), - const SizedBox(height: 16), // Adjusted spacing slightly + const SizedBox(height: 16), + // Action Buttons - Add Photo and Save Task Row( children: [ ElevatedButton.icon( style: ElevatedButton.styleFrom( - backgroundColor: themeProvider.themeColor.withOpacity(0.8), + backgroundColor: themeProvider.themeColor, foregroundColor: themeProvider.currentTextColor, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), ), - onPressed: _pickImage, // Call _pickImage on press + onPressed: _pickImage, // Change icon based on whether an image is selected icon: Icon( _selectedImage == null ? Icons.add_a_photo : Icons.check_circle_outline, size: 18, color: _selectedImage == null ? themeProvider.currentTextColor - : Colors.green, // Indicate success with green check + : Colors.green, // Indicate image uploaded with green check ), label: const Text('Add Photo'), ), diff --git a/roomiebuddy/lib/pages/group_page.dart b/roomiebuddy/lib/pages/group_page.dart index cd3a237..98980a3 100644 --- a/roomiebuddy/lib/pages/group_page.dart +++ b/roomiebuddy/lib/pages/group_page.dart @@ -12,22 +12,24 @@ class GroupPage extends StatefulWidget { } class _GroupPageState extends State { + // Text editing controllers final _userIdController = TextEditingController(); final _createGroupNameController = TextEditingController(); - - final ApiService _apiService = ApiService(); - final AuthStorage _authStorage = AuthStorage(); - - final Map _loadingInvites = {}; + // User data variables + String? _selectedGroupId; String _userId = ""; String _password = ""; - List> _userGroups = []; - String? _selectedGroupId; - List> _pendingInvites = []; String _errorMessage = ""; bool _isLoading = true; bool _isSubmitting = false; + List> _userGroups = []; + List> _pendingInvites = []; + final Map _loadingInvites = {}; + + // Services + final ApiService _apiService = ApiService(); + final AuthStorage _authStorage = AuthStorage(); @override void initState() { @@ -53,7 +55,7 @@ class _GroupPageState extends State { final userId = await _authStorage.getUserId(); final password = await _authStorage.getPassword(); - // Ensure necessary credentials exist + // Ensure credentials exist if (userId == null || password == null) { setState(() { _isLoading = false; @@ -71,7 +73,7 @@ class _GroupPageState extends State { _errorMessage = "Failed to load groups: ${groupsResponse['message']}"; }); } else { - final groups = groupsResponse['message'] as Map; + final groups = groupsResponse['data']?['groups'] as Map? ?? {}; _userGroups = groups.values.map((group) => group as Map).toList(); _selectedGroupId = null; } @@ -80,10 +82,12 @@ class _GroupPageState extends State { final invitesResponse = await _apiService.getPendingInvites(userId, password); if (!invitesResponse['success']) { setState(() { - _errorMessage = "Failed to load invites: ${invitesResponse['message']}"; + _errorMessage = _errorMessage.isNotEmpty + ? "$_errorMessage\nFailed to load invites: ${invitesResponse['message']}" + : "Failed to load invites: ${invitesResponse['message']}"; }); } else { - final invites = invitesResponse['message'] as Map; + final invites = invitesResponse['data']?['invites'] as Map? ?? {}; _pendingInvites = invites.values.map((invite) => invite as Map).toList(); } } catch (e) { @@ -133,7 +137,7 @@ class _GroupPageState extends State { try { final groupsResponse = await _apiService.getGroupList(_userId, _password); if (groupsResponse['success']) { - final groups = groupsResponse['message'] as Map; + final groups = groupsResponse['data']?['groups'] as Map? ?? {}; if(mounted){ setState(() { _userGroups = groups.values.map((group) => group as Map).toList(); @@ -260,7 +264,7 @@ class _GroupPageState extends State { try { final groupsResponse = await _apiService.getGroupList(_userId, _password); if (groupsResponse['success']) { - final groups = groupsResponse['message'] as Map; + final groups = groupsResponse['data']?['groups'] as Map? ?? {}; setState(() { _userGroups = groups.values.map((group) => group as Map).toList(); }); diff --git a/roomiebuddy/lib/pages/home_page.dart b/roomiebuddy/lib/pages/home_page.dart index 380bb36..6ccf2b0 100644 --- a/roomiebuddy/lib/pages/home_page.dart +++ b/roomiebuddy/lib/pages/home_page.dart @@ -6,6 +6,7 @@ import 'package:provider/provider.dart'; import '../common/widget/appbar/appbar.dart'; import '../providers/theme_provider.dart'; import 'package:roomiebuddy/services/auth_storage.dart'; +import 'package:roomiebuddy/services/api_service.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); @@ -15,18 +16,12 @@ class HomePage extends StatefulWidget { } class HomePageState extends State { - final String _selectedCategory = 'Today'; bool _isLoading = false; List> _tasks = []; - TextEditingController _searchController = TextEditingController(); - - final List> roommateGroups = [ - {"groupName": "Room 101", "members": ["Alice", "Bob", "Charlie"]}, - {"groupName": "Kitchen Crew", "members": ["Dana", "Eli"]}, - {"groupName": "Laundry Legends", "members": ["Fred", "Gina", "Harry"]}, - ]; + List> _roommateGroups = []; final AuthStorage _authStorage = AuthStorage(); + final ApiService _apiService = ApiService(); String? _userId; String? _password; String _userName = "User"; @@ -46,9 +41,11 @@ class HomePageState extends State { _userName = await _authStorage.getUsername() ?? "User"; if (_userId != null && _password != null) { - await fetchTasks(_userId!, _password!); + await Future.wait([ + _loadTasks(_userId!, _password!), + _loadGroups(_userId!, _password!), + ]); } else { - print("User not logged in."); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Please log in to view tasks.')), @@ -58,6 +55,50 @@ class HomePageState extends State { } } + Future _loadGroups(String userId, String password) async { + try { + final response = await _apiService.getGroupList(userId, password); + if (response['success']) { + final groupsMap = response['data']?['groups'] as Map? ?? {}; + if (mounted) { + setState(() { + _roommateGroups = groupsMap.values.map((rawGroup) { + final group = Map.from(rawGroup as Map); + + final List membersData = group['members'] ?? []; + final List> processedMembers = membersData.map((member) { + if (member is Map) { + return member; + } else if (member is Map) { + return Map.from(member); + } else { + return {'user_id': 'unknown', 'username': 'Invalid Member Data'}; + } + }).toList(); + return { + ...group, + 'members': processedMembers, + 'group_name': group['name'] ?? 'Unnamed Group' + }; + }).toList(); + }); + } + } else { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to load groups: ${response['message']}')), + ); + } + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error loading groups: $e')), + ); + } + } + } + String _getGreeting() { final hour = DateTime.now().hour; if (hour < 12) return "Good Morning"; @@ -65,7 +106,7 @@ class HomePageState extends State { return "Good Evening"; } - Future fetchTasks(String userId, String password) async { + Future _loadTasks(String userId, String password) async { if (!mounted) return; setState(() { _isLoading = true; @@ -108,7 +149,7 @@ class HomePageState extends State { return { "id": taskId, "taskName": taskData["name"] ?? "No Task Name", - "assignedBy": taskData["assigner_id"] ?? "Unknown", + "assignedBy": taskData["assigner_username"] ?? taskData["assigner_id"] ?? "Unknown", "priority": priorityStr, "description": taskData["description"] ?? "", "dueDate": dueDateStr, @@ -122,7 +163,6 @@ class HomePageState extends State { }); } } else { - print('Backend error: ${firstItem['message']}'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to load tasks: ${firstItem['message']}')), @@ -130,7 +170,6 @@ class HomePageState extends State { } } } else { - print('Unexpected response format from /get_user_task'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Failed to load tasks: Invalid server response.')), @@ -138,7 +177,6 @@ class HomePageState extends State { } } } else { - print('HTTP error ${response.statusCode}'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to load tasks: Server error ${response.statusCode}')), @@ -146,7 +184,6 @@ class HomePageState extends State { } } } catch (e) { - print('Error fetching tasks: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error fetching tasks: $e')), @@ -232,8 +269,7 @@ class HomePageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - color: themeProvider.themeColor, - padding: const EdgeInsets.only(bottom: 24), + color: Theme.of(context).scaffoldBackgroundColor, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -250,9 +286,18 @@ class HomePageState extends State { ), ), Container( - color: Colors.white.withOpacity(0.05), + color: themeProvider.themeColor, height: 180, - child: _buildGroupCarousel(), + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _roommateGroups.isEmpty + ? Center( + child: Text( + 'No groups found.', + style: TextStyle(color: themeProvider.currentSecondaryTextColor), + ), + ) + : _buildGroupCarousel(), ), ], ), @@ -331,6 +376,10 @@ class HomePageState extends State { Widget _buildGroupCarousel() { final themeProvider = Provider.of(context); + if (_roommateGroups.isEmpty) { + return const Center(child: Text('No groups to display.')); + } + return SizedBox( height: 140, child: CarouselSlider( @@ -338,9 +387,14 @@ class HomePageState extends State { height: 140.0, enlargeCenterPage: true, autoPlay: false, - viewportFraction: 0.85, + viewportFraction: 0.6, ), - items: roommateGroups.map((group) { + items: _roommateGroups.map((group) { + final List membersData = group['members'] ?? []; + final List memberNames = membersData + .map((member) => member['username'] as String? ?? 'Unknown') + .toList(); + return Builder( builder: (BuildContext context) { return GestureDetector( @@ -353,13 +407,15 @@ class HomePageState extends State { ); }, child: Container( + width: MediaQuery.of(context).size.width * 0.6, + height: 120, margin: const EdgeInsets.symmetric(horizontal: 5), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: themeProvider.currentCardBackground, borderRadius: BorderRadius.circular(15), border: Border.all(color: themeProvider.themeColor), - boxShadow: [ + boxShadow: const [ BoxShadow( color: Colors.black12, blurRadius: 5, @@ -371,7 +427,7 @@ class HomePageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - group['groupName'], + group['group_name'] ?? 'Unnamed Group', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, @@ -380,11 +436,13 @@ class HomePageState extends State { ), const SizedBox(height: 8), Text( - "Members: ${group['members'].join(', ')}", + "Members: ${memberNames.join(', ')}", style: TextStyle( fontSize: 16, color: themeProvider.currentSecondaryTextColor, ), + overflow: TextOverflow.ellipsis, + maxLines: 2, ), ], ), @@ -442,18 +500,28 @@ class GroupDetailScreen extends StatelessWidget { @override Widget build(BuildContext context) { + final List membersData = group['members'] ?? []; + final List memberNames = membersData + .map((member) => member['username'] as String? ?? 'Unknown') + .toList(); + return Scaffold( - appBar: AppBar(title: Text(group['groupName'])), + appBar: AppBar(title: Text(group['group_name'] ?? 'Group Details')), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text("Group Name: ${group['groupName']}", - style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), + Text( + "Group Name: ${group['group_name'] ?? 'Unnamed Group'}", + style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold) + ), const SizedBox(height: 20), - Text("Members:", style: const TextStyle(fontSize: 20)), - ...List.from(group['members'].map((m) => Text("- $m"))), + const Text("Members:", style: TextStyle(fontSize: 20)), + ...memberNames.map((name) => Padding( + padding: const EdgeInsets.only(left: 8.0, top: 4.0), + child: Text("- $name"), + )), ], ), ), diff --git a/roomiebuddy/lib/pages/login_screen.dart b/roomiebuddy/lib/pages/login_screen.dart index d5033d5..7ddfcbc 100644 --- a/roomiebuddy/lib/pages/login_screen.dart +++ b/roomiebuddy/lib/pages/login_screen.dart @@ -125,9 +125,8 @@ class _LoginScreenState extends State { // Navigate to main screen _navigateToMainScreen(); } else { - // Show error message setState(() { - _errorMessage = 'Incorrect email/password or account does not exist'; + _errorMessage = result['message'] ?? 'An unknown error occurred.'; }); } } catch (e) { diff --git a/roomiebuddy/lib/pages/signup_screen.dart b/roomiebuddy/lib/pages/signup_screen.dart index 06ae098..9846491 100644 --- a/roomiebuddy/lib/pages/signup_screen.dart +++ b/roomiebuddy/lib/pages/signup_screen.dart @@ -246,7 +246,6 @@ class _SignupScreenState extends State { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Registration successful! Please login.'), - backgroundColor: Colors.green, ), ); diff --git a/roomiebuddy/lib/pages/view_taskpage.dart b/roomiebuddy/lib/pages/view_taskpage.dart index 3a83e87..876e163 100644 --- a/roomiebuddy/lib/pages/view_taskpage.dart +++ b/roomiebuddy/lib/pages/view_taskpage.dart @@ -10,6 +10,6 @@ class ViewTaskpage extends StatefulWidget { class _ViewTaskpageState extends State { @override Widget build(BuildContext context) { - return Scaffold(); + return const Scaffold(); } } \ No newline at end of file diff --git a/roomiebuddy/lib/services/api_service.dart b/roomiebuddy/lib/services/api_service.dart index 4aad97b..c82cd67 100644 --- a/roomiebuddy/lib/services/api_service.dart +++ b/roomiebuddy/lib/services/api_service.dart @@ -126,10 +126,6 @@ class ApiService { // **** NEW METHOD: Get members of a specific group **** Future> getGroupMembers(String userId, String groupId, String password) async { - // Note: The backend returns { error_no: 0, message: success, members: [...] } - // The _handleResponse expects a list as the top-level JSON object. - // We need to adjust how we handle this specific response or generalize _handleResponse. - // For now, we'll make a direct call and parse differently. try { final response = await _client.post( Uri.parse('$baseUrl/get_group_members'), @@ -142,17 +138,26 @@ class ApiService { ).timeout(const Duration(seconds: 10)); if (response.statusCode >= 200 && response.statusCode < 300) { - Map responseData = jsonDecode(response.body); - if (responseData['error_no'] == '0') { - return { - 'success': true, - 'members': responseData['members'] ?? [], // Return members list - 'message': responseData['message'], - }; + // The response is a JSON array with a single object + List responseList = jsonDecode(response.body); + if (responseList.isNotEmpty) { + Map firstItem = responseList[0]; + if (firstItem['error_no'] == '0') { + return { + 'success': true, + 'members': firstItem['members'] ?? [], // Return members list + 'message': firstItem['message'], + }; + } else { + return { + 'success': false, + 'message': firstItem['message'] ?? 'Unknown backend error', + }; + } } else { return { 'success': false, - 'message': responseData['message'] ?? 'Unknown backend error', + 'message': 'Empty response from server', }; } } else { @@ -225,7 +230,7 @@ class ApiService { String groupId, String password ) async { - return await post('/invite_to_group', { + return await post('/create_invite', { 'inviter_id': inviterId, 'invitee_id': inviteeId, 'group_id': groupId, @@ -235,7 +240,7 @@ class ApiService { // Get pending group invites for a user Future> getPendingInvites(String userId, String password) async { - return await post('/get_pending_invites', { + return await post('/get_pending', { 'user_id': userId, 'password': password, }); @@ -248,7 +253,7 @@ class ApiService { String status, String password ) async { - return await post('/respond_to_invite', { + return await post('/respond_invite', { 'user_id': userId, 'invite_id': inviteId, 'status': status, // 'accepted' or 'rejected' From bf4f671eac43ea6224e81407e33fd55dbcc9e29c Mon Sep 17 00:00:00 2001 From: Airun_Iru Date: Wed, 30 Apr 2025 12:51:53 -0700 Subject: [PATCH 2/2] merged with backend update --- backend/controller_task.py | 29 ++++++++++++++++++++++----- backend/handler_task.py | 10 +++++++++ backend/main.py | 16 +++++++++++++-- backend_new.zip | Bin 13411 -> 0 bytes documentation/backend_error_code.txt | 10 ++++++++- roomiebuddy/.metadata | 25 +++++------------------ 6 files changed, 62 insertions(+), 28 deletions(-) delete mode 100644 backend_new.zip diff --git a/backend/controller_task.py b/backend/controller_task.py index 6cd0f0e..9aab959 100644 --- a/backend/controller_task.py +++ b/backend/controller_task.py @@ -164,9 +164,15 @@ def get_user_task_control(self, user_id: str, password: str) -> dict[str, dict]: "assign_id": task[8], "group_id": task[9], "completed": bool(task[10]), +<<<<<<< Updated upstream "priority": int(task[11]) if len(task) > 11 else 0, "recursive": int(task[12]) if len(task) > 12 else 0, "image_path": task[13] if len(task) > 13 else "", +======= + "priority": int(task[11]), + "recursive": int(task[12]), + "image_path": task[13], +>>>>>>> Stashed changes } return new_task_list @@ -213,9 +219,15 @@ def get_group_task_control( "assign_id": task[8], "group_id": task[9], "completed": bool(task[10]), +<<<<<<< Updated upstream "priority": int(task[11]) if len(task) > 11 else 0, "recursive": int(task[12]) if len(task) > 12 else 0, "image_path": task[13] if len(task) > 13 else "", +======= + "priority": int(task[11]), + "recursive": int(task[12]), + "image_path": task[13], +>>>>>>> Stashed changes } return new_task_list @@ -267,9 +279,15 @@ def get_completed_task_control( "assign_id": task[8], "group_id": task[9], "completed": bool(task[10]), +<<<<<<< Updated upstream "priority": int(task[11]) if len(task) > 11 else 0, "recursive": int(task[12]) if len(task) > 12 else 0, "image_path": task[13] if len(task) > 13 else "", +======= + "priority": int(task[11]), + "recursive": int(task[12]), + "image_path": task[13], +>>>>>>> Stashed changes } return new_task_list @@ -277,7 +295,6 @@ def toggle_complete_task_control( self, task_id: str, user_id: str, - group_id: str, password: str, completed: int, ) -> None: @@ -286,10 +303,6 @@ def toggle_complete_task_control( raise BackendError("Backend Error: User does not exist", "304") if not Validator().check_password(user_id=user_id, password=password): raise BackendError("Backend Error: Password is incorrect", "305") - if not Validator().check_group_exists(group_id=group_id): - raise BackendError("Backend Error: Group does not exist", "306") - if not Validator().check_user_in_group(user_id=user_id, group_id=group_id): - raise BackendError("Backend Error: User is not in the group", "310") if not Validator().check_task_exists(task_id=task_id): raise BackendError("Backend Error: Task does not exist", "309") with db_operation() as data_cursor: @@ -301,6 +314,12 @@ def toggle_complete_task_control( ), ) + def get_image_control(self, image_path: str) -> str: + """This will get the image.""" + if not exists(image_path): + raise BackendError("Backend Error: Image does not exist", "313") + return image_path + if __name__ == "__main__": print("This module is not intended to be run directly.") diff --git a/backend/handler_task.py b/backend/handler_task.py index a5b4c35..6be8027 100644 --- a/backend/handler_task.py +++ b/backend/handler_task.py @@ -86,6 +86,16 @@ def get_user_task_request(self) -> dict[str, dict[str, Any]]: user_id=user_id, password=password ) + @handle_backend_exceptions + def get_image_request(self) -> str: + """Gets the image.""" + request_data: dict[str, Any] = extract_request_data( + request=self.user_request, + required_fields=["image_path"], + ) + image_path: str = request_data["image_path"] + return TaskController().get_image_control(image_path=image_path) + if __name__ == "__main__": print("This is a module and should not be run directly.") diff --git a/backend/main.py b/backend/main.py index 8d874e4..b5b21a7 100644 --- a/backend/main.py +++ b/backend/main.py @@ -4,7 +4,7 @@ # from os.path import join from typing import Any -from flask import Flask, request, jsonify, Response +from flask import Flask, request, jsonify, Response, send_file # from werkzeug.utils import secure_filename from validator import Validator @@ -57,6 +57,7 @@ def handle_signup() -> Response: @error_handling_decorator("login") def handle_login() -> Response: """Login a user.""" +<<<<<<< Updated upstream user_info: dict[str, str] = UserHandle(request).login_user_request() # Extract user_id and username user_id: str = user_info["user_id"] @@ -66,6 +67,10 @@ def handle_login() -> Response: # (Front end expects this and needs it to store user info) [{"error_no": "0", "message": "success", "user_id": user_id, "username": username}] ) +======= + user_id: str = UserHandle(request).login_user_request() + return jsonify([{"error_no": "0", "message": "success", "user_id": user_id}]) +>>>>>>> Stashed changes @app.route("/edit_user", methods=["POST"]) @@ -116,7 +121,14 @@ def handle_delete_task() -> Response: def handle_get_user_task() -> Response: """Get all tasks for a user.""" tasks: dict[str, dict[str, Any]] = TaskHandle(request).get_user_task_request() - return jsonify([{"error_no": "0", "message": tasks}]) + return jsonify([{"error_no": "0", "message": "success", "tasks": tasks}]) + + +@app.route("/get_image", methods=["POST"]) +@error_handling_decorator("get_image") +def handle_get_image() -> Response: + """Get an image.""" + return send_file(TaskHandle(request).get_image_request(), mimetype="image/jpeg") # ----- Group Handlers ---- diff --git a/backend_new.zip b/backend_new.zip deleted file mode 100644 index d5fcfdf814deac9e4b20a70d84f5c22434e54de1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13411 zcma)@1DGV;vaZ|Kv~AnQ%(QLWwmEIv*0gQgw(ag}bK2c^_P+Z+XZ~~ddG5}ttgNbM zWxf?Fqat%fd`nIe6bu#UAIq7Ksm4Fw{Obkk_trq)(9+n(h(YdOt)TwdDt5PHR~7^a zCpCnO~`t-KB(MnMfshbcAv8>X`$0e)E z$%g5h`j>vhTo@H-6hoJnX>EJD`dn3!2PWd^=gm6Pe8CS))SJP&f%GfKu9%0&JBsdK zJAv3JdUYPAJIc-^y;=E4?%QPwH7Sn@ke~@l!s+i=hpK$*VpGXrk~3@&k9YrON@h%^ zom7$O&HoccccKS%4(yFs8`l5|y||`>(ocs@fYo0Tg)YB;(ZyefPVk%uv=nQEU9FC;CfbB?vV4usDB-6fNPFkDVhxNctj@70Y3un zX_L9j&f$0oKS<`PyHWRdh@(Ts%XWW{;nqQYzUWtD0 z_8GXQ!lC7-By1__?v&bW3D42GBTU_VDPPj}(DRBxJJa*-CM~-sV^*JK;bz#S%S6XG zo^3>lKV(}Q3$>QZ=yie_W|C(wS;5ZBG;LN6TB_2da-F$eLsXxbReuS-SN&#gzk{$= z(3EO2U6Hrdt;R)`f)4Km7)D{=k}Aa8AS#>jYizNFe@WYF7n0GjKh-6wQ>!wQ5B}DE za{21$;v%jZiyx)m@QvzW#1_9H?qOtFh4e#pPmpJKnL|SksqyM`R=*5dOn)=VvKiU7 zqqw;lzggEXs%p)Ssvin&3UXlPc&>rrr94A~Q`T$p`I@&uFMn!128U4M`bqKydX5l_GONq%e#8$1IbK4L~hwBAqN-8m*gY2400NE+BAyz z*A~S%*5p6LD>4Iyx2izgw{~7B=Sjt5#h3w+WPU?i61hgSSH9*{8UFCa%Hmvch-CGm zqj`Cv7!1ObD~`E_TG}M(dUHXaJ_D9zq(&+jjRfzo%Lm)6tVM%@SZdu$U4jzqs8XF0{Dp{ z?1(P2&)hF}jI)F2TT=6k4$%Y8JquKOQEi8lD~7D^FN+f>Wk?nm3I2W(`sUZAcz*x= zA2EP{5dR1P*81i)zXQO9+9<;B0ML0%6WLmzKsgRA3fME6m%diXM)q*$4KfMvJ4LG*lht@b!yTuu*ne9=ZqA z?y|aFZtuZ19lT90;|^3JqYd0$)*RDq5M&(43O|>=hyr_9UVnHBv|bqzxmdI2%aY9y zWQ4M|N@Ujs_;X{?6Zx2xFWer)MzIcp_87$9Ns+klXw(Zvu{#k-E1auDhRq>c^ISbP z%+E&I<^+t9GO#fW1g_5*9cX{^03kQ*3&PS~*@h88f9cJHq%RZy_!>%&kPT!KxdHfI zHMBDP{p6k5?rbg>u^?i1aaf#amE=V=Wvu9&TL%9mtS& zj7jmC9}Xnsxlx2)igEh`=68l^c5b?HmwiSdop|da)NSaBAl{r7WDkuttfvY0r(aDi zG8vjljqEHZZHhmKVSO6742__``;D9j@+8-uw!$G)#Sd~WMg(l)TtV`EZkHg$%w1?R zV6nQ?>+ZmQoKJ>f5!IWRNEv6KP-Rn4AIysF* zA5B)*d39ewpO=3)JiJN{{ct!unnlaIA!iQEdye{0to~d$vp98*wy*T(LJlgf^B!yr zp_7h@j9?uUJRTC(U6joqI9WOjTiWk1&z2C>zJi_XTePT|*Y;h`ZG^sVYKW<B7z_V8_I*#ehR(pl1p1W7 z4sWZi9mWUb?{Vh*C1zjucYLw_y<+|mXB>>}oy{GLt&MG*9O<3hoXEy%VSEXZe0SgR zj~Gcxc9(*4OY@$~XryXqv^-wmhCh!rGjCKtLmQc;K?4D)iu^tJIq5rE z{v-Hl0iu5gzmM%=##I8=lEhaHr5O-lm9G&Vc2Iegte5P>(h>-IL2j<+wT3+Q$nh@Y ztBV%q!~3;-EZc6p_MBhuW*+<#1QW?4BAR)E^`%mDBA1Fsnc-&o?wDTop85N#QgV&L zaKLMiV%5BMvXkF4Pr^CYT;f2OKXEg(pD4 z%QXj##PqIxVa?kgHseeuKAXHT;`TGfj4X$}Endo?^5Ek4v1j#($X2)Yq_k7P9GS7@ z^!(@ual^nEz8le;#{v~aY^4%0YB=U*h(=j=mYKbgLlY_;XN+R5+cG@_4-%n>?o0p@ zZAz`#O1MdM$Q06Vt2*OBRJA0aSecBn++yQ4DLP#pz;0G{#@;W{S?p2xY184r+r836 zF8PiuGB|ip^lHSL-f6|7J2S8TBJvc3Jvm;koO3KPW-OwtJVDtOM}WPXk^l0isAxaM(i3hJA|X+j zOc>=a(Aj=+rX9In0rfktuww#iq!~8Y2_zgSfj3DvmM`8*ea|<`TsjoA{zx-~R@Hkb zTs8-8OeO;|#f0rK-FxooJ5@I$9yVXH1n^^Ydg3p!O;}9Ij)x}nDqJ?2xP6trwrIGE zH;X;=bIuzCCa}V^RH9((pVXaN!D$I+WT(;91FX#&0w!8#B7T~pr%w!l5t7{7-R}hn z>#=)#@P7NbiW(J6OOIlg^%HM=BU4;UR43L6zU_oYt26qHnJ&zI#+F_9vnN&ILJGh? zH_k!xMX#qp+y`%lobkR9feK%Xe&(quZ?-;4&~nE!__{cuHWPDJLNR*<>>jBJ~hj5#TJ)7tdsbLb(Ut_w~5!;}Iu`CmaTNMen=C zy?mGpAo4OLhqDHoL!{VXF$(!WloAl$3Szzp^b$KM$hnc{QrZLGzm&2y?OY2BZFXDU z%cV>=o2G9lv(3}lG3#=7ZL>+UV&;xpF@+c5^Y}z6FG~MJJzjt?sanRVX;8tvU#mUR zi)3E>)<5Me3i_?{lv31ka#n^%1s5-0$~Ljfv5~18A@z){=d=hPswLlCh7j*|0+l*h z8;d>7XH_CKe0KKwizyQK^nq9lEJ6a?H)pBTfiLYT_T>Dku1=MlKjIgOPnSIEwZpNGI#T6z39yG2hA_e* z=R7fb7;8M^f-~s#ufAAOY5EO2#Qdd4i5GqO@eAZHnIxBu=G0J*6fU)TThRpi!+dD# zx+I&ph~gRog@fH+<|oR39^4>ex?`ZsaW_|>$KFWy{iZlYRsoFj@ephf{qjS z86r?3J7DNHIFx97+S$1~L$?pWtxBr-t}RrLgyGd+KnAQYckn zE#I8aPJyA zy;U1Gt2|6GUE#qLJBqUyjfVu1DZ^KF-K(hhV3x9rb&HO{pOBG}nFZ}|;`bbY$H$MV z9IijktoA5^w5XU<5cywq!MEL!)@E)+2Ucj*ig$q zko>54G_M4YYU#A}7iy0UiduvFN~=_(2llF&X=1zyBo7bt86GC~8T$gsGFrPi@&}pC zOMA0?b<^dTw#^w9Z=Qk=caRpIjH-X;#F4QN!<53L7Fw0Qiv3cmyx-TZioWSoDnIf7 z&3t{S!Nd7+KGF|#G1h5l4hcbxS8|>WD?r>^&t0?o?$w?DHhds^Qka*xS{s4%2ghyq z(DNtz8GLucHW>3gpu9@sfZenU#*2eMV$goK4B@@t=);)9v7so@j^S-aoH@TE&{6D{ zl6bPK9cQN-xM=+1J}rTqf~H8Cmb)%*mzBHzp{o$iE|7EXF*g;EZmaZcuA{N4d))no zZwWG&-#R-)x~5J)VSNc~nBI6Lg7fvg3@>B~3i#MB3T|rQNk~f{hK$9ewe#9$8QLXV z=YizLA`OBtjn#Z$Rd%#m8cmpMI!nq}vQV-ItEu?=8xf+eFu5C9VP5<7uRjztT4YN$ znH)OZZ=)srWv?Du#KA*bsK(}g^c&6zFzJ?cz{IY$z4;_L;k_J966OEcCZlp!-{;e3 zkPXk3_3d7%)9qbrxN5%Y5zU^e9d+|#Mqo8J*qW8SqVEp5X>C{;G|^t>bB%9#n-J03nQa-1gBml1QX**p_E+?^Pzn z!A1^Vx9VWi$#5voX@kLqnMu~4#T(#75n=~4wdz!*rE+}N$xBH6g~;|B8Eene%-j*- zqpfI^NvSIKC+mepCIywYCPsH)Y?NbVW`9x;Yg!1vNktUYATJog04K-czGvk=Fo6vA zm9P~tz8yOLyBCq14K7+xPd7FHN2$1S0Ac)iRVB$J`qhyH16JGlO1K7lW+^YCYi)(x z=|<9$k+ZWQIR+nI$Ina+XEuVwye8>pUeEReFXQu;e)4KBO8RQAbG4b3$FpGmRJ%Be zRvO(_&~LtJ_Yh%ShwDJj{O*zG;0H*oXQ^73mp4B7x3dy`C;ZCq91o+~Bzt-HizAfq zGw#&Y-TZvp6ff7vx6Iz%vMC>+f8E9Xzqzh|WWfH)b?NHbxf|*mni=cr{*SyDGEKQ? z#_vHdEhG>S_5ZTRzj9!PcJ5AQwl;JuOw7OU2n~aj#$({VVOIN0-k&$9Uzx7lhO$|i zR0t1bEUAs_6?GoX0WRp90FaBDE~fra2xkM*nJwW@IAXZkE|X(b(Sd6%tk07J>r$z;mR?ML8WT@xXh1t)uyS*#S1itA5Q%~Du{Kn; zO0Tp8+}T)bP`@;r;F(}*obkVHvgHxDVSZCYbaF%XM1XraE!Wu;O?yP8oD{Y1choil zlaJ8Cpq-8~a0vBA6!rJ74H*wP?5Gn|&TTV1_5pXf3T_RcR*H7TSO9^GCFns*hHiH( zs}mjV7ks`1<+-KMi*d*gP=oZBDWM)-Z0jf@FXj-QKaEW^XWuifvnM@otGIBx zQaWRF`BDl-gJLgmly3u^bld#Lm@9Dv7K(kX$p+dH1J)o#+~*$A#{ttBLDC*MOIMGy z2$@;X@f*$lXI9AoJvi|C0s|n!J(yLRn^JH8>wMo#0J~rMqPG$_r`SzcmBjSD_W#zU)#tWiahXyMU8! zLbYCwV;XC@RMm6KY$_@5pDppzk~d{XPUVSV^d7;h3$BcpV!~^->Fu&mV*TY?EjB1@ z^493ql*&sC8w^bZo26n5B*{U}OshkfYXAetX?D6BUPYk`rk7YG|SuaTR0@Dmj zu>wY`m7_zq-_Y1(#w^n;#od`&h-4}D_6MO`6FfWOlXzN&@j#MzxbkC!q#d2c7aMNV z*F2?tJoaZVeNC-}G?7B+t%`LD$}X@<;MDvMv&W2>&Dq1_MEM46=;?&*rCIm{=kr^B zOf`WlC|?m~%Y3g-!LHng30~J>`eEp3{4#;<)0< z!L(R?iQE>+e0p|c`u2qIS5W-FsePHe69%2%1MLVIARx;BCB*%6nEO8h+=Z686^=0K z)_nUJOLW$e+4rWe;v0du(v*pIJ(A^$+eRWcMN-iv<VIvaG&nG=%x?Yj(!Lay2A>$Unz%dntm`FC z9-$?9>?|YufL^ngA&jg;b#NC#&kgrhr(GNEEE=k_5RBP^_yla>5Auyt4GVgZi01%x zqd4;)ti!#DysvU2p7KJwNrQKw@8GHSSc2=+Bm3T?KQ)%t%~jbb$OP*`zH$348;@P7 zwh?{aTwf~aq{)n|gLh*;?Y65@X%cNiBNyJMy?|*{WOI%#^i%dEcwX*`as)Jd1-V*< zHJ0@^#k}%{DbSPxSjXD170L!VecjB@bkU{}Hdlr#^w6D-v|N3}^ zjce=Q1a}Ub&4ZjMI(4y)lIVI78}ci9q*u}vJ_asb2XYjgi@Y#q92S zUFYI%;-x`mrJ!f_nay`mmoGxzF!S)FM+`DmK`r?1DV4S$<|KtBKzfCZO|^p$H*JA_ z4w#VwcI8Ee)@J!>VJNa59+aGK*>HXSkQ^U&egB|np`?79jd~*K{&0zf!{zGshza<) zQfcgA#h!0L<`Fqk;US%ck0_1CTphC9LREprzOa!0-rwt;hCZ2W6ZbLo!v?9JLti%} z{H2I{K0fR9*JU^3eB%2V+waRm96lXBB3O=Du>mdP4eMdSUY1ctP5!2Yo#_U2K^@JlrmJ5l0ywRnhDS zdxl@#8tkAaag7Kuo2P-L#dcn9f(LU0B~X65*7gdVR8$u~0?tymVr^iKLQuD&DU?Hm z8PbYsc;K7!UK`VGMWVI;2@^aI4Ot0CIZD0!L2z-g;4G>D6Iyf$aPmq2O)a#X7Xn}M ztk3C+(?L2^bTjREx05qIoNRekJ*0_geti~bKWMv$XB&5;of~%neyVb!U>4*MMU|py zqKsEc^ciAdhsTe|!xzDRGy{}3;z!`t0Eur@oLvZmX(AHgQI3b=G{l9>!tRQ1tYuD@eK2^ti!LIm>0_8 zN67G_ckyQ1sT<4mpc95T4YM(A_;=REuafUNF1=ItJgeXoh&Q~xoN&GyZWBOxr6qd3 z2^GG`&Idhtb}%{5mi^rf&JB@ zXt4^&%tLaO#bsg(1(S;TQ)$YIGRWf<15_B|STr@_oOmF(#u)3d@$vUyBEo_}!{-G_ zb;#NB59qd0Ty=EG&8N4OLii}+<<;^(nNq%x`1|H6_ez@ikb=Z}cu{>Rl5m45H z>vis3^D#4CNphxNVrl_zg*~1RD@sIQ-^z?ROJOO@JFS!m?Z9^;Ox_sD_GMaCyR_ zMmTgrMye!k5`T7VtsbEq< zdL4*Vw%A-5B26eRVW|--V(q-3znLt(c`{7T+KP8~b*h*f1>V~Ad_TAA&cKy{aj1wqWi6WdE;;oP$ax+8E3CzGa5uaCF!cC|Z2BgG zA~X5gN?;F~-rXO1I68*idv7I+Vk`SP(`TUGaI~Iecja0Bks_MUeXkxlyFSLvg~8p0 z0@o9rq2mN)Sh!q$vSSZI^Ta>U(@Y*$!};lQ<@IiMvM`EZcnAIToo;RiaQ@0Su|swK zT6~Q}v6+s{+#UfvWKnI07)v3KGIwV7eF?;x@r(k1kw&ZYbbI1eixdKPgE14bBBoK5lC_2UNAA^*Ym1e=tHm_n^21z9jZ3&wBuIir%NTjR=0A!_#3 z)NcoPLcQs6+Dp{yF4@?|wRLOx1&UNI<0FH}?}7msnz+2+bDEy#xfcEB>#N@<<`F+0 z6rdM+X>6^nIzHNP->Nh^f+Zo+c)50X#`)r~0s4~M#KEzZ#6_f(jd?yn2;(G+Du;(P zbyRP$T}!0#K-M0iopXJdKfxb*^2AXa2W)gcl|#)Vo?;MBy!13~N!Wg0J{x>oqd;hw zJ7F`LTzDv_rzg|2^5_jQ2_EnKSd8LT5{@{xkN4HIEe}ky2~1TkA3@@}Ah22p8YlS) zWnO4R3gspiwPu(dVMk8L&ZWmJ*lC`EH2`!gZf`{9RI6^auXnB~?jns}rzE06gg?|s zG%W7%goHo=&guf<(lPZac~2grBSz~jtcHo|B{~!*Wgsdd>bRr+A!{-x{Fsbvn}oxM zm@Nru^FGWeQ{3s2=lv?Pj`mVRmnl5BnSBuD(@J8(&FHQ;N>3`Ix-L3V!ah66kxNuU ziJDEVc`ZMB+v{+;O?B!bQ^lTK34CIEE+yc?+fpGi6n`X8C38 zlEEVUQOR|dOv5;0y+)hyxm9a?2fFbv+n4SsKu3Xp3Z)tT2afI9C{%5FCO>$0Ag_#) z!EkI7EX>E8;v6*#IqGbt=Oe)8Gjr+9c;1IZP%R-qh?Bptbbl=Ge8}vsis;%aCYQjc zTIXT#_;Qs1H(7JqCWYk_#Jh&1bj(HstnMaMh)sIn;F6a|j)(5d5%A5Q4=g*C53?I+ z6e%7Xv{$1tZ<@qE`j9r$83TS6u0Pfb@6M~TT{CmZM_z8_V~xUupc`<*a=FrKP@QAT z*RlVcHU4WW=`@{UVYu48!&`=POd%RV`?|D~!+P99Az?4AT;|6dK>Du2NMLMT57;h6 z4d|8w*&F&1D`F3;yw$lNp@QVDx4)P!{KAM%Jb+=u_`>CaYTpIVtZ2(J8G?KDQ6tTd z?%Y$;EmYP{WsO)vL(|T_s-kJ<=XZnv`?0USeG|k}EefXKgVAazQ*M%(_Mx@NC&x73 z1rSd@4J+#^*QtRu(I6)SPZ$DpW{|-;o7Sz!@55_f2s6t?m`j2lteGpMWA=~Q4b6rb zeD;9k`l!|4oi?1!FtE=uT)xDr*?k5D*r6AyGUDQn3c7eAEVl~sN}*xr1D667uKmKr zjF87hg1R0vtjA17&L%!<>A;*%1$g77T*kMco*kbd~qACMc~D(rcNzw0P8lzl6&X$D8H=4!9CI7}YSMt<|rI@drS zgwS5lamd3d8viC!gl<&Y9ia`y_-=jv9S70gnr77y@H60)aa*oxXW(YrigH~~KK~A* z5ZRq;BqK(=35?v);fi3Opb;=Od@5Z9#7MX6`-8XsiePR^lrr0>j?p=+Fzk8!DBsIa z_?KCaLDV{A!3^>PfN{LiUNcV1MBl@vNozmzrQ9eJQWdB?BG}8e!qjJJfv;bHj<@SL z`p)Uj@%S!23cg?(rG|IM26WsD%)KBC~^%R;F zaz%}%UipJGYP=EI67P0gPH8HPMP}fJ(Z=B8Stub$80}{&px-sl;o1#%(b2Q?+kdeL}lLaQ| zmZ~E+p&6Y@wX~xTR#gWEKhGoR-oEu3?=JY`7L#w(ymQmF*P#i9uK4*mL@3}i&X7Mm z*1~p5eX_gg|2`j~;#&DK0LUoWdO#%jV!ImS?wCESW|+MU>TrMuWli1ZIQz>=axnG^b_JHm zM}>c&8RLmx!f&(ms+t)|^;!#+=wrSO$ySY^Qy1F#>_-k^M$iM6Gx$FE1}Sj{#Pdu%gj*M@{pjGWuFf(f;bB@)+jmYFbNm% zXxLZs2|GJ1i=*~A3z2Y1nj9HzmO_9re#K8@TrMAxb-Ya1l9?5X#64)%D@QG;4I%{i#h%(Dy; z0Ej=}E<<@j+w3=vMw%V(qPglqE^?g}5^W~;z6BN5mS>5=0ng*dp|V{ejK7`LarVtL zVU&bBWQ_Wj&jECdcyMw+g|t?+i`=VA8?g`Al-RogV?}v^b+wAf)ES%5wP#5?@h#qi zvz$KwyMoZ7k2J3-xWyD%AMQuJd51@rsv6t8470dzB_^frW{Y!-A7jnCE7KAXs##qH z_2d96sr=$5dO!t5Lo(w#02`9qW;0j&ARcECxGi&xE6(Fe$UHa2`^pGnT7cUmH!RZ{vx^P9Ff=$K|^wH+A=EBq$SmsA1*jCKhmBf z;ED`6Y$x#bTExt}P~^f=T#w#LiW6tiZ-hF7JdX{n@&HIhLhWE)F2DJ zPRF1M0$XxJA9_OM`WRjBJue_4V!Y18bB~*sO&=`jBW+j@?G{qvIVB~u+DJ28Of`;2 z1CGSs9H9$euy{_*2SYYkb{=PFMJt!r?bjKV^LCG8tf$QPEEhJ|{3=AV&hzRr zYQz^h=?c37sqH+5+bU5xsa-a;zomlfLS-pwH76B5(nVDQKyK+GYGopC#H_@?NQsf5 z!|+C6f1!p%8*U~{>svO???)>!Ce)$04AbVrSzmtnKit(k)uRYwa$j z!vTrE$NM;uY}(x}@$q^FRB(K1D9M7dg+tzZH~+3r`Da-{JqMtSE1bgHj@eafM@;ci zLD4n8^f+33bdt_hE4HHS)6by~4bo;&m*dLY$<)E{PXDD;QPm44<_Scyt4A_JcUPzN zM0s~GI?(p@cdFEjA)FwldciuAfcM_X%Kjt2I1o*YDx{g; z`5Lg_Rc()Jo8!orKjA9iF-fs!Z;qawW#cPMTkllKeTv@NmJ_2RXY&B#umG8fdS6e_ zlA0Ux-qSDW<=>>iUzYk~7}((B67$x546i)!tN1XN_P71k*1n(A0W&H&O!kr+q$8!a zeBFAbxt7vg11}25rHKP_ud59t^W3K7PH`$r84@jbs11;)tcV-b3H~~dxhzwNifH)G z9)?+iFx=&lVKP); z0i=-!9;6(Ev#~o=%n_f&DbG)gF}zjVm6`<*@XbrD2$#uhWvG3 zSN|O_2!|Z86h7@nH!`>SXKd&}A7a=ou*Qk8 zXWj_cmhRpwSj(0DueNQ9mdFqQW*efLAYA0V_74CCOoTI!fW>o(ei}|~+o+Tc=TdU( zj@zZn>E?w(_W(Vkl)x~(omcn$CmdfgmPMN-FOMOcXk&X`_^o1H`3dtNA7|@j<5P>~ z4-p#=zSPw?%SXd7F2c{4J*G$l!QCG-a=iWyyN|MHh}d4Wi8wVOzK$$ar9T?^Io~HG zw|IY~tv?Un1E(=l*;`q8;8ul24o;132G|mL(%wS_HKMz7S4_y1UNBGab@Y`^PL0s1 zL42TuBqMOSIa0!Wx(bSliu%rOl-z=_t};TdDfX?TB4=ab%yamCKL{?BQ@WUCaJ?0? zTD>aeI+xnKqcmplyd**SVP)k7p~U-~&kB-ss0+M!8rNXV;JB6UXl;V_qPLXC6?tGl znnK(PGP)>+Kq!A}U8X`x^v>>OMqFk~dnM#R5Dyv0n+OovFcMtXh;^N585JV$W!S^= zK+G=W^Git3Vhcdfmku>d4sjH#hgbO&m_{L+lV6xVDb~Ru zV9EqveTUevpJp`sP%>JWd|jeUHd)HVU#oT58*L!r6JMVD5EWUbgqrJQZ#%8wlm2!+ z#t6NItKLtj9Tg9ujXX;>8(H9l1x>0sml$X)c0C0K&9AV;D7MUXQub^)j|x^T6}vMz zf(-!}+BX-SyO6F&s^Z=-$Nz3uTVu&Ma;07y8FH5^r84!L;>I6I|-F-eYH3y4^2iKFJ;1)L69%Y#skh+_S!0u zwbm5I1OI#uVy=t225cy1nGk7h%H{kLrV@5_H5`QJ@#`0L1ozjq@4 zz5Z1q{;!D+e~UBxZl%8{<7hJiZuTuVNve=N2aj6lliTt+$^DolBl$U=3hF|}G zru;h@z+ZrW_|iG@e*^v$1^h{2bNSEA_`7uAFUmjs3LoX)Q~te>;7@jv`0;<#g@3Q< s{mBOYUFrL)3;(q6-&OejBv;w}Z)9+Y-zEV;|9+H!0s$2={^QgC0d&!*{Qv*} diff --git a/documentation/backend_error_code.txt b/documentation/backend_error_code.txt index 726f438..48904be 100644 --- a/documentation/backend_error_code.txt +++ b/documentation/backend_error_code.txt @@ -59,4 +59,12 @@ sqlite3 error codes 310: "User is not in the group" The user is not in the group. Please check the group id and try again. 311: "Invitee already has an invite" - The invitee already has an invite. Please check the group id and try again. \ No newline at end of file +<<<<<<< Updated upstream + The invitee already has an invite. Please check the group id and try again. +======= + The invitee already has an invite. Please check the group id and try again. +312: "invalid file type" + The file type is not supported. Please check the file type and try again. +313: "File not found" + The file was not found. Please check the file id and try again. +>>>>>>> Stashed changes diff --git a/roomiebuddy/.metadata b/roomiebuddy/.metadata index d044da8..01965b7 100644 --- a/roomiebuddy/.metadata +++ b/roomiebuddy/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668" + revision: "c23637390482d4cf9598c3ce3f2be31aa7332daf" channel: "stable" project_type: app @@ -13,26 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 - base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 + create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf - platform: android - create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 - base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 - - platform: ios - create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 - base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 - - platform: linux - create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 - base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 - - platform: macos - create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 - base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 - - platform: web - create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 - base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 - - platform: windows - create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 - base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 + create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf # User provided section