Django; Tips for making views thin - python

I'm a beginner and I have been working on project using Django.
I am wondering if there's a good way to avoid repeating the same code.
Also if there are similar logic in some functions, how can I decide if the logic is organized or not.
for example,
def entry_list(request):
entry_list = Entry.objects.all()
#this part is repeated
page = request.GET.get('page', 1)
paginator = Paginator(entry_list, 10)
try:
entries = paginator.page(page)
except PageNotAnInteger:
entries = paginator.page(1)
except EmptyPage:
entries = paginator.page(paginator.num_pages)
return render(request, 'blog/entry_list.html', {'entries': entries})
The logic to paginate is repeated in some other functions as well.
Where should I put the repeated code and how can I decide if I should organize code?

Using function-based views
You could encapsulate it in another function (for example construct such function in a file named utils.py):
# in app/utils.py
def get_page_entries(entry_list, page, per_page=10):
paginator = Paginator(entry_list, per_page)
try:
return paginator.page(page)
except PageNotAnInteger:
return paginator.page(1)
except EmptyPage:
return paginator.page(paginator.num_pages)
You can then use it like:
# app/views.py
from app.utils import get_page_entries
def entry_list(request):
entry_list = Entry.objects.all()
entries= get_page_entries(entry_list, request.GET.get('page', 1))
return render(request, 'blog/entry_list.html', {'entries': entries})
You can provide an optional third parameter with the number of elements per page. If not provided, it will by default be 10.
Or we can encapsulate the request.GET.get(..) logic as well, like:
# in app/utils.py
def get_page_entries(entry_list, querydict, per_page=10, key='page', default_page=1):
page = querydict.get(key, default_page)
paginator = Paginator(entry_list, per_page)
try:
return paginator.page(page)
except PageNotAnInteger:
return paginator.page(default_page)
except EmptyPage:
return paginator.page(paginator.num_pages)
and thus call it with:
# app/views.py
from app.utils import get_page_entries
def entry_list(request):
return render(request, 'blog/entry_list.html', {
'entries': get_page_entries(Entry.objects.all(), request.GET)
})
Using class-based views
You however do not need to use function-based views. This use case is covered by the ListView class [Django-doc]:
class EntryListView(ListView):
model = Entry
template_name = 'blog/entry_list.html'
context_object_name = 'entries'
paginate_by = 10
and then in the urls.py, use EntryListView.as_view() instead of the function, so:
# app/urls.py
from django.urls import path
from app.views import EntryListView
urlpatterns = [
path('entries', EntryListView.as_view(), name='entry-list'),
]
Note only did we reduce the number of lines of code, this is also a more declarative way of developing: instead of specifying how we want to do something, we specify what we want to do. How it is done is more up to Django's implementation of a ListView. Furthermore by providing the settings as attributes of the class, we can easily develop more tooling that take these parameters into account.

I asked myself the same questions for several things including:
functions I might use many times in my views,
messages I would use many times in my views but also tests (in order to test behavior of my apps),
exceptions that could be used in many models or views,
choices when defining Charfields which value can be only specific values,
context values that could be used in many templates,
etc.
Here how I work for instance:
functions: a utils.py file as Willem said does the trick,
messages: I've got a customMessages.py file used for that, where I define simple messages (e.g.:SUCCESS_M_CREATE_OBJECT=_("You created this object successfully."), then used by calling customMessages.SUCCESS_M_CREATE_OBJECT in my models, tests or views) or more complexe ones with variable fields for instance, defined by a function and called with lambda in my tests,
context: by defining dedicated functions in my context_processors.py returning dictionaries of context vars, registering them in my settings.py and then I'm fine if want to call any in my templates,
etc.
Well, there is always a way to not repeat yourself for anything you do in Python and Django, and you are just right to ask yourself those questions at anytime in your development, because your futur self will thank you for that!

Related

Django pagination: EmptyPage: That page contains no results

