How to use autocompleteselect widget in a modelform - python

I know there is a new feature in Django 2.0, which is AutocompleteSelect widget in ModelAdmin.
I am trying to use it in my custom modelForm but just failed.
Tried like this
#unit is the foreign key to the incident
class AccountForm(forms.ModelForm):
class Meta:
model = Invoice
...
...
widgets = { 'incident':widgets.AutocompleteSelect(Invoice._meta.get_field('incident').remote_field, admin.site)
}
...
#Invoice model
class Invoice(models.Model):
...
incident = models.ForeignKey(Unit, on_delete=models.CASCADE,null=True)
...
Hope anyone can help me.
Thanks

The AutocompleteSelect widget will not work outside of the admin site.
If you are using AccountForm in admin site you can use the following code:
class AccountForm(forms.ModelForm):
...
incident = forms.ModelChoiceField(
queryset= Unit.objects.all(),
widget=AutocompleteSelect(Invoice.incident.field.remote_field, admin.site),
)
...
class Meta:
model = Invoice
fields = [
'incident',
...
]
#admin.register(Invoice)
class InvoiceAdmin(admin.ModelAdmin):
form = AccountForm

AutocompleteSelect has 2 required args, rel and admin_site. The rel is used to extract the model used to query the data from and relates to an attribute on a ForeignKey or ManyToManyField. Since I wanted to use this on a field that wasn't actually a ForeignKey, I needed to override a few things to make it work:
class ClientAutocompleteSelect(AutocompleteSelect):
def get_url(self):
model = Client
return reverse(self.url_name % (self.admin_site.name, model._meta.app_label, model._meta.model_name))
class ClientChoiceField(forms.ModelChoiceField):
def __init__(self, queryset=None, widget=None, **kwargs):
if queryset is None:
queryset = Client.objects.all()
if widget is None:
widget = ClientAutocompleteSelect(None, admin.site) # pass `None` for `rel`
super().__init__(queryset, widget=widget, **kwargs)
def to_python(self, value):
return value # just return the client_id and not a Client object
class MyAdminForm(forms.ModelForm):
client_id=ClientChoiceField()
...
This requires that the end user has admin read access to the autocomplete endpoint of the model being queried. You may be able to do more hacking to change that get_url and use your own endpoint to give search results, though.

