Modify form fields in FormWizard (Django 1.4) - django

Consider the following classes:
models.py:
class Data(models.Model):
first_name = models.CharField()
checkbox_1 = models.BooleanField()
checkbox_2 = models.BooleanField()
forms.py:
class Form1(ModelForm):
class Meta:
model = Data
fields = ('first_name', 'checkbox_1',)
class Form2(ModelForm):
class Meta:
model = Data
fields = ('checkbox_2',)
Form1 is used in step 1 and Form2 is used in step 2 of a SessionWizardView.
How could I disable Form2.checkbox_2 in step 2 if the user checked Form2.checkbox_1 in step 1?
I tried to accomplish this by overriding get_form() without success:
def get_form(self, step=None, data=None, files=None):
form = super(MyWizard, self).get_form(step, data, files)
if step == '1':
form.fields['checkbox_2'].widget.attrs['disabled'] = 'disabled'
return form
Please note that I intentionally did not check the value of Form2.checkbox_1. I tried to set the widget's attributes in any case.

I solved this by overriding get_form_kwargs for the WizardView. It normally just returns an empty dictionary that get_form populates, so by overriding it to return a dictionary with the data you need prepopulated, you can pass kwargs to your form init.
def get_form_kwargs(self, step=None):
kwargs = {}
if step == '1':
your_data = self.get_cleaned_data_for_step('0')['your_data']
kwargs.update({'your_data': your_data,})
return kwargs
Then, in your form init method you can just pop the kwarg off before calling super:
self.your_data = kwargs.pop('your_data', None)
and use that value to perform whatever logic you need to on the form.

https://docs.djangoproject.com/en/1.4/ref/contrib/formtools/form-wizard/#django.contrib.formtools.wizard.views.WizardView.get_form

Related

How to save multiple models with multiple modelForms in one django form-tools WizardView

Here's my scenario:
I have two models:
class Person(models.Model):
# --- model fields ---
class Qualification(models.Model):
owner = models.ForeignKey(Person, on_delete=models.CASCADE)
# --- other fields ---
And Model forms:
class PersonalForm(forms.ModelForm):
class Meta:
model = Person
fields = ['first_name', 'last_name', 'email', 'id_number', 'date_of_birth']
class IsQualifiedForm(forms.ModelForm):
class Meta:
model = Person
fields = ['is_qualified']
class QualificationForm(forms.ModelForm):
class Meta:
model = Qualification
fields = ['level', 'course_name', 'attainment']
And finally my wizard view:
class Wizard(SessionWizardView):
template_name = 'demo/wizard_test.html'
form_list = [
("personal", PersonalForm),
("is_qualified", IsQualifiedForm),
("qualifications", QualificationForm),
]
def get_form_instance(self, step):
return self.instance_dict.get(step, None)
def done(self, form_list, **kwargs):
# What is the exact logic to be applied here to save the model forms concurrently?
return redirect('home')
I'm trying to save the form but I run into errors:
When I try to call:
for form in form_list:
form.save()
in the done() method, I get an error because the is_qualified is intercepted as null in the first step.
Plus, how do I get to set the owner field's value to the currently created person?
Any help would be appreciated.
If is_qualified is not nullable in your Person model, validation will always fail. What you can do is save both PersonalForm and IsQualifiedForm in one go, since they refer to the same model anyways. To do this, set the values of one form in the other. For example:
def done(self, form_list, **kwargs):
person = form_list[0].save(commit=False)
person.is_qualified = form_list[1].cleaned_data['is_qualified']
person.save()
return redirect('home')
Some notes:
You should probably use named steps instead of relying on form index
If your case is as simple as the form you provided, you should just make the first two forms a single form

How to populate initial values of form from queryset

