Django Model form with a hidden field won't pass validation - django

When I have a field which is hidden but specified in the modelform, it fails to pass validation. The below form fails to pass validation for the postcode field, even though I pass in the postcode data in the constructor. How do I attach data to it to pass validation correctly so that it can save?
eg
class SupplyAddressForm(forms.ModelForm):
full_address = forms.ChoiceField()
def __init__(self, postcode, *args, **kwargs):
super().__init__(*args, **kwargs)
raw_addresses_data = get_full_address(postcode)
addresses = raw_addresses_data['data']['addresses']
...........
self.fields['postcode'].initial = postcode
def save(self, commit=False):
address = super().save(commit=False)
cd = self.cleaned_data
full_address = cd['full_address']
full_address = json.loads(full_address)
......
return address
class Meta:
model = Address
fields = [
'supply_months',
'supply_years',
'postcode',
'residential_status',
]
widgets = {
'postcode': forms.HiddenInput
}

Read this documentaion to understand why Initial is not suitable for your puposes.
Instead, override the 'is_valid' method.
In 'is_valid', change the value of the hidden field AFTER the form is submitted. I got this solution here

Related

How to get Django admin's "Save as new" to work with read-only fields?

I want to implement the "Save as new" feature in Django's admin for a model such as this one:
class Plasmid (models.Model):
name = models.CharField("Name", max_length = 255, blank=False)
other_name = models.CharField("Other Name", max_length = 255, blank=True)
selection = models.CharField("Selection", max_length = 50, blank=False)
created_by = models.ForeignKey(User)
In the admin, if the user who requests a Plasmid object is NOT the same as the one who created it, some of the above-shown fields are set as read-only. If the user is the same, they are all editable. For example:
class PlasmidPage(admin.ModelAdmin):
def get_readonly_fields(self, request, obj=None):
if obj:
if not request.user == obj.created_by:
return ['name', 'created_by',]
else:
return ['created_by',]
else:
return []
def change_view(self,request,object_id,extra_context=None):
self.fields = ('name', 'other_name', 'selection', 'created_by',)
return super(PlasmidPage,self).change_view(request,object_id)
The issue I have is that when a field is read-only and a user hits the "Save as new" button, the value of that field is not 'transferred' to the new object. On the other hand, the values of fields that are not read-only are transferred.
Does anybody why, or how I could solve this problem? I want to transfer the values of both read-only and non-read-only fields to the new object.
Did you try Field.disabled attribute?
The disabled boolean argument, when set to True, disables a form field using the disabled HTML attribute so that it won’t be editable by users. Even if a user tampers with the field’s value submitted to the server, it will be ignored in favor of the value from the form’s initial data.
I did a quick test in my project. When I added a new entry the disabled fields were sent to the server.
So something like this should work for you:
class PlasmidPage(admin.ModelAdmin):
def get_form(self, request, *args, **kwargs):
form = super(PlasmidPage, self).get_form(request, *args, **kwargs)
if not request.user == self.cleaned_data['created_by'].:
form.base_fields['created_by'].disabled = True
form.base_fields['name'].disabled = True
def change_view(self,request,object_id,extra_context=None):
self.fields = ('name', 'other_name', 'selection', 'created_by',)
return super(PlasmidPage,self).change_view(request,object_id)
It happens because Django uses request.POST data to build a new object, but readonly fields are not sent with the request body. You can overcome this by making widget readonly, not the field itself, like this:
form.fields['name'].widget.attrs = {'readonly': True}
This has a drawback: it's still possible to change field values by tampering the form (e.g if you remove this readonly attribute from the widget using devtools console). You could protect from that by checking that values haven't actually changed in clean() method.
So full solution will be:
class PlasmidForm(models.ModelForm):
class Meta:
model = Plasmid
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance and not self.instance.created_by == request.user:
self.fields['name'].widget.attrs = {'readonly': True}
def clean(self):
cleaned_data = super().clean()
if self.instance and not self.instance.created_by == request.user:
self.cleaned_data['name'] = instance.name # just in case user tampered with the form
return cleaned_data
class PlasmidAdmin(admin.ModelAdmin):
form = PlasmidForm
readonly_fields = ('created_by',)
def save_model(self, request, obj, form, change):
if obj.created_by is None:
obj.created_by = request.user
super().save_model(request, obj, form, change)
Notice I left created_by to be readonly, and instead populate it with the current user whenever object is saved. I don't think you really want to transfer this property from another object.

Django: __init__() got an unexpected keyword argument 'first_name'

