diff --git a/InvenTree/users/admin.py b/InvenTree/users/admin.py index 00e3ec7040..741f898c81 100644 --- a/InvenTree/users/admin.py +++ b/InvenTree/users/admin.py @@ -11,6 +11,20 @@ from django.contrib.auth.models import Group User = get_user_model() +from users.models import RuleSet + + +class RuleSetInline(admin.TabularInline): + model = RuleSet + can_delete = False + verbose_name = 'Ruleset' + verbose_plural_name = 'Rulesets' + fields = ['name'] + [option for option in RuleSet.RULE_OPTIONS] + readonly_fields = ['name'] + max_num = len(RuleSet.RULESET_CHOICES) + min_num = 1 + extra = 0 + class InvenTreeGroupAdminForm(forms.ModelForm): @@ -18,6 +32,7 @@ class InvenTreeGroupAdminForm(forms.ModelForm): model = Group exclude = [] fields = [ + 'name', 'users', 'permissions', ] @@ -35,6 +50,7 @@ class InvenTreeGroupAdminForm(forms.ModelForm): required=False, widget=FilteredSelectMultiple('users', False), label=_('Users'), + help_text=_('Select which users are assigned to this group') ) def save_m2m(self): @@ -59,8 +75,31 @@ class RoleGroupAdmin(admin.ModelAdmin): form = InvenTreeGroupAdminForm + inlines = [ + RuleSetInline, + ] + + def get_formsets_with_inlines(self, request, obj=None): + for inline in self.get_inline_instances(request, obj): + # Hide RuleSetInline in the 'Add role' view + if not isinstance(inline, RuleSetInline) or obj is not None: + yield inline.get_formset(request, obj), inline + filter_horizontal = ['permissions'] + # Save inlines before model + # https://stackoverflow.com/a/14860703/12794913 + def save_model(self, request, obj, form, change): + if obj is not None: + # Save model immediately only if in 'Add role' view + super().save_model(request, obj, form, change) + else: + pass # don't actually save the parent instance + + def save_formset(self, request, form, formset, change): + formset.save() # this will save the children + form.instance.save() # form.instance is the parent + admin.site.unregister(Group) admin.site.register(Group, RoleGroupAdmin) diff --git a/InvenTree/users/apps.py b/InvenTree/users/apps.py index 251989770b..b352e54baf 100644 --- a/InvenTree/users/apps.py +++ b/InvenTree/users/apps.py @@ -1,8 +1,33 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from django.db.utils import OperationalError, ProgrammingError + from django.apps import AppConfig class UsersConfig(AppConfig): name = 'users' + + def ready(self): + + try: + self.assign_permissions() + except (OperationalError, ProgrammingError): + pass + + def assign_permissions(self): + + from django.contrib.auth.models import Group + from users.models import RuleSet, update_group_roles + + # First, delete any rule_set objects which have become outdated! + for rule in RuleSet.objects.all(): + if rule.name not in RuleSet.RULESET_NAMES: + print("need to delete:", rule.name) + rule.delete() + + # Update group permission assignments for all groups + for group in Group.objects.all(): + + update_group_roles(group) \ No newline at end of file diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 40a96afc6f..fd43e683e0 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -1 +1,160 @@ # -*- coding: utf-8 -*- + +from django.contrib.auth.models import Group +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from django.dispatch import receiver +from django.db.models.signals import post_save + + +class RuleSet(models.Model): + """ + A RuleSet is somewhat like a superset of the django permission class, + in that in encapsulates a bunch of permissions. + + There are *many* apps models used within InvenTree, + so it makes sense to group them into "roles". + + These roles translate (roughly) to the menu options available. + + Each role controls permissions for a number of database tables, + which are then handled using the normal django permissions approach. + """ + + RULESET_CHOICES = [ + ('general', _('General')), + ('admin', _('Admin')), + ('part', _('Parts')), + ('stock', _('Stock')), + ('build', _('Build Orders')), + ('supplier', _('Suppliers')), + ('purchase_order', _('Purchase Orders')), + ('customer', _('Customers')), + ('sales_order', _('Sales Orders')), + ] + + RULESET_NAMES = [ + choice[0] for choice in RULESET_CHOICES + ] + + RULESET_MODELS = { + 'general': [ + 'part.partstar', + ], + 'admin': [ + 'auth.group', + 'auth.user', + 'auth.permission', + 'authtoken.token', + ], + 'part': [ + 'part.part', + 'part.bomitem', + 'part.partcategory', + 'part.partattachment', + 'part.partsellpricebreak', + 'part.parttesttemplate', + 'part.partparametertemplate', + 'part.partparameter', + ], + 'stock': [ + 'stock.stockitem', + 'stock.stocklocation', + 'stock.stockitemattachment', + 'stock.stockitemtracking', + 'stock.stockitemtestresult', + ], + 'build': [ + 'part.part', + 'part.partcategory', + 'part.bomitem', + 'build.build', + 'build.builditem', + 'stock.stockitem', + 'stock.stocklocation', + ] + } + + RULE_OPTIONS = [ + 'can_view', + 'can_add', + 'can_change', + 'can_delete', + ] + + class Meta: + unique_together = ( + ('name', 'group'), + ) + + name = models.CharField( + max_length=50, + choices=RULESET_CHOICES, + blank=False, + help_text=_('Permission set') + ) + + group = models.ForeignKey( + Group, + related_name='rule_sets', + blank=False, null=False, + on_delete=models.CASCADE, + help_text=_('Group'), + ) + + can_view = models.BooleanField(verbose_name=_('View'), default=True, help_text=_('Permission to view items')) + + can_add = models.BooleanField(verbose_name=_('Create'), default=False, help_text=_('Permission to add items')) + + can_change = models.BooleanField(verbose_name=_('Update'), default=False, help_text=_('Permissions to edit items')) + + can_delete = models.BooleanField(verbose_name=_('Delete'), default=False, help_text=_('Permission to delete items')) + + def __str__(self): + return self.name + + def save(self, *args, **kwargs): + + super().save(*args, **kwargs) + + def get_models(self): + + models = { + '' + } + +def update_group_roles(group): + """ + Update group roles: + + a) Ensure default roles are assigned to each group. + b) Ensure group permissions are correctly updated and assigned + """ + + # List of permissions which must be added to the group + permissions_to_add = [] + + # List of permissions which must be removed from the group + permissions_to_delete = [] + + # Get all the rulesets associated with this group + for r in RuleSet.RULESET_CHOICES: + + rulename = r[0] + + try: + ruleset = RuleSet.objects.get(group=group, name=rulename) + except RuleSet.DoesNotExist: + # Create the ruleset with default values (if it does not exist) + ruleset = RuleSet.objects.create(group=group, name=rulename) + + # TODO - Update permissions here + + # TODO - Update group permissions + + +@receiver(post_save, sender=Group) +def create_missing_rule_sets(sender, instance, **kwargs): + + update_group_roles(instance) \ No newline at end of file diff --git a/tasks.py b/tasks.py index 9948b470d1..df386633e9 100644 --- a/tasks.py +++ b/tasks.py @@ -22,7 +22,8 @@ def apps(): 'part', 'report', 'stock', - 'InvenTree' + 'InvenTree', + 'users', ] def localDir():