From 18a3e40435137d974889ae60c0dcc638cc807996 Mon Sep 17 00:00:00 2001 From: jedi Date: Mon, 31 Aug 2026 14:41:52 +0200 Subject: [PATCH] stash --- backend/files/media_urls.py | 4 ++-- backend/files/tests.py | 10 ++++++++++ backend/toolshed/api/files.py | 3 ++- backend/toolshed/tests/test_files.py | 11 +++++++++++ frontend/src/components/AuthenticatedImage.vue | 2 +- frontend/src/views/WorkflowDetail.vue | 14 ++++++++++++-- frontend/src/views/Workflows.vue | 10 +++++----- 7 files changed, 43 insertions(+), 11 deletions(-) diff --git a/backend/files/media_urls.py b/backend/files/media_urls.py index b8c90f8..68050d6 100644 --- a/backend/files/media_urls.py +++ b/backend/files/media_urls.py @@ -28,8 +28,8 @@ def _accessible_files(request): # friends-or-self with whatever references it (item, profile picture), a member of the group # that owns the item it's attached to, or it's their own staged photo. return File.objects.filter( - Q(connected_items__owner__in=request.user.friends_or_self()) | - Q(connected_items__owner_group__in=request.user.member_of_groups.all()) | + Q(connected_items__owner__in=request.user.friends_or_self(), connected_items__is_deleted=False) | + Q(connected_items__owner_group__in=request.user.member_of_groups.all(), connected_items__is_deleted=False) | Q(profile_picture_users__in=request.user.friends_or_self()) | Q(staged_by_workflows__owner__in=request.user.user.all()) ).distinct() diff --git a/backend/files/tests.py b/backend/files/tests.py index bf2c498..b7e2870 100644 --- a/backend/files/tests.py +++ b/backend/files/tests.py @@ -187,6 +187,16 @@ class MediaUrlTestCase(FilesTestMixin, UserTestMixin, InventoryTestMixin, Toolsh self.assertEqual(reply.status_code, 404) self.assertTrue('X-Accel-Redirect' not in reply.headers) + def test_file_url_only_connected_via_deleted_item(self): + # test_file2 is only reachable through item1; soft-deleting it doesn't sever the files + # M2M row, so this would regress to serving test_file2 as if item1 were still live if + # _accessible_files ever stops excluding soft-deleted items again. + self.f['item1'].delete() + reply = client.get( + f"/media/{self.f['hash2'][:2]}/{self.f['hash2'][2:4]}/{self.f['hash2'][4:6]}/{self.f['hash2'][6:]}", + self.f['local_user1']) + self.assertEqual(reply.status_code, 404) + @override_settings(SERVE_X_ACCEL_REDIRECT=True) def test_profile_picture_url(self): self.f['local_user1'].profile_picture = self.f['test_file3'] diff --git a/backend/toolshed/api/files.py b/backend/toolshed/api/files.py index cb1dd38..5d6aa6f 100644 --- a/backend/toolshed/api/files.py +++ b/backend/toolshed/api/files.py @@ -33,7 +33,8 @@ def list_all_files(request, format=None): # request.user is a ToolshedUser here; reach group membership via public_identity. files = File.objects.select_related().filter( Q(connected_items__owner=request.user) | - Q(connected_items__owner_group__in=request.user.public_identity.member_of_groups.all()) + Q(connected_items__owner_group__in=request.user.public_identity.member_of_groups.all()), + connected_items__is_deleted=False ).distinct() return Response(FileSerializer(files, many=True).data) diff --git a/backend/toolshed/tests/test_files.py b/backend/toolshed/tests/test_files.py index a215878..f325f99 100644 --- a/backend/toolshed/tests/test_files.py +++ b/backend/toolshed/tests/test_files.py @@ -39,6 +39,17 @@ class FileApiTestCase(UserTestMixin, FilesTestMixin, InventoryTestMixin, Toolshe self.assertEqual(response.json()[1]['name'], f"/media/{self.f['hash2'][:2]}/{self.f['hash2'][2:4]}/{self.f['hash2'][4:6]}/{self.f['hash2'][6:]}") + def test_list_all_files_excludes_files_only_connected_via_deleted_item(self): + # test_file2 is only reachable through item1; test_file1 is also reachable through item2, + # which stays live. Soft-deleting item1 doesn't sever its files M2M rows, so this would + # regress to listing test_file2 as if it were still owned if the join-based filter ever + # stops excluding soft-deleted items again. + self.f['item1'].delete() + response = client.get(f"/api/v1/files/", self.f['local_user1']) + self.assertEqual(response.status_code, 200) + hashes = [f['hash'] for f in response.json()] + self.assertEqual(hashes, [self.f['hash1']]) + def test_files(self): response = client.get(f"/api/v1/item_files/{self.f['item1'].id}/", self.f['local_user1']) self.assertEqual(response.status_code, 200) diff --git a/frontend/src/components/AuthenticatedImage.vue b/frontend/src/components/AuthenticatedImage.vue index 75bec1f..31847ff 100644 --- a/frontend/src/components/AuthenticatedImage.vue +++ b/frontend/src/components/AuthenticatedImage.vue @@ -33,7 +33,7 @@ export default { }, fallbackSrc: { type: String, - default: '/assets/img/avatars/avatar.png' + default: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+CjxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjEyIiBmaWxsPSIjY2VkNGRhIi8+CjxjaXJjbGUgY3g9IjEyIiBjeT0iOS41IiByPSI0IiBmaWxsPSIjZmZmZmZmIi8+CjxwYXRoIGQ9Ik00IDIwLjJDNCAxNi4yIDcuNiAxMy41IDEyIDEzLjVzOCAyLjcgOCA2Ljd2MC4zSDR2LTAuM3oiIGZpbGw9IiNmZmZmZmYiLz4KPC9zdmc+Cg==' }, width: { type: [String, Number], diff --git a/frontend/src/views/WorkflowDetail.vue b/frontend/src/views/WorkflowDetail.vue index 8ac7f24..c8b5a44 100644 --- a/frontend/src/views/WorkflowDetail.vue +++ b/frontend/src/views/WorkflowDetail.vue @@ -248,7 +248,7 @@ export default { }, totalSteps() { - return this.stepDefinitions.length || this.workflowInstance?.total_steps || 1; + return this.stepDefinitions.length || 1; }, currentStepDefinition() { @@ -314,6 +314,7 @@ export default { ...mapActions([ 'fetchActiveWorkflows', 'updateWorkflow', + 'updateWorkflowStep', 'deleteWorkflow' ]), @@ -338,11 +339,20 @@ export default { } }, - navigateToStep(step) { + async navigateToStep(step) { const stepStr = String(step); // For navigation, we need to validate the step exists in step definitions const stepExists = this.stepDefinitions.some(step => step.step === stepStr); if (stepExists || (step >= 1 && step <= this.totalSteps)) { + try { + await this.updateWorkflowStep({ + workflowId: this.id, + currentStep: stepStr, + payload: this.workflowInstance.payload + }); + } catch (error) { + console.error('Error persisting workflow step:', error); + } this.$router.push({ name: 'workflow-detail', params: { id: this.id, step: stepStr } diff --git a/frontend/src/views/Workflows.vue b/frontend/src/views/Workflows.vue index b6d6054..d2e8f06 100644 --- a/frontend/src/views/Workflows.vue +++ b/frontend/src/views/Workflows.vue @@ -248,11 +248,11 @@ export default { } }, async viewWorkflowDetails(workflow) { - console.log('Viewing details for workflow:', workflow); - const currentStep = workflow.current_step || - workflow.payload?.current_step || - getWorkflow(workflow.name)?.stepDefinitions?.[0]?.step || - "initial"; + const currentStep = String( + workflow.current_step || + getWorkflow(workflow.slug)?.stepDefinitions?.[0]?.step || + 1 + ); this.$router.push({ name: 'workflow-detail',