Django中唯一的BooleanField值?

时间:2009-09-21 15:30:52

标签: database django django-models django-admin django-forms

假设我的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。如何才能最好地确保这种唯一性约束得到尊重?

考虑到在数据库,模型和(管理员)表单级别遵守约束的重要性的答案的最高分标记!

13 个答案:

答案 0 :(得分:51)

每当我需要完成此任务时,我所做的就是覆盖模型的save方法,并检查是否有其他模型已经设置了标志(并将其关闭)。

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)

答案 1 :(得分:24)

我没有使用自定义模型清理/保存,而是在pre_save上创建了custom field覆盖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)

答案 2 :(得分:19)

我会覆盖模型的save方法,如果你将boolean设置为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)

我尝试编辑Adam的类似答案,但是因为改变了原来的答案太多而被拒绝了。这种方式更简洁,更有效,因为在单个查询中检查其他条目。

答案 3 :(得分:8)

以下解决方案有点难看但可能会有效:

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。

答案 4 :(得分:7)

试图在这里达到目的,我发现其中一些问题成功地解决了同一个问题,并且每个问题都适用于不同的情况:

我会选择:

  • @semente:尊重数据库,模型和管理表单级别的约束,同时尽可能地覆盖Django ORM。此外,它可以可能through情况下ManyToManyField的{​​{1}}表中使用。 (我会检查并报告)

    unique_together
  • @Flyte:仅在一个额外时间点击数据库并接受当前条目作为所选条目。干净而优雅。

    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)
    

其他解决方案不适合我的情况但可行:

@nemocorp会覆盖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) 方法以执行验证。但是,它不报告哪个模型是“那个”,这不是用户友好的。尽管如此,这是一个非常好的方法,特别是如果有人不打算像@Flyte那样具有攻击性。

@saul.shanabrook@Thierry J.会创建一个自定义字段,该字段可以将任何其他“is_the_one”条目更改为clean或引发False。我只是不愿意为我的Django安装增加新功能,除非它是非常必要的。

@daigorocub:使用Django信号。我发现它是一种独特的方法,并提供了如何使用Django Signals的提示。但是我不确定这是否严格地说 - “正确”使用信号,因为我不能将此程序视为“解耦应用程序”的一部分。

答案 5 :(得分:6)

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)

答案 6 :(得分:4)

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")

这样做可以在基本管理表格中提供验证

答案 7 :(得分:4)

将这种约束添加到模型中比较简单 在Django 2.2之后。您可以直接使用UniqueConstraint.conditionDjango Docs

只需像这样覆盖模型class Meta

class Meta:
    constraints = [
        UniqueConstraint(fields=['is_the_chosen_one'], condition=Q(is_the_chosen_one=True), name='unique_is_the_chosen_one')
    ]

答案 8 :(得分:2)

就是这样。

def save(self, *args, **kwargs):
    if self.default_dp:
        DownloadPageOrder.objects.all().update(**{'default_dp': False})
    super(DownloadPageOrder, self).save(*args, **kwargs)

答案 9 :(得分:1)

我能否回答我的问题?

问题是它是在循环中发现自己,修正:

    # is this the testimonial image, if so, unselect other images
    if self.testimonial_image is True:
        others = Photograph.objects.filter(project=self.project).filter(testimonial_image=True)
        pdb.set_trace()
        for o in others:
            if o != self: ### important line
                o.testimonial_image = False
                o.save()

答案 10 :(得分:1)

我尝试了其中一些解决方案,最后只使用了另一个解决方案,只是为了代码简洁(不必覆盖表单或保存方法)。 为了使其工作,该字段在其定义中不能是唯一的,但信号确保发生这种情况。

# 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)

答案 11 :(得分:1)

使用与Saul类似的方法,但目的略有不同:

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

当尝试保存另一个值为True的记录时,此实现将引发ValidationError

另外,我添加了unique_for参数,可以设置为模型中的任何其他字段,仅检查具有相同值的记录的真实唯一性,例如:

class Phone(models.Model):
    user = models.ForeignKey(User)
    main = TrueUniqueBooleanField(unique_for='user', default=False)

答案 12 :(得分:0)

2020年更新,使初学者的事情不再那么复杂:

class Character(models.Model):
    name = models.CharField(max_length=255)
    is_the_chosen_one = models.BooleanField(blank=False, null=False, default=False)

    def save(self):
         if self.is_the_chosen_one == True:
              items = Character.objects.filter(is_the_chosen_one = True)
              for x in items:
                   x.is_the_chosen_one = False
                   x.save()
         super().save()

当然,如果您希望唯一的布尔值为False,则只需将True的每个实例与False交换,反之亦然。