Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2021-02-27 08:56:47 +11:00
commit 2871051923
17 changed files with 474 additions and 288 deletions

View 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

View 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

View File

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

View File

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

View File

@ -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
""" """
@ -27,33 +24,16 @@ class BuildAPITest(APITestCase):
'bom', 'bom',
'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(
username='testuser',
email='test@testing.com',
password='password'
)
# Put the user into a group with the correct permissions super().setUp()
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)

View File

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

View File

@ -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={}):

View File

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

View File

@ -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
@ -37,6 +38,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):
@ -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.
@ -975,12 +925,6 @@ part_api_urls = [
url(r'^attachment/', include([ url(r'^attachment/', include([
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([

View File

@ -111,34 +111,25 @@
{ {
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) { rows.forEach(function(row) {
// All selected rows deleted - reload the table requests.push(
$("#bom-table").bootstrapTable('refresh'); inventreeDelete(
} `/api/bom/${row.pk}/`,
)
);
});
var row = rows[idx]; // Wait for *all* the requests to complete
$.when.apply($, requests).then(function() {
var url = `/api/bom/${row.pk}/`; $('#bom-table').bootstrapTable('refresh');
});
inventreeDelete(
url,
{
complete: function(xhr, status) {
deleteRow(idx + 1);
}
}
)
}
// Start the deletion!
deleteRow(0);
} }
} }
); );
}); });
$('#bom-upload').click(function() { $('#bom-upload').click(function() {
location.href = "{% url 'upload-bom' part.id %}"; location.href = "{% url 'upload-bom' part.id %}";

View File

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

View File

@ -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',
] ]
def setUp(self): roles = [
# Create a user for auth 'part.view',
user = get_user_model() 'part.change',
]
user.objects.create_user('testuser', 'test@testing.com', 'password')
self.client.login(username='testuser', password='password') def setUp(self):
super().setUp()
# Add a new part # Add a new part
self.part = Part.objects.create( self.part = Part.objects.create(

View File

@ -48,15 +48,14 @@
<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 %}
<div class='btn-group'> <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> <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'> <ul class='dropdown-menu' role='menu'>
<li><a href='#' id='location-count'><span class='fas fa-clipboard-list'></span> <li><a href='#' id='location-count'><span class='fas fa-clipboard-list'></span>
{% trans "Count stock" %}</a></li> {% trans "Count stock" %}</a></li>
</ul> </ul>
</div> </div>
{% endif %} {% endif %}
@ -71,7 +70,8 @@
</ul> </ul>
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endif %}
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@ -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',
] ]
roles = [
'stock.change',
'stock.add',
'stock_location.change',
'stock_location.add',
]
def setUp(self): def setUp(self):
# Create a user for auth
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()
# 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
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)

View File

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

View File

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

View File

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