I have a FormView with a get_initial method which I am trying to use to populate the form. I am trying to get the EmployeeTypes of the receiver of the memo as values in the form.
def get_initial(self):
initial = super(NotificationView, self).get_initial()
users = Memo.objects.filter(id=self.kwargs['pk']).values('receiver__employee_type')
initial['receiving_groups'] = users
return initial
There are 2 issues here..
This returns a Queryset which looks like: <QuerySet [{'receiver__employee_type': 'Bartender'}, {'receiver__employee_type': 'Supervisor'}]> when I really need the fields in the form to be the EmployeeType itself.
Most importantly - the form isn't even rendering these fields.
Here is the form just in case:
class MemoNotificationForm(forms.Form):
class Meta:
fields = [
'receiving_groups'
]
receiving_groups = forms.MultipleChoiceField(
required=False,
widget=forms.CheckboxSelectMultiple)
How do I populate the fields of the form?
EDIT:
class Memo(models.Model):
receiver = models.ManyToManyField(EmployeeType, related_name='memos_receiver')
class EmployeeType(models.Model):
"""Stores user employee type."""
employee_type = models.CharField(
max_length=32,
unique=True)
Having a Meta on a forms.Form doesn't do anything, this is used for ModelForms
If receiving_groups should be choices of EmployeeType then it should be a ModelMultipleChoiceField
class MemoNotificationForm(forms.Form):
receiving_groups = forms.ModelMultipleChoiceField(
EmployeeType.objects.all(),
widget=forms.CheckboxSelectMultiple
)
Then you should be passing instances, or a queryset of the model in the initial
def get_initial(self):
initial = super(NotificationView, self).get_initial()
initial['receiving_groups'] = EmployeeType.objects.filter(memo__id=self.kwargs['pk'])
return initial
EDIT:
As a ModelForm this could look like so
class MemoNotificationForm(forms.ModelForm):
class Meta:
model = Memo
fields = ('receiver', )
View:
class NotificationView(FormView);
form_class = MemoNotificationForm
def get_form_kwargs(self):
kwargs = super(NotificationView, self).get_form_kwargs()
kwargs['instance'] = get_object_or_404(Memo, id=self.kwargs['pk'])
return kwargs
While #lain Shelvington is correct in the process he used to produce the form results, I had to do a little editing to make the code operate correctly...
def get_initial(self):
initial = super(NotificationView, self).get_initial()
receivers = Memo.objects.filter(id=self.kwargs['pk']).values_list('receiver')
initial['receiving_groups'] = EmployeeType.objects.filter(employee_type=receivers)
return initial

Django form not saving with ModelChoiceField - ForeignKey

I have multiple forms on my site that work and save info to my PostgreSQL database.
I am trying to create a form to save information for my Set Model:
class Set(models.Model):
settitle = models.CharField("Title", max_length=50)
setdescrip = models.CharField("Description", max_length=50)
action = models.ForeignKey(Action)
actorder = models.IntegerField("Order number")
The Set Form looks like this. I am using ModelChoiceField to pull a list of Action name fields from the Action model, this displays on the form as a select dropdown
class SetForm(ModelForm):
class Meta:
model = Set
fields = ['settitle', 'setdescrip', 'action', 'actorder']
action = forms.ModelChoiceField(queryset = Action.objects.values_list('name', flat=True), to_field_name="id")
The view for createset is below:
def createset(request):
if not request.user.is_authenticated():
return redirect('%s?next=%s' % (settings.LOGIN_URL, request.path))
elif request.method == "GET":
#create the object - Setform
form = SetForm;
#pass into it
return render(request,'app/createForm.html', { 'form':form })
elif "cancel" in request.POST:
return HttpResponseRedirect('/actions')
elif request.method == "POST":
# take all of the user data entered to create a new set instance in the table
form = SetForm(request.POST, request.FILES)
if form.is_valid():
form.save()
return HttpResponseRedirect('/actions')
else:
form = SetForm()
return render(request,'app/createForm.html', {'form':form})
When the form is filled in and valid and Save is pressed, nothing happens. No errors, the page just refreshes to a new form.
If I don't set the action field in forms.py using (action = forms.ModelChoiceField(queryset = Action.objects.values_list('name', flat=True), to_field_name="id")) then the data saves, so that is likely where I am doing something wrong. Just not sure what?
https://docs.djangoproject.com/en/stable/ref/forms/fields/#django.forms.ModelChoiceField.queryset
The queryset attribute should be a QuerySet. values_list returns a list.
You should just define the __str__ method of your Action model and you won't have to redefine the action field in the form.
If it is set and you want to use another label, you can subclass ModelChoiceField.
The __str__ (__unicode__ on Python 2) 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:
from django.forms import ModelChoiceField
class MyModelChoiceField(ModelChoiceField):
def label_from_instance(self, obj):
return "My Object #%i" % obj.id
So, in your case, either set the __str__ method of Action model, and remove the action = forms.ModelChoiceField(...) line in your form:
class Action(models.Model):
def __str__(self):
return self.name
class SetForm(ModelForm):
class Meta:
model = Set
fields = ['settitle', 'setdescrip', 'action', 'actorder']
Or either define a custom ModelChoiceField:
class MyModelChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj):
return obj.name
class SetForm(ModelForm):
class Meta:
model = Set
fields = ['settitle', 'setdescrip', 'action', 'actorder']
action = MyModelChoiceField(Action.objects.all())

