I'm creating a website and it needs support for internationalization. The default languages are Portuguese, English and Spanish. I'm using the django-i18nmodel and so far it works great.
When the admin wants to create a product, using django-admin, by default I create 3 inLine items of the model ProductI18N.
class LanguageStackedInline(admin.StackedInline):
model = ProductI18N
extra = 1
I want to create these 3 rows with the 3 default languages I mentioned above (pt-PT, en-US, es-ES). I know that in the Model I can only set a default value.
Does Django provide an easy way of doing this?
I'd like to thank uranusjr for giving me a hint for this solution. His answer did not work for me but here is what worked:
class LanguageInlineFormSet(BaseInlineFormSet):
def __init__(self, *args, **kwargs):
kwargs['initial'] = [
{'name': 'pt-PT'}, {'name': 'en-US'}, {'name': 'es-ES'}
]
super(LanguageInlineFormSet, self).__init__(*args, **kwargs)
# Rest of the code as per #uranusjr's answer
class LanguageStackedInline(admin.StackedInline):
model = ProductI18N
extra = 3 # You said you need 3 rows
formset = LanguageInlineFormSet
I kept the 'name' key as is for easy comparison.
To explain in more detail, the BaseInlineFormSet takes the initial argument as documented here:
https://docs.djangoproject.com/en/dev/topics/forms/formsets/#formsets-initial-data
So simply adding it to kwargs in the overloaded constructor works well.
EDIT: Let me also share the code I actually use in my app:
from django.conf import settings
from django.forms.models import BaseInlineFormSet
from myapp.models import MyI18N
class MyI18NInlineFormset(BaseInlineFormSet):
def __init__(self, *args, **kwargs):
kwargs['initial'] = [{'i18n_language': lang[0]}
for lang in settings.LANGUAGES
if lang[0] != settings.LANGUAGE_CODE]
super(MyI18NInlineFormset, self).__init__(*args, **kwargs)
class MyI18NInline(admin.StackedInline):
model = MyI18N
extra = max_num = len(settings.LANGUAGES) - 1
formset = MyI18NInlineFormset
This generates one form per each non-default language. It's not perfect as it doesn't take into account cases where one of the non-default languages is already saved, but it gives me a good starting point.
Provide a custom formset class for the inline admin:
from django.forms.models import BaseInlineFormSet
class LanguageInlineFormSet(BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super(LanguageInlineFormSet, self).__init__(*args, **kwargs)
# Assuming the field you want to populate to is called "name"
self.initial = [
{'name': 'pt-PT'}, {'name': 'en-US'}, {'name': 'es-ES'}
]
class LanguageStackedInline(admin.StackedInline):
model = ProductI18N
extra = 3 # You said you need 3 rows
formset = LanguageInlineFormSet
You can take a look at the documentation on admin and inline formsets for more notes on customization.
Related
I am trying to use a specific list of data in my form with Django. I am using ModelChoiceField to retrieve from the model the data I need to display in the form (to let the users select from a scolldown menu).
My query is complicate because need two filters based on variables passed by views
I've tried to use the sessions but in form is not possible to import the session (based to my knowledge).
form.py
def __init__(self, pass_variables, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['initiative'] = forms.ModelChoiceField(queryset=raid_User_Initiative.objects.filter(company=pass_variables[1], username=pass_variables[0]).values_list('initiative', flat=True))
view.py
pass_variables = ((str(request.user), companyname))
f = Issue_form(pass_variables)
If I don't pass the variable the process works. The problem is with the code above as the form don't provide any error but it doesn't pass the if f.is_valid():
Thanks
I solved myself! Anyway if anyone interested the solution is using the sessions:
form.py
Before I declare the queryset as:
initiative = forms.ModelChoiceField(queryset=raid_User_Initiative.objects.all(), to_field_name="initiative")
And it is very important to use the , to_field_name="initiative"
After I amend the queryset as:
def __init__(self, user, company, *args, **kwargs):
super(raid_Issue_form, self).__init__(*args, **kwargs)
self.fields['initiative'].queryset = raid_User_Initiative.objects.filter(company=company, username=user).values_list('initiative', flat=True)
view.py
f = raid_Issue_form(request.user, request.session['company'])
Hope this help!
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']
I would like to provide different widgets to input form fields for the same type of model field in a Django admin inline.
I have implemented a version of the Entity-Attribute-Value paradigm in my shop application (I tried eav-django and it wasn't flexible enough). In my model it is Product-Parameter-Value (see Edit below).
Everything works as I want except that when including an admin inline for the Parameter-Value pair, the same input formfield is used for every value. I understand that this is the default Django admin behaviour because it uses the same formset for each Inline row.
I have a callback on my Parameter that I would like to use (get_value_formfield). I currently have:
class SpecificationValueAdminInline(admin.TabularInline):
model = SpecificationValue
fields = ('parameter', 'value')
readonly_fields = ('parameter',)
max_num = 0
def get_formset(self, request, instance, **kwargs):
"""Take a copy of the instance"""
self.parent_instance = instance
return super().get_formset(request, instance, **kwargs)
def formfield_for_dbfield(self, db_field, **kwargs):
"""Override admin function for requesting the formfield"""
if self.parent_instance and db_field.name == 'value':
# Notice first() on the end -->
sv_instance = SpecificationValue.objects.filter(
product=self.parent_instance).first()
formfield = sv_instance.parameter.get_value_formfield()
else:
formfield = super().formfield_for_dbfield(db_field, **kwargs)
return formfield
formfield_for_dbfield is only called once for each admin page.
How would I override the default behaviour so that formfield_for_dbfield is called once for each SpecificationValue instance, preferably passing the instance in each time?
Edit:
Here is the model layout:
class Product(Model):
specification = ManyToManyField('SpecificationParameter',
through='SpecificationValue')
class SpecificationParameter(Model):
"""Other normal model fields here"""
type = models.PositiveSmallIntegerField(choices=TUPLE)
def get_value_formfield(self):
"""
Return the type of form field for parameter instance
with the correct widget for the value
"""
class SpecificationValue(Model):
product = ForeignKey(Product)
parameter = ForeignKey(SpecificationParameter)
# To store and retrieve all types of value, overrides CharField
value = CustomValueField()
The way I eventually solved this is using the form = attribute of the Admin Inline. This skips the form generation code of the ModelAdmin:
class SpecificationValueForm(ModelForm):
class Meta:
model = SpecificationValue
def __init__(self, instance=None, **kwargs):
super().__init__(instance=instance, **kwargs)
if instance:
self.fields['value'] = instance.parameter.get_value_formfield()
else:
self.fields['value'].disabled = True
class SpecificationValueAdminInline(admin.TabularInline):
form = SpecificationValueForm
Using standard forms like this, widgets with choices (e.g. RadioSelect and CheckboxSelectMultiple) have list bullets next to them in the admin interface because the <ul> doesn't have the radiolist class. You can almost fix the RadioSelect by using AdminRadioSelect(attrs={'class': 'radiolist'}) but there isn't an admin version of the CheckboxSelectMultiple so I preferred consistency. Also there is an aligned class missing from the <fieldset> wrapper element.
Looks like I'll have to live with that!
I'm using Django MultiSelectField to store a project's category/categories.
My requirement is as follows: A project can have one or more categories. When creating a project initially, the user should be able to select any of the categories listed in the select field. If a user selects 'Not Applicable', then the other categories become disabled.
When editing a project, user should not be able to unselect the originally selected categorie(s), but he can add/select other categories in addition to the original ones. The originally selected categories should be readonly when editing.
My model looks like this:
from multiselectfield import MultiSelectField
(...)
class Project(models.Model):
CAT_0 = 0
CAT_1 = 1
CAT_2 = 2
CAT_3 = 3
PROJECT_CATEGORIES = (
(CAT_0, _('Not Applicable')),
(CAT_1, _('My Category 1')),
(CAT_2, _('My Category 2')),
(CAT_3, _('My Category 3')),
)
(...)
project_categories = MultiSelectField(choices=PROJECT_CATEGORIES, max_choices=3, default=CAT_0)
And my forms.py
class ProjectForm(ModelForm):
class Meta:
model = Project
(...)
def __init__(self, *args, **kwargs):
super(ProjectForm, self).__init__(*args, **kwargs)
(...)
if self.instance.pk:
for choice in self.fields['project_categories'].choices:
if str(choice[0]) in self.instance.project_categories:
#TODO: Make this choice to be readonly i.e. user should not be able to uncheck it
else:
#TODO: Leave the choice as it is i.e. user can select/check it
# This is what I had earlier, but it makes all the choices to be readonly. Not what I want
self.fields['strategic_objectives'].widget.attrs['readonly'] = True
self.fields['strategic_objectives'].widget.attrs['disabled'] = True
How do I make sure that the original categories are shown as readonly?
Is this the way to go about it or JS will be a better option?
Is kinda useless doing that in backend before rendering, because an user can modifies your html code and enable it again.
So, disable them using JS and then in your Backend after user submit the form, you need to check it.
No metters what, have front-end validations is up to you, but you always need to have them in your backend as well.
I had the same problem as you and solved it with the help of this blogpost.
Basically it is necessary to modify the widget which is used to display the choices to be able to mark some choices as disabled.
So I created a widgets.py with the following content:
from django.forms import CheckboxSelectMultiple
class CheckboxSelectMultipleWithDisabled(CheckboxSelectMultiple):
def __init__(self, *args, **kwargs):
self.disabled = kwargs.pop("disabled", set())
super().__init__(*args, **kwargs)
def create_option(self, *args, **kwargs):
options_dict = super().create_option(*args, **kwargs)
if options_dict["value"] in self.disabled:
options_dict["attrs"]["disabled"] = True
return options_dict
Then in the constructor of the form class (ProjectForm) I exchanged the widget of the respective field (project_categories) and used the added disabled keyword argument introduced above:
def __init__(self, *args, **kwargs):
super(ProjectForm, self).__init__(*args, **kwargs)
# fill a local 'disabled' set/list/tuple/... variable using your logic
self.fields['project_categories'].widget = CheckboxSelectMultipleWithDisabled(
choices=self.fields['project_categories'].choices,
disabled=disabled
)
Also do not forget to import the new widget to use it: from .widgets import CheckboxSelectMultipleWithDisabled.
What levi posted is still true, though: you can remove the disabled tag in the html code in your browser and be able to modify the choice again.
So without any extra validation you have to trust your users.
A similar solution is presented in the answer to this question.
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()
...