Change fields of ModelForm dynamically - django

Is it possible to change what fields are displayed in a ModelForm, dynamically?
I am trying to show only a small number of fields in a ModelForm when the user adds a new instance (of the Model) from the frontend (using an add form) but larger number of fields when the user edits an instance (using an edit form).
The Form class looks something like this:
class SchoolForm(ModelForm):
class Meta:
model = School
#want to change the fields below dynamically depending on whether its an edit form or add form on the frontend
fields = ['name', 'area', 'capacity', 'num_of_teachers']
widgets = {
'area': CheckboxSelectMultiple
}
labels = {
'name': "Name of the School",
'num_of_teachers': "Total number of teachers",
}
Trying to avoid having two separate classes for add and edit since that doesnt seem DRYish. I found some SO posts with the same question for the admin page where we could override get_form() function but that does not apply here.
Also, this answer suggests using different classes as the normal way and using dynamic forms as an alternative. Perhaps dynamics forms is the way forward here but not entirely sure (I also have overridden __init__() and save() methods on the SchoolForm class).

I'm not suere if is a correct way, but i use some method in class to add fields or delete-it. I used like this:
class someForm(forms.ModelForm):
class Meta:
model = Foo
exclude = {"fieldn0","fieldn1"}
def __init__(self, *args, **kwargs):
super(someForm, self).__init__(*args, **kwargs)
self.fields['foofield1'].widget.attrs.update({'class': 'form-control'})
if self.instance.yourMethod() == "FooReturn":
self.fields['city'].widget.attrs.update({'class': 'form-control'})
else:
if 'city' in self.fields: del self.fields['city']
Hope it helps.

Related

Django Admin overwrite the __str__ method in an autocomplete_field

I want to overwrite the __str__ method in Django admin when using the autocomplete_fields = () but the returned values are using __str__.
I have a form something like
class MyAdminForm(forms.ModelForm):
placement = forms.Select(
choices = Organisation.objects.active(),
)
class Meta:
model = Lead
fields = '__all__'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['placement'].label_from_instance = lambda obj: f'{str(obj)} {obj.post_code}'
This will provide back a Select with the organisation name and post code in the dropdown fields. But there are some 80k choices so I need to using autocomplete. Within within admin.py I have
class LeadAdmin(admin.ModelAdmin):
form = LeadAdminForm
autocomplete_fields = ('placement',)
As soon as I add the autocomplete_fields I lose my postcode and it reverts to just showing the __str__
Hoa can I used autocomplete_fields and overwrite the __str__ method?
This question is answered through Benbb96 comment above which I've copied here so I can close it
So maybe this answer can help you :
stackoverflow.com/a/56865950/8439435 – Benbb96

Django admin: can I define fields order?

I use abstract models in Django like:
class Tree(models.Model):
parent = models.ForeignKey('self', default=None, null=True, blank=True,
related_name="%(app_label)s_%(class)s_parent")
class Meta:
abstract = True
class Genre(Tree):
title = models.CharField(max_length=150)
And all fields from the abstract model go first in Django's admin panel:
parent:
abstract_field2:
title:
model_field2:
...
Is there a way to put them (fields from abstract classes) in the end of the list?
Or a more general way to define order of fields?
You can order the fields as you wish using the ModelAdmin.fields option.
class GenreAdmin(admin.ModelAdmin):
fields = ('title', 'parent')
Building off rbennell's answer I used a slightly different approach using the new get_fields method introduced in Django 1.7. Here I've overridden it (the code would be in the definition of the parent's ModelAdmin) and removed and re-appended the "parent" field to the end of the list, so it will appear on the bottom of the screen. Using .insert(0,'parent') would put it at the front of the list (which should be obvious if you're familiar with python lists).
def get_fields (self, request, obj=None, **kwargs):
fields = super().get_fields(request, obj, **kwargs)
fields.remove('parent')
fields.append('parent') #can also use insert
return fields
This assumes that your fields are a list, to be honest I'm not sure if that's an okay assumption, but it's worked fine for me so far.
I know it's an old question, but wanted to thow in my two cents, since my use case was exactly like this, but i had lots of models inheriting from one class, so didn't want to write out fields for every admin. Instead I extended the get_form model and rearranged the fields to ensure parent always comes at the end of the fields in the admin panel for add/change view.
class BaseAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
form = super(BaseAdmin, self).get_form(request, obj, **kwargs)
parent = form.base_fields.pop('parent')
form.base_fields['parent '] = parent
return form
base_fields is an OrderedDict, so this appends the 'parent' key to the end.
Then, extend this admin for any classes where you want parent to appear at the end:
class GenreAdmin(BaseAdmin):
pass
This simple solution from #acquayefrank (in the comments) worked for me:
The order of fields would depend on the order in which you declare them in your models.

