web-dev-qa-db-ja.com

Djangoの一意のBooleanField値?

私のmodels.pyがそうであると仮定してください:

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

Characterインスタンスの1つだけにis_the_chosen_one == Trueと他のすべてのis_the_chosen_one == False。この一意性の制約が尊重されるようにするにはどうすればよいですか?

データベース、モデル、および(管理者)フォームレベルでの制約を尊重することの重要性を考慮に入れた回答に対するトップマーク!

80
sampablokuper

このタスクを実行する必要があるときはいつでも、モデルの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)
59
Adam

モデルのsaveメソッドをオーバーライドします。ブール値をTrueに設定している場合は、他のすべてがFalseに設定されていることを確認してください。

from Django.db import transaction

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

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

Adamが同様の回答を編集してみましたが、元の回答の変更が多すぎるため拒否されました。この方法は、他のエントリのチェックが単一のクエリで実行されるため、より簡潔で効率的です。

29
Ellis Percival

カスタムモデルのクリーニング/保存を使用する代わりに、_ カスタムフィールド を作成して、pre_saveDjango.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)
27
saul.shanabrook

ここで答えを出して収支を合わせようとすると、それらのいくつかは同じ問題にうまく対処し、それぞれが異なる状況に適していることがわかりました。

私は選択します:

  • @ semente :Django ORMを最小限に抑えながら、データベース、モデル、管理フォームレベルでの制約を尊重します。さらに、 多分 aunique_togetherの状況では、throughManyToManyFieldテーブル内で使用できます。 (確認して報告します)

    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)
    
  • @ Ellis Percival :データベースに1回だけヒットし、現在のエントリを選択したエントリとして受け入れます。クリーンでエレガント。

    from Django.db import transaction
    
    class Character(models.Model):
        name = models.CharField(max_length=255)
        is_the_chosen_one = models.BooleanField()
    
    def save(self, *args, **kwargs):
        if not self.is_the_chosen_one:
            # The use of return is explained in the comments
            return super(Character, self).save(*args, **kwargs)  
        with transaction.atomic():
            Character.objects.filter(
                is_the_chosen_one=True).update(is_the_chosen_one=False)
            # The use of return is explained in the comments
            return super(Character, self).save(*args, **kwargs)  
    

私のケースには適していないが実行可能な他のソリューション:

@ nemocorp は、検証を実行するためにcleanメソッドをオーバーライドしています。ただし、どのモデルが「1つ」であるかは報告されないため、ユーザーフレンドリーではありません。それにもかかわらず、@ Flyteほどアグレッシブになろうとしない人にとっては、これは非常に素晴らしいアプローチです。

@ saul.shanabrook および @ Thierry J。 は、他の「is_the_one」エントリをFalseに変更するか、ValidationErrorを発生させるカスタムフィールドを作成します。 Djangoインストールに絶対に必要でない限り、私は新しい機能を実装することに消極的です。

@ daigorocub :Djangoシグナルを使用します。これはユニークなアプローチで、使用方法のヒントを提供します Djangoシグナル 。しかし、私はこれが「厳密に言えば」信号の「適切な」使用であるかどうかはわかりません。この手順を「分離されたアプリケーション」の一部と見なすことができないためです。

9
raratiru

次の解決策は少し見苦しいですが、うまくいくかもしれません:

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は1つしか持てません。

9
semente
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
shadfc
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")

これを行うと、基本的な管理フォームで検証を利用できるようになります

4
nemocorp

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)
2
Thierry J.

そして、それだけです。

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

私はこれらの解決策のいくつかを試してみましたが、コードを短くするために、別の解決策になりました(フォームをオーバーライドしたり、メソッドを保存したりする必要はありません)。これを機能させるには、フィールドをその定義内で一意にすることはできませんが、シグナルによってそれが確実に行われます。

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

質問に答えるとポイントがもらえますか?

問題は、ループ内で自分自身を見つけることでした。

    # 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()
1
bytejunkie