Django how to write DRY views - django

I have many views that call the same functions every time and I wonder before I go ahead with this approach if anyway I can make it more DRY.
For example I have many pages on a site that have on the left side menu a list of the same articles and photos. So on each of my views I do the following:
context_dict = {'articles': get_articles(blogger.id), 'photos': get_photos(blogger.id)}
return render_to_response('...', context_dict, context)
It must exist a way that I don't have to repeat myself every time since they are required on 90% of the pages.

The issue of repeating view functionality is part of why many people like class-based views. You could implement a method that adds those variables to the base class, and then have other views inherit from that one, or provide a standardized "render" method. For example:
class BaseView(View):
template = 'public/base_template.html'
def get(self, *args, **options):
return render_to_response(self.template, self.render_view())
def render_view(self, *args, **options):
context = {"photos": Photo.objects.all()}
return context
class OtherView(BaseView):
template = 'public/other_template.html'
def render_view(self, *args, **options):
context = super(OtherView, self).render_view(*args, **options)
context['additional_context'] = True
return context
...or something similar. Then, you don't have to worry about calling render with variables that are already included.
I can think of a few ways to accomplish this with function-based views, but I think class-based lends itself very well to DRY principles, so I thought I'd spread the gospel :)
https://docs.djangoproject.com/en/1.9/topics/class-based-views/intro/

You mean something like
def get_extra_context(blog_id):
return {'articles': get_articles(blogger.id), 'photos': get_photos(blogger.id)}
A call to get_extra_context has to be made in every view of course.

As Robert Townley says, class based views are very helpful for keeping to DRY principles. I often use some simple mixins to share some logic between different views. Your class based views can then inherit from this mixin if they need this functionality. For example:
class BloggerMixin(object):
articles_context_name = 'articles'
photos_context_name = 'photos'
def get_blogger(self):
""" I'm just assumming your blogger is the current user for simplicity.
(And I'm assuming they're logged in already)"""
return self.request.user
def get_articles(self):
return Article.objects.filter(blogger=self.get_blogger())
def get_photos(self):
return Photo.objects.filter(blogger=self.get_blogger())
def get_context_data(self, **kwargs):
context = super(BloggerMixin, self).get_context_data(**kwargs)
context[self.articles_context_name] = self.get_articles()
context[self.photos_context_name] = self.get_photos()
return context
This would allow you to inherit this extra functionality on class based views that need it:
class ExampleView(BloggerMixin, ListView):
model = SomeOtherModel
Our very simple ExampleView class will now have a list of Article, Photo and SomeOtherModel in its context.

Related

Reuse queryset in multiple views

I am building a application which has several views - HomePageView, SearchPageView and DetailPageView. The aforementioned views return the same queryset. My question is, what is the proper way to define let's say a "global" queryset which would be then used in multiple Views.
To illustrate my point here is an example of what I have:
class HomePageView(TemplateView):
def get_queryset():
return Systemevents.objects.filter(**filter)
class SearchPageView(ListView):
def get_queryset():
return Systemevents.objects.filter(**filter)
class LogDetailView(DetailView):
def get_queryset():
return Systemevents.objects.filter(**filter)
What I would like to achieve:
global queryset = Systemevents.objects.filter(**filter)
class HomePageView(TemplateView):
def get_queryset():
return queryset
class SearchPageView(ListView):
def get_queryset():
return queryset
class LogDetailView(DetailView):
def get_queryset():
return queryset
Thanks in advance,
Jordan
you can use something called manager for that for reusing that query set you have to specify all your logic inside the django manager so you don't have to go for a long query set just small readable query set checkout more about in the django documentation
https://docs.djangoproject.com/en/3.1/topics/db/managers/

Trying to filter by user group using class based view with django-tables2, can't access self.user

