Override get_FIELD_display method in Django model - django

Let's say I have a field called field in my model, with a choices parameter defining the values to be returned by the get_field_display method.
I need the get_field_display method to return a different value based on another field. Is there any way to override get_field_display?
This doesn't work:
def get_field_display(self):
if self.other_field == 1:
return 'Other value'
return super.get_field_display(self)

You can't call super because the function is defined not by the parent class but by the ModelBase metaclass. Try with this:
def get_field_display(self):
if self.other_field == 1:
value = 'Other value'
else:
field_object = self._meta.get_field('field')
value = self._get_FIELD_display(field_object)
return value

What you can do is to create a different function in the same model, then monkey patch it. For example, in admin.py you may do something like:
ClassName.get_field_display = ClassName.get_patched_field_display
It's not very 'nice' but it works for me.

Related

Built-In callable as a default argument for a Django Field

I have a JSONField that I need to apply a default dictionary to. As per the documentation, I am avoiding passing the mutable dictionary to the default field. This is done by instead passing the copy method to the default argument like such:
default_dict = {'some_key': 'some value'}
class MyModel(models.Model):
my_field = models.JSONField(default=default_dict.copy)
When applying makemigrations, this is failing because of the following condition in django.db.migrations.serializer.FunctionTypeSerializer:
if self.value.__module__ is None:
raise ValueError("Cannot serialize function %r: No module" % self.value)
I can get around this by defining a callable that returns a copy, but I think this is adding unnecessary syntax and makes it harder to read:
class ADict(dict):
def __call__(self):
return self.copy()
default_dict = ADict({'some_key': 'some value'})
class MyModel(models.Model):
my_field = models.JSONField(default=default_dict)
Is there a way to pass a built-in objects method as the default value for a Django field?
You can't do this since it basically needs to be a named function, whereas default_dict.copy is an "anonymous" function.
You can however make a named function like:
default_dict = {'some_key': 'some value'}
def copy_default_dict():
return default_dict.copy()
class MyModel(models.Model):
my_field = models.JSONField(default=copy_default_dict)
or even simpler:
def copy_default_dict():
return {'some_key': 'some value'}
class MyModel(models.Model):
my_field = models.JSONField(default=copy_default_dict)

list_display - boolean icons for methods

When defining the list_display array for a ModelAdmin class, if a BooleanField or NullBooleanField is given the UI will use nice looking icons instead of True/False text in the column. If a method that returns a boolean is given, however, it simply prints out True/False.
Is there a way to make it use the pretty icons for a boolean method?
This is documented, although it's a bit hard to find - go a couple of screens down from here, and you'll find this:
If the string given is a method of the model, ModelAdmin or a callable that returns True or False Django will display a pretty "on" or "off" icon if you give the method a boolean attribute whose value is True.
and the example given is:
def born_in_fifties(self):
return self.birthday.strftime('%Y')[:3] == '195'
born_in_fifties.boolean = True
Thanks to #daniel-roseman (rtfm)
Since Django 3.2 there is a decorator #admin.display(boolean=True):
If the string (in list_display) given is a method of the model,
ModelAdmin or a callable that returns True, False, or None, Django
will display a pretty “yes”, “no”, or “unknown” icon if you wrap the
method with the display() decorator passing the boolean argument with
the value set to True:
class Person(models.Model):
birthday = models.DateField()
#admin.display(boolean=True)
def born_in_fifties(self):
return 1950 <= self.birthday.year < 1960
I got this to work for me (Django 3.1.10)
class MyAdmin(MyModel):
list_display = ("field_as_boolean", )
def field_as_boolean(self, obj):
return True if obj.field else False
field_as_boolean.boolean = True
field_as_boolean.short_description = "field_name"

Django ModelChoiceField: filtering query set and setting default value as an object

