Django model form using forms.ModelMultipleChoiceField - django

I have a ModelForm in my Django app that uses a forms.ModelMultipleChoiceField, which displays as a forms.CheckboxSelectMultiple widget on the form. This ModelForm is used to select/de-select values for a many-to-many relation. Here's the problem: when you uncheck all of the checkboxes and save the form, it doesn't save. If you uncheck all but 1, it does save properly.
Are there any tricks I'm missing here about model forms and many-to-many relations? Am I encountering a bug? I'm new to Django. Thanks in advance.
Custom Field:
class NetworkMessageChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj):
return obj.display_message
Model Form:
class MessageTemplateForm(forms.ModelForm):
network_messages = NetworkMessageChoiceField(queryset=NetworkMessageTemplate.objects,
widget=forms.CheckboxSelectMultiple())
class Meta:
model = UserProfile
fields = ('network_messages',)
View that saves form:
def save_message_templates(request, extra_context=dict()):
try:
profile_obj = request.user.get_profile()
except ObjectDoesNotExist:
profile_obj = UserProfile(user=request.user)
if request.method == 'POST':
form = MessageTemplateForm(request.POST, instance=profile_obj)
if form.is_valid():
form.save()
return redirect('/')
return index(request, message_template_form=form)
Edit:
My form field was missing Required=False.
class MessageTemplateForm(forms.ModelForm):
network_messages = NetworkMessageChoiceField(queryset=NetworkMessageTemplate.objects,
widget=forms.CheckboxSelectMultiple(),
required=False)
class Meta:
model = UserProfile
fields = ('network_messages',)

You didn't paste what your model looks like, so I am guessing that network_messages field in your model is required. If that is the case, then when you attempt to submit the form with the value of that field as NULL (empty), then form.is_valid() is not returning True and therefore your form.save() is never being executed.
Have you tried executing this stuff from an interactive shell, instantiating the form and attempting to manually save() it?

Related

Pass url parameter to Django ModelForm without rendering it as input on form

I have a url /<subject_id>/comments/new/ which renders a Django ModelForm. I am using a view class derived from FormView to process the form. I wish to do the following:
subject_id should not appear on the rendered form.
subject_id should be added to the form prior to is_valid() being called, or if this is not possible should be added to the Comment instance.
forms/comment_form.py:
class CommentForm(ModelForm):
class Meta:
model = Comment
fields = ['text']
views.py:
class CommentCreate(FormView):
form_class = CommentForm
def form_valid(self, form):
# Do some stuff to the validated Comment instance
# Maybe save the comment, maybe not
return super().form_valid(form)
How do I do this? If I add subject_id as a field in CommentForm then it appears on the rendered form. If I don't then the form is instantiated with subject_id present from `self.kwargs['subject_id'] and complains of an "unexpected keyword argument".
After some hunting around in the docs I have discovered that the correct answer is to use the get_form() method to pre-populate the form with the data that I don't want to appear on the form, but that needs to be present for validation.
class CommentCreate(FormView):
form_class = CommentForm
def get_form(self):
self.subject= get_object_or_404(Subject, id=self.kwargs['subject_id'])
partial_comment = Comment(user=self.request.user, subject=self.subject)
form = CommentForm(**self.get_form_kwargs(), instance=partial_comment)
return form
You can remove subject_id from form fields:
class CommentForm(ModelForm):
class Meta:
model = Comment
fields = ['text']
And add it to new comment object in form_valid method like this:
class OrderCreate(FormView):
form_class = CommentForm
def form_valid(self, form):
subject_id = self.kwargs['subject_id']
subject = Subject.objects.get(id=subject_id)
form.instance.subject_id = subject
return super().form_valid(form)

Django UpdateView - get initial data from many-to-many field and add them when saving form

