\ No newline at end of file
diff --git a/InvenTree/templates/stock_table.html b/InvenTree/templates/stock_table.html
index c41d2181c9..be26b1a976 100644
--- a/InvenTree/templates/stock_table.html
+++ b/InvenTree/templates/stock_table.html
@@ -6,19 +6,27 @@
{% trans "Export" %}
{% if read_only %}
{% else %}
+ {% if perms.stock.add_stockitem %}
{% trans "New Stock Item" %}
+ {% endif %}
+ {% if perms.stock.change_stockitem or perms.stock.delete_stockitem %}
- {% if perms.part.view_part %}
+ {% if roles.part.view %}
{% include "InvenTree/latest_parts.html" with collapse_id="latest_parts" %}
{% include "InvenTree/bom_invalid.html" with collapse_id="bom_invalid" %}
{% include "InvenTree/starred_parts.html" with collapse_id="starred" %}
{% endif %}
- {% if perms.build.view_build %}
+ {% if roles.build.view %}
{% include "InvenTree/build_pending.html" with collapse_id="build_pending" %}
{% endif %}
- {% if perms.stock.view_stockitem %}
+ {% if roles.stock.view %}
{% include "InvenTree/low_stock.html" with collapse_id="order" %}
{% include "InvenTree/required_stock_build.html" with collapse_id="stock_to_build" %}
{% endif %}
- {% if perms.order.view_purchaseorder %}
+ {% if roles.purchase_order.view %}
{% include "InvenTree/po_outstanding.html" with collapse_id="po_outstanding" %}
{% endif %}
- {% if perms.order.view_salesorder %}
+ {% if roles.sales_order.view %}
{% include "InvenTree/so_outstanding.html" with collapse_id="so_outstanding" %}
{% endif %}
From fa21d66c411aadf71031ba415b02ea12e64fd3cf Mon Sep 17 00:00:00 2001
From: Oliver Walters
Date: Tue, 6 Oct 2020 09:54:37 +1100
Subject: [PATCH 13/26] Fix logic for global context object 'roles'
- User may be a part of multiple groups
- Roles are additive across groups
---
InvenTree/InvenTree/context.py | 25 ++++++++++++++++---------
1 file changed, 16 insertions(+), 9 deletions(-)
diff --git a/InvenTree/InvenTree/context.py b/InvenTree/InvenTree/context.py
index 71aee5c2c5..f9d856f566 100644
--- a/InvenTree/InvenTree/context.py
+++ b/InvenTree/InvenTree/context.py
@@ -35,18 +35,25 @@ def user_roles(request):
user = request.user
- roles = {}
+ roles = {
+ }
for group in user.groups.all():
for rule in group.rule_sets.all():
- roles[rule.name] = {
- 'view': rule.can_view or user.is_superuser,
- 'add': rule.can_add or user.is_superuser,
- 'change': rule.can_change or user.is_superuser,
- 'delete': rule.can_delete or user.is_superuser,
- }
- print("Roles:")
- print(roles)
+ # Ensure the role name is in the dict
+ if rule.name not in roles:
+ roles[rule.name] = {
+ 'view': user.is_superuser,
+ 'add': user.is_superuser,
+ 'change': user.is_superuser,
+ 'delete': user.is_superuser
+ }
+
+ # Roles are additive across groups
+ roles[rule.name]['view'] |= rule.can_view
+ roles[rule.name]['add'] |= rule.can_add
+ roles[rule.name]['change'] |= rule.can_change
+ roles[rule.name]['delete'] |= rule.can_delete
return {'roles': roles}
From d2e2e7511f70e90c2e6f3787bc66bc5ea65a9df3 Mon Sep 17 00:00:00 2001
From: Oliver Walters
Date: Tue, 6 Oct 2020 10:07:39 +1100
Subject: [PATCH 14/26] Update templates: Change perms to roles
---
InvenTree/part/templates/part/bom.html | 2 +-
InvenTree/part/templates/part/build.html | 2 +-
InvenTree/part/templates/part/category.html | 14 +++++++-------
InvenTree/part/templates/part/notes.html | 2 +-
InvenTree/part/templates/part/params.html | 8 ++++----
InvenTree/part/templates/part/part_base.html | 16 ++++++++--------
InvenTree/part/templates/part/tabs.html | 2 +-
InvenTree/templates/navbar.html | 10 +++++-----
InvenTree/templates/stock_table.html | 8 ++++----
9 files changed, 32 insertions(+), 32 deletions(-)
diff --git a/InvenTree/part/templates/part/bom.html b/InvenTree/part/templates/part/bom.html
index 610e60847a..c996edc2db 100644
--- a/InvenTree/part/templates/part/bom.html
+++ b/InvenTree/part/templates/part/bom.html
@@ -39,7 +39,7 @@
{% elif part.active %}
- {% if perms.part.change_part %}
+ {% if roles.part.change %}
{% if part.is_bom_valid == False %}
diff --git a/InvenTree/part/templates/part/build.html b/InvenTree/part/templates/part/build.html
index ad51d33ab2..bfd72a2f70 100644
--- a/InvenTree/part/templates/part/build.html
+++ b/InvenTree/part/templates/part/build.html
@@ -10,7 +10,7 @@
{% if part.active %}
- {% if perms.build.add_build %}
+ {% if roles.build.add %}
{% trans "Start New Build" %}
{% endif %}
{% endif %}
diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html
index 25417a1d9b..d73aba0291 100644
--- a/InvenTree/part/templates/part/category.html
+++ b/InvenTree/part/templates/part/category.html
@@ -9,7 +9,7 @@
{% if category %}
{{ category.name }}
- {% if user.is_staff and perms.part.change_partcategory %}
+ {% if user.is_staff and roles.part.change %}
{% endif %}
@@ -20,18 +20,18 @@
{% endif %}
- {% if perms.part.add_partcategory %}
+ {% if roles.part.add %}
{% endif %}
{% if category %}
- {% if perms.part.change_partcategory %}
+ {% if roles.part.change %}
{% endif %}
- {% if perms.part.delete_partcategory %}
+ {% if roles.part.delete %}
@@ -110,13 +110,13 @@
{% trans "Export" %}
- {% if perms.part.add_part %}
+ {% if roles.part.add %}
{% trans "New Part" %}
{% endif %}
{% trans "Options" %}
- {% if perms.part.change_part %}
+ {% if roles.part.change %}
From dc2c9aa662e9a4af344ed60da0db84045da1e3a0 Mon Sep 17 00:00:00 2001
From: Oliver Walters
Date: Tue, 6 Oct 2020 11:29:38 +1100
Subject: [PATCH 15/26] Add InvenTreeRoleMixin
- Simplifies permission requirements for views
- e.g. 'part.view' rather than 'part.view_partcategory'
---
InvenTree/InvenTree/views.py | 54 ++++++++++++++++++++++++++++++++++++
InvenTree/part/api.py | 4 +++
InvenTree/part/views.py | 25 +++++++++--------
InvenTree/stock/api.py | 4 +++
InvenTree/users/models.py | 32 +++++++++++++++++++++
5 files changed, 108 insertions(+), 11 deletions(-)
diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py
index d940229ebe..9cd4aeb514 100644
--- a/InvenTree/InvenTree/views.py
+++ b/InvenTree/InvenTree/views.py
@@ -22,6 +22,7 @@ from django.views.generic.base import TemplateView
from part.models import Part, PartCategory
from stock.models import StockLocation, StockItem
from common.models import InvenTreeSetting, ColorTheme
+from users.models import check_user_role, RuleSet
from .forms import DeleteForm, EditUserForm, SetPasswordForm, ColorThemeSelectForm
from .helpers import str2bool
@@ -682,3 +683,56 @@ class DatabaseStatsView(AjaxView):
"""
return ctx
+
+
+class InvenTreeRoleMixin(PermissionRequiredMixin):
+ """
+ Permission class based on user roles, not user 'permissions'.
+
+ To specify which role is required for the mixin,
+ set the class attribute 'role_required' to something like the following:
+
+ role_required = 'part.add'
+ role_required = [
+ 'part.change',
+ 'build.add',
+ ]
+ """
+
+ # By default, no roles are required
+ # Roles must be specified
+ role_required = None
+
+ def has_permission(self):
+ """
+ Determine if the current user
+ """
+
+ roles_required = []
+
+ if type(self.role_required) is str:
+ roles_required.append(self.role_required)
+ elif type(self.role_required) in [list, tuple]:
+ roles_required = self.role_required
+
+ # List of permissions that will be required
+ permissions = []
+
+ user = self.request.user
+
+ # Superuser can have any permissions they desire
+ if user.is_superuser:
+ return True
+
+ print(type(self), "Required roles:", roles_required)
+
+ for required in roles_required:
+
+ (role, permission) = required.split('.')
+
+ # Return False if the user does not have *any* of the required roles
+ if not check_user_role(user, role, permission):
+ return False
+
+ # We did not fail any required checks
+ return True
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index d4aeec5bd9..d643b8671a 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -44,6 +44,10 @@ 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.
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py
index 8e0980c35e..e352cddb08 100644
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -12,7 +12,6 @@ from django.shortcuts import HttpResponseRedirect
from django.utils.translation import gettext_lazy as _
from django.urls import reverse, reverse_lazy
from django.views.generic import DetailView, ListView, FormView, UpdateView
-from django.contrib.auth.mixins import PermissionRequiredMixin
from django.forms.models import model_to_dict
from django.forms import HiddenInput, CheckboxInput
from django.conf import settings
@@ -39,17 +38,21 @@ from .admin import PartResource
from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
from InvenTree.views import QRCodeView
+from InvenTree.views import InvenTreeRoleMixin
from InvenTree.helpers import DownloadFile, str2bool
-class PartIndex(PermissionRequiredMixin, ListView):
+class PartIndex(InvenTreeRoleMixin, ListView):
""" View for displaying list of Part objects
"""
+
model = Part
template_name = 'part/category.html'
context_object_name = 'parts'
- permission_required = ('part.view_part', 'part.view_partcategory')
+
+ role_required = 'part.view'
+
def get_queryset(self):
return Part.objects.all().select_related('category')
@@ -658,7 +661,7 @@ class PartNotes(UpdateView):
return ctx
-class PartDetail(PermissionRequiredMixin, DetailView):
+class PartDetail(InvenTreeRoleMixin, DetailView):
""" Detail view for Part object
"""
@@ -666,7 +669,7 @@ class PartDetail(PermissionRequiredMixin, DetailView):
queryset = Part.objects.all().select_related('category')
template_name = 'part/detail.html'
- permission_required = 'part.view_part'
+ role_required = 'part.view'
# Add in some extra context information based on query params
def get_context_data(self, **kwargs):
@@ -869,7 +872,7 @@ class BomValidate(AjaxUpdateView):
return self.renderJsonResponse(request, form, data, context=self.get_context())
-class BomUpload(PermissionRequiredMixin, FormView):
+class BomUpload(InvenTreeRoleMixin, FormView):
""" View for uploading a BOM file, and handling BOM data importing.
The BOM upload process is as follows:
@@ -905,7 +908,7 @@ class BomUpload(PermissionRequiredMixin, FormView):
missing_columns = []
allowed_parts = []
- permission_required = ('part.change_part', 'part.add_bomitem')
+ role_required = ('part.change', 'part.add')
def get_success_url(self):
part = self.get_object()
@@ -1931,7 +1934,7 @@ class PartParameterDelete(AjaxDeleteView):
ajax_form_title = _('Delete Part Parameter')
-class CategoryDetail(PermissionRequiredMixin, DetailView):
+class CategoryDetail(InvenTreeRoleMixin, DetailView):
""" Detail view for PartCategory """
model = PartCategory
@@ -1939,7 +1942,7 @@ class CategoryDetail(PermissionRequiredMixin, DetailView):
queryset = PartCategory.objects.all().prefetch_related('children')
template_name = 'part/category_partlist.html'
- permission_required = 'part.view_partcategory'
+ role_required = 'part.view'
def get_context_data(self, **kwargs):
@@ -2081,13 +2084,13 @@ class CategoryCreate(AjaxCreateView):
return initials
-class BomItemDetail(PermissionRequiredMixin, DetailView):
+class BomItemDetail(InvenTreeRoleMixin, DetailView):
""" Detail view for BomItem """
context_object_name = 'item'
queryset = BomItem.objects.all()
template_name = 'part/bom-detail.html'
- permission_required = 'part.view_bomitem'
+ role_required = 'part.view'
class BomItemCreate(AjaxCreateView):
diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py
index 790de7d879..ba802b75d9 100644
--- a/InvenTree/stock/api.py
+++ b/InvenTree/stock/api.py
@@ -52,6 +52,10 @@ class StockCategoryTree(TreeSerializer):
def get_items(self):
return StockLocation.objects.all().prefetch_related('stock_items', 'children')
+ permission_classes = [
+ permissions.IsAuthenticated,
+ ]
+
class StockDetail(generics.RetrieveUpdateDestroyAPIView):
""" API detail endpoint for Stock object
diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py
index 3bd976f0a4..24b0318695 100644
--- a/InvenTree/users/models.py
+++ b/InvenTree/users/models.py
@@ -329,3 +329,35 @@ def create_missing_rule_sets(sender, instance, **kwargs):
"""
update_group_roles(instance)
+
+
+def check_user_role(user, role, permission):
+ """
+ Check if a user has a particular role:permission combination.
+
+ If the user is a superuser, this will return True
+ """
+
+ if user.is_superuser:
+ return True
+
+ for group in user.groups.all():
+
+ for rule in group.rule_sets.all():
+
+ if rule.name == role:
+
+ if permission == 'add' and rule.can_add:
+ return True
+
+ if permission == 'change' and rule.can_change:
+ return True
+
+ if permission == 'view' and rule.can_view:
+ return True
+
+ if permission == 'delete' and rule.can_delete:
+ return True
+
+ # No matching permissions found
+ return False
From c740cce5e435523e800e15dcb73433c1673b2f46 Mon Sep 17 00:00:00 2001
From: Oliver Walters
Date: Tue, 6 Oct 2020 11:31:04 +1100
Subject: [PATCH 16/26] PEP fixes
---
InvenTree/InvenTree/views.py | 9 +++------
InvenTree/part/views.py | 1 -
2 files changed, 3 insertions(+), 7 deletions(-)
diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py
index 9cd4aeb514..ca8f6c646c 100644
--- a/InvenTree/InvenTree/views.py
+++ b/InvenTree/InvenTree/views.py
@@ -22,7 +22,7 @@ from django.views.generic.base import TemplateView
from part.models import Part, PartCategory
from stock.models import StockLocation, StockItem
from common.models import InvenTreeSetting, ColorTheme
-from users.models import check_user_role, RuleSet
+from users.models import check_user_role
from .forms import DeleteForm, EditUserForm, SetPasswordForm, ColorThemeSelectForm
from .helpers import str2bool
@@ -700,12 +700,12 @@ class InvenTreeRoleMixin(PermissionRequiredMixin):
"""
# By default, no roles are required
- # Roles must be specified
+ # Roles must be specified
role_required = None
def has_permission(self):
"""
- Determine if the current user
+ Determine if the current user
"""
roles_required = []
@@ -715,9 +715,6 @@ class InvenTreeRoleMixin(PermissionRequiredMixin):
elif type(self.role_required) in [list, tuple]:
roles_required = self.role_required
- # List of permissions that will be required
- permissions = []
-
user = self.request.user
# Superuser can have any permissions they desire
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py
index e352cddb08..09390bf8c0 100644
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -53,7 +53,6 @@ class PartIndex(InvenTreeRoleMixin, ListView):
role_required = 'part.view'
-
def get_queryset(self):
return Part.objects.all().select_related('category')
From 11d31960c7c64b78b6ced94fe662d611119596a9 Mon Sep 17 00:00:00 2001
From: Oliver Walters
Date: Tue, 6 Oct 2020 11:42:16 +1100
Subject: [PATCH 17/26] Change modal form permissions to use new "role"
strategy
---
InvenTree/InvenTree/views.py | 119 +++++++++++++++--------------------
InvenTree/part/views.py | 74 +++++++++++-----------
2 files changed, 89 insertions(+), 104 deletions(-)
diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py
index ca8f6c646c..198903db9a 100644
--- a/InvenTree/InvenTree/views.py
+++ b/InvenTree/InvenTree/views.py
@@ -108,31 +108,66 @@ class TreeSerializer(views.APIView):
return JsonResponse(response, safe=False)
-class AjaxMixin(PermissionRequiredMixin):
+class InvenTreeRoleMixin(PermissionRequiredMixin):
+ """
+ Permission class based on user roles, not user 'permissions'.
+
+ To specify which role is required for the mixin,
+ set the class attribute 'role_required' to something like the following:
+
+ role_required = 'part.add'
+ role_required = [
+ 'part.change',
+ 'build.add',
+ ]
+ """
+
+ # By default, no roles are required
+ # Roles must be specified
+ role_required = None
+
+ def has_permission(self):
+ """
+ Determine if the current user
+ """
+
+ roles_required = []
+
+ if type(self.role_required) is str:
+ roles_required.append(self.role_required)
+ elif type(self.role_required) in [list, tuple]:
+ roles_required = self.role_required
+
+ user = self.request.user
+
+ # Superuser can have any permissions they desire
+ if user.is_superuser:
+ return True
+
+ for required in roles_required:
+
+ (role, permission) = required.split('.')
+
+ # Return False if the user does not have *any* of the required roles
+ if not check_user_role(user, role, permission):
+ return False
+
+ # We did not fail any required checks
+ return True
+
+
+class AjaxMixin(InvenTreeRoleMixin):
""" AjaxMixin provides basic functionality for rendering a Django form to JSON.
Handles jsonResponse rendering, and adds extra data for the modal forms to process
on the client side.
Any view which inherits the AjaxMixin will need
- correct permissions set using the 'permission_required' attribute
+ correct permissions set using the 'role_required' attribute
"""
- # By default, allow *any* permissions
- permission_required = '*'
-
- def has_permission(self):
- """
- Override the default behaviour of has_permission from PermissionRequiredMixin.
-
- Basically, if permission_required attribute = '*',
- no permissions are actually required!
- """
-
- if self.permission_required == '*':
- return True
- else:
- return super().has_permission()
+ # By default, allow *any* role
+ role_required = None
# By default, point to the modal_form template
# (this can be overridden by a child class)
@@ -683,53 +718,3 @@ class DatabaseStatsView(AjaxView):
"""
return ctx
-
-
-class InvenTreeRoleMixin(PermissionRequiredMixin):
- """
- Permission class based on user roles, not user 'permissions'.
-
- To specify which role is required for the mixin,
- set the class attribute 'role_required' to something like the following:
-
- role_required = 'part.add'
- role_required = [
- 'part.change',
- 'build.add',
- ]
- """
-
- # By default, no roles are required
- # Roles must be specified
- role_required = None
-
- def has_permission(self):
- """
- Determine if the current user
- """
-
- roles_required = []
-
- if type(self.role_required) is str:
- roles_required.append(self.role_required)
- elif type(self.role_required) in [list, tuple]:
- roles_required = self.role_required
-
- user = self.request.user
-
- # Superuser can have any permissions they desire
- if user.is_superuser:
- return True
-
- print(type(self), "Required roles:", roles_required)
-
- for required in roles_required:
-
- (role, permission) = required.split('.')
-
- # Return False if the user does not have *any* of the required roles
- if not check_user_role(user, role, permission):
- return False
-
- # We did not fail any required checks
- return True
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py
index 09390bf8c0..6498774285 100644
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -80,7 +80,7 @@ class PartAttachmentCreate(AjaxCreateView):
ajax_form_title = _("Add part attachment")
ajax_template_name = "modal_form.html"
- permission_required = 'part.add_partattachment'
+ role_required = 'part.add'
def post_save(self):
""" Record the user that uploaded the attachment """
@@ -130,7 +130,7 @@ class PartAttachmentEdit(AjaxUpdateView):
ajax_template_name = 'modal_form.html'
ajax_form_title = _('Edit attachment')
- permission_required = 'part.change_partattachment'
+ role_required = 'part.change'
def get_data(self):
return {
@@ -153,7 +153,7 @@ class PartAttachmentDelete(AjaxDeleteView):
ajax_template_name = "attachment_delete.html"
context_object_name = "attachment"
- permission_required = 'part.delete_partattachment'
+ role_required = 'part.delete'
def get_data(self):
return {
@@ -168,7 +168,7 @@ class PartTestTemplateCreate(AjaxCreateView):
form_class = part_forms.EditPartTestTemplateForm
ajax_form_title = _("Create Test Template")
- permission_required = 'part.add_parttesttemplate'
+ role_required = 'part.add'
def get_initial(self):
@@ -197,7 +197,7 @@ class PartTestTemplateEdit(AjaxUpdateView):
form_class = part_forms.EditPartTestTemplateForm
ajax_form_title = _("Edit Test Template")
- permission_required = 'part.change_parttesttemplate'
+ role_required = 'part.change'
def get_form(self):
@@ -213,7 +213,7 @@ class PartTestTemplateDelete(AjaxDeleteView):
model = PartTestTemplate
ajax_form_title = _("Delete Test Template")
- permission_required = 'part.delete_parttesttemplate'
+ role_required = 'part.delete'
class PartSetCategory(AjaxUpdateView):
@@ -223,7 +223,7 @@ class PartSetCategory(AjaxUpdateView):
ajax_form_title = _('Set Part Category')
form_class = part_forms.SetPartCategoryForm
- permission_required = 'part.change_part'
+ role_required = 'part.change'
category = None
parts = []
@@ -308,7 +308,7 @@ class MakePartVariant(AjaxCreateView):
ajax_form_title = _('Create Variant')
ajax_template_name = 'part/variant_part.html'
- permission_required = 'part.add_part'
+ role_required = 'part.add'
def get_part_template(self):
return get_object_or_404(Part, id=self.kwargs['pk'])
@@ -388,7 +388,7 @@ class PartDuplicate(AjaxCreateView):
ajax_form_title = _("Duplicate Part")
ajax_template_name = "part/copy_part.html"
- permission_required = 'part.add_part'
+ role_required = 'part.add'
def get_data(self):
return {
@@ -513,7 +513,7 @@ class PartCreate(AjaxCreateView):
ajax_form_title = _('Create new part')
ajax_template_name = 'part/create_part.html'
- permission_required = 'part.add_part'
+ role_required = 'part.add'
def get_data(self):
return {
@@ -637,7 +637,7 @@ class PartNotes(UpdateView):
template_name = 'part/notes.html'
model = Part
- permission_required = 'part.change_part'
+ role_required = 'part.change'
fields = ['notes']
@@ -734,7 +734,7 @@ class PartQRCode(QRCodeView):
ajax_form_title = _("Part QR Code")
- permission_required = 'part.view_part'
+ role_required = 'part.view'
def get_qr_data(self):
""" Generate QR code data for the Part """
@@ -755,7 +755,7 @@ class PartImageUpload(AjaxUpdateView):
form_class = part_forms.PartImageForm
- permission_required = 'part.change_part'
+ role_required = 'part.change'
def get_data(self):
return {
@@ -770,7 +770,7 @@ class PartImageSelect(AjaxUpdateView):
ajax_template_name = 'part/select_image.html'
ajax_form_title = _('Select Part Image')
- permission_required = 'part.change_part'
+ role_required = 'part.change'
fields = [
'image',
@@ -813,7 +813,7 @@ class PartEdit(AjaxUpdateView):
ajax_form_title = _('Edit Part Properties')
context_object_name = 'part'
- permission_required = 'part.change_part'
+ role_required = 'part.change'
def get_form(self):
""" Create form for Part editing.
@@ -839,7 +839,7 @@ class BomValidate(AjaxUpdateView):
context_object_name = 'part'
form_class = part_forms.BomValidateForm
- permission_required = ('part.change_part')
+ role_required = 'part.change'
def get_context(self):
return {
@@ -1507,7 +1507,7 @@ class BomUpload(InvenTreeRoleMixin, FormView):
class PartExport(AjaxView):
""" Export a CSV file containing information on multiple parts """
- permission_required = 'part.view_part'
+ role_required = 'part.view'
def get_parts(self, request):
""" Extract part list from the POST parameters.
@@ -1586,7 +1586,7 @@ class BomDownload(AjaxView):
- File format should be passed as a query param e.g. ?format=csv
"""
- permission_required = ('part.view_part', 'part.view_bomitem')
+ role_required = 'part.view'
model = Part
@@ -1641,7 +1641,7 @@ class BomExport(AjaxView):
form_class = part_forms.BomExportForm
ajax_form_title = _("Export Bill of Materials")
- permission_required = ('part.view_part', 'part.view_bomitem')
+ role_required = 'part.view'
def get(self, request, *args, **kwargs):
return self.renderJsonResponse(request, self.form_class())
@@ -1692,7 +1692,7 @@ class PartDelete(AjaxDeleteView):
ajax_form_title = _('Confirm Part Deletion')
context_object_name = 'part'
- permission_required = 'part.delete_part'
+ role_required = 'part.delete'
success_url = '/part/'
@@ -1710,8 +1710,8 @@ class PartPricing(AjaxView):
ajax_form_title = _("Part Pricing")
form_class = part_forms.PartPriceForm
- permission_required = ('company.view_supplierpricebreak', 'part.view_part')
-
+ role_required = ['sales_order.view', 'part.view']
+
def get_part(self):
try:
return Part.objects.get(id=self.kwargs['pk'])
@@ -1829,7 +1829,7 @@ class PartPricing(AjaxView):
class PartParameterTemplateCreate(AjaxCreateView):
""" View for creating a new PartParameterTemplate """
- permission_required = 'part.add_partparametertemplate'
+ role_required = 'part.add'
model = PartParameterTemplate
form_class = part_forms.EditPartParameterTemplateForm
@@ -1839,7 +1839,7 @@ class PartParameterTemplateCreate(AjaxCreateView):
class PartParameterTemplateEdit(AjaxUpdateView):
""" View for editing a PartParameterTemplate """
- permission_required = 'part.change_partparametertemplate'
+ role_required = 'part.change'
model = PartParameterTemplate
form_class = part_forms.EditPartParameterTemplateForm
@@ -1849,7 +1849,7 @@ class PartParameterTemplateEdit(AjaxUpdateView):
class PartParameterTemplateDelete(AjaxDeleteView):
""" View for deleting an existing PartParameterTemplate """
- permission_required = 'part.delete_partparametertemplate'
+ role_required = 'part.delete'
model = PartParameterTemplate
ajax_form_title = _("Delete Part Parameter Template")
@@ -1858,7 +1858,7 @@ class PartParameterTemplateDelete(AjaxDeleteView):
class PartParameterCreate(AjaxCreateView):
""" View for creating a new PartParameter """
- permission_required = 'part.add_partparameter'
+ role_required = 'part.add'
model = PartParameter
form_class = part_forms.EditPartParameterForm
@@ -1910,7 +1910,7 @@ class PartParameterCreate(AjaxCreateView):
class PartParameterEdit(AjaxUpdateView):
""" View for editing a PartParameter """
- permission_required = 'part.change_partparameter'
+ role_required = 'part.change'
model = PartParameter
form_class = part_forms.EditPartParameterForm
@@ -1926,7 +1926,7 @@ class PartParameterEdit(AjaxUpdateView):
class PartParameterDelete(AjaxDeleteView):
""" View for deleting a PartParameter """
- permission_required = 'part.delete_partparameter'
+ role_required = 'part.delete'
model = PartParameter
ajax_template_name = 'part/param_delete.html'
@@ -1991,7 +1991,7 @@ class CategoryEdit(AjaxUpdateView):
ajax_template_name = 'modal_form.html'
ajax_form_title = _('Edit Part Category')
- permission_required = 'part.change_partcategory'
+ role_required = 'part.change'
def get_context_data(self, **kwargs):
context = super(CategoryEdit, self).get_context_data(**kwargs).copy()
@@ -2030,7 +2030,7 @@ class CategoryDelete(AjaxDeleteView):
context_object_name = 'category'
success_url = '/part/'
- permission_required = 'part.delete_partcategory'
+ role_required = 'part.delete'
def get_data(self):
return {
@@ -2046,7 +2046,7 @@ class CategoryCreate(AjaxCreateView):
ajax_template_name = 'modal_form.html'
form_class = part_forms.EditCategoryForm
- permission_required = 'part.add_partcategory'
+ role_required = 'part.add'
def get_context_data(self, **kwargs):
""" Add extra context data to template.
@@ -2099,7 +2099,7 @@ class BomItemCreate(AjaxCreateView):
ajax_template_name = 'modal_form.html'
ajax_form_title = _('Create BOM item')
- permission_required = 'part.add_bomitem'
+ role_required = 'part.add'
def get_form(self):
""" Override get_form() method to reduce Part selection options.
@@ -2167,7 +2167,7 @@ class BomItemEdit(AjaxUpdateView):
ajax_template_name = 'modal_form.html'
ajax_form_title = _('Edit BOM item')
- permission_required = 'part.change_bomitem'
+ role_required = 'part.change'
def get_form(self):
""" Override get_form() method to filter part selection options
@@ -2217,7 +2217,7 @@ class BomItemDelete(AjaxDeleteView):
context_object_name = 'item'
ajax_form_title = _('Confim BOM item deletion')
- permission_required = 'part.delete_bomitem'
+ role_required = 'part.delete'
class PartSalePriceBreakCreate(AjaxCreateView):
@@ -2227,7 +2227,7 @@ class PartSalePriceBreakCreate(AjaxCreateView):
form_class = part_forms.EditPartSalePriceBreakForm
ajax_form_title = _('Add Price Break')
- permission_required = 'part.add_partsellpricebreak'
+ role_required = 'part.add'
def get_data(self):
return {
@@ -2278,7 +2278,7 @@ class PartSalePriceBreakEdit(AjaxUpdateView):
form_class = part_forms.EditPartSalePriceBreakForm
ajax_form_title = _('Edit Price Break')
- permission_required = 'part.change_partsellpricebreak'
+ role_required = 'part.change'
def get_form(self):
@@ -2295,4 +2295,4 @@ class PartSalePriceBreakDelete(AjaxDeleteView):
ajax_form_title = _("Delete Price Break")
ajax_template_name = "modal_delete_form.html"
- permission_required = 'part.delete_partsalepricebreak'
+ role_required = 'part.delete'
From d691b15f4b0a42f2356c6c1563c96a862e7c8a75 Mon Sep 17 00:00:00 2001
From: Oliver Walters
Date: Tue, 6 Oct 2020 12:34:30 +1100
Subject: [PATCH 18/26] Fix conflicts
---
InvenTree/users/models.py | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py
index 24b0318695..773dec36f7 100644
--- a/InvenTree/users/models.py
+++ b/InvenTree/users/models.py
@@ -328,7 +328,12 @@ def create_missing_rule_sets(sender, instance, **kwargs):
group permissions.
"""
- update_group_roles(instance)
+ created = kwargs.get('created', False)
+ # To trigger the group permissions update: update_fields should not be None
+ update_fields = kwargs.get('update_fields', None)
+
+ if created or update_fields:
+ update_group_roles(instance)
def check_user_role(user, role, permission):
From b3e4efd96e79cfe1562d249bbd17502c08e12deb Mon Sep 17 00:00:00 2001
From: Oliver Walters
Date: Tue, 6 Oct 2020 16:03:19 +1100
Subject: [PATCH 19/26] Unit testing fixes
---
InvenTree/part/test_api.py | 35 ++++++++++++++---------------------
InvenTree/part/test_views.py | 31 +++++++++++--------------------
InvenTree/users/models.py | 11 +++++------
3 files changed, 30 insertions(+), 47 deletions(-)
diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py
index 9fdc2688cb..328ad3ef9e 100644
--- a/InvenTree/part/test_api.py
+++ b/InvenTree/part/test_api.py
@@ -3,13 +3,13 @@ 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.status_codes import StockStatus
-from InvenTree.helpers import addUserPermissions
class PartAPITest(APITestCase):
@@ -36,27 +36,20 @@ class PartAPITest(APITestCase):
password='password'
)
- # Add the permissions required to access the API endpoints
- perms = [
- 'view_part',
- 'add_part',
- 'change_part',
- 'delete_part',
- 'view_partcategory',
- 'add_partcategory',
- 'change_partcategory',
- 'view_bomitem',
- 'add_bomitem',
- 'change_bomitem',
- 'view_partattachment',
- 'change_partattachment',
- 'add_partattachment',
- 'view_parttesttemplate',
- 'add_parttesttemplate',
- 'change_parttesttemplate',
- ]
+ # Put the user into a group with the correct permissions
+ group = Group.objects.create(name='mygroup')
+ self.user.groups.add(group)
- addUserPermissions(self.user, perms)
+ # 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')
diff --git a/InvenTree/part/test_views.py b/InvenTree/part/test_views.py
index b1ae991a0c..d8c345d243 100644
--- a/InvenTree/part/test_views.py
+++ b/InvenTree/part/test_views.py
@@ -3,8 +3,7 @@
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth import get_user_model
-
-from InvenTree.helpers import addUserPermissions
+from django.contrib.auth.models import Group
from .models import Part
@@ -31,26 +30,18 @@ class PartViewTestCase(TestCase):
password='password'
)
- # Add the permissions required to access the pages
- perms = [
- 'view_part',
- 'add_part',
- 'change_part',
- 'delete_part',
- 'view_partcategory',
- 'add_partcategory',
- 'change_partcategory',
- 'view_bomitem',
- 'add_bomitem',
- 'change_bomitem',
- 'view_partattachment',
- 'change_partattachment',
- 'add_partattachment',
- ]
+ # Put the user into a group with the correct permissions
+ group = Group.objects.create(name='mygroup')
+ self.user.groups.add(group)
- addUserPermissions(self.user, perms)
+ # 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
- self.user.save()
+ rule.save()
self.client.login(username='username', password='password')
diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py
index 773dec36f7..050bbe2ef9 100644
--- a/InvenTree/users/models.py
+++ b/InvenTree/users/models.py
@@ -171,6 +171,10 @@ class RuleSet(models.Model):
super().save(*args, **kwargs)
+ if self.group:
+ # Update the group too!
+ self.group.save()
+
def get_models(self):
"""
Return the database tables / models that this ruleset covers.
@@ -328,12 +332,7 @@ def create_missing_rule_sets(sender, instance, **kwargs):
group permissions.
"""
- created = kwargs.get('created', False)
- # To trigger the group permissions update: update_fields should not be None
- update_fields = kwargs.get('update_fields', None)
-
- if created or update_fields:
- update_group_roles(instance)
+ update_group_roles(instance)
def check_user_role(user, role, permission):
From 88f73443ee6922980356adc15e28aa6df55875b4 Mon Sep 17 00:00:00 2001
From: Oliver Walters
Date: Tue, 6 Oct 2020 16:43:39 +1100
Subject: [PATCH 20/26] Yet more style fixes
---
InvenTree/users/models.py | 3 ---
1 file changed, 3 deletions(-)
diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py
index 6513e979aa..fcf137bb01 100644
--- a/InvenTree/users/models.py
+++ b/InvenTree/users/models.py
@@ -338,9 +338,6 @@ def create_missing_rule_sets(sender, instance, **kwargs):
then we can now use these RuleSet values to update the
group permissions.
"""
- created = kwargs.get('created', False)
- # To trigger the group permissions update: update_fields should not be None
- update_fields = kwargs.get('update_fields', None)
update_group_roles(instance)
From ab454e5ba4fdac8567ef2f7656fb60120f20a205 Mon Sep 17 00:00:00 2001
From: Oliver Walters
Date: Tue, 6 Oct 2020 16:46:13 +1100
Subject: [PATCH 21/26] More template changes: perms -> roles
---
InvenTree/build/templates/build/build_base.html | 2 +-
InvenTree/company/templates/company/company_base.html | 2 +-
InvenTree/order/templates/order/order_base.html | 2 +-
InvenTree/order/templates/order/sales_order_base.html | 2 +-
InvenTree/part/templates/part/part_base.html | 4 ++--
InvenTree/part/templates/part/tabs.html | 4 ++--
InvenTree/stock/templates/stock/item_base.html | 2 +-
InvenTree/stock/templates/stock/location.html | 2 +-
InvenTree/templates/slide.html | 2 +-
9 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html
index 915433b055..ed3da576d5 100644
--- a/InvenTree/build/templates/build/build_base.html
+++ b/InvenTree/build/templates/build/build_base.html
@@ -35,7 +35,7 @@ src="{% static 'img/blank_image.png' %}"
{{ build.quantity }} x {{ build.part.full_name }}
- {% if user.is_staff and perms.build.change_build %}
+ {% if user.is_staff and roles.build.change %}
{% endif %}
{% endif %}
- {% if part.purchaseable and perms.order.view_purchaseorder %}
+ {% if part.purchaseable and roles.purchase_order.view %}
{% if part.is_template == False %}
- {% if order.status == PurchaseOrderStatus.PENDING %}
+ {% if order.status == PurchaseOrderStatus.PENDING and roles.purchase_order.change %}
{% trans "Add Line Item" %}
{% endif %}
@@ -209,12 +209,12 @@ $("#po-table").inventreeTable({
var pk = row.pk;
- {% if order.status == PurchaseOrderStatus.PENDING %}
+ {% if order.status == PurchaseOrderStatus.PENDING and roles.purchase_order.delete %}
html += makeIconButton('fa-edit icon-blue', 'button-line-edit', pk, '{% trans "Edit line item" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-line-delete', pk, '{% trans "Delete line item" %}');
{% endif %}
- {% if order.status == PurchaseOrderStatus.PLACED %}
+ {% if order.status == PurchaseOrderStatus.PLACED and roles.purchase_order.change %}
if (row.received < row.quantity) {
html += makeIconButton('fa-clipboard-check', 'button-line-receive', pk, '{% trans "Receive line item" %}');
}
diff --git a/InvenTree/order/templates/order/purchase_orders.html b/InvenTree/order/templates/order/purchase_orders.html
index 1019092151..d02af36ff5 100644
--- a/InvenTree/order/templates/order/purchase_orders.html
+++ b/InvenTree/order/templates/order/purchase_orders.html
@@ -14,7 +14,9 @@ InvenTree | {% trans "Purchase Orders" %}
+ {% if roles.purchase_order.add %}
{% trans "New Purchase Order" %}
+ {% endif %}