Allowing Edit to editable=False Fields in Django Admin - django-models

DRF will use the editable=False on a field to default the Serializer to read-only. This is a very helpful / safe default that I take advantage of (ie I won't forget to set the Serializer to read-only). That being said once I have set editable=False is there any way to then force the Django admin to allow editing one of those fields?
Presumably the admin is a super user and I do want him to be able to change the fields value but fore safety I want the default Serializer logic to be read only.
UPDATE
I don't actually need to be able to edit the field as much as "set-it" when I create the object.

You are going about this the wrong way.
Your models should be the most pure implementation of the things you are modelling. If something about a model is fixed (for example a creation date) it shouldn't be editable in the model, if its mutable, then leave as editable in the model.
Otherwise, in the future you (or someone else) might be stuck wondering why a field which is set to editable=False is some how being changed. Especially as the documentation states:
If False, the field will not be displayed in the admin or any other ModelForm. They are also skipped during model validation.
If you have one view in which it shouldn't be editable (such as in the API), then override it there.
If you have multiple serilaizers for a model, instead make an abstract serializer with a read_only_fields set and then subclass that. For example:
class AbstractFooSerializer(serializers.ModelSerializer):
class Meta:
model = Foo
read_only_fields = ('bar',)
class MainFooSerializer(AbstractFooSerializer):
pass
class DifferentFooSerializer(AbstractFooSerializer):
pass
If you really, really want to use editable=False, but allow the item to be edited in the Admin site only on creation you have an up hill battle.
Probably the best approach would be to reimplement the AdminForm you are using for the Admin
So instead of:
class FooAdmin(admin.ModelAdmin):
Use:
class FooAdmin(admin.ModelAdmin):
form = MySpecialForm
Then declare the form:
class MySpecialForm(forms.Model):
def __init__(self, *args, **kwargs):
self.is_new = False
if kwargs.get('instance',None) is None:
# There is no instance, thus its a new item
self.is_new = True
self.fields['one_time_field'] = forms.CharField() # Or what have you.
super(MySpecialForm, self).__init__(*args, **kwargs)
def save(self, commit=True):
instance = super(MySpecialForm, self).save(commit)
if self.is_new:
instance.your_one_time_only_field = self.one_time_field
instance.save()
return instance
Note: you will need to manually add a field and save each readonly field that you want to do this for. This may or may not be 100% functional.

For those who want to allow editing of a non-editabled field only during creation (no instance.pk, yet):
# models.py
class Entity(Model):
name = CharField(max_length=200, unique=True, null=False, blank=False, editable=False)
# admin.py
#register(Entity)
class EntityAdmin(ModelAdmin):
def get_readonly_fields(self, request, obj=None):
if obj: # This is the case when obj is already created i.e. it's an edit
return ['id', 'name']
else:
return []
# this override prevents that the new_name field shows up in the change form if it's not a creation
def get_form(self, request, obj=None, **kwargs):
orig_self_form = self.form
if not obj:
self.form = CreateEntityForm
result = super().get_form(request, obj=obj, **kwargs)
self.form = orig_self_form
return result
# forms.py
class CreateEntityForm(ModelForm):
new_name = CharField(max_length=200, min_length=2, label='Name', required=True)
def clean_new_name(self):
code = self.cleaned_data['new_name']
# validate uniqueness - if you need
exists = Entity.objects.filter(name=code).first()
if exists:
raise ValidationError('Entity with this name already exists: {}', exists)
return name
def save(self, commit=True):
if self.instance.pk:
raise NotImplementedError('Editing of existing Entity is not allowed!')
self.instance.name = self.cleaned_data['new_name'].upper()
return super().save(commit)
class Meta:
model = Entity
fields = ['new_name']
exclude = ['id', 'name']

Related

Serializer field for side effect model django rest framework

I have a django.db.models.Model A whose instances are created in a rest_framework.serializers.ModelSerializer from POST requests.
Depending on the data being sent in the POST, I would like to create one of several other "addon" models, let's say B or C, which I link to the original through a django.db.models.OneToOneField:
from django.db import models
class A(models.Model):
some_field = models.CharField()
class B(models.Model):
a = models.OneToOneField(A, related_name='addon', on_delete=models.CASCADE)
class C(models.Model):
a = models.OneToOneField(A, related_name='addon', on_delete=models.CASCADE)
What I would like to is to have a serializer which validates the incoming data, including some string indicating which addon to use. The serializer then creates the model instance of A and based on this creates the addon model.
I do not want to create a utility field in model A used to determine which addon to use, I would like to create the model directly using the instance of model A and information from the POST itself.
At the same time when accessing the data through a get, I would like to return the original string used to determine which addon to use.
What I have come up with so far:
from rest_framework import serializers
str2model = {'b': B, 'c': C}
class AddonField(serializers.Field):
def to_representation(self, value):
# I completely ignore "value" as no "internal value" is set in "to_internal_value"
myvalue = self.parent.instance.addon
for addon_name, addon_class in str2model.items():
if isinstance(myvalue, addon_class):
return addon_name
def to_internal_value(self, data):
# I create the "internal value" after "A" instance is created, thus here I do nothing?
return data
class ASerializer(serializers.ModelSerializer):
some_field = serializers.CharField()
the_addon = AddonField()
def validate_the_addon(self, value): # here addon is a string
if value in str2model.keys():
return value
def create(self, validated_data):
addon_name = validated_data.pop('the_addon')
addon_class = str2model[addon]
a = super(ASerializer, self).create(validated_data)
addon_class.objects.create(a=a)
return a
class Meta:
model = A
fields = ["some_field", "the_addon"]
When testing this I get:
AttributeError: Got AttributeError when attempting to get a value for field `the_addon` on serializer `ASerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `A` instance.
Original exception text was: 'A' object has no attribute 'the_addon'.
How can I temporarily store the_addon in the serializer until the A instance has been created?
This is how I would typically approach it
# Serializer
class ASerializer(serializers.Serializer):
some_field = serializers.CharField()
addon_b = serializers.CharField(required=False, allow_null=True)
addon_c = serializers.CharField(required=False, allow_null=True)
def create(self, validated_data):
addon_b = validated_data.pop('addon_b')
addon_c = validated_data.pop('addon_c')
a = A.objects.create(some_field=validated_data['some_field'])
if addon_b:
B.objects.create(a=a)
if addon_c:
C.objects.create(a=a)
return a
You can do other validations if necessary.
class TestAPIView01(generics.CreateAPIView):
permission_classes = {}
serializer_class = serializers.ASerializer
queryset = A.objects.all()
Also, look at the related_name on B and C you may want to consider making them different, as that might throw an error in the future. Cheers

Dynamicaly connect two models in Django

let's suppose i have two models like below:
class Client(models.Model):
login = models.CharField(max_length=100)
password = models.CharField(max_length=100)
class Users(models.Model):
user_login = models.CharField(max_length=100)
user_pass = models.CharField(max_length=100)
One model [Users] is filled with data, second [Client] is empty. First i can populate Client based on Users. Now all Django data changes will by held only on Client model.
Question:
How to dynamically connect this two models fields? For example if some data is changed on model Client then according data will by changed also on Users model.
Solution suggestion:
I can update or populate Client data and simultaneously update or populate Users data. Is there another solution to do this?
if login is unique then no need to maintain foreign key relations, You can override the save() methods in both models else You have to maintain the foreign key relation and override the save() methods.
With help of this post i manage to link two models with below code:
class Client(models.Model):
login = models.CharField(max_length=100)
password = models.CharField(max_length=100)
# save previous instance
def __init__(self, *args, **kwargs):
super(Invoice, self).__init__(*args, **kwargs)
self.original_login = self.login
# override save method to update another model fields
def save(self, *args, **kwargs):
# import in place to avoid circular imports
from app.models import Users
if self.login != self.original_login:
Users.objects.filter(id=self.id).update(user_login=self.login)
# save changed data on Client model
super(Client, self).save(*args, **kwargs)
The same method goes to the Users model.
Note that this two model are in different apps.

Enforcing values for some fields when using ModelForm

I have a Django app where users submit orders for payment. Clearly, security is important. I want to minimise the amount of code that I have to write, to avoid introducing any security holes, and ease maintenance.
The model is simple:
class Order(models.Model):
user = models.ForeignKey(User)
created = models.DateTimeField()
paid = models.DateTimeField(null=True, blank=True)
items = models.ManyToManyField(Item)
I'm using a CreateView to create instances of Order:
class OrderView(CreateView):
model = Order
form_class = OrderForm
I want to enforce values for certain fields in those instances. For example, I want the instance user field set to the current logged-in user. I don't want any possibility that the user can change the value of this field, so I don't want it to appear in the form at all. Therefore I use a custom ModelForm to remove these fields from the form:
class OrderForm(forms.ModelForm):
class Meta:
model = Order
# For security, we control exactly which fields are placed
# in the form, rather than excluding some:
fields = ('items',)
Now I want the newly created Order instances to have the user field set to the current logged-in user. I can't find any documentation about what is the best way to do this.
(A) I can override the form's save() method to modify the object before saving, but it feels like this code doesn't belong in the form, which doesn't know anything about the user field. I also don't have access to the request here, which I'd need to determine the current user. But it might look like this:
class OrderForm(forms.ModelForm):
def save(self, commit=True):
instance = super(OrderForm, self).save(commit=False)
instance.user = get_request_magic().user
if commit:
instance.save()
return instance
(B) I can override the view's form_valid method to save the object with commit=False, like a class-based version of this question. But I can't call the superclass method directly, because it saves the object with no way to disable commit, so I have to manually skip a generation of form_valid which is nasty. Apart from that complaint, this does look like the best way I've found so far:
class OrderView(CreateView):
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.user = self.request.user
self.object.save()
return super(ModelFormMixin, self).form_valid(form)
(C) I could write a replacement for CreateView that adds a hook to allow objects to be changed before saving them. But that feels like more boilerplate and duplication.
(D) I can't provide an initial value, because there's no form field to put it in, so it will be ignored.
Any other ideas? If (B) the best option, is there any way around the hacky way of manually specifying which superclass' form_valid method I want to call?
Django user Charettes answered the question for me:
You can achieve this by overriding form_valid:
class OrderCreateViewMixin(CreateView):
def form_valid(self, form):
form.instance.user = request.user
return super(OrderCreateViewMixin, self).form_valid(form)
Which pointed me towards the right part of the documentation:
class AuthorCreate(CreateView):
form_class = AuthorForm
model = Author
def form_valid(self, form):
form.instance.created_by = self.request.user
return super(AuthorCreate, self).form_valid(form)
This is definitely the simplest and cleanest answer I've found so far. It doesn't require modifying the form in any way, although it does directly access its instance member which is a bit ugly. However, at least it's officially documented, so it's unlikely to break.
There are probably multiple approaches to this. I would do this:
Create a constructor in your form which takes the request:
def __init__(self, *args, **kwargs):
request = kwargs.pop('request', None)
super(OrderForm, self).__init__(*args, **kwargs)
self.request = request
When creating your form for POST processing, instantiate it as follows:
form = OrderForm(data=request.POST, request=request)
Now, in your save() method, you have access to the user on the request by referencing self.request.user and can set it accordingly on your model.
The way I've gone about handling this situation with CBVs, is to pass in an unsaved instance of the model to the form. This is how I've done it:
class OrderView(CreateView):
def get_form_kwargs(self):
self.object = Order(user=self.request.user)
return super(OrderView, self).get_form_kwargs()
Both CreateView and UpdateView will add instance to the form kwargs, setting it to the value of self.object.
The only other way, besides what you've already mentioned, is to construct your view class from the same elements that CreateView does, and then change the get and post methods to populate self.object there. I've done that when I have needed a lot of create views in my project:
class OrderView(SingleObjectTemplateResponseMixin, ModelFormMixin, ProcessFormView):
template_name_suffix = '_form'
def get(self, request, *args, **kwargs):
self.object = Order(user=request.user)
return super(OrderView, self).get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.object = Order(user=request.user)
return super(OrderView, self).post(request, *args, **kwargs)
Here is a more generalized version to be reused: https://gist.github.com/4439975

Django-nonrel form field for ListField

I'm experimenting with django-nonrel on appengine and trying to use a djangotoolbox.fields.ListField to implement a many-to-many relation. As I read in the documentation a ListField is something that you can use to make a workaround for djamgo-nonrel not supporting many-to-many relations.
This is an excerpt from my model:
class MyClass(models.Model):
field = ListField(models.ForeignKey(AnotherClass))
So if I am getting this right I am creating a list of foreign keys to another class to show a relationship with multiple instances of another class
With this approach everything works fine ... No Exceptions. I can create `MyClass' objects in code and views. But when I try to use the admin interface I get the following error
No form field implemented for <class 'djangotoolbox.fields.ListField'>
So I though I would try something that I haven't done before. Create my own field. Well actually my own form for editing MyClass instances in the admin interface. Here is what I did:
class MyClassForm(ModelForm):
field = fields.MultipleChoiceField(choices=AnotherClass.objects.all(), widget=FilteredSelectMultiple("verbose_name", is_stacked=False))
class Meta:
model = MyClass
then I pass MyClassForm as the form to use to the admin interface
class MyClassAdmin(admin.ModelAdmin):
form = MyClassForm
admin.site.register(MyClass, MyClassAdmin)
I though that this would work but It doesn't. When I go to the admin interface I get the same error as before. Can anyone tell what I am doing wrong here ... or if you have any other suggestions or success stories of using the ListField, SetField, etc. from djangotoolbox.fields in the admin interface it would be very much appreciated.
OK, here is what I did to get this all working ...
I'll start from the beginning
This is what what my model looked like
class MyClass(models.Model):
field = ListField(models.ForeignKey(AnotherClass))
I wanted to be able to use the admin interface to create/edit instances of this model using a multiple select widget for the list field. Therefore, I created some custom classes as follows
class ModelListField(ListField):
def formfield(self, **kwargs):
return FormListField(**kwargs)
class ListFieldWidget(SelectMultiple):
pass
class FormListField(MultipleChoiceField):
"""
This is a custom form field that can display a ModelListField as a Multiple Select GUI element.
"""
widget = ListFieldWidget
def clean(self, value):
#TODO: clean your data in whatever way is correct in your case and return cleaned data instead of just the value
return value
These classes allow the listfield to be used in the admin. Then I created a form to use in the admin site
class MyClassForm(ModelForm):
def __init__(self, *args, **kwargs):
super(MyClasstForm,self).__init__(*args, **kwargs)
self.fields['field'].widget.choices = [(i.pk, i) for i in AnotherClass.objects.all()]
if self.instance.pk:
self.fields['field'].initial = self.instance.field
class Meta:
model = MyClass
After having done this I created a admin model and registered it with the admin site
class MyClassAdmin(admin.ModelAdmin):
form = MyClassForm
def __init__(self, model, admin_site):
super(MyClassAdmin,self).__init__(model, admin_site)
admin.site.register(MyClass, MyClassAdmin)
This is now working in my code. Keep in mind that this approach might not at all be well suited for google_appengine as I am not very adept at how it works and it might create inefficient queries an such.
As far as I understand, you're trying to have a M2M relationship in django-nonrel, which is not an out-of-the-box functionality. For starters, if you want a quick hack, you can go with this simple class and use a CharField to enter foreign keys manually:
class ListFormField(forms.Field):
""" A form field for being able to display a djangotoolbox.fields.ListField. """
widget = ListWidget
def clean(self, value):
return [v.strip() for v in value.split(',') if len(v.strip()) > 0]
But if you want to have a multiple selection from a list of models normally you'd have to use ModelMultipleChoiceField, which is also not functional in django-nonrel. Here's what I've done to emulate a M2M relationship using a MultipleSelectField:
Let's say you have a M2M relationship between 2 classes, SomeClass and AnotherClass respectively. You want to select the relationship on the form for SomeClass. Also I assume you want to hold the references as a ListField in SomeClass. (Naturally you want to create M2M relationships as they're explained here, to prevent exploding indexes if you're working on App Engine).
So you have your models like:
class SomeClass(models.Model):
another_class_ids = ListField(models.PositiveIntegerField(), null=True, blank=True)
#fields go here
class AnotherClass(models.Model):
#fields go here
And in your form:
class SomeClassForm(forms.ModelForm):
#Empty field, will be populated after form is initialized
#Otherwise selection list is not refreshed after new entities are created.
another_class = forms.MultipleChoiceField(required=False)
def __init__(self, *args, **kwargs):
super(SomeClassForm,self).__init__(*args, **kwargs)
self.fields['another_class'].choices = [(item.pk,item) for item in AnotherClass.objects.all()]
if self.instance.pk: #If class is saved, highlight the instances that are related
self.fields['another_class'].initial = self.instance.another_class_ids
def save(self, *args, **kwargs):
self.instance.another_class_ids = self.cleaned_data['another_class']
return super(SomeClassForm, self).save()
class Meta:
model = SomeClass
Hopefully this should get you going for the start, I implemented this functionality for normal forms, adjust it for admin panel shouldn't be that hard.
This could be unrelated but for the admin interface, be sure you have djangotoolbox listed after django.contrib.admin in the settings.. INSTALLED_APPS
You could avoid a custom form class for such usage by inquiring for the model object
class ModelListField(ListField):
def __init__(self, embedded_model=None, *args, **kwargs):
super(ModelListField, self).__init__(*args, **kwargs)
self._model = embedded_model.embedded_model
def formfield(self, **kwargs):
return FormListField(model=self._model, **kwargs)
class ListFieldWidget(SelectMultiple):
pass
class FormListField(MultipleChoiceField):
widget = ListFieldWidget
def __init__(self, model=None, *args, **kwargs):
self._model = model
super(FormListField, self).__init__(*args, **kwargs)
self.widget.choices = [(unicode(i.pk), i) for i in self._model.objects.all()]
def to_python(self, value):
return [self._model.objects.get(pk=key) for key in value]
def clean(self, value):
return value

Unique BooleanField value in Django?

Suppose my models.py is like so:
class Character(models.Model):
name = models.CharField(max_length=255)
is_the_chosen_one = models.BooleanField()
I want only one of my Character instances to have is_the_chosen_one == True and all others to have is_the_chosen_one == False . How can I best ensure this uniqueness constraint is respected?
Top marks to answers that take into account the importance of respecting the constraint at the database, model and (admin) form levels!
Whenever I've needed to accomplish this task, what I've done is override the save method for the model and have it check if any other model has the flag already set (and turn it off).
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)
I'd override the save method of the model and if you've set the boolean to True, make sure all others are set to 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)
I tried editing the similar answer by Adam, but it was rejected for changing too much of the original answer. This way is more succinct and efficient as the checking of other entries is done in a single query.
It is simpler to add this kind of constraint to your model
after Django version 2.2. You can directly use UniqueConstraint.condition. Django Docs
Just override your models class Meta like this:
class Meta:
constraints = [
UniqueConstraint(fields=['is_the_chosen_one'], condition=Q(is_the_chosen_one=True), name='unique_is_the_chosen_one')
]
Instead of using custom model cleaning/saving, I created a custom field overriding the pre_save method on django.db.models.BooleanField. Instead of raising an error if another field was True, I made all other fields False if it was True. Also instead of raising an error if the field was False and no other field was True, I saved it the field as 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)
Trying to make ends meet with the answers here, I find that some of them address the same issue successfully and each one is suitable in different situations:
I would choose:
#semente: Respects the constraint at the database, model and admin form levels while it overrides Django ORM the least possible. Moreover it can be used inside a through table of a ManyToManyField in aunique_together situation.
class MyModel(models.Model):
is_the_chosen_one = models.BooleanField(null=True, 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)
Update: NullBooleanField will be deprecated by Django-4.0, for BooleanField(null=True).
#Ellis Percival: Hits the database only one extra time and accepts the current entry as the chosen one. Clean and elegant.
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)
Other solutions not suitable for my case but viable:
#nemocorp is overriding the clean method to perform a validation. However, it does not report back which model is "the one" and this is not user friendly. Despite that, it is a very nice approach especially if someone does not intend to be as aggressive as #Flyte.
#saul.shanabrook and #Thierry J. would create a custom field which would either change any other "is_the_one" entry to False or raise a ValidationError. I am just reluctant to impement new features to my Django installation unless it is absoletuly necessary.
#daigorocub: Uses Django signals. I find it a unique approach and gives a hint of how to use Django Signals. However I am not sure whether this is a -strictly speaking- "proper" use of signals since I cannot consider this procedure as part of a "decoupled application".
The following solution is a little bit ugly but might work:
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)
If you set is_the_chosen_one to False or None it will be always NULL. You can have NULL as much as you want, but you can only have one True.
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
You can use the above form for admin as well, just use
class CharacterAdmin(admin.ModelAdmin):
form = CharacterForm
admin.site.register(Character, CharacterAdmin)
And that's all.
def save(self, *args, **kwargs):
if self.default_dp:
DownloadPageOrder.objects.all().update(**{'default_dp': False})
super(DownloadPageOrder, self).save(*args, **kwargs)
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")
Doing this made the validation available in the basic admin form
Using a similar approach as Saul, but slightly different purpose:
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
This implementation will raise a ValidationError when attempting to save another record with a value of True.
Also, I have added the unique_for argument which can be set to any other field in the model, to check true-uniqueness only for records with the same value, such as:
class Phone(models.Model):
user = models.ForeignKey(User)
main = TrueUniqueBooleanField(unique_for='user', default=False)
Do I get points for answering my question?
problem was it was finding itself in the loop, fixed by:
# 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()
I tried some of these solutions, and ended up with another one, just for the sake of code shortness (don't have to override forms or save method).
For this to work, the field can't be unique in it's definition but the signal makes sure that happens.
# 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)
2020 update to make things less complicated for beginners:
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()
Of course, if you want the unique boolean to be False, you would just swap every instance of True with False and vice versa.
When implementing a solution which overwrites model.save()*, I ran into the issue of Django Admin raising an error before hitting model.save(). The cause seems to be Admin calling model.clean() (or perhaps model.full_clean(), I didn't investigate too carefully) before calling model.save(). model.clean() in turn calls model.validate_unique() which raises a ValidationError before my custom save method can take care of the unique violation. To solve this I overwrote model.validate_unique() as follows:
def validate_unique(self, exclude=None):
try:
super().validate_unique(exclude=exclude)
except ValidationError as e:
validation_errors = e.error_dict
try:
list_validation_errors = validation_errors["is_the_chosen_one"]
for validation_error in list_validation_errors:
if validation_error.code == "unique":
list_validation_errors.remove(validation_error)
if not list_validation_errors:
validation_errors.pop(key)
except KeyError:
continue
if e.error_dict:
raise e
* the same would be true for a signal solution using pre_save, as pre_save is also not sent before .validate_unique is called

Resources