mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
2871051923
94
InvenTree/InvenTree/api_tester.py
Normal file
94
InvenTree/InvenTree/api_tester.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
"""
|
||||||
|
Helper functions for performing API unit tests
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
|
||||||
|
class InvenTreeAPITestCase(APITestCase):
|
||||||
|
"""
|
||||||
|
Base class for running InvenTree API tests
|
||||||
|
"""
|
||||||
|
|
||||||
|
# User information
|
||||||
|
username = 'testuser'
|
||||||
|
password = 'mypassword'
|
||||||
|
email = 'test@testing.com'
|
||||||
|
|
||||||
|
superuser = False
|
||||||
|
auto_login = True
|
||||||
|
|
||||||
|
# Set list of roles automatically associated with the user
|
||||||
|
roles = []
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
# Create a user to log in with
|
||||||
|
self.user = get_user_model().objects.create_user(
|
||||||
|
username=self.username,
|
||||||
|
password=self.password,
|
||||||
|
email=self.email
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a group for the user
|
||||||
|
self.group = Group.objects.create(name='my_test_group')
|
||||||
|
self.user.groups.add(self.group)
|
||||||
|
|
||||||
|
if self.superuser:
|
||||||
|
self.user.is_superuser = True
|
||||||
|
self.user.save()
|
||||||
|
|
||||||
|
for role in self.roles:
|
||||||
|
self.assignRole(role)
|
||||||
|
|
||||||
|
if self.auto_login:
|
||||||
|
self.client.login(username=self.username, password=self.password)
|
||||||
|
|
||||||
|
def assignRole(self, role):
|
||||||
|
"""
|
||||||
|
Set the user roles for the registered user
|
||||||
|
"""
|
||||||
|
|
||||||
|
# role is of the format 'rule.permission' e.g. 'part.add'
|
||||||
|
|
||||||
|
rule, perm = role.split('.')
|
||||||
|
|
||||||
|
for ruleset in self.group.rule_sets.all():
|
||||||
|
|
||||||
|
if ruleset.name == rule:
|
||||||
|
|
||||||
|
if perm == 'view':
|
||||||
|
ruleset.can_view = True
|
||||||
|
elif perm == 'change':
|
||||||
|
ruleset.can_change = True
|
||||||
|
elif perm == 'delete':
|
||||||
|
ruleset.can_delete = True
|
||||||
|
elif perm == 'add':
|
||||||
|
ruleset.can_add = True
|
||||||
|
|
||||||
|
ruleset.save()
|
||||||
|
break
|
||||||
|
|
||||||
|
def get(self, url, data={}, code=200):
|
||||||
|
"""
|
||||||
|
Issue a GET request
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = self.client.get(url, data, format='json')
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, code)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def post(self, url, data):
|
||||||
|
"""
|
||||||
|
Issue a POST request
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = self.client.post(url, data=data, format='json')
|
||||||
|
|
||||||
|
return response
|
76
InvenTree/InvenTree/permissions.py
Normal file
76
InvenTree/InvenTree/permissions.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from rest_framework import permissions
|
||||||
|
|
||||||
|
import users.models
|
||||||
|
|
||||||
|
|
||||||
|
class RolePermission(permissions.BasePermission):
|
||||||
|
"""
|
||||||
|
Role mixin for API endpoints, allowing us to specify the user "role"
|
||||||
|
which is required for certain operations.
|
||||||
|
|
||||||
|
Each endpoint can have one or more of the following actions:
|
||||||
|
- GET
|
||||||
|
- POST
|
||||||
|
- PUT
|
||||||
|
- PATCH
|
||||||
|
- DELETE
|
||||||
|
|
||||||
|
Specify the required "role" using the role_required attribute.
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
|
||||||
|
role_required = "part"
|
||||||
|
|
||||||
|
The RoleMixin class will then determine if the user has the required permission
|
||||||
|
to perform the specified action.
|
||||||
|
|
||||||
|
For example, a DELETE action will be rejected unless the user has the "part.remove" permission
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
"""
|
||||||
|
Determine if the current user has the specified permissions
|
||||||
|
"""
|
||||||
|
|
||||||
|
# First, check that the user is authenticated!
|
||||||
|
auth = permissions.IsAuthenticated()
|
||||||
|
|
||||||
|
if not auth.has_permission(request, view):
|
||||||
|
return False
|
||||||
|
|
||||||
|
user = request.user
|
||||||
|
|
||||||
|
# Superuser can do it all
|
||||||
|
if user.is_superuser:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Map the request method to a permission type
|
||||||
|
rolemap = {
|
||||||
|
'GET': 'view',
|
||||||
|
'OPTIONS': 'view',
|
||||||
|
'POST': 'add',
|
||||||
|
'PUT': 'change',
|
||||||
|
'PATCH': 'change',
|
||||||
|
'DELETE': 'delete',
|
||||||
|
}
|
||||||
|
|
||||||
|
permission = rolemap[request.method]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Extract the model name associated with this request
|
||||||
|
model = view.serializer_class.Meta.model
|
||||||
|
|
||||||
|
# And the specific database table
|
||||||
|
table = model._meta.db_table
|
||||||
|
except AttributeError:
|
||||||
|
# We will assume that if the serializer class does *not* have a Meta,
|
||||||
|
# then we don't need a permission
|
||||||
|
return True
|
||||||
|
|
||||||
|
result = users.models.RuleSet.check_table_permission(user, table, permission)
|
||||||
|
|
||||||
|
return result
|
@ -278,6 +278,7 @@ REST_FRAMEWORK = {
|
|||||||
'DEFAULT_PERMISSION_CLASSES': (
|
'DEFAULT_PERMISSION_CLASSES': (
|
||||||
'rest_framework.permissions.IsAuthenticated',
|
'rest_framework.permissions.IsAuthenticated',
|
||||||
'rest_framework.permissions.DjangoModelPermissions',
|
'rest_framework.permissions.DjangoModelPermissions',
|
||||||
|
'InvenTree.permissions.RolePermission',
|
||||||
),
|
),
|
||||||
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema'
|
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema'
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
""" Low level tests for the InvenTree API """
|
""" Low level tests for the InvenTree API """
|
||||||
|
|
||||||
from rest_framework.test import APITestCase
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
|
from InvenTree.api_tester import InvenTreeAPITestCase
|
||||||
|
|
||||||
|
from users.models import RuleSet
|
||||||
|
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
|
|
||||||
|
|
||||||
class APITests(APITestCase):
|
class APITests(InvenTreeAPITestCase):
|
||||||
""" Tests for the InvenTree API """
|
""" Tests for the InvenTree API """
|
||||||
|
|
||||||
fixtures = [
|
fixtures = [
|
||||||
@ -19,15 +21,13 @@ class APITests(APITestCase):
|
|||||||
'category',
|
'category',
|
||||||
]
|
]
|
||||||
|
|
||||||
username = 'test_user'
|
|
||||||
password = 'test_pass'
|
|
||||||
|
|
||||||
token = None
|
token = None
|
||||||
|
|
||||||
|
auto_login = False
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
||||||
# Create a user (but do not log in!)
|
super().setUp()
|
||||||
get_user_model().objects.create_user(self.username, 'user@email.com', self.password)
|
|
||||||
|
|
||||||
def basicAuth(self):
|
def basicAuth(self):
|
||||||
# Use basic authentication
|
# Use basic authentication
|
||||||
@ -78,3 +78,82 @@ class APITests(APITestCase):
|
|||||||
self.assertIn('instance', data)
|
self.assertIn('instance', data)
|
||||||
|
|
||||||
self.assertEquals('InvenTree', data['server'])
|
self.assertEquals('InvenTree', data['server'])
|
||||||
|
|
||||||
|
def test_role_view(self):
|
||||||
|
"""
|
||||||
|
Test that we can access the 'roles' view for the logged in user.
|
||||||
|
|
||||||
|
Also tests that it is *not* accessible if the client is not logged in.
|
||||||
|
"""
|
||||||
|
|
||||||
|
url = reverse('api-user-roles')
|
||||||
|
|
||||||
|
response = self.client.get(url, format='json')
|
||||||
|
|
||||||
|
# Not logged in, so cannot access user role data
|
||||||
|
self.assertTrue(response.status_code in [401, 403])
|
||||||
|
|
||||||
|
# Now log in!
|
||||||
|
self.basicAuth()
|
||||||
|
|
||||||
|
response = self.get(url)
|
||||||
|
|
||||||
|
data = response.data
|
||||||
|
|
||||||
|
self.assertIn('user', data)
|
||||||
|
self.assertIn('username', data)
|
||||||
|
self.assertIn('is_staff', data)
|
||||||
|
self.assertIn('is_superuser', data)
|
||||||
|
self.assertIn('roles', data)
|
||||||
|
|
||||||
|
roles = data['roles']
|
||||||
|
|
||||||
|
role_names = roles.keys()
|
||||||
|
|
||||||
|
# By default, 'view' permissions are provided
|
||||||
|
for rule in RuleSet.RULESET_NAMES:
|
||||||
|
self.assertIn(rule, role_names)
|
||||||
|
|
||||||
|
self.assertIn('view', roles[rule])
|
||||||
|
|
||||||
|
self.assertNotIn('add', roles[rule])
|
||||||
|
self.assertNotIn('change', roles[rule])
|
||||||
|
self.assertNotIn('delete', roles[rule])
|
||||||
|
|
||||||
|
def test_with_superuser(self):
|
||||||
|
"""
|
||||||
|
Superuser should have *all* roles assigned
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.user.is_superuser = True
|
||||||
|
self.user.save()
|
||||||
|
|
||||||
|
self.basicAuth()
|
||||||
|
|
||||||
|
response = self.get(reverse('api-user-roles'))
|
||||||
|
|
||||||
|
roles = response.data['roles']
|
||||||
|
|
||||||
|
for rule in RuleSet.RULESET_NAMES:
|
||||||
|
self.assertIn(rule, roles.keys())
|
||||||
|
|
||||||
|
for perm in ['view', 'add', 'change', 'delete']:
|
||||||
|
self.assertIn(perm, roles[rule])
|
||||||
|
|
||||||
|
def test_with_roles(self):
|
||||||
|
"""
|
||||||
|
Assign some roles to the user
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.basicAuth()
|
||||||
|
response = self.get(reverse('api-user-roles'))
|
||||||
|
|
||||||
|
self.assignRole('part.delete')
|
||||||
|
self.assignRole('build.change')
|
||||||
|
response = self.get(reverse('api-user-roles'))
|
||||||
|
|
||||||
|
roles = response.data['roles']
|
||||||
|
|
||||||
|
# New role permissions should have been added now
|
||||||
|
self.assertIn('delete', roles['part'])
|
||||||
|
self.assertIn('change', roles['build'])
|
||||||
|
@ -3,19 +3,16 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from rest_framework.test import APITestCase
|
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.contrib.auth.models import Group
|
|
||||||
|
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
from build.models import Build
|
from build.models import Build
|
||||||
|
|
||||||
from InvenTree.status_codes import BuildStatus
|
from InvenTree.status_codes import BuildStatus
|
||||||
|
from InvenTree.api_tester import InvenTreeAPITestCase
|
||||||
|
|
||||||
|
|
||||||
class BuildAPITest(APITestCase):
|
class BuildAPITest(InvenTreeAPITestCase):
|
||||||
"""
|
"""
|
||||||
Series of tests for the Build DRF API
|
Series of tests for the Build DRF API
|
||||||
"""
|
"""
|
||||||
@ -28,32 +25,15 @@ class BuildAPITest(APITestCase):
|
|||||||
'build',
|
'build',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Required roles to access Build API endpoints
|
||||||
|
roles = [
|
||||||
|
'build.change',
|
||||||
|
'build.add'
|
||||||
|
]
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
# Create a user for auth
|
|
||||||
user = get_user_model()
|
|
||||||
|
|
||||||
self.user = user.objects.create_user(
|
super().setUp()
|
||||||
username='testuser',
|
|
||||||
email='test@testing.com',
|
|
||||||
password='password'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Put the user into a group with the correct permissions
|
|
||||||
group = Group.objects.create(name='mygroup')
|
|
||||||
self.user.groups.add(group)
|
|
||||||
|
|
||||||
# Give the group *all* the permissions!
|
|
||||||
for rule in group.rule_sets.all():
|
|
||||||
rule.can_view = True
|
|
||||||
rule.can_change = True
|
|
||||||
rule.can_add = True
|
|
||||||
rule.can_delete = True
|
|
||||||
|
|
||||||
rule.save()
|
|
||||||
|
|
||||||
group.save()
|
|
||||||
|
|
||||||
self.client.login(username='testuser', password='password')
|
|
||||||
|
|
||||||
|
|
||||||
class BuildListTest(BuildAPITest):
|
class BuildListTest(BuildAPITest):
|
||||||
@ -63,34 +43,26 @@ class BuildListTest(BuildAPITest):
|
|||||||
|
|
||||||
url = reverse('api-build-list')
|
url = reverse('api-build-list')
|
||||||
|
|
||||||
def get(self, status_code=200, data={}):
|
|
||||||
|
|
||||||
response = self.client.get(self.url, data, format='json')
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status_code)
|
|
||||||
|
|
||||||
return response.data
|
|
||||||
|
|
||||||
def test_get_all_builds(self):
|
def test_get_all_builds(self):
|
||||||
"""
|
"""
|
||||||
Retrieve *all* builds via the API
|
Retrieve *all* builds via the API
|
||||||
"""
|
"""
|
||||||
|
|
||||||
builds = self.get()
|
builds = self.get(self.url)
|
||||||
|
|
||||||
self.assertEqual(len(builds), 5)
|
self.assertEqual(len(builds.data), 5)
|
||||||
|
|
||||||
builds = self.get(data={'active': True})
|
builds = self.get(self.url, data={'active': True})
|
||||||
self.assertEqual(len(builds), 1)
|
self.assertEqual(len(builds.data), 1)
|
||||||
|
|
||||||
builds = self.get(data={'status': BuildStatus.COMPLETE})
|
builds = self.get(self.url, data={'status': BuildStatus.COMPLETE})
|
||||||
self.assertEqual(len(builds), 4)
|
self.assertEqual(len(builds.data), 4)
|
||||||
|
|
||||||
builds = self.get(data={'overdue': False})
|
builds = self.get(self.url, data={'overdue': False})
|
||||||
self.assertEqual(len(builds), 5)
|
self.assertEqual(len(builds.data), 5)
|
||||||
|
|
||||||
builds = self.get(data={'overdue': True})
|
builds = self.get(self.url, data={'overdue': True})
|
||||||
self.assertEqual(len(builds), 0)
|
self.assertEqual(len(builds.data), 0)
|
||||||
|
|
||||||
def test_overdue(self):
|
def test_overdue(self):
|
||||||
"""
|
"""
|
||||||
@ -109,7 +81,9 @@ class BuildListTest(BuildAPITest):
|
|||||||
target_date=in_the_past
|
target_date=in_the_past
|
||||||
)
|
)
|
||||||
|
|
||||||
builds = self.get(data={'overdue': True})
|
response = self.get(self.url, data={'overdue': True})
|
||||||
|
|
||||||
|
builds = response.data
|
||||||
|
|
||||||
self.assertEqual(len(builds), 1)
|
self.assertEqual(len(builds), 1)
|
||||||
|
|
||||||
@ -152,11 +126,15 @@ class BuildListTest(BuildAPITest):
|
|||||||
Build.objects.rebuild()
|
Build.objects.rebuild()
|
||||||
|
|
||||||
# Search by parent
|
# Search by parent
|
||||||
builds = self.get(data={'parent': parent.pk})
|
response = self.get(self.url, data={'parent': parent.pk})
|
||||||
|
|
||||||
|
builds = response.data
|
||||||
|
|
||||||
self.assertEqual(len(builds), 5)
|
self.assertEqual(len(builds), 5)
|
||||||
|
|
||||||
# Search by ancestor
|
# Search by ancestor
|
||||||
builds = self.get(data={'ancestor': parent.pk})
|
response = self.get(self.url, data={'ancestor': parent.pk})
|
||||||
|
|
||||||
|
builds = response.data
|
||||||
|
|
||||||
self.assertEqual(len(builds), 20)
|
self.assertEqual(len(builds), 20)
|
||||||
|
@ -1,32 +1,24 @@
|
|||||||
from rest_framework.test import APITestCase
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
|
|
||||||
from InvenTree.helpers import addUserPermissions
|
from InvenTree.api_tester import InvenTreeAPITestCase
|
||||||
|
|
||||||
from .models import Company
|
from .models import Company
|
||||||
|
|
||||||
|
|
||||||
class CompanyTest(APITestCase):
|
class CompanyTest(InvenTreeAPITestCase):
|
||||||
"""
|
"""
|
||||||
Series of tests for the Company DRF API
|
Series of tests for the Company DRF API
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def setUp(self):
|
roles = [
|
||||||
# Create a user for auth
|
'purchase_order.add',
|
||||||
user = get_user_model()
|
'purchase_order.change',
|
||||||
self.user = user.objects.create_user('testuser', 'test@testing.com', 'password')
|
|
||||||
|
|
||||||
perms = [
|
|
||||||
'view_company',
|
|
||||||
'change_company',
|
|
||||||
'add_company',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
addUserPermissions(self.user, perms)
|
def setUp(self):
|
||||||
|
|
||||||
self.client.login(username='testuser', password='password')
|
super().setUp()
|
||||||
|
|
||||||
Company.objects.create(name='ACME', description='Supplier', is_customer=False, is_supplier=True)
|
Company.objects.create(name='ACME', description='Supplier', is_customer=False, is_supplier=True)
|
||||||
Company.objects.create(name='Drippy Cup Co.', description='Customer', is_customer=True, is_supplier=False)
|
Company.objects.create(name='Drippy Cup Co.', description='Customer', is_customer=True, is_supplier=False)
|
||||||
@ -36,24 +28,24 @@ class CompanyTest(APITestCase):
|
|||||||
url = reverse('api-company-list')
|
url = reverse('api-company-list')
|
||||||
|
|
||||||
# There should be two companies
|
# There should be two companies
|
||||||
response = self.client.get(url, format='json')
|
response = self.get(url)
|
||||||
self.assertEqual(len(response.data), 3)
|
self.assertEqual(len(response.data), 3)
|
||||||
|
|
||||||
data = {'is_customer': True}
|
data = {'is_customer': True}
|
||||||
|
|
||||||
# There should only be one customer
|
# There should only be one customer
|
||||||
response = self.client.get(url, data, format='json')
|
response = self.get(url, data)
|
||||||
self.assertEqual(len(response.data), 1)
|
self.assertEqual(len(response.data), 1)
|
||||||
|
|
||||||
data = {'is_supplier': True}
|
data = {'is_supplier': True}
|
||||||
|
|
||||||
# There should be two suppliers
|
# There should be two suppliers
|
||||||
response = self.client.get(url, data, format='json')
|
response = self.get(url, data)
|
||||||
self.assertEqual(len(response.data), 2)
|
self.assertEqual(len(response.data), 2)
|
||||||
|
|
||||||
def test_company_detail(self):
|
def test_company_detail(self):
|
||||||
url = reverse('api-company-detail', kwargs={'pk': 1})
|
url = reverse('api-company-detail', kwargs={'pk': 1})
|
||||||
response = self.client.get(url, format='json')
|
response = self.get(url)
|
||||||
|
|
||||||
self.assertEqual(response.data['name'], 'ACME')
|
self.assertEqual(response.data['name'], 'ACME')
|
||||||
|
|
||||||
@ -68,5 +60,5 @@ class CompanyTest(APITestCase):
|
|||||||
# Test search functionality in company list
|
# Test search functionality in company list
|
||||||
url = reverse('api-company-list')
|
url = reverse('api-company-list')
|
||||||
data = {'search': 'cup'}
|
data = {'search': 'cup'}
|
||||||
response = self.client.get(url, data, format='json')
|
response = self.get(url, data)
|
||||||
self.assertEqual(len(response.data), 2)
|
self.assertEqual(len(response.data), 2)
|
||||||
|
@ -3,13 +3,12 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework.test import APITestCase
|
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
|
from InvenTree.api_tester import InvenTreeAPITestCase
|
||||||
|
|
||||||
|
|
||||||
class TestReportTests(APITestCase):
|
class TestReportTests(InvenTreeAPITestCase):
|
||||||
"""
|
"""
|
||||||
Tests for the StockItem TestReport templates
|
Tests for the StockItem TestReport templates
|
||||||
"""
|
"""
|
||||||
@ -21,17 +20,16 @@ class TestReportTests(APITestCase):
|
|||||||
'stock',
|
'stock',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
roles = [
|
||||||
|
'stock.view',
|
||||||
|
'stock_location.view',
|
||||||
|
]
|
||||||
|
|
||||||
list_url = reverse('api-stockitem-testreport-list')
|
list_url = reverse('api-stockitem-testreport-list')
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
user = get_user_model()
|
|
||||||
|
|
||||||
self.user = user.objects.create_user('testuser', 'test@testing.com', 'password')
|
super().setUp()
|
||||||
|
|
||||||
self.user.is_staff = True
|
|
||||||
self.user.save()
|
|
||||||
|
|
||||||
self.client.login(username='testuser', password='password')
|
|
||||||
|
|
||||||
def do_list(self, filters={}):
|
def do_list(self, filters={}):
|
||||||
|
|
||||||
|
@ -4,16 +4,16 @@ Tests for the Order API
|
|||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from rest_framework.test import APITestCase
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
|
from InvenTree.api_tester import InvenTreeAPITestCase
|
||||||
|
|
||||||
from .models import PurchaseOrder, SalesOrder
|
from .models import PurchaseOrder, SalesOrder
|
||||||
|
|
||||||
|
|
||||||
class OrderTest(APITestCase):
|
class OrderTest(InvenTreeAPITestCase):
|
||||||
|
|
||||||
fixtures = [
|
fixtures = [
|
||||||
'category',
|
'category',
|
||||||
@ -26,25 +26,20 @@ class OrderTest(APITestCase):
|
|||||||
'sales_order',
|
'sales_order',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
roles = [
|
||||||
|
'purchase_order.change',
|
||||||
|
'sales_order.change',
|
||||||
|
]
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
# Create a user for auth
|
|
||||||
get_user_model().objects.create_user('testuser', 'test@testing.com', 'password')
|
|
||||||
self.client.login(username='testuser', password='password')
|
|
||||||
|
|
||||||
def doGet(self, url, data={}):
|
|
||||||
|
|
||||||
return self.client.get(url, data=data, format='json')
|
|
||||||
|
|
||||||
def doPost(self, url, data={}):
|
|
||||||
return self.client.post(url, data=data, format='json')
|
|
||||||
|
|
||||||
def filter(self, filters, count):
|
def filter(self, filters, count):
|
||||||
"""
|
"""
|
||||||
Test API filters
|
Test API filters
|
||||||
"""
|
"""
|
||||||
|
|
||||||
response = self.doGet(
|
response = self.get(
|
||||||
self.LIST_URL,
|
self.LIST_URL,
|
||||||
filters
|
filters
|
||||||
)
|
)
|
||||||
@ -98,7 +93,7 @@ class PurchaseOrderTest(OrderTest):
|
|||||||
|
|
||||||
url = '/api/order/po/1/'
|
url = '/api/order/po/1/'
|
||||||
|
|
||||||
response = self.doGet(url)
|
response = self.get(url)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
@ -111,7 +106,7 @@ class PurchaseOrderTest(OrderTest):
|
|||||||
|
|
||||||
url = reverse('api-po-attachment-list')
|
url = reverse('api-po-attachment-list')
|
||||||
|
|
||||||
response = self.doGet(url)
|
response = self.get(url)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
@ -161,7 +156,7 @@ class SalesOrderTest(OrderTest):
|
|||||||
|
|
||||||
url = '/api/order/so/1/'
|
url = '/api/order/so/1/'
|
||||||
|
|
||||||
response = self.doGet(url)
|
response = self.get(url)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
@ -173,6 +168,6 @@ class SalesOrderTest(OrderTest):
|
|||||||
|
|
||||||
url = reverse('api-so-attachment-list')
|
url = reverse('api-so-attachment-list')
|
||||||
|
|
||||||
response = self.doGet(url)
|
response = self.get(url)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
@ -12,12 +12,12 @@ from django.db.models import Q, F, Count, Prefetch, Sum
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import filters, serializers
|
from rest_framework import filters, serializers
|
||||||
from rest_framework import generics, permissions
|
from rest_framework import generics
|
||||||
|
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from .models import Part, PartCategory, BomItem, PartStar
|
from .models import Part, PartCategory, BomItem
|
||||||
from .models import PartParameter, PartParameterTemplate
|
from .models import PartParameter, PartParameterTemplate
|
||||||
from .models import PartAttachment, PartTestTemplate
|
from .models import PartAttachment, PartTestTemplate
|
||||||
from .models import PartSellPriceBreak
|
from .models import PartSellPriceBreak
|
||||||
@ -30,6 +30,7 @@ from . import serializers as part_serializers
|
|||||||
from InvenTree.views import TreeSerializer
|
from InvenTree.views import TreeSerializer
|
||||||
from InvenTree.helpers import str2bool, isNull
|
from InvenTree.helpers import str2bool, isNull
|
||||||
from InvenTree.api import AttachmentMixin
|
from InvenTree.api import AttachmentMixin
|
||||||
|
|
||||||
from InvenTree.status_codes import BuildStatus
|
from InvenTree.status_codes import BuildStatus
|
||||||
|
|
||||||
|
|
||||||
@ -38,6 +39,8 @@ class PartCategoryTree(TreeSerializer):
|
|||||||
title = "Parts"
|
title = "Parts"
|
||||||
model = PartCategory
|
model = PartCategory
|
||||||
|
|
||||||
|
queryset = PartCategory.objects.all()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def root_url(self):
|
def root_url(self):
|
||||||
return reverse('part-index')
|
return reverse('part-index')
|
||||||
@ -45,10 +48,6 @@ class PartCategoryTree(TreeSerializer):
|
|||||||
def get_items(self):
|
def get_items(self):
|
||||||
return PartCategory.objects.all().prefetch_related('parts', 'children')
|
return PartCategory.objects.all().prefetch_related('parts', 'children')
|
||||||
|
|
||||||
permission_classes = [
|
|
||||||
permissions.IsAuthenticated,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class CategoryList(generics.ListCreateAPIView):
|
class CategoryList(generics.ListCreateAPIView):
|
||||||
""" API endpoint for accessing a list of PartCategory objects.
|
""" API endpoint for accessing a list of PartCategory objects.
|
||||||
@ -689,55 +688,6 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class PartStarDetail(generics.RetrieveDestroyAPIView):
|
|
||||||
""" API endpoint for viewing or removing a PartStar object """
|
|
||||||
|
|
||||||
queryset = PartStar.objects.all()
|
|
||||||
serializer_class = part_serializers.PartStarSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class PartStarList(generics.ListCreateAPIView):
|
|
||||||
""" API endpoint for accessing a list of PartStar objects.
|
|
||||||
|
|
||||||
- GET: Return list of PartStar objects
|
|
||||||
- POST: Create a new PartStar object
|
|
||||||
"""
|
|
||||||
|
|
||||||
queryset = PartStar.objects.all()
|
|
||||||
serializer_class = part_serializers.PartStarSerializer
|
|
||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
|
||||||
|
|
||||||
# Override the user field (with the logged-in user)
|
|
||||||
data = request.data.copy()
|
|
||||||
data['user'] = str(request.user.id)
|
|
||||||
|
|
||||||
serializer = self.get_serializer(data=data)
|
|
||||||
|
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
self.perform_create(serializer)
|
|
||||||
headers = self.get_success_headers(serializer.data)
|
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
|
||||||
|
|
||||||
permission_classes = [
|
|
||||||
permissions.IsAuthenticated,
|
|
||||||
]
|
|
||||||
|
|
||||||
filter_backends = [
|
|
||||||
DjangoFilterBackend,
|
|
||||||
filters.SearchFilter
|
|
||||||
]
|
|
||||||
|
|
||||||
filter_fields = [
|
|
||||||
'part',
|
|
||||||
'user',
|
|
||||||
]
|
|
||||||
|
|
||||||
search_fields = [
|
|
||||||
'partname'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class PartParameterTemplateList(generics.ListCreateAPIView):
|
class PartParameterTemplateList(generics.ListCreateAPIView):
|
||||||
""" API endpoint for accessing a list of PartParameterTemplate objects.
|
""" API endpoint for accessing a list of PartParameterTemplate objects.
|
||||||
|
|
||||||
@ -976,12 +926,6 @@ part_api_urls = [
|
|||||||
url(r'^$', PartAttachmentList.as_view(), name='api-part-attachment-list'),
|
url(r'^$', PartAttachmentList.as_view(), name='api-part-attachment-list'),
|
||||||
])),
|
])),
|
||||||
|
|
||||||
# Base URL for PartStar API endpoints
|
|
||||||
url(r'^star/', include([
|
|
||||||
url(r'^(?P<pk>\d+)/?', PartStarDetail.as_view(), name='api-part-star-detail'),
|
|
||||||
url(r'^$', PartStarList.as_view(), name='api-part-star-list'),
|
|
||||||
])),
|
|
||||||
|
|
||||||
# Base URL for part sale pricing
|
# Base URL for part sale pricing
|
||||||
url(r'^sale-price/', include([
|
url(r'^sale-price/', include([
|
||||||
url(r'^.*$', PartSalePriceList.as_view(), name='api-part-sale-price-list'),
|
url(r'^.*$', PartSalePriceList.as_view(), name='api-part-sale-price-list'),
|
||||||
|
@ -111,30 +111,21 @@
|
|||||||
{
|
{
|
||||||
accept: function() {
|
accept: function() {
|
||||||
|
|
||||||
// Delete each row one at a time!
|
// Keep track of each DELETE request
|
||||||
function deleteRow(idx) {
|
var requests = [];
|
||||||
|
|
||||||
if (idx >= rows.length) {
|
|
||||||
// All selected rows deleted - reload the table
|
|
||||||
$("#bom-table").bootstrapTable('refresh');
|
|
||||||
}
|
|
||||||
|
|
||||||
var row = rows[idx];
|
|
||||||
|
|
||||||
var url = `/api/bom/${row.pk}/`;
|
|
||||||
|
|
||||||
|
rows.forEach(function(row) {
|
||||||
|
requests.push(
|
||||||
inventreeDelete(
|
inventreeDelete(
|
||||||
url,
|
`/api/bom/${row.pk}/`,
|
||||||
{
|
|
||||||
complete: function(xhr, status) {
|
|
||||||
deleteRow(idx + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// Start the deletion!
|
// Wait for *all* the requests to complete
|
||||||
deleteRow(0);
|
$.when.apply($, requests).then(function() {
|
||||||
|
$('#bom-table').bootstrapTable('refresh');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -138,13 +138,14 @@
|
|||||||
<div class='panel-heading'>
|
<div class='panel-heading'>
|
||||||
<h4>
|
<h4>
|
||||||
{% block heading %}
|
{% block heading %}
|
||||||
<!-- Heading goes here -->
|
{% trans "Part Categories" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class='panel-content'>
|
<div class='panel-content'>
|
||||||
{% block details %}
|
{% block details %}
|
||||||
<!-- Content goes here -->
|
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='part-table'>
|
||||||
|
</table>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,18 +1,16 @@
|
|||||||
from rest_framework.test import APITestCase
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.contrib.auth.models import Group
|
|
||||||
|
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
from stock.models import StockItem
|
from stock.models import StockItem
|
||||||
from company.models import Company
|
from company.models import Company
|
||||||
|
|
||||||
|
from InvenTree.api_tester import InvenTreeAPITestCase
|
||||||
from InvenTree.status_codes import StockStatus
|
from InvenTree.status_codes import StockStatus
|
||||||
|
|
||||||
|
|
||||||
class PartAPITest(APITestCase):
|
class PartAPITest(InvenTreeAPITestCase):
|
||||||
"""
|
"""
|
||||||
Series of tests for the Part DRF API
|
Series of tests for the Part DRF API
|
||||||
- Tests for Part API
|
- Tests for Part API
|
||||||
@ -27,32 +25,16 @@ class PartAPITest(APITestCase):
|
|||||||
'test_templates',
|
'test_templates',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
roles = [
|
||||||
|
'part.change',
|
||||||
|
'part.add',
|
||||||
|
'part.delete',
|
||||||
|
'part_category.change',
|
||||||
|
'part_category.add',
|
||||||
|
]
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
# Create a user for auth
|
super().setUp()
|
||||||
user = get_user_model()
|
|
||||||
|
|
||||||
self.user = user.objects.create_user(
|
|
||||||
username='testuser',
|
|
||||||
email='test@testing.com',
|
|
||||||
password='password'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Put the user into a group with the correct permissions
|
|
||||||
group = Group.objects.create(name='mygroup')
|
|
||||||
self.user.groups.add(group)
|
|
||||||
|
|
||||||
# Give the group *all* the permissions!
|
|
||||||
for rule in group.rule_sets.all():
|
|
||||||
rule.can_view = True
|
|
||||||
rule.can_change = True
|
|
||||||
rule.can_add = True
|
|
||||||
rule.can_delete = True
|
|
||||||
|
|
||||||
rule.save()
|
|
||||||
|
|
||||||
group.save()
|
|
||||||
|
|
||||||
self.client.login(username='testuser', password='password')
|
|
||||||
|
|
||||||
def test_get_categories(self):
|
def test_get_categories(self):
|
||||||
""" Test that we can retrieve list of part categories """
|
""" Test that we can retrieve list of part categories """
|
||||||
@ -254,7 +236,7 @@ class PartAPITest(APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class PartAPIAggregationTest(APITestCase):
|
class PartAPIAggregationTest(InvenTreeAPITestCase):
|
||||||
"""
|
"""
|
||||||
Tests to ensure that the various aggregation annotations are working correctly...
|
Tests to ensure that the various aggregation annotations are working correctly...
|
||||||
"""
|
"""
|
||||||
@ -268,13 +250,14 @@ class PartAPIAggregationTest(APITestCase):
|
|||||||
'test_templates',
|
'test_templates',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
roles = [
|
||||||
|
'part.view',
|
||||||
|
'part.change',
|
||||||
|
]
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
# Create a user for auth
|
|
||||||
user = get_user_model()
|
|
||||||
|
|
||||||
user.objects.create_user('testuser', 'test@testing.com', 'password')
|
super().setUp()
|
||||||
|
|
||||||
self.client.login(username='testuser', password='password')
|
|
||||||
|
|
||||||
# Add a new part
|
# Add a new part
|
||||||
self.part = Part.objects.create(
|
self.part = Part.objects.create(
|
||||||
|
@ -48,7 +48,6 @@
|
|||||||
<li><a href='#' id='barcode-check-in'><span class='fas fa-arrow-right'></span> {% trans "Check-in Items" %}</a></li>
|
<li><a href='#' id='barcode-check-in'><span class='fas fa-arrow-right'></span> {% trans "Check-in Items" %}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
<!-- Check permissions and owner -->
|
<!-- Check permissions and owner -->
|
||||||
{% if owner_control.value == "False" or owner_control.value == "True" and user in owners or user.is_superuser %}
|
{% if owner_control.value == "False" or owner_control.value == "True" and user in owners or user.is_superuser %}
|
||||||
{% if roles.stock.change %}
|
{% if roles.stock.change %}
|
||||||
@ -73,6 +72,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='col-sm-6'>
|
<div class='col-sm-6'>
|
||||||
|
@ -7,20 +7,18 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from rest_framework.test import APITestCase
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
|
|
||||||
from InvenTree.helpers import addUserPermissions
|
|
||||||
from InvenTree.status_codes import StockStatus
|
from InvenTree.status_codes import StockStatus
|
||||||
|
from InvenTree.api_tester import InvenTreeAPITestCase
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
|
|
||||||
from .models import StockItem, StockLocation
|
from .models import StockItem, StockLocation
|
||||||
|
|
||||||
|
|
||||||
class StockAPITestCase(APITestCase):
|
class StockAPITestCase(InvenTreeAPITestCase):
|
||||||
|
|
||||||
fixtures = [
|
fixtures = [
|
||||||
'category',
|
'category',
|
||||||
@ -32,34 +30,16 @@ class StockAPITestCase(APITestCase):
|
|||||||
'stock_tests',
|
'stock_tests',
|
||||||
]
|
]
|
||||||
|
|
||||||
def setUp(self):
|
roles = [
|
||||||
# Create a user for auth
|
'stock.change',
|
||||||
user = get_user_model()
|
'stock.add',
|
||||||
|
'stock_location.change',
|
||||||
self.user = user.objects.create_user('testuser', 'test@testing.com', 'password')
|
'stock_location.add',
|
||||||
|
|
||||||
self.user.is_staff = True
|
|
||||||
self.user.save()
|
|
||||||
|
|
||||||
# Add the necessary permissions to the user
|
|
||||||
perms = [
|
|
||||||
'view_stockitemtestresult',
|
|
||||||
'change_stockitemtestresult',
|
|
||||||
'add_stockitemtestresult',
|
|
||||||
'add_stocklocation',
|
|
||||||
'change_stocklocation',
|
|
||||||
'add_stockitem',
|
|
||||||
'change_stockitem',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
addUserPermissions(self.user, perms)
|
def setUp(self):
|
||||||
|
|
||||||
self.client.login(username='testuser', password='password')
|
super().setUp()
|
||||||
|
|
||||||
def doPost(self, url, data={}):
|
|
||||||
response = self.client.post(url, data=data, format='json')
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class StockLocationTest(StockAPITestCase):
|
class StockLocationTest(StockAPITestCase):
|
||||||
@ -232,6 +212,9 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
response = self.get_stock(expired=1)
|
response = self.get_stock(expired=1)
|
||||||
self.assertEqual(len(response), 20)
|
self.assertEqual(len(response), 20)
|
||||||
|
|
||||||
|
self.user.is_staff = True
|
||||||
|
self.user.save()
|
||||||
|
|
||||||
# Now, ensure that the expiry date feature is enabled!
|
# Now, ensure that the expiry date feature is enabled!
|
||||||
InvenTreeSetting.set_setting('STOCK_ENABLE_EXPIRY', True, self.user)
|
InvenTreeSetting.set_setting('STOCK_ENABLE_EXPIRY', True, self.user)
|
||||||
|
|
||||||
@ -449,7 +432,7 @@ class StocktakeTest(StockAPITestCase):
|
|||||||
data = {}
|
data = {}
|
||||||
|
|
||||||
# POST with a valid action
|
# POST with a valid action
|
||||||
response = self.doPost(url, data)
|
response = self.post(url, data)
|
||||||
self.assertContains(response, "must contain list", status_code=status.HTTP_400_BAD_REQUEST)
|
self.assertContains(response, "must contain list", status_code=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
data['items'] = [{
|
data['items'] = [{
|
||||||
@ -457,7 +440,7 @@ class StocktakeTest(StockAPITestCase):
|
|||||||
}]
|
}]
|
||||||
|
|
||||||
# POST without a PK
|
# POST without a PK
|
||||||
response = self.doPost(url, data)
|
response = self.post(url, data)
|
||||||
self.assertContains(response, 'must contain a valid pk', status_code=status.HTTP_400_BAD_REQUEST)
|
self.assertContains(response, 'must contain a valid pk', status_code=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# POST with a PK but no quantity
|
# POST with a PK but no quantity
|
||||||
@ -465,14 +448,14 @@ class StocktakeTest(StockAPITestCase):
|
|||||||
'pk': 10
|
'pk': 10
|
||||||
}]
|
}]
|
||||||
|
|
||||||
response = self.doPost(url, data)
|
response = self.post(url, data)
|
||||||
self.assertContains(response, 'must contain a valid pk', status_code=status.HTTP_400_BAD_REQUEST)
|
self.assertContains(response, 'must contain a valid pk', status_code=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
data['items'] = [{
|
data['items'] = [{
|
||||||
'pk': 1234
|
'pk': 1234
|
||||||
}]
|
}]
|
||||||
|
|
||||||
response = self.doPost(url, data)
|
response = self.post(url, data)
|
||||||
self.assertContains(response, 'must contain a valid quantity', status_code=status.HTTP_400_BAD_REQUEST)
|
self.assertContains(response, 'must contain a valid quantity', status_code=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
data['items'] = [{
|
data['items'] = [{
|
||||||
@ -480,7 +463,7 @@ class StocktakeTest(StockAPITestCase):
|
|||||||
'quantity': '10x0d'
|
'quantity': '10x0d'
|
||||||
}]
|
}]
|
||||||
|
|
||||||
response = self.doPost(url, data)
|
response = self.post(url, data)
|
||||||
self.assertContains(response, 'must contain a valid quantity', status_code=status.HTTP_400_BAD_REQUEST)
|
self.assertContains(response, 'must contain a valid quantity', status_code=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
data['items'] = [{
|
data['items'] = [{
|
||||||
@ -488,7 +471,7 @@ class StocktakeTest(StockAPITestCase):
|
|||||||
'quantity': "-1.234"
|
'quantity': "-1.234"
|
||||||
}]
|
}]
|
||||||
|
|
||||||
response = self.doPost(url, data)
|
response = self.post(url, data)
|
||||||
self.assertContains(response, 'must not be less than zero', status_code=status.HTTP_400_BAD_REQUEST)
|
self.assertContains(response, 'must not be less than zero', status_code=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# Test with a single item
|
# Test with a single item
|
||||||
@ -499,7 +482,7 @@ class StocktakeTest(StockAPITestCase):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
response = self.doPost(url, data)
|
response = self.post(url, data)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
def test_transfer(self):
|
def test_transfer(self):
|
||||||
@ -518,13 +501,13 @@ class StocktakeTest(StockAPITestCase):
|
|||||||
|
|
||||||
url = reverse('api-stock-transfer')
|
url = reverse('api-stock-transfer')
|
||||||
|
|
||||||
response = self.doPost(url, data)
|
response = self.post(url, data)
|
||||||
self.assertContains(response, "Moved 1 parts to", status_code=status.HTTP_200_OK)
|
self.assertContains(response, "Moved 1 parts to", status_code=status.HTTP_200_OK)
|
||||||
|
|
||||||
# Now try one which will fail due to a bad location
|
# Now try one which will fail due to a bad location
|
||||||
data['location'] = 'not a location'
|
data['location'] = 'not a location'
|
||||||
|
|
||||||
response = self.doPost(url, data)
|
response = self.post(url, data)
|
||||||
self.assertContains(response, 'Valid location must be specified', status_code=status.HTTP_400_BAD_REQUEST)
|
self.assertContains(response, 'Valid location must be specified', status_code=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework import generics, permissions
|
from rest_framework import generics, permissions
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
@ -8,6 +11,53 @@ from rest_framework.authtoken.models import Token
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
|
from .models import RuleSet, check_user_role
|
||||||
|
|
||||||
|
|
||||||
|
class RoleDetails(APIView):
|
||||||
|
"""
|
||||||
|
API endpoint which lists the available role permissions
|
||||||
|
for the current user
|
||||||
|
|
||||||
|
(Requires authentication)
|
||||||
|
"""
|
||||||
|
|
||||||
|
permission_classes = [
|
||||||
|
permissions.IsAuthenticated
|
||||||
|
]
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
|
||||||
|
user = request.user
|
||||||
|
|
||||||
|
roles = {}
|
||||||
|
|
||||||
|
for ruleset in RuleSet.RULESET_CHOICES:
|
||||||
|
|
||||||
|
role, text = ruleset
|
||||||
|
|
||||||
|
permissions = []
|
||||||
|
|
||||||
|
for permission in RuleSet.RULESET_PERMISSIONS:
|
||||||
|
if check_user_role(user, role, permission):
|
||||||
|
|
||||||
|
permissions.append(permission)
|
||||||
|
|
||||||
|
if len(permissions) > 0:
|
||||||
|
roles[role] = permissions
|
||||||
|
else:
|
||||||
|
roles[role] = None
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'user': user.pk,
|
||||||
|
'username': user.username,
|
||||||
|
'roles': roles,
|
||||||
|
'is_staff': user.is_staff,
|
||||||
|
'is_superuser': user.is_superuser,
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response(data)
|
||||||
|
|
||||||
|
|
||||||
class UserDetail(generics.RetrieveAPIView):
|
class UserDetail(generics.RetrieveAPIView):
|
||||||
""" Detail endpoint for a single user """
|
""" Detail endpoint for a single user """
|
@ -67,15 +67,19 @@ class RuleSet(models.Model):
|
|||||||
'part_partparametertemplate',
|
'part_partparametertemplate',
|
||||||
'part_partparameter',
|
'part_partparameter',
|
||||||
'part_partrelated',
|
'part_partrelated',
|
||||||
|
'part_partstar',
|
||||||
],
|
],
|
||||||
'stock_location': [
|
'stock_location': [
|
||||||
'stock_stocklocation',
|
'stock_stocklocation',
|
||||||
|
'label_stocklocationlabel',
|
||||||
],
|
],
|
||||||
'stock': [
|
'stock': [
|
||||||
'stock_stockitem',
|
'stock_stockitem',
|
||||||
'stock_stockitemattachment',
|
'stock_stockitemattachment',
|
||||||
'stock_stockitemtracking',
|
'stock_stockitemtracking',
|
||||||
'stock_stockitemtestresult',
|
'stock_stockitemtestresult',
|
||||||
|
'report_testreport',
|
||||||
|
'label_stockitemlabel',
|
||||||
],
|
],
|
||||||
'build': [
|
'build': [
|
||||||
'part_part',
|
'part_part',
|
||||||
@ -86,6 +90,7 @@ class RuleSet(models.Model):
|
|||||||
'build_buildorderattachment',
|
'build_buildorderattachment',
|
||||||
'stock_stockitem',
|
'stock_stockitem',
|
||||||
'stock_stocklocation',
|
'stock_stocklocation',
|
||||||
|
'report_buildreport',
|
||||||
],
|
],
|
||||||
'purchase_order': [
|
'purchase_order': [
|
||||||
'company_company',
|
'company_company',
|
||||||
@ -115,14 +120,9 @@ class RuleSet(models.Model):
|
|||||||
'common_colortheme',
|
'common_colortheme',
|
||||||
'common_inventreesetting',
|
'common_inventreesetting',
|
||||||
'company_contact',
|
'company_contact',
|
||||||
'label_stockitemlabel',
|
|
||||||
'label_stocklocationlabel',
|
|
||||||
'report_reportasset',
|
'report_reportasset',
|
||||||
'report_reportsnippet',
|
'report_reportsnippet',
|
||||||
'report_testreport',
|
|
||||||
'report_buildreport',
|
|
||||||
'report_billofmaterialsreport',
|
'report_billofmaterialsreport',
|
||||||
'part_partstar',
|
|
||||||
'users_owner',
|
'users_owner',
|
||||||
|
|
||||||
# Third-party tables
|
# Third-party tables
|
||||||
@ -166,6 +166,26 @@ class RuleSet(models.Model):
|
|||||||
|
|
||||||
can_delete = models.BooleanField(verbose_name=_('Delete'), default=False, help_text=_('Permission to delete items'))
|
can_delete = models.BooleanField(verbose_name=_('Delete'), default=False, help_text=_('Permission to delete items'))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def check_table_permission(cls, user, table, permission):
|
||||||
|
"""
|
||||||
|
Check if the provided user has the specified permission against the table
|
||||||
|
"""
|
||||||
|
|
||||||
|
# If the table does *not* require permissions
|
||||||
|
if table in cls.RULESET_IGNORE:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Work out which roles touch the given table
|
||||||
|
for role in cls.RULESET_NAMES:
|
||||||
|
if table in cls.RULESET_MODELS[role]:
|
||||||
|
|
||||||
|
if check_user_role(user, role, permission):
|
||||||
|
return True
|
||||||
|
|
||||||
|
print("failed permission check for", table, permission)
|
||||||
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_model_permission_string(model, permission):
|
def get_model_permission_string(model, permission):
|
||||||
"""
|
"""
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from . import views
|
from . import api
|
||||||
|
|
||||||
user_urls = [
|
user_urls = [
|
||||||
url(r'^(?P<pk>[0-9]+)/?$', views.UserDetail.as_view(), name='user-detail'),
|
url(r'^(?P<pk>[0-9]+)/?$', api.UserDetail.as_view(), name='user-detail'),
|
||||||
|
|
||||||
url(r'token', views.GetAuthToken.as_view(), name='api-token'),
|
url(r'roles', api.RoleDetails.as_view(), name='api-user-roles'),
|
||||||
|
url(r'token', api.GetAuthToken.as_view(), name='api-token'),
|
||||||
|
|
||||||
url(r'^$', views.UserList.as_view()),
|
url(r'^$', api.UserList.as_view()),
|
||||||
]
|
]
|
||||||
|
Loading…
Reference in New Issue
Block a user