How to search on a Many to Many field in Django? - python

I have a Profile model with a ManyToManyField on another model Specialty.
I want to have a simple search on the Profile model against specialties and return matching profiles. As it stands, my form displays in my template correctly, but I can't get anything after the submission.
models.py:
from django.db import models
from django.conf import settings
class Specialty(models.Model):
title = models.CharField(max_length=255)
class Meta:
verbose_name_plural = 'Specialties'
def __unicode__(self):
return u"%s" % self.title
class Profile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL)
specialties = models.ManyToManyField(Specialty, blank=True)
def __unicode__(self):
return u"%s" % (self.user.username)
def get_absolute_url(self):
return reverse("profile_detail", args=[str(self.user.username)])
forms.py:
from django import forms
from .profiles.models import Profile, Specialty
class ProfileSearchForm(forms.ModelForm):
specialty = forms.ModelMultipleChoiceField(queryset=Specialty.objects.all(), widget=forms.CheckboxSelectMultiple, required=False)
class Meta:
model = Profile
fields = ('specialty',)
views.py:
from django.views.generic.edit import FormView
from django.core.urlresolvers import reverse_lazy
from .forms import ProfileSearchForm
from .profiles.models import Profile
class IndexView(FormView):
template_name = 'index.html'
form_class = ProfileSearchForm
success_url = reverse_lazy('index')
def form_valid(self, form):
specialty = form.cleaned_data['specialty']
self.profile_list = Profile.objects.filter(specialty__in=specialty)
return super(IndexView, self).form_valid(form)
index.html:
<form action="{% url 'index' %}" method="get">
{{ form.as_p }}
<p><input type="submit" value="Search"></p>
</form>
<ul>
{% for profile in profile_list %}
<li>{{ profile.user.get_full_name }}</li>
{% endfor %}
</ul>
I have a feeling it has to do with self.profile_list. I don't know if/how it should go into a get_extra_context. It can't exist on the first visit, so I don't know how to make it exist or pass it around. I'm also not sure if the Profile.objects.filter(specialty__in=specialty) is quite the right way to field lookup on a many-to-many field.
I'm also open to other search suggestions like Haystack if they have advantages. I prefer a group of checkboxes, which I don't think Haystack can handle via faceting.

Thanks, Gergo and Cameron. I got it fixed now. You were right about that one problem, but there were quite a few steps left to go.
What I really wanted was a ListView plus the ability to do a simple search, which should be a FormMixin that lets me add form_class and success_url, instead of it all as a FormView.
When a default model is specified in a ListView, the view blows away the context, so form never reaches the template. get_context_data needs to add the form back to the context, of which the docs have an example.
form_valid should be removed because a search is never a POST request, despite what the docs say under the "Note" in FormMixin requiring form_valid and form_invalid.
I need get_queryset to either get a default queryset via model or read the GET request's specialties value and filter the results appropriately.
For bonus points, get_form_kwargs needs to pass the current request to the form so initial form values can remain after a page refresh. The tricky part is that when using ModelMultipleChoiceField, you have to use request.GET's getlist and not get method to read that list of values.
All together now...
forms.py:
from django import forms
from .profiles.models import Profile, Specialty
class ProfileSearchForm(forms.ModelForm):
specialties = forms.ModelMultipleChoiceField(queryset=Specialty.objects.all(), widget=forms.CheckboxSelectMultiple, required=False)
class Meta:
model = Profile
fields = ('specialties',)
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request')
super(ProfileSearchForm, self).__init__(*args, **kwargs)
self.fields['specialties'].initial = self.request.GET.getlist('specialties')
views.py:
from django.views.generic import ListView
from django.views.generic.edit import FormMixin
from django.core.urlresolvers import reverse_lazy
from .profiles.models import Profile
from .forms import ProfileSearchForm
class IndexView(FormMixin, ListView):
model = Profile
template_name = 'index.html'
form_class = ProfileSearchForm
success_url = reverse_lazy('index')
def get_queryset(self):
queryset = super(IndexView, self).get_queryset()
specialties = self.request.GET.getlist('specialties')
if specialties:
queryset = queryset.filter(specialties__in=specialties).distinct('user')
return queryset
def get_form_kwargs(self):
kwargs = super(IndexView, self).get_form_kwargs()
kwargs['request'] = self.request
return kwargs
def get_context_data(self, **kwargs):
context = super(IndexView, self).get_context_data(**kwargs)
form_class = self.get_form_class()
context['form'] = self.get_form(form_class)
return context