I spent a few hours trying to understand why my code (built on #Tim 's answer) would not work, until I stumble on a comment about missing references to css/js files.
Here is a complete working solution to use the AutocompleteSelect widget in any custom form for signed-in users having both 'staff' and 'view' access to the given model:
from django.urls import reverse
from django.contrib.admin.widgets import AutocompleteSelect
from django.contrib import admin
class UserAutocompleteSelect(AutocompleteSelect):
def get_url(self):
model = CustomUser
return reverse(self.url_name % (self.admin_site.name, model._meta.app_label, model._meta.model_name))
class UserChoiceField(forms.ModelChoiceField):
def __init__(self, queryset=None, widget=None, **kwargs):
if queryset is None:
queryset = CustomUser.objects.all()
if widget is None:
widget = UserAutocompleteSelect(None, admin.site) # pass `None` for `rel`
super().__init__(queryset, widget=widget, **kwargs)
class UserAutocompleteSelectForm(forms.ModelForm):
"""
for changing user on Play objects
using amdin module autocomplete
"""
user = UserChoiceField(
# queryset=CustomUser.objects.all(),
help_text=_('Select the user to replace the current one')
)
class Meta:
model = Play
fields = ('user', )
You can use the same, replacing CustomUser and Play by your own models
If (like me) this is not working out-of-the-box with the html template you're using, that means that you need to include the required css/js files to your template. Here is a simple way to do it :
Providing that the form is declared as such in the view:
form = UserAutocompleteSelectForm()
...
context = {
'form': form,
...
}
return render(request, 'users/change_user.html', context)
you should add the following lines to the html template to include the required css/js files:
{% block extrahead %}
{{ block.super }}
{{ form.media }}
{% endblock %}

Related

Foriegn key shown as a drop down. Any way to show it as a text value ? (django)

In my application, on page1, I create a Project object and then on page 2, I create a batch object.
The "batch" object has a many-to-one relationship with "Project".
It therefore needs to display the Project object when batch form is called.
I can show the Project object on a batch form but it is shown as a drop down.
I would like it to be shown as a field value but it doesnt work.
Can you please help.
Thanks.
models.py
Class Project
name = models.CharField()
Class Batch
name = models.CharField()
project = models.ForeignKey('Project', on_delete=models.CASCADE)
template.py
<a href="{% url 'create_batch_url' pk=project.id %}" role="button" >Link to Batch</a>
passing the foreign key from
urls.py
path('batch/new/<int:pk>', batch_views.review_batch, name='create_batch_url'),
views.py
simple view which calls the model form
forms.py
class BatchForm(ModelForm):
class Meta:
model = Batch
fields = ('name', 'project',)
def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request")
project_id = kwargs.pop("project_id")
#Only show the project where the request came from
self.fields['project'].queryset = Project.objects.filter(id=project_id)
You need to add a widgets variable to the form meta. Link to the documentation
from django.forms import TextInput
class BatchForm(ModelForm):
class Meta:
model = Batch
fields = ('name', 'project',)
widgets = {'project': TextInput}
def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request")
project_id = kwargs.pop("project_id")
#Only show the project where the request came from
self.fields['project'].queryset = Project.objects.filter(id=project_id)

Django ModelForm with model having ForeignKey field does not display selectBox correctly

This is probably easy to solve. I created a form which use forms.ModelForm.
My model has ForeignKey field. Form creates a select field for foreignKey, but but does not display value correctly.
models.py
from django.db import models
# Create your models here.
class Gender(models.Model):
name = models.CharField(max_length=8, null=True)
class Meta:
db_table='gender'
class UserExample(models.Model):
name = models.CharField(max_length=16,null=True)
age = models.IntegerField(blank=True, null=True)
gender = models.ForeignKey('Gender', on_delete=models.SET_NULL, null=True)
class Meta:
db_table='userExample'
def __str__(self):
return ""
forms.py
from django import forms
from .models import UserExample, Gender
class UserForm(forms.ModelForm):
class Meta:
model = UserExample
fields = ('name', 'age', 'gender')
views.py
from django.shortcuts import render
from .forms import UserForm
# Create your views here.
def index(request):
form = UserForm
return render(
request,
'index.html',
{'form': form}
)
urls.py
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.index, name='index'),
]
index.html
<html>
<head>
<title>test </title>
</head>
<body>
<form action="formreturn/" method="post">
{{ form.as_p }}
<input type="submit" value="Submit" />
</form>
</body>
</html>
And after I launch my app. In select box I get only selection for gender objects but not for gender names.
Optional I used to add values using sqllite3 like this:
sqlite> insert into gender values(1,"Male");
sqlite> insert into gender values(2,"Female");
Implement __unicode__ or __str__ method in Gender model,
def __unicode__(self):
return '%s' % self.name
And it will display gender names in your option choices.
How to customize the default form field behavior of ForeignKey Model field
ForeignKey maps to ModelChoiceField and you can override the default behavior of the same. You can override the 'Select' option field value using 'to_field_name' parameter (useful when you have multiple unique fields in your related model) otherwise option field values are defaulted to pk field.
'empty_label' will change the default "--------" with the empty_label atrribute
forms.py
class UserForm(forms.ModelForm):
gender = forms.ModelChoiceField(queryset=Gender.objects.all(),
to_field_name = 'name',
empty_label="Select Gender")
class Meta:
model = UserExample
fields = ('name', 'age', 'gender')
Option display names are defaulted to __str__ method output. You can override the default behavior by writing a custom choice field class (inherited from ModelChoiceField) and override the label_from_instance() method.
def__str__(self):
return self.name
You will add this on your gender class
I already gave this answer in another place, hope it's not bad to copy paste.
In my case, I didn't wanna go make an str for my billion models, so I just did this:
You can make one custom ModelChoiceField to which you can pass a function. That way if you have different fields for which you want different attributes to be displayed, you can have only 1 class:
class CustomModelChoiceField(forms.ModelChoiceField):
name_function = staticmethod(lambda obj: obj)
def __init__(self, name_function, *args, **kwargs):
if not name_function is None: self.name_function = name_function
super(CustomModelChoiceField, self).__init__(*args, **kwargs)
def label_from_instance(self, obj):
return self.name_function(obj);
You can then call it as simply as this:
form_field = CustomModelChoiceField(
lambda obj: obj.get_full_name(),
queryset=Whatever.objects.all(),
)
You can also pass None in case you're doing some dynamic stuff and it'll just basically default to a regular ModelChoiceField. I'm not too much of a python guy but this works for me.

