misc: implement soft-delete on RoleParenting (#57500)
This commit is contained in:
parent
a8994cfc62
commit
c004a56673
|
@ -0,0 +1,27 @@
|
||||||
|
# Generated by Django 2.2.19 on 2021-10-06 10:30
|
||||||
|
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('a2_rbac', '0028_ou_home_url'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='roleparenting',
|
||||||
|
name='created',
|
||||||
|
field=models.DateTimeField(
|
||||||
|
auto_now_add=True, default=django.utils.timezone.now, verbose_name='Creation date'
|
||||||
|
),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='roleparenting',
|
||||||
|
name='deleted',
|
||||||
|
field=models.DateTimeField(null=True, verbose_name='Deletion date'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -226,7 +226,10 @@ class User(AbstractBaseUser, PermissionMixin):
|
||||||
|
|
||||||
def roles_and_parents(self):
|
def roles_and_parents(self):
|
||||||
qs1 = self.roles.all()
|
qs1 = self.roles.all()
|
||||||
qs2 = qs1.model.objects.filter(child_relation__child__in=qs1)
|
qs2 = qs1.model.objects.filter(
|
||||||
|
child_relation__deleted__isnull=True,
|
||||||
|
child_relation__child__in=qs1,
|
||||||
|
)
|
||||||
qs = (qs1 | qs2).order_by('name').distinct()
|
qs = (qs1 | qs2).order_by('name').distinct()
|
||||||
rp_qs = RoleParenting.objects.filter(child__in=qs1)
|
rp_qs = RoleParenting.objects.filter(child__in=qs1)
|
||||||
qs = qs.prefetch_related(models.Prefetch('child_relation', queryset=rp_qs), 'child_relation__parent')
|
qs = qs.prefetch_related(models.Prefetch('child_relation', queryset=rp_qs), 'child_relation__parent')
|
||||||
|
|
|
@ -388,7 +388,9 @@ class Command(BaseCommand):
|
||||||
direct_members = manager_role.members.all()
|
direct_members = manager_role.members.all()
|
||||||
direct_members_count = direct_members.count()
|
direct_members_count = direct_members.count()
|
||||||
direct_children = Role.objects.filter(
|
direct_children = Role.objects.filter(
|
||||||
parent_relation__parent=manager_role, parent_relation__direct=True
|
parent_relation__deleted__isnull=True,
|
||||||
|
parent_relation__parent=manager_role,
|
||||||
|
parent_relation__direct=True,
|
||||||
)
|
)
|
||||||
direct_children_count = direct_children.count()
|
direct_children_count = direct_children.count()
|
||||||
show = members_count or self.verbosity > 1
|
show = members_count or self.verbosity > 1
|
||||||
|
@ -398,7 +400,9 @@ class Command(BaseCommand):
|
||||||
self.notice('- "%s" has problematic manager roles', role)
|
self.notice('- "%s" has problematic manager roles', role)
|
||||||
self.warning(' - %s', manager_role, ending=' ')
|
self.warning(' - %s', manager_role, ending=' ')
|
||||||
direct_parents = Role.objects.filter(
|
direct_parents = Role.objects.filter(
|
||||||
child_relation__child=manager_role, child_relation__direct=True
|
child_relation__deleted__isnull=True,
|
||||||
|
child_relation__child=manager_role,
|
||||||
|
child_relation__direct=True,
|
||||||
)
|
)
|
||||||
if show:
|
if show:
|
||||||
self.warning('DELETE', ending=' ')
|
self.warning('DELETE', ending=' ')
|
||||||
|
|
|
@ -36,12 +36,10 @@ class UserResource(ModelResource):
|
||||||
roles = Field()
|
roles = Field()
|
||||||
|
|
||||||
def dehydrate_roles(self, instance):
|
def dehydrate_roles(self, instance):
|
||||||
result = set()
|
roles = {role for role in instance.roles.all()}
|
||||||
for role in instance.roles.all():
|
# optimization as parent_relation is prefetched, filter deleted__isnull=True using python
|
||||||
result.add(role)
|
parents = {rp.parent for role in roles for rp in role.parent_relation.all() if not rp.deleted}
|
||||||
for pr in role.parent_relation.all():
|
return ', '.join(str(x) for x in roles | parents)
|
||||||
result.add(pr.parent)
|
|
||||||
return ', '.join(str(x) for x in result)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
|
|
|
@ -432,7 +432,7 @@ class RoleChildrenView(RoleViewMixin, views.HideOUColumnMixin, views.BaseSubTabl
|
||||||
Q(pk__in=children.filter(is_direct=False)), output_field=BooleanField()
|
Q(pk__in=children.filter(is_direct=False)), output_field=BooleanField()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
rp_qs = RoleParenting.objects.filter(parent__in=children).annotate(name=F('parent__name'))
|
rp_qs = RoleParenting.alive.filter(parent__in=children).annotate(name=F('parent__name'))
|
||||||
qs = qs.prefetch_related(Prefetch('parent_relation', queryset=rp_qs, to_attr='via'))
|
qs = qs.prefetch_related(Prefetch('parent_relation', queryset=rp_qs, to_attr='via'))
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
@ -494,7 +494,7 @@ class RoleParentsView(RoleViewMixin, views.HideOUColumnMixin, views.BaseSubTable
|
||||||
Q(pk__in=parents.filter(is_direct=False)), output_field=BooleanField()
|
Q(pk__in=parents.filter(is_direct=False)), output_field=BooleanField()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
rp_qs = RoleParenting.objects.filter(child__in=parents).annotate(name=F('child__name'))
|
rp_qs = RoleParenting.alive.filter(child__in=parents).annotate(name=F('child__name'))
|
||||||
qs = qs.prefetch_related(Prefetch('child_relation', queryset=rp_qs, to_attr='via'))
|
qs = qs.prefetch_related(Prefetch('child_relation', queryset=rp_qs, to_attr='via'))
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
|
@ -264,8 +264,8 @@ class UserRolesTable(Table):
|
||||||
)
|
)
|
||||||
ou = tables.Column()
|
ou = tables.Column()
|
||||||
via = tables.TemplateColumn(
|
via = tables.TemplateColumn(
|
||||||
'{% if not record.member %}{% for rel in record.child_relation.all %}{{ rel.child }} {% if not'
|
'{% if not record.member %}{% for rel in record.child_relation.all %}'
|
||||||
' forloop.last %}, {% endif %}{% endfor %}{% endif %}',
|
'{{ rel.child }} {% if not forloop.last %}, {% endif %}{% endfor %}{% endif %}',
|
||||||
verbose_name=_('Inherited from'),
|
verbose_name=_('Inherited from'),
|
||||||
orderable=False,
|
orderable=False,
|
||||||
attrs={"td": {"class": "via"}},
|
attrs={"td": {"class": "via"}},
|
||||||
|
|
|
@ -640,7 +640,7 @@ class UserRolesView(HideOUColumnMixin, BaseSubTableView):
|
||||||
if self.is_ou_specified():
|
if self.is_ou_specified():
|
||||||
roles = self.object.roles.all()
|
roles = self.object.roles.all()
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
rp_qs = RoleParenting.objects.filter(child__in=roles)
|
rp_qs = RoleParenting.alive.filter(child__in=roles)
|
||||||
qs = Role.objects.all()
|
qs = Role.objects.all()
|
||||||
qs = qs.prefetch_related(models.Prefetch('child_relation', queryset=rp_qs, to_attr='via'))
|
qs = qs.prefetch_related(models.Prefetch('child_relation', queryset=rp_qs, to_attr='via'))
|
||||||
qs = qs.prefetch_related(
|
qs = qs.prefetch_related(
|
||||||
|
|
|
@ -8,7 +8,7 @@ class DjangoRBACConfig(AppConfig):
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from django.db.models.signals import post_delete, post_migrate, post_save
|
from django.db.models.signals import post_delete, post_migrate, post_save
|
||||||
|
|
||||||
from . import signal_handlers, utils
|
from . import signal_handlers, signals, utils
|
||||||
|
|
||||||
# update role parenting when new role parenting is created
|
# update role parenting when new role parenting is created
|
||||||
post_save.connect(signal_handlers.role_parenting_post_save, sender=utils.get_role_parenting_model())
|
post_save.connect(signal_handlers.role_parenting_post_save, sender=utils.get_role_parenting_model())
|
||||||
|
@ -16,6 +16,14 @@ class DjangoRBACConfig(AppConfig):
|
||||||
post_delete.connect(
|
post_delete.connect(
|
||||||
signal_handlers.role_parenting_post_delete, sender=utils.get_role_parenting_model()
|
signal_handlers.role_parenting_post_delete, sender=utils.get_role_parenting_model()
|
||||||
)
|
)
|
||||||
|
# or soft-created
|
||||||
|
signals.post_soft_create.connect(
|
||||||
|
signal_handlers.role_parenting_post_soft_delete, sender=utils.get_role_parenting_model()
|
||||||
|
)
|
||||||
|
# or soft-deleted
|
||||||
|
signals.post_soft_delete.connect(
|
||||||
|
signal_handlers.role_parenting_post_soft_delete, sender=utils.get_role_parenting_model()
|
||||||
|
)
|
||||||
# create CRUD operations and admin
|
# create CRUD operations and admin
|
||||||
post_migrate.connect(signal_handlers.create_base_operations, sender=self)
|
post_migrate.connect(signal_handlers.create_base_operations, sender=self)
|
||||||
# update role parenting in post migrate
|
# update role parenting in post migrate
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import datetime
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
@ -8,7 +9,7 @@ from django.db.models import query
|
||||||
from django.db.models.query import Prefetch, Q
|
from django.db.models.query import Prefetch, Q
|
||||||
from django.db.transaction import atomic
|
from django.db.transaction import atomic
|
||||||
|
|
||||||
from . import utils
|
from . import signals, utils
|
||||||
|
|
||||||
|
|
||||||
class AbstractBaseManager(models.Manager):
|
class AbstractBaseManager(models.Manager):
|
||||||
|
@ -107,7 +108,10 @@ class RoleQuerySet(query.QuerySet):
|
||||||
return self.filter(members=user).parents().distinct()
|
return self.filter(members=user).parents().distinct()
|
||||||
|
|
||||||
def parents(self, include_self=True, annotate=False):
|
def parents(self, include_self=True, annotate=False):
|
||||||
qs = self.model.objects.filter(child_relation__child__in=self)
|
qs = self.model.objects.filter(
|
||||||
|
child_relation__deleted__isnull=True,
|
||||||
|
child_relation__child__in=self,
|
||||||
|
)
|
||||||
if include_self:
|
if include_self:
|
||||||
qs = self | qs
|
qs = self | qs
|
||||||
qs = qs.distinct()
|
qs = qs.distinct()
|
||||||
|
@ -116,7 +120,10 @@ class RoleQuerySet(query.QuerySet):
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def children(self, include_self=True, annotate=False):
|
def children(self, include_self=True, annotate=False):
|
||||||
qs = self.model.objects.filter(parent_relation__parent__in=self)
|
qs = self.model.objects.filter(
|
||||||
|
parent_relation__deleted__isnull=True,
|
||||||
|
parent_relation__parent__in=self,
|
||||||
|
)
|
||||||
if include_self:
|
if include_self:
|
||||||
qs = self | qs
|
qs = self | qs
|
||||||
qs = qs.distinct()
|
qs = qs.distinct()
|
||||||
|
@ -128,7 +135,10 @@ class RoleQuerySet(query.QuerySet):
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
prefetch = Prefetch('roles', queryset=self, to_attr='direct')
|
prefetch = Prefetch('roles', queryset=self, to_attr='direct')
|
||||||
return (
|
return (
|
||||||
User.objects.filter(Q(roles__in=self) | Q(roles__parent_relation__parent__in=self))
|
User.objects.filter(
|
||||||
|
Q(roles__in=self)
|
||||||
|
| Q(roles__parent_relation__parent__in=self, roles__parent_relation__deleted__isnull=True)
|
||||||
|
)
|
||||||
.distinct()
|
.distinct()
|
||||||
.prefetch_related(prefetch)
|
.prefetch_related(prefetch)
|
||||||
)
|
)
|
||||||
|
@ -168,6 +178,29 @@ class RoleParentingManager(models.Manager):
|
||||||
raise self.model.DoesNotExist
|
raise self.model.DoesNotExist
|
||||||
return self.get(parent=parent, child=child, direct=direct)
|
return self.get(parent=parent, child=child, direct=direct)
|
||||||
|
|
||||||
|
def soft_create(self, parent, child):
|
||||||
|
with atomic(savepoint=False):
|
||||||
|
rp, created = self.get_or_create(parent=parent, child=child, direct=True)
|
||||||
|
new = created or rp.deleted
|
||||||
|
if not created and rp.deleted:
|
||||||
|
rp.created = datetime.datetime.now()
|
||||||
|
rp.deleted = None
|
||||||
|
rp.save(update_fields=['created', 'deleted'])
|
||||||
|
if new:
|
||||||
|
signals.post_soft_create.send(sender=self.model, instance=rp)
|
||||||
|
|
||||||
|
def soft_delete(self, parent, child):
|
||||||
|
from . import signals
|
||||||
|
|
||||||
|
qs = self.filter(parent=parent, child=child, deleted__isnull=True, direct=True)
|
||||||
|
with atomic(savepoint=False):
|
||||||
|
rp = qs.first()
|
||||||
|
if rp:
|
||||||
|
count = qs.update(deleted=datetime.datetime.now())
|
||||||
|
# read-commited, view of tables can change during transaction
|
||||||
|
if count:
|
||||||
|
signals.post_soft_delete.send(sender=self.model, instance=rp)
|
||||||
|
|
||||||
def update_transitive_closure(self):
|
def update_transitive_closure(self):
|
||||||
"""Recompute the transitive closure of the inheritance relation
|
"""Recompute the transitive closure of the inheritance relation
|
||||||
from scratch. Add missing indirect relations and delete
|
from scratch. Add missing indirect relations and delete
|
||||||
|
@ -179,8 +212,10 @@ class RoleParentingManager(models.Manager):
|
||||||
|
|
||||||
with atomic(savepoint=False):
|
with atomic(savepoint=False):
|
||||||
# existing direct paths
|
# existing direct paths
|
||||||
direct = set(self.filter(direct=True).values_list('parent_id', 'child_id'))
|
direct = set(self.filter(direct=True, deleted__isnull=True).values_list('parent_id', 'child_id'))
|
||||||
old_indirects = set(self.filter(direct=False).values_list('parent_id', 'child_id'))
|
old_indirects = set(
|
||||||
|
self.filter(direct=False, deleted__isnull=True).values_list('parent_id', 'child_id')
|
||||||
|
)
|
||||||
indirects = set(direct)
|
indirects = set(direct)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
@ -197,22 +232,26 @@ class RoleParentingManager(models.Manager):
|
||||||
# Delete old ones
|
# Delete old ones
|
||||||
obsolete = old_indirects - indirects - direct
|
obsolete = old_indirects - indirects - direct
|
||||||
if obsolete:
|
if obsolete:
|
||||||
obsolete_values = ', '.join('(%s, %s)' % (a, b) for a, b in obsolete)
|
sql = '''UPDATE "%s" AS relation \
|
||||||
sql = '''DELETE FROM "%s" AS relation \
|
SET deleted = now()\
|
||||||
USING (VALUES %s) AS dead(parent_id, child_id) \
|
FROM (VALUES %s) AS dead(parent_id, child_id) \
|
||||||
WHERE relation.direct = 'false' AND relation.parent_id = dead.parent_id \
|
WHERE relation.direct = 'false' AND relation.parent_id = dead.parent_id \
|
||||||
AND relation.child_id = dead.child_id''' % (
|
AND relation.child_id = dead.child_id AND deleted IS NULL''' % (
|
||||||
self.model._meta.db_table,
|
self.model._meta.db_table,
|
||||||
obsolete_values,
|
', '.join('(%s, %s)' % (a, b) for a, b in obsolete),
|
||||||
)
|
)
|
||||||
cur.execute(sql)
|
cur.execute(sql)
|
||||||
# Create new indirect relations
|
# Create new indirect relations
|
||||||
new = indirects - old_indirects - direct
|
new = indirects - old_indirects - direct
|
||||||
if new:
|
if new:
|
||||||
new_values = ', '.join(
|
new_values = ', '.join(
|
||||||
("(%s, %s, 'false')" % (parent_id, child_id) for parent_id, child_id in new)
|
(
|
||||||
|
"(%s, %s, 'false', now(), NULL)" % (parent_id, child_id)
|
||||||
|
for parent_id, child_id in new
|
||||||
|
)
|
||||||
)
|
)
|
||||||
sql = '''INSERT INTO "%s" (parent_id, child_id, direct) VALUES %s''' % (
|
sql = '''INSERT INTO "%s" (parent_id, child_id, direct, created, deleted) VALUES %s \
|
||||||
|
ON CONFLICT (parent_id, child_id, direct) DO UPDATE SET created = EXCLUDED.created, deleted = NULL''' % (
|
||||||
self.model._meta.db_table,
|
self.model._meta.db_table,
|
||||||
new_values,
|
new_values,
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Generated by Django 2.2.19 on 2021-10-06 10:34
|
||||||
|
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('django_rbac', '0007_add_unique_constraints'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='roleparenting',
|
||||||
|
name='created',
|
||||||
|
field=models.DateTimeField(
|
||||||
|
auto_now_add=True, default=django.utils.timezone.now, verbose_name='Creation date'
|
||||||
|
),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='roleparenting',
|
||||||
|
name='deleted',
|
||||||
|
field=models.DateTimeField(null=True, verbose_name='Deletion date'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -14,6 +14,7 @@ from django.db import models
|
||||||
from django.db.models.query import Prefetch, Q
|
from django.db.models.query import Prefetch, Q
|
||||||
from django.utils.translation import gettext
|
from django.utils.translation import gettext
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from model_utils.managers import QueryManager
|
||||||
|
|
||||||
from . import backends, constants, managers, utils
|
from . import backends, constants, managers, utils
|
||||||
|
|
||||||
|
@ -186,19 +187,19 @@ class RoleAbstractBase(AbstractOrganizationalUnitScopedBase, AbstractBase):
|
||||||
|
|
||||||
def add_child(self, child):
|
def add_child(self, child):
|
||||||
RoleParenting = utils.get_role_parenting_model()
|
RoleParenting = utils.get_role_parenting_model()
|
||||||
RoleParenting.objects.get_or_create(parent=self, child=child, direct=True)
|
RoleParenting.objects.soft_create(self, child)
|
||||||
|
|
||||||
def remove_child(self, child):
|
def remove_child(self, child):
|
||||||
RoleParenting = utils.get_role_parenting_model()
|
RoleParenting = utils.get_role_parenting_model()
|
||||||
RoleParenting.objects.filter(parent=self, child=child, direct=True).delete()
|
RoleParenting.objects.soft_delete(self, child)
|
||||||
|
|
||||||
def add_parent(self, parent):
|
def add_parent(self, parent):
|
||||||
RoleParenting = utils.get_role_parenting_model()
|
RoleParenting = utils.get_role_parenting_model()
|
||||||
RoleParenting.objects.get_or_create(parent=parent, child=self, direct=True)
|
RoleParenting.objects.soft_create(parent, self)
|
||||||
|
|
||||||
def remove_parent(self, parent):
|
def remove_parent(self, parent):
|
||||||
RoleParenting = utils.get_role_parenting_model()
|
RoleParenting = utils.get_role_parenting_model()
|
||||||
RoleParenting.objects.filter(child=self, parent=parent, direct=True).delete()
|
RoleParenting.objects.soft_delete(parent, self)
|
||||||
|
|
||||||
def parents(self, include_self=True, annotate=False):
|
def parents(self, include_self=True, annotate=False):
|
||||||
return self.__class__.objects.filter(pk=self.pk).parents(include_self=include_self, annotate=annotate)
|
return self.__class__.objects.filter(pk=self.pk).parents(include_self=include_self, annotate=annotate)
|
||||||
|
@ -211,8 +212,12 @@ class RoleAbstractBase(AbstractOrganizationalUnitScopedBase, AbstractBase):
|
||||||
def all_members(self):
|
def all_members(self):
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
prefetch = Prefetch('roles', queryset=self.__class__.objects.filter(pk=self.pk), to_attr='direct')
|
prefetch = Prefetch('roles', queryset=self.__class__.objects.filter(pk=self.pk), to_attr='direct')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
User.objects.filter(Q(roles=self) | Q(roles__parent_relation__parent=self))
|
User.objects.filter(
|
||||||
|
Q(roles=self)
|
||||||
|
| Q(roles__parent_relation__parent=self) & Q(roles__parent_relation__deleted__isnull=True)
|
||||||
|
)
|
||||||
.distinct()
|
.distinct()
|
||||||
.prefetch_related(prefetch)
|
.prefetch_related(prefetch)
|
||||||
)
|
)
|
||||||
|
@ -249,8 +254,11 @@ class RoleParentingAbstractBase(models.Model):
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
direct = models.BooleanField(default=True, blank=True)
|
direct = models.BooleanField(default=True, blank=True)
|
||||||
|
created = models.DateTimeField(verbose_name=_('Creation date'), auto_now_add=True)
|
||||||
|
deleted = models.DateTimeField(verbose_name=_('Deletion date'), null=True)
|
||||||
|
|
||||||
objects = managers.RoleParentingManager()
|
objects = managers.RoleParentingManager()
|
||||||
|
alive = QueryManager(deleted__isnull=True)
|
||||||
|
|
||||||
def natural_key(self):
|
def natural_key(self):
|
||||||
return [self.parent.natural_key(), self.child.natural_key(), self.direct]
|
return [self.parent.natural_key(), self.child.natural_key(), self.direct]
|
||||||
|
|
|
@ -19,6 +19,11 @@ def role_parenting_post_delete(sender, instance, **kwargs):
|
||||||
sender.objects.update_transitive_closure()
|
sender.objects.update_transitive_closure()
|
||||||
|
|
||||||
|
|
||||||
|
def role_parenting_post_soft_delete(sender, instance, **kwargs):
|
||||||
|
'''Close the role parenting relation after instance soft-deletion'''
|
||||||
|
sender.objects.update_transitive_closure()
|
||||||
|
|
||||||
|
|
||||||
def create_base_operations(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, **kwargs):
|
def create_base_operations(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, **kwargs):
|
||||||
'''Create some basic operations, matching permissions from Django'''
|
'''Create some basic operations, matching permissions from Django'''
|
||||||
if not router.allow_migrate(using, models.Operation):
|
if not router.allow_migrate(using, models.Operation):
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
from django import dispatch
|
||||||
|
|
||||||
|
# update role parenting transitive closure when role parenting is deleted
|
||||||
|
post_soft_create = dispatch.Signal(providing_args=['instance'])
|
||||||
|
post_soft_delete = dispatch.Signal(providing_args=['instance'])
|
|
@ -41,7 +41,7 @@ def test_role_parenting(db):
|
||||||
assert Role.objects.count() == 10
|
assert Role.objects.count() == 10
|
||||||
assert RoleParenting.objects.count() == 0
|
assert RoleParenting.objects.count() == 0
|
||||||
for i in range(1, 3):
|
for i in range(1, 3):
|
||||||
RoleParenting.objects.create(parent=roles[i - 1], child=roles[i])
|
RoleParenting.objects.soft_create(parent=roles[i - 1], child=roles[i])
|
||||||
assert RoleParenting.objects.filter(direct=True).count() == 2
|
assert RoleParenting.objects.filter(direct=True).count() == 2
|
||||||
assert RoleParenting.objects.filter(direct=False).count() == 1
|
assert RoleParenting.objects.filter(direct=False).count() == 1
|
||||||
for i, role in enumerate(roles[:3]):
|
for i, role in enumerate(roles[:3]):
|
||||||
|
@ -59,7 +59,7 @@ def test_role_parenting(db):
|
||||||
assert role.parents().count() == i + 1
|
assert role.parents().count() == i + 1
|
||||||
assert role.children(False).count() == 3 - i - 1
|
assert role.children(False).count() == 3 - i - 1
|
||||||
assert role.parents(False).count() == i
|
assert role.parents(False).count() == i
|
||||||
RoleParenting.objects.create(parent=roles[2], child=roles[3])
|
RoleParenting.objects.soft_create(parent=roles[2], child=roles[3])
|
||||||
assert RoleParenting.objects.filter(direct=True).count() == 5
|
assert RoleParenting.objects.filter(direct=True).count() == 5
|
||||||
assert RoleParenting.objects.filter(direct=False).count() == 10
|
assert RoleParenting.objects.filter(direct=False).count() == 10
|
||||||
for i in range(6):
|
for i in range(6):
|
||||||
|
@ -69,17 +69,79 @@ def test_role_parenting(db):
|
||||||
assert role.parents().count() == i + 1
|
assert role.parents().count() == i + 1
|
||||||
assert role.children(False).count() == 6 - i - 1
|
assert role.children(False).count() == 6 - i - 1
|
||||||
assert role.parents(False).count() == i
|
assert role.parents(False).count() == i
|
||||||
RoleParenting.objects.filter(parent=roles[2], child=roles[3], direct=True).delete()
|
RoleParenting.objects.soft_delete(roles[2], roles[3])
|
||||||
assert RoleParenting.objects.filter(direct=True).count() == 4
|
assert (
|
||||||
assert RoleParenting.objects.filter(direct=False).count() == 2
|
RoleParenting.objects.filter(
|
||||||
|
direct=True,
|
||||||
|
deleted__isnull=True,
|
||||||
|
).count()
|
||||||
|
== 4
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
RoleParenting.objects.filter(
|
||||||
|
direct=False,
|
||||||
|
deleted__isnull=True,
|
||||||
|
).count()
|
||||||
|
== 2
|
||||||
|
)
|
||||||
# test that it works with cycles
|
# test that it works with cycles
|
||||||
RoleParenting.objects.create(parent=roles[2], child=roles[3])
|
RoleParenting.objects.soft_create(parent=roles[2], child=roles[3])
|
||||||
RoleParenting.objects.create(parent=roles[5], child=roles[0])
|
RoleParenting.objects.soft_create(parent=roles[5], child=roles[0])
|
||||||
for role in roles[:6]:
|
for role in roles[:6]:
|
||||||
assert role.children().count() == 6
|
assert role.children().count() == 6
|
||||||
assert role.parents().count() == 6
|
assert role.parents().count() == 6
|
||||||
|
|
||||||
|
|
||||||
|
def test_role_parenting_soft_delete_children(db):
|
||||||
|
OrganizationalUnit = utils.get_ou_model()
|
||||||
|
Role = utils.get_role_model()
|
||||||
|
RoleParenting = utils.get_role_parenting_model()
|
||||||
|
|
||||||
|
ou = OrganizationalUnit.objects.create(name='ou')
|
||||||
|
roles = []
|
||||||
|
for i in range(10):
|
||||||
|
roles.append(Role.objects.create(name='r%d' % i, ou=ou))
|
||||||
|
assert not len(RoleParenting.objects.all())
|
||||||
|
|
||||||
|
rps = []
|
||||||
|
for i in range(5):
|
||||||
|
rps.append(RoleParenting.objects.soft_create(parent=roles[9 - i], child=roles[i]))
|
||||||
|
assert len(RoleParenting.objects.all()) == 5
|
||||||
|
for i in range(5):
|
||||||
|
roles[9 - i].remove_child(roles[i])
|
||||||
|
assert len(RoleParenting.objects.all()) == 5
|
||||||
|
assert len(RoleParenting.objects.filter(deleted__isnull=True).all()) == 4 - i
|
||||||
|
for i in range(5):
|
||||||
|
roles[9 - i].add_child(roles[i])
|
||||||
|
assert len(RoleParenting.objects.all()) == 5
|
||||||
|
assert len(RoleParenting.objects.filter(deleted__isnull=True).all()) == i + 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_role_parenting_soft_delete_parents(db):
|
||||||
|
OrganizationalUnit = utils.get_ou_model()
|
||||||
|
Role = utils.get_role_model()
|
||||||
|
RoleParenting = utils.get_role_parenting_model()
|
||||||
|
|
||||||
|
ou = OrganizationalUnit.objects.create(name='ou')
|
||||||
|
roles = []
|
||||||
|
for i in range(10):
|
||||||
|
roles.append(Role.objects.create(name='r%d' % i, ou=ou))
|
||||||
|
assert not len(RoleParenting.objects.all())
|
||||||
|
|
||||||
|
rps = []
|
||||||
|
for i in range(5):
|
||||||
|
rps.append(RoleParenting.objects.soft_create(child=roles[9 - i], parent=roles[i]))
|
||||||
|
assert len(RoleParenting.objects.all()) == 5
|
||||||
|
for i in range(5):
|
||||||
|
roles[9 - i].remove_parent(roles[i])
|
||||||
|
assert len(RoleParenting.objects.all()) == 5
|
||||||
|
assert len(RoleParenting.objects.filter(deleted__isnull=True).all()) == 4 - i
|
||||||
|
for i in range(5):
|
||||||
|
roles[9 - i].add_parent(roles[i])
|
||||||
|
assert len(RoleParenting.objects.all()) == 5
|
||||||
|
assert len(RoleParenting.objects.filter(deleted__isnull=True).all()) == i + 1
|
||||||
|
|
||||||
|
|
||||||
SIZE = 1000
|
SIZE = 1000
|
||||||
SPAN = 50
|
SPAN = 50
|
||||||
|
|
||||||
|
@ -255,7 +317,9 @@ def test_random_role_parenting(db):
|
||||||
break
|
break
|
||||||
z = new_z
|
z = new_z
|
||||||
real = np.zeros((c, c), dtype=bool)
|
real = np.zeros((c, c), dtype=bool)
|
||||||
for parent_id, child_id in RoleParenting.objects.values_list('parent_id', 'child_id'):
|
for parent_id, child_id in RoleParenting.objects.filter(deleted__isnull=True).values_list(
|
||||||
|
'parent_id', 'child_id'
|
||||||
|
):
|
||||||
real[parent_id][child_id] = True
|
real[parent_id][child_id] = True
|
||||||
assert np.array_equal(real, z & ~one)
|
assert np.array_equal(real, z & ~one)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue