In django inline model admin, how to prefill extra fields with values - python

class MyCustomInline(admin.TabularInline):
min_num = 1
extra = 0
fields = ['matcher', 'param0', 'param1']
model = MyModel
form = MyCustomInlineForm
def get_formset(self, request, obj=None, **kwargs):
extra_fields = {
'param0': forms.CharField(label='First Param', required=False),
'param1': forms.CharField(label='Second Param', required=False)
}
kwargs['form'] = type('MyCustomInline', (MyCustomInlineForm,), extra_fields)
return super(MyCustomInline, self).get_formset(request, obj, **kwargs)
This is basically how I define my inline form so that it has two extra fields - matcher is a standard field in the related table and the inline form handles it automatically. And I save the extra param values in a different storage via overriding the save() in MyCustomInlineForm.
But if I edit an existing record - matcher value appears correctly but I obviously also want to preload the param0 and param1 with the corresponding values. Where can I hook up to do that?

I managed to do it on my own. I also managed to simplify the way I define my custom extra fields, without overriding get_formset method:
class MyCustomInlineForm(forms.ModelForm):
matcher = forms.ChoiceField(choices=[(v['name'], v['name']) for v in matchers], label='Matcher')
param0 = forms.CharField(label='First Param', required=False)
param1 = forms.CharField(label='Second Param', required=False)
def __init__(self, *args, **kwargs):
super(MyCustomInlineForm, self).__init__(*args, **kwargs)
if self.instance.pk:
""" self.instance is the model for the current row.
If there is a pk property that is not None, it means it's not
a new, empty inline model but we are working with existing one."""
self.initial['param0'], self.initial['param1'] = custom_way_to_load_params(self.instance)
def save(self, commit=True):
model = super(MyCustomInlineForm, self).save(True)
param0 = self.cleaned_data['param0']
param1 = self.cleaned_data['param1']
custom_way_to_save_params(model, param0, param1)
return model
class MyCustomInline(admin.TabularInline):
min_num = 1
extra = 0
fields = ['matcher', 'param0', 'param1']
model = MyModel
form = MyCustomInlineForm
If needed - validation of custom params could be done by overriding is_valid() method of forms.ModelForm class and adding errors via self.add_error(). I hope it helps someone.

Related

Create Django Serializer with default fields in Meta and query params

I'm trying to write a Serializer that would take dynamic fields and add them to the restricted number of fields specified in Meta, but it seems that there's no method to "add back" a field to the serializer once it's been created.
Dynamic Fields per Django documentation
class DynamicFieldsModelSerializer(ModelSerializer):
"""
A ModelSerializer that takes an additional `fields` argument that
controls which fields should be displayed.
"""
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
fields = kwargs.pop('fields', None)
# Instantiate the superclass normally
super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)
if fields is not None:
# Drop any fields that are not specified in the `fields` argument.
allowed = set(fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
class BldgLocationAndFilters(DynamicFieldsModelSerializer):
latitude = fields.FloatField(source='lat_016_decimal')
longitude = fields.FloatField(source='long_017_decimal')
class Meta:
model = Bldg
fields = ('latitude', 'longitude')
I'd like to do something that would modify the DynamicFieldsModelSerializer such that fields can be appended to the set that has already been filtered down, but it looks like the Meta fields override everything such that nothing can be added back (fields can only be removed
Pseudocode of desired behavior:
class DynamicFieldsUnionModelSerializer(ModelSerializer):
"""
A ModelSerializer that takes an additional `fields` argument that
controls which fields should be displayed.
"""
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
fields = kwargs.pop('fields', None)
# Instantiate the superclass normally
super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)
if fields is not None:
new_fields = set(fields)
existing = set(self.fields)
unique_new = new_fields.union(existing) - existing
for field_name in unique_new:
self.fields.update(field_name)
If BldgLocationAndFilters was called as serializer = BldLocationAndFilters(fields=['type']), I'd expect the resulting returns to have fields = ('latitude', 'longitude', 'type')
DynamicFieldsModelSerializer only works on removing existing fields, because the implementation depends on fields already built in __init__. You can add fields after the __init__, but you have to build them again somehow (not just add names).
But one way to support this is to override the serializer's get_field_names method which works with unbuilt field names:
class BldgLocationAndFilters(ModelSerializer):
latitude = fields.FloatField(source='lat_016_decimal')
longitude = fields.FloatField(source='long_017_decimal')
class Meta:
model = Bldg
fields = ('latitude', 'longitude')
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
self._fields_to_add = kwargs.pop('fields', None)
super().__init__(*args, **kwargs)
def get_field_names(self, *args, **kwargs):
original_fields = super().get_field_names(*args, **kwargs)
if self._fields_to_add:
return set(list(original_fields) + list(self._fields_to_add))
return original_fields
# Should use latitude, longitude, and type
BldgLocationAndFilters(instance, fields=('type',)).data
Note that this is using just ModelSerializer.
Or just define your serializer with __all__ (while still using DynamicFieldsModelSerializer) and set the fields on a per-need basis:
class BldgLocationAndFilters(DynamicFieldsModelSerializer):
latitude = fields.FloatField(source='lat_016_decimal')
longitude = fields.FloatField(source='long_017_decimal')
class Meta:
model = Bldg
fields = '__all__'
BldgLocationAndFilters(instance, fields=('latitude', 'longitude', 'type')).data

