I have the following example where I have buildings(address, location,...) and apartments(name, size, type, building). One building containing multiple apartments.
class BuildingAdmin(admin.ModelAdmin):
inlines = [ApartmentInline,]
class ApartmentInline(admin.StackedInline):
def get_formset(self, request, obj=None, **kwargs):
formset = super(ApartmentInline, self).get_formset(request, obj=None, **kwargs)
#Here i'd like to see the values of inline fields, for example size or building that.
#Similar to how one can access ModelAdmin fields with obj.location within get_form
formset.form.base_fields["type"].widget = SelectMultiple(choices=custom_choices)
return formset
I'd like to be able to get the current apartments instance and field values when editing the object (for example size), so that I can create custom choices (querying other DB's or API's) for another field (type).
To modify the inline form widget you can override ModelInlineForm which gives each inline instance access after initializing.
from django.contrib import admin
from django.urls import resolve
class ApartmentInlineForm(forms.ModelForm):
class Meta:
model = A
fields = (
'...',
'...',
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# calculate widget choices
self.fields['type'].widget = SelectMultiple(choices=custom_choices)
class ApartmentInline(admin.StackedInline):
model = Apartment
form = ApartmentInlineForm
Related
Referring to Django - one-to-one modelAdmin i am still searching for a solution to my problem with the admin interface of Django and my OneToOne relationship.
I have the following model which extends the standard User model with an additional attribute is_thing_staff:
class ThingStaff(models.Model):
""" Extends the django user model by a separate model relationship which holds additional user
attributes
"""
user = models.OneToOneField(User, on_delete=models.CASCADE)
# by default a new user is not a staff member which take care of the thing administration
is_thing_staff = models.BooleanField(default=False)
def __str__(self):
return u"{}".format(self.user.username)
class Meta:
verbose_name = "Thing Staff"
verbose_name_plural = "Thing Staff"
If i create a new ThingStaff object in the django admin interface, i can select all users, even if there is already a relationship for a user. Saving a new object with a duplicate association to a user results in an error, that there is already an ThingStaff object associated with that User. So far this is more or less ok.
But why show up possible selections if they would result in an error in the next step? So i excluded them via
from django import forms
from django.contrib import admin
from .models import ThingStaff
class ThingStaffForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(ThingStaffForm, self).__init__(*args, **kwargs)
self.fields['user'].queryset = User.objects.exclude(
id__in=ThingStaff.objects.values_list('user_id', flat=True)
)
#admin.register(ThingStaff)
class ThingStaffAdmin(admin.ModelAdmin):
form = ThingStaffForm
Great so far: The already associated users will not show up in the dropdown during the creation of a new ThingStaff object.
But if i want to change an existing association, the related user will also not show up in the dropdown which makes it impossible to reset the is_thing_staff flag.
So my question is: How can i enable this specific user again for the change view in the django admin interface?
Django's ModelForm distinguishes between add and change views (each one has it's on own method). This means that you can override it:
class ThingStaffAdmin(ModelAdmin):
def add_view(self, *args, **kwargs):
self.form = ThingStaffAddForm
return super().add_view(*args, **kwargs)
def change_view(self, *args, **kwargs):
self.form = ThingStaffChangeForm
return super().change_view(*args, **kwargs)
More in the docs:
https://docs.djangoproject.com/en/2.1/ref/contrib/admin/#django.contrib.admin.ModelAdmin.change_view
your exclution list must be updated and selected user for this ThingStaff must not excluded
update your form like this
class ThingStaffForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(ThingStaffForm, self).__init__(*args, **kwargs)
exclude_user = ThingStaff.objects.all()
if self.instance:
exclude_user = exclude_user.exclude(pk=self.instance.pk)
self.fields['user'].queryset = User.objects.exclude(id__in=exclude_user.values('user_id'))
this code check if current form is edit form and have an instance exclude that from exclude list.
I'd like to fill one field of my model automatically. It holds a client IP.
I've defined an CreateView as follows:
class MyView(CreateView):
def post(self, request, *args, **kwargs):
self.form_class.client_ip = request.META.get('REMOTE_ADDR')
super(MyView, self).post(request, *args, **kwargs)
model = MyModel
form_class = MyForm
and MyForm in that way:
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
exclude = ('ip',)
And I have no idea how to fill this exluded field.
In MyView you should add a method called get_initial which returns the initial values of the form (as a dictionary). For example:
def get_initial(self):
return { 'ip': ... }
These initial values are then used when the form is created.
excluded fields have be to manually filled. you can look into the middleware processors that can add to the request.POST dict and you can override the __init__ on the MyForm, [first call super] and then set the model's ip field.
the middleware processor can also add to POST some other request level attributes, making it available for future use.
I have a model, one field of it is a ForeignKey, so i see select in django admin, is it possiable to customize labels of this select?
class Model(models.Model):
name = models.CharField()
def __unicode__(self):
return self.name
class Part(models.Model):
name = models.CharField()
parent = model.ForeignKey(Model)
def __unicode__(self):
return self.name
def name_with_model(self):
return self.name + ' ' + parent.name
class SmallPart(models.Model):
name = models.CharField()
parent = model.ForeignKey(Part)
when I add new SmallPart I see select tag with names of parts, I need to see name_with_model
If you mean the field label:
using code from:
Django Admin - Overriding the widget of a custom form field
# forms.py
from django import forms
from django.contrib import admin
class ProductAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(ProductAdminForm, self).__init__(*args, **kwargs)
self.fields['tags'].label = 'Custom Label'
Then, in your ModelAdmin object, you specify the form:
from django.contrib import admin
from models import Product
from forms import ProductAdminForm
class ProductAdmin(admin.ModelAdmin):
form = ProductAdminForm
admin.site.register(Product, ProductAdmin)
If you mean the labels in the select drop down:
Override the widget like in the answer above.
edit:
The default form field for a fk field is a model choice field. From the docs
The unicode method of the model
will be called to generate string
representations of the objects for use
in the field's choices; to provide
customized representations, subclass
ModelChoiceField and override
label_from_instance. This method will
receive a model object, and should
return a string suitable for
representing it. For example:
class MyModelChoiceField(ModelChoiceField):
def label_from_instance(self, obj):
return obj.name_with_model()
and then:
class SmallPartAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(SmallPartAdminForm, self).__init__(*args, **kwargs)
self.fields['parent'] = MyModelChoiceField(queryset=Part.objects.all())
A Django autofield when displayed using a formset is hidden by default. What would be the best way to show it?
At the moment, the model is declared as,
class MyModel:
locid = models.AutoField(primary_key=True)
...
When this is rendered using Django formsets,
class MyModelForm(ModelForm):
class Meta:
model = MyModel
fields = ('locid', 'name')
it shows up on the page as,
<input id="id_form-0-locid" type="hidden" value="707" name="form-0-locid"/>
Thanks.
Edit
I create the formset like this -
LocFormSet = modelformset_factory(MyModel)
pformset = LocFormSet(request.POST, request.FILES, queryset=MyModel.objects.order_by('name'))
Second Edit
Looks like I'm not using the custom form class I defined there, so the question needs slight modification..
How would I create a formset from a custom form (which will show a hidden field), as well as use a custom queryset?
At the moment, I can either inherit from a BaseModelFormSet class and use a custom query set, or I can use the ModelForm class to add a custom field to a form. Is there a way to do both with a formset?
Third Edit
I'm now using,
class MyModelForm(ModelForm):
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
locid = forms.IntegerField(min_value = 1, required=True)
self.fields['locid'].widget.attrs["type"] = 'visible'
self.queryset = MyModel.objects.order_by('name')
class Meta:
model = MyModel
fields = ('locid', 'name')
LocFormSet = modelformset_factory(MyModel, form = MyModelForm)
pformset = LocFormSet()
But this still doesn't
Show locid
Use the custom query that was specified.
Try changing the default field type:
from django import forms
class MyModelForm(ModelForm):
locid = forms.IntegerField(min_value=1, required=True)
class Meta:
model = MyModel
fields = ('locid', 'name')
EDIT: Tested and works...
As you say, you are not using the custom form you have defined. This is because you aren't passing it in anywhere, so Django can't know about it.
The solution is simple - just pass the custom form class into modelformset_factory:
LocFormSet = modelformset_factory(MyModel, form=MyModelForm)
Edit in response to update 3:
Firstly, you have the redefinition for locid in the wrong place - it needs to be at the class level, not inside the __init__.
Secondly, putting the queryset inside the form won't do anything at all - forms don't know about querysets. You should go back to what you were doing before, passing it in as a parameter when you instantiate the formset. (Alternatively, you could define a custom formset, but that seems like overkill.)
class MyModelForm(ModelForm):
locid = forms.IntegerField(min_value=1, required=True)
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
self.fields['locid'].widget.attrs["type"] = 'visible'
class Meta:
model = MyModel
fields = ('locid', 'name')
LocFormSet = modelformset_factory(MyModel, form = MyModelForm)
pformset = LocFormSet(request.POST, request.FILES,
queryset=MyModel.objects.order_by('name')))
Okay, none of the approaches above worked for me. I solved this issue from the template side, finally.
There is a ticket filed (http://code.djangoproject.com/ticket/10427), which adds a "value" option to a template variable for a form. For instance, it allows,
{{form.locid.value}}
to be shown. This is available as a patch, which can be installed in the SVN version of django using "patch -p0 file.patch"
Remember, the {{form.locid.value}} variable will be used in conjunction with the invisible form - otherwise, the submit and save operations for the formset will crash.
This is Not the same as {{form.locid.data}} - as is explained in the ticket referred to above.
The reason that the autofield is hidden, is that both BaseModelFormSet and BaseInlineFormSet override that field in add_field. The way to fix it is to create your own formset and override add_field without calling super. Also you don't have to explicitly define the primary key.
you have to pass the formset to modelformset_factory:
LocFormSet = modelformset_factory(MyModel,
formset=VisiblePrimaryKeyFormSet)
This is in the formset class:
from django.forms.models import BaseInlineFormSet, BaseModelFormSet, IntegerField
from django.forms.formsets import BaseFormSet
class VisiblePrimaryKeyFormset(BaseModelFormSet):
def add_fields(self, form, index):
self._pk_field = pk = self.model._meta.pk
if form.is_bound:
pk_value = form.instance.pk
else:
try:
pk_value = self.get_queryset()[index].pk
except IndexError:
pk_value = None
form.fields[self._pk_field.name] = IntegerField( initial=pk_value,
required=True) #or any other field you would like to display the pk in
BaseFormSet.add_fields(self, form, index) # call baseformset which does not modify your primary key field
I am hoping to dynamically update a ModelForm's inline Meta class from my view. Although this code seems to update the exclude list in the Meta class, the output from as_p(), as_ul(), etc does not reflect the updated Meta exclude.
I assume then that the html is generated when the ModelForm is created not when the as_*() is called. Is there a way to force the update of the HTML?
Is this even the best way to do it? I just assumed this should work.
Thoughts?
from django.forms import ModelForm
from testprogram.online_bookings.models import Passenger
class PassengerInfoForm(ModelForm):
def set_form_excludes(self, exclude_list):
self.Meta.exclude = excludes_list
class Meta:
model = Passenger
exclude = []
The Meta class is used to dynamically construct the form definition - so by the time you've created the ModelForm instance, the fields not in the exclude have already been added as the new object's attributes.
The normal way to do it would be to just have multiple class definitions for each possible exclude list. But if you want the form itself to be dynamic, you'll have to create a class definition on the fly. Something like:
def get_form(exclude_list):
class MyForm(ModelForm):
class Meta:
model = Passenger
exclude = exclude_list
return MyForm
form_class = get_form(('field1', 'field2'))
form = form_class()
UPDATE: I just revisited this post and thought I'd post a little more idiomatic way to handle a dynamic class:
def PassengerForm(exclude_list, *args, **kwargs):
class MyPassengerForm(ModelForm):
class Meta:
model = Passenger
exclude = exclude_list
def __init__(self):
super(MyPassengerForm, self).__init__(*args, **kwargs)
return MyPassengerForm()
form = PassengerForm(('field1', 'field2'))
Another way:
class PassengerInfoForm(ModelForm):
def __init__(self, *args, **kwargs):
exclude_list=kwargs.pop('exclude_list', '')
super(PassengerInfoForm, self).__init__(*args, **kwargs)
for field in exclude_list:
del self.fields[field]
class Meta:
model = Passenger
form = PassengerInfoForm(exclude_list=['field1', 'field2'])
Similar approach, somewhat different goal (generic ModelForm for arbitrary models):
from django.contrib.admin.widgets import AdminDateWidget
from django.forms import ModelForm
from django.db import models
def ModelFormFactory(some_model, *args, **kwargs):
"""
Create a ModelForm for some_model
"""
widdict = {}
# set some widgets for special fields
for field in some_model._meta.local_fields:
if type(field) is models.DateField:
widdict[field.name] = AdminDateWidget()
class MyModelForm(ModelForm): # I use my personal BaseModelForm as parent
class Meta:
model = some_model
widgets = widdict
return MyModelForm(*args, **kwargs)
Use modelform_factory (doc):
from django.forms.models import modelform_factory
from testprogram.online_bookings.models import Passenger
exclude = ('field1', 'field2')
CustomForm = modelform_factory(model=Passenger, exclude=exclude) # generates ModelForm dynamically
custom_form = CustomForm(data=request.POST, ...) # form instance