Django: get_object_or_404 for ListView - django

I have the following ListView. I know about get_object_or_404. But is there a way to show a 404 page if the object doesn't exist?
class OrderListView(ListView):
template_name = 'orders/order_list.html'
def get_queryset(self):
return OrderItem.objects.filter(
order__order_reference=self.kwargs['order_reference'],
)

You can raise a 404 error for a ListView, by changing the allow_empty [django-doc] attribute to False:
class OrderListView(ListView):
template_name = 'orders/order_list.html'
allow_empty = False
def get_queryset(self):
return OrderItem.objects.filter(
order__order_reference=self.kwargs['order_reference'],
)
If we inspect the soure code of the BaseListView (a class that is one of the ancestors of the ListView class), then we see:
class BaseListView(MultipleObjectMixin, View):
"""A base view for displaying a list of objects."""
def get(self, request, *args, **kwargs):
self.object_list = self.get_queryset()
allow_empty = self.get_allow_empty()
if not allow_empty:
# When pagination is enabled and object_list is a queryset,
# it's better to do a cheap query than to load the unpaginated
# queryset in memory.
if self.get_paginate_by(self.object_list) is not None and hasattr(self.object_list, 'exists'):
is_empty = not self.object_list.exists()
else:
is_empty = not self.object_list
if is_empty:
raise Http404(_("Empty list and '%(class_name)s.allow_empty' is False.") % {
'class_name': self.__class__.__name__,
})
context = self.get_context_data()
return self.render_to_response(context)
So it also takes pagination, etc. into account, and shifts the responsibility at the get(..) function level.

You can use get_list_or_404:
from django.shortcuts import get_list_or_404
def get_queryset(self):
my_objects = get_list_or_404(OrderItem, order__order_reference=self.kwargs['order_reference'])

Related

Redirect if query has no result

I've made a page with an input which connects to this view:
class SearchResultView(ListView):
model = RecipeSet
template_name = 'core/set_result.html'
context_object_name = 'recipe_set'
def get_queryset(self):
query = self.request.GET.get('q')
object_list = RecipeSet.objects.filter(
Q(set_name__exact=query)
)
if object_list.exists():
return object_list
else:
return redirect('core:dashboard')
I've used set_name__exact for this query and want to redirect users if the search returned no objects, how do I go about this? I've tried to use an if/else statement to check the objects but that doesn't seem to work.
The .get_queryset(…) [Django-doc] method should return a QuerySet, not a list, tuple, HttpResponse, etc.
You can however alter the behavior, by setting the allow_empty attribute to allow_empty = False, and override the dispatch method such that in case of a Http404, you redirect:
from django.http import Http404
from django.shortcuts import redirect
class SearchResultView(ListView):
allow_empty = False
model = RecipeSet
template_name = 'core/set_result.html'
context_object_name = 'recipe_set'
def get_queryset(self):
return RecipeSet.objects.filter(
set_name=self.request.GET.get('q')
)
def dispatch(self, *args, **kwargs):
try:
return super().dispatch(*args, **kwargs)
except Http404:
return redirect('core:dashboard')
Personally I would just change the .exists to .count:
class SearchResultView(ListView):
model = RecipeSet
template_name = 'core/set_result.html'
context_object_name = 'recipe_set'
def get_queryset(self):
query = self.request.GET.get('q')
object_list = RecipeSet.objects.filter(
Q(set_name__exact=query)
)
if object_list.count():
return object_list
else:
return redirect('core:dashboard')

Django: access the parent instance from the Inline model admin

