From 1853577ff5a5c5be8f76c5a4983769d5b078eeef Mon Sep 17 00:00:00 2001 From: jedi Date: Tue, 1 Sep 2026 02:10:50 +0200 Subject: [PATCH] stash visibility --- backend/toolshed/admin.py | 16 +++++++----- backend/toolshed/api/idmap.py | 3 ++- backend/toolshed/api/info.py | 13 ++++++++-- backend/toolshed/api/inventory.py | 11 +++++--- ...nventoryitem_visibility_policy_and_more.py | 23 ++++++++++++++++ backend/toolshed/models.py | 12 +++++++++ backend/toolshed/offlinedata.py | 4 +++ backend/toolshed/serializers.py | 6 +++-- backend/toolshed/tests/fixtures.py | 14 +++++----- backend/toolshed/tests/test_api.py | 11 ++++++++ backend/toolshed/tests/test_idmap.py | 21 +++++++++++++++ backend/toolshed/tests/test_inventory.py | 20 ++++++++++++++ backend/toolshed/tests/test_locations.py | 17 ++++++++++++ backend/toolshed/tests/test_offlinedata.py | 3 ++- .../workflows/FotoFirstBulkImportWorkflow.vue | 7 ++++- .../workflows/FotoFirstFieldInput.vue | 10 +++++++ frontend/src/store.js | 26 ++++++++++++++++--- frontend/src/views/Admin.vue | 14 +++++++++- frontend/src/views/Inventory.vue | 9 +++++-- frontend/src/views/InventoryEdit.vue | 12 ++++++++- frontend/src/views/InventoryNew.vue | 13 +++++++++- frontend/src/views/StorageLocation.vue | 11 +++++--- frontend/src/views/StorageLocationDetail.vue | 6 +++++ frontend/src/views/StorageLocationEdit.vue | 13 +++++++++- frontend/src/views/StorageLocationNew.vue | 13 +++++++++- testdata/generate_testdata.py | 7 +++-- 26 files changed, 276 insertions(+), 39 deletions(-) create mode 100644 backend/toolshed/migrations/0025_inventoryitem_visibility_policy_and_more.py diff --git a/backend/toolshed/admin.py b/backend/toolshed/admin.py index d813ac7..cea1e9b 100644 --- a/backend/toolshed/admin.py +++ b/backend/toolshed/admin.py @@ -18,10 +18,11 @@ class ItemPropertyInline(admin.TabularInline): class InventoryItemAdmin(admin.ModelAdmin): - list_display = ('name', 'description', 'category', 'availability_policy', 'owned_quantity', 'owner', - 'owner_group', 'storage_location', 'get_tags', 'get_properties') - search_fields = ('name', 'description', 'category__name', 'availability_policy', 'owner__username', - 'owner_group__name', 'storage_location__name', 'tags__name', 'itemproperty__property__name') + list_display = ('name', 'description', 'category', 'availability_policy', 'visibility_policy', 'owned_quantity', + 'owner', 'owner_group', 'storage_location', 'get_tags', 'get_properties') + search_fields = ('name', 'description', 'category__name', 'availability_policy', 'visibility_policy', + 'owner__username', 'owner_group__name', 'storage_location__name', 'tags__name', + 'itemproperty__property__name') inlines = (ItemTagInline, ItemPropertyInline) def get_queryset(self, request): @@ -64,9 +65,10 @@ admin.site.register(Category, CategoryAdmin) class StorageLocationAdmin(admin.ModelAdmin): - list_display = ('name', 'description', 'category', 'parent', 'owner') - search_fields = ('name', 'description', 'category__name', 'parent__name', 'owner__username') - list_filter = ('category', 'owner') + list_display = ('name', 'description', 'category', 'parent', 'owner', 'visibility_policy') + search_fields = ('name', 'description', 'category__name', 'parent__name', 'owner__username', + 'visibility_policy') + list_filter = ('category', 'owner', 'visibility_policy') admin.site.register(StorageLocation, StorageLocationAdmin) diff --git a/backend/toolshed/api/idmap.py b/backend/toolshed/api/idmap.py index a6451f1..d6050f9 100644 --- a/backend/toolshed/api/idmap.py +++ b/backend/toolshed/api/idmap.py @@ -43,7 +43,8 @@ def resolve_short_id(request, kind, owner_id, local_id): except model.DoesNotExist: return Response(status=404) is_owner = request.user.user.filter(pk=owner.pk).exists() - if getattr(obj, 'availability_policy', 'share') == 'private' and not is_owner: + if not is_owner and (getattr(obj, 'availability_policy', 'share') == 'private' + or obj.visibility_policy == 'private'): return Response(status=403) return Response({'handle': f'{owner.username}@{owner.domain}', 'id': obj.id}) if kind in ('group_item', 'group_storage_location'): diff --git a/backend/toolshed/api/info.py b/backend/toolshed/api/info.py index 6fc5036..8d9e7dd 100644 --- a/backend/toolshed/api/info.py +++ b/backend/toolshed/api/info.py @@ -46,6 +46,13 @@ def list_availability_policies(request, format=None): return Response(InventoryItem.AVAILABILITY_POLICY_CHOICES) +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([SignatureAuthentication]) +def list_visibility_policies(request, format=None): + return Response(InventoryItem.VISIBILITY_POLICY_CHOICES) + + @api_view(['GET']) @permission_classes([IsAuthenticated]) @authentication_classes([SignatureAuthentication]) @@ -54,15 +61,17 @@ def combined_info(request, format=None): properties = PropertySerializer(Property.objects.all(), many=True).data categories = [str(category) for category in Category.objects.all()] policies = InventoryItem.AVAILABILITY_POLICY_CHOICES + visibility_policies = InventoryItem.VISIBILITY_POLICY_CHOICES domains = [domain.name for domain in Domain.objects.filter(open_registration=True)] return Response( - {'tags': tags, 'properties': properties, 'availability_policies': policies, 'categories': categories, - 'domains': domains}) + {'tags': tags, 'properties': properties, 'availability_policies': policies, + 'visibility_policies': visibility_policies, 'categories': categories, 'domains': domains}) urlpatterns = [ path('availability_policies/', list_availability_policies, name='availability_policies'), + path('visibility_policies/', list_visibility_policies, name='visibility_policies'), path('properties/', list_properties, name='propertylist'), path('categories/', list_categories, name='categorylist'), path('domains/', list_domains, name='domainlist'), diff --git a/backend/toolshed/api/inventory.py b/backend/toolshed/api/inventory.py index 15b7eac..7cc61d1 100644 --- a/backend/toolshed/api/inventory.py +++ b/backend/toolshed/api/inventory.py @@ -25,7 +25,7 @@ def inventory_items(identity): friend_user = friend.user.first() if friend_user: for item in friend_user.inventory_items.all(): - if item.availability_policy != 'private': + if item.availability_policy != 'private' and item.visibility_policy != 'private': yield item @@ -55,7 +55,7 @@ class InventoryItemViewSet(viewsets.ModelViewSet): return InventoryItem.objects.none() queryset = InventoryItem.objects.filter(owner=owner_user) if not identity.user.filter(pk=owner_user.pk).exists(): - queryset = queryset.exclude(availability_policy='private') + queryset = queryset.exclude(availability_policy='private').exclude(visibility_policy='private') else: return InventoryItem.objects.none() # InventoryItemSerializer touches owner/owner_group/category/storage_location (FKs) and @@ -132,7 +132,7 @@ class StorageLocationViewSet(viewsets.ModelViewSet): lookup_url_kwarg = 'pk' def get_queryset(self): - # See docs/implementation.md#owner-handle-scoped-routes; unlike items, StorageLocation has no availability_policy, so a friend sees all of a user's locations. + # See docs/implementation.md#owner-handle-scoped-routes; unlike items, StorageLocation has no availability_policy, but visibility_policy='private' still hides a location from friends. if type(self.request.user) != KnownIdentity: return StorageLocation.objects.none() identity = self.request.user @@ -147,7 +147,10 @@ class StorageLocationViewSet(viewsets.ModelViewSet): if owner_user: if owner_user not in identity.friends_or_self(): return StorageLocation.objects.none() - return StorageLocation.objects.filter(owner=owner_user) + queryset = StorageLocation.objects.filter(owner=owner_user) + if not identity.user.filter(pk=owner_user.pk).exists(): + queryset = queryset.exclude(visibility_policy='private') + return queryset return StorageLocation.objects.none() def perform_create(self, serializer): diff --git a/backend/toolshed/migrations/0025_inventoryitem_visibility_policy_and_more.py b/backend/toolshed/migrations/0025_inventoryitem_visibility_policy_and_more.py new file mode 100644 index 0000000..e427932 --- /dev/null +++ b/backend/toolshed/migrations/0025_inventoryitem_visibility_policy_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.2 on 2026-08-31 23:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('toolshed', '0024_workflowinstance_current_step'), + ] + + operations = [ + migrations.AddField( + model_name='inventoryitem', + name='visibility_policy', + field=models.CharField(choices=[('public', 'Public'), ('friends', 'Friends'), ('private', 'Private')], default='friends', max_length=20), + ), + migrations.AddField( + model_name='storagelocation', + name='visibility_policy', + field=models.CharField(choices=[('public', 'Public'), ('friends', 'Friends'), ('private', 'Private')], default='friends', max_length=20), + ), + ] diff --git a/backend/toolshed/models.py b/backend/toolshed/models.py index 65de960..6113f6a 100644 --- a/backend/toolshed/models.py +++ b/backend/toolshed/models.py @@ -107,6 +107,13 @@ class OwnerItemSequence(models.Model): return seq.last_id +VISIBILITY_POLICY_CHOICES = ( + ('public', 'Public'), + ('friends', 'Friends'), + ('private', 'Private'), +) + + class InventoryItem(SoftDeleteModel): AVAILABILITY_POLICY_CHOICES = ( ('sell', 'Sell'), @@ -115,6 +122,7 @@ class InventoryItem(SoftDeleteModel): ('share', 'Share'), ('private', 'Private'), ) + VISIBILITY_POLICY_CHOICES = VISIBILITY_POLICY_CHOICES internal_id = models.AutoField(primary_key=True) # Externally visible id, sequential/gapless within owner/owner_group's own items (see @@ -125,6 +133,7 @@ class InventoryItem(SoftDeleteModel): description = models.TextField(null=True, blank=True) category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, related_name='inventory_items') availability_policy = models.CharField(max_length=20, choices=AVAILABILITY_POLICY_CHOICES, default='private') + visibility_policy = models.CharField(max_length=20, choices=VISIBILITY_POLICY_CHOICES, default='private') owned_quantity = models.IntegerField(default=1, validators=[MinValueValidator(0)]) owner = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, null=True, blank=True, related_name='inventory_items') @@ -195,6 +204,8 @@ class OwnerStorageLocationSequence(models.Model): class StorageLocation(models.Model): + VISIBILITY_POLICY_CHOICES = VISIBILITY_POLICY_CHOICES + internal_id = models.AutoField(primary_key=True) # Externally visible id, sequential/gapless within the owner/owner_group's own locations (see # OwnerStorageLocationSequence), never internal_id; always allocate via create_for_owner, not @@ -209,6 +220,7 @@ class StorageLocation(models.Model): related_name='storage_locations') owner_group = models.ForeignKey(Group, on_delete=models.CASCADE, null=True, blank=True, related_name='storage_locations') + visibility_policy = models.CharField(max_length=20, choices=VISIBILITY_POLICY_CHOICES, default='private') class Meta: constraints = [ diff --git a/backend/toolshed/offlinedata.py b/backend/toolshed/offlinedata.py index ee9f383..0f93f1e 100644 --- a/backend/toolshed/offlinedata.py +++ b/backend/toolshed/offlinedata.py @@ -24,6 +24,7 @@ def inventory_rows(user): 'description': item.description or '', 'category': item.category.get_handle() if item.category else '', 'availability_policy': item.availability_policy, + 'visibility_policy': item.visibility_policy, 'owned_quantity': item.owned_quantity, 'storage_location': str(item.storage_location) if item.storage_location else '', 'tags': ', '.join(tag.get_handle() for tag in item.tags.all()), @@ -67,6 +68,7 @@ def location_rows(user): 'category': str(location.category) if location.category else '', 'parent': str(location.parent) if location.parent else '', 'path': location_path(location), + 'visibility_policy': location.visibility_policy, } @@ -342,6 +344,7 @@ def import_locations(user, data): defaults = { 'description': row.get('description', '') or '', 'category': category, + 'visibility_policy': row.get('visibility_policy') or 'private', } try: location = StorageLocation.objects.get(owner=user, name=name, parent=parent) @@ -551,6 +554,7 @@ def import_inventory(user, data, available_files): description=row.get('description', '') or '', category=category, availability_policy=row.get('availability_policy') or 'private', + visibility_policy=row.get('visibility_policy') or 'private', owned_quantity=owned_quantity, storage_location=storage_location, ) diff --git a/backend/toolshed/serializers.py b/backend/toolshed/serializers.py index 07f60b9..af21446 100644 --- a/backend/toolshed/serializers.py +++ b/backend/toolshed/serializers.py @@ -211,7 +211,8 @@ class StorageLocationSerializer(serializers.ModelSerializer): class Meta: model = StorageLocation - fields = ['id', 'name', 'description', 'path', 'category', 'owner', 'owner_group', 'parent'] + fields = ['id', 'name', 'description', 'path', 'category', 'owner', 'owner_group', 'parent', + 'visibility_policy'] read_only_fields = ['id', 'path'] @staticmethod @@ -258,7 +259,8 @@ class InventoryItemSerializer(serializers.ModelSerializer): class Meta: model = InventoryItem fields = ['id', 'name', 'description', 'owner', 'owner_group', 'category', 'availability_policy', - 'owned_quantity', 'tags', 'tags_input', 'properties', 'files', 'storage_location'] + 'visibility_policy', 'owned_quantity', 'tags', 'tags_input', 'properties', 'files', + 'storage_location'] read_only_fields = ['id'] def get_tags(self, obj): diff --git a/backend/toolshed/tests/fixtures.py b/backend/toolshed/tests/fixtures.py index 69c2e42..9f4e647 100644 --- a/backend/toolshed/tests/fixtures.py +++ b/backend/toolshed/tests/fixtures.py @@ -37,10 +37,10 @@ class InventoryTestMixin(CategoryTestMixin, TagTestMixin, PropertyTestMixin): self.f['item1'] = InventoryItem.create_for_owner( owner=self.f['local_user1'], owned_quantity=1, name='test1', description='test', category=self.f['cat1'], - availability_policy='friends') + availability_policy='friends', visibility_policy='friends') self.f['item2'] = InventoryItem.create_for_owner( owner=self.f['local_user1'], owned_quantity=1, name='test2', description='test2', category=self.f['cat1'], - availability_policy='friends') + availability_policy='friends', visibility_policy='friends') self.f['item2'].tags.add(self.f['tag1'], through_defaults={}) self.f['item2'].tags.add(self.f['tag2'], through_defaults={}) ItemProperty.objects.create(inventory_item=self.f['item2'], property=self.f['prop1'], value='value1').save() @@ -49,12 +49,14 @@ class InventoryTestMixin(CategoryTestMixin, TagTestMixin, PropertyTestMixin): class LocationTestMixin: def prepare_locations(self): - self.f['loc1'] = StorageLocation.create_for_owner(name='loc1', owner=self.f['local_user1']) + self.f['loc1'] = StorageLocation.create_for_owner(name='loc1', owner=self.f['local_user1'], + visibility_policy='friends') self.f['loc2'] = StorageLocation.create_for_owner(name='loc2', owner=self.f['local_user1'], - category=self.f['cat1']) - self.f['loc3'] = StorageLocation.create_for_owner(name='loc3', owner=self.f['local_user1'], parent=self.f['loc1']) + category=self.f['cat1'], visibility_policy='friends') + self.f['loc3'] = StorageLocation.create_for_owner(name='loc3', owner=self.f['local_user1'], parent=self.f['loc1'], + visibility_policy='friends') self.f['loc4'] = StorageLocation.create_for_owner(name='loc4', owner=self.f['local_user1'], parent=self.f['loc1'], - category=self.f['cat1']) + category=self.f['cat1'], visibility_policy='friends') class WorkflowTestMixin: diff --git a/backend/toolshed/tests/test_api.py b/backend/toolshed/tests/test_api.py index 69c25df..e8b41a1 100644 --- a/backend/toolshed/tests/test_api.py +++ b/backend/toolshed/tests/test_api.py @@ -46,6 +46,15 @@ class CombinedApiTestCase(UserTestMixin, CategoryTestMixin, TagTestMixin, Proper self.assertEqual(response.json(), [['sell', 'Sell'], ['rent', 'Rent'], ['lend', 'Lend'], ['share', 'Share'], ['private', 'Private']]) + def test_visibility_policy_api_anonymous(self): + response = anonymous_client.get('/api/v1/visibility_policies/') + self.assertEqual(response.status_code, 403) + + def test_visibility_policy_api(self): + response = client.get('/api/v1/visibility_policies/', self.f['local_user1']) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), [['public', 'Public'], ['friends', 'Friends'], ['private', 'Private']]) + def test_combined_api_anonymous(self): response = anonymous_client.get('/api/v1/info/') self.assertEqual(response.status_code, 403) @@ -55,6 +64,8 @@ class CombinedApiTestCase(UserTestMixin, CategoryTestMixin, TagTestMixin, Proper self.assertEqual(response.status_code, 200) self.assertEqual(response.json()['availability_policies'], [['sell', 'Sell'], ['rent', 'Rent'], ['lend', 'Lend'], ['share', 'Share'], ['private', 'Private']]) + self.assertEqual(response.json()['visibility_policies'], + [['public', 'Public'], ['friends', 'Friends'], ['private', 'Private']]) self.assertEqual(response.json()['categories'], ['cat1', 'cat2', 'cat3', 'cat1/subcat1', 'cat1/subcat2', 'cat1/subcat1/subcat1', 'cat1/subcat1/subcat2']) diff --git a/backend/toolshed/tests/test_idmap.py b/backend/toolshed/tests/test_idmap.py index c73335a..d486f84 100644 --- a/backend/toolshed/tests/test_idmap.py +++ b/backend/toolshed/tests/test_idmap.py @@ -89,6 +89,17 @@ class ResolveShortIdApiTestCase(UserTestMixin, InventoryTestMixin, GroupTestMixi self.f['local_user1']) self.assertEqual(reply.status_code, 200) + def test_resolve_item_visibility_private_not_owner(self): + private_item = InventoryItem.create_for_owner( + owner=self.f['local_user1'], owned_quantity=1, name='secret', availability_policy='friends', + visibility_policy='private') + reply = client.get(f'/api/v1/resolve_short_id/item/{self.owner_id}/{private_item.id}/', + self.f['local_user2']) + self.assertEqual(reply.status_code, 403) + reply = client.get(f'/api/v1/resolve_short_id/item/{self.owner_id}/{private_item.id}/', + self.f['local_user1']) + self.assertEqual(reply.status_code, 200) + def test_resolve_item_unknown_owner(self): reply = client.get('/api/v1/resolve_short_id/item/999999/1/', self.f['local_user2']) self.assertEqual(reply.status_code, 404) @@ -109,6 +120,16 @@ class ResolveShortIdApiTestCase(UserTestMixin, InventoryTestMixin, GroupTestMixi self.assertEqual(reply.status_code, 200) self.assertEqual(reply.json(), {'handle': 'testuser1@example.com', 'id': self.f['loc1'].id}) + def test_resolve_storage_location_visibility_private_not_owner(self): + private_location = StorageLocation.create_for_owner( + owner=self.f['local_user1'], name='secret-loc', visibility_policy='private') + reply = client.get(f'/api/v1/resolve_short_id/storage_location/{self.owner_id}/{private_location.id}/', + self.f['local_user2']) + self.assertEqual(reply.status_code, 403) + reply = client.get(f'/api/v1/resolve_short_id/storage_location/{self.owner_id}/{private_location.id}/', + self.f['local_user1']) + self.assertEqual(reply.status_code, 200) + def test_resolve_storage_location_unknown_local_id(self): reply = client.get(f'/api/v1/resolve_short_id/storage_location/{self.owner_id}/999999/', self.f['local_user1']) diff --git a/backend/toolshed/tests/test_inventory.py b/backend/toolshed/tests/test_inventory.py index 162d7c5..8a08b49 100644 --- a/backend/toolshed/tests/test_inventory.py +++ b/backend/toolshed/tests/test_inventory.py @@ -205,6 +205,15 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase): self.assertEqual(reply.json()[0]['name'], 'test1') self.assertEqual(reply.json()[1]['name'], 'test2') + def test_search_items_excludes_visibility_private(self): + InventoryItem.create_for_owner( + owner=self.f['local_user1'], owned_quantity=1, name='test-secret', availability_policy='friends', + visibility_policy='private') + reply = client.get('/api/v1/search/?query=test', self.f['local_user2']) + self.assertEqual(reply.status_code, 200) + self.assertEqual(len(reply.json()), 2) + self.assertNotIn('test-secret', [item['name'] for item in reply.json()]) + def test_search_items_fail(self): reply = client.get('/api/v1/search/', self.f['local_user1']) self.assertEqual(reply.status_code, 400) @@ -246,6 +255,17 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase): self.f['local_user1']) self.assertEqual(reply.status_code, 200) + def test_get_shared_item_visibility_private(self): + private_item = InventoryItem.create_for_owner( + owner=self.f['local_user1'], owned_quantity=1, name='secret', availability_policy='friends', + visibility_policy='private') + reply = client.get('/api/v1/inventory_items/testuser1@example.com/' + str(private_item.id) + '/', + self.f['local_user2']) + self.assertEqual(reply.status_code, 404) + reply = client.get('/api/v1/inventory_items/testuser1@example.com/' + str(private_item.id) + '/', + self.f['local_user1']) + self.assertEqual(reply.status_code, 200) + def test_get_shared_item_unknown_handle(self): reply = client.get('/api/v1/inventory_items/nobody@example.com/' + str(self.f['item1'].id) + '/', self.f['local_user2']) diff --git a/backend/toolshed/tests/test_locations.py b/backend/toolshed/tests/test_locations.py index c6d84b4..5c1ab75 100644 --- a/backend/toolshed/tests/test_locations.py +++ b/backend/toolshed/tests/test_locations.py @@ -128,6 +128,23 @@ class LocationApiTestCase(UserTestMixin, InventoryTestMixin, LocationTestMixin, self.assertEqual(reply.status_code, 200) self.assertEqual(len(reply.json()), 0) + def test_get_shared_location_visibility_private(self): + private_location = StorageLocation.create_for_owner( + owner=self.f['local_user1'], name='secret-loc', visibility_policy='private') + reply = client.get( + '/api/v1/storage_locations/{}/{}/'.format(self.own_handle, private_location.id), self.f['local_user2']) + self.assertEqual(reply.status_code, 404) + reply = client.get( + '/api/v1/storage_locations/{}/{}/'.format(self.own_handle, private_location.id), self.f['local_user1']) + self.assertEqual(reply.status_code, 200) + + def test_list_locations_excludes_visibility_private_for_friend(self): + StorageLocation.create_for_owner(owner=self.f['local_user1'], name='secret-loc', visibility_policy='private') + reply = client.get('/api/v1/storage_locations/{}/'.format(self.own_handle), self.f['local_user2']) + self.assertEqual(reply.status_code, 200) + self.assertEqual(len(reply.json()), 4) + self.assertNotIn('secret-loc', [loc['name'] for loc in reply.json()]) + def test_cannot_delete_other_users_location(self): # local_user2 is a friend of local_user1 (see prepare_inventory), so local_user1's own # handle resolves and the location is visible - friends can read but never write, so this diff --git a/backend/toolshed/tests/test_offlinedata.py b/backend/toolshed/tests/test_offlinedata.py index 8a5a732..c558fa4 100644 --- a/backend/toolshed/tests/test_offlinedata.py +++ b/backend/toolshed/tests/test_offlinedata.py @@ -233,7 +233,7 @@ class ExportImportApiRoundTripTestCase(UserTestMixin, CategoryTestMixin, TagTest item = InventoryItem.create_for_owner( owner=self.f['local_user1'], name='drill', description='cordless drill', - category=self.f['cat1'], availability_policy='friends', owned_quantity=2) + category=self.f['cat1'], availability_policy='friends', visibility_policy='public', owned_quantity=2) item.tags.add(self.f['tag1'], self.f['tag2'], through_defaults={}) ItemProperty.objects.create(inventory_item=item, property=self.f['prop1'], value='10cm, 20cm') ItemProperty.objects.create(inventory_item=item, property=self.f['prop2'], value='a=b') @@ -251,6 +251,7 @@ class ExportImportApiRoundTripTestCase(UserTestMixin, CategoryTestMixin, TagTest new_item = InventoryItem.objects.get(owner=self.f['local_user2'], name='drill') self.assertEqual(new_item.category, self.f['cat1']) + self.assertEqual(new_item.visibility_policy, 'public') self.assertEqual(sorted(t.name for t in new_item.tags.all()), ['tag1', 'tag2']) values = {ip.property.name: ip.value for ip in new_item.itemproperty_set.select_related('property')} diff --git a/frontend/src/components/workflow/workflows/FotoFirstBulkImportWorkflow.vue b/frontend/src/components/workflow/workflows/FotoFirstBulkImportWorkflow.vue index 1998a36..cc20c9c 100644 --- a/frontend/src/components/workflow/workflows/FotoFirstBulkImportWorkflow.vue +++ b/frontend/src/components/workflow/workflows/FotoFirstBulkImportWorkflow.vue @@ -98,6 +98,7 @@ @@ -187,6 +188,7 @@ @@ -293,6 +295,7 @@ const PRESET_FIELD_DEFS = [ {key: 'owner_group', label: 'Owner', type: 'owner_group'}, {key: 'owned_quantity', label: 'Quantity', type: 'number'}, {key: 'availability_policy', label: 'Availability Policy', type: 'availability_policy'}, + {key: 'visibility_policy', label: 'Visibility Policy', type: 'visibility_policy'}, {key: 'storage_location', label: 'Storage Location', type: 'storage_location'} ]; @@ -307,6 +310,7 @@ function defaultValueFor(key) { case 'owned_quantity': return 1; case 'availability_policy': + case 'visibility_policy': return 'private'; default: return ''; @@ -390,7 +394,7 @@ export default { } }, computed: { - ...mapState(['user', 'availability_policies', 'storage_locations', 'groups', 'groupMemberships']), + ...mapState(['user', 'availability_policies', 'visibility_policies', 'storage_locations', 'groups', 'groupMemberships']), ownerGroups() { const hostedHandles = new Set(this.groups.map(group => group.handle)); const foreign = this.groupMemberships.filter(m => !hostedHandles.has(m.handle)); @@ -619,6 +623,7 @@ export default { properties: details.properties, owned_quantity: details.owned_quantity, availability_policy: details.availability_policy, + visibility_policy: details.visibility_policy, storage_location: details.storage_location, owner_group: details.owner_group }); diff --git a/frontend/src/components/workflow/workflows/FotoFirstFieldInput.vue b/frontend/src/components/workflow/workflows/FotoFirstFieldInput.vue index 3f8291e..206c9a4 100644 --- a/frontend/src/components/workflow/workflows/FotoFirstFieldInput.vue +++ b/frontend/src/components/workflow/workflows/FotoFirstFieldInput.vue @@ -20,6 +20,12 @@ + + +
+ + +
+
+ + +
+
+ + +
+
+ + +