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': (
|
||||
'rest_framework.permissions.IsAuthenticated',
|
||||
'rest_framework.permissions.DjangoModelPermissions',
|
||||
'InvenTree.permissions.RolePermission',
|
||||
),
|
||||
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema'
|
||||
}
|
||||
|
@ -1,15 +1,17 @@
|
||||
""" Low level tests for the InvenTree API """
|
||||
|
||||
from rest_framework.test import APITestCase
|
||||
from rest_framework import status
|
||||
|
||||
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
|
||||
|
||||
|
||||
class APITests(APITestCase):
|
||||
class APITests(InvenTreeAPITestCase):
|
||||
""" Tests for the InvenTree API """
|
||||
|
||||
fixtures = [
|
||||
@ -19,15 +21,13 @@ class APITests(APITestCase):
|
||||
'category',
|
||||
]
|
||||
|
||||
username = 'test_user'
|
||||
password = 'test_pass'
|
||||
|
||||
token = None
|
||||
|
||||
auto_login = False
|
||||
|
||||
def setUp(self):
|
||||
|
||||
# Create a user (but do not log in!)
|
||||
get_user_model().objects.create_user(self.username, 'user@email.com', self.password)
|
||||
super().setUp()
|
||||
|
||||
def basicAuth(self):
|
||||
# Use basic authentication
|
||||
@ -78,3 +78,82 @@ class APITests(APITestCase):
|
||||
self.assertIn('instance', data)
|
||||
|
||||
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 rest_framework.test import APITestCase
|
||||
|
||||
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 build.models import Build
|
||||
|
||||
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
|
||||
"""
|
||||
@ -28,32 +25,15 @@ class BuildAPITest(APITestCase):
|
||||
'build',
|
||||
]
|
||||
|
||||
# Required roles to access Build API endpoints
|
||||
roles = [
|
||||
'build.change',
|
||||
'build.add'
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
# Create a user for auth
|
||||
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')
|
||||
super().setUp()
|
||||
|
||||
|
||||
class BuildListTest(BuildAPITest):
|
||||
@ -63,34 +43,26 @@ class BuildListTest(BuildAPITest):
|
||||
|
||||
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):
|
||||
"""
|
||||
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})
|
||||
self.assertEqual(len(builds), 1)
|
||||
builds = self.get(self.url, data={'active': True})
|
||||
self.assertEqual(len(builds.data), 1)
|
||||
|
||||
builds = self.get(data={'status': BuildStatus.COMPLETE})
|
||||
self.assertEqual(len(builds), 4)
|
||||
builds = self.get(self.url, data={'status': BuildStatus.COMPLETE})
|
||||
self.assertEqual(len(builds.data), 4)
|
||||
|
||||
builds = self.get(data={'overdue': False})
|
||||
self.assertEqual(len(builds), 5)
|
||||
builds = self.get(self.url, data={'overdue': False})
|
||||
self.assertEqual(len(builds.data), 5)
|
||||
|
||||
builds = self.get(data={'overdue': True})
|
||||
self.assertEqual(len(builds), 0)
|
||||
builds = self.get(self.url, data={'overdue': True})
|
||||
self.assertEqual(len(builds.data), 0)
|
||||
|
||||
def test_overdue(self):
|
||||
"""
|
||||
@ -109,7 +81,9 @@ class BuildListTest(BuildAPITest):
|
||||
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)
|
||||
|
||||
@ -152,11 +126,15 @@ class BuildListTest(BuildAPITest):
|
||||
Build.objects.rebuild()
|
||||
|
||||
# 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)
|
||||
|
||||
# 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)
|
||||
|
@ -1,32 +1,24 @@
|
||||
from rest_framework.test import APITestCase
|
||||
from rest_framework import status
|
||||
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
|
||||
|
||||
|
||||
class CompanyTest(APITestCase):
|
||||
class CompanyTest(InvenTreeAPITestCase):
|
||||
"""
|
||||
Series of tests for the Company DRF API
|
||||
"""
|
||||
|
||||
roles = [
|
||||
'purchase_order.add',
|
||||
'purchase_order.change',
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
# Create a user for auth
|
||||
user = get_user_model()
|
||||
self.user = user.objects.create_user('testuser', 'test@testing.com', 'password')
|
||||
|
||||
perms = [
|
||||
'view_company',
|
||||
'change_company',
|
||||
'add_company',
|
||||
]
|
||||
|
||||
addUserPermissions(self.user, perms)
|
||||
|
||||
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='Drippy Cup Co.', description='Customer', is_customer=True, is_supplier=False)
|
||||
@ -36,24 +28,24 @@ class CompanyTest(APITestCase):
|
||||
url = reverse('api-company-list')
|
||||
|
||||
# There should be two companies
|
||||
response = self.client.get(url, format='json')
|
||||
response = self.get(url)
|
||||
self.assertEqual(len(response.data), 3)
|
||||
|
||||
data = {'is_customer': True}
|
||||
|
||||
# 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)
|
||||
|
||||
data = {'is_supplier': True}
|
||||
|
||||
# There should be two suppliers
|
||||
response = self.client.get(url, data, format='json')
|
||||
response = self.get(url, data)
|
||||
self.assertEqual(len(response.data), 2)
|
||||
|
||||
def test_company_detail(self):
|
||||
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')
|
||||
|
||||
@ -68,5 +60,5 @@ class CompanyTest(APITestCase):
|
||||
# Test search functionality in company list
|
||||
url = reverse('api-company-list')
|
||||
data = {'search': 'cup'}
|
||||
response = self.client.get(url, data, format='json')
|
||||
response = self.get(url, data)
|
||||
self.assertEqual(len(response.data), 2)
|
||||
|
@ -3,13 +3,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
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
|
||||
"""
|
||||
@ -21,17 +20,16 @@ class TestReportTests(APITestCase):
|
||||
'stock',
|
||||
]
|
||||
|
||||
roles = [
|
||||
'stock.view',
|
||||
'stock_location.view',
|
||||
]
|
||||
|
||||
list_url = reverse('api-stockitem-testreport-list')
|
||||
|
||||
def setUp(self):
|
||||
user = get_user_model()
|
||||
|
||||
self.user = user.objects.create_user('testuser', 'test@testing.com', 'password')
|
||||
|
||||
self.user.is_staff = True
|
||||
self.user.save()
|
||||
|
||||
self.client.login(username='testuser', password='password')
|
||||
super().setUp()
|
||||
|
||||
def do_list(self, filters={}):
|
||||
|
||||
|
@ -4,16 +4,16 @@ Tests for the Order API
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from rest_framework.test import APITestCase
|
||||
from rest_framework import status
|
||||
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from InvenTree.api_tester import InvenTreeAPITestCase
|
||||
|
||||
from .models import PurchaseOrder, SalesOrder
|
||||
|
||||
|
||||
class OrderTest(APITestCase):
|
||||
class OrderTest(InvenTreeAPITestCase):
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
@ -26,25 +26,20 @@ class OrderTest(APITestCase):
|
||||
'sales_order',
|
||||
]
|
||||
|
||||
roles = [
|
||||
'purchase_order.change',
|
||||
'sales_order.change',
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
|
||||
# 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')
|
||||
super().setUp()
|
||||
|
||||
def filter(self, filters, count):
|
||||
"""
|
||||
Test API filters
|
||||
"""
|
||||
|
||||
response = self.doGet(
|
||||
response = self.get(
|
||||
self.LIST_URL,
|
||||
filters
|
||||
)
|
||||
@ -98,7 +93,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
|
||||
url = '/api/order/po/1/'
|
||||
|
||||
response = self.doGet(url)
|
||||
response = self.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@ -111,7 +106,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
|
||||
url = reverse('api-po-attachment-list')
|
||||
|
||||
response = self.doGet(url)
|
||||
response = self.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
@ -161,7 +156,7 @@ class SalesOrderTest(OrderTest):
|
||||
|
||||
url = '/api/order/so/1/'
|
||||
|
||||
response = self.doGet(url)
|
||||
response = self.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@ -173,6 +168,6 @@ class SalesOrderTest(OrderTest):
|
||||
|
||||
url = reverse('api-so-attachment-list')
|
||||
|
||||
response = self.doGet(url)
|
||||
response = self.get(url)
|
||||
|
||||
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.response import Response
|
||||
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.urls import reverse
|
||||
|
||||
from .models import Part, PartCategory, BomItem, PartStar
|
||||
from .models import Part, PartCategory, BomItem
|
||||
from .models import PartParameter, PartParameterTemplate
|
||||
from .models import PartAttachment, PartTestTemplate
|
||||
from .models import PartSellPriceBreak
|
||||
@ -30,6 +30,7 @@ from . import serializers as part_serializers
|
||||
from InvenTree.views import TreeSerializer
|
||||
from InvenTree.helpers import str2bool, isNull
|
||||
from InvenTree.api import AttachmentMixin
|
||||
|
||||
from InvenTree.status_codes import BuildStatus
|
||||
|
||||
|
||||
@ -38,6 +39,8 @@ class PartCategoryTree(TreeSerializer):
|
||||
title = "Parts"
|
||||
model = PartCategory
|
||||
|
||||
queryset = PartCategory.objects.all()
|
||||
|
||||
@property
|
||||
def root_url(self):
|
||||
return reverse('part-index')
|
||||
@ -45,10 +48,6 @@ class PartCategoryTree(TreeSerializer):
|
||||
def get_items(self):
|
||||
return PartCategory.objects.all().prefetch_related('parts', 'children')
|
||||
|
||||
permission_classes = [
|
||||
permissions.IsAuthenticated,
|
||||
]
|
||||
|
||||
|
||||
class CategoryList(generics.ListCreateAPIView):
|
||||
""" 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):
|
||||
""" 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'),
|
||||
])),
|
||||
|
||||
# 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
|
||||
url(r'^sale-price/', include([
|
||||
url(r'^.*$', PartSalePriceList.as_view(), name='api-part-sale-price-list'),
|
||||
|
@ -111,34 +111,25 @@
|
||||
{
|
||||
accept: function() {
|
||||
|
||||
// Delete each row one at a time!
|
||||
function deleteRow(idx) {
|
||||
// Keep track of each DELETE request
|
||||
var requests = [];
|
||||
|
||||
if (idx >= rows.length) {
|
||||
// All selected rows deleted - reload the table
|
||||
$("#bom-table").bootstrapTable('refresh');
|
||||
}
|
||||
rows.forEach(function(row) {
|
||||
requests.push(
|
||||
inventreeDelete(
|
||||
`/api/bom/${row.pk}/`,
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
var row = rows[idx];
|
||||
|
||||
var url = `/api/bom/${row.pk}/`;
|
||||
|
||||
inventreeDelete(
|
||||
url,
|
||||
{
|
||||
complete: function(xhr, status) {
|
||||
deleteRow(idx + 1);
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Start the deletion!
|
||||
deleteRow(0);
|
||||
// Wait for *all* the requests to complete
|
||||
$.when.apply($, requests).then(function() {
|
||||
$('#bom-table').bootstrapTable('refresh');
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
$('#bom-upload').click(function() {
|
||||
location.href = "{% url 'upload-bom' part.id %}";
|
||||
|
@ -138,13 +138,14 @@
|
||||
<div class='panel-heading'>
|
||||
<h4>
|
||||
{% block heading %}
|
||||
<!-- Heading goes here -->
|
||||
{% trans "Part Categories" %}
|
||||
{% endblock %}
|
||||
</h4>
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
{% block details %}
|
||||
<!-- Content goes here -->
|
||||
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='part-table'>
|
||||
</table>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,18 +1,16 @@
|
||||
from rest_framework.test import APITestCase
|
||||
from rest_framework import status
|
||||
|
||||
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 stock.models import StockItem
|
||||
from company.models import Company
|
||||
|
||||
from InvenTree.api_tester import InvenTreeAPITestCase
|
||||
from InvenTree.status_codes import StockStatus
|
||||
|
||||
|
||||
class PartAPITest(APITestCase):
|
||||
class PartAPITest(InvenTreeAPITestCase):
|
||||
"""
|
||||
Series of tests for the Part DRF API
|
||||
- Tests for Part API
|
||||
@ -27,32 +25,16 @@ class PartAPITest(APITestCase):
|
||||
'test_templates',
|
||||
]
|
||||
|
||||
roles = [
|
||||
'part.change',
|
||||
'part.add',
|
||||
'part.delete',
|
||||
'part_category.change',
|
||||
'part_category.add',
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
# Create a user for auth
|
||||
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')
|
||||
super().setUp()
|
||||
|
||||
def test_get_categories(self):
|
||||
""" 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)
|
||||
|
||||
|
||||
class PartAPIAggregationTest(APITestCase):
|
||||
class PartAPIAggregationTest(InvenTreeAPITestCase):
|
||||
"""
|
||||
Tests to ensure that the various aggregation annotations are working correctly...
|
||||
"""
|
||||
@ -268,13 +250,14 @@ class PartAPIAggregationTest(APITestCase):
|
||||
'test_templates',
|
||||
]
|
||||
|
||||
roles = [
|
||||
'part.view',
|
||||
'part.change',
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
# Create a user for auth
|
||||
user = get_user_model()
|
||||
|
||||
user.objects.create_user('testuser', 'test@testing.com', 'password')
|
||||
|
||||
self.client.login(username='testuser', password='password')
|
||||
super().setUp()
|
||||
|
||||
# Add a new part
|
||||
self.part = Part.objects.create(
|
||||
|
@ -48,15 +48,14 @@
|
||||
<li><a href='#' id='barcode-check-in'><span class='fas fa-arrow-right'></span> {% trans "Check-in Items" %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
<!-- Check permissions and owner -->
|
||||
{% if owner_control.value == "False" or owner_control.value == "True" and user in owners or user.is_superuser %}
|
||||
{% if roles.stock.change %}
|
||||
<div class='btn-group'>
|
||||
<button id='stock-actions' title='{% trans "Stock actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-boxes'></span> <span class='caret'></span></button>
|
||||
<ul class='dropdown-menu' role='menu'>
|
||||
<li><a href='#' id='location-count'><span class='fas fa-clipboard-list'></span>
|
||||
{% trans "Count stock" %}</a></li>
|
||||
{% if roles.stock.change %}
|
||||
<div class='btn-group'>
|
||||
<button id='stock-actions' title='{% trans "Stock actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-boxes'></span> <span class='caret'></span></button>
|
||||
<ul class='dropdown-menu' role='menu'>
|
||||
<li><a href='#' id='location-count'><span class='fas fa-clipboard-list'></span>
|
||||
{% trans "Count stock" %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -71,7 +70,8 @@
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -7,20 +7,18 @@ from __future__ import unicode_literals
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from rest_framework.test import APITestCase
|
||||
from rest_framework import status
|
||||
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.api_tester import InvenTreeAPITestCase
|
||||
|
||||
from common.models import InvenTreeSetting
|
||||
|
||||
from .models import StockItem, StockLocation
|
||||
|
||||
|
||||
class StockAPITestCase(APITestCase):
|
||||
class StockAPITestCase(InvenTreeAPITestCase):
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
@ -32,34 +30,16 @@ class StockAPITestCase(APITestCase):
|
||||
'stock_tests',
|
||||
]
|
||||
|
||||
roles = [
|
||||
'stock.change',
|
||||
'stock.add',
|
||||
'stock_location.change',
|
||||
'stock_location.add',
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
# Create a user for auth
|
||||
user = get_user_model()
|
||||
|
||||
self.user = user.objects.create_user('testuser', 'test@testing.com', 'password')
|
||||
|
||||
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)
|
||||
|
||||
self.client.login(username='testuser', password='password')
|
||||
|
||||
def doPost(self, url, data={}):
|
||||
response = self.client.post(url, data=data, format='json')
|
||||
|
||||
return response
|
||||
super().setUp()
|
||||
|
||||
|
||||
class StockLocationTest(StockAPITestCase):
|
||||
@ -232,6 +212,9 @@ class StockItemListTest(StockAPITestCase):
|
||||
response = self.get_stock(expired=1)
|
||||
self.assertEqual(len(response), 20)
|
||||
|
||||
self.user.is_staff = True
|
||||
self.user.save()
|
||||
|
||||
# Now, ensure that the expiry date feature is enabled!
|
||||
InvenTreeSetting.set_setting('STOCK_ENABLE_EXPIRY', True, self.user)
|
||||
|
||||
@ -449,7 +432,7 @@ class StocktakeTest(StockAPITestCase):
|
||||
data = {}
|
||||
|
||||
# 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)
|
||||
|
||||
data['items'] = [{
|
||||
@ -457,7 +440,7 @@ class StocktakeTest(StockAPITestCase):
|
||||
}]
|
||||
|
||||
# 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)
|
||||
|
||||
# POST with a PK but no quantity
|
||||
@ -465,14 +448,14 @@ class StocktakeTest(StockAPITestCase):
|
||||
'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)
|
||||
|
||||
data['items'] = [{
|
||||
'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)
|
||||
|
||||
data['items'] = [{
|
||||
@ -480,7 +463,7 @@ class StocktakeTest(StockAPITestCase):
|
||||
'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)
|
||||
|
||||
data['items'] = [{
|
||||
@ -488,7 +471,7 @@ class StocktakeTest(StockAPITestCase):
|
||||
'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)
|
||||
|
||||
# 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)
|
||||
|
||||
def test_transfer(self):
|
||||
@ -518,13 +501,13 @@ class StocktakeTest(StockAPITestCase):
|
||||
|
||||
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)
|
||||
|
||||
# Now try one which will fail due to a bad 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)
|
||||
|
||||
|
||||
|
@ -1,3 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import generics, permissions
|
||||
from django.contrib.auth.models import User
|
||||
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 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):
|
||||
""" Detail endpoint for a single user """
|
@ -67,15 +67,19 @@ class RuleSet(models.Model):
|
||||
'part_partparametertemplate',
|
||||
'part_partparameter',
|
||||
'part_partrelated',
|
||||
'part_partstar',
|
||||
],
|
||||
'stock_location': [
|
||||
'stock_stocklocation',
|
||||
'label_stocklocationlabel',
|
||||
],
|
||||
'stock': [
|
||||
'stock_stockitem',
|
||||
'stock_stockitemattachment',
|
||||
'stock_stockitemtracking',
|
||||
'stock_stockitemtestresult',
|
||||
'report_testreport',
|
||||
'label_stockitemlabel',
|
||||
],
|
||||
'build': [
|
||||
'part_part',
|
||||
@ -86,6 +90,7 @@ class RuleSet(models.Model):
|
||||
'build_buildorderattachment',
|
||||
'stock_stockitem',
|
||||
'stock_stocklocation',
|
||||
'report_buildreport',
|
||||
],
|
||||
'purchase_order': [
|
||||
'company_company',
|
||||
@ -115,14 +120,9 @@ class RuleSet(models.Model):
|
||||
'common_colortheme',
|
||||
'common_inventreesetting',
|
||||
'company_contact',
|
||||
'label_stockitemlabel',
|
||||
'label_stocklocationlabel',
|
||||
'report_reportasset',
|
||||
'report_reportsnippet',
|
||||
'report_testreport',
|
||||
'report_buildreport',
|
||||
'report_billofmaterialsreport',
|
||||
'part_partstar',
|
||||
'users_owner',
|
||||
|
||||
# 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'))
|
||||
|
||||
@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
|
||||
def get_model_permission_string(model, permission):
|
||||
"""
|
||||
|
@ -1,11 +1,12 @@
|
||||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
from . import api
|
||||
|
||||
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