Уникальное значение BooleanField в Django? - PullRequest
69 голосов
/ 21 сентября 2009

Предположим, что мой models.py такой:

class Character(models.Model):
    name = models.CharField(max_length=255)
    is_the_chosen_one = models.BooleanField()

Я хочу, чтобы только один из моих Character экземпляров имел is_the_chosen_one == True, а все остальные * is_the_chosen_one == False. Как я могу наилучшим образом обеспечить соблюдение этого ограничения уникальности?

Высший балл за ответы, которые учитывают важность соблюдения ограничений на уровне базы данных, модели и (администратора) формы!

Ответы [ 11 ]

51 голосов
/ 21 сентября 2009

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

class Character(models.Model):
    name = models.CharField(max_length=255)
    is_the_chosen_one = models.BooleanField()

    def save(self, *args, **kwargs):
        if self.is_the_chosen_one:
            try:
                temp = Character.objects.get(is_the_chosen_one=True)
                if self != temp:
                    temp.is_the_chosen_one = False
                    temp.save()
            except Character.DoesNotExist:
                pass
        super(Character, self).save(*args, **kwargs)
24 голосов
/ 01 августа 2012

Вместо использования очистки / сохранения пользовательской модели, я создал пользовательское поле , переопределяющее метод pre_save для django.db.models.BooleanField. Вместо того, чтобы выдавать ошибку, если другое поле было True, я сделал все остальные поля False, если оно было True. Также вместо того, чтобы выдавать ошибку, если поле было False, а никакое другое поле не было True, я сохранил его как True

fields.py

from django.db.models import BooleanField


class UniqueBooleanField(BooleanField):
    def pre_save(self, model_instance, add):
        objects = model_instance.__class__.objects
        # If True then set all others as False
        if getattr(model_instance, self.attname):
            objects.update(**{self.attname: False})
        # If no true object exists that isnt saved model, save as True
        elif not objects.exclude(id=model_instance.id)\
                        .filter(**{self.attname: True}):
            return True
        return getattr(model_instance, self.attname)

# To use with South
from south.modelsinspector import add_introspection_rules
add_introspection_rules([], ["^project\.apps\.fields\.UniqueBooleanField"])

models.py

from django.db import models

from project.apps.fields import UniqueBooleanField


class UniqueBooleanModel(models.Model):
    unique_boolean = UniqueBooleanField()

    def __unicode__(self):
        return str(self.unique_boolean)
21 голосов
/ 07 мая 2014

Я бы переопределил метод сохранения модели, и если вы установили логическое значение True, убедитесь, что все остальные установлены в False.

from django.db import transaction

class Character(models.Model):
    name = models.CharField(max_length=255)
    is_the_chosen_one = models.BooleanField()

    @transaction.atomic
    def save(self, *args, **kwargs):
        if self.is_the_chosen_one:
            Character.objects.filter(
                is_the_chosen_one=True).update(is_the_chosen_one=False)
        super(Character, self).save(*args, **kwargs)

Я попытался отредактировать аналогичный ответ Адама, но он был отклонен из-за слишком большого изменения исходного ответа. Этот способ более лаконичен и эффективен, поскольку проверка других записей выполняется в одном запросе.

8 голосов
/ 04 сентября 2012

Следующее решение немного уродливо, но может работать:

class MyModel(models.Model):
    is_the_chosen_one = models.NullBooleanField(default=None, unique=True)

    def save(self, *args, **kwargs):
        if self.is_the_chosen_one is False:
            self.is_the_chosen_one = None
        super(MyModel, self).save(*args, **kwargs)

Если вы установите для is_the_chosen_one значение False или None, оно всегда будет NULL. Вы можете иметь NULL столько, сколько хотите, но у вас может быть только одна True.

7 голосов
/ 07 сентября 2016

Пытаясь свести концы с концами с ответами здесь, я обнаружил, что некоторые из них успешно решают одну и ту же проблему, и каждый подходит в различных ситуациях:

Я бы выбрал:

  • @ semente : Учитывает ограничение на уровне базы данных, модели и формы администратора, в то время как оно переопределяет Django ORM как можно меньше. Более того, возможно может быть использовано внутри through таблицы ManyToManyField в unique_together ситуации. (я проверю и сообщу)

    class MyModel(models.Model):
        is_the_chosen_one = models.NullBooleanField(default=None, unique=True)
    
        def save(self, *args, **kwargs):
            if self.is_the_chosen_one is False:
                self.is_the_chosen_one = None
            super(MyModel, self).save(*args, **kwargs)
    
  • @ Flyte : Попадание в базу данных только один раз и принятие текущей записи в качестве выбранной. Чистый и элегантный.

    from django.db import transaction
    
    class Character(models.Model):
        name = models.CharField(max_length=255)
        is_the_chosen_one = models.BooleanField()
    
        @transaction.atomic
        def save(self, *args, **kwargs):
            if self.is_the_chosen_one:
                Character.objects.filter(
                    is_the_chosen_one=True).update(is_the_chosen_one=False)
            super(Character, self).save(*args, **kwargs)
    