When using Django CBV ListView with pagination:
class Proposals(ListView):
model = Proposal
ordering = "id"
paginate_by = 10
In the browser, if I provide a page that is out of range, I get an error:
I would like to have a different behaviour: to fallback to the last existing page if the provided page is out of range.
I dug into Django source code paginator.py file and was surprised to find some code that does exactly this:
So using paginator.get_page(page) (and not paginator.page(page)) would be the way to go. However, ListView does not use it as you can see here:
What is the best way to deal with this?
Thanks.
The only solution I found is by overriding the paginate_queryset method.
However I don't like it as I'm forced to rewrite the whole logic while I just want to change a single line.
Open to any better suggestion.
class PermissivePaginationListView(ListView):
def paginate_queryset(self, queryset, page_size):
"""
This is an exact copy of the original method, jut changing `page` to `get_page` method to prevent errors with out of range pages.
This is useful with HTMX, when the last row of the table is deleted, as the current page in URL is not valid anymore because there is no result in it.
"""
paginator = self.get_paginator(
queryset,
page_size,
orphans=self.get_paginate_orphans(),
allow_empty_first_page=self.get_allow_empty(),
)
page_kwarg = self.page_kwarg
page = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 1
try:
page_number = int(page)
except ValueError:
if page == "last":
page_number = paginator.num_pages
else:
raise Http404(_("Page is not “last”, nor can it be converted to an int."))
try:
page = paginator.get_page(page_number)
return (paginator, page, page.object_list, page.has_other_pages())
except InvalidPage as e:
raise Http404(
_("Invalid page (%(page_number)s): %(message)s")
% {"page_number": page_number, "message": str(e)}
)

Control requests to view and template output in django

This is a view for get all the records in the EducationalRecord model:
def all_education_resume(request):
RESUME_INFO['view'] = 'education'
educations_resume = EducationalRecord.objects.all().order_by('-created_date')
template = 'resumes/all_resume.html'
context = {'educations_resume': educations_resume, 'resume_info': RESUME_INFO}
return render(request, template, context)
Now, if I want to write exactly this view for other models (like job resumes, research resumes , etc.),
I must another view one separately.
My question is:
How can I get a view for all these requests, so first check the URL of
the request and then do the relevant query? How can I control URL
requests in my views?
My other question is exactly the same as my first question,with this difference:
control view that must render in specific template.In other words,in
second question the ratio between the template and the view is instead
of the ratio of the view to the url or how to create a template for
multiple views (for example, for a variety of database resume
resumes, I have a template) and then, depending on which view render,
the template output is different.
I have implemented these two issues as follows:
I wrote a view for each of request!
In each view, I set the value of RESUME_INFO['view'], and then I've checked it in a template page and specified the corresponding template.
What is the best solution to these two questions?
How can I get a view for all these requests, so first check the URL of the request and then do the relevant query? How can I control URL requests in my views?
You can access request.path, or you can let the url(..)s pass a parameter with kwargs that holds a reference to the model for example, but this is usually bad design. Typically if you use different models, you will likely have to order these different as well, filter these differently, render these differently, etc. If not, then this typically indicates that something is wrong with the modeling.
You can however make use of class-based views [Django-doc], to remove as much boilerplate as posssible. Your view looks like a ListView [Django-doc], by using such view, and patching where necessary, we can omit most of the "boilerplate" code:
# app/views.py
from django.views.generic.list import ListView
class MyBaseListView(ListView):
resume_info = None
template = 'resumes/all_resume.html'
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context['resume_info'] = {'view': self.resume_info}
return context
In the individual listviews, you then only need to specify the resume_info and the model or queryset to render it with the 'all_resume.html' template, for example:
# app/views.py
# ...
class EducationalResumeView(MyBaseListView):
queryset = EducationalRecord.objects.order_by('-created_date')
resume_info = 'education'
class OtherModelView(MyBaseListView):
model = OtherModel
resume_info = 'other_info'
So we can here use inheritance to define common things only once, and use it in multiple views. In case we need to change something in a specific view, we can override it at that level.
In the urls.py, you define such view with the .as_view() method [Django-doc]. For example:
# app/urls.py
from django.urls import path
from app.views import EducationalResumeView, OtherModelView
urlpatterns = [
path('education/', EducationalResumeView.as_view()),
path('other/', OtherModelView.as_view()),
]

