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 @@
+
+