Form Problems - Setting Initial Value

I am trying to set the initial value of a field on a form. The field is not part of the model, but when I try and set it to a value the field is blank. From my research it could be because the form is "bound" which makes some sense to me, but in this case the field is not part of the model.
My form:
#Form for editing profile
class CatForm(forms.ModelForm):
pictureid = forms.CharField()
class Meta:
model = Cat
fields = ['name']
def __init__(self, *args, **kwargs):
picid = kwargs.pop("pictureid")
print(picid)
super(CatForm, self).__init__(*args, **kwargs)
self.fields['pictureid'] = forms.CharField(initial=picid, required=False)
The model:
class Cat(models.Model):
name = models.CharField(max_length=34,null=False)
From the view it is called like this:
catform = CatForm(request.POST, pictureid=instance.id)
I was expecting it to set the field to the value of the initial attribute, but it doesn't. I have tried testing it by directly adding a string, but doesn't set.
This is what seems to be working for me:
class CatForm(forms.ModelForm):
class Meta:
model = Cat
fields = ['name']
def __init__(self, *args, **kwargs):
picid = kwargs.pop("pictureid")
super(CatForm, self).__init__(*args, **kwargs)
self.fields['pictureid'] = forms.CharField(initial=picid)
I also needed to drop the "request.POST" from the call to this when initiating the form.
If you want to render the pictureid in GET request, then you can try like this:
catform = CatForm(initial={'pictureid': instance.id})
For GET request, you don't need to override the __init__ method.
But, if you want to use the Catform in POST request, to use the value of pictureid somewhere else(lets say in save method), then you will need to override __init__ method here.
class CatForm(forms.ModelForm):
pictureid = forms.CharField()
class Meta:
model = Cat
fields = ['name']
def __init__(self, *args, **kwargs):
picid = kwargs.pop("pictureid")
print(picid)
super(CatForm, self).__init__(*args, **kwargs)
self.pictureid = picid
def save(self, *args, **kwargs):
print(self.pictureid) # if you want to use it in save method
return super().save(*args, **kwargs)

Dynamically exclude or include a field in Django REST framework serializer