How can I access the parent instance from the inline model admin?
My goal is to override the has_add_permission function based on the status of the parent instance. I don't want to allow to add a child if the status of the parent is different than 1.
class ChildInline(admin.TabularInline):
model = Child
form = ChildForm
fields = (
...
)
extra = 0
def has_add_permission(self, request):
# Return True only if the parent has status == 1
# How to get to the parent instance?
#return True
class ParentAdmin(admin.ModelAdmin):
inlines = [ChildInline,]
Django < 2.0 Answer:
Use Django's Request object (which you have access to) to retrieve the request.path_info, then retrieve the PK from the args in the resolve match. Example:
from django.contrib import admin
from django.core.urlresolvers import resolve
from app.models import YourParentModel, YourInlineModel
class YourInlineModelInline(admin.StackedInline):
model = YourInlineModel
def get_parent_object_from_request(self, request):
"""
Returns the parent object from the request or None.
Note that this only works for Inlines, because the `parent_model`
is not available in the regular admin.ModelAdmin as an attribute.
"""
resolved = resolve(request.path_info)
if resolved.args:
return self.parent_model.objects.get(pk=resolved.args[0])
return None
def has_add_permission(self, request):
parent = self.get_parent_object_from_request(request)
# Validate that the parent status is active (1)
if parent:
return parent.status == 1
# No parent - return original has_add_permission() check
return super(YourInlineModelInline, self).has_add_permission(request)
#admin.register(YourParentModel)
class YourParentModelAdmin(admin.ModelAdmin):
inlines = [YourInlineModelInline]
Django >= 2.0 Answer:
Credit to Mark Chackerian for the below update:
Use Django's Request object (which you have access to) to retrieve the request.path_info, then retrieve the PK from the args in the resolve match. Example:
from django.contrib import admin
from django.urls import resolve
from app.models import YourParentModel, YourInlineModel
class YourInlineModelInline(admin.StackedInline):
model = YourInlineModel
def get_parent_object_from_request(self, request):
"""
Returns the parent object from the request or None.
Note that this only works for Inlines, because the `parent_model`
is not available in the regular admin.ModelAdmin as an attribute.
"""
resolved = resolve(request.path_info)
if resolved.args:
return self.parent_model.objects.get(pk=resolved.args[0])
return None
def has_add_permission(self, request):
parent = self.get_parent_object_from_request(request)
# Validate that the parent status is active (1)
if parent:
return parent.status == 1
# No parent - return original has_add_permission() check
return super(YourInlineModelInline, self).has_add_permission(request)
#admin.register(YourParentModel)
class YourParentModelAdmin(admin.ModelAdmin):
inlines = [YourInlineModelInline]
I think this is a cleaner way to get the parent instance in the inline model.
class ChildInline(admin.TabularInline):
model = Child
form = ChildForm
fields = (...)
extra = 0
def get_formset(self, request, obj=None, **kwargs):
self.parent_obj = obj
return super(ChildInline, self).get_formset(request, obj, **kwargs)
def has_add_permission(self, request):
# Return True only if the parent has status == 1
return self.parent_obj.status == 1
class ParentAdmin(admin.ModelAdmin):
inlines = [ChildInline, ]
I tried the solution of Michael B but didn't work for me, I had to use this instead (a small modification that uses kwargs):
def get_parent_object_from_request(self, request):
"""
Returns the parent object from the request or None.
Note that this only works for Inlines, because the `parent_model`
is not available in the regular admin.ModelAdmin as an attribute.
"""
resolved = resolve(request.path_info)
if resolved.kwargs:
return self.parent_model.objects.get(pk=resolved.kwargs["object_id"])
return None
BaseInlineFormSet has the attribute self.instance which is the reference to the parent object.
In the constructor, the queryset is intialized and filtered using this instance. If you need adjustments, you can change the queryset argument to the constructor or use inlineformset_factory to set the formset up according to your needs.
class BaseInlineFormSet(BaseModelFormSet):
"""A formset for child objects related to a parent."""
def __init__(self, data=None, files=None, instance=None,
save_as_new=False, prefix=None, queryset=None, **kwargs):
if instance is None:
self.instance = self.fk.remote_field.model()
else:
self.instance = instance
self.save_as_new = save_as_new
if queryset is None:
queryset = self.model._default_manager
if self.instance.pk is not None:
qs = queryset.filter(**{self.fk.name: self.instance})
else:
qs = queryset.none()
self.unique_fields = {self.fk.name}
super().__init__(data, files, prefix=prefix, queryset=qs, **kwargs)
see https://docs.djangoproject.com/en/3.2/_modules/django/forms/models/
If you extends from this, make sure to run super().__init__() before accessing self.instance.
Place the following on the parent admin model so that the parent model instance is available to any inline under the parent model:
def get_form(self, request, obj=None, **kwargs):
request._obj = obj
return super().get_form(request, obj, **kwargs)
Then, in the inline (using a customers field as an example):
def formfield_for_manytomany(self, db_field, request, **kwargs):
if db_field.name == "customers":
if request._obj is not None:
kwargs["queryset"] = request._obj.customers
else:
kwargs["queryset"] = Customer.objects.none()
return super().formfield_for_manytomany(db_field, request, **kwargs)
You only need to add obj parameter and check the parent model status
class ChildInline(admin.TabularInline):
model = Child
form = ChildForm
fields = (
...
)
extra = 0
#You only need to add obj parameter
#obj is parent object now you can easily check parent object status
def has_add_permission(self, request, obj=None):
if obj.status == 1:
return True
else:
return False
class ParentAdmin(admin.ModelAdmin):
inlines = [ChildInline,]
You can also retrieve it simply from the request path using re module if you do not expect numbers in your path.
for example:
import re
def get_queryset(self, request):
instance_id = re.sub(r"\D+", "", request.path)
or
def get_parent_object_from_request(self, request):
instance_id = re.sub(r"\D+", "", request.path)

