Compare commits

...

3 commits

Author SHA1 Message Date
f4894d3a8c hide private items from friends in search
All checks were successful
continuous-integration/drone/push Build is passing
2024-02-20 16:21:12 +01:00
93335b2776 add StorageLocation model
All checks were successful
continuous-integration/drone/push Build is passing
2024-02-20 15:26:00 +01:00
0d394d531b add Friendrequests 2023-06-22 11:44:28 +02:00
12 changed files with 221 additions and 21 deletions

1
.gitignore vendored
View file

@ -129,4 +129,5 @@ dmypy.json
.pyre/ .pyre/
staticfiles/ staticfiles/
userfiles/
testdata.py testdata.py

View file

@ -60,6 +60,8 @@ def verify_incoming_friend_request(request, raw_request_body):
befriender_key = request.data['befriender_key'] befriender_key = request.data['befriender_key']
except KeyError: except KeyError:
return False return False
if not befriender or not befriender_key:
return False
if username + "@" + domain != befriender: if username + "@" + domain != befriender:
return False return False
if len(befriender_key) != 64: if len(befriender_key) != 64:

View file

@ -72,7 +72,7 @@ class FriendsRequests(APIView, ViewSetMixin):
befriender_username=befriender_username, befriender_username=befriender_username,
befriender_domain=befriender_domain, befriender_domain=befriender_domain,
befriender_public_key=user.public_identity.public_key, befriender_public_key=user.public_identity.public_key,
secret=secret, # request.data['secret'] # TODO ?? secret=secret,
befriendee_user=befriendee_user.get(), befriendee_user=befriendee_user.get(),
) )
return Response(status=status.HTTP_201_CREATED, data={'secret': secret, 'status': "pending"}) return Response(status=status.HTTP_201_CREATED, data={'secret': secret, 'status': "pending"})
@ -81,7 +81,7 @@ class FriendsRequests(APIView, ViewSetMixin):
befriender_user=user, befriender_user=user,
befriendee_username=befriendee_username, befriendee_username=befriendee_username,
befriendee_domain=befriendee_domain, befriendee_domain=befriendee_domain,
secret=secret, # request.data['secret'] # TODO ?? secret=secret,
) )
return Response(status=status.HTTP_201_CREATED, data={'secret': secret, 'status': "pending"}) return Response(status=status.HTTP_201_CREATED, data={'secret': secret, 'status': "pending"})
elif verify_incoming_friend_request(request, raw_request): elif verify_incoming_friend_request(request, raw_request):

View file

@ -7,8 +7,8 @@ from rest_framework.response import Response
from authentication.models import ToolshedUser, KnownIdentity from authentication.models import ToolshedUser, KnownIdentity
from authentication.signature_auth import SignatureAuthentication from authentication.signature_auth import SignatureAuthentication
from toolshed.models import InventoryItem from toolshed.models import InventoryItem, StorageLocation
from toolshed.serializers import InventoryItemSerializer from toolshed.serializers import InventoryItemSerializer, StorageLocationSerializer
router = routers.SimpleRouter() router = routers.SimpleRouter()
@ -24,6 +24,7 @@ def inventory_items(identity):
for friend in identity.friends.all(): for friend in identity.friends.all():
if friend_user := friend.user.get(): if friend_user := friend.user.get():
for item in friend_user.inventory_items.all(): for item in friend_user.inventory_items.all():
if item.availability_policy != 'private':
yield item yield item
@ -61,7 +62,19 @@ def search_inventory_items(request):
return Response({'error': 'No query provided.'}, status=400) 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'inventory_items', InventoryItemViewSet, basename='inventory_items')
router.register(r'storage_locations', StorageLocationViewSet, basename='storage_locations')
urlpatterns = router.urls + [ urlpatterns = router.urls + [
path('search/', search_inventory_items, name='search_inventory_items'), path('search/', search_inventory_items, name='search_inventory_items'),

View file

@ -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'),
),
]

View file

@ -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),
),
]

View file