How to enable function to render templates based on try, except blocks from within another view in Django?

I am creating an application in Django that would allow my users to order items from my site based on information already stored in the database.
Not all my users should be able to order certain items, for this purpose I have written a pipeline with comparison statements and try, except blocks.
A small, reproduce-able piece of code looks like this:
vendor.py
def guest_constraint(request)
# Ensure user in request is a house-guest by checking if it has an active token.
try:
guest = GuestProfile.objects.get(user=request.user.id)
except ObjectDoesNotExist:
return render(request, 'extGuest/appGuestError/not_hotel_login.html')
# Check for Hotel Room Information linked to Guest Token
try:
room_information = RoomInformation.objects.get(guest_token=guest.token)
except ObjectDoesNotExist:
return render(request, 'extGuest/appGuestError/constraint_error.html')
views.py
from .vendor import guest_constraint
#login_required
def index(request):
guest_contraint(request) # Filter out users with no access to this process.
user = request.user # Grab user defined in request.
name = user.get_short_name() # Grab first name of user.
return render(request, 'extGuest/appGuestFlow/choose_order_type.html')
Challenge: I can successfully import this small script into my view and I can see its content is run except for the return render(request, template) part.
To explain myself better, the try/except block successfully catch the exception, however it does not returns the template specified in the block but instead it goes back to the view and renders the template I have in the view.
What have I tried? If I place the code of guest_constraint (vendor.py) and place it inside index (views.py) I see no problem and works as expected. This however doesn't scale well as I wish to call guest_contraint for many different functions in views.py
I'm fairly new to programming and Django and I still have lots to learn. If you could please give me a hint on what you think I'm doing wrong or what topic on Django (or even basic Python) could help me tackle this issue it would be greatly appreciated. Thanks!
Edit: I forgot to mention, I'm using Django 1.11.6.
Edit 2: Yikes, I forgot to include how I use the function in my index view. My bad, sorry.
Solution:
Thanks to #cwallenpoole and some changes I did on his response I was able to edit my code to do what I wanted to do and it now looks like this:
vendor.py | Updated
def guest_constraint(function):
def _inner_guest_constraint(request)
# This part should be familiar
try:
guest = GuestProfile.objects.get(user=request.user.id)
except ObjectDoesNotExist:
return render(request, 'extGuest/appGuestError/not_hotel_login.html')
try:
room_information = RoomInformation.objects.get(guest_token=guest.token)
except ObjectDoesNotExist:
return render(request, 'extGuest/appGuestError/constraint_error.html')
# Once all checks are passed return flow back to function.
return function(request)
# return the wrapping
return _inner_guest_constraint
views.py | Updated
from .vendor import guest_constraint
#login_required
#guest_constraint
def index(request):
user = request.user # Grab user defined in request.
name = user.get_short_name() # Grab first name of user.
return render(request, 'extGuest/appGuestFlow/choose_order_type.html')
It seems like you might want to consider using an annotation instead of just as a function:
def guest_constraint(fn):
def _inner_guest_constraint(request)
# This part should be familiar
try:
guest = GuestProfile.objects.get(user=request.user.id)
except ObjectDoesNotExist:
return render(request, 'extGuest/appGuestError/not_hotel_login.html')
try:
room_information = RoomInformation.objects.get(guest_token=guest.token)
except ObjectDoesNotExist:
return render(request, 'extGuest/appGuestError/constraint_error.html')
# Call the wrapped function
fn(request)
# return the wrapping
return _inner_guest_constraint
Then you could simply annotate:
#login_required
#guest_constraint
def index(request):
You could also modify it so that your wrapping function adds parameters:
def guest_constraint(fn):
def _inner_guest_constraint(*args,**kwargs):
# stuff
kwargs.update({'guest':guest, 'room_info': room_information})
fn(*args,**kwargs)
return _inner_guest_constraint
That would mean that you'd need to make sure that your annotated views took guest and room_info params, but it would also mean that you're defining variables once.
Your sample views.py doesn't show anything using guest_constraint, just the import. It sounds like you want something in the middle of index (and other views) that will check guest_constraint. One simple way to handle it is to return two values - a result status and the render() results. Add False to each of the existing return render() lines and at the end of the function return True, None. Full function becomes:
def guest_constraint(request)
# Ensure user in request is a house-guest by checking if it has an active token.
try:
guest = GuestProfile.objects.get(user=request.user.id)
except ObjectDoesNotExist:
return False, render(request, 'extGuest/appGuestError/not_hotel_login.html')
# Check for Hotel Room Information linked to Guest Token
try:
room_information = RoomInformation.objects.get(guest_token=guest.token)
except ObjectDoesNotExist:
return False, render(request, 'extGuest/appGuestError/constraint_error.html')
# Everything is good
return True, None
and in views.py you could have:
constraint_ok, constraint_render = guest_constraint(request)
if not constraint_ok:
return constraint_render

Django Rest Framework 3.1 breaks pagination.PaginationSerializer

I just updated to Django Rest Framework 3.1 and it seems that all hell broke loose.
in my serializers.py I was having the following code:
class TaskSerializer(serializers.ModelSerializer):
class Meta:
model = task
exclude = ('key', ...)
class PaginatedTaskSerializer(pagination.PaginationSerializer):
class Meta:
object_serializer_class = TaskSerializer
which was working just fine. Now with the release of 3.1 I can't find examples on how to do the same thing since PaginationSerializer is no longer there.
I have tried to subclass PageNumberPagination and use its default paginate_queryset and get_paginated_response methods but I can no longer get their results serialized.
In other words my problem is that I can no longer do this:
class Meta:
object_serializer_class = TaskSerializer
Any ideas?
Thanks in advance
I think I figured it out (for the most part at least):
What we should have used from the very beginning is this:
Just use the built-in paginator and change your views.py to this:
from rest_framework.pagination import PageNumberPagination
class CourseListView(AuthView):
def get(self, request, format=None):
"""
Returns a JSON response with a listing of course objects
"""
courses = Course.objects.order_by('name').all()
paginator = PageNumberPagination()
# From the docs:
# The paginate_queryset method is passed the initial queryset
# and should return an iterable object that contains only the
# data in the requested page.
result_page = paginator.paginate_queryset(courses, request)
# Now we just have to serialize the data just like you suggested.
serializer = CourseSerializer(result_page, many=True)
# From the docs:
# The get_paginated_response method is passed the serialized page
# data and should return a Response instance.
return paginator.get_paginated_response(serializer.data)
For the desired page size just set the PAGE_SIZE in settings.py:
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 15
}
You should be all set now with all the options present in the body of the response (count, next and back links) ordered just like before the update.
However there is one more thing that still troubles me: We should also be able to get the new html pagination controls which for some reason are missing for now...
I could definitely use a couple more suggestions on this...
I am not sure if this is the completely correct way to do it, but it works for my needs. It uses the Django Paginator and a custom serializer.
Here is my View Class that retrieves the objects for serialization
class CourseListView(AuthView):
def get(self, request, format=None):
"""
Returns a JSON response with a listing of course objects
"""
courses = Course.objects.order_by('name').all()
serializer = PaginatedCourseSerializer(courses, request, 25)
return Response(serializer.data)
Here is the hacked together Serializer that uses my Course serializer.
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
class PaginatedCourseSerializer():
def __init__(self, courses, request, num):
paginator = Paginator(courses, num)
page = request.QUERY_PARAMS.get('page')
try:
courses = paginator.page(page)
except PageNotAnInteger:
courses = paginator.page(1)
except EmptyPage:
courses = paginator.page(paginator.num_pages)
count = paginator.count
previous = None if not courses.has_previous() else courses.previous_page_number()
next = None if not courses.has_next() else courses.next_page_number()
serializer = CourseSerializer(courses, many=True)
self.data = {'count':count,'previous':previous,
'next':next,'courses':serializer.data}
This gives me a result that is similar to the behavior that the old paginator gave.
{
"previous": 1,
"next": 3,
"courses": [...],
"count": 384
}
I hope this helps. I still think there has got to be a beter way to do this wiht the new API, but it's just not documented well. If I figure anything more out, I'll edit my post.
EDIT
I think I have found a better, more elegant way to do it bey creating my own custom paginator to get behavior like I used to get with the old Paginated Serializer class.
This is a custom paginator class. I overloaded the response and next page methods to get the result I want (i.e. ?page=2 instead of the full url).
from rest_framework.response import Response
from rest_framework.utils.urls import replace_query_param
class CustomCoursePaginator(pagination.PageNumberPagination):
def get_paginated_response(self, data):
return Response({'count': self.page.paginator.count,
'next': self.get_next_link(),
'previous': self.get_previous_link(),
'courses': data})
def get_next_link(self):
if not self.page.has_next():
return None
page_number = self.page.next_page_number()
return replace_query_param('', self.page_query_param, page_number)
def get_previous_link(self):
if not self.page.has_previous():
return None
page_number = self.page.previous_page_number()
return replace_query_param('', self.page_query_param, page_number)
Then my course view is very similar to how you implemented it, only this time using the Custom paginator.
class CourseListView(AuthView):
def get(self, request, format=None):
"""
Returns a JSON response with a listing of course objects
"""
courses = Course.objects.order_by('name').all()
paginator = CustomCoursePaginator()
result_page = paginator.paginate_queryset(courses, request)
serializer = CourseSerializer(result_page, many=True)
return paginator.get_paginated_response(serializer.data)
Now I get the result that I'm looking for.
{
"count": 384,
"next": "?page=3",
"previous": "?page=1",
"courses": []
}
I am still not certain about how this works for the Browsable API (I don't user this feature of drf). I think you can also create your own custom class for this. I hope this helps!
I realize over a year has passed since this was posted but hoping this helps others. The response to my similar question was the solution for me. I am using DRF 3.2.3.
Django Rest Framework 3.2.3 pagination not working for generics.ListCreateAPIView
Seeing how it was implemented gave me the solution needed to get pagination + the controls in the visible API.
https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/mixins.py#L39

'Page' object has no attribute 'ordered' Exception

I need to paginate my modelformset_factory but I am getting this exception:
Django Version: 1.6.2
Exception Type: AttributeError
Exception Value:
'Page' object has no attribute 'ordered'
I have this in my view:
BrandFormSet = modelformset_factory(Brand, form=BrandFormList, extra=0, **kw)
paginator, brands = paginate(request, Brand.objects.filter(shop=shop), 10)
paginate function code:
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
def paginate(request, object_list, per_page):
paginator = Paginator(object_list, per_page)
page = request.GET.get('page')
try:
objects = paginator.page(page)
except PageNotAnInteger:
objects = paginator.page(1)
except EmptyPage:
objects = paginator.page(paginator.num_pages)
return paginator, objects
Now this in my view:
print type(Brand.objects.filter(shop=request.user.shop))
print type(brands)
Outputs:
<class 'django.db.models.query.QuerySet'>
<class 'django.core.paginator.Page'>
I am not sure what to do at this point.
The problem here is that you're using brands (a Page) in a context that's expecting a QuerySet.
It doesn't seem as though pagination and formsets were designed to work together. From glancing at the source code, it looks like brands.object_list might still be a QuerySet. If so, use that where you were using brands before.
This answer gives a hacky solution for using the two together, but it's quite inefficient. (It makes one trip to the database to get the desired item ids, and then constructs a new QueryList that explicitly includes those ids.)

Categories

Resources