Setting initial values in Form Meta class

Let's suposse I have the following model:
class Example(models.Model):
name = models.CharField(max_length=40)
I want its form to have an initial value for the field 'name', so it could be:
class ExampleForm(forms.ModelForm):
name = forms.CharField(initial="initial_name")
That's good enought for this simple example but in case I have more complex ModelFields (i.e. with overwritten widgets) I'm missing all when re-assigning the field 'name' with the basic forms.CharField.
My question: Is there a way to set the initials in the Meta class, in the same way the widgets can be? something like...
class ExampleForm(forms.ModelForm):
class Meta:
initials = {
'name': 'initial_name',
}
These are the options that I would checkout:
Option 1: Provide initial form data when instantiating the form.
This is the most basic way to do it. In your views.py, simply provide the data with the initial keyword argument.
views.py
from forms import ExampleForm
INITIAL_DATA = {'name': 'initial_name'}
def my_view(request):
...
form = ExampleForm(initial=INITIAL_DATA)
...
Not too tricky. The only downside would be if you use and abuse that form and get tired of passing in the initial data.
Option 2 (ideal for your case): Provide the initial data in the __init__ method of the class.
Forms in Django are designed to accept initial data at instantiation, so you can mess with those values in the __init__ method. Without testing it, this what I imagine it would look like:
forms.py
class ExampleForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
"""If no initial data, provide some defaults."""
initial = kwargs.get('initial', {})
initial['name'] = 'initial_name'
kwargs['initial'] = initial
super(ExampleForm, self).__init__(*args, **kwargs)
This would be optimal because you can ignore the initial={...} in the view code.

Django ModelForms: cleanest way to hide fields and enforce defaults

Let's say I have a single ModelForm which can be filled out by different tiers of users. The Admin can edit any field of that form; but for other users, I need to have certain fields pre-defined, and read-only and/or hidden.
Using my CBV's get_form_kwargs method, I have made the form aware of the user that's bringing it up, and, in its __init__ method I react accordingly, tweaking the form's exclude, and the fields' required and initial properties; and then, in my view's form_valid, I further enforce the values. But, frankly, I'm neither sure that every operation I do is actually needed, nor whether there's some gaping hole I'm not aware of.
So, what's the best, cleanest way of doing this?
Assuming there aren't a lot of combinations, I would create a different form that meets the different needs of your users. Then override def get_form_class and return the correct form based on your needs. This keeps the different use cases separate and gives flexibility if you need to change things in the future without breaking the other forms.
# models.py
class Foo(models.Model):
bar = model.CharField(max_length=100)
baz = model.CharField(max_length=100)
biz = model.CharField(max_length=100)
# forms.py
class FooForm(forms.ModelForm): # for admins
class Meta:
model = Foo
class FooForm(forms.ModelForm): # users who can't see bar
boo = forms.CharField()
class Meta:
model = Foo
exclude = ['bar']
class FooFormN(forms.ModelForm): # as many different scenarios as you need
def __init__(self, *args, **kwargs)
super(FooFormN, self).__init__(*args, **kwargs)
self.fields['biz'].widget.attrs['readonly'] = True
class Meta:
model = Foo
# views.py
class SomeView(UpdateView):
def get_form_class(self):
if self.request.user.groups.filter(name="some_group").exists():
return FooForm
# etc.

field choices() as queryset?