I have a serializer in Django REST framework defined as follows:
class QuestionSerializer(serializers.Serializer):
id = serializers.CharField()
question_text = QuestionTextSerializer()
topic = TopicSerializer()
Now I have two API views that use the above serializer:
class QuestionWithTopicView(generics.RetrieveAPIView):
# I wish to include all three fields - id, question_text
# and topic in this API.
serializer_class = QuestionSerializer
class QuestionWithoutTopicView(generics.RetrieveAPIView):
# I want to exclude topic in this API.
serializer_class = ExamHistorySerializer
One solution is to write two different serializers. But there must be a easier solution to conditionally exclude a field from a given serializer.
Have you tried this technique
class QuestionSerializer(serializers.Serializer):
def __init__(self, *args, **kwargs):
remove_fields = kwargs.pop('remove_fields', None)
super(QuestionSerializer, self).__init__(*args, **kwargs)
if remove_fields:
# for multiple fields in a list
for field_name in remove_fields:
self.fields.pop(field_name)
class QuestionWithoutTopicView(generics.RetrieveAPIView):
serializer_class = QuestionSerializer(remove_fields=['field_to_remove1' 'field_to_remove2'])
If not, once try it.
Creating a new serializer is the way to go. By conditionally removing fields in a serializer you are adding extra complexity and making you code harder to quickly diagnose. You should try to avoid mixing the responsibilities of a single class.
Following basic object oriented design principles is the way to go.
QuestionWithTopicView is a QuestionWithoutTopicView but with an additional field.
class QuestionSerializer(serializers.Serializer):
id = serializers.CharField()
question_text = QuestionTextSerializer()
topic = TopicSerializer()
class TopicQuestionSerializer(QuestionSerializer):
topic = TopicSerializer()
You can set fields and exclude properties of Meta
Here is an Example:
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
class Meta:
model = User
exclude = ['id', 'email', 'mobile']
def __init__(self, *args, **kwargs):
super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)
# #note: For example based on user, we will send different fields
if self.context['request'].user == self.instance.user:
# Or set self.Meta.fields = ['first_name', 'last_name', 'email', 'mobile',]
self.Meta.exclude = ['id']
Extending above answer to a more generic one
class QuestionSerializer(serializers.Serializer):
def __init__(self, *args, **kwargs):
fields = kwargs.pop('fields', None)
super(QuestionSerializer, self).__init__(*args, **kwargs)
if fields is not None:
allowed = set(fields.split(','))
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
class QuestionWithoutTopicView(generics.RetrieveAPIView):
def get_serializer(self, *args, **kwargs):
kwargs['context'] = self.get_serializer_context()
fields = self.request.GET.get('display')
serializer_class = self.get_serializer_class()
return serializer_class(fields=fields,*args, **kwargs)
def get_serializer_class(self):
return QuestionSerializer
Now we can give a query parameter called display to output any custom display format http://localhost:8000/questions?display=param1,param2
You can use to representation method and just pop values:
def to_representation(self, instance):
"""Convert `username` to lowercase."""
ret = super().to_representation(instance)
ret.pop('username') = ret['username'].lower()
return ret
you can find them here
https://www.django-rest-framework.org/api-guide/serializers/#overriding-serialization-and-deserialization-behavior

How do I programmatically set fields in ModelForm in Django?

