I know there isn't MultipleChoiceField for a Model, you can only use it on Forms.
Today I face an issue when analyzing a new project related with Multiple Choices.
I would like to have a field like a CharField with choices with the option of multiple choice.
I solved this issue other times by creating a CharField and managed the multiple choices in the form with a forms.MultipleChoiceField and store the choices separated by commas.
In this project, due to configuration, I cannot do it as I mention above, I need to do it in the Models, and I prefer NOT to edit the Django admin form neither use forms. I need a Model Field with multiple choices option
Have someone solved anything like this via Models ?
Maybe overriding some of the models function or using a custom widget... I don't know, I'm kinda lost here.
Edit
I'm aware off simple choices, I would like to have something like:
class MODEL(models.Model):
MY_CHOICES = (
('a', 'Hola'),
('b', 'Hello'),
('c', 'Bonjour'),
('d', 'Boas'),
)
...
...
my_field = models.CharField(max_length=1, choices=MY_CHOICES)
...
but with the capability of saving multiple choices not only 1 choice.
You need to think about how you are going to store the data at a database level. This will dictate your solution.
Presumably, you want a single column in a table that is storing multiple values. This will also force you to think about how you will serialize - for example, you can't simply do comma separated if you need to store strings that might contain commas.
However, you are probably best off using a solution like django-multiselectfield
In case You are using Postgres consider using ArrayField.
from django.db import models
from django.contrib.postgres.fields import ArrayField
class WhateverModel(models.Model):
WHATEVER_CHOICE = u'1'
SAMPLE_CHOICES = (
(WHATEVER_CHOICE, u'one'),
)
choices = ArrayField(
models.CharField(choices=SAMPLE_CHOICES, max_length=2, blank=True, default=WHATEVER_CHOICE),
)
From the two, https://pypi.python.org/pypi/django-select-multiple-field/ looks more well rounded and complete. It even has a nice set of unittests.
The problem I found is that it throws a Django 1.10 deprecation warning in the class that implements the model field.
I fixed this and sent a PR. The latest code, until they merge my PR (if they ever decide to hehe) is in my fork of the repo, here: https://github.com/matiasherranz/django-select-multiple-field
Cheers!
M.-
In Your Case, I used ManyToManyField
It Will be something like that:
class MY_CHOICES(models.Model)
choice = models.CharField(max_length=154, unique=True)
class MODEL(models.Model):
...
...
my_field = models.ManyToManyField(MY_CHOICES)
So, now you can select multiple choices
You can use an IntegerField for the model and powers of two for the choices (a bitmap field). I'm not sure why Django doesn't have this already built-in.
class MyModel(models.Model):
A = 1
B = 2
C = 4
MY_CHOICES = ((A, "foo"), (B, "bar"), (C, "baz"))
my_field = models.IntegerField(default=0)
from functools import reduce
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
# it can be set to required=True if needed
my_multi_field = forms.TypedMultipleChoiceField(
coerce=int, choices=MyModel.MY_CHOICES, required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['my_multi_field'].initial = [
c for c, _ in MyModel.MY_CHOICES
if self.instance.my_field & c
]
def save(self, *args, **kwargs):
self.instance.my_field = reduce(
lambda x, y: x | y,
self.cleaned_data.get('my_multi_field', []),
0)
return super().save(*args, **kwargs)
It can be queried like this: MyModel.objects.filter(my_field=MyModel.A | MyModel.C) to get all records with A and C set.
If you want the widget to look like a text input and still be able to allow selecting several options from suggestions, you might be looking for Select2. There is also django-select2 that integrates it with Django Forms and Admin.
Postgres only.
Quite late but for those who come across this
based on #lechup answer i came across this gist.
Take a look at that gist there more improved versions there
from django import forms
from django.contrib.postgres.fields import ArrayField
class ChoiceArrayField(ArrayField):
"""
A field that allows us to store an array of choices.
Uses Django 1.9's postgres ArrayField
and a MultipleChoiceField for its formfield.
Usage:
choices = ChoiceArrayField(models.CharField(max_length=...,
choices=(...,)),
default=[...])
"""
def formfield(self, **kwargs):
defaults = {
'form_class': forms.MultipleChoiceField,
'choices': self.base_field.choices,
}
defaults.update(kwargs)
# Skip our parent's formfield implementation completely as we don't
# care for it.
# pylint:disable=bad-super-call
return super(ArrayField, self).formfield(**defaults)
Which then i saw it in another production code in one of my other projects.. it worked so well that i thought it was from Django's default fields. I was googling just to find the Django docs that i came here. :)
The easiest way I found (just I use eval() to convert string gotten from input to tuple to read again for form instance or other place)
This trick works very well
#model.py
class ClassName(models.Model):
field_name = models.CharField(max_length=100)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.field_name:
self.field_name= eval(self.field_name)
#form.py
CHOICES = [('pi', 'PI'), ('ci', 'CI')]
class ClassNameForm(forms.ModelForm):
field_name = forms.MultipleChoiceField(choices=CHOICES)
class Meta:
model = ClassName
fields = ['field_name',]
#view.py
def viewfunction(request, pk):
ins = ClassName.objects.get(pk=pk)
form = ClassNameForm(instance=ins)
if request.method == 'POST':
form = form (request.POST, instance=ins)
if form.is_valid():
form.save()
...
Related
I recently added a source='get_fieldname_display to my serializer. It worked perfectly for the purpose of obtaining the display value of a choices tuple but now I can no longer POST data using the API end point without getting an error:
TypeError: 'get_fieldname_display' is an invalid keyword argument for this function
To be clear, the addition to the serializer was this line specifically:
fieldName = serializers.CharField(source='fieldName_display')
I know that this line is causing the problem because when I comment it out, I can POST data without a problem. However, I need this line in there so I may obtain the display names from the choices tuple when I am GETting data.
I think the problem may be remedied if I use two different serializers, one for GET and another for POST, but I am not sure how to go about doing this--I am using a generics.ListCreateAPIView in my views.py.
EDIT:
My model looks like this:
class MakeObjects(models.Model):
FIELD_NAME_CHOICES = (
("01", "Choice 1"),
("02", "Choice 2"),
)
fieldname = CharField(choices = FIELD_NAME_CHOICES)
My serializer looks like this:
class ObjectSerializer(serializers.ModelSerializer):
fieldname = serializers.CharField(source='get_fieldname_display')
class Meta:
model = MakeObjects
fields = ('__all__')
To achieve that, you need custom serializer field.
Here's the snippet for python 3:
class DisplayNameWritableField(serializers.ChoiceField):
def __init__(self, **kwargs):
self.html_cutoff = kwargs.pop('html_cutoff', self.html_cutoff)
self.html_cutoff_text = kwargs.pop('html_cutoff_text', self.html_cutoff_text)
self.allow_blank = kwargs.pop('allow_blank', False)
super(ChoiceField, self).__init__(**kwargs)
def to_representation(self, value):
return self.choices.get(value, value)
def bind(self, field_name, parent):
super().bind(field_name, parent)
self.choices = parent.Meta.model._meta.get_field(field_name).choices
Then:
class YourModelSerializer(serializers.ModelSerializer):
your_choiced_model_field = DisplayNameWritableField()
This is a bit hacky though, so not all auto-docs engines detect choices correctly, even though this works pretty well.
You might also like https://github.com/encode/django-rest-framework/issues/1755
you can try adding the new field in:
read_only_fields = ['fieldName']
Let's assume I have a model like this:
class Data(models.Model):
a = models.CharField()
b = models.CharField()
c = models.IntegerField()
I would like to setup a serializer in such a way that it automatically fills in field c and it is not required for a POST. I tried to overwrite the create function of the serializer, but it doesn't work:
class DataSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Data
fields = ('a', 'b')
def create(self, validated_data, **kwargs):
Data.objects.c = 5
return Data.objects.create(**validated_data)
However, if I try this, I end up with an IntegrityError: NOT NULL constraint failed: model_data.c. What is the syntax that I have to use here?
EDIT: Updated formatting.
The reason you're getting the error because field c is not set to null = True - as such an error is raised at the validation stage even before the serializer hits the create method.
Bear in mind that the process goes like this:
Submit serializer data
field-level validation happens - this includes checks for null integrity, min/max length etc and also any custom field validations defined in def validate_<field_name>
object-level validation happens - this calls the def validate method
validated data is passed to the save method, depending on how you designed the serializer - it will save the instance, or route the data to either create or update
All of the info regarding this can be found in Django's and DRF's docs.
A few things to consider:
are you setting a global default for that field? If so, set the default in your models - c = models.IntegerField(default=a_number_or_a_callable_that_returns_an_integer)
do you intend to display the field? If so, include c in your fields and add one more Meta attribute - read_only_fields = ('c',)
If it's neither of the above, you might want to override the validate_c method
Apologies for the poor formatting, typing it on my phone - will update once I get to a computer
In your code Data.objects.c = 5 does nothing.
If you want to set this value yourself use validated_data['c'] = 5 or Data.objects.create(c=5, **validated_data) (just not both at the same time).
Rather than doing this in the serializer, there are hooks in the generic views that allow you to pass values to the serializer. So in your case you might have:
class DataViewSet(ModelViewSet):
# ...
def perform_create(self, serializer):
serializer.save(c=5)
See the "Save and deletion hooks" section here
The thing is quite obvious to my mind, still I can get it working.
Previously I tried to get the filtered model instances from MultipleModelChoiceField by overriding the __init__ method and it worked as expected. Now I need to get only pk from those instances and I decided to do it in MultipleChoiceField. I try to do it the following way but do not succeed:
class AnswerForm(forms.Form):
answers = forms.MultipleChoiceField(
choices = [answer.pk for answer in Answer.objects.all()],
widget = forms.CheckboxSelectMultiple,
)
def __init__(self, *args, **kwargs):
q_pk = kwargs.pop('q_pk')
super(AnswerForm, self).__init__(*args, **kwargs)
self.fields['answers'].choices = [answer.pk for answer in Answer.objects.filter(question__pk=q_pk)]
In a nutshell: don't do this, stick with ModelMultipleChoiceField.
It obviously won't work because choices expects a list of tuples. Taking that in account, [answer.pk for answer in Answer.objects.filter(question__pk=q_pk)] can be rewritten like Answer.objects.filter(question__pk=q_pk).values_list('pk', 'someotherfield'), which brings you back to what ModelMultipleChoiceField does.
Many thanks to Ivan for his pointing me at using ModelChoiceField.
It is my inattention, since I only now figured out that I need some other model fields (except pk) to be passed to the form as well.
In that case the best way, that I found to get the model primary key as a value of a chosen input(s) is to get the entire models from form first and then iterate them to get the desired field value as follows:
forms.py
class AnswerForm(forms.Form):
answer = forms.ModelMultipleChoiceField(
queryset = Answer.objects.all(),
widget = forms.CheckboxSelectMultiple,
)
def __init__(self, *args, **kwargs):
q_pk = kwargs.pop('q_pk', None)
super(AnswerForm, self).__init__(*args, **kwargs)
self.fields['answer'].queryset = Answer.objects.filter(question__pk=q_pk)
views.py
checked = [answer.pk for answer in form.cleaned_data['answer']]
Edited with own answer: original question below.
In here it recommends a few approaches, and the simplest one to me is to just add an extra field but override save to update it, then I get standard functionality for free. So my app is now:
#views.py
highest_p2w = Car.objects.filter().order_by('-p2w')[0]
lowest_p2w = Car.objects.filter().order_by('p2w')[0]
.
#models.py
p2w = models.FloatField("Power to weight ratio",editable=False)
def save(self, *args, **kwargs):
self.p2w = (float(self.bhp) * 1000 ) / float(self.weight)
super(Car, self).save(*args, **kwargs)
The only disadvantage is that I had to do a save() on any existing records to update the value. But any new records enter the value at save() time.
Original Question
I'd like to have a dynamic value which is calculated from 2 fields returned in Django from the model.
I think I can do this with a method, but I need to be able to sort on it just like the other fields.
#Models.py:
class Car(models.Model):
weight = models.IntegerField("Weight in KG")
bhp = models.IntegerField("BHP")
I'd like to have a field called power_to_weight_ratio that just calculates ( self.bhp * 1000 ) / self.weight
As this is a dynamic value, it doesn't need to be stored. BUT it does need to be sortable, as I sorted on all the other fields in the model.
I'd think I could just do something like
power_to_weight = ( self.bhp * 1000 ) / self.weight
but I assume I need to start overriding methods to give me the ability to sort. Django docs don't seem to mention this in the model custom field documentation.
Thanks.
I am so glad that I've done it! ^_^ free to enjoy it
I've tried the following that is most similar to satisfy your need (tested sqlite3 database)
admin.py
from django.contrib import admin
from .models import Car
class CarAdmin(admin.ModelAdmin):
list_display = ('weight','bhp','power_to_weight2',)
def queryset(self,request):
return super(CarAdmin,self).queryset(request).extra(select={'ptw':'(CAST((bhp) AS FLOAT))/weight'})
def power_to_weight2(self,obj):
return obj.bhp*1000/float(obj.weight)#python 2.x,float not need in python3.x
power_to_weight2.short_description = 'power_to_weight'
power_to_weight2.admin_order_field = 'ptw'
admin.site.register(Car,CarAdmin)
about model.objects.extra() see: https://docs.djangoproject.com/en/dev/ref/models/querysets/#extra
When query database, / means you are doing integer division
so use CAST AS FLOAT to convert it to float,detail see here: What is wrong with this SQL Server query division calculation?
There's photologue application, simple photo gallery for django, implementing Photo and Gallery objects.
Gallery object has ManyToMany field, which references Photo objects.
I need to be able to get list of all Photos for a given Gallery. Is it possible to add Gallery filter to Photo's admin page?
If it's possible, how to do it best?
You need to write a custom FilterSpec! Custom Filter in Django Admin on Django 1.3 or below
It'll look like this:
from django.contrib.admin.filterspecs import RelatedFilterSpec, FilterSpec
from models import Gallery
class GalleryFilterSpec(RelatedFilterSpec):
def __init__(self, f, request, params, model, model_admin):
self.lookup_kwarg = f.name
self._lookup_model = f.rel.to
self.lookup_val = request.GET.get(self.lookup_kwarg, None)
self.user = request.user
self.lookup_choices = [(g.pk, g.name) for g in Gallery.objects.all()]
def has_output(self):
return len(self.lookup_choices) > 1
def title(self):
return self._lookup_model._meta.verbose_name
FilterSpec.filter_specs.insert(0,
(lambda f: f.rel.to == Gallery, GalleryFilterSpec))
Put it in a module filters.py in your app package and import it in you admin.py (it's important to import it, so that the filter becomes registered on the admin site!)
EDIT: "f" is the field instance, in this case models.ManyToManyField The last line registers the FilterSpec for all fields that have a relation to the Gallery model. This will not work as you mentioned if the field is defined on the Gallery model, since django.contrib.admin.views.main.ChangeList.get_filters checks if the field you define in the list really exist on the model (doesnt work for related_name either). I think the easiest way around is that you could make a custom template for that changelist and hardcode your filter in there, the FilterSpec itself isn't need for the filtering itself, django uses just the url get parameters for that!
Well, that's how I've done it.
I made custom admin template "change_list.html". Custom template tag creates a list of all existing galleries. Filtering is made like this:
class PhotoAdmin(admin.ModelAdmin):
...
def queryset(self, request):
if request.COOKIES.has_key("gallery"):
gallery = Gallery.objects.filter(title_slug=request.COOKIES["gallery"])
if len(gallery)>0:
return gallery[0].photos.all()
return super(PhotoAdmin, self).queryset(request)
Cookie is set with javascript.
For future reference for others to find, if you have a relationship it's bi-directional, so you can get the photos for galleries or the galleries for a photo via a ModelAdmin.
Let's say you have a changelist view for your Photo model:
from django.contrib import admin
from yourapp.models import Photo
class PhotoAdmin(admin.ModelAdmin):
list_filter = ('galleries', )
admin.site.register(Photo, PhotoAdmin)
Then in the admin you'll see a filter showing all of the galleries and if you click one it'll filter the list to show you only photos for that gallery.
Of course, this may not be practical if there are a LOT of galleries, but you can get there just by using the well-documented ModelAdmin rather than hacking together a template or filterspec.
http://docs.djangoproject.com/en/dev/ref/contrib/admin/#modeladmin-objects
#Jough Dempsey pointed out you maybe don't need a custom FilterSpec just for m2m fields.
However today I found I wanted one for a django-taggit tag field. The tags are basically an m2m relation but it complains that 'TaggableManager' object has no attribute 'get_choices' if you try and add the tag field into list_filter.
In this case it was #lazerscience's code to the rescue...
However it didn't work when used against Django 1.3, needed a couple of new lines added, compare my version below which works:
class TagFilterSpec(RelatedFilterSpec):
def __init__(self, f, request, params, model, model_admin, field_path=None):
super(RelatedFilterSpec, self).__init__(
f, request, params, model, model_admin, field_path=field_path)
self.lookup_title = f.verbose_name # use field name
self.lookup_kwarg = f.name
self.lookup_kwarg_isnull = '%s__isnull' % (self.field_path)
self._lookup_model = f.rel.to
self.lookup_val = request.GET.get(self.lookup_kwarg, None)
self.lookup_val_isnull = request.GET.get(
self.lookup_kwarg_isnull, None)
self.user = request.user
self.lookup_choices = [(g.pk, g.name) for g in Tag.objects.all()]
def has_output(self):
return len(self.lookup_choices) > 1
def title(self):
return self._lookup_model._meta.verbose_name
FilterSpec.filter_specs.insert(0,
(lambda f: f.rel.to == Tag, TagFilterSpec))