Creating a generic search view in Django - django

I am struggling to create my custom generic view in django to easily create search pages for certain models. I'd like to use it like this:
class MyModelSearchView(SearchView):
template_name = 'path/to/template.html'
model = MyModel
fields = ['name', 'email', 'whatever']
which will result in a view that returns a search form on GET and both form and results on POST.
The fields specifies which fields of MyModel will be available for a user to search.
class SearchView(FormView):
def get_form(self, form_class=None):
# what I'v already tried:
class SearchForm(ModelForm):
class Meta:
model = self.model
fields = self.fields
return SearchForm()
def post(self, request, *args, **kwargs):
# perform searching and return results
The problem with the code above is that form will not be submitted if certain fields are not be properly filled. User should be allowed to provide only part of fields to search but with the code I provided the form generated with ModelForm prevents that (for example because a field in a model cannot be blank).
My questions are:
Is it possible to generate a form based on a model to omit this behaviour?
Or is there any simpler way to create SearchView class?
I don't want to manually write forms if it's possible.

One way to accomplish this is to set blank=True on the field in MyModel, as indicated in the docs:
If the model field has blank=True, then required is set to False on the form field. Otherwise, required=True.
But for this to be a generic solution, you can't count on being able to modify the model fields. You can instead set the fields' required attribute to False immediately after the instance is created:
class SearchForm(ModelForm):
class Meta:
model = self.model
fields = self.fields
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for (field_name, field) in self.fields.items():
field.required = False

Since you're using the ModelForm for searching, you should set all the fields as required=False, by overriding the __init__ method:
def get_form(self, form_class=None):
# what I'v already tried:
class SearchForm(ModelForm):
class Meta:
model = self.model
fields = self.fields
def __init__(self, *args, **kwargs):
super(SearchForm, self).__init__(*args, **kwargs)
for field in self.fields:
self.fields[field].required = False
return SearchForm()
Though I suggest you should user django-filter, which makes it easier and cleaner to filter your searches. First you need to install it:
pip install django-filter
Then add it to your INSTALLED_APPS. After that you can create a filters.py file in your app:
# myapp/filters.py
import django_filters as filters
from .models import MyModel
MyModelFilterSet(filters.FilterSet):
class Meta:
model = MyModel
fields = ['name', 'email', 'whatever']
By default it's going to filter with the __exact lookup. You can change this in a couple of ways, just take a look here and here. To know which lookups you can use, take a look here.
After creating your filters.py file you can add it to a View, like a ListView:
# myapp/views.py
from django.views.generic import ListView
from .filters import MyModelFilterSet
from .models import MyModel
class MyModelSearchView(ListView):
template_name = 'path/to/template.html'
model = MyModel
def get_queryset(self):
qs = self.model.objects.all()
filtered_model_list = MyModelFilterSet(self.request.GET, queryset=qs)
return filtered_model_list.qs
There's a lot more you can do with django-filter. Here's the full documentation.

Related

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)

Add `help_text` to Django Admin field

I'm attempting to display an image when editing a user on the admin panel, but I can't figure out how to add help text.
I'm using this Django Admin Show Image from Imagefield code, which works fine.
However the short_description attribute only names the image, and help_text doesn't seem to add an text below it.
How can I add help_text to this field like normal model fields?
EDIT:
I would like to have help_text on the image like the password field does in this screenshot:
Use a custom form if you don't want change a model:
from django import forms
class MyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
self.fields['image'].help_text = 'My help text'
class Meta:
model = MyModel
exclude = ()
#admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
form = MyForm
# ...
It took me a while to figure out. If you've defined a custom field in your admin.py only and the field is not in your model. Use a custom ModelForm:
class SomeModelForm(forms.ModelForm):
# You don't need to define a custom form field or setup __init__()
class Meta:
model = SomeModel
help_texts = {'avatar': "User's avatar Image"}
exclude = ()
And in the admin.py:
class SomeModelAdmin(admin.ModelAdmin):
form = SomeModelForm
# ...
If you don't want to create a custom model form class :
class MyModelAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, change=False, **kwargs):
form = super().get_form(request, obj=obj, change=change, **kwargs)
form.base_fields["image"].help_text = "Some help text..."
return form

modelformset_factory: display more fields for specific instances

I'm using a modelformset_factory to edit multiple instances of Product in the same form:
ProductFormSet = modelformset_factory(Product, fields=('code', 'state'))
form_products = ProductFormSet()
It works well.
But now I need to display an additional field of the Product model in the form but only for a specific instance of Product. I'm not sure if it can be done in a simple manner in Django. Is it possible to do so using a modelformset_factory?
You can specify the form in the modelformset_factory, so create a model form (in forms.py if you have one) override the __init__method to add extra fields.
I would move the fields from the formsetfactory arguments to the form
in forms.py (assuming you have one)
class ProductForm(forms.ModelForm):
model = Product
def __init__(self, *args, **kwargs):
super(ProductForm, self).__init__(*args, **kwargs)
if 'instance' in kwargs :
product = kwargs['instance']
# to add an extra field, add something like this
self.fields['extra_field'] = forms.CharField(max_length=30)
class Meta:
fields = ('code', 'state')
Then pass that to your modelformset factory with the form argument
ProductFormSet = modelformset_factory(Product, form=ProductForm )
form_products = ProductFormSet()

Django model form - Exclude a field that has no model field

I have a simple model form what I use through the admin interface. Some of my model fields store datas that require a bit more time to calculate (they come from other sites). So I decided to put an extra boolean field to the form to decide to crawl these datas again or not.
class MyModelForm(forms.ModelForm):
update_values = forms.BooleanField(required=False) #this field has no model field
class Meta:
model = MyModel
This extra field doesn't exist in the model because only the form needs it.
The problem is that I only want it to appear if it's an existing record in the database.
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
if self.instance.pk is None:
#remove that field somehow
I tried nearly everything. Exclude it, delete the variable but nothing wants to work. I also tried dynamically add the field if self.instance.pk is exists but that didn't work too.
Any idea how to do the trick?
Thanks for your answers.
You could subclass the form and add the extra field in the subclass:
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
class MyUpdateModelForm(MyModelForm):
update_values = forms.BooleanField(required=False) #this field has no model field
class Meta:
model = MyModel
You can then override the get_form method of your admin, which is passed the current instance: get_form(self, request, obj=None, **kwargs)
Rather than removing the field in __init__ if instance.pk is not None, how about adding it if it is None? Remove the class-level declaration and just change the logic:
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
if self.instance and self.instance.pk is not None:
self.fields['update_values'] = forms.BooleanField(required=False)

CBV Django Form View set data for ChoiceField

I'm using the Django Form View and I want to enter custom choices per user to my Choicefield.
How can I do this?
Can I use maybe the get_initial function?
Can I overwrite the field?
When I want to change certain things about a form such as the label text, adding required fields or filtering a list of choices etc. I follow a pattern where I use a ModelForm and add a few utility methods to it which contain my overriding code (this helps keep __init__ tidy). These methods are then called from __init__ to override the defaults.
class ProfileForm(forms.ModelForm):
class Meta:
model = Profile
fields = ('country', 'contact_phone', )
def __init__(self, *args, **kwargs):
super(ProfileForm, self).__init__(*args, **kwargs)
self.set_querysets()
self.set_labels()
self.set_required_values()
self.set_initial_values()
def set_querysets(self):
"""Filter ChoiceFields here."""
# only show active countries in the ‘country’ choices list
self.fields["country"].queryset = Country.objects.filter(active=True)
def set_labels(self):
"""Override field labels here."""
pass
def set_required_values(self):
"""Make specific fields mandatory here."""
pass
def set_initial_values(self):
"""Set initial field values here."""
pass
If the ChoiceField is the only thing you're going to be customising, this is all you need:
class ProfileForm(forms.ModelForm):
class Meta:
model = Profile
fields = ('country', 'contact_phone', )
def __init__(self, *args, **kwargs):
super(ProfileForm, self).__init__(*args, **kwargs)
# only show active countries in the ‘country’ choices list
self.fields["country"].queryset = Country.objects.filter(active=True)
You can then make your FormView use this form with like this:
class ProfileFormView(FormView):
template_name = "profile.html"
form_class = ProfileForm