How do I pass a parent id as an fk to child object's ModelForm using generic class-based views in Django?

I am trying to use Django Generic Class-Based Views to build a CRUD interface to a two-model database. I have a working CRUD interface to the parent model, and am stuck trying to get the child Create working. For consistency with other Django examples, take the parent to be Author and the child to be Book. What is the simplest way to allow users to add Books to an Author?
In HTML terms, I think that I want to make a link on the Author detail page that includes the ID of the Author, have that ID be pre-set on the Book form, and then have the Book form processing use that ID as the PK of the Book. But I don't understand how to use Django to make this happen. I have read through https://docs.djangoproject.com/en/1.6/topics/class-based-views/generic-editing/, How do I use CreateView with a ModelForm, How do I set initial data on a Django class based generic createview with request data, and Set initial value to modelform in class based generic views, each of which seems to answer a slightly different question.
Here is the relevant code:
models.py
class Author(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
author = models.ForeignKey(Author)
title = models.CharField(max_length=500)
views.py
class BookCreate(CreateView):
form_class = BookForm
def get_success_url(self):
return reverse('myapp:author_read',args=(self.object.author.pk))
forms.py
class BookForm(forms.Modelform):
class Meta:
model = Book
urls.py
url(r'^(?P<pk>\d+)/$', AuthorRead.as_view(), name='author_read'),
url(r'^book/create/(?P<author_id>\d+)/$', BookCreate.as_view(), name='book_create'),
templates/myapp/author_detail.html
...
<p>Add a book</p>
...
templates/myapp/book_form.html
<form action="" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Done">
</form>
Questions
1) How do I get the Author ID from the Book page URL to the Author form, and then processed correctly? With the sample code above, the Django debugger shows that it's present in this way:
View function Arguments Keyword arguments URL name
myapp.views.BookCreate () {'author_id': u'1234'} book_create
but I don't understand how to grab that variable out of the ... context? ... and put it into the form.
1a) Can I make it a url parameter instead of part of the URL itself, i.e., book/create?author=1234 instead of book/create/1234/? Or even make the whole thing a POST so that it's not part of the URL? Which is the best practice, and how is it done?
2) Once the variable is in the form, how can it be present as a hidden input, so that the user doesn't have to see it?
With the url that you defined in author_detail.html the author_id variable will be accessible in the view as self.kwargs['author_id']
# views.py
class BookCreate(CreateView):
...
def form_valid(self, form):
book = form.save(commit=False)
author_id = form.data['author_id']
author = get_object_or_404(Author, id=author_id)
book.author = author
return super(BookCreate, self).form_valid(form)
...
def get_context_data(self, **kwargs):
context = super(BookCreate, self).get_context_data(**kwargs)
context['a_id'] = self.kwargs['author_id']
return context
Then in your form you can add:
class BookForm(forms.Modelform):
...
def __init__(self, *args, **kwargs):
self.fields["author_id"] = forms.CharField(widget=forms.HiddenInput())
super(BookForm, self).__init__(self, *args, **kwargs)
Then in the template:
<input type=hidden name="author_id" value="{{ a_id }}">
The form_valid in the view should retrieve the id, get the appropriate author and set that author as the books author. The commit=False prevents the model getting saved at first while you set the author and calling super will result in form.save(commit=True) being called.
You could pass the author id to the form, here's some directions:
class BookForm(forms.Modelform):
author = None
class Meta:
model = Book
def __init__(self, *args, **kwargs):
self.author = kwargs.pop('author')
super(BookForm, self).__init__(*args, **kwargs)
def save(self, commit=True):
# your custom save (returns book)
class BookCreate(CreateView):
form_class = BookForm
def get_form_kwargs(self):
kwargs = super(BookCreate, self).get_form_kwargs()
kwargs['author'] = # pass your author object
return kwargs
I had a similar situation and, when doing the accepted answer steps I encountered 2 errors (I'm using Python 2.7):
object has no attribute 'fields' which was fixed by using answer to a similar question: https://stackoverflow.com/a/8928501/2097023 from #scotchandsoda:
...self.fields should be placed before calling super(...)
def __init__(self, users_list, **kw):
super(BaseWriteForm, self).__init__(**kw)
self.fields['recipients'].queryset = User.objects.filter(pk__in=users_list)
object has no attribute 'get' which was fixed using answer: https://stackoverflow.com/a/36951830/2097023 from #luke_dupin:
...this error can also be generated by incorrectly passing arguments in the init of a form, which is used for an admin model.
Example:
class MyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(self, *args, **kwargs)
Notice the double passing of self? It should be:
class MyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)

