• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

inventree / InvenTree / 4548642839

pending completion
4548642839

push

github

GitHub
[Feature] Add RMA support (#4488)

1082 of 1082 new or added lines in 49 files covered. (100.0%)

26469 of 30045 relevant lines covered (88.1%)

0.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.55
/InvenTree/users/models.py
1
"""Database model definitions for the 'users' app"""
2

3
import logging
1✔
4

5
from django.contrib.auth import get_user_model
1✔
6
from django.contrib.auth.models import Group, Permission
1✔
7
from django.contrib.contenttypes.fields import GenericForeignKey
1✔
8
from django.contrib.contenttypes.models import ContentType
1✔
9
from django.core.cache import cache
1✔
10
from django.db import models
1✔
11
from django.db.models import Q, UniqueConstraint
1✔
12
from django.db.models.signals import post_delete, post_save
1✔
13
from django.db.utils import IntegrityError
1✔
14
from django.dispatch import receiver
1✔
15
from django.urls import reverse
1✔
16
from django.utils.translation import gettext_lazy as _
1✔
17

18
from InvenTree.ready import canAppAccessDatabase
1✔
19

20
logger = logging.getLogger("inventree")
1✔
21

22

23
class RuleSet(models.Model):
1✔
24
    """A RuleSet is somewhat like a superset of the django permission class, in that in encapsulates a bunch of permissions.
25

26
    There are *many* apps models used within InvenTree,
27
    so it makes sense to group them into "roles".
28

29
    These roles translate (roughly) to the menu options available.
30

31
    Each role controls permissions for a number of database tables,
32
    which are then handled using the normal django permissions approach.
33
    """
34

35
    RULESET_CHOICES = [
1✔
36
        ('admin', _('Admin')),
37
        ('part_category', _('Part Categories')),
38
        ('part', _('Parts')),
39
        ('stocktake', _('Stocktake')),
40
        ('stock_location', _('Stock Locations')),
41
        ('stock', _('Stock Items')),
42
        ('build', _('Build Orders')),
43
        ('purchase_order', _('Purchase Orders')),
44
        ('sales_order', _('Sales Orders')),
45
        ('return_order', _('Return Orders')),
46
    ]
47

48
    RULESET_NAMES = [
1✔
49
        choice[0] for choice in RULESET_CHOICES
50
    ]
51

52
    RULESET_PERMISSIONS = [
1✔
53
        'view', 'add', 'change', 'delete',
54
    ]
55

56
    RULESET_MODELS = {
1✔
57
        'admin': [
58
            'auth_group',
59
            'auth_user',
60
            'auth_permission',
61
            'authtoken_token',
62
            'authtoken_tokenproxy',
63
            'users_ruleset',
64
            'report_reportasset',
65
            'report_reportsnippet',
66
            'report_billofmaterialsreport',
67
            'report_purchaseorderreport',
68
            'report_salesorderreport',
69
            'account_emailaddress',
70
            'account_emailconfirmation',
71
            'sites_site',
72
            'socialaccount_socialaccount',
73
            'socialaccount_socialapp',
74
            'socialaccount_socialtoken',
75
            'otp_totp_totpdevice',
76
            'otp_static_statictoken',
77
            'otp_static_staticdevice',
78
            'plugin_pluginconfig',
79
            'plugin_pluginsetting',
80
            'plugin_notificationusersetting',
81
            'common_newsfeedentry',
82
        ],
83
        'part_category': [
84
            'part_partcategory',
85
            'part_partcategoryparametertemplate',
86
            'part_partcategorystar',
87
        ],
88
        'part': [
89
            'part_part',
90
            'part_partpricing',
91
            'part_bomitem',
92
            'part_bomitemsubstitute',
93
            'part_partattachment',
94
            'part_partsellpricebreak',
95
            'part_partinternalpricebreak',
96
            'part_parttesttemplate',
97
            'part_partparametertemplate',
98
            'part_partparameter',
99
            'part_partrelated',
100
            'part_partstar',
101
            'part_partcategorystar',
102
            'company_supplierpart',
103
            'company_manufacturerpart',
104
            'company_manufacturerpartparameter',
105
            'company_manufacturerpartattachment',
106
            'label_partlabel',
107
        ],
108
        'stocktake': [
109
            'part_partstocktake',
110
            'part_partstocktakereport',
111
        ],
112
        'stock_location': [
113
            'stock_stocklocation',
114
            'label_stocklocationlabel',
115
        ],
116
        'stock': [
117
            'stock_stockitem',
118
            'stock_stockitemattachment',
119
            'stock_stockitemtracking',
120
            'stock_stockitemtestresult',
121
            'report_testreport',
122
            'label_stockitemlabel',
123
        ],
124
        'build': [
125
            'part_part',
126
            'part_partcategory',
127
            'part_bomitem',
128
            'part_bomitemsubstitute',
129
            'build_build',
130
            'build_builditem',
131
            'build_buildorderattachment',
132
            'stock_stockitem',
133
            'stock_stocklocation',
134
            'report_buildreport',
135
        ],
136
        'purchase_order': [
137
            'company_company',
138
            'company_companyattachment',
139
            'company_contact',
140
            'company_manufacturerpart',
141
            'company_manufacturerpartparameter',
142
            'company_supplierpart',
143
            'company_supplierpricebreak',
144
            'order_purchaseorder',
145
            'order_purchaseorderattachment',
146
            'order_purchaseorderlineitem',
147
            'order_purchaseorderextraline',
148
            'report_purchaseorderreport',
149
        ],
150
        'sales_order': [
151
            'company_company',
152
            'company_companyattachment',
153
            'company_contact',
154
            'order_salesorder',
155
            'order_salesorderallocation',
156
            'order_salesorderattachment',
157
            'order_salesorderlineitem',
158
            'order_salesorderextraline',
159
            'order_salesordershipment',
160
            'report_salesorderreport',
161
        ],
162
        'return_order': [
163
            'company_company',
164
            'company_companyattachment',
165
            'company_contact',
166
            'order_returnorder',
167
            'order_returnorderlineitem',
168
            'order_returnorderextraline',
169
            'order_returnorderattachment',
170
            'report_returnorderreport',
171
        ]
172
    }
173

174
    # Database models we ignore permission sets for
175
    RULESET_IGNORE = [
1✔
176
        # Core django models (not user configurable)
177
        'admin_logentry',
178
        'contenttypes_contenttype',
179

180
        # Models which currently do not require permissions
181
        'common_colortheme',
182
        'common_inventreesetting',
183
        'common_inventreeusersetting',
184
        'common_webhookendpoint',
185
        'common_webhookmessage',
186
        'common_notificationentry',
187
        'common_notificationmessage',
188
        'users_owner',
189

190
        # Third-party tables
191
        'error_report_error',
192
        'exchange_rate',
193
        'exchange_exchangebackend',
194
        'user_sessions_session',
195

196
        # Django-q
197
        'django_q_ormq',
198
        'django_q_failure',
199
        'django_q_task',
200
        'django_q_schedule',
201
        'django_q_success',
202
    ]
203

204
    RULESET_CHANGE_INHERIT = [
1✔
205
        ('part', 'partparameter'),
206
        ('part', 'bomitem'),
207
    ]
208

209
    RULE_OPTIONS = [
1✔
210
        'can_view',
211
        'can_add',
212
        'can_change',
213
        'can_delete',
214
    ]
215

216
    class Meta:
1✔
217
        """Metaclass defines additional model properties"""
218
        unique_together = (
1✔
219
            ('name', 'group'),
220
        )
221

222
    name = models.CharField(
1✔
223
        max_length=50,
224
        choices=RULESET_CHOICES,
225
        blank=False,
226
        help_text=_('Permission set')
227
    )
228

229
    group = models.ForeignKey(
1✔
230
        Group,
231
        related_name='rule_sets',
232
        blank=False, null=False,
233
        on_delete=models.CASCADE,
234
        help_text=_('Group'),
235
    )
236

237
    can_view = models.BooleanField(verbose_name=_('View'), default=True, help_text=_('Permission to view items'))
1✔
238

239
    can_add = models.BooleanField(verbose_name=_('Add'), default=False, help_text=_('Permission to add items'))
1✔
240

241
    can_change = models.BooleanField(verbose_name=_('Change'), default=False, help_text=_('Permissions to edit items'))
1✔
242

243
    can_delete = models.BooleanField(verbose_name=_('Delete'), default=False, help_text=_('Permission to delete items'))
1✔
244

245
    @classmethod
1✔
246
    def check_table_permission(cls, user, table, permission):
1✔
247
        """Check if the provided user has the specified permission against the table."""
248

249
        # Superuser knows no bounds
250
        if user.is_superuser:
1✔
251
            return True
×
252

253
        # If the table does *not* require permissions
254
        if table in cls.RULESET_IGNORE:
1✔
255
            return True
1✔
256

257
        # Work out which roles touch the given table
258
        for role in cls.RULESET_NAMES:
1✔
259
            if table in cls.RULESET_MODELS[role]:
1✔
260

261
                if check_user_role(user, role, permission):
1✔
262
                    return True
1✔
263

264
        # Check for children models which inherits from parent role
265
        for (parent, child) in cls.RULESET_CHANGE_INHERIT:
1✔
266
            # Get child model name
267
            parent_child_string = f'{parent}_{child}'
1✔
268

269
            if parent_child_string == table:
1✔
270
                # Check if parent role has change permission
271
                if check_user_role(user, parent, 'change'):
1✔
272
                    return True
1✔
273

274
        # Print message instead of throwing an error
275
        name = getattr(user, 'name', user.pk)
1✔
276
        logger.debug(f"User '{name}' failed permission check for {table}.{permission}")
1✔
277

278
        return False
1✔
279

280
    @staticmethod
1✔
281
    def get_model_permission_string(model, permission):
1✔
282
        """Construct the correctly formatted permission string, given the app_model name, and the permission type."""
283
        model, app = split_model(model)
1✔
284

285
        return "{app}.{perm}_{model}".format(
1✔
286
            app=app,
287
            perm=permission,
288
            model=model
289
        )
290

291
    def __str__(self, debug=False):  # pragma: no cover
292
        """Ruleset string representation."""
293
        if debug:
294
            # Makes debugging easier
295
            return f'{str(self.group).ljust(15)}: {self.name.title().ljust(15)} | ' \
296
                   f'v: {str(self.can_view).ljust(5)} | a: {str(self.can_add).ljust(5)} | ' \
297
                   f'c: {str(self.can_change).ljust(5)} | d: {str(self.can_delete).ljust(5)}'
298
        else:
299
            return self.name
300

301
    def save(self, *args, **kwargs):
1✔
302
        """Intercept the 'save' functionality to make addtional permission changes:
303

304
        It does not make sense to be able to change / create something,
305
        but not be able to view it!
306
        """
307
        if self.can_add or self.can_change or self.can_delete:
1✔
308
            self.can_view = True
1✔
309

310
        if self.can_add or self.can_delete:
1✔
311
            self.can_change = True
1✔
312

313
        super().save(*args, **kwargs)
1✔
314

315
        if self.group:
1✔
316
            # Update the group too!
317
            self.group.save()
1✔
318

319
    def get_models(self):
1✔
320
        """Return the database tables / models that this ruleset covers."""
321
        return self.RULESET_MODELS.get(self.name, [])
1✔
322

323

324
def split_model(model):
1✔
325
    """Get modelname and app from modelstring."""
326
    *app, model = model.split('_')
1✔
327

328
    # handle models that have
329
    if len(app) > 1:
1✔
330
        app = '_'.join(app)
1✔
331
    else:
332
        app = app[0]
1✔
333

334
    return model, app
1✔
335

336

337
def split_permission(app, perm):
1✔
338
    """Split permission string into permission and model."""
339
    permission_name, *model = perm.split('_')
1✔
340
    # handle models that have underscores
341
    if len(model) > 1:  # pragma: no cover
342
        app += '_' + '_'.join(model[:-1])
343
        perm = permission_name + '_' + model[-1:][0]
344
    model = model[-1:][0]
1✔
345
    return perm, model
1✔
346

347

348
def update_group_roles(group, debug=False):
1✔
349
    """Iterates through all of the RuleSets associated with the group, and ensures that the correct permissions are either applied or removed from the group.
350

351
    This function is called under the following conditions:
352

353
    a) Whenever the InvenTree database is launched
354
    b) Whenver the group object is updated
355

356
    The RuleSet model has complete control over the permissions applied to any group.
357
    """
358
    if not canAppAccessDatabase(allow_test=True):
1✔
359
        return  # pragma: no cover
360

361
    # List of permissions already associated with this group
362
    group_permissions = set()
1✔
363

364
    # Iterate through each permission already assigned to this group,
365
    # and create a simplified permission key string
366
    for p in group.permissions.all():
1✔
367
        (permission, app, model) = p.natural_key()
1✔
368

369
        permission_string = '{app}.{perm}'.format(
1✔
370
            app=app,
371
            perm=permission
372
        )
373

374
        group_permissions.add(permission_string)
1✔
375

376
    # List of permissions which must be added to the group
377
    permissions_to_add = set()
1✔
378

379
    # List of permissions which must be removed from the group
380
    permissions_to_delete = set()
1✔
381

382
    def add_model(name, action, allowed):
1✔
383
        """Add a new model to the pile.
384

385
        Args:
386
            name: The name of the model e.g. part_part
387
            action: The permission action e.g. view
388
            allowed: Whether or not the action is allowed
389
        """
390
        if action not in ['view', 'add', 'change', 'delete']:  # pragma: no cover
391
            raise ValueError("Action {a} is invalid".format(a=action))
392

393
        permission_string = RuleSet.get_model_permission_string(model, action)
1✔
394

395
        if allowed:
1✔
396

397
            # An 'allowed' action is always preferenced over a 'forbidden' action
398
            if permission_string in permissions_to_delete:
1✔
399
                permissions_to_delete.remove(permission_string)
1✔
400

401
            permissions_to_add.add(permission_string)
1✔
402

403
        else:
404

405
            # A forbidden action will be ignored if we have already allowed it
406
            if permission_string not in permissions_to_add:
1✔
407
                permissions_to_delete.add(permission_string)
1✔
408

409
    # Get all the rulesets associated with this group
410
    for r in RuleSet.RULESET_CHOICES:
1✔
411

412
        rulename = r[0]
1✔
413

414
        try:
1✔
415
            ruleset = RuleSet.objects.get(group=group, name=rulename)
1✔
416
        except RuleSet.DoesNotExist:
1✔
417
            # Create the ruleset with default values (if it does not exist)
418
            ruleset = RuleSet.objects.create(group=group, name=rulename)
1✔
419

420
        # Which database tables does this RuleSet touch?
421
        models = ruleset.get_models()
1✔
422

423
        for model in models:
1✔
424
            # Keep track of the available permissions for each model
425

426
            add_model(model, 'view', ruleset.can_view)
1✔
427
            add_model(model, 'add', ruleset.can_add)
1✔
428
            add_model(model, 'change', ruleset.can_change)
1✔
429
            add_model(model, 'delete', ruleset.can_delete)
1✔
430

431
    def get_permission_object(permission_string):
1✔
432
        """Find the permission object in the database, from the simplified permission string.
433

434
        Args:
435
            permission_string: a simplified permission_string e.g. 'part.view_partcategory'
436

437
        Returns the permission object in the database associated with the permission string
438
        """
439
        (app, perm) = permission_string.split('.')
1✔
440

441
        perm, model = split_permission(app, perm)
1✔
442

443
        try:
1✔
444
            content_type = ContentType.objects.get(app_label=app, model=model)
1✔
445
            permission = Permission.objects.get(content_type=content_type, codename=perm)
1✔
446
        except ContentType.DoesNotExist:  # pragma: no cover
447
            logger.warning(f"Error: Could not find permission matching '{permission_string}'")
448
            permission = None
449

450
        return permission
1✔
451

452
    # Add any required permissions to the group
453
    for perm in permissions_to_add:
1✔
454

455
        # Ignore if permission is already in the group
456
        if perm in group_permissions:
1✔
457
            continue
1✔
458

459
        permission = get_permission_object(perm)
1✔
460

461
        if permission:
1✔
462
            group.permissions.add(permission)
1✔
463

464
        if debug:  # pragma: no cover
465
            logger.debug(f"Adding permission {perm} to group {group.name}")
466

467
    # Remove any extra permissions from the group
468
    for perm in permissions_to_delete:
1✔
469

470
        # Ignore if the permission is not already assigned
471
        if perm not in group_permissions:
1✔
472
            continue
1✔
473

474
        permission = get_permission_object(perm)
1✔
475

476
        if permission:
1✔
477
            group.permissions.remove(permission)
1✔
478

479
        if debug:  # pragma: no cover
480
            logger.debug(f"Removing permission {perm} from group {group.name}")
481

482
    # Enable all action permissions for certain children models
483
    # if parent model has 'change' permission
484
    for (parent, child) in RuleSet.RULESET_CHANGE_INHERIT:
1✔
485
        parent_child_string = f'{parent}_{child}'
1✔
486

487
        # Check each type of permission
488
        for action in ['view', 'change', 'add', 'delete']:
1✔
489
            parent_perm = f'{parent}.{action}_{parent}'
1✔
490

491
            if parent_perm in group_permissions:
1✔
492
                child_perm = f'{parent}.{action}_{child}'
1✔
493

494
                # Check if child permission not already in group
495
                if child_perm not in group_permissions:
1✔
496
                    # Create permission object
497
                    add_model(parent_child_string, action, ruleset.can_delete)
1✔
498
                    # Add to group
499
                    permission = get_permission_object(child_perm)
1✔
500
                    if permission:
1✔
501
                        group.permissions.add(permission)
1✔
502
                        logger.debug(f"Adding permission {child_perm} to group {group.name}")
1✔
503

504

505
def clear_user_role_cache(user):
1✔
506
    """Remove user role permission information from the cache.
507

508
    - This function is called whenever the user / group is updated
509

510
    Args:
511
        user: The User object to be expunged from the cache
512
    """
513

514
    for role in RuleSet.RULESET_MODELS.keys():
1✔
515
        for perm in ['add', 'change', 'view', 'delete']:
1✔
516
            key = f"role_{user}_{role}_{perm}"
1✔
517
            cache.delete(key)
1✔
518

519

520
def get_user_roles(user):
1✔
521
    """Return all roles available to a given user"""
522

523
    roles = set()
×
524

525
    for group in user.groups.all():
×
526
        for rule in group.rule_sets.all():
×
527
            name = rule.name
×
528
            if rule.can_view:
×
529
                roles.add(f'{name}.view')
×
530
            if rule.can_add:
×
531
                roles.add(f'{name}.add')
×
532
            if rule.can_change:
×
533
                roles.add(f'{name}.change')
×
534
            if rule.can_delete:
×
535
                roles.add(f'{name}.delete')
×
536

537
    return roles
×
538

539

540
def check_user_role(user, role, permission):
1✔
541
    """Check if a user has a particular role:permission combination.
542

543
    If the user is a superuser, this will return True
544
    """
545
    if user.is_superuser:
1✔
546
        return True
1✔
547

548
    # First, check the cache
549
    key = f"role_{user}_{role}_{permission}"
1✔
550

551
    result = cache.get(key)
1✔
552

553
    if result is not None:
1✔
554
        return result
1✔
555

556
    # Default for no match
557
    result = False
1✔
558

559
    for group in user.groups.all():
1✔
560

561
        for rule in group.rule_sets.all():
1✔
562

563
            if rule.name == role:
1✔
564

565
                if permission == 'add' and rule.can_add:
1✔
566
                    result = True
1✔
567
                    break
1✔
568

569
                if permission == 'change' and rule.can_change:
1✔
570
                    result = True
1✔
571
                    break
1✔
572

573
                if permission == 'view' and rule.can_view:
1✔
574
                    result = True
1✔
575
                    break
1✔
576

577
                if permission == 'delete' and rule.can_delete:
1✔
578
                    result = True
1✔
579
                    break
1✔
580

581
    # Save result to cache
582
    cache.set(key, result, timeout=3600)
1✔
583
    return result
1✔
584

585

586
class Owner(models.Model):
1✔
587
    """The Owner class is a proxy for a Group or User instance.
588

589
    Owner can be associated to any InvenTree model (part, stock, build, etc.)
590

591
    owner_type: Model type (Group or User)
592
    owner_id: Group or User instance primary key
593
    owner: Returns the Group or User instance combining the owner_type and owner_id fields
594
    """
595

596
    class Meta:
1✔
597
        """Metaclass defines extra model properties"""
598
        # Ensure all owners are unique
599
        constraints = [
1✔
600
            UniqueConstraint(fields=['owner_type', 'owner_id'],
601
                             name='unique_owner')
602
        ]
603

604
    @classmethod
1✔
605
    def get_owners_matching_user(cls, user):
1✔
606
        """Return all "owner" objects matching the provided user.
607

608
        Includes:
609
        - An exact match for the user
610
        - Any groups that the user is a part of
611
        """
612
        user_type = ContentType.objects.get(app_label='auth', model='user')
1✔
613
        group_type = ContentType.objects.get(app_label='auth', model='group')
1✔
614

615
        owners = []
1✔
616

617
        try:
1✔
618
            owners.append(cls.objects.get(owner_id=user.pk, owner_type=user_type))
1✔
619
        except Exception:  # pragma: no cover
620
            pass
621

622
        for group in user.groups.all():
1✔
623
            try:
1✔
624
                owner = cls.objects.get(owner_id=group.pk, owner_type=group_type)
1✔
625
                owners.append(owner)
1✔
626
            except Exception:  # pragma: no cover
627
                pass
628

629
        return owners
1✔
630

631
    @staticmethod
1✔
632
    def get_api_url():  # pragma: no cover
633
        """Returns the API endpoint URL associated with the Owner model"""
634
        return reverse('api-owner-list')
635

636
    owner_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True, blank=True)