I think you're looking for Profile.objects.filter(specialties__in=specialty) - profile doesn't have a specialty field, it has a specialties field.

Related

Django class-based form with dropdowns populated with data from model / db

This my Django code:
forms.py
from django import forms
class MyForm(forms.Form):
name = forms.CharField()
location = forms.CharField()
views.py
class MyFormView(FormView):
template_name = 'form.html'
form_class = MyForm
success_url = 'home'
def get(self, request, *args, **kwargs):
form = self.form_class
return render(request, 'form.html', {'form': form})
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST)
if form.is_valid():
form.save()
return redirect('home')
else:
return render(request, self.template_name, {'form': form})
template form.html
<form method="post" action="{% url 'form' %}">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" class="btn btn-primary btn-block py-2" value="OK">
</form>
models.py
class MyModel(models.Model):
# ...
name = models.OneToOneField(Person, on_delete=models.CASCADE))
location = models.ForeignKey(Location, on_delete=models.CASCADE)
date = models.DateTimeField(auto_now_add=True)
# ...
I want both two fields (name, location) to be drop-downs (combobox-es) not CharFields, and I want to populate their entries / values with data coming from db (django models). How can I do that? I am completely new to CBV idea.
You should use ModelChoiceField instead of CharField so:
from django import forms
from .models import YourModelName
class ReleaseForm(forms.Form):
name = forms.ModelChoiceField(queryset=YourModelName.objects.all())
location = forms.ModelChoiceField(queryset=YourModelName.objects.all())
Form class doesn't have a save() method unlike modelforms so you should manually save the form using cleaned_data in form_valid() method as:
from django.shortcuts import render, redirect
from django.urls import reverse_lazy
from django.views.generic import FormView
from .forms import MyForm
from .models import MyModel
class MyFormView(FormView):
template_name = 'form.html'
form_class = MyForm
success_url = reverse_lazy('home')
def form_valid(self, form):
name = form.cleaned_data['name']
location = form.cleaned_data['location']
MyModel.objects.create(name=name, location=location)
return super().form_valid(form)

Django not rendering form fields