Change a Django form field to a hidden field

I have a Django form with a RegexField, which is very similar to a normal text input field.
In my view, under certain conditions I want to hide it from the user, and trying to keep the form as similar as possible. What's the best way to turn this field into a HiddenInput field?
I know I can set attributes on the field with:
form['fieldname'].field.widget.attr['readonly'] = 'readonly'
And I can set the desired initial value with:
form.initial['fieldname'] = 'mydesiredvalue'
However, that won't change the form of the widget.
What's the best / most "django-y" / least "hacky" way to make this field a <input type="hidden"> field?
This may also be useful: {{ form.field.as_hidden }}
If you have a custom template and view you may exclude the field and use {{ modelform.instance.field }} to get the value.
also you may prefer to use in the view:
field = form.fields['field_name']
field.widget = field.hidden_widget()
but I'm not sure it will protect save method on post.
edit: field with multiple values don't supports HiddenInput as input type, so use default hidden input widget for this field instead.
an option that worked for me, define the field in the original form as:
forms.CharField(widget = forms.HiddenInput(), required = False)
then when you override it in the new Class it will keep it's place.
Firstly, if you don't want the user to modify the data, then it seems cleaner to simply exclude the field. Including it as a hidden field just adds more data to send over the wire and invites a malicious user to modify it when you don't want them to. If you do have a good reason to include the field but hide it, you can pass a keyword arg to the modelform's constructor. Something like this perhaps:
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
def __init__(self, *args, **kwargs):
from django.forms.widgets import HiddenInput
hide_condition = kwargs.pop('hide_condition',None)
super(MyModelForm, self).__init__(*args, **kwargs)
if hide_condition:
self.fields['fieldname'].widget = HiddenInput()
# or alternately: del self.fields['fieldname'] to remove it from the form altogether.
Then in your view:
form = MyModelForm(hide_condition=True)
I prefer this approach to modifying the modelform's internals in the view, but it's a matter of taste.
For normal form you can do
class MyModelForm(forms.ModelForm):
slug = forms.CharField(widget=forms.HiddenInput())
If you have model form you can do the following
class MyModelForm(forms.ModelForm):
class Meta:
model = TagStatus
fields = ('slug', 'ext')
widgets = {'slug': forms.HiddenInput()}
You can also override __init__ method
class Myform(forms.Form):
def __init__(self, *args, **kwargs):
super(Myform, self).__init__(*args, **kwargs)
self.fields['slug'].widget = forms.HiddenInput()
If you want the field to always be hidden, use the following:
class MyForm(forms.Form):
hidden_input = forms.CharField(widget=forms.HiddenInput(), initial="value")
If you want the field to be conditionally hidden, you can do the following:
form = MyForm()
if condition:
form.fields["field_name"].widget = forms.HiddenInput()
form.fields["field_name"].initial = "value"
Example of a model:
models.py
from django.db import models
class YourModel(models.Model):
fieldname = models.CharField(max_length=255, default="default")
In your form, you can add widgets with ModelForm. To make it hidden add 'type': 'hidden' as shown below👇
forms.py
from .models import YourModel
from django import forms
class YourForm(forms.ModelForm):
class Meta:
model = YourModel
fields = ('fieldname',)
widgets = {
'fieldname': forms.TextInput(attrs={'type': 'hidden'}),
}
If you don't know how to add it to your views.py file, here is some videos about it.
If you use Function Based View:
https://www.youtube.com/watch?v=6oOHlcHkX2U
If you use Class Based View:
https://www.youtube.com/watch?v=KB_wDXBwhUA
{{ form.field}}
{{ form.field.as_hidden }}
with this jinja format we can have both visible form fields and hidden ones too.
if you want to hide and disable the field to protect the data inside. as others mentioned use the hiddenInput widget and make it disable
in your form init
example:
if not current_user.is_staff:
self.base_fields['pictureValid'].disabled = True
self.base_fields['pictureValid'].widget = forms.HiddenInput()
With render_field is easy
{% render_field form.field hidden=True %}
You can just use css :
#id_fieldname, label[for="id_fieldname"] {
position: absolute;
display: none
}
This will make the field and its label invisible.