Django DetailView + show related record of another Model

I would like to return to the User related record.
Somebody can help me?
part of my view
class UserProfileDetailView(DetailView):
model = get_user_model()
slug_field = "username"
template_name = "perfil.html"
def get_object(self, queryset=None):
user = super(UserProfileDetailView, self).get_object(queryset)
UserProfile.objects.get_or_create(user=user)
return user
Something like a old way>
def my_view(request, slug):
var = get_object_or_404(Model, slug=slug)
xxx = AnotherModel.objects.filter(var=var)
...
how can i perfome this in the first view UserProfileDetailView,
show related data?
What I do in this case is add the related object into the context data. It would be something like this:
class UserProfileDetailView(DetailView):
model = get_user_model()
slug_field = "username"
template_name = "perfil.html"
def get_context_data(self, **kwargs):
# xxx will be available in the template as the related objects
context = super(UserProfileDetailView, self).get_context_data(**kwargs)
context['xxx'] = AnotherModel.objects.filter(var=self.get_object())
return context
Another approach is to extend DetailView with MultipleObjectMixin, as in this example:
from django.views.generic import DetailView
from django.views.generic.list import MultipleObjectMixin
from django.contrib.auth import get_user_model
class DetailListView(MultipleObjectMixin, DetailView):
related_model_name = None
def get_queryset(self):
# a bit of safety checks
if not hasattr(self, "related_model_name"):
raise AttributeError(
"%s.related_model_name is missing." % (
self.__class__.__name,))
if not self.related_object_name:
raise NotImplementedError(
"%s.related_model_name must not be None." % (
self.__class__.__name,))
# get the object
obj = self.get_object()
# get the related model attached to the object
related_model = getattr(obj, "%s_set" % self.related_model_name, None)
# safety check if related model doesn't exist
if not related_model:
raise AttributeError(
"%s instance has no attribute \"%s_set\"" % (
obj.__class__.__name__, self.related_model_name)
# return the related model queryset
return related_model.all()
def get(self, request, *args, **kwargs):
self.object_list = self.get_queryset()
return super(DetailListView, self).get(request, *args, **kwargs)
class UserProfileDetailView(DetailListView):
template_name = "perfil.html"
model = get_user_model()
slug_field = "username"
related_model_name = "anothermodel"
def get_object(self, queryset=None):
user = super(UserProfileDetailView, self).get_object(queryset)
UserProfile.objects.get_or_create(user=user)
return user
In my opinion this approach is a little less cleaner and understandable, but it has a huge advantage in reusability. It definitely has one downside: if you are using the class variable context_object_name, it will refer to the related objects list and not to the object itself (this has to do with how the inheritance chain is set up when constructing the class).

Pass url argument to ListView queryset

models.py
class Lab(Model):
acronym = CharField(max_length=10)
class Message(Model):
lab = ForeignKey(Lab)
urls.py
urlpatterns = patterns('',
url(r'^(?P<lab>\w+)/$', ListView.as_view(
queryset=Message.objects.filter(lab__acronym='')
)),
)
I want to pass the lab keyword argument to the ListView queryset. That means if lab equals to TEST, the resulting queryset will be Message.objects.filter(lab__acronym='TEST').
How can I do that?
You need to write your own view for that and then just override the get_queryset method:
class CustomListView(ListView):
def get_queryset(self):
return Message.objects.filter(lab__acronym=self.kwargs['lab'])
and use CustomListView in urls also.
class CustomListView(ListView):
model = Message
def get(self, request, *args, **kwargs):
# either
self.object_list = self.get_queryset()
self.object_list = self.object_list.filter(lab__acronym=kwargs['lab'])
# or
queryset = Lab.objects.filter(acronym=kwargs['lab'])
if queryset.exists():
self.object_list = self.object_list.filter(lab__acronym=kwargs['lab'])
else:
raise Http404("No lab found for this acronym")
# in both cases
context = self.get_context_data()
return self.render_to_response(context)

How do I use an UpdateView to update a Django Model?

I'm trying to update a model in Django using the class-based generic view UpdateView.
I read the page Updating User model in Django with class based UpdateView to try and get me started, but I'm getting an error 'WSGIRequest' object has no attribute 'id'
I'm a fresh face to Django, so please be forgiving if I'm doing something stupid.
//urls.py
url(r'^portfolios/update/(?P<id>\d+)/$',PortfoliosUpdateView.as_view()),
//views.py
class PortfoliosUpdateView(UpdateView):
form_class = PortfoliosCreateForm
model = Portfolios
template_name = 'portfolios/create.html'
def get(self, request, **kwargs):
self.object = Portfolios.objects.get(id=self.request.id)
form_class = self.get_form_class()
form = self.get_form(form_class)
context = self.get_context_data(object=self.object, form=form)
return self.render_to_response(context)
def get_object(self, queryset=None):
obj = Portfolios.objects.get(id=self.request.id)
return obj
It's mostly just a modified version of the code originally posted, but I thought it'd work. I know that I'm trying to retrieve the id passed as a GET parameter, but that doesn't seem to come through in the request variable. Am I going about this the wrong way?
Thanks
Edit: I think I fixed it, but this may be wrong:
I changed the lines
self.object = Portfolios.objects.get(id=self.request.id)
obj = Portfolios.objects.get(id=self.request.id)
to
self.object = Portfolios.objects.get(id=self.kwargs['id'])
obj = Portfolios.objects.get(id=self.kwargs['id'])
I could be wrong.
It should be:
def get_object(self, queryset=None):
obj = Portfolios.objects.get(id=self.kwargs['id'])
return obj
Look at class based generic view dispatch explains that keyword arguments are assigned to self.kwargs.:
def dispatch(self, request, *args, **kwargs):
# Try to dispatch to the right method; if a method doesn't exist,
# defer to the error handler. Also defer to the error handler if the
# request method isn't on the approved list.
if request.method.lower() in self.http_method_names:
handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed
self.request = request
self.args = args
self.kwargs = kwargs
return handler(request, *args, **kwargs)
id = self.request.GET.get('id',None) is what you needed when trying to access the GET query string.
However, your view can be simplified:
from django.conf.urls import *
from django.views.generic import UpdateView
from yourapp.models import Portfolios
from yourapp.forms import PortfoliosCreateForm
urlpatterns = patterns('',
url('^portfolios/update/(?P<pk>[\w-]+)$', UpdateView.as_view(
model=Portfolios,
form_class=PortfoliosCreateForm,
template_name='portfolios/create.html',
success_url='/portfolios'
), name='portfolio_update'),
)
views.py
class MyUpdateView(UpdateView):
model = ModelName # required
template_name = 'x/h1.html'
form_class = ModelNameForm
success_url = reverse_lazy('app:page1')
def get_queryset(self):
"""
Optional condition to restrict what users can see
"""
queryset = super().get_queryset()
return queryset.filter(id__lt=20)
def get_success_url(self):
return reverse_lazy(
'app1:abc',
kwargs={'pk': self.object.id}
)
urls.py
In urlpatterns=[]
path('xyz/<pk>/', MyUpdateView.as_view(),name='xyz')
my_model_view.html
{{form}}
You will be able to edit ModelName at url /xyz/<pk>/ where <pk> can be anything from 1 to 20 based on our condition in get_queryset(). Take that condition out to allow users to edit any object.
self.object is only available after post request to the UpdateView.