From bedda66949e2812faa1270048b0ce8fe940cf75d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 3 Oct 2020 17:37:20 +1000 Subject: [PATCH 01/16] Add custom admin view for the "Group" model - Ref: https://github.com/Microdisseny/django-groupadmin-users - Adds ability to edit users within a particular group from the group admin page! --- InvenTree/InvenTree/settings.py | 1 + InvenTree/users/admin.py | 67 ++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index f9e3285781..21b8a0ead1 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -138,6 +138,7 @@ INSTALLED_APPS = [ 'part.apps.PartConfig', 'report.apps.ReportConfig', 'stock.apps.StockConfig', + 'users.apps.UsersConfig', # Third part add-ons 'django_filters', # Extended filter functionality diff --git a/InvenTree/users/admin.py b/InvenTree/users/admin.py index b12fa73f94..00e3ec7040 100644 --- a/InvenTree/users/admin.py +++ b/InvenTree/users/admin.py @@ -1,3 +1,66 @@ # -*- coding: utf-8 -*- -# from __future__ import unicode_literals -# from django.contrib import admin +from __future__ import unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from django.contrib import admin +from django import forms +from django.contrib.auth import get_user_model +from django.contrib.admin.widgets import FilteredSelectMultiple +from django.contrib.auth.models import Group + +User = get_user_model() + + +class InvenTreeGroupAdminForm(forms.ModelForm): + + class Meta: + model = Group + exclude = [] + fields = [ + 'users', + 'permissions', + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if self.instance.pk: + # Populate the users field with the current Group users. + self.fields['users'].initial = self.instance.user_set.all() + + # Add the users field. + users = forms.ModelMultipleChoiceField( + queryset=User.objects.all(), + required=False, + widget=FilteredSelectMultiple('users', False), + label=_('Users'), + ) + + def save_m2m(self): + # Add the users to the Group. + # Deprecated in Django 1.10: Direct assignment to a reverse foreign key + # or many-to-many relation + + self.instance.user_set.set(self.cleaned_data['users']) + + def save(self, *args, **kwargs): + # Default save + instance = super().save() + # Save many-to-many data + self.save_m2m() + return instance + + +class RoleGroupAdmin(admin.ModelAdmin): + """ + Custom admin interface for the Group model + """ + + form = InvenTreeGroupAdminForm + + filter_horizontal = ['permissions'] + + +admin.site.unregister(Group) +admin.site.register(Group, RoleGroupAdmin) From 16f1b4c78437bf58772788527a28f774b52024b7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 3 Oct 2020 23:45:24 +1000 Subject: [PATCH 02/16] Add hook to update group permission roles (doesn't do anything yet) --- InvenTree/users/admin.py | 39 ++++++++++ InvenTree/users/apps.py | 25 ++++++ InvenTree/users/models.py | 159 ++++++++++++++++++++++++++++++++++++++ tasks.py | 3 +- 4 files changed, 225 insertions(+), 1 deletion(-) 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(): From 9e4cc73b1cba090275bdd86962c22aa821087504 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 4 Oct 2020 00:01:18 +1000 Subject: [PATCH 03/16] Add migration files --- InvenTree/users/migrations/0001_initial.py | 31 ++++++++++++++++++++++ InvenTree/users/migrations/__init__.py | 0 2 files changed, 31 insertions(+) create mode 100644 InvenTree/users/migrations/0001_initial.py create mode 100644 InvenTree/users/migrations/__init__.py diff --git a/InvenTree/users/migrations/0001_initial.py b/InvenTree/users/migrations/0001_initial.py new file mode 100644 index 0000000000..04071c5a63 --- /dev/null +++ b/InvenTree/users/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# Generated by Django 3.0.7 on 2020-10-03 13:44 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0011_update_proxy_permissions'), + ] + + operations = [ + migrations.CreateModel( + name='RuleSet', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(choices=[('general', 'General'), ('admin', 'Admin'), ('part', 'Parts'), ('stock', 'Stock'), ('build', 'Build Orders'), ('supplier', 'Suppliers'), ('purchase_order', 'Purchase Orders'), ('customer', 'Customers'), ('sales_order', 'Sales Orders')], help_text='Permission set', max_length=50)), + ('can_view', models.BooleanField(default=True, help_text='Permission to view items', verbose_name='View')), + ('can_add', models.BooleanField(default=False, help_text='Permission to add items', verbose_name='Create')), + ('can_change', models.BooleanField(default=False, help_text='Permissions to edit items', verbose_name='Update')), + ('can_delete', models.BooleanField(default=False, help_text='Permission to delete items', verbose_name='Delete')), + ('group', models.ForeignKey(help_text='Group', on_delete=django.db.models.deletion.CASCADE, related_name='rule_sets', to='auth.Group')), + ], + options={ + 'unique_together': {('name', 'group')}, + }, + ), + ] diff --git a/InvenTree/users/migrations/__init__.py b/InvenTree/users/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 6bc5fe2497c7d17038e17758dcf0cddaa88b2567 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 4 Oct 2020 00:03:10 +1000 Subject: [PATCH 04/16] Tab fix --- InvenTree/users/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/users/admin.py b/InvenTree/users/admin.py index 741f898c81..9a6b324bce 100644 --- a/InvenTree/users/admin.py +++ b/InvenTree/users/admin.py @@ -87,7 +87,7 @@ class RoleGroupAdmin(admin.ModelAdmin): filter_horizontal = ['permissions'] - # Save inlines before model + # Save inlines before model # https://stackoverflow.com/a/14860703/12794913 def save_model(self, request, obj, form, change): if obj is not None: From 2039100d3e7359d31a97ba39274f414ec4ec82f6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 4 Oct 2020 00:24:48 +1000 Subject: [PATCH 05/16] Add some unit testing --- InvenTree/company/apps.py | 3 +- .../migrations/0019_auto_20200413_0642.py | 2 - InvenTree/part/apps.py | 2 +- InvenTree/users/models.py | 26 ++++++++--- InvenTree/users/tests.py | 46 ++++++++++++++++++- 5 files changed, 67 insertions(+), 12 deletions(-) diff --git a/InvenTree/company/apps.py b/InvenTree/company/apps.py index 0afb18f616..5f84ce507f 100644 --- a/InvenTree/company/apps.py +++ b/InvenTree/company/apps.py @@ -38,4 +38,5 @@ class CompanyConfig(AppConfig): company.image = None company.save() except (OperationalError, ProgrammingError): - print("Could not generate Company thumbnails") + # Getting here probably meant the database was in test mode + pass diff --git a/InvenTree/company/migrations/0019_auto_20200413_0642.py b/InvenTree/company/migrations/0019_auto_20200413_0642.py index c81dfd795a..c3c2f58ea0 100644 --- a/InvenTree/company/migrations/0019_auto_20200413_0642.py +++ b/InvenTree/company/migrations/0019_auto_20200413_0642.py @@ -24,7 +24,6 @@ def reverse_association(apps, schema_editor): # Exit if there are no SupplierPart objects # This crucial otherwise the unit test suite fails! if SupplierPart.objects.count() == 0: - print("No SupplierPart objects - skipping") return print("Reversing migration for manufacturer association") @@ -105,7 +104,6 @@ def associate_manufacturers(apps, schema_editor): # Exit if there are no SupplierPart objects # This crucial otherwise the unit test suite fails! if SupplierPart.objects.count() == 0: - print("No SupplierPart objects - skipping") return # Link a 'manufacturer_name' to a 'Company' diff --git a/InvenTree/part/apps.py b/InvenTree/part/apps.py index 2137ec5d89..198e58e337 100644 --- a/InvenTree/part/apps.py +++ b/InvenTree/part/apps.py @@ -37,4 +37,4 @@ class PartConfig(AppConfig): part.image = None part.save() except (OperationalError, ProgrammingError): - print("Could not generate Part thumbnails") + pass diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index fd43e683e0..9395f3e1f2 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -28,9 +28,7 @@ class RuleSet(models.Model): ('part', _('Parts')), ('stock', _('Stock')), ('build', _('Build Orders')), - ('supplier', _('Suppliers')), ('purchase_order', _('Purchase Orders')), - ('customer', _('Customers')), ('sales_order', _('Sales Orders')), ] @@ -73,6 +71,21 @@ class RuleSet(models.Model): 'build.builditem', 'stock.stockitem', 'stock.stocklocation', + ], + 'purchase_order': [ + 'company.company', + 'company.supplierpart', + 'company.supplierpricebreak', + 'order.purchaseorder', + 'order.purchaseorderattachment', + 'order.purchaseorderlineitem', + ], + 'sales_order': [ + 'company.company', + 'order.salesorder', + 'order.salesorderattachment', + 'order.salesorderlineitem', + 'order.salesorderallocation', ] } @@ -119,10 +132,11 @@ class RuleSet(models.Model): super().save(*args, **kwargs) def get_models(self): + """ + Return the database tables / models that this ruleset covers. + """ - models = { - '' - } + return self.RULESET_MODELS.get(self.name, []) def update_group_roles(group): """ @@ -157,4 +171,4 @@ def update_group_roles(group): @receiver(post_save, sender=Group) def create_missing_rule_sets(sender, instance, **kwargs): - update_group_roles(instance) \ No newline at end of file + update_group_roles(instance) diff --git a/InvenTree/users/tests.py b/InvenTree/users/tests.py index 57c7c1fe6b..3687762fba 100644 --- a/InvenTree/users/tests.py +++ b/InvenTree/users/tests.py @@ -1,4 +1,46 @@ # -*- coding: utf-8 -*- -# from __future__ import unicode_literals +from __future__ import unicode_literals + +from django.test import TestCase + +from users.models import RuleSet + + +class RuleSetModelTest(TestCase): + """ + Some simplistic tests to ensure the RuleSet model is setup correctly. + """ + + def test_ruleset_models(self): + + keys = RuleSet.RULESET_MODELS.keys() + + # Check if there are any rulesets which do not have models defined + + missing = [name for name in RuleSet.RULESET_NAMES if name not in keys] + + if len(missing) > 0: + print("The following rulesets do not have models assigned:") + for m in missing: + print("-", m) + + # Check if models have been defined for a ruleset which is incorrect + extra = [name for name in keys if name not in RuleSet.RULESET_NAMES] + + if len(extra) > 0: + print("The following rulesets have been improperly added to RULESET_MODELS:") + for e in extra: + print("-", e) + + # Check that each ruleset has models assigned + empty = [key for key in keys if len(RuleSet.RULESET_MODELS[key]) == 0] + + if len(empty) > 0: + print("The following rulesets have empty entries in RULESET_MODELS:") + for e in empty: + print("-", e) + + self.assertEqual(len(missing), 0) + self.assertEqual(len(extra), 0) + self.assertEqual(len(empty), 0) -# from django.test import TestCase From 6c2eb959a6a63356bfd38aac65db5f68c2ab52b5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 4 Oct 2020 00:34:22 +1000 Subject: [PATCH 06/16] More unit testing --- InvenTree/users/models.py | 72 +++++++++++++++++++-------------------- InvenTree/users/tests.py | 28 +++++++++++++++ 2 files changed, 64 insertions(+), 36 deletions(-) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 9395f3e1f2..403dc94291 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -38,54 +38,54 @@ class RuleSet(models.Model): RULESET_MODELS = { 'general': [ - 'part.partstar', + 'part_partstar', ], 'admin': [ - 'auth.group', - 'auth.user', - 'auth.permission', - 'authtoken.token', + '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', + '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', + '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', + 'part_part', + 'part_partcategory', + 'part_bomitem', + 'build_build', + 'build_builditem', + 'stock_stockitem', + 'stock_stocklocation', ], 'purchase_order': [ - 'company.company', - 'company.supplierpart', - 'company.supplierpricebreak', - 'order.purchaseorder', - 'order.purchaseorderattachment', - 'order.purchaseorderlineitem', + 'company_company', + 'part_supplierpart', + 'part_supplierpricebreak', + 'order_purchaseorder', + 'order_purchaseorderattachment', + 'order_purchaseorderlineitem', ], 'sales_order': [ - 'company.company', - 'order.salesorder', - 'order.salesorderattachment', - 'order.salesorderlineitem', - 'order.salesorderallocation', + 'company_company', + 'order_salesorder', + 'order_salesorderattachment', + 'order_salesorderlineitem', + 'order_salesorderallocation', ] } diff --git a/InvenTree/users/tests.py b/InvenTree/users/tests.py index 3687762fba..6c088441c9 100644 --- a/InvenTree/users/tests.py +++ b/InvenTree/users/tests.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from django.test import TestCase +from django.apps import apps from users.models import RuleSet @@ -44,3 +45,30 @@ class RuleSetModelTest(TestCase): self.assertEqual(len(extra), 0) self.assertEqual(len(empty), 0) + def test_model_names(self): + """ + Test that each model defined in the rulesets is valid, + based on the database schema! + """ + + available_models = apps.get_models() + + available_tables = [] + + for model in available_models: + table_name = model.objects.model._meta.db_table + available_tables.append(table_name) + + errors = 0 + + # Now check that each defined model is a valid table name + for key in RuleSet.RULESET_MODELS.keys(): + + models = RuleSet.RULESET_MODELS[key] + + for m in models: + if m not in available_tables: + print("{n} is not a valid database table".format(n=m)) + errors += 1 + + self.assertEqual(errors, 0) \ No newline at end of file From 1ded9e1fc0627dae362a14d70aae7b76aba31370 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 4 Oct 2020 00:38:53 +1000 Subject: [PATCH 07/16] Add a warning showing which databases tables are not covered by defined rulesets --- InvenTree/users/models.py | 1 + InvenTree/users/tests.py | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 403dc94291..c419f7a25a 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -45,6 +45,7 @@ class RuleSet(models.Model): 'auth_user', 'auth_permission', 'authtoken_token', + 'users_ruleset', ], 'part': [ 'part_part', diff --git a/InvenTree/users/tests.py b/InvenTree/users/tests.py index 6c088441c9..485c02542a 100644 --- a/InvenTree/users/tests.py +++ b/InvenTree/users/tests.py @@ -61,14 +61,30 @@ class RuleSetModelTest(TestCase): errors = 0 + assigned_models = [] + # Now check that each defined model is a valid table name for key in RuleSet.RULESET_MODELS.keys(): models = RuleSet.RULESET_MODELS[key] for m in models: + + assigned_models.append(m) + if m not in available_tables: print("{n} is not a valid database table".format(n=m)) errors += 1 - self.assertEqual(errors, 0) \ No newline at end of file + self.assertEqual(errors, 0) + + missing_models = [] + + for model in available_tables: + if model not in assigned_models: + missing_models.append(model) + + if len(missing_models) > 0: + print("WARNING: The following database models are not covered by the define RuleSet permissions:") + for m in missing_models: + print("-", m) \ No newline at end of file From c09b4980adc85981e06267ab5b70dde164459a80 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 4 Oct 2020 00:43:02 +1000 Subject: [PATCH 08/16] PEP fixes --- InvenTree/users/admin.py | 4 ++-- InvenTree/users/apps.py | 4 ++-- InvenTree/users/models.py | 7 ++++--- InvenTree/users/tests.py | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/InvenTree/users/admin.py b/InvenTree/users/admin.py index 9a6b324bce..a556e60080 100644 --- a/InvenTree/users/admin.py +++ b/InvenTree/users/admin.py @@ -9,10 +9,10 @@ from django.contrib.auth import get_user_model from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.auth.models import Group -User = get_user_model() - from users.models import RuleSet +User = get_user_model() + class RuleSetInline(admin.TabularInline): model = RuleSet diff --git a/InvenTree/users/apps.py b/InvenTree/users/apps.py index b352e54baf..07e303c1be 100644 --- a/InvenTree/users/apps.py +++ b/InvenTree/users/apps.py @@ -14,7 +14,7 @@ class UsersConfig(AppConfig): try: self.assign_permissions() except (OperationalError, ProgrammingError): - pass + pass def assign_permissions(self): @@ -30,4 +30,4 @@ class UsersConfig(AppConfig): # Update group permission assignments for all groups for group in Group.objects.all(): - update_group_roles(group) \ No newline at end of file + update_group_roles(group) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index c419f7a25a..a0ae03d803 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -71,7 +71,7 @@ class RuleSet(models.Model): 'build_build', 'build_builditem', 'stock_stockitem', - 'stock_stocklocation', + 'stock_stocklocation', ], 'purchase_order': [ 'company_company', @@ -139,6 +139,7 @@ class RuleSet(models.Model): return self.RULESET_MODELS.get(self.name, []) + def update_group_roles(group): """ Update group roles: @@ -148,10 +149,10 @@ def update_group_roles(group): """ # List of permissions which must be added to the group - permissions_to_add = [] + # permissions_to_add = [] # List of permissions which must be removed from the group - permissions_to_delete = [] + # permissions_to_delete = [] # Get all the rulesets associated with this group for r in RuleSet.RULESET_CHOICES: diff --git a/InvenTree/users/tests.py b/InvenTree/users/tests.py index 485c02542a..b88867a271 100644 --- a/InvenTree/users/tests.py +++ b/InvenTree/users/tests.py @@ -87,4 +87,4 @@ class RuleSetModelTest(TestCase): if len(missing_models) > 0: print("WARNING: The following database models are not covered by the define RuleSet permissions:") for m in missing_models: - print("-", m) \ No newline at end of file + print("-", m) From d5c0c12d78528b165ee0d0a3d2cc28e02a7ac9e3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 4 Oct 2020 11:03:14 +1100 Subject: [PATCH 09/16] Add some more unit testing - ALL models must be covered by rulesets - Added a RULESET_IGNORE list for models we do not want permissions for --- InvenTree/users/models.py | 88 ++++++++++++++++++++++++++++++++++++++- InvenTree/users/tests.py | 8 ++-- 2 files changed, 91 insertions(+), 5 deletions(-) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index a0ae03d803..8dd634acdf 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -90,6 +90,23 @@ class RuleSet(models.Model): ] } + # Database models we ignore permission sets for + RULESET_IGNORE = [ + # Core django models (not user configurable) + 'django_admin_log', + 'django_content_type', + 'django_session', + + # Models which currently do not require permissions + 'common_inventreesetting', + 'common_currency', + 'common_colortheme', + 'company_contact', + 'label_stockitemlabel', + 'report_testreport', + 'report_reportasset', + ] + RULE_OPTIONS = [ 'can_view', 'can_add', @@ -125,6 +142,21 @@ class RuleSet(models.Model): can_delete = models.BooleanField(verbose_name=_('Delete'), default=False, help_text=_('Permission to delete items')) + @staticmethod + def get_model_permission_string(model, permission): + """ + Construct the correctly formatted permission string, + given the app_model name, and the permission type. + """ + + app, model = model.split('_') + + return "{app}.{perm}_{model}".format( + app=app, + perm=permission, + model=model + ) + def __str__(self): return self.name @@ -148,11 +180,43 @@ def update_group_roles(group): b) Ensure group permissions are correctly updated and assigned """ + # List of permissions already associated with this group + group_permissions = '??????' + # List of permissions which must be added to the group - # permissions_to_add = [] + permissions_to_add = set() # List of permissions which must be removed from the group - # permissions_to_delete = [] + permissions_to_delete = set() + + def add_model(name, action, allowed): + """ + Add a new model to the pile: + + args: + name - The name of the model e.g. part_part + action - The permission action e.g. view + allowed - Whether or not the action is allowed + """ + + if not action in ['view', 'add', 'change', 'delete']: + raise ValueError("Action {a} is invalid".format(a=action)) + + permission_string = RuleSet.get_model_permission_string(model, action) + + if allowed: + + # An 'allowed' action is always preferenced over a 'forbidden' action + if permission_string in permissions_to_delete: + permissions_to_delete.remove(permission_string) + + permissions_to_add.add(permission_string) + + else: + + # A forbidden action will be ignored if we have already allowed it + if permission_string not in permissions_to_add: + permissions_to_delete.add(permission_string) # Get all the rulesets associated with this group for r in RuleSet.RULESET_CHOICES: @@ -165,12 +229,32 @@ def update_group_roles(group): # Create the ruleset with default values (if it does not exist) ruleset = RuleSet.objects.create(group=group, name=rulename) + # Which database tables does this RuleSet touch? + models = ruleset.get_models() + + for model in models: + # Keep track of the available permissions for each model + + add_model(model, 'view', ruleset.can_view) + add_model(model, 'add', ruleset.can_add) + add_model(model, 'change', ruleset.can_change) + add_model(model, 'delete', ruleset.can_delete) + # TODO - Update permissions here # TODO - Update group permissions + print("To add:", permissions_to_add) + print("To delete:", permissions_to_delete) + @receiver(post_save, sender=Group) def create_missing_rule_sets(sender, instance, **kwargs): + """ + Called *after* a Group object is saved. + As the linked RuleSet instances are saved *before* the Group, + then we can now use these RuleSet values to update the + group permissions. + """ update_group_roles(instance) diff --git a/InvenTree/users/tests.py b/InvenTree/users/tests.py index b88867a271..8db631ab98 100644 --- a/InvenTree/users/tests.py +++ b/InvenTree/users/tests.py @@ -76,15 +76,17 @@ class RuleSetModelTest(TestCase): print("{n} is not a valid database table".format(n=m)) errors += 1 - self.assertEqual(errors, 0) missing_models = [] for model in available_tables: - if model not in assigned_models: + if model not in assigned_models and model not in RuleSet.RULESET_IGNORE: missing_models.append(model) if len(missing_models) > 0: - print("WARNING: The following database models are not covered by the define RuleSet permissions:") + print("The following database models are not covered by the defined RuleSet permissions:") for m in missing_models: print("-", m) + + self.assertEqual(errors, 0) + self.assertEqual(len(missing_models), 0) \ No newline at end of file From c19c014f55e3b0db29ea03a7c32bb4f610178fe0 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 4 Oct 2020 12:18:31 +1100 Subject: [PATCH 10/16] Add or remove permissions from groups as defined by the RuleSet links - Only runs when the group is changed - Does not add permissions if they already exist - Does not remove permissions if they do not exist --- InvenTree/users/models.py | 87 ++++++++++++++++++++++++++++++++------- InvenTree/users/tests.py | 3 +- 2 files changed, 73 insertions(+), 17 deletions(-) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 8dd634acdf..1bffbf86a9 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- -from django.contrib.auth.models import Group +from django.contrib.auth.models import Group, Permission +from django.contrib.contenttypes.models import ContentType from django.db import models from django.utils.translation import gettext_lazy as _ @@ -172,16 +173,35 @@ class RuleSet(models.Model): return self.RULESET_MODELS.get(self.name, []) -def update_group_roles(group): +def update_group_roles(group, debug=False): """ - Update group roles: - - a) Ensure default roles are assigned to each group. - b) Ensure group permissions are correctly updated and assigned + + Iterates through all of the RuleSets associated with the group, + and ensures that the correct permissions are either applied or removed from the group. + + This function is called under the following conditions: + + a) Whenever the InvenTree database is launched + b) Whenver the group object is updated + + The RuleSet model has complete control over the permissions applied to any group. + """ # List of permissions already associated with this group - group_permissions = '??????' + group_permissions = set() + + # Iterate through each permission already assigned to this group, + # and create a simplified permission key string + for p in group.permissions.all(): + (permission, app, model) = p.natural_key() + + permission_string = '{app}.{perm}'.format( + app=app, + perm=permission + ) + + group_permissions.add(permission_string) # List of permissions which must be added to the group permissions_to_add = set() @@ -199,7 +219,7 @@ def update_group_roles(group): allowed - Whether or not the action is allowed """ - if not action in ['view', 'add', 'change', 'delete']: + if action not in ['view', 'add', 'change', 'delete']: raise ValueError("Action {a} is invalid".format(a=action)) permission_string = RuleSet.get_model_permission_string(model, action) @@ -210,13 +230,16 @@ def update_group_roles(group): if permission_string in permissions_to_delete: permissions_to_delete.remove(permission_string) - permissions_to_add.add(permission_string) + if permission_string not in group_permissions: + permissions_to_add.add(permission_string) else: # A forbidden action will be ignored if we have already allowed it if permission_string not in permissions_to_add: - permissions_to_delete.add(permission_string) + + if permission_string in group_permissions: + permissions_to_delete.add(permission_string) # Get all the rulesets associated with this group for r in RuleSet.RULESET_CHOICES: @@ -240,12 +263,46 @@ def update_group_roles(group): add_model(model, 'change', ruleset.can_change) add_model(model, 'delete', ruleset.can_delete) - # TODO - Update permissions here + def get_permission_object(permission_string): + """ + Find the permission object in the database, + from the simplified permission string - # TODO - Update group permissions + Args: + permission_string - a simplified permission_string e.g. 'part.view_partcategory' - print("To add:", permissions_to_add) - print("To delete:", permissions_to_delete) + Returns the permission object in the database associated with the permission string + """ + + (app, perm) = permission_string.split('.') + + (permission_name, model) = perm.split('_') + + content_type = ContentType.objects.get(app_label=app, model=model) + + permission = Permission.objects.get(content_type=content_type, codename=perm) + + return permission + + # Add any required permissions to the group + for perm in permissions_to_add: + + permission = get_permission_object(perm) + + group.permissions.add(permission) + + if debug: + print(f"Adding permission {perm} to group {group.name}") + + # Remove any extra permissions from the group + for perm in permissions_to_delete: + + permission = get_permission_object(perm) + + group.permissions.remove(permission) + + if debug: + print(f"Removing permission {perm} from group {group.name}") @receiver(post_save, sender=Group) @@ -253,7 +310,7 @@ def create_missing_rule_sets(sender, instance, **kwargs): """ Called *after* a Group object is saved. As the linked RuleSet instances are saved *before* the Group, - then we can now use these RuleSet values to update the + then we can now use these RuleSet values to update the group permissions. """ diff --git a/InvenTree/users/tests.py b/InvenTree/users/tests.py index 8db631ab98..ad3a6ec5ae 100644 --- a/InvenTree/users/tests.py +++ b/InvenTree/users/tests.py @@ -76,7 +76,6 @@ class RuleSetModelTest(TestCase): print("{n} is not a valid database table".format(n=m)) errors += 1 - missing_models = [] for model in available_tables: @@ -89,4 +88,4 @@ class RuleSetModelTest(TestCase): print("-", m) self.assertEqual(errors, 0) - self.assertEqual(len(missing_models), 0) \ No newline at end of file + self.assertEqual(len(missing_models), 0) From cda52a58e318c7f0aeed0cc1c50b2617418f9d5d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 4 Oct 2020 12:19:56 +1100 Subject: [PATCH 11/16] Remove manual 'permissions' control from groups admin interface - Does not actually *do* anything any more as the RuleSet approach overrides it anyway --- InvenTree/users/admin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/users/admin.py b/InvenTree/users/admin.py index a556e60080..c9546e0335 100644 --- a/InvenTree/users/admin.py +++ b/InvenTree/users/admin.py @@ -34,7 +34,6 @@ class InvenTreeGroupAdminForm(forms.ModelForm): fields = [ 'name', 'users', - 'permissions', ] def __init__(self, *args, **kwargs): From 31b699d5216bb0df1ff64b813db78eecd14bc53b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 4 Oct 2020 12:47:19 +1100 Subject: [PATCH 12/16] Hide "user permissions" view from the admin interface --- InvenTree/users/admin.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/InvenTree/users/admin.py b/InvenTree/users/admin.py index c9546e0335..86d4bb1a86 100644 --- a/InvenTree/users/admin.py +++ b/InvenTree/users/admin.py @@ -8,6 +8,7 @@ from django import forms from django.contrib.auth import get_user_model from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.auth.models import Group +from django.contrib.auth.admin import UserAdmin from users.models import RuleSet @@ -15,6 +16,10 @@ User = get_user_model() class RuleSetInline(admin.TabularInline): + """ + Class for displaying inline RuleSet data in the Group admin page. + """ + model = RuleSet can_delete = False verbose_name = 'Ruleset' @@ -27,6 +32,11 @@ class RuleSetInline(admin.TabularInline): class InvenTreeGroupAdminForm(forms.ModelForm): + """ + Custom admin form for the Group model. + + Adds the ability for editing user membership directly in the group admin page. + """ class Meta: model = Group @@ -54,8 +64,6 @@ class InvenTreeGroupAdminForm(forms.ModelForm): def save_m2m(self): # Add the users to the Group. - # Deprecated in Django 1.10: Direct assignment to a reverse foreign key - # or many-to-many relation self.instance.user_set.set(self.cleaned_data['users']) @@ -100,5 +108,28 @@ class RoleGroupAdmin(admin.ModelAdmin): form.instance.save() # form.instance is the parent +class InvenTreeUserAdmin(UserAdmin): + """ + Custom admin page for the User model. + + Hides the "permissions" view as this is now handled + entirely by groups and RuleSets. + + (And it's confusing!) + """ + + fieldsets = ( + (None, {'fields': ('username', 'password')}), + (_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}), + (_('Permissions'), { + 'fields': ('is_active', 'is_staff', 'is_superuser', 'groups'), + }), + (_('Important dates'), {'fields': ('last_login', 'date_joined')}), + ) + + admin.site.unregister(Group) admin.site.register(Group, RoleGroupAdmin) + +admin.site.unregister(User) +admin.site.register(User, InvenTreeUserAdmin) From 929411e49a96beea57b33cf1312d92a31b3829ea Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 4 Oct 2020 12:53:24 +1100 Subject: [PATCH 13/16] Remove "general" ruleset --- InvenTree/users/models.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 1bffbf86a9..8f349e2f18 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -24,7 +24,6 @@ class RuleSet(models.Model): """ RULESET_CHOICES = [ - ('general', _('General')), ('admin', _('Admin')), ('part', _('Parts')), ('stock', _('Stock')), @@ -38,9 +37,6 @@ class RuleSet(models.Model): ] RULESET_MODELS = { - 'general': [ - 'part_partstar', - ], 'admin': [ 'auth_group', 'auth_user', @@ -99,13 +95,14 @@ class RuleSet(models.Model): 'django_session', # Models which currently do not require permissions - 'common_inventreesetting', - 'common_currency', 'common_colortheme', + 'common_currency', + 'common_inventreesetting', 'company_contact', 'label_stockitemlabel', - 'report_testreport', 'report_reportasset', + 'report_testreport', + 'part_partstar', ] RULE_OPTIONS = [ From fb09f53dc9a2016cd472ac5c63a6202b930ae616 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 4 Oct 2020 12:58:45 +1100 Subject: [PATCH 14/16] Add missing migration file --- .../migrations/0002_auto_20201004_0158.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 InvenTree/users/migrations/0002_auto_20201004_0158.py diff --git a/InvenTree/users/migrations/0002_auto_20201004_0158.py b/InvenTree/users/migrations/0002_auto_20201004_0158.py new file mode 100644 index 0000000000..a0573a89be --- /dev/null +++ b/InvenTree/users/migrations/0002_auto_20201004_0158.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.7 on 2020-10-04 01:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='ruleset', + name='name', + field=models.CharField(choices=[('admin', 'Admin'), ('part', 'Parts'), ('stock', 'Stock'), ('build', 'Build Orders'), ('purchase_order', 'Purchase Orders'), ('sales_order', 'Sales Orders')], help_text='Permission set', max_length=50), + ), + ] From 095ef51991b1f857bf52d5b3f2cff11a51164426 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 5 Oct 2020 08:29:36 +1100 Subject: [PATCH 15/16] Cleanup unit testing --- InvenTree/users/tests.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/InvenTree/users/tests.py b/InvenTree/users/tests.py index ad3a6ec5ae..d87649e2db 100644 --- a/InvenTree/users/tests.py +++ b/InvenTree/users/tests.py @@ -59,8 +59,6 @@ class RuleSetModelTest(TestCase): table_name = model.objects.model._meta.db_table available_tables.append(table_name) - errors = 0 - assigned_models = [] # Now check that each defined model is a valid table name @@ -72,10 +70,6 @@ class RuleSetModelTest(TestCase): assigned_models.append(m) - if m not in available_tables: - print("{n} is not a valid database table".format(n=m)) - errors += 1 - missing_models = [] for model in available_tables: @@ -87,5 +81,18 @@ class RuleSetModelTest(TestCase): for m in missing_models: print("-", m) - self.assertEqual(errors, 0) + extra_models = [] + + defined_models = assigned_models + RuleSet.RULESET_IGNORE + + for model in defined_models: + if model not in available_tables: + extra_models.append(model) + + if len(extra_models) > 0: + print("The following RuleSet permissions do not match a database model:") + for m in extra_models: + print("-", m) + self.assertEqual(len(missing_models), 0) + self.assertEqual(len(extra_models), 0) From 898c604b3b0d01094b715acadae76e35bdd15b65 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 5 Oct 2020 08:55:15 +1100 Subject: [PATCH 16/16] Fix incorrect permission names - Uses the app_model name, *NOT* the name of the database table - Adds extra tests to ensure that permissions get assigned and removed correctly --- InvenTree/users/models.py | 19 ++++++---- InvenTree/users/tests.py | 79 ++++++++++++++++++++++++++++++++++----- 2 files changed, 80 insertions(+), 18 deletions(-) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 8f349e2f18..09f2a046d1 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -72,8 +72,8 @@ class RuleSet(models.Model): ], 'purchase_order': [ 'company_company', - 'part_supplierpart', - 'part_supplierpricebreak', + 'company_supplierpart', + 'company_supplierpricebreak', 'order_purchaseorder', 'order_purchaseorderattachment', 'order_purchaseorderlineitem', @@ -90,9 +90,9 @@ class RuleSet(models.Model): # Database models we ignore permission sets for RULESET_IGNORE = [ # Core django models (not user configurable) - 'django_admin_log', - 'django_content_type', - 'django_session', + 'admin_logentry', + 'contenttypes_contenttype', + 'sessions_session', # Models which currently do not require permissions 'common_colortheme', @@ -275,9 +275,12 @@ def update_group_roles(group, debug=False): (permission_name, model) = perm.split('_') - content_type = ContentType.objects.get(app_label=app, model=model) - - permission = Permission.objects.get(content_type=content_type, codename=perm) + try: + content_type = ContentType.objects.get(app_label=app, model=model) + permission = Permission.objects.get(content_type=content_type, codename=perm) + except ContentType.DoesNotExist: + print(f"Error: Could not find permission matching '{permission_string}'") + permission = None return permission diff --git a/InvenTree/users/tests.py b/InvenTree/users/tests.py index d87649e2db..d14ffc4950 100644 --- a/InvenTree/users/tests.py +++ b/InvenTree/users/tests.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals from django.test import TestCase from django.apps import apps +from django.contrib.auth.models import Group from users.models import RuleSet @@ -53,13 +54,15 @@ class RuleSetModelTest(TestCase): available_models = apps.get_models() - available_tables = [] + available_tables = set() + # Extract each available database model and construct a formatted string for model in available_models: - table_name = model.objects.model._meta.db_table - available_tables.append(table_name) + label = model.objects.model._meta.label + label = label.replace('.', '_').lower() + available_tables.add(label) - assigned_models = [] + assigned_models = set() # Now check that each defined model is a valid table name for key in RuleSet.RULESET_MODELS.keys(): @@ -68,26 +71,32 @@ class RuleSetModelTest(TestCase): for m in models: - assigned_models.append(m) + assigned_models.add(m) - missing_models = [] + missing_models = set() for model in available_tables: if model not in assigned_models and model not in RuleSet.RULESET_IGNORE: - missing_models.append(model) + missing_models.add(model) if len(missing_models) > 0: print("The following database models are not covered by the defined RuleSet permissions:") for m in missing_models: print("-", m) - extra_models = [] + extra_models = set() - defined_models = assigned_models + RuleSet.RULESET_IGNORE + defined_models = set() + + for model in assigned_models: + defined_models.add(model) + + for model in RuleSet.RULESET_IGNORE: + defined_models.add(model) for model in defined_models: if model not in available_tables: - extra_models.append(model) + extra_models.add(model) if len(extra_models) > 0: print("The following RuleSet permissions do not match a database model:") @@ -96,3 +105,53 @@ class RuleSetModelTest(TestCase): self.assertEqual(len(missing_models), 0) self.assertEqual(len(extra_models), 0) + + def test_permission_assign(self): + """ + Test that the permission assigning works! + """ + + # Create a new group + group = Group.objects.create(name="Test group") + + rulesets = group.rule_sets.all() + + # Rulesets should have been created automatically for this group + self.assertEqual(rulesets.count(), len(RuleSet.RULESET_CHOICES)) + + # Check that all permissions have been assigned permissions? + permission_set = set() + + for models in RuleSet.RULESET_MODELS.values(): + + for model in models: + permission_set.add(model) + + # Every ruleset by default sets one permission, the "view" permission set + self.assertEqual(group.permissions.count(), len(permission_set)) + + # Add some more rules + for rule in rulesets: + rule.can_add = True + rule.can_change = True + + rule.save() + + group.save() + + # There should now be three permissions for each rule set + self.assertEqual(group.permissions.count(), 3 * len(permission_set)) + + # Now remove *all* permissions + for rule in rulesets: + rule.can_view = False + rule.can_add = False + rule.can_change = False + rule.can_delete = False + + rule.save() + + group.save() + + # There should now not be any permissions assigned to this group + self.assertEqual(group.permissions.count(), 0)