From 0d394d531b06318465c2687ef991098503f804f4 Mon Sep 17 00:00:00 2001 From: jedi Date: Thu, 22 Jun 2023 11:44:28 +0200 Subject: [PATCH 1/3] add Friendrequests --- backend/authentication/signature_auth.py | 2 ++ backend/toolshed/api/friend.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/authentication/signature_auth.py b/backend/authentication/signature_auth.py index fd36705..f6b280a 100644 --- a/backend/authentication/signature_auth.py +++ b/backend/authentication/signature_auth.py @@ -60,6 +60,8 @@ def verify_incoming_friend_request(request, raw_request_body): befriender_key = request.data['befriender_key'] except KeyError: return False + if not befriender or not befriender_key: + return False if username + "@" + domain != befriender: return False if len(befriender_key) != 64: diff --git a/backend/toolshed/api/friend.py b/backend/toolshed/api/friend.py index efedd80..a0337c5 100644 --- a/backend/toolshed/api/friend.py +++ b/backend/toolshed/api/friend.py @@ -72,7 +72,7 @@ class FriendsRequests(APIView, ViewSetMixin): befriender_username=befriender_username, befriender_domain=befriender_domain, befriender_public_key=user.public_identity.public_key, - secret=secret, # request.data['secret'] # TODO ?? + secret=secret, befriendee_user=befriendee_user.get(), ) return Response(status=status.HTTP_201_CREATED, data={'secret': secret, 'status': "pending"}) @@ -81,7 +81,7 @@ class FriendsRequests(APIView, ViewSetMixin): befriender_user=user, befriendee_username=befriendee_username, befriendee_domain=befriendee_domain, - secret=secret, # request.data['secret'] # TODO ?? + secret=secret, ) return Response(status=status.HTTP_201_CREATED, data={'secret': secret, 'status': "pending"}) elif verify_incoming_friend_request(request, raw_request): From 93335b27762e786159250a8a1fbf2f67c5de711b Mon Sep 17 00:00:00 2001 From: jedi Date: Wed, 29 Nov 2023 23:38:01 +0100 Subject: [PATCH 2/3] add StorageLocation model --- .gitignore | 1 + backend/toolshed/api/inventory.py | 16 ++++- ...location_inventoryitem_storage_location.py | 32 +++++++++ backend/toolshed/models.py | 15 ++++ backend/toolshed/serializers.py | 23 +++++- backend/toolshed/tests/fixtures.py | 18 ++++- backend/toolshed/tests/test_friend.py | 11 +++ backend/toolshed/tests/test_locations.py | 71 +++++++++++++++++++ 8 files changed, 179 insertions(+), 8 deletions(-) create mode 100644 backend/toolshed/migrations/0004_storagelocation_inventoryitem_storage_location.py create mode 100644 backend/toolshed/tests/test_locations.py diff --git a/.gitignore b/.gitignore index e2b6211..2e85c98 100644 --- a/.gitignore +++ b/.gitignore @@ -129,4 +129,5 @@ dmypy.json .pyre/ staticfiles/ +userfiles/ testdata.py \ No newline at end of file diff --git a/backend/toolshed/api/inventory.py b/backend/toolshed/api/inventory.py index b60ebee..1d7db7f 100644 --- a/backend/toolshed/api/inventory.py +++ b/backend/toolshed/api/inventory.py @@ -7,8 +7,8 @@ from rest_framework.response import Response from authentication.models import ToolshedUser, KnownIdentity from authentication.signature_auth import SignatureAuthentication -from toolshed.models import InventoryItem -from toolshed.serializers import InventoryItemSerializer +from toolshed.models import InventoryItem, StorageLocation +from toolshed.serializers import InventoryItemSerializer, StorageLocationSerializer router = routers.SimpleRouter() @@ -61,7 +61,19 @@ def search_inventory_items(request): return Response({'error': 'No query provided.'}, status=400) +class StorageLocationViewSet(viewsets.ModelViewSet): + serializer_class = StorageLocationSerializer + authentication_classes = [SignatureAuthentication] + permission_classes = [IsAuthenticated] + + def get_queryset(self): + if type(self.request.user) == KnownIdentity and self.request.user.user.exists(): + return StorageLocation.objects.filter(owner=self.request.user.user.get()) + return StorageLocation.objects.none() + + router.register(r'inventory_items', InventoryItemViewSet, basename='inventory_items') +router.register(r'storage_locations', StorageLocationViewSet, basename='storage_locations') urlpatterns = router.urls + [ path('search/', search_inventory_items, name='search_inventory_items'), diff --git a/backend/toolshed/migrations/0004_storagelocation_inventoryitem_storage_location.py b/backend/toolshed/migrations/0004_storagelocation_inventoryitem_storage_location.py new file mode 100644 index 0000000..83dc7a9 --- /dev/null +++ b/backend/toolshed/migrations/0004_storagelocation_inventoryitem_storage_location.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.2 on 2024-02-20 13:50 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('toolshed', '0003_inventoryitem_files_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='StorageLocation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('description', models.TextField(blank=True, null=True)), + ('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='storage_locations', to='toolshed.category')), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='storage_locations', to=settings.AUTH_USER_MODEL)), + ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='toolshed.storagelocation')), + ], + ), + migrations.AddField( + model_name='inventoryitem', + name='storage_location', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='inventory_items', to='toolshed.storagelocation'), + ), + ] diff --git a/backend/toolshed/models.py b/backend/toolshed/models.py index f1448a9..14ee76b 100644 --- a/backend/toolshed/models.py +++ b/backend/toolshed/models.py @@ -62,6 +62,8 @@ class InventoryItem(SoftDeleteModel): tags = models.ManyToManyField(Tag, through='ItemTag', related_name='inventory_items') properties = models.ManyToManyField(Property, through='ItemProperty') files = models.ManyToManyField(File, related_name='connected_items') + storage_location = models.ForeignKey('StorageLocation', on_delete=models.CASCADE, null=True, blank=True, + related_name='inventory_items') def clean(self): if (self.name is None or self.name == "") and self.files.count() == 0: @@ -77,3 +79,16 @@ class ItemProperty(models.Model): class ItemTag(models.Model): tag = models.ForeignKey(Tag, on_delete=models.CASCADE) inventory_item = models.ForeignKey(InventoryItem, on_delete=models.CASCADE) + + +class StorageLocation(models.Model): + name = models.CharField(max_length=255) + description = models.TextField(null=True, blank=True) + category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, blank=True, + related_name='storage_locations') + parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children') + owner = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='storage_locations') + + def __str__(self): + parent = str(self.parent) + "/" if self.parent else "" + return parent + self.name diff --git a/backend/toolshed/serializers.py b/backend/toolshed/serializers.py index 82f9892..73edeff 100644 --- a/backend/toolshed/serializers.py +++ b/backend/toolshed/serializers.py @@ -3,7 +3,7 @@ from authentication.models import KnownIdentity, ToolshedUser, FriendRequestInco from authentication.serializers import OwnerSerializer from files.models import File from files.serializers import FileSerializer -from toolshed.models import Category, Property, ItemProperty, InventoryItem, Tag +from toolshed.models import Category, Property, ItemProperty, InventoryItem, Tag, StorageLocation class FriendSerializer(serializers.ModelSerializer): @@ -11,7 +11,7 @@ class FriendSerializer(serializers.ModelSerializer): class Meta: model = KnownIdentity - fields = ['username', 'public_key'] + fields = ['id', 'username', 'public_key'] def get_username(self, obj): return obj.username + '@' + obj.domain @@ -48,6 +48,23 @@ class CategorySerializer(serializers.ModelSerializer): return Category.objects.get(name=data.split("/")[-1]) +class StorageLocationSerializer(serializers.ModelSerializer): + owner = OwnerSerializer(read_only=True) + category = CategorySerializer(required=False, allow_null=True) + path = serializers.SerializerMethodField() + + class Meta: + model = StorageLocation + fields = ['id', 'name', 'description', 'path', 'category', 'owner'] + read_only_fields = ['path'] + + @staticmethod + def get_path(obj): + if obj.parent: + return StorageLocationSerializer.get_path(obj.parent) + "/" + obj.name + return obj.name + + class ItemPropertySerializer(serializers.ModelSerializer): property = PropertySerializer(read_only=True) @@ -74,7 +91,7 @@ class InventoryItemSerializer(serializers.ModelSerializer): class Meta: model = InventoryItem fields = ['id', 'name', 'description', 'owner', 'category', 'availability_policy', 'owned_quantity', 'owner', - 'tags', 'properties', 'files'] + 'tags', 'properties', 'files', 'storage_location'] def to_internal_value(self, data): files = data.pop('files', []) diff --git a/backend/toolshed/tests/fixtures.py b/backend/toolshed/tests/fixtures.py index 6755591..6b866cc 100644 --- a/backend/toolshed/tests/fixtures.py +++ b/backend/toolshed/tests/fixtures.py @@ -1,4 +1,4 @@ -from toolshed.models import Category, Tag, Property, InventoryItem, ItemProperty +from toolshed.models import Category, Tag, Property, InventoryItem, ItemProperty, StorageLocation class CategoryTestMixin: @@ -13,8 +13,10 @@ class CategoryTestMixin: class TagTestMixin: def prepare_tags(self): - self.f['tag1'] = Tag.objects.create(name='tag1', description='tag1 description', category=self.f['cat1'], origin='test') - self.f['tag2'] = Tag.objects.create(name='tag2', description='tag2 description', category=self.f['cat1'], origin='test') + self.f['tag1'] = Tag.objects.create(name='tag1', description='tag1 description', category=self.f['cat1'], + origin='test') + self.f['tag2'] = Tag.objects.create(name='tag2', description='tag2 description', category=self.f['cat1'], + origin='test') self.f['tag3'] = Tag.objects.create(name='tag3', origin='test') @@ -41,3 +43,13 @@ class InventoryTestMixin(CategoryTestMixin, TagTestMixin, PropertyTestMixin): 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() ItemProperty.objects.create(inventory_item=self.f['item2'], property=self.f['prop2'], value='value2').save() + + +class LocationTestMixin: + def prepare_locations(self): + self.f['loc1'] = StorageLocation.objects.create(name='loc1', owner=self.f['local_user1']) + self.f['loc2'] = StorageLocation.objects.create(name='loc2', owner=self.f['local_user1'], + category=self.f['cat1']) + self.f['loc3'] = StorageLocation.objects.create(name='loc3', owner=self.f['local_user1'], parent=self.f['loc1']) + self.f['loc4'] = StorageLocation.objects.create(name='loc4', owner=self.f['local_user1'], parent=self.f['loc1'], + category=self.f['cat1']) diff --git a/backend/toolshed/tests/test_friend.py b/backend/toolshed/tests/test_friend.py index a6da890..d689600 100644 --- a/backend/toolshed/tests/test_friend.py +++ b/backend/toolshed/tests/test_friend.py @@ -210,6 +210,17 @@ class FriendRequestIncomingTestCase(UserTestMixin, ToolshedTestCase): }) self.assertEqual(reply.status_code, 400) + def test_post_request_missing_key_none(self): + befriender = self.f['ext_user1'] + befriendee = self.f['local_user1'] + reply = client.post('/api/friendrequests/', befriender, { + 'befriender': str(befriender), + 'befriendee': str(befriendee), + 'befriender_key': None, + 'secret': 'secret2' + }) + self.assertEqual(reply.status_code, 400) + def test_post_request_breaking_key(self): befriender = self.f['ext_user1'] befriendee = self.f['local_user1'] diff --git a/backend/toolshed/tests/test_locations.py b/backend/toolshed/tests/test_locations.py new file mode 100644 index 0000000..d792857 --- /dev/null +++ b/backend/toolshed/tests/test_locations.py @@ -0,0 +1,71 @@ +from authentication.tests import SignatureAuthClient, UserTestMixin, ToolshedTestCase +from files.tests import FilesTestMixin +from toolshed.models import InventoryItem, Category +from toolshed.tests import InventoryTestMixin, LocationTestMixin + +client = SignatureAuthClient() + + +class LocationApiTestCase(UserTestMixin, InventoryTestMixin, LocationTestMixin, ToolshedTestCase): + + def setUp(self): + super().setUp() + self.prepare_users() + self.prepare_categories() + self.prepare_tags() + self.prepare_properties() + self.prepare_locations() + self.prepare_inventory() + + def test_locations(self): + self.assertEqual("loc1", str(self.f['loc1'])) + self.assertEqual("loc1", self.f['loc1'].name) + self.assertEqual("loc2", str(self.f['loc2'])) + self.assertEqual("loc2", self.f['loc2'].name) + self.assertEqual("loc1/loc3", str(self.f['loc3'])) + self.assertEqual("loc3", self.f['loc3'].name) + self.assertEqual(self.f['loc1'], self.f['loc3'].parent) + self.assertEqual("loc1/loc4", str(self.f['loc4'])) + self.assertEqual("loc4", self.f['loc4'].name) + self.assertEqual(self.f['loc1'], self.f['loc4'].parent) + + def test_get_inventory(self): + reply = client.get('/api/inventory_items/', self.f['local_user1']) + self.assertEqual(reply.status_code, 200) + self.assertEqual(len(reply.json()), 2) + self.assertEqual(reply.json()[0]['name'], 'test1') + self.assertEqual(reply.json()[0]['description'], 'test') + self.assertEqual(reply.json()[0]['owned_quantity'], 1) + self.assertEqual(reply.json()[0]['tags'], []) + self.assertEqual(reply.json()[0]['properties'], []) + self.assertEqual(reply.json()[0]['category'], 'cat1') + self.assertEqual(reply.json()[0]['availability_policy'], 'friends') + self.assertEqual(reply.json()[1]['name'], 'test2') + self.assertEqual(reply.json()[1]['description'], 'test2') + self.assertEqual(reply.json()[1]['owned_quantity'], 1) + self.assertEqual(reply.json()[1]['tags'], ['tag1', 'tag2']) + self.assertEqual(reply.json()[1]['properties'], + [{'name': 'prop1', 'value': 'value1'}, {'name': 'prop2', 'value': 'value2'}]) + self.assertEqual(reply.json()[1]['category'], 'cat1') + self.assertEqual(reply.json()[1]['availability_policy'], 'friends') + + def test_get_inventory_item(self): + reply = client.get('/api/storage_locations/', self.f['local_user1']) + self.assertEqual(reply.status_code, 200) + self.assertEqual(len(reply.json()), 4) + self.assertEqual(reply.json()[0]['name'], 'loc1') + self.assertEqual(reply.json()[0]['description'], None) + self.assertEqual(reply.json()[0]['category'], None) + self.assertEqual(reply.json()[0]['path'], 'loc1') + self.assertEqual(reply.json()[1]['name'], 'loc2') + self.assertEqual(reply.json()[1]['description'], None) + self.assertEqual(reply.json()[1]['category'], 'cat1') + self.assertEqual(reply.json()[1]['path'], 'loc2') + self.assertEqual(reply.json()[2]['name'], 'loc3') + self.assertEqual(reply.json()[2]['description'], None) + self.assertEqual(reply.json()[2]['category'], None) + self.assertEqual(reply.json()[2]['path'], 'loc1/loc3') + self.assertEqual(reply.json()[3]['name'], 'loc4') + self.assertEqual(reply.json()[3]['description'], None) + self.assertEqual(reply.json()[3]['category'], 'cat1') + self.assertEqual(reply.json()[3]['path'], 'loc1/loc4') From f4894d3a8c8bea946c769a99832b5a366f452d61 Mon Sep 17 00:00:00 2001 From: jedi Date: Sat, 17 Feb 2024 19:32:19 +0100 Subject: [PATCH 3/3] hide private items from friends in search --- backend/toolshed/api/inventory.py | 3 ++- ..._alter_inventoryitem_availability_policy.py | 18 ++++++++++++++++++ backend/toolshed/models.py | 10 +++++++++- backend/toolshed/tests/test_inventory.py | 18 +++++++++--------- 4 files changed, 38 insertions(+), 11 deletions(-) create mode 100644 backend/toolshed/migrations/0005_alter_inventoryitem_availability_policy.py diff --git a/backend/toolshed/api/inventory.py b/backend/toolshed/api/inventory.py index 1d7db7f..c39a5c8 100644 --- a/backend/toolshed/api/inventory.py +++ b/backend/toolshed/api/inventory.py @@ -24,7 +24,8 @@ def inventory_items(identity): for friend in identity.friends.all(): if friend_user := friend.user.get(): for item in friend_user.inventory_items.all(): - yield item + if item.availability_policy != 'private': + yield item class InventoryItemViewSet(viewsets.ModelViewSet): diff --git a/backend/toolshed/migrations/0005_alter_inventoryitem_availability_policy.py b/backend/toolshed/migrations/0005_alter_inventoryitem_availability_policy.py new file mode 100644 index 0000000..fa86880 --- /dev/null +++ b/backend/toolshed/migrations/0005_alter_inventoryitem_availability_policy.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.2 on 2024-02-20 15:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('toolshed', '0004_storagelocation_inventoryitem_storage_location'), + ] + + operations = [ + migrations.AlterField( + model_name='inventoryitem', + name='availability_policy', + field=models.CharField(choices=[('sell', 'Sell'), ('rent', 'Rent'), ('lend', 'Lend'), ('share', 'Share'), ('private', 'Private')], default='private', max_length=20), + ), + ] diff --git a/backend/toolshed/models.py b/backend/toolshed/models.py index 14ee76b..de32826 100644 --- a/backend/toolshed/models.py +++ b/backend/toolshed/models.py @@ -50,12 +50,20 @@ class Tag(models.Model): class InventoryItem(SoftDeleteModel): + AVAILABILITY_POLICY_CHOICES = ( + ('sell', 'Sell'), + ('rent', 'Rent'), + ('lend', 'Lend'), + ('share', 'Share'), + ('private', 'Private'), + ) + published = models.BooleanField(default=False) name = models.CharField(max_length=255, null=True, blank=True) description = models.TextField(null=True, blank=True) category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, blank=True, related_name='inventory_items') - availability_policy = models.CharField(max_length=255, default="private") + availability_policy = models.CharField(max_length=20, choices=AVAILABILITY_POLICY_CHOICES, default='private') owned_quantity = models.IntegerField(default=1, validators=[MinValueValidator(0)]) owner = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='inventory_items') created_at = models.DateTimeField(auto_now_add=True) diff --git a/backend/toolshed/tests/test_inventory.py b/backend/toolshed/tests/test_inventory.py index 747c19e..a05421c 100644 --- a/backend/toolshed/tests/test_inventory.py +++ b/backend/toolshed/tests/test_inventory.py @@ -38,7 +38,7 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase): def test_post_new_item(self): reply = client.post('/api/inventory_items/', self.f['local_user1'], { - 'availability_policy': 'friends', + 'availability_policy': 'rent', 'category': 'cat2', 'name': 'test3', 'description': 'test', @@ -50,7 +50,7 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase): self.assertEqual(reply.status_code, 201) self.assertEqual(InventoryItem.objects.count(), 3) item = InventoryItem.objects.get(name='test3') - self.assertEqual(item.availability_policy, 'friends') + self.assertEqual(item.availability_policy, 'rent') self.assertEqual(item.category, Category.objects.get(name='cat2')) self.assertEqual(item.name, 'test3') self.assertEqual(item.description, 'test') @@ -61,7 +61,7 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase): def test_post_new_item2(self): reply = client.post('/api/inventory_items/', self.f['local_user1'], { - 'availability_policy': 'friends', + 'availability_policy': 'share', 'name': 'test3', 'description': 'test', 'owned_quantity': 1, @@ -70,7 +70,7 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase): self.assertEqual(reply.status_code, 201) self.assertEqual(InventoryItem.objects.count(), 3) item = InventoryItem.objects.get(name='test3') - self.assertEqual(item.availability_policy, 'friends') + self.assertEqual(item.availability_policy, 'share') self.assertEqual(item.category, None) self.assertEqual(item.name, 'test3') self.assertEqual(item.description, 'test') @@ -80,7 +80,7 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase): def test_post_new_item_empty(self): reply = client.post('/api/inventory_items/', self.f['local_user1'], { - 'availability_policy': 'friends', + 'availability_policy': 'rent', 'owned_quantity': 1, 'image': '', }) @@ -89,7 +89,7 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase): def test_post_new_item3(self): reply = client.post('/api/inventory_items/', self.f['local_user1'], { - 'availability_policy': 'friends', + 'availability_policy': 'private', 'name': 'test3', 'description': 'test', 'owned_quantity': 1, @@ -99,7 +99,7 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase): self.assertEqual(reply.status_code, 201) self.assertEqual(InventoryItem.objects.count(), 3) item = InventoryItem.objects.get(name='test3') - self.assertEqual(item.availability_policy, 'friends') + self.assertEqual(item.availability_policy, 'private') self.assertEqual(item.category, None) self.assertEqual(item.name, 'test3') self.assertEqual(item.description, 'test') @@ -109,7 +109,7 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase): def test_put_item(self): reply = client.put('/api/inventory_items/1/', self.f['local_user1'], { - 'availability_policy': 'friends', + 'availability_policy': 'sell', 'name': 'test4', 'description': 'new description', 'owned_quantity': 100, @@ -121,7 +121,7 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase): self.assertEqual(reply.status_code, 200) self.assertEqual(InventoryItem.objects.count(), 2) item = InventoryItem.objects.get(id=1) - self.assertEqual(item.availability_policy, 'friends') + self.assertEqual(item.availability_policy, 'sell') self.assertEqual(item.category, None) self.assertEqual(item.name, 'test4') self.assertEqual(item.description, 'new description')