I need to make a form, which have 1 select and 1 text input. Select must be taken from database.
model looks like this:
class Province(models.Model):
name = models.CharField(max_length=30)
slug = models.SlugField(max_length=30)
def __unicode__(self):
return self.name
It's rows to this are added only by admin, but all users can see it in forms.
I want to make a ModelForm from that. I made something like this:
class ProvinceForm(ModelForm):
class Meta:
CHOICES = Province.objects.all()
model = Province
fields = ('name',)
widgets = {
'name': Select(choices=CHOICES),
}
but it doesn't work. The select tag is not displayed in html. What did I wrong?
UPDATE:
This solution works as I wanto it to work:
class ProvinceForm(ModelForm):
def __init__(self, *args, **kwargs):
super(ProvinceForm, self).__init__(*args, **kwargs)
user_provinces = UserProvince.objects.select_related().filter(user__exact=self.instance.id).values_list('province')
self.fields['name'].queryset = Province.objects.exclude(id__in=user_provinces).only('id', 'name')
name = forms.ModelChoiceField(queryset=None, empty_label=None)
class Meta:
model = Province
fields = ('name',)
Read Maersu's answer for the method that just "works".
If you want to customize, know that choices takes a list of tuples, ie (('val','display_val'), (...), ...)
Choices doc:
An iterable (e.g., a list or tuple) of
2-tuples to use as choices for this
field.
from django.forms.widgets import Select
class ProvinceForm(ModelForm):
class Meta:
CHOICES = Province.objects.all()
model = Province
fields = ('name',)
widgets = {
'name': Select(choices=( (x.id, x.name) for x in CHOICES )),
}
ModelForm covers all your needs (Also check the Conversion List)
Model:
class UserProvince(models.Model):
user = models.ForeignKey(User)
province = models.ForeignKey(Province)
Form:
class ProvinceForm(ModelForm):
class Meta:
model = UserProvince
fields = ('province',)
View:
if request.POST:
form = ProvinceForm(request.POST)
if form.is_valid():
obj = form.save(commit=True)
obj.user = request.user
obj.save()
else:
form = ProvinceForm()
If you need to use a query for your choices then you'll need to overwrite the __init__ method of your form.
Your first guess would probably be to save it as a variable before your list of fields but you shouldn't do that since you want your queries to be updated every time the form is accessed. You see, once you run the server the choices are generated and won't change until your next server restart. This means your query will be executed only once and forever hold your peace.
# Don't do this
class MyForm(forms.Form):
# Making the query
MYQUERY = User.objects.values_list('id', 'last_name')
myfield = forms.ChoiceField(choices=(*MYQUERY,))
class Meta:
fields = ('myfield',)
The solution here is to make use of the __init__ method which is called on every form load. This way the result of your query will always be updated.
# Do this instead
class MyForm(forms.Form):
class Meta:
fields = ('myfield',)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Make the query here
MYQUERY = User.objects.values_list('id', 'last_name')
self.fields['myfield'] = forms.ChoiceField(choices=(*MYQUERY,))
Querying your database can be heavy if you have a lot of users so in the future I suggest some caching might be useful.
the two solutions given by maersu and Yuji 'Tomita' Tomita perfectly works, but there are cases when one cannot use ModelForm (django3 link), ie the form needs sources from several models / is a subclass of a ModelForm class and one want to add an extra field with choices from another model, etc.
ChoiceField is to my point of view a more generic way to answer the need.
The example below provides two choice fields from two models and a blank choice for each :
class MixedForm(forms.Form):
speaker = forms.ChoiceField(choices=([['','-'*10]]+[[x.id, x.__str__()] for x in Speakers.objects.all()]))
event = forms.ChoiceField(choices=( [['','-'*10]]+[[x.id, x.__str__()] for x in Events.objects.all()]))
If one does not need a blank field, or one does not need to use a function for the choice label but the model fields or a property it can be a bit more elegant, as eugene suggested :
class MixedForm(forms.Form):
speaker = forms.ChoiceField(choices=((x.id, x.__str__()) for x in Speakers.objects.all()))
event = forms.ChoiceField(choices=(Events.objects.values_list('id', 'name')))
using values_list() and a blank field :
event = forms.ChoiceField(choices=([['','-------------']] + list(Events.objects.values_list('id', 'name'))))
as a subclass of a ModelForm, using the one of the robos85 question :
class MixedForm(ProvinceForm):
speaker = ...