I'm trying to use a class based view using django-tables2 to define the table and template returned based on what group the logged in user belongs to.
This is my attempt at doing so:
class cases(LoginRequiredMixin, SingleTableView):
login_url = '/account/login/'
if User.objects.filter(pk=self.request.user.id, groups__name='teachers').exists():
model = Graduation
table_class = TeachersTable
template_name = 'mysite/teachers.html'
elif User.objects.filter(pk=self.request.user.id, groups__name='students').exists():
model = Graduation
table_class = StudentsTable
template_name = 'mysite/students.html'
I think the approach is more or less correct (I've only learned about class based views today), but I am unsure how to access the user id.
The relevant parts of this view should only be called when a user is logged in (at least I think) because I'm using the LoginRequiredMixin, so 'self' should exist.
The answers I have seen addressing this problem say override get_queryset, but I am reluctant to do that as I think that will break django-tables2.
What is the best approach in this case to do what I am trying to do?
There are a few things going on here.
First, all code in a class runs when the module is loaded, not when the view is run. So your code is running at the wrong time. Partially because of this, you don't have access to self.
self isn't that magic, it doesn't appear from nowhere, you can only use it in class methods:
class Foo:
self.spam = "eggs" # Wrong, no self here.
class Bar:
def set_spam(self): # self is the instance of the class.
self.spam = "eggs" # Works.
I'm not sure what table_class is, from a search it looks like it comes from django-tables, so there's a bit of guesswork here. It seems like you want something like this:
class GraduationCaseView(LoginRequiredMixin, SingleTableView):
model = Graduation
def get_template_names(self):
if self.request.user.groups.filter(name='teachers').exists():
return ['mysite/teachers.html']
return 'mysite/students.html'
def get_table_class(self):
if self.request.user.groups.filter(name='teachers').exists():
return TeachersTable
return StudentsTable
This should work. There is an issue with this: you'd be doing the same database query twice. There are some ways around this, but it requires knowing a bit about CBVs and their execution order. Since I'm not sure what SingleTableView is doing, this may or may not work:
class GraduationCaseView(LoginRequiredMixin, SingleTableView):
model = Graduation
def get_queryset(self):
qs = super().get_queryset()
if self.request.user.groups.filter(name='teachers').exists():
self.group = 'teachers'
else:
self.group = 'students'
return qs
def get_template_names(self):
if self.group == 'teachers':
return ['mysite/teachers.html']
return 'mysite/students.html'
def get_table_class(self):
if self.group == 'teachers':
return TeachersTable
return StudentsTable
You should probably read up on Python's documentation of classes, too, so you understand how they work.
One more thing, you don't need to set login_url on your view if settings.LOGIN_URL is set.

Django class based views example

I can't understand the class based views, so I am trying to figure it out with an example. Here is what I have so far:
#urls.py
url(r'^(?P<langcode>[a-zA-Z-]+/about/$', about, name='about')
#views.py
def about(request, langcode):
languages = Language.objects.values_list('code', flat=True)
language = get_object_or_404(Language, pk=langcode)
return render(request, 'about.html', {
'languages': languages,
'language': language
})
I also have some other functional views which contain the first 2 lines of about:
languages = Language.objects.values_list('code', flat=True)
language = get_object_or_404(Language, pk=langcode)
So, what I want to do now is:
create a class BaseView (or however you want to call it) which extends
something from django.generic.views and which will determine the language and languages parameters for the context based on the langcode input parameter
Create the class AboutView(BaseView) (so extending the BaseView) which will somehow define the template name about.html to be used for rendering.
I will further have another class based view, also to extend BaseView and which is simillar to AboutView, but which sets one more context parameter called region depending as well on the langcode input parameter
Can someone show me exactly how to code this stuff? thank you
Here's a simple way to achieve what you want:
You first define the common logic, using the TemplateView generic view:
class MyBaseView(TemplateView):
def dispatch(self, request, *args, **kwargs):
# dispatch takes care of "reading" the parameters from the url
self.language = get_object_or_404(Language, pk=kwargs.pop("langcode")) # I would use some kind of default value to prevent exception, but its up to your logic
return TemplateView.dispatch(self, request, *args, **kwargs)
def get_context_data(self, **kwargs):
# get_context_data creates the context
context = TemplateView.get_context_data(self, **kwargs)
context.update({"language": self.language,
"languages": Language.objects.values_list('code', flat=True)})
return context
Then, you don't even need an AboutView because all you want is to control the template_name, so in your urls.py:
# in urls,py you can just use this, instead of defining an AboutView, you pass the template_name you want to use, this is sufficient because TemplateView expects a template_name attribute
url('^(?P<langcode>[a-zA-Z]+)/about/$', MyBaseView.as_view(template_name="about.html"), name='about')
and finally, for the other view that uses region you can just inherit from the MyBaseView, and add the context you want:
class AboutViewWithRegion(MyBaseView):
def get_context_data(self, **kwargs):
context = MyBaseView.get_context_data(self, **kwargs)
context.update({"region": <<your logic here>>})
return context
Hope that helps!

How to write a basic try/except in a Django Generic Class View

I'd like to write an except clause that redirects the user if there isn't something in a queryset. Any suggestions welcome. I'm a Python noob, which I get is the issue here.
Here is my current code:
def get_queryset(self):
try:
var = Model.objects.filter(user=self.request.user, done=False)
except:
pass
return var
I want to do something like this:
def get_queryset(self):
try:
var = Model.objects.filter(user=self.request.user, done=False)
except:
redirect('add_view')
return var
A try except block in the get_queryset method isn't really appropriate. Firstly, Model.objects.filter() won't raise an exception if the queryset is empty - it just returns an empty queryset. Secondly, the get_queryset method is meant to return a queryset, not an HttpResponse, so if you try to redirect inside that method, you'll run into problems.
I think you might find it easier to write a function based view. A first attempt might look like this:
from django.shortcuts import render
def my_view(request):
"""
Display all the objects belonging to the user
that are not done, or redirect if there are not any,
"""
objects = Model.objects.filter(user=self.request.user, done=False)
if not objects:
return HttpResponseRedirect("/empty-queryset-url/")
return render(request, 'myapp/template.html', {"objects": objects})
The advantage is that the flow of your function is pretty straight forward. This doesn't have as many features as the ListView generic class based view (it's missing pagination for example), but it is pretty clear to anyone reading your code what the view is doing.
If you really want to use the class based view, you have to dig into the CBV documentation for multiple object mixins and the source code, and find a suitable method to override.
In this case, you'll find that the ListView behaviour is quite different to what you want, because it never redirects. It displays an empty page by default, or a 404 page if you set allow_empty = False. I think you would have to override the get method to look something like this (untested).
class MyView(ListView):
def get_queryset(self):
return Model.objects.filter(user=self.request.user, done=False)
def get(self, request, *args, **kwargs):
self.object_list = self.get_queryset()
if len(self.object_list == 0):
return HttpResponseRedirect("/empty-queryset-url/")
context = self.get_context_data(object_list=self.object_list)
return self.render_to_response(context)
This is purely supplemental to #Alasdair's answer. It should really be a comment, but couldn't be formatted properly that way. Instead of actually redefining get on the ListView, you could override simply with:
class MyView(ListView):
allow_empty = False # Causes 404 to be raised if queryset is empty
def get(self, request, *args, **kwargs):
try:
return super(MyView, self).get(request, *args, **kwargs)
except Http404:
return HttpResponseRedirect("/empty-queryset-url/")
That way, you're not responsible for the entire implementation of get. If Django changes it in the future, you're still good to go.