From this question I want to convert my form from regular Form to ModelForm so I can take advantage of instance parameter in ModelForm.
Here is my current form code:
class OrderDetailForm(forms.Form):
def __init__(
self,
user,
can_edit_work_type=None,
can_edit_vendor=None,
can_edit_note=None,
*args,
**kwargs
):
super(OrderDetailForm, self).__init__(*args, **kwargs)
if can_edit_work_type:
self.fields['work_type'] = forms.ChoiceField(choices=Order.WORK_TYPE_CHOICES)
if can_edit_vendor:
self.fields['vendor'] = forms.ModelChoiceField(
queryset=Vendor.objects.all(),
empty_label="Choose a vendor",
)
if can_edit_note:
self.fields['note'] = forms.CharField(widget=forms.Textarea)
def clean(self):
super(OrderDetailForm, self).clean()
if 'note' in self.cleaned_data:
if len(self.cleaned_data['note']) < 50:
self._errors['note'] = self.error_class([u"Please enter a longer note."])
del self.cleaned_data['note']
return self.cleaned_data
As you can see, I have some if statements that determine whether the fields even show you in the forms (logically it means certain users can only edit certain parts of the fields).
How would I do that in ModelForm? I understand fields to be a tuple, so it can't be appended like I did in Form. So I want to do something like
class OrderDetailForm(forms.ModelForm):
class Meta:
model = Order
# fields = ('work_type', 'vendor', 'note') I can't do that since I need to be able to control it. See below.
# Can I control widgets even if that field doesn't exist?
widgets = {
'note': forms.Textarea(),
}
def __init__(
self,
user,
can_edit_work_type=None,
can_edit_vendor=None,
can_edit_note=None,
*args,
**kwargs
):
super(OrderDetailForm, self).__init__(*args, **kwargs)
fields = []
if can_edit_work_type:
fields.append('work_type')
if can_edit_vendor:
fields.append('vendor')
if can_edit_note:
fields.append('note')
self.Meta.fields = tuple(fields) # Does this work?
def clean(self):
super(OrderDetailForm, self).clean()
if 'note' in self.cleaned_data:
if len(self.cleaned_data['note']) < 50:
self._errors['note'] = self.error_class([u"Please enter a longer note."])
del self.cleaned_data['note']
return self.cleaned_data
Is that possible? How do you control the fields in ModelForm?
Another possible way is generate a inline form class in the view to exclude fields based on the request, for example, define a normal model form for Order model, called OrderDetailForm:
class OrderDetailForm(forms.ModelForm):
class Meta:
model = Order
fields = ('work_type', 'vendor', 'note')
widgets = {
'note': forms.Textarea(),
}
In the view, for example, edit order, create a customized form based on the OrderDetailForm:
def edit(request, order_id):
order = Order.objects.get(pk=order_id)
can_edit_work_type = bool(request.REQUEST.get('can_edit_work_type', False))
can_edit_vender = bool(request.REQUEST.get('can_edit_vender', False))
can_edit_note = bool(request.REQUEST.get('can_edit_note', False))
exclude_fields = []
if not can_edit_work_type:
exclude_fields.append('work_type')
if not can_edit_vender:
exclude_fields.append('vender')
if not can_edit_note:
exclude_fields.append('note')
class CustomizedOrderForm(OrderDetailForm):
class Meta:
model = Order
exclude = tuple(exclude_fields)
if request.method == 'POST':
form = CustomizedOrderForm(instance=order, data=request.POST)
if form.is_valid():
form.save()
else:
form = CustomizedOrderForm(instance=order)
return render(request, 'order_form.html', {'form': form})
The ModelForm api is very similar to that of the regular Form. The advantage is that you now get model validation in addition to conveniences like default widgets, the instance kwarg, and the save method.
fields attr is still dict-like. You can see fields getting built by the metaclass here. Then, going through the inheritance and calling super() in the BaseModelForm.__init__, we arrive at a deepcopy of the declared fields, originally a SortedDict. This is common to Form and ModelForm, both subclasses of BaseForm.
Put the fields in the exclude and add them the way you are doing in the original __init__.
Clean them the same way.
Then, you can override the save method: you can call super() to get the object back and deal with the data in cleaned_data however you want.
class OrderDetailForm(forms.ModelForm):
# regular fields, not based on bools
# ...
class Meta:
model = Order
exclude = ('work_type', 'vendor', 'note')
# or fields = (...other fields )
def __init__(
self,
user,
can_edit_work_type=None,
can_edit_vendor=None,
can_edit_note=None,
*args,
**kwargs,
):
super(OrderDetailForm, self).__init__(*args, **kwargs)
if can_edit_work_type:
self.fields['work_type'] = forms.ChoiceField(
choices=Order.WORK_TYPE_CHOICES)
if can_edit_vendor:
self.fields['vendor'] = forms.ModelChoiceField(
queryset=Vendor.objects.all(),
empty_label="Choose a vendor",
)
if can_edit_note:
self.fields['note'] = forms.CharField(widget=forms.Textarea)
def clean(self):
# I never call super() in clean .. do I? .. hmmm
# maybe I should or is sth magic going on?
# alternately,
# data = self.cleaned_data
# let's call super though
data = super(OrderDetailForm, self).clean()
if 'note' in data:
if len(data['note']) < 50:
# I raise a validation error so .is_valid() comes back False
# form.errors happens magically ...
raise forms.ValidationError("Not long enough ...")
return data
def save(self, *args, **kwargs):
data = self.cleaned_data
# maybe do some stuff here
# ...
# commit=True or commit=False could be important
order = super(OrderDetailForm, self).save(*args, **kwargs)
if 'note' in data:
order.note = data['note']
# ... do other stuff
# probably ...
order.save()
# respect how model forms work.
return order

Accessing parent model instance from modelform of admin inline