1✔
637

638
    owner_id = models.PositiveIntegerField(null=True, blank=True)
1✔
639

640
    owner = GenericForeignKey('owner_type', 'owner_id')
1✔
641

642
    def __str__(self):
1✔
643
        """Defines the owner string representation."""
644
        return f'{self.owner} ({self.owner_type.name})'
1✔
645

646
    def name(self):
1✔
647
        """Return the 'name' of this owner."""
648
        return str(self.owner)
1✔
649

650
    def label(self):
1✔
651
        """Return the 'type' label of this owner i.e. 'user' or 'group'."""
652
        return str(self.owner_type.name)
1✔
653

654
    @classmethod
1✔
655
    def create(cls, obj):
1✔
656
        """Check if owner exist then create new owner entry."""
657
        # Check for existing owner
658
        existing_owner = cls.get_owner(obj)
1✔
659

660
        if not existing_owner:
1✔
661
            # Create new owner
662
            try:
1✔
663
                return cls.objects.create(owner=obj)
1✔
664
            except IntegrityError:  # pragma: no cover
665
                return None
666

667
        return existing_owner
1✔
668

669
    @classmethod
1✔
670
    def get_owner(cls, user_or_group):
1✔
671
        """Get owner instance for a group or user."""
672
        user_model = get_user_model()
