I have an UpdateView which contains a form and an InlineFormetSet that is related to the form model (I simplified the code below):
#models.py
class Note(Model):
content = models.TextField()
class Dialog(Model):
message = models.TextField()
note = modes.ForeignKey(Note)
#views.py
class NoteUpdateView(UpdateView):
model = Note
form_class = NoteForm
def get_context_data(self, **kwargs):
context = super(NoteUpdateView ,self).get_context_data(**kwargs)
self.object = self.get_object()
dialogFormset = inlineformset_factory(Note,
Dialog,
fields='__all__',
extra=0)
dialog = dialogFormset(instance=self.object)
context['dialog'] = dialog
return context
def post(self, request, *args, **kwargs):
form = self.get_form(self.get_form_class())
dialog_form = DialogFormset(self.request.POST, instance=Note.objects.get(id=self.kwargs['pk']))
if (form.is_valid() and dialog_form.is_valid()):
return self.form_valid(form, result_form, dialog_form)
else:
return self.form_invalid(form, result_form, dialog_form)
def form_valid(self, form, result_form, dialog_form):
self.object, created = Note.objects.update_or_create(pk=self.kwargs['pk'], defaults=form.cleaned_data)
dialog_form.save()
return HttpResponseRedirect(self.get_success_url())
def form_invalid(self, form, result_form, dialog_form):
return self.render_to_response(self.get_context_data(form=form,
result_form=result_form,
dialog_form=dialog_form))
The purpose of NoteUpdateView is to render existing Note and Dialog when a GET request is made tonote/11. A user may delete an existing Dialog, which is not handled by the code above.
To handle delete, I can do the following on POST:
1) fetch all of the dialog records related to the requested Note:
dialogs = Note.objects.filter(pk=self.kwargs['pk'])
2) loop through self.request.POST and see if the formsets contained in the submitted data also exist in the dialogs created above.
3) If a record is dialogs but not submitted via POST, then that dialog is considered to be removed by the user. Thus, preform delete operation.
I am sure I can implement these steps. But since I am not very familiar with Django's formset. I wonder if there is any built-in classes or methods that I should use to automate these steps. What is the Django way of doing what I just described?
Ok, I figured out what the problem was. The problem that I run into is due to the use of django-crispy-forms. Let me explain what happened:
When Django renders InlineFormSet, it's can_delete attribute is set to True automatically. When this attribute is set to True, a hidden input field is inserted into the rendered HTML:
<input type="hidden" name="dialog_set-0-DELETE" id="id_dialog_set-0-DELETE">
I used django-crispy-forms to render my forms so that they are styled with bootstrap3. When rendering inlineformset using crispy-forms, a FormHelper needs to be defined.
This is because when you have multiple inlineformset forms on the page, you will only want one <form> tag surrounds them instead of giving each inlineformset form it's own <form> tag. To do that, I had to define the FormHelper like this:
#models.py
class Dialog(Model):
info1 = models.TextField()
info2 = models.TextField()
#forms.py
class DialogFormSetHelper(FormHelper):
def __init__(self, *args, **kwargs):
super(DialogFormSetHelper, self).__init__(*args, **kwargs)
self.form_tag = False # This line removes the '<form>' tag
self.disable_csrf = True # No need to insert the CSRF string with each inlineform
self.layout = Layout(
Field('info1', rows='3'), # make sure the name of the field matches the names defined in the corresponding model
Field('info2', rows='3')
)
I need django-crispy-forms to set the row number of a textarea tag to be 3. Thus, I had to specifically redefine how my textarea fields look like under Layout. What I didn't realize when using the Layout is that anything that you didn't define in it will not be rendered in the HTML.
From the look of the code, I didn't miss any fields defined in the Dialog model. But, what I didn't realize is that the hidden fields that come with InlineFormSet are not rendered in the HTML unless I specifically define them in the Layout object and in the template. To get formset & inlineformset working properly, you will need the following hidden inputs:
formset.manageform. They look like this in the HTML:
<input id="id_dialog_set-TOTAL_FORMS" name="dialog_set-TOTAL_FORMS" type="hidden" value="1">
<input id="id_dialog_set-INITIAL_FORMS" name="dialog_set-INITIAL_FORMS" type="hidden" value="1">
<input id="id_dialog_set-MIN_NUM_FORMS" name="dialog_set-MIN_NUM_FORMS" type="hidden" value="0">
<input id="id_dialog_set-MAX_NUM_FORMS" name="dialog_set-MAX_NUM_FORMS" type="hidden" value="1000">
The primary key that is associated with each inlineformset form, and a foreign key that the inlineformset refers to. They look like this in HTML:
<input id="id_dialog_set-0-note" name="dialog_set-0-note" type="hidden" value="11"> <!-- This line identifies the foreign key`s id -->
<input id="id_dialog_set-0-id" name="dialog_set-0-id" type="hidden" value="4"> <!-- This line identifies the inlineformset form`s id -->
[A DELETE hidden field when can_delete is set to True] (https://docs.djangoproject.com/en/1.9/topics/forms/formsets/#can-delete). It looks like this in the HTML:
<input type="hidden" name="dialog_set-0-DELETE" id="id_dialog_set-0-DELETE">
In my template, I had the first two covered:
<form method="post" action="{{ action }}" enctype="multipart/form-data" id="note_form">
{% crispy form %}
{# the management_form is covered here #}
{{ dialog.management_form }}
{% for form in dialog %}
<div class="formset-container">
<div class="dialog-title">
{% crispy form dialogHelper %}
</div>
{# the hidden fields are covered here #}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
</div>
{% endfor %}
</form>
What I didn't have is the DELETE hidden input. To add it to the HTML, I had to add it this way in the Layout:
#forms.py
class DialogFormSetHelper(FormHelper):
def __init__(self, *args, **kwargs):
super(DialogFormSetHelper, self).__init__(*args, **kwargs)
self.form_tag = False
self.disable_csrf = True
self.layout = Layout(
Field('info1', rows='3'),
Field('info2', rows='3'),
Field('DELETE') # <- ADD THIS LINE
)
Finally, everything works properly now
The Django way is to check if someone has made a library that handles this for you :-).
So take a look at the exellent django-extra-views and it's InlineFormSetView. I've used it a lot and it works really well. In your case your view becomes something like this:
from extra_views import InlineFormSetView
class NoteUpdateView(InlineFormSetView):
model = Note
inline_model = Dialog
form_class = NoteForm
extra = 0
def get_context_data(self, **kwargs):
context = super(NoteUpdateView ,self).get_context_data(**kwargs)
context['dialog'] = context['formset']
return context
You could skip .get_context_data method as well if you update your template to refer to the formset as "formset" instead.
Related
I'm using a ModelMultipleChoiceField to allow users to select certain model objects to delete. It works fine when a single object is selected, but when multiple objects are selected then a validation error is triggered: "Ensure this value has at most 50 characters". The 50 character limit is from the underlying model's max_length attribute. I'm not sure why this validation is happening at all since I am selecting existing model objects, and even less sure why they are combining the character lengths of all my selections instead of validating each selection individually. I've also noticed that approximately 20 extra characters are being counted for each object selected when totalling the character length. Any help is appreciated.
Here is my code:
Model:
class Template(models.Model):
# Relationships
user = models.ForeignKey("users.CustomUser", on_delete=models.CASCADE)
# Fields
name = models.CharField(max_length=50)
description = models.TextField(max_length=250)
docx_file = models.FileField(("DOCX File"),
upload_to=user_directory_path,
validators=[FileExtensionValidator(allowed_extensions=['docx'])])
created = models.DateTimeField(auto_now_add=True, editable=False)
last_updated = models.DateTimeField(auto_now=True, editable=False)
def __str__(self):
return self.name
Form:
class TemplateChoiceDelete(forms.ModelForm):
name = forms.ModelMultipleChoiceField(queryset=Template.objects.all())
class Meta:
model = Template
fields = ['name']
# Limit results to templates owned by the current user
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')
super(TemplateChoiceDelete, self).__init__(*args, **kwargs)
self.fields['name'].queryset = Template.objects.filter(user=user)
View: (ignore the filter code, that is a work in progress relating to another feature)
def manage_templates(request):
f = TemplateFilter(request.GET, queryset=Template.objects.filter(user=request.user))
if request.method == 'GET':
choice_form = TemplateChoiceDelete(request.GET, user=request.user)
print(choice_form)
if choice_form.is_valid():
choice_form.cleaned_data['name'].delete()
else:
form = TemplateChoiceDelete
return render(request, 'docs/manage_templates.html', {'filter': f, 'choice_form': choice_form})
Template: (again, please ignore the code relating to filters)
{% block content %}
<p> <b> Search </b> </p>
<form method="get">
{{ filter.form.as_p }}
<input type="submit" value="Search" />
</form>
{% for obj in filter.qs %}
{{ obj.name }} | {{ obj.description }}<br />
{% endfor %}
<br>
<p><b> Delete Templates </b></p>
<form method="GET">
{{ choice_form.as_p }}
<input type="submit" onclick="return confirm('Are you sure? This will delete the selected template(s)')" value="Delete">
</form>
{% endblock %}
The error
This is not a ModelForm, if you use a ModelForm, it will of course "inherit" certain validators from the model. This is a simple Form, so:
class TemplateChoiceDelete(forms.Form):
name = forms.ModelMultipleChoiceField(queryset=Template.objects.all())
# no Meta
# Limit results to templates owned by the current user
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')
super().__init__(*args, **kwargs)
self.fields['name'].queryset = Template.objects.filter(user=user)
A ModelForm is a Form that is tailored towards a certain model to create or update a model instance, not to edit items that have "something to do" with that model.
Note: Section 9 of the HTTP protocol
specifies that requests like GET and HEAD should not have side-effets, so you
should not change entities with such requests. Normally POST, PUT, PATCH, and
DELETE requests are used for this. In that case you make a small <form> that
will trigger a POST request, or you use some AJAX calls.
In my project, i have a template where i'm trying to put two forms for different use cases. I've never come across this problem before, so i don't really know where to go from here to use two forms in the same page.
At first i thought of creating another view to handle each form, but i think that this solution would create problems with the rendering of my templates, other than not being sustainable if i should have this problem again with another template.
After making some research, i found a solution but it works for class based views, but i'd like to avoid that since my view is already a function based view, and i would have to make a lot of changes in my code.
Would it be possible to solve this problem with a function based view? Every advice is appreciated
First field
class FirstForm(forms.ModelForm):
firstfield = forms.CharField()
secondfield = forms.CharField()
class Meta:
model = MyModel
fields = ("firstfield", "secondfield")
def save(self, commit=True):
send = super(FirstForm, self).save(commit=False)
if commit:
send.save()
return send**
Second Form
class SecondForm(forms.ModelForm):
firstfield = forms.FloatField()
secondfield = forms.Floatfield()
thirdfield = forms.CharField()
class Meta:
model = MyModelTwo
fields = ("firstfield", "secondfield", "thirdfield")
def save(self, commit=True):
send = super(SecondForm, self).save(commit=False)
if commit:
send.save()
return send
Template
<h3> First Form </h3>
<form method="post" novalidate>
{% csrf_token %}
{% include 'main/includes/bs4_form.html' with form=form %}
<button type="submit" class="btn btn-danger" style="background-color: red;">SUBMIT</button>
</form>
<h3> Second Form </h3>
<form method="post" novalidate>
{% csrf_token %}
{% include 'main/includes/bs4_form.html' with form=form %}
<button type="submit" class="btn btn-danger" style="background-color: red;">SUBMIT</button>
</form>
EDIT: my view:
def myview(request):
# if this is a POST request we need to process the form data
if request.method == 'POST':
# create a form instance and populate it with data from the request:
form = FirstForm(request.POST)
# check whether it's valid:
if form.is_valid():
# process the data in form.cleaned_data as required
# ...
# redirect to a new URL:
send = form.save()
send.save()
messages.success(request, f"Success")
# if a GET (or any other method) we'll create a blank form
else:
form = FirstForm()
return render(request,
"main/mytemplate.html",
context={"form":form})
This answer is a bit general because you haven't included your view function. You can add each of these forms to your view's context. Something like this:
views.py
...
from .forms import FirstForm, SecondForm
...
def some_view(request):
context = {
'first_form': FirstForm(request.POST or None),
'second_form': SecondForm(request.POST or None)
}
return render(request, "app/some_template.html", context)
I have a django template in which I'm dynamically rendering multiple fields (using ajax)
Below is a Django form (which has been rendered in a template) whose fields have same names. I want to use the cleaned_data method to clean form data in views.py before storing them in the database.
index.html
<div class="form-container">
<!-- ASSUMING I HAVE ALREADY ADDED FIELDS DYNAMICALLY -->
<form id = "orderForm" action="newPickupOrder/" method="post" name="processForm">
<input type='text' name='this_field'>
<input type='text' name='this_field'>
<button type="submit">Submit</button>
</form>
</div>
<form id="addItemForm">
{% csrf_token %}
<!-- BUTTON TO ADD MORE FIELDS DYNAMICALLY -->
<button id = "addItemButton">Add item</button>
</form>
<script>
var addItemButton = document.querySelector('#addItemButton');
addItemButton.onclick = function(){
$.ajax({
type: 'POST',
url: 'addItem/',
data: addItemForm.serialize(),
success: function (response) {
$("#orderForm").append(response);
console.log('Success');
},
error: function (response) {
console.log('Error = '+response);
}
});
};
</script>
forms.py
class ItemForm(forms.Form):
this_field = forms.CharField()
urls.py
urlpatterns = [
url(r'^newPickupOrder/$', views.pickup_order_view, name='new_pickup_order'),
]
views.py
def add_item(request):
if request.method == 'POST':
itemForm = ItemForm()
return HttpResponse(itemForm.as_p())
def pickup_order_view(request):
if request.method == 'POST':
form = ItemForm(request.POST)
same_name_fields = request.POST.getlist('this_field')
# WANT TO CLEAN DATA IN same_name_fields
if form.is_valid():
print(form.cleaned_data)
# ONLY PRINTS THE LAST FIELD's DATA
return HttpResponseRedirect('/viewPickupRequests')
The problem I'm facing is that if I use form.cleaned_data['this_field'], only the last field's data is fetched i.e. in this example, the field with value anotherTestValue is fetched and cleaned. If I fetch the data using request.POST.getlist('this_field'), all the fields' data is fetched and stored as a list, but, I don't know how to clean it using cleaned_data method. Is there a way to apply the cleaned_data method to the list of field data?
I'm sorry, I can't test if this works so this is not really an answer - but the comment system is not suitable for larger code chunks so I'm posting here.
Django forms lack a field type that renders to multiple text inputs with the same name. The proper thing to do would be to write a new form field class and a new widget. Since you are not rendering the form in the template (you are using it only for validation) I will omit the widget part.
class AcceptAnythingMultipleChoiceField(forms.MultipleChoiceField):
def validate(self, value):
if self.required and not value:
raise ValidationError(
self.error_messages['required'],
code='required'
)
Then use this field class instead of forms.CharField() (you may need to pass an empty choices parameter).
[update]
So essentially what you're saying is that I need to create new form field class and then render it to the template each time the user wants to add a new field? What if user has to add 15 fields, I'll need to create 15 classes then! I think this method won't be suitable in scenarios where number of fields required to be generated is large. I feel there should be some elegant way to do this which i'm not aware of – The OP
No, it is not what I'm saying. You probably want to subclass something like MultipleHiddenInput and set AcceptAnythingMultipleChoiceField.widget to it. You will have to create a new template based on the template for MultipleHiddenInput and replace input type="hidden" for type="text" (the original template is django/forms/widgets/multiple_hidden.html).
class AcceptAnythingWidget(MultipleHiddenInput):
template_name = 'django/forms/widgets/multiple_visible.html'
class AcceptAnythingMultipleChoiceField(forms.MultipleChoiceField):
widget = AcceptAnythingWidget
def validate(self, value):
if self.required and not value:
raise ValidationError(
self.error_messages['required'],
code='required'
)
This should render as many <input name='this_field'> as needed for instantiated forms at the frontend if you use:
{{ form.this_field }}
in the template, but will not add/remove them dynamically.
In order to do that you must plug in the JavaScript required to add/remove inputs dynamically in the widget but I will left this as an exercise for you. Look at Form Assets (the Media class) in the docs in order to figure out how to do that.
I think that what you are looking for is formsets. https://docs.djangoproject.com/en/2.0/topics/forms/formsets/
from django.forms import formset_factory
ItemFormSet = formset_factory(ItemForm, extra=2)
You can the essentialy use ItemFormSet in the way you would use a normal form except that this objects is iterable.
You will also have to change your jquery if you want to dynamically add items. There are many examples online on how to do this. In short what you do is
clone one of the forms in the formset
clear all the values from the copied form
update the input's (prefixes of) id's
Using Formsets doesn't solve the problem of fetching and validating
fields with same name. The issue still remains
It does however generate the end result you wanted (see below). My question would be why you need to have inputs with the same name? If there is some jquery stuff that uses these names I dont see any reason why you wouldn't be able to use name like... or assign a class to the inputs instead.
def pickup_order_view(request):
if request.method == 'GET':
ItemFormSet = formset_factory(ItemForm, extra=5)
item_formset = ItemFormSet()
template = "some_template.html"
template_context = {'item_formset': item_formset}
return render(request, template, template_context)
if request.method == 'POST':
ItemFormSet = formset_factory(ItemForm)
item_formset = ItemFormSet(request.POST)
same_name_fields=[]
if item_formset.is_valid():
for item_form in item_formset:
same_name_fields.append(item_form.cleaned_data['this_field'])
print(same_name_fields)
Template
<form id = "orderForm" action="newPickupOrder/" method="post" name="processForm">
{% csrf_token %}
{{ item_formset.management_form }}
{{ for item_form in item_formset }}
{{ item_form.as_p }}
{{ endfor }}
<input type='submit' value='submit'>
</form>
Go to newPickupOrder/ , fill in the 5 fields, hit submit, and watch it print your list.
I am building a form in django based on my models. I have this in my views.py:
class GroupCreateView(CreateView):
model = Group
form_class = GroupForm
template_name = 'ipaswdb/group/group_form.html'
the template is looking good with some nice form css and a datepicker and the like. I also created a form so I can add widgets in my forms.py
class GroupForm(forms.ModelForm):
notes=forms.CharField(widget = forms.Textarea)
billing_address = forms.ModelChoiceField(queryset=Address.objects.all(), widget=forms.Select(attrs={'tabindex':'5'}))
start_date = forms.DateField(widget=forms.TextInput(attrs=
{
'class':'datepicker',
'tabindex' : '5',
'placeholder' : 'Groups start date'
}))
class Meta:
model=Group
exclude = ['created_at', 'updated_at']
All this makes sense to me, I can see how the form is constructed based off my model and gets populated with things say in the ModelChoiceField etc.. I am just not sure how the def some_method in my views.py comes into play. So in my form template for the form action I have this:
<h1> Add a new Group </h1>
<form action="." method="post">
{% csrf_token %}
<div class="col-2">
{{ form.group_name.errors }}
<label>
Group Name:
<input placeholder="Enter the groups name" id="id_group_name" name="group_name" tabindex="1">
</label>
</div>
<div class="col-2">
{{ form.group_contact.errors }}
<label>
Gorup Contact
<input placeholder="Enter the groups contact name" id="id_group_contact" name="group_contact" tabindex="2">
</label>
</div>
<div class="col-2">
{{ form.tin.errors }}
<label>
TIN Number
<input placeholder="Groups TIN#" id="id_tin" name="tin" tabindex="3">
</label>
</div>
<div class="col-2">
{{ form.npi.errors }}
<label>
NPI Number
<input placeholder="Groups NPI#" id="id_npi" name="npi" tabindex="4">
</label>
etc etc etc
Which I think calls some default method in the view? I am just not sure what that method is. This is also for adding a new group, I am guessing I need another view or something to handle the case where they are editing an already existing group? This blog demo I was using did it all in the views.py with a forms.py as well thought here was no class GroupCreateView(CreateView): esque method in the example I was working off of in the views.py, there was this method (Note not my method):
def post_detail(request, year, month, day, post):
post = get_object_or_404(Post, slug=post,
status='published',
publish__year=year,
publish__month=month,
publish__day=day)
comments = post.comments.filter(active=True)
#their form stuff
if request.method == 'POST':
comment_form = CommentForm(data=request.POST)
if comment_form.is_valid():
new_comment = comment_form.save(commit=False)
new_comment.post = post
new_comment.save()
else:
comment_form=CommentForm()
return render(request,
'blog/post/detail.html',
{'post': post, 'comments':comments, 'comment_form':comment_form})
My question is (and I cannot remember what example to quote it)but what is the class GroupCreateView(CreateView): really doing and how can I get the form it references/creates to come back to call the right action ultimately letting me validate and save to the database?
Also a second sort of part is, how could I extend this (roughly) to handle the case where it is adding a new group, and also maybe another case where it is editing an existing one? (I ask this second question here because I am sure it relates to the answer from the first).
From my urls.py
url(r'group/add/$', GroupCreateView.as_view(), name='group-add'),
Dont be afraid to read django's source code :P, the generic class has two methods: "get" and "post" (and "put" too, but it calls "post") you can overwrite any of them if you need to.
class BaseCreateView(ModelFormMixin, ProcessFormView):
"""
Base view for creating an new object instance.
Using this base class requires subclassing to provide a response mixin.
"""
def get(self, request, *args, **kwargs):
self.object = None
return super(BaseCreateView, self).get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.object = None
return super(BaseCreateView, self).post(request, *args, **kwargs)
but it also inherits the methods of it's parents so it could be a little hard to read. I always check the docs for the generic views, it gives you a list of all the methods that you can overwrite on every generic class. Now you can overwrite all the methods you want without repeating code (that's why I <3 CBV)
I think in your case you might want to overwrite the form_valid() method to do something before redirecting to the success page
Hope this helps
As an example, let's take a look at the 'next' parameter in django.contrib.auth
If the client is trying to access some resources which is only available for authenticated users, the login url will be modified and attached with extra parameter as ?next=the_next_url . Then, the LoginForm could set this parameter into context_data and generate a form with a hidden input which contains its value like
{% if redirect_field_value %}
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" />
{% endif %}
However, how can I do this if I generate the form completely with django-crispy-form? In this case, the template file contains nothing but
{% crispy_tag form %}
form will be set as context data, which means I have to push the parameter from request.GET in the form as a hidden input widget.
How could I do this?
Finally, I figured it out by myself.
To solve this problem, the context_data in the original template based solution should be passed as initial into the constructor of forms.Form.
For example, with django CVB, get_initial is the right point to pass initial data to forms
def get_initial(self):
initial = Super(ThisCBV, self).get_initial()
redirect_field_name = self.get_redirect_field_name()
if (redirect_field_name in self.request.GET and
redirect_field_value in self.request.GET):
initial.update({
"redirect_field_name": redirect_field_name,
"redirect_field_value": self.request.REQUEST.get(
redirect_field_name),
})
return initial
Then, it is possible to add a field dynamically in the instance of forms.Form
def __init__(self, *args, **kwargs):
super(ThisForm, self).__init__(*args, **kwargs)
if ('redirect_field_name' in kwargs['initial'] and
'redirect_field_value' in kwargs['initial']):
self.has_redirection = True
self.redirect_field_name = kwargs['initial'].get('redirect_field_name')
self.redirect_field_value = kwargs['initial'].get('redirect_field_value')
## dynamically add a field into form
hidden_field = forms.CharField(widget=forms.HiddenInput())
self.fields.update({
self.redirect_field_name: hidden_field
})
## show this field in layout
self.helper = FormHelper()
self.helper.layout = Layout(
Field(
self.redirect_field_name,
type='hidden',
value=self.redirect_field_value
)
)
You can ask Django Crispy Form to not render the <form> tag, and only generate the <input> tags, which will let you then add your own extra <input>.
You do this by setting the form helper's form_tag property to False.
This is all documented in detail here. Note that unlike the example, you won't need {% crispy second_form %}, you'll only need to add your own if block there.