add /files/ and /item_files/ API endpoints
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
j3d1 2023-07-07 19:28:17 +02:00
parent 3afeed81b3
commit d00d637dca
11 changed files with 430 additions and 33 deletions

View file

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

View file

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

View file

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

View file

@ -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/<int:item_id>/', item_files),
path('item_files/<int:item_id>/<int:file_id>/', delete_item_file),
]

View file

@ -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):
with transaction.atomic():
if serializer.instance.owner == self.request.user.user.get():
serializer.save()
serializer.save().clean()
def perform_destroy(self, instance):
if instance.owner == self.request.user.user.get():

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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