How to show hidden autofield in django formset

A Django autofield when displayed using a formset is hidden by default. What would be the best way to show it?
At the moment, the model is declared as,
class MyModel:
locid = models.AutoField(primary_key=True)
...
When this is rendered using Django formsets,
class MyModelForm(ModelForm):
class Meta:
model = MyModel
fields = ('locid', 'name')
it shows up on the page as,
<input id="id_form-0-locid" type="hidden" value="707" name="form-0-locid"/>
Thanks.
Edit
I create the formset like this -
LocFormSet = modelformset_factory(MyModel)
pformset = LocFormSet(request.POST, request.FILES, queryset=MyModel.objects.order_by('name'))
Second Edit
Looks like I'm not using the custom form class I defined there, so the question needs slight modification..
How would I create a formset from a custom form (which will show a hidden field), as well as use a custom queryset?
At the moment, I can either inherit from a BaseModelFormSet class and use a custom query set, or I can use the ModelForm class to add a custom field to a form. Is there a way to do both with a formset?
Third Edit
I'm now using,
class MyModelForm(ModelForm):
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
locid = forms.IntegerField(min_value = 1, required=True)
self.fields['locid'].widget.attrs["type"] = 'visible'
self.queryset = MyModel.objects.order_by('name')
class Meta:
model = MyModel
fields = ('locid', 'name')
LocFormSet = modelformset_factory(MyModel, form = MyModelForm)
pformset = LocFormSet()
But this still doesn't
Show locid
Use the custom query that was specified.
Try changing the default field type:
from django import forms
class MyModelForm(ModelForm):
locid = forms.IntegerField(min_value=1, required=True)
class Meta:
model = MyModel
fields = ('locid', 'name')
EDIT: Tested and works...
As you say, you are not using the custom form you have defined. This is because you aren't passing it in anywhere, so Django can't know about it.
The solution is simple - just pass the custom form class into modelformset_factory:
LocFormSet = modelformset_factory(MyModel, form=MyModelForm)
Edit in response to update 3:
Firstly, you have the redefinition for locid in the wrong place - it needs to be at the class level, not inside the __init__.
Secondly, putting the queryset inside the form won't do anything at all - forms don't know about querysets. You should go back to what you were doing before, passing it in as a parameter when you instantiate the formset. (Alternatively, you could define a custom formset, but that seems like overkill.)
class MyModelForm(ModelForm):
locid = forms.IntegerField(min_value=1, required=True)
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
self.fields['locid'].widget.attrs["type"] = 'visible'
class Meta:
model = MyModel
fields = ('locid', 'name')
LocFormSet = modelformset_factory(MyModel, form = MyModelForm)
pformset = LocFormSet(request.POST, request.FILES,
queryset=MyModel.objects.order_by('name')))
Okay, none of the approaches above worked for me. I solved this issue from the template side, finally.
There is a ticket filed (http://code.djangoproject.com/ticket/10427), which adds a "value" option to a template variable for a form. For instance, it allows,
{{form.locid.value}}
to be shown. This is available as a patch, which can be installed in the SVN version of django using "patch -p0 file.patch"
Remember, the {{form.locid.value}} variable will be used in conjunction with the invisible form - otherwise, the submit and save operations for the formset will crash.
This is Not the same as {{form.locid.data}} - as is explained in the ticket referred to above.
The reason that the autofield is hidden, is that both BaseModelFormSet and BaseInlineFormSet override that field in add_field. The way to fix it is to create your own formset and override add_field without calling super. Also you don't have to explicitly define the primary key.
you have to pass the formset to modelformset_factory:
LocFormSet = modelformset_factory(MyModel,
formset=VisiblePrimaryKeyFormSet)
This is in the formset class:
from django.forms.models import BaseInlineFormSet, BaseModelFormSet, IntegerField
from django.forms.formsets import BaseFormSet
class VisiblePrimaryKeyFormset(BaseModelFormSet):
def add_fields(self, form, index):
self._pk_field = pk = self.model._meta.pk
if form.is_bound:
pk_value = form.instance.pk
else:
try:
pk_value = self.get_queryset()[index].pk
except IndexError:
pk_value = None
form.fields[self._pk_field.name] = IntegerField( initial=pk_value,
required=True) #or any other field you would like to display the pk in
BaseFormSet.add_fields(self, form, index) # call baseformset which does not modify your primary key field

Categories

Resources