I am writing a class-based view that lets the employees setup their profile. Because the employee model has a few foreign key field (e.g. employer is a forieng key referencing company model), I decided not to use ModelForm and resort to good old forms so the user can enter the name of the company they work in rather than 32.
Here is my code:
class Employee_ProfileSetting(forms.Form):
first_name = forms.CharField(label = 'First Name', max_length = 30)
last_name = forms.CharField(label = 'Last Name', max_length = 30)
email = forms.EmailField()
employer = forms.CharField(max_length = 50)
cell = forms.CharField(max_length = 20)
driver_license_num = forms.CharField(max_length=20)
birth_year = forms.IntegerField()
start_date = forms.IntegerField(help_text = 'Year you started with the company')
title = forms.CharField(max_length = 30)
def __init__(self, *args, **kwargs):
if not args: # args is empty, meaning a fresh object
super(Employee_ProfileSetting, self).__init__(*args, **kwargs)
else:
# Retrieving the form's information
self.first_name = args[0].get('first_name')
self.last_name = args[0]['last_name']
self.email = args[0]['email']
self.cell = args[0]['cell']
self.driver_license_num = args[0]['driver_license_num']
self.birth_year = args[0]['birth_year']
self.start_year = args[0]['start_date']
self.title = args[0]['title']
super(Employee_ProfileSetting, self).__init__(*args, **kwargs)
The constructor would then allow me to do this in my class-based view:
# Inside class AdminSetting(View):
def post(self, request):
form = self.form_class(request.POST)
if form.is_valid():
cd = form.cleaned_data
employee_profile = Employee_ProfileSetting(**cd) # Calling the constructor
employee_profile.save() # The save function is overridden
admin, created = Employee.objects.get_or_create(**cd) # If a matching employee exists, it gets that object. Otherwise, it creates it.
if created: # Object was not found, and so it was created
return HttpResponseRedirect('success.html')
When I run it, it gives me the error:
__init__() got an unexpected keyword argument 'first_name'
So, my problem is two-fold:
1) What is wrong with the code? and what does that error mean?
2) Is there a better way to let the user fill in all the fields, including the foreign key fields, in a form and save fields accordingly? e.g. the field corresponding to a foreign key is saved in its respective table first (company1.employee_set.create() and then saving the other fields. Can a ModelForm be used?
Constructor of model can't take any arguments that have no matching field inside model. So if you try to pass first_name into model that doesn't have first_name field, you will get that exception.
To do it properly, you can create multiple forms and use them. Form should ignore extra POST data and take only what it needs. Also you can pass commit=False into ModelForm.save method, so created object won't be saved. That way you can pass some additional data, for example Employee_ProfileSettings ID can be passed into Employee before saving it.
The way you define the init is very strange. It should be something like this:
def __init__(self, data, *args, **kwargs):
if not data: # data is empty, meaning a fresh object
super(Employee_ProfileSetting, self).__init__(*args, **kwargs)
else:
# Retrieving the form's information
self.first_name = data[0].get('first_name')
self.last_name = data[0]['last_name']
self.email = data[0]['email']
self.cell = data[0]['cell']
self.driver_license_num = data[0]['driver_license_num']
self.birth_year = data[0]['birth_year']
self.start_year = data[0]['start_date']
self.title = data[0]['title']
super(Employee_ProfileSetting, self).__init__(*args, **kwargs)
And second thing is how you call you class:
employee_profile = Employee_ProfileSetting(**cd)
Which expands all the clean_data list. This expands to:
employee_profile = Employee_ProfileSetting(first_name=cleaned_data[first_name], ...)
Instead you should call it like this:
employee_profile = Employee_ProfileSetting(form.cleaned_data)
So that the dictionary instance is not expanded.
Finally, I don't think I understood why you cannot use a model form...

Django form instance is missing a field

So I have a problem. I have a model form:
class TeamForm(forms.ModelForm):
class Meta:
model = Team
fields = ['name','category','association','division','gender','logo','season']
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super(TeamForm, self).__init__(*args, **kwargs)
instance = getattr(self, 'instance', None)
utils = CPUtils()
season_utils = SeasonUtils()
if instance and instance.pk is None:
self.fields['division'].initial = 1
self.fields['season'].initial = season_utils.getCurrentSeason().id
if user_role != 'admin':
self.fields['division'].widget.attrs['disabled'] = True
self.fields['division'].required = False
self.fields['season'].widget.attrs['disabled'] = True
self.fields['season'].required = False
So I am setting the initial of the form for the field season.
On my form, it is showing just fine.
So now in my clean method, I want to run some validation and I need to get the instance season.
In my clean method:
if cleaned_data.get('season') is None:
cleaned_data['season'] = self.instance.season
But it says that self.instance.season: DoesNotExist: Team has no season.
I have been trying to figure this out for a while now and I have no idea what's going on...
EDIT:
Here my model:
name = models.CharField(max_length=25,verbose_name=_("name"))
slug = AutoSlugField(unique=True,populate_from='name')
season = models.ForeignKey(Season,verbose_name=_("season"),blank=False)
association = models.ForeignKey(Association,blank=False,null=True,verbose_name=_("association"),on_delete=models.SET_NULL)
category = models.ForeignKey(Category,verbose_name=_("category"),blank=False,null=True,default=1,on_delete=models.SET_NULL)
division = models.ForeignKey(Division,verbose_name=_("division"),blank=False,null=True,default=1,on_delete=models.SET_NULL)
Also, the instance does not have season only during create and not during update...
Thanks,
Ara
The reason it says:
self.instance.season: DoesNotExist
Is that you are accessing self. Self here is actually the Form, and you are asking the Form for an attribute called instance, and then asking the instance attribute if it has a season attribute. The Form doesn't have this attribute.
What you really want to do is get the Model instance based on the Season ID, not the Form instance. The Form instance only holds the data temporarily while you clean it before you use the Model to save it to the database.
If you are trying to clean() the season, then you would want to access it this way if Season is an ID in your Form:
class TeamForm(forms.ModelForm):
# form logic here
def clean(self):
cleaned_data = super(TeamForm, self).clean()
# now get the Season Object from the cleaned_data dictionary
# if 'season' == season_id
try:
season_id = cleaned_data['season']
season = Season.object.get(pk=season_id)
except KeyError:
# season id does not exist, so do something here
pass

Setting value of Django field inside of Form class

I'm trying to set the value of a Django field inside of the Form class. Here is my model
class Workout(models.Model):
user = models.ForeignKey(User , db_column='userid')
datesubmitted = models.DateField()
workoutdate = models.DateField();
bodyweight = models.FloatField(null=True);
workoutname = models.CharField(max_length=250)
Here is the form class, in which i am attempting to achieve this:
class WorkoutForm(forms.ModelForm):
class Meta:
model = Workout
def __init__(self,*args, **kwargs):
# this is obviously wrong, I don't know what variable to set self.data to
self.datesubmitted = self.data['datesubmitted']
Ok, sorry guys. I'm passing the request.POST data to the WorkoutForm in my view like this
w = WorkoutForm(request.POST)
However, unfortunately the names of the html elements have different names then the names of the model. For instance, there is no date submitted field in the html. This is effectively a time stamp that is produced and saved in the database.
So I need to be able to save it inside the form class some how, I think.
That is why I am trying to set the datesubmitted field to datetime.datetime.now()
Basically I am using the form class to make the verification easier, and I AM NOT using the form for it's html output, which I completely disregard.
You have to do that in the save method of your form
class WorkoutForm(forms.ModelForm):
class Meta:
model = Workout
def __init__(self,*args, **kwargs):
super(WorkoutForm, self).__init__(*args, **kwargs)
def save(self, *args, **kw):
instance = super(WorkoutForm, self).save(commit=False)
instance.datesubmitted = datetime.now()
instance.save()
How ever you can set that in your model also to save the current datetime when ever a new object is created:
datesubmitted = models.DateTimeField(auto_now_add=True)
You can set some extra values set in form as:
form = WorkOutForm(curr_datetime = datetime.datetime.now(), request.POST) # passing datetime as a keyword argument
then in form get and set it:
def __init__(self,*args, **kwargs):
self.curr_datetime = kwargs.pop('curr_datetime')
super(WorkoutForm, self).__init__(*args, **kwargs)
You should not be using a ModelForm for this. Use a normal Form, and either in the view or in a method create a new model instance, copy the values, and return the model instance.

Django, adding excluded properties to the submitted modelform

I've a modelform and I excluded two fields, the create_date and the created_by fields. Now I get the "Not Null" error when using the save() method because the created_by is empty.
I've tried to add the user id to the form before the save() method like this: form.cleaned_data['created_by'] = 1 and form.cleaned_data['created_by_id'] = 1. But none of this works.
Can someone explain to me how I can 'add' additional stuff to the submitted modelform so that it will save?
class Location(models.Model):
name = models.CharField(max_length = 100)
created_by = models.ForeignKey(User)
create_date = models.DateTimeField(auto_now=True)
class LocationForm(forms.ModelForm):
class Meta:
model = Location
exclude = ('created_by', 'create_date', )
Since you have excluded the fields created_by and create_date in your form, trying to assign them through form.cleaned_data does not make any sense.
Here is what you can do:
If you have a view, you can simply use form.save(commit=False) and then set the value of created_by
def my_view(request):
if request.method == "POST":
form = LocationForm(request.POST)
if form.is_valid():
obj = form.save(commit=False)
obj.created_by = request.user
obj.save()
...
...
`
If you are using the Admin, you can override the save_model() method to get the desired result.
class LocationAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
obj.created_by = request.user
obj.save()
Pass a user as a parameter to form constructor, then use it to set created_by field of a model instance:
def add_location(request):
...
form = LocationForm(user=request.user)
...
class LocationForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')
super(forms.ModelForm, self).__init__(*args, **kwargs)
self.instance.created_by = user
The correct solution is to pass an instance of the object with pre-filled fields to the model form's constructor. That way the fields will be populated at validation time. Assigning values after form.save() may result in validation errors if fields are required.
LocationForm(request.POST or None, instance=Location(
created_by=request.user,
create_date=datetime.now(),
))
Notice that instance is an unsaved object, so the id will not be assigned until form saves it.
One way to do this is by using form.save(commit=False) (doc)
That will return an object instance of the model class without committing it to the database.
So, your processing might look something like this:
form = some_form(request.POST)
location = form.save(commit=False)
user = User(pk=1)
location.created_by = user
location.create_date = datetime.now()
location.save()