I'm using Django class based generic view. In my models.py I have a model called MyModel with many-to-many field called m2m. I have multiple groups of users they can edit the m2m field. Each group of users can only see and add their portion to the field - using get_form to set what they can see in the m2m field. The problem I'm having is that when one user enter his record it will delete the initial records in the m2m field. I need to somehow get the initial values from the m2m field save them and then add them to the new ones when the form is submitted. Here is my views.py:
class MyModelUpdate(UpdateView):
model = MyModel
fields = ['m2m']
def get_initial(self):
return initials
def get_form(self, form_class=None):
form = super(MyModelUpdate, self).get_form(form_class)
form.fields["m2m"].queryset = DiffModel.objects.filter(user = self.request.user)
return form
def form_valid(self, form):
form.instance.m2m.add( ??? add the initial values)
return super(MyModelUpdate, self).form_valid(form)
def get_success_url(self):
...
I'm adding this answer to offer a simplified explanation of this kind of problem, and also because the OP switches from an UpdateView to a function based view in his solution, which might not be what some users are looking for.
If you are using UpdateView for a model that has a ManyToMany field, but you are not displaying it to the user because you just want this data to be left alone, after saving the form all the m2m values will be erased.
That's obviously because Django expects this field to be included in the form, and not including it is the same as just sending it empty, therefore, to tell Django to delete all ManyToMany relationships.
In that simple case, you don't need to define the form_valid and then retrieve the original values and so on, you just need to tell Django not to expect this field.
So, if that's you view:
class ProjectFormView(generic.UpdateView):
model = Project
form_class = ProjectForm
template_name = 'project.html'
In your form, exclude the m2m field:
class ProjectForm(forms.ModelForm):
class Meta:
model = Project
fields = '__all__'
exclude = ['many_to_many_field']
after few days of searching and coding I've found a solution.
views.py:
from itertools import chain
from .forms import MyForm,
def MyModelUpdate(request, pk):
template_name = 'mytemplate.html'
instance = MyModel.objects.get(pk = pk)
instance_m2m = instance.m2m.exclude(user=request.user)
if request.method == "GET":
form = MyForm(instance=instance, user=request.user)
return render(request, template_name, {'form':form})
else:
form = MyForm(request.POST or None, instance=instance, user=request.user)
if form.is_valid():
post = form.save(commit=False)
post.m2m = chain(form.cleaned_data['m2m'], instance_m2m)
post.save()
return redirect(...)
forms.py:
from django import forms
from .models import MyModel
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ['m2m']
def __init__(self, *args, **kwargs):
current_user = kwargs.pop('user')
super(MyForm, self).__init__(*args, **kwargs)
self.fields['m2m'].queryset = self.fields['m2m'].queryset.filter(user=current_user)

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

Hiding foreign key fields from ModelForms in Django

I am using django ModelForms to generate my input forms.
I specify in my form model to only use a set of fields:
class <Model>Form(ModelForm):
class Meta:
model = <Model>
fields = ('date', 'comment_1')
My model is defined as:
class <Model>(models.Model):
fk_id_1 = models.ForeignKey(<ExternalModel1>, null=False, blank=False)
fk_id_2 = models.ForeignKey(<ExternalModel2>, null=False, blank=False)
date = models.DateField()
comment_1 = models.CharField(max_length=100)
comment_2 = models.CharField(max_length=100)
However, the ForeignKey boxes show.
How is it possible for me to hide them from the form? Also, how can I set the values for those dropboxes from within the view and not sure, say JQuery externally to do it? Ideally, after the ''is_valid()'' check, I would like to set the IDs of my Foreign Keys and then do save. Maybe I should look into solving this using another way?
This is the View:
def <Model>_add(request, trainee_id):
<Model>FormSet = modelformset_factory(<Model>)
if request.method == 'POST':
formset = <Model>FormSet(request.POST, request.FILES)
if formset.is_valid() and formset.has_changed():
formset.save()
# do something.
else:
formset = <Model>FormSet(queryset=<Model>.objects.none())
return render_to_response("<Model>_add.html", {
"formset": formset, "fk_id_1": fk_id_1,
}, context_instance=RequestContext(request))
I can solve this issue using JQuery but I would like a more elegant approach.
Note: I tried posting this earlier but I think it was not as clear as it is here: Presetting values on a foreign entity relationship in a ModelForm ... I didn't understand exactly what was said about QuerySet.
You need to be a bit more explicit in how you define the form:
class <Model>Form(ModelForm):
class Meta:
model = <Model>
fields = ['date', 'comment_1']
exclude = ['fk_id_1', 'fk_id_2']
Then in your view:
from django.shortcuts import render, redirect
def <Model>_add(request, trainee_id):
<Model>FormSet = modelformset_factory(<Model>)
if request.method == 'POST':
formset = <Model>FormSet(request.POST, request.FILES)
if formset.is_valid() and formset.has_changed():
forms = formset.save(commit=False)
for form in forms:
form.fk_id_1 = SomeOtherModel.objects.get(pk=1)
form.fk_id_2 = SomeOtherModel.objects.get(pk=2)
form.save()
# add your success redirect here, for example:
return redirect('/')
else:
formset = <Model>FormSet(queryset=<Model>.objects.none())
return render(request, "<Model>_add.html", {"formset": formset})
Every ModelForm also has a save() method. doc
or:
in views.py
form.instance.fk_id_1 = ...
form.instance.fk_id_2 = ...
form.save()