I looked at similar questions but they do not seem to apply. I have a very simple django form which does not show on the website, I only see the Submit button. Here are the relevant files:
models.py
from django.db import models
from django.urls import reverse
import uuid
# Create your models here.
class Job(models.Model):
id = models.UUIDField(
primary_key=True,
default=uuid.uuid4,
editable=False)
job_name = models.CharField(max_length=200)
#One to many relationship requires on_delete
email = models.EmailField()
def __str__(self):
return self.job_name
forms.py
from django import forms
class JobForm(forms.Form):
job_name = forms.CharField(max_length=200)
email = forms.EmailField()
views.py
from django.shortcuts import render
from django.views.generic import TemplateView
from .forms import JobForm
from .models import Job
class HomePageView(TemplateView):
template_name = 'index.html'
class SubmitPageView(TemplateView):
template_name = 'submit.html'
def submit_job(request):
# Retrieve post by id
if request.method == 'POST':
# Form was submitted
form = JobForm(request.POST)
if form.is_valid():
#Form fields passed validation
#If the form is valid, we retrieve the validated data accessing
#form.cleaned_data. This attribute is a dictionary of form fields and their values.
cd = form.cleaned_data
my_model = Job()
my_model.job_name = cd.get('job_name')
my_model.email = cd.get('email')
# Save the job to the database
my_model.save()
else:
form = JobForm()
return render(request, SubmitPageView(), {'form': form})
And in my template I have
<form method="POST" action=".">
<table>
{% csrf_token %}
{{ form.as_table }}
</table>
which gets rendered as:
<form method="POST" action=".">
<table>
<input type="hidden" name="csrfmiddlewaretoken" value="I7yL9XAUhEPiriKVHKtqh9UfhsLWoJrBo68uguqMecX8gmuNoJV7gykvsPc7FtQ2">
</table>
OK, I found the solution by following https://docs.djangoproject.com/en/3.0/topics/class-based-views/intro/
Basically, as I was using class-based views, the functions to get and post the form need to be subsumed into the class-based view for that page. Here is the current version
of views.py:
from django.shortcuts import render
from django.views.generic import TemplateView
from .forms import JobForm
from .models import Job
class HomePageView(TemplateView):
template_name = 'index.html'
class SubmitPageView(TemplateView):
form_class = JobForm
template_name = 'submit.html'
def get(self, request, *args, **kwargs):
form = self.form_class()
return render(request, self.template_name, {'form': form})
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST)
if form.is_valid():
#Form fields passed validation
#If the form is valid, we retrieve the validated data accessing
#form.cleaned_data. This attribute is a dictionary of form fields and their values.
cd = form.cleaned_data
my_model = Job()
my_model.job_name = cd.get('job_name')
my_model.email = cd.get('email')
# Save the job to the database
my_model.save()
else:
form = JobForm()
return render(request, self.template_name, {'form': form})
Try code below:
# if a GET (or any other method) we'll create a blank form
else:
form = JobForm()
return render(request, 'submit.html', {'form': form})
<form action="/your-name/" method="post">
{% csrf_token %}
{{ form.as_table }}
<input type="submit" value="Submit">
</form>
Does it make a difference if you define the form as a modelForm and explicitly state the model and fields?
Add/modify the following to your Forms.py:
class JobForm(forms.ModelForm):
class Meta:
model = Job
fields = ('job_name', 'email')
job_name = forms....

List of current user objects in Django ListView

I want to render list of all objects on my template, for which their author is the currently logged in user. I passed the username of current user to url.py:
My List
My urls.py:
path('myscenarios/<str:username>/', MyScenarioListView.as_view(), name='myscenarios'),
My question is how to build the queryset in views.py and what to type in template block in my html?
class MyScenarioListView(LoginRequiredMixin, ListView):
model = Scenario
template_name = 'testmanager/myscenarios.html'
context_object_name = 'myscenarios'
def get_queryset(self):
user = get_object_or_404(User, username=self.kwargs.get('username'))
return Scenario.objects.filter(scenarioAuthor = user).order_by('-date_posted')
What code should I type in my myscenarios.html file?
I want to render list of all objects on my template, which their author is current logged user.
Then you should not encode the user in the path, since a "hacker" can then simply change the URL to see the items belonging to a different user.
You can make use of self.request.user here. The path thus looks like:
path('myscenarios/', MyScenarioListView.as_view(), name='myscenarios'),
and in the view, we use:
class MyScenarioListView(LoginRequiredMixin, ListView):
model = Scenario
template_name = 'testmanager/myscenarios.html'
context_object_name = 'myscenarios'
def get_queryset(self):
return Scenario.objects.filter(
scenarioAuthor=self.request.user
).order_by('-date_posted')
It will pass the Scenarios as myscenarios to the template, so you can render this with:
{% for scenario in myscenarios %}
{{ scenario }}
{% endfor %}

Django model CreateView does not render form fields in HTML

I am trying to setup a class based 'CreateView' for a model in my django site, but when the create html page renders, the model fields are not rendered. Only the submit button shows up on the web page. However, when debugging, I overrided the 'form_invalid' method in the class view, and the form object had the required HTML for all fields stored in the object. If I take this HTML and manually add it to the HTML of the create page in the browser I can fill out the fields and post the data to the database.
At this point I have not found an obvious answer as to why the form fields are not rendered so any help on this would be greatly appreciated.
environment used: python 3.7.3, django 2.2.3
Solution:
This issue was fixed by changing the form name in the view context data.
In views.py:
def get_context_data(self, *args, **kwargs):
context = super(CreateAlertView, self).get_context_data(**kwargs)
context["alert_form"]=context["form"]
return context
Or...
In the HTML template change 'alert_form' to 'form' to match the default context.
models.py:
class Alert(models.Model):
RAIN = 'Rain'
SNOW = 'Snow'
COLD = 'Cold'
HEAT = 'Heat'
WEATHER_CHOICES = [
(RAIN, 'Rain'),
(SNOW, 'Snow'),
(COLD, 'Cold'),
(HEAT, 'Heat'),
]
DAILY = 'Daily'
WEEKLY = 'Weekly'
INTERVAL_CHOICES = [
(DAILY, 'Daily'),
(WEEKLY, 'Weekly'),
]
weather_type = models.CharField(max_length=15, choices=WEATHER_CHOICES, default=RAIN)
interval = models.CharField(max_length=10, choices=INTERVAL_CHOICES, default=DAILY)
search_length = models.IntegerField(default=1)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
active = models.BooleanField(default=False)
views.py:
class CreateAlertView(LoginRequiredMixin, CreateView):
template_name = 'users/alert_form.html'
#model = Alert
form_class = AlertModelForm
success_url = 'users/profile/'
def form_valid(self, form):
print('validation')
form.instance.user = self.request.user
return super().form_valid(form)
def form_invalid(self, form):
print(form) # check form HTML here
return super().form_invalid(form)
forms.py:
class AlertModelForm(ModelForm):
class Meta:
model = Alert
exclude = ['user']
urls.py:
urlpatterns = [
path('alert/create/', CreateAlertView.as_view(), name='alert'),
]
html template:
<h1>create an alert</h1>
<form method="post">
{% csrf_token %}
{{ alert_form.as_p }}
{{ alert_form.non_field_errors }}
{{ field.errors }}
<button type="submit">Save changes</button>
</form>
Create page as rendered:
Create page with manually modified HTML:
The context name for the form set by the CreateView (FormMixin) is "form", your template is referencing "alert_form"
Here is a helpful website for seeing all options available in the class based views

Why the form return 'Method not allowed' while extending CreateView CBV in DJANGO

I am not sure what is not working correctly here. The CBV CreateView include a form but when I try to click on 'submit' in the template I receive the 'error' Method Not Allowed (POST)
forms.py
class DateInput(forms.DateInput):
input_type = 'date'
class BookingForm(forms.ModelForm):
class Meta:
model = Booking
fields = ('check_in',
'check_out')
widgets = {
'check_in': DateInput(),
'check_out': DateInput()
}
class PropertyDetailView(DetailView):
model = PropertyListing
context_object_name = 'name'
template_name = 'core/property-detail.html'
def get_context_data(self, *args, **kwargs):
context = super(PropertyDetailView, self).get_context_data(**kwargs)
context['property_details'] = PropertyListing.objects.filter(pk=self.kwargs.get('pk'))
# Form
context['form'] = BookingForm()
return context
just the form
HTML
<form class="col-sm-3" role="form" action="" method="POST">
{% csrf_token %}
{{ form|crispy }}
<input class="btn btn-primary" type="submit" value="Create" />
</form>
Does anybody have an idea why?
Like the error says, a DetailView [Django-doc] does not implement a handler for POST requests. Therefore if you make a POST request to the handler, it will produce a HTTP 405 error: Method not allowed.
We thus will need to implement a handler for a POST request ourselves. The good news is that a lot of functionality is already implemented in the ModelFormMixin [Django-doc]. We thus can implement this like:
class PropertyDetailView(ModelFormMixin, DetailView):
model = PropertyListing
context_object_name = 'name'
template_name = 'core/property-detail.html'
form_class = BookingForm
success_url = ...
def get_context_data(self, *args, **kwargs):
context = super(PropertyDetailView, self).get_context_data(**kwargs)
context['property_details'] = PropertyListing.objects.filter(pk=self.kwargs.get('pk'))
return context
def post(self, *args, **kwargs):
self.object = None
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
You thus do not need to add a Form to the context data (the ModelFormMixin will do that), nor do you need to handle the form yourself. You will however need to specify a success_url [Django-doc], or override the form_valid method [Django-doc].
That being said, it might be better to use a CreateView [Django-doc] or an UpdateView [Django-doc], and just apply some code changes to add details of your objects to it.

Categories

Resources