Django: validating unique_together constraints in a ModelForm with excluded fields

I have a form:
class CourseStudentForm(forms.ModelForm):
class Meta:
model = CourseStudent
exclude = ['user']
for a model with some complicated requirements:
class CourseStudent(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
semester = models.ForeignKey(Semester)
block = models.ForeignKey(Block)
course = models.ForeignKey(Course)
grade = models.PositiveIntegerField()
class Meta:
unique_together = (
('semester', 'block', 'user'),
('user','course','grade'),
)
I want the new object to use the current logged in user for CourseStudent.user:
class CourseStudentCreate(CreateView):
model = CourseStudent
form_class = CourseStudentForm
success_url = reverse_lazy('quests:quests')
def form_valid(self, form):
form.instance.user = self.request.user
return super(CourseStudentCreate, self).form_valid(form)
This works, however, because the user is not part of the form, it misses the validation that Django would otherwise do with the unique_together constraints.
How can I get my form and view to use Django's validation on these constraints rather than having to write my own?
I though of passing the user in a hidden field in the form (rather than exclude it), but that appears to be unsafe (i.e. the user value could be changed)?
Setting form.instance.user in form_valid is too late, because the form has already been validated by then. Since that's the only custom thing your form_valid method does, you should remove it.
You could override get_form_kwargs, and pass in a CourseStudent instance with the user already set:
class CourseStudentCreate(CreateView):
model = CourseStudent
form_class = CourseStudentForm
success_url = reverse_lazy('quests:quests')
def get_form_kwargs(self):
kwargs = super(CreateView, self).get_form_kwargs()
kwargs['instance'] = CourseStudent(user=self.request.user)
return kwargs
That isn't enough to make it work, because the form validation skips the unique together constraints that refer to the user field. The solution is to override the model form's full_clean() method, and explicitly call validate_unique() on the model. Overriding the clean method (as you would normally do) doesn't work, because the instance hasn't been populated with values from the form at that point.
class CourseStudentForm(forms.ModelForm):
class Meta:
model = CourseStudent
exclude = ['user']
def full_clean(self):
super(CourseStudentForm, self).full_clean()
try:
self.instance.validate_unique()
except forms.ValidationError as e:
self._update_errors(e)
This worked for me, please check. Requesting feedback/suggestions.
(Based on this SO post.)
1) Modify POST request to send the excluded_field.
def post(self, request, *args, **kwargs):
obj = get_object_or_404(Model, id=id)
request.POST = request.POST.copy()
request.POST['excluded_field'] = obj
return super(Model, self).post(request, *args, **kwargs)
2) Update form's clean method with the required validation
def clean(self):
cleaned_data = self.cleaned_data
product = cleaned_data.get('included_field')
component = self.data['excluded_field']
if Model.objects.filter(included_field=included_field, excluded_field=excluded_field).count() > 0:
del cleaned_data['included_field']
self.add_error('included_field', 'Combination already exists.')
return cleaned_data

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()