I have a form which has more than 10 fields. Now i want a particular field, lets say "requirements". This can be more than one requirement, i can deal with that using rich text editor as ask my user to input all the requirements as a ordered list. But for better user experience i am asking this !
I am gonna keep a button under "requirements" field, so that user can click on this button to get a new field. By this, i want all fields to be combined in a dict like
requirements = {'field1','extrafield1'} and etc
How to perform this ? I cant user formset ( as am just adding dynamic field not whole form )
How to deal this with django forms ?
I had a similar problem and came up with following solution (it's based on this answer mainly, but I was able to avoid any javascript):
form contains hidden field to store count of requirement fields; by default form has one such field, but pressing button "add_requirement" increases this count in TestView.post(). On __init__ form adds new fields with proper indexes, if needed, and on save() it goes through all requirement indexes to collect values from respective fields.
TestView.post() works differently for form's two buttons: "add_requirement" just increases fields count and renders form again with added field; default "submit" button saves valid form and redirects to success_url or re-displays invalid one.
forms.py
class SimpleForm(forms.Form):
requirements_count = forms.CharField(widget=forms.HiddenInput(), initial=1)
stable_field = forms.CharField(label='Stable field', required=False)
other_stable_field = forms.CharField(label='Other field', required=False)
requirements_1 = forms.CharField(label='Requirements', required=False)
def __init__(self, *args, **kwargs):
super(SimpleForm, self).__init__(*args, **kwargs)
if self.is_bound:
requirements_count = int(self.data.get('requirements_count', 1))
else:
requirements_count = int(self.initial.get('requirements_count', 1))
if requirements_count > 1:
for index in range(2, requirements_count + 1):
self.fields.update(
{'requirements_' + str(index):
forms.CharField(label='Requirements', required=False)}
)
def save(self, *args, **kwargs):
form = self.cleaned_data
requirements_count = int(form.get('requirements_count', 1))
requirements_list = []
for index in range(1, requirements_count + 1):
requirements = form.get('requirements_' + str(index), '')
if requirements:
requirements_list.append(requirements)
views.py
class TestView(FormView):
template_name = 'testtemplate.html'
form_class = SimpleForm
form = None
success_url = reverse_lazy('home')
def post(self, request, *args, **kwargs):
request_POST = request.POST
requirements_count = int(request_POST.get('requirements_count', 1))
if 'add_requirement' in request_POST:
new_initial = get_initial(request_POST)
new_initial['requirements_count'] = requirements_count + 1
self.form = SimpleForm(initial=new_initial)
context = self.get_context_data(form=self.form)
response = render(request, self.template_name, context)
else:
self.form = SimpleForm(data=request_POST)
context = self.get_context_data(form=self.form)
if self.form.is_valid():
response = self.form_valid(self.form)
else:
response = render(request, self.template_name, context)
return response
def form_valid(self, form):
form.save()
return super().form_valid(self.form)
# helper function
def get_initial(previous_data):
new_initial = {}
for key, value in previous_data.items():
if value != '' and key != 'csrfmiddlewaretoken':
new_initial.update({key: value})
return new_initial
testtemplate.html
<form action="" method="post">
{% csrf_token %}
<table border="1">
{{ form.as_table }}
</table>
<input type="submit">
<input type="submit" name="add_requirement" value="Add another requirement">
{{ form.non_field_errors }}
</form>
I'm in no way Django expert and it may be not the best solution, but it works for me and is relatively simple.
Related
For example i use the extended user model called "profile" and in the profile i have many fields describing the using. what i want is that the user choose whether he is a business user or a regular customer on the web site. but once this field is updated it cannot be updated again, in which the field appears in the profile edit form but as non-editable field.
What is the best way to do that?
I thought of using an additional field in the database where i include an "update date" and if this field is not null then the user cannot update. but it seems to me too much coding for this small feature.
Thanks in advance
The code :
forms.py
class ProfileForm(forms.ModelForm):
class Meta:
model = Profile
fields = ['phone','profile_type','city','image']
template (html)
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{userform.as_p}} <br>
<table>
<tr>
<td> {{profileform.profile_type.label}}</td>
<td>{{profileform.profile_type}}</td>
</tr>
<tr>
<td> {{profileform.image.label}}</td>
<td>{{profileform.image}}</td>
</tr>
</table>
<br>
<input type="submit" name="" value="Update Profile">
</form>
Views.py
def profile_edit(request):
profile = Profile.objects.get(user=request.user)
if request.method == 'POST':
userform = UserForm(request.POST,instance=request.user)
profileform = ProfileForm(request.POST,request.FILES,instance=profile)
if userform.is_valid() and profileform.is_valid():
userform.save()
myprofile = profileform.save(commit=False)
myprofile.user = request.user
myprofile.save()
return redirect(reverse('accounts:profile'))
else :
userform = UserForm(instance=request.user)
profileform = ProfileForm(instance=profile)
return render(request,'accounts/profile_edit.html',{'userform':userform , 'profileform':profileform})
What about something like:
class MyModel(models.Model):
user_type = models.CharField(max_length=100, choices=(("a","a"), ("b","b")))
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._original_user_type = self.user_type
def save(self, *args, **kwargs):
if self._original_user_type != self.user_type:
raise ValidationError("You can't change user_type!")
super().save(*args, **kwargs)
This should prevent you from changing user_type without requiring another field.
(But I haven't tested it.)
updated
In light of your updates it seems like you'd like to catch the error during form processing as opposed to model processing. There are a number of ways to solve this.
You could add a custom clean method to the form to check if user_type has changed and raise a ValidationError if it has. Something like:
def clean(self):
cleaned_data = super().clean()
instance = getattr(self, "instance", None)
if instance and instance.pk and instance.user_type != cleaned_data["user_type"]:
raise ValidationError("You can't change user_type!")
return cleaned_data
You could add a custom clean method to the field to negate any changes to user_type. Something like:
def clean_user_type(self):
instance = getattr(self, "instance", None)
if instance and instance.pk:
return instance.user_type
else:
return self.cleaned_data["user_type"]
Or, you could just disable the user_type field if the underlying model already exists:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
instance = getattr(self, "instance", None)
if instance and instance.pk:
self.fields["user_type"].disabled = True
(This is still untested code.)
I want to create a form that shows 2 different textboxes with a minimum and a maximum value of my model's price field, which is a DecimalField. But I'm not sure where to begin. I know I can calculate the min and max value, but I'm not sure how to add that to the placeholder and/or value text. So for right now, I'm just using the view to push the values, but they won't submit in the form. Here is my code so far:
forms.py
class ProductSearchForm(forms.ModelForm):
price_min = forms.DecimalField()
price_max = forms.DecimalField()
def __init__(self, *args, **kwargs):
super(ProductSearchForm, self).__init__(*args, **kwargs)
self.fields['length_range'].empty_label = "any size"
self.fields['hull_type'].empty_label = "any type"
self.fields['power'].empty_label = "any type"
self.fields['speed'].empty_label = "any speed"
self.fields['hull_only_available'].empty_label = None
self.fields['price_min'].widget.attrs['min'] = kwargs['price']['price__min']
self.fields['price_max'].widget.attrs['max'] = kwargs['price']['price__max']
class Meta:
model = Product
fields = ('length_range', 'hull_type', 'price', 'power', 'speed', 'hull_only_available')
views.py
class IndexView(FormView):
template_name = 'index.html'
form_class = ProductSearchForm
success_url = "search/"
def get_price(self):
price = getattr(self, "_price", None)
if price is None:
price = Product.objects.all().aggregate(Min('price'), Max('price'))
setattr(self, "_price", price)
return price
def get_context_data(self, **kwargs):
context = super(IndexView, self).get_context_data(**kwargs)
context['length_ranges'] = LengthRange.objects.all().order_by('pk')
context['hull_types'] = Hull.objects.all().order_by('pk')
context['power_configs'] = PowerConfiguration.objects.all().order_by('pk')
context['speed_ranges'] = SpeedRange.objects.all().order_by('pk')
return context
def get_form_kwargs(self):
form_kwargs = super(IndexView, self).get_form_kwargs()
form_kwargs['price'] = self.get_price()
return form_kwargs
index.html
<form class="nl-form" action="{% url 'boatsales:search' %}" method="get">
A boat with a length of
{{ form.length_range }}
, with hull type of
{{ form.hull_type }}
with
{{ form.power }}
power
configuration and a top speed between
{{ form.speed }}.
My budget is from $<input type="text" value="{{ price.price__min|intcomma }}"
placeholder="{{ price.price__min|intcomma }}"
data-subline="Our current lowest price is: <em>{{ price__min|intcomma }}</em>"/>
to
$<input
type="text" value="{{ price.price__max|intcomma }}"
placeholder="{{ price.price__min|intcomma }}"
data-subline="Our current highest price is: <em>{{ price.price__min|intcomma }}</em>"/>.
Hull only
availability <select>
<option value="False" selected>is not</option>
<option value="True">is</option>
</select> a concern.
<div class="container">
<button type="submit"
class="btn-a btn-a_size_large btn-a_color_theme">
Show me the results!
</button>
</div>
</form>
EDIT
Now I'm getting a TypeError: __init__() got an unexpected keyword argument 'price'
Seems to be coming from this line in views.py
context = super(IndexView, self).get_context_data(**kwargs)
Sorry, ignore my previous answer, I completely misunderstood your question.
I think the problem here is that you are creating a form with only the fields that are contained in Product.
However you need a form that contains MORE fields. There is nothing that limits you to only using fields that are in the model.
So the idea would be to ignore price (which is a specific price), and instead add price_min and price_max:
class ProductSearchForm(forms.ModelForm):
price_min = form.DecimalField()
price_max = form.DecimalField()
def __init__(self, *args, **kwargs):
# Pop the price argument, as the parent form doesn't know about it!
price = kwargs.pop("price")
super(ProductSearchForm, self).__init__(*args, **kwargs)
# ... existing code...
self.fields['price_min'].widget.attrs['min'] = price['price__min']
self.fields['price_min'].widget.attrs['max'] = price['price__max']
self.fields['price_max'].widget.attrs['min'] = price['price__min']
self.fields['price_max'].widget.attrs['max'] = price['price__max']
You can use these fields in your template as any other regular field.
You can then pass the current price values as you kind of already attempting to do, via get_form_kwargs:
class IndexView(FormView):
# ... existing code...
def get_form_kwargs(self):
form_kwargs = super(IndexView, self).get_form_kwargs()
form_kwargs['price'] = Product.objects.all().aggregate(Min('price'), Max('price'))
return form_kwargs
Extra suggestion:
If you want to avoid making a query to Product twice to get the min/max, then you can create a method that caches the result, like:
(in view)
def get_price(self):
price = getattr(self, "_price", None)
if price is None:
price = Product.objects.all().aggregate(Min('price'), Max('price'))
setattr(self, "_price", price)
return price
and then you can call self.get_price() to populate the form kwargs and the template context.
I have a formset, but I can't figure out how to prepopulate it with data from the database.
Truth be told, I have a model Library and a model Book that has a foreign key to Library. I want someone to be able to edit all of the book in one library (at a time).
html
{{ book_form.management_form }}
{% for form in book_form.forms %}
{{ form.as_p }}
{% endfor %}
views.py
class LibraryUpdateView(LoginRequiredMixin, UpdateView):
model = Library
form_class = LibraryChangeForm
template_name = 'libraries/library_update.html'
def get_context_data(self, *args, **kwargs):
context = super(LibraryUpdateView, self).get_context_data(*args, **kwargs)
if self.request.POST:
context['book_form'] = BookFormSet(self.request.POST)
else:
context['book_form'] = BookFormSet()
return context
forms
class LibraryChangeForm(forms.ModelForm):
class Meta:
exclude = ['updated']
BookFormSet = inlineformset_factory(Library, Book, exclude = () )
How do I prepopulate the html page to show the data in the formset. In my loop there are 3 loops for {{form.as_p}}. When I run print(BookFormsSet()) I see the following
<input type="hidden" name="perdateinfo_set-TOTAL_FORMS" value="3"...
So it looks like it is generating three blank forms.
When I run print(PerDateInfoFormSet(qs,qs2)) for qs = Library.objects.filter(library_id='slkdfj') and qs2=Book.objects.filter(library_id='slkdfj') It tells me too many values to unpack (expected 2)
Can I pass in a query set? I imagine that I will be passing in a qs.first() to get the library and then multiple (qs2) query sets to get all of the books in the library.
You need to pass instance argument to the formset:
def get_context_data(self, *args, **kwargs):
context = super(LibraryUpdateView, self).get_context_data(*args, **kwargs)
if self.request.POST:
context['book_form'] = BookFormSet(self.request.POST, instance=self.object)
else:
context['book_form'] = BookFormSet(instance=self.object)
return context
I have a simple form witch ModelChoiceField. This is a part of my view.py file:
def premium(request, id):
context = {}
try:
site = Site.objects.get(id=id)
except Site.DoesNotExist:
raise Http404("Nie ma takiej strony")
if request.method == 'POST':
premium_form = PremiumForm(request.POST)
if premium_form.is_valid():
# group = Group.objects.get(id=request.POST["kod"])
print('OK')
else:
print('NOT OK')
else:
premium_form = PremiumForm(site)
premium_form.fields['group'].queryset =
premium_form.fields['group'].queryset.exclude(group_name=site.group)
context['site'] = site
context['form'] = premium_form
context['category'] = site.category
context['subcategory'] = site.subcategory
return render(request, 'mainapp/premium.html', context)
This is my form:
class PremiumForm(forms.Form):
def __init__(self, site, *args, **kwargs):
super(PremiumForm, self).__init__(*args, **kwargs)
self.fields['group'].initial = 2
self.fields['group'].empty_label = None
group = forms.ModelChoiceField(
queryset=Group.objects.filter(is_active=True),
help_text="<div id='group'></div>",
label="Some text",
required=False)
My premium.html file:
<form method="post" action="" class="form-horizontal">
{% csrf_token %}
{% bootstrap_form form layout='horizontal'%} <br>
{% bootstrap_button "Submit" size='large' button_type="submit" button_class="btn-primary btn-main-add" %}
</form>
When I press "Submit" button I get "NOT OK". I can't resolve this problem. I don't have any idea how to validate forms.ModelChoiceField. Thanks for any help.
Form should be initialised with kwargs:
premium_form = PremiumForm(site=site)
And inside init:
def __init__(self, *args, **kwargs):
site = kwargs['site']
However, site is not used inside form initialization so you can just remove it and it will solve the issue.
When I submit this form & all fields are correctly populated, the form.is _valid() returns false & all the fields give : this field is required error, even the CharField!!!
can anybody see what's wrong?
this is my form:
class TemplateConfiguredForm(forms.Form):
"""This form represents the TemplateConfigured Form"""
template = forms.ChoiceField(widget=forms.Select(attrs={'id':'TemplateChoice'}))
logo = forms.ImageField( widget = forms.FileInput(attrs={'id': 'inputLogo'}))
image = forms.ImageField(widget = forms.FileInput(attrs={'id': 'inputImage'}))
message = forms.CharField(widget = forms.Textarea(attrs={'id': 'inputText', 'rows':5, 'cols':25}))
def __init__(self, custom_choices=None, *args, **kwargs):
super(TemplateConfiguredForm, self).__init__(*args, **kwargs)
r = requests.get('http://127.0.0.1:8000/sendMails/api/templates/?format=json')
json = r.json()
custom_choices=( ( template['url'], template['name']) for template in json)
if custom_choices:
self.fields['template'].choices = custom_choices
this my template:
<form id="template_form" method="post" role="form" enctype="multipart/form-data" action="{% url 'create_templates' %}" >
{% csrf_token %}
{{ form.as_p }}
{% buttons %}
<input type="submit" value="Save Template"/>
{% endbuttons %}
</form>
this is my view:
def create_templates(request):
if request.method == 'POST':
form = TemplateConfiguredForm(request.POST, request.FILES)
if form.is_valid():
template_configured = TemplateConfigured()
template_configured.owner = request.user
template_configured.logo = form.cleaned_data["logo"]
template_configured.image = form.cleaned_data["image"]
template_configured.message = form.cleaned_data["message"]
template = form.cleaned_data['template']
template = dict(form.fields['template'].choices)[template]
template_configured.template = Template.objects.get(name = template)
template_configured.save()
saved = True
else:
print form.errors
else:
form = TemplateConfiguredForm()
return render(request, 'sendMails/createTemplates.html', locals())
You've changed the signature of the form so that the first positional argument is custom_choices. Don't do that.
You don't seem to actually be passing that value at all from your view, so you should probably remove it completely. But if you do need it, you should get it from the kwargs dict:
def __init__(self, *args, **kwargs):
custom_choices = kwargs.pop('custom_choices')
super(TemplateConfiguredForm, self).__init__(*args, **kwargs)
The data you pass in your form, here:
form = TemplateConfiguredForm(request.POST, request.FILES)
is captured by the first keyword argument of your signature:
def __init__(self, custom_choices=None, *args, **kwargs):
Remove the custom_choices=None