def paired_object_safe_to_delete(base_object):
    collector = NestedObjects(using='default')
    collector.collect([base_object])
    collected = collector.nested()
    if len(collected) > 2:
        return False
    assert collected[0] == base_object
    if len(collected) == 1:
        return True
    if len(collected[1]) != 1:
        return False
    assert collected[1][0] == base_object.extra
    return True
 def cascade_restore(self, collection=None):
     if not collection:
         collector = NestedObjects(using=DEFAULT_DB_ALIAS)
         collector.collect(self)
         collection = collector.nested()
     try:
         collection.undelete()
     except AttributeError:
         try:
             for x in collection:
                 self.cascade_restore(x)
         except TypeError:
             pass
def exclude_item(obj, stdout):
    collector = NestedObjects(using="default")
    collector.collect([obj])
    stdout.write("Os seguintes objetos serão excluídos:")
    for item in collector.nested():
        str_item(item, stdout)
    stdout.write("Os seguintes objetos impedem a exclusão:")
    for item in collector.protected:
        str_item(item, stdout)
    continuar = input("Deseja continuar: [s/n] ")
    if continuar == "s":
        stdout.write(str(obj.delete()))
        stdout.write("Excluído com sucesso")
Example #4
0
def get_related_objects(obj, using=DEFAULT_DB_ALIAS):
    # This code is based on https://github.com/makinacorpus/django-safedelete
    collector = NestedObjects(using=using)
    collector.collect([obj])

    def flatten(elem):
        if isinstance(elem, list):
            return itertools.chain.from_iterable(map(flatten, elem))
        elif obj != elem:
            return (elem, )
        return ()

    return flatten(collector.nested())
Example #5
0
    def get_translatable_children(self, obj):
        """
        Obtain all the translatable children from "obj"

        :param obj:
        :return:
        """
        collector = NestedObjects(using='default')
        collector.collect([obj])
        object_list = collector.nested()
        items = self.get_elements(object_list)
        # avoid first object because it's the main object
        return items[1:]
Example #6
0
    def get_info_before_delete_user(self,
                                    remove_sounds=False,
                                    remove_user=False):
        """
        This method can be called before delete_user to display to the user the
        elements that will be modified
        """

        ret = {}
        if remove_sounds:
            sounds = Sound.objects.filter(user=self.user)
            packs = Pack.objects.filter(user=self.user)
            collector = NestedObjects(using='default')
            collector.collect(sounds)
            ret['deleted'] = collector
            ret['logic_deleted'] = packs
        if remove_user:
            collector = NestedObjects(using='default')
            collector.collect([self.user])
            ret['deleted'] = collector
        ret['anonymised'] = self
        return ret
Example #7
0
    def get_deleted_objects(self):
        collector = NestedObjects(using=router.db_for_write(self.model))
        collector.collect([self.object])
        perms_needed = set()

        def format_callback(obj):

            p = '%s.%s' % (
                obj._meta.app_label,
                get_permission_codename('delete', obj._meta)
            )

            if not self.request.user.has_perm(p):
                perms_needed.add(obj._meta.verbose_name)

            registered = obj.__class__ in self.request.djangobmf_site.modules

            # only show bmf modules
            if not registered:
                return None

            return format_html(
                '{0}: {1}',
                obj._meta.verbose_name,
                obj
            )

        def format_protected_callback(obj):

        #   if obj.__class__ in self.request.djangobmf_site.modules and not obj._bmfmeta.only_related:
        #       return format_html(
        #           '{0}: <a href="{1}">{2}</a>',
        #           obj._meta.verbose_name,
        #           obj.bmfmodule_detail(),
        #           obj
        #       )
        #   else:
            return format_html(
                '{0}: {1}',
                obj._meta.verbose_name,
                obj
            )

        to_delete = collector.nested(format_callback)

        protected = [
            format_protected_callback(obj) for obj in collector.protected
        ]

        return to_delete, perms_needed, protected
    def safe_delete(self, model):
        collector = NestedObjects(using=connection.cursor().db.alias)
        collector.collect([model])
        if len(collector.nested()) > 1:
            related_objects = "\n\t".join(
                [repr(m) for m in collector.nested()[1:]]
            )
            raise UnsafeToDelete(
                "Can't delete '{}' with related objects: \n {}".format(
                    model, related_objects
                )
            )

        return model.delete()
Example #9
0
    def get_context_data(self, **kwargs):
        context = super(BaseDeleteView, self).get_context_data(**kwargs)

        collector = NestedObjects(using='default')
        collector.collect([self.get_object()])
        context['deleted_objects'] = collector.nested()

        if self.model:
            context['model_name'] = self.model._meta.verbose_name.title()
            context[
                'model_name_plural'] = self.model._meta.verbose_name_plural.title(
                )

        return context
Example #10
0
def related_objects(obj):
    """ Return a generator to the objects that would be deleted if we delete "obj" (excluding obj) """

    collector = NestedObjects(using=router.db_for_write(obj))
    collector.collect([obj])

    def flatten(elem):
        if isinstance(elem, list):
            return itertools.chain.from_iterable(map(flatten, elem))
        elif obj != elem:
            return (elem, )
        return ()

    return flatten(collector.nested())
Example #11
0
    def get_deleted_objects(self, objs, user, using='default'):
        """
        Find all objects related to ``objs`` that should also be deleted. ``objs``
        must be a homogeneous iterable of objects (e.g. a QuerySet).

        Return a nested list of strings suitable for display in the
        template with the ``unordered_list`` filter.

        Encontre todos os objetos relacionados a ``objs`` que também devem ser deletados. ``objs``
         deve ser um iterável homogêneo de objetos (por exemplo, um QuerySet).

         Retornar uma lista aninhada de sequências adequadas para exibição no
         template com o filtro `` unordered_list``.
        """
        collector = NestedObjects(using=using)
        collector.collect(objs)
        perms_needed = set()

        def format_callback(obj):
            opts = obj._meta

            no_edit_link = '%s: %s' % (str(opts.verbose_name).title(), obj)

            try:
                url = reverse('%s:%s-update'% (
                                  opts.app_label,
                                  opts.model_name),
                              None, (quote(obj.pk),))


            except NoReverseMatch:
                # Change url doesn't exist -- don't display link to edit
                return no_edit_link

            p = '%s.%s' % (opts.app_label, get_permission_codename('delete', opts))
            if not user.has_perm(p):
                perms_needed.add(opts.verbose_name.title())
            # Display a link to the admin page.
            return format_html('{}: <a href="{}">{}</a>',
                               str(opts.verbose_name).title(),
                               url,
                               obj)

        to_delete = collector.nested(format_callback)

        protected = [format_callback(obj) for obj in collector.protected]
        model_count = {model._meta.verbose_name_plural: len(objs) for model, objs in collector.model_objs.items()}

        return perms_needed, protected
Example #12
0
def get_deleted_objects(objs):
    collector = NestedObjects(using='default')
    collector.collect([objs])

    def format_callback(obj):
        opts = obj._meta
        no_edit_link = '%s: %s' % (capfirst(opts.verbose_name),
                                   force_text(obj))
        return no_edit_link

    # to_delete = collector.nested(format_callback)
    to_delete = collector.nested()
    protected = [format_callback(obj) for obj in collector.protected]
    model_count = {model._meta.verbose_name_plural: len(objs) for model, objs in collector.model_objs.items()}
    return to_delete, model_count, protected
Example #13
0
def get_deleted_objects(objs, request, admin_site):
    """
    Patched django/contrib/admin/utils.py
    to skip collecting links for related nested objects
    """
    try:
        obj = objs[0]
    except IndexError:
        return [], {}, set(), []
    else:
        using = router.db_for_write(obj._meta.model)
    collector = NestedObjects(using=using)
    collector.collect(objs)
    model_count = {model._meta.verbose_name_plural: len(objs) for model, objs in collector.model_objs.items()}
    to_delete = ['{}: {}'.format(cap_words(k), v) for k, v in model_count.items()]
    return to_delete, model_count, None, None
Example #14
0
def get_protected_related(entity):
    """ Return a list of objects that prevent this entity from being deleted """

    using = router.db_for_write(entity)
    collector = NestedObjects(using=using)
    collector.collect([entity])

    def callable_func(obj):
        data = {
            'pk': obj.pk,
            'verbose_name': obj._meta.verbose_name,
            'verbose_name_plural': str(obj._meta.verbose_name_plural)
        }
        return data

    return [callable_func(o) for o in collector.protected]
Example #15
0
def delete_engagement(request, eid):
    engagement = get_object_or_404(Engagement, pk=eid)
    product = engagement.product
    form = DeleteEngagementForm(instance=engagement)

    if request.method == 'POST':
        if 'id' in request.POST and str(engagement.id) == request.POST['id']:
            form = DeleteEngagementForm(request.POST, instance=engagement)
            if form.is_valid():
                del engagement.tags
                engagement.delete()
                messages.add_message(request,
                                     messages.SUCCESS,
                                     'Engagement and relationships removed.',
                                     extra_tags='alert-success')
                create_notification(
                    event='other',
                    title='Deletion of %s' % engagement.name,
                    description='The engagement "%s" was deleted by %s' %
                    (engagement.name, request.user),
                    url=request.build_absolute_uri(
                        reverse('view_engagements', args=(product.id, ))),
                    recipients=[engagement.lead],
                    icon="exclamation-triangle")

                if engagement.engagement_type == 'CI/CD':
                    return HttpResponseRedirect(
                        reverse("view_engagements_cicd", args=(product.id, )))
                else:
                    return HttpResponseRedirect(
                        reverse("view_engagements", args=(product.id, )))

    collector = NestedObjects(using=DEFAULT_DB_ALIAS)
    collector.collect([engagement])
    rels = collector.nested()

    product_tab = Product_Tab(product.id,
                              title="Delete Engagement",
                              tab="engagements")
    product_tab.setEngagement(engagement)
    return render(
        request, 'dojo/delete_engagement.html', {
            'product_tab': product_tab,
            'engagement': engagement,
            'form': form,
            'rels': rels,
        })
Example #16
0
def delete_data():
    for user in User.objects.filter(id__in=[
            602, 654, 616, 611, 577, 613, 424, 580, 598, 574, 590, 579, 581,
            588, 575, 645, 626, 576, 597, 615, 637, 50, 13, 181, 592, 552, 559,
            189, 163, 641, 620, 558, 591, 638, 634, 636, 627, 642, 628, 612,
            609, 560, 630, 646, 639, 578, 551, 564, 606, 38, 46, 583, 589, 557,
            656, 655, 633, 566, 610, 595, 562, 619, 596, 607, 649, 650, 644,
            586, 198, 553, 197, 568, 572, 561, 632, 622, 608, 621, 605, 569,
            618, 617, 570, 640, 402, 584, 593, 624, 603, 623, 582, 550, 635,
            563, 601, 585, 600, 652, 27, 565, 614, 599, 648, 464, 555, 554,
            567, 643, 604, 629, 631, 556, 653, 571, 587, 573, 15, 14
    ]):
        # for user in User.objects.filter(username='******'):
        collector = NestedObjects(using='default')  # or specific database
        collector.collect([user])
        to_delete = collector.nested()
        print(to_delete, user.profile.name, user.profile.phone)
Example #17
0
    def process_preview(self, request, form, context):

        if "_delete" in request.POST:
            context['action'] = "_delete"

            def format_callback(obj):
                return '%s: %s' % (capfirst(obj._meta.verbose_name), obj)

            ps = form.cleaned_data['obj_list']
            collector = NestedObjects(using='default')  # or specific database
            collector.collect(ps)
            to_delete = collector.nested(format_callback)

            context['objs'] = to_delete

        else:
            self.preview_actions(request, form, context)
Example #18
0
    def xls_export(self, _models, root=None, root_qs=None):
        if (root and root_qs) or ((root or root_qs) is None):
            raise RuntimeError(
                _("Either a root object or a root queryset must be provided"))

        workbook = None
        try:
            workbookfile = self.dest or NamedTemporaryFile(dir=self.tmpdir,
                                                           delete=False)
            workbook = Workbook()
            del workbook['Sheet']

            sheets = {}

            lmodels = {}
            for k, v in _models.items():
                lname = k.lower()
                model_name = lname.rsplit('.')[1]
                lmodels[lname] = v
                sheets[model_name] = workbook.create_sheet(title=model_name)
                sheets[model_name].append(v)

            if root:
                root_qs = root._meta.model.objects.filter(pk=root.pk)

            using = router.db_for_write(root_qs.first()._meta.model)
            collector = NestedObjects(using=using)
            collector.collect(root_qs)

            def callback(obj):
                fields = lmodels.get(obj._meta.label_lower, None)
                if fields:
                    sheets[obj._meta.model_name].append(
                        [getattr(obj, x) for x in fields])

            collector.nested(callback)
            workbook.save(workbookfile)
            return workbookfile.name

        except Exception as e:
            if workbook:
                if not workbookfile.closed:
                    workbookfile.close()
                if os.path.exists(workbookfile.name):
                    os.remove(workbookfile.name)
            raise e
Example #19
0
 def delete_queryset(self, request, queryset):
     # this method is called by delete_selected and can be overridden
     try:
         obj = queryset[0]
     except IndexError:
         return
     else:
         using = router.db_for_write(obj._meta.model)
     collector = NestedObjects(using=using)
     collector.collect(queryset)
     to_delete = []
     for item, value in collector.model_objs.items():
         to_delete += value
     to_delete += collector.protected
     # since we are only performing soft delete, we must soft_delete related objects too, if possible
     for obj in to_delete:
         if hasattr(obj, 'soft_delete'):
             obj.soft_delete()
Example #20
0
def remove_dupes(apps, schema_editor):
    Person = apps.get_model("people", "Person")
    Leaflet = apps.get_model("leaflets", "Leaflet")
    qs = (Person.objects.values("remote_id").annotate(
        count=Count("remote_id")).filter(count__gt=1))
    for person_dict in qs:
        print(person_dict)
        models = Person.objects.filter(remote_id=person_dict["remote_id"])
        keep_person = models[0]
        dupes = models[1:]
        for person in dupes:
            person.leaflet_set.update(publisher_person=keep_person)
            collector = NestedObjects(using="default")
            collector.collect([person])
            collected = collector.nested()
            assert len(collected) < 2 or not any(
                [isinstance(x, Leaflet) for x in collected[1]])
            person.delete()
Example #21
0
def serialize_public_draws(file_):
    backup_cutoff = dt.datetime.now(dt.timezone.utc) - dt.timedelta(days=10)
    backup_ids = set()
    collector = NestedObjects(using="default")
    for result in models.Result.objects.filter(
            schedule_date__gt=backup_cutoff):
        draw = result.draw
        if draw.id in backup_ids:
            continue
        collector.collect([draw])
        backup_ids.add(draw.id)
    for ss in models.SecretSantaResult.objects.filter(
            created_at__gt=backup_cutoff):
        collector.collect([ss])
    for payment in models.Payment.objects.filter(created_at__gt=backup_cutoff):
        collector.collect([payment])
    all_draw_objects = itertools.chain.from_iterable(collector.data.values())
    serializers.serialize("json", all_draw_objects, stream=file_)
Example #22
0
def get_delete_cascade(request, title):
    document = get_object_or_404(Document, url_title=title)
    check_permissions(document, request.user, [document.edit_permission_name])

    collector = NestedObjects(using=DEFAULT_DB_ALIAS)
    collector.collect([document])
    delete_cascade = collector.nested()

    # remove all subclasses of current document from the list because that does not add much helpful information
    simplified_delete_cascade = []
    for cascade_item in delete_cascade:
        if issubclass(type(document), type(
                cascade_item)) and not type(document) == type(cascade_item):
            continue
        simplified_delete_cascade.append(cascade_item)

    return HttpResponse(
        json.dumps(delete_cascade_to_json(simplified_delete_cascade)))
Example #23
0
def delete_test(request, tid):
    test = get_object_or_404(Test, pk=tid)
    eng = test.engagement
    form = DeleteTestForm(instance=test)

    if request.method == 'POST':
        if 'id' in request.POST and str(test.id) == request.POST['id']:
            form = DeleteTestForm(request.POST, instance=test)
            if form.is_valid():
                product = test.engagement.product
                test.delete()
                messages.add_message(request,
                                     messages.SUCCESS,
                                     'Test and relationships removed.',
                                     extra_tags='alert-success')
                create_notification(
                    event='other',
                    title='Deletion of %s' % test.title,
                    product=product,
                    description='The test "%s" was deleted by %s' %
                    (test.title, request.user),
                    url=request.build_absolute_uri(
                        reverse('view_engagement', args=(eng.id, ))),
                    recipients=[test.engagement.lead],
                    icon="exclamation-triangle")
                return HttpResponseRedirect(
                    reverse('view_engagement', args=(eng.id, )))

    collector = NestedObjects(using=DEFAULT_DB_ALIAS)
    collector.collect([test])
    rels = collector.nested()

    product_tab = Product_Tab(test.engagement.product.id,
                              title="Delete Test",
                              tab="engagements")
    product_tab.setEngagement(test.engagement)
    return render(
        request, 'dojo/delete_test.html', {
            'test': test,
            'product_tab': product_tab,
            'form': form,
            'rels': rels,
            'deletable_objects': rels,
        })
Example #24
0
def get_deleted_objects(objs, user):
    """
    Find all objects related to ``objs`` that should also be deleted. ``objs``
    must be a homogeneous iterable of objects (e.g. a QuerySet).

    Returns a nested list of strings suitable for display in the
    template with the ``unordered_list`` filter.

    Copied and updated from django.contrib.admin.utils for front end display.
    """
    using = router.db_for_write(objs[0].__class__)
    collector = NestedObjects(using=using)
    collector.collect(objs)
    perms_needed = set()

    def format_callback(obj):
        opts = obj._meta

        no_edit_link = '%s: %s' % (capfirst(
            opts.verbose_name), force_text(obj))

        p = '%s.%s' % (opts.app_label, get_permission_codename('delete', opts))
        if not user.has_perm(p):
            perms_needed.add(opts.verbose_name)

        if hasattr(obj, 'get_absolute_url'):
            url = obj.get_absolute_url()
            # Display a link to the admin page.
            return format_html('{}: <a href="{}">{}</a>',
                               capfirst(opts.verbose_name), url, obj)
        else:
            # Don't display link to edit, because it either has no
            # admin or is edited inline.
            return no_edit_link

    to_delete = collector.nested(format_callback)

    protected = [format_callback(obj) for obj in collector.protected]
    model_count = {
        model._meta.verbose_name_plural: len(objs)
        for model, objs in collector.model_objs.items()
    }

    return to_delete, model_count, perms_needed, protected
Example #25
0
def get_deleted_objects(objs, request, admin_site):
    """
    Find all objects related to ``objs`` that should also be deleted. ``objs``
    must be a homogeneous iterable of objects (e.g. a QuerySet).

    Return a nested list of strings suitable for display in the
    template with the ``unordered_list`` filter.
    """
    try:
        obj = objs[0]
    except IndexError:
        return [], {}, set(), []
    else:
        using = router.db_for_write(obj._meta.model)
    collector = NestedObjects(using=using)
    collector.collect(objs)
    perms_needed = set()

    def format_callback(obj):
        model = obj.__class__
        has_admin = model in admin_site._registry
        opts = obj._meta

        no_edit_link = '%s: %s' % (capfirst(opts.verbose_name), obj)

        if has_admin:
            # Display a link to the admin page.
            return format_html('{}: <a href="{}">{}</a>',
                               capfirst(opts.verbose_name),
                               obj.get_absolute_url, obj)
        else:
            # Don't display link to edit, because it either has no
            # admin or is edited inline.
            return no_edit_link

    to_delete = collector.nested(format_callback)

    protected = [format_callback(obj) for obj in collector.protected]
    model_count = {
        model._meta.verbose_name_plural: len(objs)
        for model, objs in collector.model_objs.items()
    }

    return to_delete, model_count, perms_needed, protected
Example #26
0
def delete_jira(request, tid):
    jira_instance = get_object_or_404(JIRA_Instance, pk=tid)
    # eng = test.engagement
    # TODO Make Form
    form = DeleteJIRAInstanceForm(instance=jira_instance)

    if request.method == 'POST':
        if 'id' in request.POST and str(
                jira_instance.id) == request.POST['id']:
            form = DeleteJIRAInstanceForm(request.POST, instance=jira_instance)
            if form.is_valid():
                try:
                    jira_instance.delete()
                    messages.add_message(
                        request,
                        messages.SUCCESS,
                        'JIRA Conf and relationships removed.',
                        extra_tags='alert-success')
                    create_notification(
                        event='other',
                        title='Deletion of JIRA: %s' %
                        jira_instance.configuration_name,
                        description='JIRA "%s" was deleted by %s' %
                        (jira_instance.configuration_name, request.user),
                        url=request.build_absolute_uri(reverse('jira')),
                    )
                    return HttpResponseRedirect(reverse('jira'))
                except Exception as e:
                    add_error_message_to_response(
                        'Unable to delete JIRA Instance, probably because it is used by JIRA Issues: %s'
                        % str(e))

    collector = NestedObjects(using=DEFAULT_DB_ALIAS)
    collector.collect([jira_instance])
    rels = collector.nested()

    add_breadcrumb(title="Delete", top_level=False, request=request)
    return render(
        request, 'dojo/delete_jira.html', {
            'inst': jira_instance,
            'form': form,
            'rels': rels,
            'deletable_objects': rels,
        })
Example #27
0
def soft_delete_selected(modeladmin, request, queryset):
    """
    In charge of deactivating instances instead of deleting them
    """
    opts = modeladmin.model._meta
    using = router.db_for_write(modeladmin.model)
    count = queryset.count()

    if not count:
        return None

    # soft delete selected items
    if hasattr(queryset, 'soft_delete'):
        queryset.soft_delete()
    else:
        for obj in queryset:
            soft_delete_item = getattr(obj, 'soft_delete', None)
            if not soft_delete_item:
                modeladmin.message_user(
                    request,
                    _("{verbose_name_plural} has not soft delete available.".
                      format({"verbose_name_plural":
                              opts.verbose_name_plural})), messages.SUCCESS)
                return None
            soft_delete_item()

    # soft delete nested items
    collector = NestedObjects(using=using)
    collector.collect(queryset)
    for model_base, nested_objects_collected in collector.data.items():
        if model_base == modeladmin.model or not hasattr(
                model_base, 'soft_delete'):
            continue

        for nested_object in nested_objects_collected:
            nested_object.soft_delete()

    # response success message
    modeladmin.message_user(
        request,
        _("Successfully soft deleted {count} {items}.".format(
            count=count, items=model_ngettext(modeladmin.opts, count))),
        messages.SUCCESS)
    return None
Example #28
0
def delete_finding_group(request, fgid):
    logger.debug('delete finding group: %s', fgid)
    finding_group = get_object_or_404(Finding_Group, pk=fgid)
    form = DeleteFindingGroupForm(instance=finding_group)

    if request.method == 'POST':
        if 'id' in request.POST and str(
                finding_group.id) == request.POST['id']:
            form = DeleteFindingGroupForm(request.POST, instance=finding_group)
            if form.is_valid():
                product = finding_group.test.engagement.product
                finding_group.delete()
                messages.add_message(
                    request,
                    messages.SUCCESS,
                    'Finding Group and relationships removed.',
                    extra_tags='alert-success')

                create_notification(
                    event='other',
                    title='Deletion of %s' % finding_group.name,
                    product=product,
                    description='The finding group "%s" was deleted by %s' %
                    (finding_group.name, request.user),
                    url=request.build_absolute_uri(
                        reverse('view_test', args=(finding_group.test.id, ))),
                    icon="exclamation-triangle")
                return HttpResponseRedirect(
                    reverse('view_test', args=(finding_group.test.id, )))

    collector = NestedObjects(using=DEFAULT_DB_ALIAS)
    collector.collect([finding_group])
    rels = collector.nested()
    product_tab = Product_Tab(finding_group.test.engagement.product.id,
                              title="Product",
                              tab="settings")

    return render(
        request, 'dojo/delete_finding_group.html', {
            'finding_group': finding_group,
            'form': form,
            'product_tab': product_tab,
            'rels': rels,
        })
Example #29
0
    def _get_protected(self):
        using = router.db_for_write(self.object)
        collector = NestedObjects(using=using)
        collector.collect([self.object])

        def callable_func(obj):
            meta = obj._meta
            data = {
                'object':
                obj,
                'verbose_name':
                obj._meta.verbose_name,
                'has_permission':
                self.request.user.has_perm('%s.delete_%s' %
                                           (meta.app_label, meta.model_name))
            }
            return data

        return [callable_func(o) for o in collector.protected]
    def soft_delete_cascade_policy_action(self, **kwargs):
        # Soft-delete on related objects before
        for related in related_objects(self):
            if is_safedelete_cls(related.__class__) and not getattr(related, FIELD_NAME):
                related.delete(force_policy=SOFT_DELETE, **kwargs)

        # soft-delete the object
        self._delete(force_policy=SOFT_DELETE, **kwargs)

        collector = NestedObjects(using=router.db_for_write(self))
        collector.collect([self])
        # update fields (SET, SET_DEFAULT or SET_NULL)
        for model, instances_for_fieldvalues in collector.field_updates.items():
            for (field, value), instances in instances_for_fieldvalues.items():
                query = models.sql.UpdateQuery(model)
                query.update_batch(
                    [obj.pk for obj in instances],
                    {field.name: value},
                    collector.using,
                )