From 3f140d95125985ab629e4bb64d9c982b888009b4 Mon Sep 17 00:00:00 2001 From: Are Schjetne Date: Tue, 16 Dec 2025 19:17:53 +0100 Subject: [PATCH 01/11] =?UTF-8?q?=F0=9F=9B=82=20Add=20check=20for=20privat?= =?UTF-8?q?e=20not=20whilst=20adding=20to=20an=20trip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- turplanlegger/views/trips.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/turplanlegger/views/trips.py b/turplanlegger/views/trips.py index e7e9b0ad..ec6e0b8c 100644 --- a/turplanlegger/views/trips.py +++ b/turplanlegger/views/trips.py @@ -123,11 +123,10 @@ def add_note_to_trip(trip_id: int): if Permission.verify(note.owner, note.permissions, g.user.id, AccessLevel.READ) is PermissionResult.NOT_FOUND: raise ApiProblem('Failed to add note to trip', 'Note was not found', 404) - # I'm lazy, so I'm keeping this here until I fix private attribute for Note - # if note.private is True: - # note_perms = Permission.verify(note.owner, note.permissions, g.user.id, AccessLevel.READ) - # if note_perms is PermissionResult.NOT_FOUND: - # raise ApiProblem('Failed to add note to trip', 'Note was not found', 404) + if note.private is True: + note_perms = Permission.verify(note.owner, note.permissions, g.user.id, AccessLevel.READ) + if note_perms is PermissionResult.NOT_FOUND: + raise ApiProblem('Failed to add note to trip', 'Note was not found', 404) try: trip.add_note_reference(note.id) From b74440f86a73d96254ab12e23cdf226cf4734db4 Mon Sep 17 00:00:00 2001 From: Are Schjetne Date: Tue, 16 Dec 2025 19:25:29 +0100 Subject: [PATCH 02/11] =?UTF-8?q?=F0=9F=A9=B9=20Fix=20check=20if=20note=20?= =?UTF-8?q?already=20exists=20in=20an=20trip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- turplanlegger/views/trips.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/turplanlegger/views/trips.py b/turplanlegger/views/trips.py index ec6e0b8c..1929d5b6 100644 --- a/turplanlegger/views/trips.py +++ b/turplanlegger/views/trips.py @@ -123,6 +123,9 @@ def add_note_to_trip(trip_id: int): if Permission.verify(note.owner, note.permissions, g.user.id, AccessLevel.READ) is PermissionResult.NOT_FOUND: raise ApiProblem('Failed to add note to trip', 'Note was not found', 404) + if note.id in trip.notes: + return jsonify(status='ok', count=1, trip=trip.serialize), 200 + if note.private is True: note_perms = Permission.verify(note.owner, note.permissions, g.user.id, AccessLevel.READ) if note_perms is PermissionResult.NOT_FOUND: @@ -133,7 +136,7 @@ def add_note_to_trip(trip_id: int): except Exception as e: raise ApiProblem('Failed to add note to trip', str(e), 500) - return jsonify(trip.serialize), 201 + return jsonify(status='ok', count=1, trip=trip.serialize), 201 @api.route('/trips//routes', methods=['PATCH']) From 6cb03a9083098e10b091fd168f6e4658019ee2c8 Mon Sep 17 00:00:00 2001 From: Are Schjetne Date: Tue, 16 Dec 2025 19:26:07 +0100 Subject: [PATCH 03/11] =?UTF-8?q?=E2=9C=85=20Add=20test=20for=20adding=20a?= =?UTF-8?q?lready=20exists=20note=20to=20a=20trip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_trip.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/test_trip.py b/tests/test_trip.py index c0fb26de..2aa660e1 100644 --- a/tests/test_trip.py +++ b/tests/test_trip.py @@ -213,6 +213,41 @@ def test_create_trip_add_note(self): self.assertEqual(data['trip']['notes'], [note_id]) + def test_create_trip_add_existing_note(self): + response = self.client.post('/trips', data=json.dumps(self.trip), headers=self.headers_json) + self.assertEqual(response.status_code, 201) + data = json.loads(response.data.decode('utf-8')) + trip_id = data['id'] + + # Create note + response = self.client.post('/notes', data=json.dumps(self.note), headers=self.headers_json) + self.assertEqual(response.status_code, 201) + data = json.loads(response.data.decode('utf-8')) + note_id = data['id'] + + # Add note to trip + response = self.client.patch( + f'/trips/{trip_id}/notes', data=json.dumps({'note_id': note_id}), headers=self.headers_json + ) + self.assertEqual(response.status_code, 201) + + response = self.client.get(f'/trips/{trip_id}', headers=self.headers) + self.assertEqual(response.status_code, 200) + data = json.loads(response.data.decode('utf-8')) + + self.assertEqual(len(data['trip']['notes']), 1) + self.assertEqual(data['trip']['notes'], [note_id]) + + # Add note again to trip + response = self.client.patch( + f'/trips/{trip_id}/notes', data=json.dumps({'note_id': note_id}), headers=self.headers_json + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.data.decode('utf-8')) + + self.assertEqual(len(data['trip']['notes']), 1) + self.assertEqual(data['trip']['notes'], [note_id]) + def test_create_trip_add_route(self): response = self.client.post('/trips', data=json.dumps(self.trip), headers=self.headers_json) self.assertEqual(response.status_code, 201) From 83a43cbaf3fc2167b71d31c75a2d108126ac1c24 Mon Sep 17 00:00:00 2001 From: Are Schjetne Date: Tue, 16 Dec 2025 20:14:00 +0100 Subject: [PATCH 04/11] =?UTF-8?q?=F0=9F=A9=B9=20Fix=20check=20if=20route?= =?UTF-8?q?=20already=20exists=20in=20trip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- turplanlegger/views/trips.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/turplanlegger/views/trips.py b/turplanlegger/views/trips.py index 1929d5b6..399006ed 100644 --- a/turplanlegger/views/trips.py +++ b/turplanlegger/views/trips.py @@ -161,12 +161,15 @@ def add_route_to_trip(trip_id: int): if Permission.verify(route.owner, route.permissions, g.user.id, AccessLevel.READ) is PermissionResult.NOT_FOUND: raise ApiProblem('Failed to add route to trip', 'Route was not found', 404) + if route.id in trip.routes: + return jsonify(status='ok', count=1, trip=trip.serialize), 200 + try: trip.add_route_reference(route.id) except Exception as e: raise ApiProblem('Failed to add route to trip', str(e), 500) - return jsonify(trip.serialize), 201 + return jsonify(status='ok', count=1, trip=trip.serialize), 201 @api.route('/trips//item_lists', methods=['PATCH']) From e0dc0767f8c397f9b2ce4a606c0414aff68181ce Mon Sep 17 00:00:00 2001 From: Are Schjetne Date: Tue, 16 Dec 2025 20:14:51 +0100 Subject: [PATCH 05/11] =?UTF-8?q?=F0=9F=97=83=EF=B8=8F=20Add=20optional=20?= =?UTF-8?q?returning=20when=20inserting=20into=20database?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- turplanlegger/database/base.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/turplanlegger/database/base.py b/turplanlegger/database/base.py index ffa89249..9489a4d8 100644 --- a/turplanlegger/database/base.py +++ b/turplanlegger/database/base.py @@ -539,25 +539,22 @@ def add_trip_note_reference(self, trip_id, note_id): insert_ref = """ INSERT INTO trips_notes_references (trip_id, note_id) VALUES (%(trip_id)s, %(note_id)s) - RETURNING * """ - return self._insert(insert_ref, {'trip_id': trip_id, 'note_id': note_id}) + return self._insert(insert_ref, {'trip_id': trip_id, 'note_id': note_id}, False) def add_trip_item_list_reference(self, trip_id, item_list_id): insert_ref = """ INSERT INTO trips_item_lists_references (trip_id, item_list_id) VALUES (%(trip_id)s, %(item_list_id)s) - RETURNING * """ - return self._insert(insert_ref, {'trip_id': trip_id, 'item_list_id': item_list_id}) + return self._insert(insert_ref, {'trip_id': trip_id, 'item_list_id': item_list_id}, False) def add_trip_route_reference(self, trip_id, route_id): insert_ref = """ INSERT INTO trips_routes_references (trip_id, route_id) VALUES (%(trip_id)s, %(route_id)s) - RETURNING * """ - return self._insert(insert_ref, {'trip_id': trip_id, 'route_id': route_id}) + return self._insert(insert_ref, {'trip_id': trip_id, 'route_id': route_id}, False) def get_trip(self, trip_id: int, deleted=False): select = 'SELECT * FROM trips WHERE id = %s' @@ -682,14 +679,14 @@ def delete_trip_date(self, trip_date_id): return self._updateone(update, {'id': trip_date_id}, returning=True) # Helpers - def _insert(self, query, vars): + def _insert(self, query, vars, returning=True): """ Insert, with return. """ self._log('_insert', query, vars) with self.conn.transaction(): self.cur.execute(query, vars) - return self.cur.fetchone() + return self.cur.fetchone() if returning else None def _fetchone(self, query, vars): """ From 7869d2efd3d6cb11b296c2c071c3820589197817 Mon Sep 17 00:00:00 2001 From: Are Schjetne Date: Tue, 16 Dec 2025 20:15:11 +0100 Subject: [PATCH 06/11] =?UTF-8?q?=E2=9C=85=20Add=20test=20for=20adding=20a?= =?UTF-8?q?lready=20exists=20route=20to=20trip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_trip.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/tests/test_trip.py b/tests/test_trip.py index 2aa660e1..8527330e 100644 --- a/tests/test_trip.py +++ b/tests/test_trip.py @@ -267,10 +267,39 @@ def test_create_trip_add_route(self): self.assertEqual(response.status_code, 201) data = json.loads(response.data.decode('utf-8')) - response = self.client.get(f'/trips/{trip_id}', headers=self.headers) + self.assertEqual(len(data['trip']['routes']), 1) + self.assertEqual(data['trip']['routes'], [route_id]) + + def test_create_trip_add_existing_route(self): + response = self.client.post('/trips', data=json.dumps(self.trip), headers=self.headers_json) + self.assertEqual(response.status_code, 201) + data = json.loads(response.data.decode('utf-8')) + trip_id = data['id'] + + # Create route + response = self.client.post('/routes', data=json.dumps(self.route), headers=self.headers_json) + self.assertEqual(response.status_code, 201) + data = json.loads(response.data.decode('utf-8')) + route_id = data['id'] + + # Add route to trip + response = self.client.patch( + f'/trips/{trip_id}/routes', data=json.dumps({'route_id': route_id}), headers=self.headers_json + ) + self.assertEqual(response.status_code, 201) + data = json.loads(response.data.decode('utf-8')) + + self.assertEqual(len(data['trip']['routes']), 1) + self.assertEqual(data['trip']['routes'], [route_id]) + + # Add existing route to trip + response = self.client.patch( + f'/trips/{trip_id}/routes', data=json.dumps({'route_id': route_id}), headers=self.headers_json + ) self.assertEqual(response.status_code, 200) data = json.loads(response.data.decode('utf-8')) + self.assertEqual(len(data['trip']['routes']), 1) self.assertEqual(data['trip']['routes'], [route_id]) def test_create_trip_add_item_list(self): From 6eb9f74b8d40bb5cd375b5c872ee14c747764c58 Mon Sep 17 00:00:00 2001 From: Are Schjetne Date: Tue, 16 Dec 2025 20:29:40 +0100 Subject: [PATCH 07/11] =?UTF-8?q?=F0=9F=A9=B9=20Fix=20check=20if=20item=5F?= =?UTF-8?q?list=20already=20exists=20in=20trip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- turplanlegger/views/trips.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/turplanlegger/views/trips.py b/turplanlegger/views/trips.py index 399006ed..79637306 100644 --- a/turplanlegger/views/trips.py +++ b/turplanlegger/views/trips.py @@ -179,21 +179,32 @@ def add_item_list_to_trip(trip_id: int): if not trip: raise ApiProblem('Failed to add item list to trip', 'Trip was not found', 404) + perms = Permission.verify(trip.owner, trip.permissions, g.user.id, AccessLevel.MODIFY) + if perms is PermissionResult.NOT_FOUND: + if trip.private is True: + raise ApiProblem('Trip not found', 'The requested trip was not found', 404) + raise ApiProblem('Insufficient permissions', 'Not sufficient permissions to modify the trip', 403) + if perms is PermissionResult.INSUFFICIENT_PERMISSIONS: + raise ApiProblem('Insufficient permissions', 'Not sufficient permissions to modify the trip', 403) + item_list = ItemList.find_item_list(request.json.get('item_list_id', None)) if not item_list: raise ApiProblem('Failed to add item list to trip', 'Item list was not found', 404) - if ( - Permission.verify(item_list.owner, item_list.permissions, g.user.id, AccessLevel.READ) - is PermissionResult.NOT_FOUND - ): - raise ApiProblem('Failed to add item list to trip', 'Item list was not found', 404) + if item_list.private is True: + note_perms = Permission.verify(item_list.owner, item_list.permissions, g.user.id, AccessLevel.READ) + if note_perms is PermissionResult.NOT_FOUND: + raise ApiProblem('Failed to add item list to trip', 'Item list was not found', 404) + + if item_list.id in trip.routes: + return jsonify(status='ok', count=1, trip=trip.serialize), 200 + try: trip.add_item_list_reference(item_list.id) except Exception as e: raise ApiProblem('Failed to add item list to trip', str(e), 500) - return jsonify(trip.serialize), 201 + return jsonify(status='ok', count=1, trip=trip.serialize), 201 @api.route('/trips//owner', methods=['PATCH']) From 2766f383d43d80f737a57694f88339199d82fa32 Mon Sep 17 00:00:00 2001 From: Are Schjetne Date: Tue, 16 Dec 2025 20:30:15 +0100 Subject: [PATCH 08/11] =?UTF-8?q?=F0=9F=94=A5=20Remove=20double=20permissi?= =?UTF-8?q?on=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- turplanlegger/views/trips.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/turplanlegger/views/trips.py b/turplanlegger/views/trips.py index 79637306..21563c2c 100644 --- a/turplanlegger/views/trips.py +++ b/turplanlegger/views/trips.py @@ -120,9 +120,6 @@ def add_note_to_trip(trip_id: int): if not note: raise ApiProblem('Failed to add note to trip', 'Note was not found', 404) - if Permission.verify(note.owner, note.permissions, g.user.id, AccessLevel.READ) is PermissionResult.NOT_FOUND: - raise ApiProblem('Failed to add note to trip', 'Note was not found', 404) - if note.id in trip.notes: return jsonify(status='ok', count=1, trip=trip.serialize), 200 From 1973b45aa8b14aba8c0cb66e00ac030984c956f2 Mon Sep 17 00:00:00 2001 From: Are Schjetne Date: Tue, 16 Dec 2025 20:31:32 +0100 Subject: [PATCH 09/11] =?UTF-8?q?=F0=9F=90=9B=20Fix=20bug=20where=20refera?= =?UTF-8?q?nces=20would=20be=20returned=20as=20database=20row?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- turplanlegger/models/trip.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/turplanlegger/models/trip.py b/turplanlegger/models/trip.py index 74d0e4d2..fa85cddd 100644 --- a/turplanlegger/models/trip.py +++ b/turplanlegger/models/trip.py @@ -147,7 +147,7 @@ def delete(self) -> bool: def update(self, updated_fields) -> None: return db.update_trip(self, updated_fields) - def add_note_reference(self, note_id: int) -> 'Trip': + def add_note_reference(self, note_id: int) -> None: """Adds a note to the trip instance Args: @@ -157,9 +157,9 @@ def add_note_reference(self, note_id: int) -> 'Trip': dict of notes from the database """ db.add_trip_note_reference(self.id, note_id) - self.notes = db.get_trip_notes(self.id) + self.notes = [item.note_id for item in db.get_trip_notes(self.id)] - def add_route_reference(self, route_id: int) -> 'Trip': + def add_route_reference(self, route_id: int) -> None: """Adds a route to the trip instance Args: @@ -169,11 +169,11 @@ def add_route_reference(self, route_id: int) -> 'Trip': dict of routes from the database """ db.add_trip_route_reference(self.id, route_id) - self.routes = db.get_trip_routes(self.id) + self.routes = [item.route_id for item in db.get_trip_routes(self.id)] def add_item_list_reference(self, item_list_id: int) -> 'Trip': db.add_trip_item_list_reference(self.id, item_list_id) - self.routes = db.get_trip_item_lists(self.id) + self.item_lists = [item.item_list_id for item in db.get_trip_item_lists(self.id)] @staticmethod def update_trip_dates(dates: JSON, trip: 'Trip') -> 'TRIP_DATE_UPDATE_STATUS': From a819295189fd0ef971228b11273d3fb0d926cc1c Mon Sep 17 00:00:00 2001 From: Are Schjetne Date: Tue, 16 Dec 2025 20:33:03 +0100 Subject: [PATCH 10/11] =?UTF-8?q?=F0=9F=A4=A1=20Check=20the=20correct=20li?= =?UTF-8?q?st?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- turplanlegger/views/trips.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/turplanlegger/views/trips.py b/turplanlegger/views/trips.py index 21563c2c..2f5c96b1 100644 --- a/turplanlegger/views/trips.py +++ b/turplanlegger/views/trips.py @@ -193,7 +193,7 @@ def add_item_list_to_trip(trip_id: int): if note_perms is PermissionResult.NOT_FOUND: raise ApiProblem('Failed to add item list to trip', 'Item list was not found', 404) - if item_list.id in trip.routes: + if item_list.id in trip.item_lists: return jsonify(status='ok', count=1, trip=trip.serialize), 200 try: From 45fa359cf865e78bd5c42c853fa5ecc78fafb24e Mon Sep 17 00:00:00 2001 From: Are Schjetne Date: Tue, 16 Dec 2025 20:33:30 +0100 Subject: [PATCH 11/11] =?UTF-8?q?=E2=9C=85=20Add=20test=20for=20adding=20a?= =?UTF-8?q?lready=20exists=20item=5Flist=20to=20trip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_trip.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/test_trip.py b/tests/test_trip.py index 8527330e..798b71b1 100644 --- a/tests/test_trip.py +++ b/tests/test_trip.py @@ -324,10 +324,19 @@ def test_create_trip_add_item_list(self): self.assertEqual(response.status_code, 201) data = json.loads(response.data.decode('utf-8')) - response = self.client.get(f'/trips/{trip_id}', headers=self.headers) + self.assertEqual(len(data['trip']['item_lists']), 1) + self.assertEqual(data['trip']['item_lists'], [item_list_id]) + + # Add existing item_list to trip + response = self.client.patch( + f'/trips/{trip_id}/item_lists', + data=json.dumps({'item_list_id': item_list_id}), + headers=self.headers_json, + ) self.assertEqual(response.status_code, 200) data = json.loads(response.data.decode('utf-8')) + self.assertEqual(len(data['trip']['item_lists']), 1) self.assertEqual(data['trip']['item_lists'], [item_list_id]) def test_change_trip_owner(self):