Django: return several views from a single URL without redirection

With function based Django view it was simple to switch between several different views based on a condition, e.g. something like:
def base_view(request):
if some_condition():
return foo_view(request)
else:
return bar_view(request)
I can't find a simple way to do the same with the new class-based generic views. The only way I can think of is to redisrect, which I would like to avoid for various reasons:
def base_view(request):
if some_condition():
return redirect(reverse("name_of_url_to_class-based_view_foo"))
else:
return redirect("/url_to_class-based_view_bar/")
Any suggestions?
This is equivalent to your example with class based views.
class FooView(View):
pass
class BarView(View):
pass
class BaseView(View):
# staticmethod to avoid adding 'self' to the arguments
foo_view = staticmethod(FooView.as_view())
bar_view = staticmethod(BarView.as_view())
def dispatch(self, request, *args, **kwargs):
if some_condition():
return self.foo_view(request, *args, **kwargs)
else:
return self.bar_view(request, *args, **kwargs)
Even though the Django docs do say that the function based generic views are now deprecated I think the only reason to switch would be if you're writing less code.
If you're still set on switching, you'll want to first identify which class based views or mixins are most appropriate (single object, multiple objects, date based, forms, etc.). If the conditional was used to select a function that returns different context data / template to provide to the view, you can push the conditional down into an overridden get_queryset|get_context_data|get_object|get_template_names depending on your use case.
For example,
def get_context_data(self, **kwargs):
# Call the base implementation first to get a context
context = super(BaseView, self).get_context_data(**kwargs)
# Add in the publisher
if some_condition():
context['some_data'] = ...
else:
context['other_data'] = ...
return context
If all else fails and you're still determined to have class based views, you could probably also override get(self, request, *args, **kwargs) and do the switching there to the appropriate method. The detailed docs are getting better but I've still found myself poking through the source code to figure out how to accomplish what I want.