Django ModelForm overriding __init__ - python

I'm trying to populate a Select list of a ModelForm, with the Django groups the current users belongs to.
No errors arise, but I get only an empty Select list.
This is my code:
class ArchiveForm(forms.ModelForm):
class Meta:
model = Archive
fields = ['tags', 'version', 'sharegp']
localized_fields = None
labels = {'tags': 'Related Keywords'}
sharegp = forms.ChoiceField(label='Share with groups')
def __init__(self, user, *args, **kwargs):
#import pudb;pudb.set_trace()
self.user = user
super(ArchiveForm, self).__init__(*args, **kwargs)
self.fields['sharegp'].queryset = Group.objects.filter(user=self.user)
self.fields['sharegp'].widget.choices = self.fields['sharegp'].choices
Note that if I enable the debugger in the first line of the __init__ method, and step forward all along the function, the line:
self.fields['sharegp'].queryset
Gives the correct list containing the groups for that user, but that is not passed to the actual form.
What could I be missing? Thank you!

This is how I ended up solving this:
I was wrongly choosing the type of the field: The correct one is ModelChoiceField:
class ArchiveForm(forms.ModelForm):
class Meta:
model = Archive
fields = ['tags', 'version', 'sharegp']
localized_fields = None
labels = {'tags': 'Related Keywords'}
user = None
usergroups = None
sharegp = forms.ModelChoiceField(label='Share with groups', queryset=usergroups)
def __init__(self, user, *args, **kwargs):
self.user = user
self.usergroups = Group.objects.filter(user=self.user)
super(ArchiveForm, self).__init__(*args, **kwargs)
self.fields['sharegp'].queryset = self.usergroups

That last line is overwriting the queryset assigned in previous one. Remove it.

Related

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)

Want to exclude some data from ModelForm

I try to make the form with objectl linked to only current base object (exclude others from their model):
forms.py
class RoomTypeForm(ModelForm):
class Meta:
model = RoomType
fields = {'Name', 'Rooms', 'Beds', 'Facilities', 'Capacity'}
exclude = ('Building',)
def __init__(self, *args, **kwargs):
self.building_id = kwargs.pop('building_id')
self.fields['Capacity'].queryset = Capacity.objects.filter(
Building=self.building_id
)
views.py
def building_details(request, hotel_id, building_id):
capacity_form = CapacityForm
roomtype_form = RoomTypeForm
args = {}
args.update(csrf(request))
args['building'] = Building.objects.get(id=building_id)
args['capacity'] = Capacity.objects.filter(Building=building_id)
args['roomtypes'] = RoomType.objects.filter(Building=building_id)
args['capform'] = capacity_form
args['rtform'] = roomtype_form(building_id=building_id)
return render_to_response('building.html', args)
But I have the error:
'RoomTypeForm' object has no attribute 'fields'
in line:
self.fields['Capacity'].queryset =
Capacity.objects.filter(Building=self.building_id)
How may it be possible to make all of this will be working? Please help.
You need to call the super() class in __init__
def __init__(self, *args, **kwargs):
self.building_id = kwargs.pop('building_id')
super(RoomTypeForm, self).__init__(*args, **kwargs)
self.fields['Capacity'].queryset = Capacity.objects.filter(Building=self.building_id)
Or if you are using python3, you could just do:
super().__init__(*args, **kwargs)
Another side note, it is standard practice to specify a list or a tuple in the fields. You specified a set.

How to use "Readonly Field" outside Admin

I have a form that I need to show my project outside the area of administration, some fields can not be edited but can see them.
To do this would be great "AdminReadonlyField" found in "django.contrib.admin.helpers" The problem is that you can not do.
I have tried to create some widget that can replace this complex class, but I can not get it to work properly with DateTiemField fields.
class UserUpdateForm(forms.ModelForm):
"""
We need field "date_joined" can not be edited but can see them
"""
class Meta:
model = User
fields = ('first_name', 'last_name',
'email', 'date_joined', 'slug')
def __init__(self, user, *args, **kwargs):
kwargs['instance'] = user
super(UserUpdateForm, self).__init__(*args, **kwargs)
self.fields['date_joined'].widget = widgets.CMDateTimeText()
def clean_date_joined(self):
instance = getattr(self, 'instance', None)
if instance and instance.pk:
return instance.date_joined
else:
return self.cleaned_data['date_joined']
My code, something is not right.
class CMDateTimeText(DateTimeBaseInput):
"""
A SplitDateTime Widget that has some admin-specific styling.
Hereda Field and Widget.
"""
format_key = 'DATETIME_FORMAT'
def __init__(self, attrs=None, format=None):
# Use slightly better defaults than HTML's 20x2 box
default_attrs = {'class': 'date_id'}
if attrs:
default_attrs.update(attrs)
super(CMDateTimeText, self).__init__(attrs=default_attrs, format=format)
def render(self, name, value, attrs=None):
if value is None:
value = ''
value = self._format_value(value)
final_attrs = self.build_attrs(attrs, name=name)
return format_html('<p{}>{}</p>', flatatt(final_attrs), conditional_escape(value))
Result image:
any idea how to do "AdminReadonlyField"" any view or form?
So after hours of looking for various solutions, I found out how to do it the Django way.
Simply add the attribute disabled to the field in the form (not the widget!):
# in __init__() with crispy-forms for instance
self.fields['field'].disabled = True
# as form field
field = forms.CharField(disabled=True)
And it works... Django is taking care of not saving the field, if some hacker tampered with it although it's disabled. Only works with Django 1.9+.

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

Categories