I'm using a TabularInline in Django's admin, configured to show one extra blank form.
class MyChildInline(admin.TabularInline):
model = MyChildModel
form = MyChildInlineForm
extra = 1
The model looks like MyParentModel->MyChildModel->MyInlineForm.
I'm using a custom form so I can dynamically lookup values and populate choices in a field. e.g.
class MyChildInlineForm(ModelForm):
my_choice_field = forms.ChoiceField()
def __init__(self, *args, **kwargs):
super(MyChildInlineForm, self).__init__(*args, **kwargs)
# Lookup ID of parent model.
parent_id = None
if "parent_id" in kwargs:
parent_id = kwargs.pop("parent_id")
elif self.instance.parent_id:
parent_id = self.instance.parent_id
elif self.is_bound:
parent_id = self.data['%s-parent'% self.prefix]
if parent_id:
parent = MyParentModel.objects.get(id=parent_id)
if rev:
qs = parent.get_choices()
self.fields['my_choice_field'].choices = [(r.name,r.value) for r in qs]
This works fine for the inline records bound to an actual record, but for the extra blank form, it doesn't display any values in my choice field, since it doesn't have any record id and there can't lookup the associated MyParentModel record.
I've inspected all the values I could find (args, kwargs, self.data, self.instance, etc) but I can't find any way to access the parent object the tabular inline is bound to. Is there any way to do this?
Update: As of Django 1.9, there is a def get_form_kwargs(self, index) method in the BaseFormSet class. Hence, overriding that passes the data to the form.
This would be the Python 3 / Django 1.9+ version:
class MyFormSet(BaseInlineFormSet):
def get_form_kwargs(self, index):
kwargs = super().get_form_kwargs(index)
kwargs['parent_object'] = self.instance
return kwargs
class MyForm(forms.ModelForm):
def __init__(self, *args, parent_object, **kwargs):
self.parent_object = parent_object
super(MyForm, self).__init__(*args, **kwargs)
class MyChildInline(admin.TabularInline):
formset = MyFormSet
form = MyForm
For Django 1.8 and below:
To pass a value of a formset to the individual forms, you'd have to see how they are constructed. An editor/IDE with "jump to definition" really helps here to dive into the ModelAdmin code, and learn about the inlineformset_factory and it's BaseInlineFormSet class.
From there you'll find that the form is constructed in _construct_form() and you can override that to pass extra parameters. It will likely look something like this:
class MyFormSet(BaseInlineFormSet):
def _construct_form(self, i, **kwargs):
kwargs['parent_object'] = self.instance
return super(MyFormSet, self)._construct_form(i, **kwargs)
#property
def empty_form(self):
form = self.form(
auto_id=self.auto_id,
prefix=self.add_prefix('__prefix__'),
empty_permitted=True,
parent_object=self.instance,
)
self.add_fields(form, None)
return form
class MyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.parent_object = kwargs.pop('parent_object', None)
super(MyForm, self).__init__(*args, **kwargs)
class MyChildInline(admin.TabularInline):
formset = MyFormSet
form = MyForm
Yes, this involves a private _construct_form function.
update Note: This doesn't cover the empty_form, hence your form code needs to accept the parameters optionally.
I'm using Django 1.10 and it works for me:
Create a FormSet and put the parent object into kwargs:
class MyFormSet(BaseInlineFormSet):
def get_form_kwargs(self, index):
kwargs = super(MyFormSet, self).get_form_kwargs(index)
kwargs.update({'parent': self.instance})
return kwargs
Create a Form and pop an atribute before super called
class MyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
parent = kwargs.pop('parent')
super(MyForm, self).__init__(*args, **kwargs)
# do whatever you need to with parent
Put that in the inline admin:
class MyModelInline(admin.StackedInline):
model = MyModel
fields = ('my_fields', )
form = MyFrom
formset = MyFormSet
AdminModel has some methods like get_formsets. It receives an object and returns a bunch of formsets. I think you can add some info about parent object to that formset classes and use it later in formset's __init__
Expanding on ilvar's answer a bit, If the form field of interest is constructed from a model field, you can use the following construction to apply custom behavior to it:
class MyChildInline(admin.TabularInline):
model = MyChildModel
extra = 1
def get_formset(self, request, parent=None, **kwargs):
def formfield_callback(db_field):
"""
Constructor of the formfield given the model field.
"""
formfield = self.formfield_for_dbfield(db_field, request=request)
if db_field.name == 'my_choice_field' and parent is not None:
formfield.choices = parent.get_choices()
return formfield
return super(MyChildInline, self).get_formset(
request, obj=obj, formfield_callback=formfield_callback, **kwargs)
return result

Categories