1✔
673
        owner = None
1✔
674
        content_type_id = 0
1✔
675
        content_type_id_list = [ContentType.objects.get_for_model(Group).id,
1✔
676
                                ContentType.objects.get_for_model(user_model).id]
677

678
        # If instance type is obvious: set content type
679
        if isinstance(user_or_group, Group):
1✔
680
            content_type_id = content_type_id_list[0]
1✔
681
        elif isinstance(user_or_group, get_user_model()):
1✔
682
            content_type_id = content_type_id_list[1]
1✔
683

684
        if content_type_id:
1✔
685
            try:
1✔
686
                owner = Owner.objects.get(owner_id=user_or_group.id,
1✔
687
                                          owner_type=content_type_id)
688
            except Owner.DoesNotExist:
1✔
689
                pass
1✔
690

691
        return owner
1✔
692

693
    def get_related_owners(self, include_group=False):
1✔
694
        """Get all owners "related" to an owner.
695

696
        This method is useful to retrieve all "user-type" owners linked to a "group-type" owner
697
        """
698
        user_model = get_user_model()
1✔
699
        related_owners = None
1✔
700

701
        if type(self.owner) is Group:
1✔
702
            users = user_model.objects.filter(groups__name=self.owner.name)
1✔
703

704
            if include_group:
1✔
705
                # Include "group-type" owner in the query
706
                query = Q(owner_id__in=users, owner_type=ContentType.objects.get_for_model(user_model).id) | \
1✔
707
                    Q(owner_id=self.owner.id, owner_type=ContentType.objects.get_for_model(Group).id)
708
            else:
709
                query = Q(owner_id__in=users, owner_type=ContentType.objects.get_for_model(user_model).id)
1✔
710

711
            related_owners = Owner.objects.filter(query)
1✔
712

713
        elif type(self.owner) is user_model:
1✔
714
            related_owners = [self]
1✔
715

716
        return related_owners
1✔
717

718
    def is_user_allowed(self, user, include_group: bool = False):
1✔
719
        """Check if user is allowed to access something owned by this owner."""
720

721
        user_owner = Owner.get_owner(user)
1✔
722
        return user_owner in self.get_related_owners(include_group=include_group)
1✔
723

724

725
@receiver(post_save, sender=Group, dispatch_uid='create_owner')
1✔
726
@receiver(post_save, sender=get_user_model(), dispatch_uid='create_owner')
1✔
727
def create_owner(sender, instance, **kwargs):
1✔
728
    """Callback function to create a new owner instance after either a new group or user instance is saved."""
729
    Owner.create(obj=instance)
