diff --git a/backend/authentication/serializers.py b/backend/authentication/serializers.py new file mode 100644 index 0000000..4a62dcf --- /dev/null +++ b/backend/authentication/serializers.py @@ -0,0 +1,12 @@ +from rest_framework import serializers + +from authentication.models import ToolshedUser + + +class OwnerSerializer(serializers.ReadOnlyField): + class Meta: + model = ToolshedUser + fields = ['username', 'domain'] + + def to_representation(self, value): + return value.username + '@' + value.domain diff --git a/backend/backend/urls.py b/backend/backend/urls.py index 5e94b5e..24ee4d9 100644 --- a/backend/backend/urls.py +++ b/backend/backend/urls.py @@ -35,5 +35,6 @@ urlpatterns = [ path('api/', include('toolshed.api.friend')), path('api/', include('toolshed.api.inventory')), path('api/', include('toolshed.api.info')), + path('api/', include('toolshed.api.files')), path('docs/', schema_view.with_ui('swagger', cache_timeout=0), name='api-docs'), ] diff --git a/backend/files/serializers.py b/backend/files/serializers.py new file mode 100644 index 0000000..e4bc82e --- /dev/null +++ b/backend/files/serializers.py @@ -0,0 +1,20 @@ +from django.core.files.base import ContentFile +from rest_framework import serializers + +from files.models import File + + +class FileSerializer(serializers.Serializer): + data = serializers.CharField() + mime_type = serializers.CharField() + class Meta: + model = File + fields = ['data', 'mime_type'] + read_only_fields = ['id', 'size', 'name'] + + def to_representation(self, instance): + return {'id': instance.id, 'name': instance.file.url, 'size': instance.file.size, + 'mime_type': instance.mime_type} + + def create(self, validated_data): + return File.objects.get_or_create(**validated_data)[0] diff --git a/backend/toolshed/api/files.py b/backend/toolshed/api/files.py new file mode 100644 index 0000000..b56e4dc --- /dev/null +++ b/backend/toolshed/api/files.py @@ -0,0 +1,74 @@ +from django.urls import path +from rest_framework import status +from rest_framework.decorators import api_view, permission_classes, authentication_classes +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from authentication.signature_auth import SignatureAuthenticationLocal +from files.models import File +from files.serializers import FileSerializer +from toolshed.models import InventoryItem + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([SignatureAuthenticationLocal]) +def list_all_files(request, format=None): # /files/ + files = File.objects.select_related().filter(connected_items__owner=request.user).distinct() + return Response(FileSerializer(files, many=True).data) + + +def get_item_files(request, item_id): + try: + item = InventoryItem.objects.get(id=item_id, owner=request.user) + files = item.files.all() + return Response(FileSerializer(files, many=True).data) + except InventoryItem.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + + +def post_item_file(request, item_id): + try: + item = InventoryItem.objects.get(id=item_id, owner=request.user) + serializer = FileSerializer(data=request.data) + if serializer.is_valid(): + file = serializer.save() + item.files.add(file) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except InventoryItem.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + + +@api_view(['POST', 'GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([SignatureAuthenticationLocal]) +def item_files(request, item_id, format=None): # /item_files/ + if request.method == 'GET': + return get_item_files(request, item_id) + elif request.method == 'POST': + return post_item_file(request, item_id) + + +@api_view(['DELETE']) +@permission_classes([IsAuthenticated]) +@authentication_classes([SignatureAuthenticationLocal]) +def delete_item_file(request, item_id, file_id, format=None): # /item_files/ + try: + item = InventoryItem.objects.get(id=item_id, owner=request.user) + file = item.files.get(id=file_id) + item.files.remove(file_id) + if file.connected_items.count() == 0: + file.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + except InventoryItem.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + except File.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + + +urlpatterns = [ + path('files/', list_all_files), + path('item_files//', item_files), + path('item_files///', delete_item_file), +] diff --git a/backend/toolshed/api/inventory.py b/backend/toolshed/api/inventory.py index 3617ad2..eb472e9 100644 --- a/backend/toolshed/api/inventory.py +++ b/backend/toolshed/api/inventory.py @@ -1,3 +1,4 @@ +from django.db import transaction from django.urls import path from rest_framework import routers, viewsets from rest_framework.decorators import authentication_classes, api_view, permission_classes @@ -35,11 +36,13 @@ class InventoryItemViewSet(viewsets.ModelViewSet): return InventoryItem.objects.filter(owner=self.request.user.user.get()) def perform_create(self, serializer): - serializer.save(owner=self.request.user.user.get()) + with transaction.atomic(): + serializer.save(owner=self.request.user.user.get()).clean() def perform_update(self, serializer): - if serializer.instance.owner == self.request.user.user.get(): - serializer.save() + with transaction.atomic(): + if serializer.instance.owner == self.request.user.user.get(): + serializer.save().clean() def perform_destroy(self, instance): if instance.owner == self.request.user.user.get(): diff --git a/backend/toolshed/migrations/0003_inventoryitem_files_and_more.py b/backend/toolshed/migrations/0003_inventoryitem_files_and_more.py new file mode 100644 index 0000000..b558420 --- /dev/null +++ b/backend/toolshed/migrations/0003_inventoryitem_files_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2.2 on 2023-07-07 17:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('files', '0001_initial'), + ('toolshed', '0002_inventoryitem_itemtag_itemproperty_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='inventoryitem', + name='files', + field=models.ManyToManyField(related_name='connected_items', to='files.file'), + ), + migrations.AlterField( + model_name='inventoryitem', + name='availability_policy', + field=models.CharField(default='private', max_length=255), + ), + migrations.AlterField( + model_name='inventoryitem', + name='description', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='inventoryitem', + name='name', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/backend/toolshed/models.py b/backend/toolshed/models.py index d3a4968..f1448a9 100644 --- a/backend/toolshed/models.py +++ b/backend/toolshed/models.py @@ -1,8 +1,10 @@ from django.db import models from django.core.validators import MinValueValidator from django_softdelete.models import SoftDeleteModel +from rest_framework.exceptions import ValidationError from authentication.models import ToolshedUser, KnownIdentity +from files.models import File class Category(SoftDeleteModel): @@ -28,7 +30,7 @@ class Property(models.Model): unit_name_plural = models.CharField(max_length=255, null=True, blank=True) base2_prefix = models.BooleanField(default=False) dimensions = models.IntegerField(null=False, blank=False, default=1, validators=[MinValueValidator(1)]) - origin = models.CharField(max_length=255) + origin = models.CharField(max_length=255, null=False, blank=False) class Meta: verbose_name_plural = 'properties' @@ -41,7 +43,7 @@ class Tag(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='tags') - origin = models.CharField(max_length=255) + origin = models.CharField(max_length=255, null=False, blank=False) def __str__(self): return self.name @@ -49,16 +51,21 @@ class Tag(models.Model): class InventoryItem(SoftDeleteModel): published = models.BooleanField(default=False) - name = models.CharField(max_length=255) - description = models.TextField() + 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) + availability_policy = models.CharField(max_length=255, 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') + tags = models.ManyToManyField(Tag, through='ItemTag', related_name='inventory_items') + properties = models.ManyToManyField(Property, through='ItemProperty') + files = models.ManyToManyField(File, related_name='connected_items') + + def clean(self): + if (self.name is None or self.name == "") and self.files.count() == 0: + raise ValidationError("Name or at least one file must be set") class ItemProperty(models.Model): diff --git a/backend/toolshed/serializers.py b/backend/toolshed/serializers.py index 41bd40a..cccebd4 100644 --- a/backend/toolshed/serializers.py +++ b/backend/toolshed/serializers.py @@ -1,5 +1,9 @@ from rest_framework import serializers + from authentication.models import KnownIdentity, ToolshedUser +from authentication.serializers import OwnerSerializer +from files.models import File +from files.serializers import FileSerializer from toolshed.models import Category, Property, ItemProperty, InventoryItem, Tag @@ -34,15 +38,6 @@ class CategorySerializer(serializers.ModelSerializer): return Category.objects.get(name=data.split("/")[-1]) -class InventoryItemOwnerSerializer(serializers.ReadOnlyField): - class Meta: - model = ToolshedUser - fields = '__all__' - - def to_representation(self, value): - return value.username + '@' + value.domain - - class ItemPropertySerializer(serializers.ModelSerializer): property = PropertySerializer(read_only=True) @@ -60,7 +55,7 @@ class ItemPropertySerializer(serializers.ModelSerializer): class InventoryItemSerializer(serializers.ModelSerializer): - owner = InventoryItemOwnerSerializer(read_only=True) + owner = OwnerSerializer(read_only=True) tags = serializers.SlugRelatedField(many=True, required=False, queryset=Tag.objects.all(), slug_field='name') properties = ItemPropertySerializer(many=True, required=False, source='itemproperty_set') category = CategorySerializer(required=False, allow_null=True) @@ -70,14 +65,34 @@ class InventoryItemSerializer(serializers.ModelSerializer): fields = ['id', 'name', 'description', 'owner', 'category', 'availability_policy', 'owned_quantity', 'owner', 'tags', 'properties'] + def to_internal_value(self, data): + files = data.pop('files', []) + ret = super().to_internal_value(data) + ret['files'] = files + return ret + def create(self, validated_data): tags = validated_data.pop('tags', []) props = validated_data.pop('itemproperty_set', []) + files = validated_data.pop('files', []) item = InventoryItem.objects.create(**validated_data) for tag in tags: item.tags.add(tag, through_defaults={}) for prop in props: ItemProperty.objects.create(inventory_item=item, property=prop['property'], value=prop['value']) + for file in files: + if type(file) == dict: + file_serializer = FileSerializer(data=file) + if file_serializer.is_valid(): + file_serializer.save() + item.files.add(file_serializer.instance) + else: + raise serializers.ValidationError(file_serializer.errors) + elif type(file) == int: + if File.objects.filter(id=file).exists(): + item.files.add(File.objects.get(id=file)) + else: + raise serializers.ValidationError("File with id {} does not exist".format(file)) item.save() return item diff --git a/backend/toolshed/tests/fixtures.py b/backend/toolshed/tests/fixtures.py index 443bf6d..6755591 100644 --- a/backend/toolshed/tests/fixtures.py +++ b/backend/toolshed/tests/fixtures.py @@ -3,28 +3,28 @@ from toolshed.models import Category, Tag, Property, InventoryItem, ItemProperty class CategoryTestMixin: def prepare_categories(self): - self.f['cat1'] = Category.objects.create(name='cat1') - self.f['cat2'] = Category.objects.create(name='cat2') - self.f['cat3'] = Category.objects.create(name='cat3') - self.f['subcat1'] = Category.objects.create(name='subcat1', parent=self.f['cat1']) - self.f['subcat2'] = Category.objects.create(name='subcat2', parent=self.f['cat1']) - self.f['subcat3'] = Category.objects.create(name='subcat3', parent=self.f['subcat1']) + self.f['cat1'] = Category.objects.create(name='cat1', origin='test') + self.f['cat2'] = Category.objects.create(name='cat2', origin='test') + self.f['cat3'] = Category.objects.create(name='cat3', origin='test') + self.f['subcat1'] = Category.objects.create(name='subcat1', parent=self.f['cat1'], origin='test') + self.f['subcat2'] = Category.objects.create(name='subcat2', parent=self.f['cat1'], origin='test') + self.f['subcat3'] = Category.objects.create(name='subcat3', parent=self.f['subcat1'], origin='test') class TagTestMixin: def prepare_tags(self): - 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']) - self.f['tag3'] = Tag.objects.create(name='tag3') + 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') class PropertyTestMixin: def prepare_properties(self): - self.f['prop1'] = Property.objects.create(name='prop1') + self.f['prop1'] = Property.objects.create(name='prop1', origin='test') self.f['prop2'] = Property.objects.create( - name='prop2', description='prop2 description', category=self.f['cat1']) + name='prop2', description='prop2 description', category=self.f['cat1'], origin='test') self.f['prop3'] = Property.objects.create( - name='prop3', description='prop3 description', category=self.f['cat1']) + name='prop3', description='prop3 description', category=self.f['cat1'], origin='test') class InventoryTestMixin(CategoryTestMixin, TagTestMixin, PropertyTestMixin): diff --git a/backend/toolshed/tests/test_files.py b/backend/toolshed/tests/test_files.py new file mode 100644 index 0000000..1925a02 --- /dev/null +++ b/backend/toolshed/tests/test_files.py @@ -0,0 +1,140 @@ +from django.test import Client +from authentication.tests import SignatureAuthClient, UserTestMixin, ToolshedTestCase +from files.tests import FilesTestMixin +from toolshed.models import File + +from toolshed.tests import InventoryTestMixin + +anonymous_client = Client() +client = SignatureAuthClient() + + +class FileApiTestCase(UserTestMixin, FilesTestMixin, InventoryTestMixin, ToolshedTestCase): + + def setUp(self): + super().setUp() + self.prepare_users() + self.prepare_files() + self.prepare_categories() + self.prepare_tags() + self.prepare_properties() + self.prepare_inventory() + self.f['item1'].files.add(self.f['test_file1']) + self.f['item1'].files.add(self.f['test_file2']) + self.f['item2'].files.add(self.f['test_file1']) + + def test_files_anonymous(self): + response = anonymous_client.get(f"/api/item_files/{self.f['item1'].id}/") + self.assertEqual(response.status_code, 403) + + def test_list_all_files(self): + response = client.get(f"/api/files/", self.f['local_user1']) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.json()), 2) + self.assertEqual(response.json()[0]['mime_type'], 'text/plain') + self.assertEqual(response.json()[0]['name'], + f"/media/{self.f['hash1'][:2]}/{self.f['hash1'][2:4]}/{self.f['hash1'][4:6]}/{self.f['hash1'][6:]}") + self.assertEqual(response.json()[1]['mime_type'], 'text/plain') + self.assertEqual(response.json()[1]['name'], + f"/media/{self.f['hash2'][:2]}/{self.f['hash2'][2:4]}/{self.f['hash2'][4:6]}/{self.f['hash2'][6:]}") + + def test_files(self): + response = client.get(f"/api/item_files/{self.f['item1'].id}/", self.f['local_user1']) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.json()), 2) + self.assertEqual(response.json()[0]['mime_type'], 'text/plain') + self.assertEqual(response.json()[0]['name'], + f"/media/{self.f['hash1'][:2]}/{self.f['hash1'][2:4]}/{self.f['hash1'][4:6]}/{self.f['hash1'][6:]}") + self.assertEqual(response.json()[1]['mime_type'], 'text/plain') + self.assertEqual(response.json()[1]['name'], + f"/media/{self.f['hash2'][:2]}/{self.f['hash2'][2:4]}/{self.f['hash2'][4:6]}/{self.f['hash2'][6:]}") + + def test_files_not_found(self): + response = client.get(f"/api/item_files/99999/", self.f['local_user1']) + self.assertEqual(response.status_code, 404) + + def test_post_file(self): + response = client.post(f"/api/item_files/{self.f['item1'].id}/", self.f['local_user1'], + {'data': self.f['encoded_content4'], 'mime_type': 'text/plain'}) + self.assertEqual(response.status_code, 201) + self.assertEqual(File.objects.count(), 4) + self.assertEqual(File.objects.last().mime_type, 'text/plain') + self.assertEqual(File.objects.last().file.read(), self.f['test_content4']) + self.assertEqual(File.objects.last().file.size, len(self.f['test_content4'])) + self.assertEqual(File.objects.last().file.name, + f"{self.f['hash4'][:2]}/{self.f['hash4'][2:4]}/{self.f['hash4'][4:6]}/{self.f['hash4'][6:]}") + + def test_post_file_duplicate(self): + self.assertEqual(File.objects.count(), 3) + self.assertEqual(self.f['item1'].files.count(), 2) + response = client.post(f"/api/item_files/{self.f['item1'].id}/", self.f['local_user1'], + {'data': self.f['encoded_content3'], 'mime_type': 'text/plain'}) + self.assertEqual(response.status_code, 201) + self.assertEqual(File.objects.count(), 3) + self.assertEqual(self.f['item1'].files.count(), 3) + self.assertEqual(self.f['item1'].files.last().file.read(), self.f['test_content3']) + self.assertEqual(self.f['item1'].files.last().file.size, len(self.f['test_content3'])) + self.assertEqual(self.f['item1'].files.last().file.name, + f"{self.f['hash3'][:2]}/{self.f['hash3'][2:4]}/{self.f['hash3'][4:6]}/{self.f['hash3'][6:]}") + + def test_post_file_invalid(self): + response = client.post(f"/api/item_files/{self.f['item1'].id}/", self.f['local_user1'], + {'data': self.f['encoded_content4']}) + self.assertEqual(response.status_code, 400) + + def test_post_file_not_found_item(self): + response = client.post(f"/api/item_files/99999/", self.f['local_user1'], + {'data': self.f['encoded_content3'], 'mime_type': 'text/plain'}) + self.assertEqual(response.status_code, 404) + self.assertEqual(File.objects.count(), 3) + + def test_post_file_not_authenticated(self): + response = anonymous_client.post(f"/api/item_files/{self.f['item1'].id}/", + {'data': self.f['encoded_content3'], 'mime_type': 'text/plain'}) + self.assertEqual(response.status_code, 403) + self.assertEqual(File.objects.count(), 3) + + def test_post_file_not_authorized(self): + response = client.post(f"/api/item_files/{self.f['item1'].id}/", self.f['local_user2'], + {'data': self.f['encoded_content3'], 'mime_type': 'text/plain'}) + self.assertEqual(response.status_code, 404) + self.assertEqual(File.objects.count(), 3) + + def test_delete_file(self): + response = client.delete(f"/api/item_files/{self.f['item1'].id}/{self.f['test_file1'].id}/", + self.f['local_user1']) + self.assertEqual(response.status_code, 204) + self.assertEqual(File.objects.count(), 3) + self.assertEqual(self.f['item1'].files.count(), 1) + + def test_delete_file_last_use(self): + response = client.delete(f"/api/item_files/{self.f['item1'].id}/{self.f['test_file2'].id}/", + self.f['local_user1']) + self.assertEqual(response.status_code, 204) + self.assertEqual(File.objects.count(), 2) + self.assertEqual(self.f['item1'].files.count(), 1) + + def test_delete_file_not_found(self): + response = client.delete(f"/api/item_files/{self.f['item1'].id}/99999/", self.f['local_user1']) + self.assertEqual(response.status_code, 404) + self.assertEqual(File.objects.count(), 3) + self.assertEqual(self.f['item1'].files.count(), 2) + + def test_delete_file_not_found_item(self): + response = client.delete(f"/api/item_files/99999/{self.f['test_file1'].id}/", self.f['local_user1']) + self.assertEqual(response.status_code, 404) + self.assertEqual(File.objects.count(), 3) + self.assertEqual(self.f['item1'].files.count(), 2) + + def test_delete_file_not_owner(self): + response = client.delete(f"/api/item_files/{self.f['item1'].id}/{self.f['test_file1'].id}/", + self.f['local_user2']) + self.assertEqual(response.status_code, 404) + self.assertEqual(File.objects.count(), 3) + self.assertEqual(self.f['item1'].files.count(), 2) + + def test_delete_file_anonymous(self): + response = anonymous_client.delete(f"/api/item_files/{self.f['item1'].id}/{self.f['test_file1'].id}/") + self.assertEqual(response.status_code, 403) + self.assertEqual(File.objects.count(), 3) + self.assertEqual(self.f['item1'].files.count(), 2) diff --git a/backend/toolshed/tests/test_inventory.py b/backend/toolshed/tests/test_inventory.py index 5cf8014..d71e6a2 100644 --- a/backend/toolshed/tests/test_inventory.py +++ b/backend/toolshed/tests/test_inventory.py @@ -1,11 +1,12 @@ from authentication.tests import SignatureAuthClient, UserTestMixin, ToolshedTestCase +from files.tests import FilesTestMixin from toolshed.models import InventoryItem, Category from toolshed.tests import InventoryTestMixin, CategoryTestMixin, TagTestMixin, PropertyTestMixin client = SignatureAuthClient() -class InventoryTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase): +class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase): def setUp(self): super().setUp() @@ -77,6 +78,15 @@ class InventoryTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase): self.assertEqual([t for t in item.tags.all()], []) self.assertEqual([p for p in item.properties.all()], []) + def test_post_new_item_empty(self): + reply = client.post('/api/inventory_items/', self.f['local_user1'], { + 'availability_policy': 'friends', + 'owned_quantity': 1, + 'image': '', + }) + self.assertEqual(reply.status_code, 400) + self.assertEqual(InventoryItem.objects.count(), 2) + def test_post_new_item3(self): reply = client.post('/api/inventory_items/', self.f['local_user1'], { 'availability_policy': 'friends', @@ -202,3 +212,84 @@ class InventoryTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase): reply = client.get('/api/search/?query=test', self.f['ext_user1']) self.assertEqual(reply.status_code, 200) self.assertEqual(len(reply.json()), 0) + + +class TestInventoryItemWithFileApiTestCase(UserTestMixin, FilesTestMixin, InventoryTestMixin, ToolshedTestCase): + def setUp(self): + super().setUp() + self.prepare_users() + self.prepare_categories() + self.prepare_tags() + self.prepare_properties() + self.prepare_files() + self.prepare_inventory() + + def test_post_item_with_file_id(self): + reply = client.post('/api/inventory_items/', self.f['local_user1'], { + 'name': 'test4', + 'description': 'test', + 'category': 'cat1', + 'owned_quantity': 1, + 'tags': ['tag1', 'tag2'], + 'properties': [{'name': 'prop1', 'value': 'value1'}, {'name': 'prop2', 'value': 'value2'}], + 'files': [self.f['test_file1'].id] + }) + self.assertEqual(reply.status_code, 201) + self.assertEqual(InventoryItem.objects.count(), 3) + item = InventoryItem.objects.get(id=3) + self.assertEqual(item.availability_policy, 'private') + self.assertEqual(item.category, Category.objects.get(name='cat1')) + self.assertEqual(item.name, 'test4') + self.assertEqual(item.description, 'test') + self.assertEqual(item.owned_quantity, 1) + self.assertEqual([t for t in item.tags.all()], [self.f['tag1'], self.f['tag2']]) + self.assertEqual([p for p in item.properties.all()], [self.f['prop1'], self.f['prop2']]) + self.assertEqual([p.value for p in item.itemproperty_set.all()], ['value1', 'value2']) + self.assertEqual([f for f in item.files.all()], [self.f['test_file1']]) + + def test_post_item_with_encoded_file(self): + reply = client.post('/api/inventory_items/', self.f['local_user1'], { + 'name': 'test4', + 'description': 'test', + 'category': 'cat1', + 'owned_quantity': 1, + 'tags': ['tag1', 'tag2'], + 'properties': [{'name': 'prop1', 'value': 'value1'}, {'name': 'prop2', 'value': 'value2'}], + 'files': [{'data': self.f['encoded_content3'], 'mime_type': 'text/plain'}] + }) + self.assertEqual(reply.status_code, 201) + self.assertEqual(InventoryItem.objects.count(), 3) + item = InventoryItem.objects.get(id=3) + self.assertEqual(item.availability_policy, 'private') + self.assertEqual(item.category, Category.objects.get(name='cat1')) + self.assertEqual(item.name, 'test4') + self.assertEqual(item.description, 'test') + self.assertEqual(item.owned_quantity, 1) + self.assertEqual([t for t in item.tags.all()], [self.f['tag1'], self.f['tag2']]) + self.assertEqual([p for p in item.properties.all()], [self.f['prop1'], self.f['prop2']]) + self.assertEqual([p.value for p in item.itemproperty_set.all()], ['value1', 'value2']) + self.assertEqual([f for f in item.files.all()], [self.f['test_file3']]) + + def test_post_item_with_file_id_fail(self): + reply = client.post('/api/inventory_items/', self.f['local_user1'], { + 'name': 'test4', + 'description': 'test', + 'category': 'cat1', + 'owned_quantity': 1, + 'tags': ['tag1', 'tag2'], + 'properties': [{'name': 'prop1', 'value': 'value1'}, {'name': 'prop2', 'value': 'value2'}], + 'files': [99999] + }) + self.assertEqual(reply.status_code, 400) + + def test_post_item_with_encoded_file_fail(self): + reply = client.post('/api/inventory_items/', self.f['local_user1'], { + 'name': 'test4', + 'description': 'test', + 'category': 'cat1', + 'owned_quantity': 1, + 'tags': ['tag1', 'tag2'], + 'properties': [{'name': 'prop1', 'value': 'value1'}, {'name': 'prop2', 'value': 'value2'}], + 'files': [{'data': self.f['encoded_content3']}] + }) + self.assertEqual(reply.status_code, 400) \ No newline at end of file