I have a Django Form class defined likes this in Models:
class AccountDetailsForm(forms.Form):
...
adminuser = forms.ModelChoiceField(queryset=User.objects.all())
This works OK, but it has some limitations I can't seem to work around:
(1) I would like to use a filter on the queryset, based on a variable accountid passed to the form, like this:
User.objects.filter(account=accountid)
This can't work in the model because accountid can't be passed as a variable, of course.
It follows that the queryset must somehow be defined in the Views, but as far as I can see it's a required field in the Form class.
(2) I would like to make the default choice of AccountDetailsForm an object in the database, which I can select in the Views like this:
User.objects.filter(account=accountid).filter(primary_user=1)
I've tried specifying the adminuser as a default value in the form, (which works with other standard form fields, like CharField):
adminuser = User.objects.filter(account=accountid).filter(primary_user=1)
...
form = AccountDetailsForm({'adminuser': adminuser})
return render_to_response('accounts/edit/accountdetails.html',
{'form': form, 'account':account})
But no luck.
Should I be using something other than ModelChoiceField given the flexibility I need here?
Thanks.
Override the init method and accept a new keyword argument
class AccountDetailsForm(forms.Form):
...
adminuser = forms.ModelChoiceField(queryset=User.objects.all())
def __init__(self, *args, **kwargs):
accountid = kwargs.pop('accountid', None)
super(AccountDetailsForm, self).__init__(*args, **kwargs)
if accountid:
self.fields['adminuser'].queryset = User.objects.filter(account=accountid)
form = AccountDetailsForm(accountid=3)
You can always just set the choices manually in the view as well.
form = AccountDetailsForm()
form.fields['adminuser'].queryset = User.objects.filter(account=accountid)
Be warned: you are not setting default values by passing in a dictionary to a form like in your example.
You are actually creating a Bound Form, potentially triggering validation and all that jazz.
To set defaults, use the initials argument.
form = AccountDetailsForm(initial={'adminuser':'3'})
You can override the field in the view
yourForm = AccountDetailsForm()
yourForm.fields['accomodation'] = forms.ModelChoiceField(User.objects.filter(account=accountid).filter(primary_user=1))
Something that hasn't been mentioned here yet is the Form.clean() method. This method is specifically for custom validation.
For your example, you could do something like this:
class AccountDetailsForm(forms.Form):
adminuser = forms.ModelChoiceField(queryset=User.objects.all())
account_id = forms.IntegerField() # or ModelChoiceField if that applies
def clean(self):
account_id = self.cleaned_data['account_id']
self.cleaned_data['adminuser'] = User.objects.filter(account_id=account_id)
return self.cleaned_data
The clean() method gets called after the default clean methods, so you can use self.cleaned_data (same as form.cleaned_data in the view) and return it however you'd like.
Even better, you can name the method according to the field you'd like to clean (def clean_adminuser) and make easier to read.
def clean_adminuser(self):
account_id = self.cleaned_data['account_id']
return User.objects.filter(account_id=account_id)
Also in this method you can call Form.add_error() if there are any issues you want to handle.
In Django 2.0 you can pass object (User in your case) from the view to the form like this (you have to retrieve obj from the DB first):
form = AccountDetailsForm(initial={'adminuser': adminuser})
It will give you a default selected object (answers your 2) question)

Django, query filtering from model method

I have these models:
def Foo(Models.model):
size = models.IntegerField()
# other fields
def is_active(self):
if check_condition:
return True
else:
return False
def Bar(Models.model):
foo = models.ForeignKey("Foo")
# other fields
Now I want to query Bars that are having active Foo's as such:
Bar.objects.filter(foo.is_active())
I am getting error such as
SyntaxError at /
('non-keyword arg after keyword arg'
How can I achieve this?
You cannot query against model methods or properties. Either use the criteria within it in the query, or filter in Python using a list comprehension or genex.
You could also use a custom manager. Then you could run something like this:
Bar.objects.foo_active()
And all you have to do is:
class BarManager(models.Manager):
def foo_active(self):
# use your method to filter results
return you_custom_queryset
Check out the docs.
I had similar problem: I am using class-based view object_list and I had to filter by model's method. (storing the information in database wasn't an option because the property was based on time and I would have to create a cronjob and/or... no way)
My answer is ineffective and I don't know how it's gonna scale on larger data; but, it works:
q = Model.objects.filter(...)...
# here is the trick
q_ids = [o.id for o in q if o.method()]
q = q.filter(id__in=q_ids)
You can't filter on methods, however if the is_active method on Foo checks an attribute on Foo, you can use the double-underscore syntax like Bar.objects.filter(foo__is_active_attribute=True)

Django, ModelChoiceField() and initial value

I'm using something like this:
field1 = forms.ModelChoiceField(queryset=...)
How can I make my form show the a value as selected?
If you want to set the default initial value you should be defining initial like other form fields except you set it to the id instead.
Say you've got field1 like this:
class YourForm(forms.Form):
field1 = forms.ModelChoiceField(queryset = MyModel.objects.all() )
then you need to set initial when you create your form like this:
form = YourForm(initial = {'field1': instance_of_mymodel.pk })
rather than:
form = YourForm(initial = {'field1': instance_of_mymodel })
I'm also assuming you've defined __unicode__ for your models so this displays correctly.
You can just use
field1 = forms.ModelChoiceField(queryset=..., initial=0)
to make the first value selected etc. It's more generic way, then the other answer.
The times they have changed:
The default initial value can now be set by defining initial like other form fields except you set it to the id instead.
Now this will suffice:
form = YourForm(initial = {'field1': instance_of_mymodel })
Though both still work.
The code
form = YourForm(initial = {'field1': instance_of_mymodel.pk })
and
form = YourForm(initial = {'field1': instance_of_mymodel })
or initial field directly following:
field1 = forms.ModelChoiceField(queryset=..., initial=0)
All work.
The first two ways will override the final way.
field1 = forms.ModelChoiceField(queryset=Model.objects.all(), empty_label="Selected value")
It's as simple as that....!
Just want to add this answer after stumbling on this question. I know it works on Django 3.2, at least.
If you have some calculated value in the __init__ method, you can do this to set the initial value at instantiation as well:
def __init__(self, value, *args, **kwargs):
# super call, etc.
self.do_something(value)
self.fields['field'].initial = value
If the form does multiple things with value, it's a bit more DRY to pass it only once instead of redundantly with the initial kwarg in instantiation.
You could do this as well:
form = YourForm(initial = {'field1': pk })
if you are parsing your primary key through a query string or via an ajax call no need for an instance, the query set has already handled that for your drop down, the pk indexes the state you want