Django Form problems with UserProfile

I'd like to create a "update user's profile" page to let users modify their profiles, so I come up with the following models:
class Profile(models.Model):
user = models.OneToOneField(User)
nick_name = models.CharField(blank=True,max_length=100)
school = models.CharField(blank=True,max_length=100)
motto = models.CharField(blank=True,max_length=100)
class ProfileForm(ModelForm):
class Meta:
model = Profile
And my view is designed as:
#login_required
def update_profile_view(request):
if request.method == 'POST':
user = request.user
try:
profile = user.get_profile()
except Exception:
profile = Profile.objects.create(user=user)
form = ProfileForm(request.POST)
if form.is_valid():
cd = form.cleaned_data
profile.nick_name = cd['nick_name']
profile.school = cd['school']
profile.motto = cd['motto']
profile.save()
return HttpResponseRedirect('/main_page/')
else:
form = ProfileForm()
return render(request, 'update_profile.html', {'form':form})
The relationship between an user and a profile is apparently 1to1, and with request I can determine the current user. So the form's user field needn't to be filled. Unfortunately, this couldn't pass "the form.is_valid()" test. And it seems hard to modify a form before "is_valid" invoked. For simplicity, I don't want to create my own Form Class, neither do I want to write customized form validation. Is there any other way to solve the problem?
Your view can be greatly simplified:
#login_required
def update_profile_view(request):
try:
profile = Profile.objects.get(user=request.user)
except Profile.DoesNotExist:
profile = None
form = ProfileForm(request.POST or None, instance=profile)
if request.method == 'POST':
if form.is_valid():
form.save()
return HttpResponseRedirect('/main_page/')
return render(request, 'update_profile.html', {'form':form})
There's no need to manually assign the fields like you're doing. Django ORM knows how to do an insert versus an update automatically. So if you simply pass the ProfileForm an instance of a Profile, it knows to do an update. If there's no instance of a profile, it's going to do an insert.
Now, if you want to make the assignment of the user transparent in the UI, you'll need to exclude the user field from the form and assign it yourself. There are a couple of different ways to do that.
I would also recommend leveraging reverse in your redirect so you don't have a hard-coded path.
You have basicly two choices:
1 Modification of ProfileForm:
class ProfileForm(ModelForm):
class Meta:
model = Profileclass
exclude = ('user',)
2 Change this lines as follows:
form = ProfileForm(request.POST, instance=profile)
if form.is_valid():
updated_profile = form.save()
You can either set the user field's value to not required in the init method (self.fields['user'].required = False) of the form or set the user not editable in the model (editable=False).
In your view method, call profile = form.save(commit=False), then do profile.user = your_user and profile.save()
You don't have to apply the cleaned data manually to the profile since the ModelForm does this.