Другие решения, не подходящие для моего случая, но жизнеспособные:

@ nemocorp переопределяет метод clean для выполнения проверки. Тем не менее, он не сообщает, какая модель является "той", и это не удобно для пользователя. Несмотря на это, это очень хороший подход, особенно если кто-то не намерен быть таким агрессивным, как @ Flyte.

@saul.shanabrook и @ Thierry J. создадут пользовательское поле, которое либо изменит любую другую запись is_the_one на False, либо повысит ValidationError. Я просто не хочу препятствовать новым функциям в моей установке Django, если это не нужно абсолютно.

@ daigorocub : Используются сигналы Django. Я считаю это уникальным подходом и дает подсказку, как использовать Django Signals . Однако я не уверен, является ли это «строго говоря» «правильным» использованием сигналов, поскольку я не могу рассматривать эту процедуру как часть «несвязанного приложения».

6 голосов
/ 27 октября 2009
class Character(models.Model):
    name = models.CharField(max_length=255)
    is_the_chosen_one = models.BooleanField()

    def save(self, *args, **kwargs):
        if self.is_the_chosen_one:
            qs = Character.objects.filter(is_the_chosen_one=True)
            if self.pk:
                qs = qs.exclude(pk=self.pk)
            if qs.count() != 0:
                # choose ONE of the next two lines
                self.is_the_chosen_one = False # keep the existing "chosen one"
                #qs.update(is_the_chosen_one=False) # make this obj "the chosen one"
        super(Character, self).save(*args, **kwargs)

class CharacterForm(forms.ModelForm):
    class Meta:
        model = Character

    # if you want to use the new obj as the chosen one and remove others, then
    # be sure to use the second line in the model save() above and DO NOT USE
    # the following clean method
    def clean_is_the_chosen_one(self):
        chosen = self.cleaned_data.get('is_the_chosen_one')
        if chosen:
            qs = Character.objects.filter(is_the_chosen_one=True)
            if self.instance.pk:
                qs = qs.exclude(pk=self.instance.pk)
            if qs.count() != 0:
                raise forms.ValidationError("A Chosen One already exists! You will pay for your insolence!")
        return chosen

Вы также можете использовать вышеуказанную форму для администратора, просто используйте

class CharacterAdmin(admin.ModelAdmin):
    form = CharacterForm
admin.site.register(Character, CharacterAdmin)
4 голосов
/ 09 марта 2012
class Character(models.Model):
    name = models.CharField(max_length=255)
    is_the_chosen_one = models.BooleanField()

    def clean(self):
        from django.core.exceptions import ValidationError
        c = Character.objects.filter(is_the_chosen_one__exact=True)  
        if c and self.is_the_chosen:
            raise ValidationError("The chosen one is already here! Too late")

Благодаря этому проверка стала доступна в базовой форме администратора

2 голосов
/ 20 августа 2015

И это все.

def save(self, *args, **kwargs):
    if self.default_dp:
        DownloadPageOrder.objects.all().update(**{'default_dp': False})
    super(DownloadPageOrder, self).save(*args, **kwargs)
1 голос
/ 13 июля 2016

Используется такой же подход, как у Саула, но немного другое назначение:

class TrueUniqueBooleanField(BooleanField):

    def __init__(self, unique_for=None, *args, **kwargs):
        self.unique_for = unique_for
        super(BooleanField, self).__init__(*args, **kwargs)

    def pre_save(self, model_instance, add):
        value = super(TrueUniqueBooleanField, self).pre_save(model_instance, add)

        objects = model_instance.__class__.objects

        if self.unique_for:
            objects = objects.filter(**{self.unique_for: getattr(model_instance, self.unique_for)})

        if value and objects.exclude(id=model_instance.id).filter(**{self.attname: True}):
            msg = 'Only one instance of {} can have its field {} set to True'.format(model_instance.__class__, self.attname)
            if self.unique_for:
                msg += ' for each different {}'.format(self.unique_for)
            raise ValidationError(msg)

        return value

Эта реализация вызовет ValidationError при попытке сохранить другую запись со значением True.

Кроме того, я добавил аргумент unique_for, который можно установить для любого другого поля в модели, чтобы проверять уникальность только для записей с тем же значением, например:

class Phone(models.Model):
    user = models.ForeignKey(User)
    main = TrueUniqueBooleanField(unique_for='user', default=False)
1 голос
/ 23 февраля 2016

Я попробовал некоторые из этих решений и в итоге получил другое, просто из-за нехватки кода (не нужно переопределять формы или сохранять метод). Чтобы это работало, поле не может быть уникальным по определению, но сигнал гарантирует, что это произойдет.

# making default_number True unique
@receiver(post_save, sender=Character)
def unique_is_the_chosen_one(sender, instance, **kwargs):
    if instance.is_the_chosen_one:
        Character.objects.all().exclude(pk=instance.pk).update(is_the_chosen_one=False)
...