@ -50,18 +50,28 @@ class Tag(models.Model):
class InventoryItem(SoftDeleteModel): class InventoryItem(SoftDeleteModel):
AVAILABILITY_POLICY_CHOICES = (
('sell', 'Sell'),
('rent', 'Rent'),
('lend', 'Lend'),
('share', 'Share'),
('private', 'Private'),
)
published = models.BooleanField(default=False) published = models.BooleanField(default=False)
name = models.CharField(max_length=255, null=True, blank=True) name = models.CharField(max_length=255, null=True, blank=True)
description = models.TextField(null=True, blank=True) description = models.TextField(null=True, blank=True)
category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, blank=True, category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, blank=True,
related_name='inventory_items') 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)]) owned_quantity = models.IntegerField(default=1, validators=[MinValueValidator(0)])
owner = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='inventory_items') owner = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='inventory_items')
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
tags = models.ManyToManyField(Tag, through='ItemTag', related_name='inventory_items') tags = models.ManyToManyField(Tag, through='ItemTag', related_name='inventory_items')
properties = models.ManyToManyField(Property, through='ItemProperty') properties = models.ManyToManyField(Property, through='ItemProperty')
files = models.ManyToManyField(File, related_name='connected_items') 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): def clean(self):
if (self.name is None or self.name == "") and self.files.count() == 0: if (self.name is None or self.name == "") and self.files.count() == 0:
@ -77,3 +87,16 @@ class ItemProperty(models.Model):
class ItemTag(models.Model): class ItemTag(models.Model):
tag = models.ForeignKey(Tag, on_delete=models.CASCADE) tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
inventory_item = models.ForeignKey(InventoryItem, 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

View file

@ -3,7 +3,7 @@ from authentication.models import KnownIdentity, ToolshedUser, FriendRequestInco
from authentication.serializers import OwnerSerializer from authentication.serializers import OwnerSerializer
from files.models import File from files.models import File
from files.serializers import FileSerializer 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): class FriendSerializer(serializers.ModelSerializer):
@ -11,7 +11,7 @@ class FriendSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = KnownIdentity model = KnownIdentity
fields = ['username', 'public_key'] fields = ['id', 'username', 'public_key']
def get_username(self, obj): def get_username(self, obj):
return obj.username + '@' + obj.domain return obj.username + '@' + obj.domain
@ -48,6 +48,23 @@ class CategorySerializer(serializers.ModelSerializer):
return Category.objects.get(name=data.split("/")[-1]) 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): class ItemPropertySerializer(serializers.ModelSerializer):
property = PropertySerializer(read_only=True) property = PropertySerializer(read_only=True)
@ -74,7 +91,7 @@ class InventoryItemSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = InventoryItem model = InventoryItem
fields = ['id', 'name', 'description', 'owner', 'category', 'availability_policy', 'owned_quantity', 'owner', 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): def to_internal_value(self, data):
files = data.pop('files', []) files = data.pop('files', [])

View file

@ -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: class CategoryTestMixin:
@ -13,8 +13,10 @@ class CategoryTestMixin:
class TagTestMixin: class TagTestMixin:
def prepare_tags(self): def prepare_tags(self):
self.f['tag1'] = Tag.objects.create(name='tag1', description='tag1 description', category=self.f['cat1'], origin='test') self.f['tag1'] = Tag.objects.create(name='tag1', description='tag1 description', category=self.f['cat1'],
self.f['tag2'] = Tag.objects.create(name='tag2', description='tag2 description', category=self.f['cat1'], origin='test') 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') 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={}) 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['prop1'], value='value1').save()
ItemProperty.objects.create(inventory_item=self.f['item2'], property=self.f['prop2'], value='value2').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'])

View file

@ -210,6 +210,17 @@ class FriendRequestIncomingTestCase(UserTestMixin, ToolshedTestCase):
}) })
self.assertEqual(reply.status_code, 400) 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): def test_post_request_breaking_key(self):
befriender = self.f['ext_user1'] befriender = self.f['ext_user1']
befriendee = self.f['local_user1'] befriendee = self.f['local_user1']

View file

@ -38,7 +38,7 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase):
def test_post_new_item(self): def test_post_new_item(self):
reply = client.post('/api/inventory_items/', self.f['local_user1'], { reply = client.post('/api/inventory_items/', self.f['local_user1'], {
'availability_policy': 'friends', 'availability_policy': 'rent',
'category': 'cat2', 'category': 'cat2',
'name': 'test3', 'name': 'test3',
'description': 'test', 'description': 'test',
@ -50,7 +50,7 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase):
self.assertEqual(reply.status_code, 201) self.assertEqual(reply.status_code, 201)
self.assertEqual(InventoryItem.objects.count(), 3) self.assertEqual(InventoryItem.objects.count(), 3)
item = InventoryItem.objects.get(name='test3') 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.category, Category.objects.get(name='cat2'))
self.assertEqual(item.name, 'test3') self.assertEqual(item.name, 'test3')
self.assertEqual(item.description, 'test') self.assertEqual(item.description, 'test')
@ -61,7 +61,7 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase):
def test_post_new_item2(self): def test_post_new_item2(self):
reply = client.post('/api/inventory_items/', self.f['local_user1'], { reply = client.post('/api/inventory_items/', self.f['local_user1'], {
'availability_policy': 'friends', 'availability_policy': 'share',
'name': 'test3', 'name': 'test3',
'description': 'test', 'description': 'test',
'owned_quantity': 1, 'owned_quantity': 1,
@ -70,7 +70,7 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase):
self.assertEqual(reply.status_code, 201) self.assertEqual(reply.status_code, 201)
self.assertEqual(InventoryItem.objects.count(), 3) self.assertEqual(InventoryItem.objects.count(), 3)
item = InventoryItem.objects.get(name='test3') 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.category, None)
self.assertEqual(item.name, 'test3') self.assertEqual(item.name, 'test3')
self.assertEqual(item.description, 'test') self.assertEqual(item.description, 'test')
@ -80,7 +80,7 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase):
def test_post_new_item_empty(self): def test_post_new_item_empty(self):
reply = client.post('/api/inventory_items/', self.f['local_user1'], { reply = client.post('/api/inventory_items/', self.f['local_user1'], {
'availability_policy': 'friends', 'availability_policy': 'rent',
'owned_quantity': 1, 'owned_quantity': 1,
'image': '', 'image': '',
}) })
@ -89,7 +89,7 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase):
def test_post_new_item3(self): def test_post_new_item3(self):
reply = client.post('/api/inventory_items/', self.f['local_user1'], { reply = client.post('/api/inventory_items/', self.f['local_user1'], {
'availability_policy': 'friends', 'availability_policy': 'private',
'name': 'test3', 'name': 'test3',
'description': 'test', 'description': 'test',
'owned_quantity': 1, 'owned_quantity': 1,
@ -99,7 +99,7 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase):
self.assertEqual(reply.status_code, 201) self.assertEqual(reply.status_code, 201)
self.assertEqual(InventoryItem.objects.count(), 3) self.assertEqual(InventoryItem.objects.count(), 3)
item = InventoryItem.objects.get(name='test3') 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.category, None)
self.assertEqual(item.name, 'test3') self.assertEqual(item.name, 'test3')
self.assertEqual(item.description, 'test') self.assertEqual(item.description, 'test')
@ -109,7 +109,7 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase):
def test_put_item(self): def test_put_item(self):
reply = client.put('/api/inventory_items/1/', self.f['local_user1'], { reply = client.put('/api/inventory_items/1/', self.f['local_user1'], {
'availability_policy': 'friends', 'availability_policy': 'sell',
'name': 'test4', 'name': 'test4',
'description': 'new description', 'description': 'new description',
'owned_quantity': 100, 'owned_quantity': 100,
@ -121,7 +121,7 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase):
self.assertEqual(reply.status_code, 200) self.assertEqual(reply.status_code, 200)
self.assertEqual(InventoryItem.objects.count(), 2) self.assertEqual(InventoryItem.objects.count(), 2)
item = InventoryItem.objects.get(id=1) 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.category, None)
self.assertEqual(item.name, 'test4') self.assertEqual(item.name, 'test4')
self.assertEqual(item.description, 'new description') self.assertEqual(item.description, 'new description')

View file

@ -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')