1✔
730

731

732
@receiver(post_delete, sender=Group, dispatch_uid='delete_owner')
1✔
733
@receiver(post_delete, sender=get_user_model(), dispatch_uid='delete_owner')
1✔
734
def delete_owner(sender, instance, **kwargs):
1✔
735
    """Callback function to delete an owner instance after either a new group or user instance is deleted."""
736
    owner = Owner.get_owner(instance)
1✔
737
    owner.delete()
1✔
738

739

740
@receiver(post_save, sender=get_user_model(), dispatch_uid='clear_user_cache')
1✔
741
def clear_user_cache(sender, instance, **kwargs):
1✔
742
    """Callback function when a user object is saved"""
743

744
    clear_user_role_cache(instance)
1✔
745

746

747
@receiver(post_save, sender=Group, dispatch_uid='create_missing_rule_sets')
1✔
748
def create_missing_rule_sets(sender, instance, **kwargs):
1✔
749
    """Called *after* a Group object is saved.
750

751
    As the linked RuleSet instances are saved *before* the Group, then we can now use these RuleSet values to update the group permissions.
752
    """
753
    update_group_roles(instance)
1✔
754

755
    for user in get_user_model().objects.filter(groups__name=instance.name):
1✔
756
        clear_user_role_cache(user)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc