Подтвердить что ты не робот

Запретить удаление в модели Django

У меня есть такая настройка (упрощенная для этого вопроса):

class Employee(models.Model):
    name = models.CharField(name, unique=True)

class Project(models.Model):
    name = models.CharField(name, unique=True)
    employees = models.ManyToManyField(Employee)

Когда сотрудник будет удален, я хочу проверить, подключен ли он к каким-либо проектам. Если это так, удаление должно быть невозможным.

Я знаю о сигналах и как их обрабатывать. Я могу подключиться к сигналу pre_delete и заставить его генерировать исключение, например ValidationError. Это предотвращает удаление, но оно не обрабатывается изящно формами и т.д.

Это похоже на ситуацию, с которой столкнулись другие. Я надеюсь, что кто-то может указать на более элегантное решение.

4b9b3361

Ответ 1

Я искал ответ на эту проблему, не смог найти хороший, который будет работать для обеих моделей. Model.delete() и QuerySet.delete(). Я пошел и, вроде, выполнил решение Стива К. Я использовал это решение, чтобы убедиться, что объект (Employee в этом примере) не может быть удален из базы данных в любом случае, но установлен в неактивный.

Это поздний ответ. Просто ради других людей, которые ищут, я прикладываю свое решение здесь.

Вот код:

class CustomQuerySet(QuerySet):
    def delete(self):
        self.update(active=False)


class ActiveManager(models.Manager):
    def active(self):
        return self.model.objects.filter(active=True)

    def get_queryset(self):
        return CustomQuerySet(self.model, using=self._db)


class Employee(models.Model):
    name = models.CharField(name, unique=True)
    active = models.BooleanField(default=True, editable=False)

    objects = ActiveManager()

    def delete(self):
        self.active = False
        self.save()

Использование:

Employee.objects.active() # use it just like you would .all()

или в admin:

class Employee(admin.ModelAdmin):

    def queryset(self, request):
        return super(Employee, self).queryset(request).filter(active=True)

Ответ 2

Если вы знаете, что никаких попыток удаления массовых сотрудников никогда не будет, вы можете просто переопределить delete в своей модели и вызвать только super, если это законная операция.

К сожалению, все, что может называть queryset.delete(), перейдет прямо к SQL: http://docs.djangoproject.com/en/dev/topics/db/queries/#deleting-objects

Но я не вижу такой проблемы, потому что вы тот, кто пишет этот код, и можете гарантировать, что никогда не будет queryset.delete() для сотрудников. Вызовите delete() вручную.

Я надеюсь, что удаление сотрудников относительно редко.

def delete(self, *args, **kwargs):
    if not self.related_query.all():
        super(MyModel, self).delete(*args, **kwargs)

Ответ 3

Это приведет к завершению решения из реализации в моем приложении. Некоторый код представляет собой ответ LWN.

Есть четыре ситуации, когда ваши данные удаляются:

  • SQL-запрос
  • Вызов delete() в экземпляре модели: project.delete()
  • Вызов delete() на основе QuerySet: Project.objects.all().delete()
  • Удалено поле ForeignKey на другой модели

Хотя в первом случае вы ничего не можете сделать, остальные три могут контролироваться мелкозернистым. Один из советов заключается в том, что в большинстве случаев вы никогда не должны удалять сами данные, поскольку эти данные отражают историю и использование нашего приложения. Настройка на active Булево поле предпочтительнее.

Чтобы предотвратить delete() в экземпляре Model, подкласс delete() в объявлении модели:

    def delete(self):
        self.active = False
        self.save(update_fields=('active',))

В то время как delete() на экземпляре QuerySet требуется небольшая настройка с настраиваемым диспетчером объектов, как в ответе LWN.

Оберните это до многоразовой реализации:

class ActiveQuerySet(models.QuerySet):
    def delete(self):
        self.save(update_fields=('active',))


class ActiveManager(models.Manager):
    def active(self):
        return self.model.objects.filter(active=True)

    def get_queryset(self):
        return ActiveQuerySet(self.model, using=self._db)


class ActiveModel(models.Model):
    """ Use `active` state of model instead of delete it
    """
    active = models.BooleanField(default=True, editable=False)
    class Meta:
        abstract = True

    def delete(self):
        self.active = False
        self.save()

    objects = ActiveManager()

Использование, только класс подкласса ActiveModel:

class Project(ActiveModel):
    ...

Тем не менее наш объект все равно можно удалить, если любое из его полей ForeignKey будет удалено:

class Employee(models.Model):
    name = models.CharField(name, unique=True)

class Project(models.Model):
    name = models.CharField(name, unique=True)
    manager = purchaser = models.ForeignKey(
        Employee, related_name='project_as_manager')

>>> manager.delete() # this would cause `project` deleted as well

Этого можно предотвратить, добавив аргумент on_delete в поле Model:

class Project(models.Model):
    name = models.CharField(name, unique=True)
    manager = purchaser = models.ForeignKey(
        Employee, related_name='project_as_manager',
        on_delete=models.PROTECT)

Значение по умолчанию on_delete равно CASCADE, что приведет к удалению вашего экземпляра с помощью PROTECT вместо этого, который поднимет ProtectedError (подкласс IntegrityError). Другая цель этого заключается в том, что ForeignKey данных следует хранить в качестве ссылки.

Ответ 4

У меня есть предложение, но я не уверен, что это лучше вашей текущей идеи. Взглянув на ответ здесь для отдаленной, но не связанной с этим проблемы, вы можете переопределить в django admin различные действия, по существу удалив их и используя свои собственные. Так, например, где они:

def really_delete_selected(self, request, queryset):
    deleted = 0
    notdeleted = 0
    for obj in queryset:
        if obj.project_set.all().count() > 0:
            # set status to fail
            notdeleted = notdeleted + 1
            pass
        else:
            obj.delete()
            deleted = deleted + 1
    # ...

Если вы не используете администратора django, как я, просто создайте эту проверку в логике пользовательского интерфейса, прежде чем разрешить пользователю удалять объект.

Ответ 5

Я хотел бы предложить еще один вариант ответов LWN и anhdat, в котором мы используем deleted вместо поля active, и мы исключаем "удаленные" объекты из набора запросов по умолчанию, чтобы рассматривать эти объекты как более отсутствующие, если мы специально их не включили.

class SoftDeleteQuerySet(models.QuerySet):
    def delete(self):
        self.update(deleted=True)


class SoftDeleteManager(models.Manager):
    use_for_related_fields = True

    def with_deleted(self):
        return SoftDeleteQuerySet(self.model, using=self._db)

    def deleted(self):
        return self.with_deleted().filter(deleted=True)

    def get_queryset(self):
        return self.with_deleted().exclude(deleted=True)


class SoftDeleteModel(models.Model):
    """ 
    Sets `deleted` state of model instead of deleting it
    """
    deleted = models.NullBooleanField(editable=False)  # NullBooleanField for faster migrations with Postgres if changing existing models
    class Meta:
        abstract = True

    def delete(self):
        self.deleted = True
        self.save()

    objects = SoftDeleteManager()


class Employee(SoftDeleteModel):
    ...

Использование:

Employee.objects.all()           # will only return objects that haven't been 'deleted'
Employee.objects.with_deleted()  # gives you all, including deleted
Employee.objects.deleted()       # gives you only deleted objects

Как указано в ответе anhdat, не забудьте установить on_delete свойство в ForeignKeys на вашей модели, чтобы избежать каскадного поведения, например

class Employee(SoftDeleteModel):
    latest_project = models.ForeignKey(Project, on_delete=models.PROTECT)

Примечание:

Аналогичная функциональность включена в django-model-utils SoftDeletableModel как я только что открыл. Стоит проверить. Приходит с некоторыми другими удобными вещами.

Ответ 6

Для тех, кто ссылается на эти вопросы с той же проблемой с отношением ForeignKey, правильным ответом будет использование поля Djago on_delete=models.PROTECT в отношении ForeignKey. Это предотвратит удаление любого объекта, у которого есть ссылки на внешние ключи. Это не будет работать для отношений ManyToManyField (как обсуждалось в этом вопросе), но отлично работает для полей ForeignKey.

Итак, если бы модели были такими, это помогло бы предотвратить удаление  любой объект Employee, который имеет один или несколько связанных с ним объектов Project:

class Employee(models.Model):
    name = models.CharField(name, unique=True)

class Project(models.Model):
    name = models.CharField(name, unique=True)
    employees = models.ForeignKey(Employee, on_delete=models.PROTECT)

Документацию можно найти ЗДЕСЬ.