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/
staticfiles/
userfiles/
testdata.py

View file

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

View file

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

View file

@ -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()
@ -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):
@ -61,7 +62,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'),

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):
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)
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 +87,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

View file

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

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

View file

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

View file

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

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