How do I rewrite the function-based view that only updates an object into a class-based view? Something like this (wrote this for a tutorial).
from django.contrib.auth.decorators import permission_required
#permission_required("catalog.mark_returned")
def mark_returned(request, pk):
bk = BookInstance.objects.get(pk=pk)
if bk.status == "a":
#some tests here
bk.status = "a"
bk.due_back = None
bk.borrower = None
bk.save()
return redirect("on-loan")
And in general, does it make sense to use generics for things like that?
Because currently, I only use the generic list and detail views.
Sorry for beginner-level questions:)
It's a view without a template or a form; that kinda excludes the usefulness of just about every generic view except the basic View class itself.
You would override the ClassBasedView's dispatch method (since the request method does not matter) with the logic of your function view.
The pk value can be accessed through self.args[0] or self.kwargs['_name_of_captured_variable_here'].
Does it make sense? In general, I would say becoming familiar with CBVs and especially the generic ones will do you good in the long run. In your special case, however, you gain or lose nothing by using a CBV over function view. The view is just too simplistic.
Related
My question is more about refactoring, best practices and potential vulnerability while organizing my routes and views in DRF.
My app is really a simple one and all the views are to be described in the same fashion. Take my views.py file for example:
# views.py
class TrackView(viewsets.ModelViewSet):
queryset = Track.objects.all()
serializer_class = TrackSerializer
class AlbumView(viewsets.ModelViewSet):
queryset = Album.objects.all()
serializer_class = TrackSerializer
class ArtistView(viewsets.ModelViewSet):
queryset = Artist.objects.all()
serializer_class = ArtistSerializer
class ChartView(viewsets.ModelViewSet):
queryset = Chart.objects.all()
serializer_class = ChartSerializer
And in urls.py I can do:
#urls.py
router = DefaultRouter()
router.register(r'tracks', TrackView)
router.register(r'albums', AlbumView)
router.register(r'artists', ArtistsView)
router.register(r'charts', ChartsView)
urlpatterns = [
path('', include(router.urls)),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]
In this scenario every new View is a copied logic of each other only changing the name of the model they use, and for every new model added that needs a view I would basically have to create the same repeated code in urls.py and views.py.
Instead of doing like that I refactored it like this:
# views.py
from rest_framework import viewsets
class APIView(viewsets.ModelViewSet):
queryset = None
def get_queryset(self):
return eval(self.basename).objects.all()
def get_serializer_class(self):
return eval(self.basename+'Serializer')
# urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from top_charts.views import *
from .routes import ROUTE_NAMES
router = DefaultRouter()
for route in ROUTE_NAMES:
router.register(r''+route.lower()+'s', APIView, basename=route)
urlpatterns = [
path('', include(router.urls)),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]
# Here in routes.py would be the only place I'd need to update when I have a new model
ROUTE_NAMES = [
'Track',
'Album',
'Artist',
'Chart'
]
It works, and I can access the urls of all endpoints just fine and it has a much cleaner code. My question is: is this a good practice or do you see any problem with my implementation?
There are always concerns when it comes to the use of eval() but I could not think pragmatically what could go wrong in here since ROUTE_NAMES is the one that would define the evaluation and that would totally be under my control, so it doesn't seem like a place where I leave for potential vulnerability.
Also I don't see this sort of implementation suggestion anywhere in DRF documentation. They always go for the repeated code logic of views, which left me suspicious and wondering why.
Part by part:
About the URLs, I don't think what you have done is very wrong, but it is certainly not a good practise. I see three main issues.
Having the routes in the URLs file makes you able to see the views and paths directly. Separating those into two files is not really useful. You save 2 lines of code, but that doesn't make the code more efficient and it doesn't make it cleaner either.
A problem that you might have is that whenever you need to add a route that doesn't follow the same pattern as all other ones you have, you'll have to add it separately. Or if you have to modify one of the already existing ones you'll have to remove the item from the list and, again, add another route separately. You've got more files and data to maintain but every route could be clearly defined in one line in urls.py.
Suppose you have the following:
ROUTE_NAMES = [
'Track',
'Album',
'Artist'
'Chart'
]
It looks the same, right? But a comma is missing after the third element, and now you've got an app that works in a completely different without crashing while running the server, because the code is actually completely fine. Having the routes that way is a source of errors that would be easily avoided just by having one router.register per line.
About the views, I also have 2 main concerns.
The first one is basically the points 1 and 2 I've explained about the URLs combined: you save lines of code but that doesn't make the code that more efficient, and the moment you need to add more functionalities to one view you'll have to remake it because there's no way for you to modify only one leaving the others the way they are.
A big concern is on the use of eval, as you had already guessed. Yes, I too fail to see a way to exploit that, but isn't it better to just not use it? If you really need to save those few lines of code, there are other ways to do what you intend without using eval or other dangerous methods. For example, using getattr:
from rest_framework import viewsets
from . import models, serializers
class APIView(viewsets.ModelViewSet):
queryset = None
def get_queryset(self):
return getattr(models, self.basename).objects.all()
def get_serializer_class(self):
return getattr(serializers, self.basename + 'Serializer')
This way, the exact same is achieved without using a too dangerous method, limiting the items you can get to whatever has been declared or imported in models.py and serializers.py, instead of allowing the execution of any expression. However, this is still not recommended because the problem I've stated in point 1 remains.
At the start of your question you talk about refactoring. I would say that Django and DRF don't really consider refactoring in their guides and docs because they provide very simple examples and use cases for their software. Your example application is very small too, and the code is very clean, so there's no real reason to refactor that. I could talk for hours about when and how to refactor code, but as a general rule don't refactor anything that is clearly understandable if efficiency is not critical.
I am starting out with django and I have come across functions like reverse_lazy instead of reverse and gettext_lazy instead of gettext for translation.
From the information, I could get online, it seems that these lazy functions only call the original functions when they are to be used and not immediately they are declared.
What exactly does this mean and in what real life coding senerios should I use the lazy version as against the original function.
Thanks a lot in advance
This is the sort of thing that is only needed per-specific-situation, as you've intuited yourself.
A common example is with Generic Class-based Views. The attributes of the view are evaluated when the views.py file is read. But at the time it's being read, the urls file will not have been read yet! So assigning to an attribute with reverse() will fail, because any url named in the arguments sent to reverse() will not be available to the view file. But with reverse_lazy(), you can assign a url to an attribute in the class-based view, and it will not be updated until the named url is actually available:
class RegisterView(CreateView):
form_class = CustomUserCreationForm
success_url = reverse_lazy('index') # reverse() would fail here!
template_name = 'registration/register.html')
Again: success_url is an attribute of the view, and will be evaluated when the views.py file is read. But at that time, the urls will not have been read, so 'index', a named url, will not be available yet. We use reverse_lazy() instead of reverse() to get around this problem; 'index' is not evaluated until it's actually needed, and now we can define our attribute as desired.
Note that if your view has a user-defined method, you can use the normal reverse() call inside the method, if needed, because unlike view attributes, a view's methods are not evaluated until the method is explicitly called -- and by that time, the urls will be available.
There are other use cases, for sure, but CBV's are a common case where reverse_lazy() becomes necessary, specifically when assigning urls to attributes.
I have a viewset and some methods in it and getschedule is one of them.
def getschedule(self, request):
In the urls.py if I map the method getschedule like this
url(r'^event/(?P<pk>[0-9]+)/getschedule/$', EventSingleViewSet.getschedule, name='event-schedule'),
I get this error "getschedule() missing 1 required positional argument: 'request'
"
But if I do the mapping like this,
url(r'^event/(?P<pk>[0-9]+)/getschedule/$', event_getschedule, name='event-schedule'),
......
event_getschedule = EventViewSet.as_view({
'get': 'getschedule'
}, renderer_classes=[JSONRenderer])
it works and gives me a response.
I don't understand how the request is getting passed to the method in the second approach. Need help understanding this.
I would also like to know how i could make my first approach work.
If your viewset already is tied to a router then you can use
#detail_route, or #list_route to point it to a url with the name of your viewset method.
Check this part of documentation : http://www.django-rest-framework.org/tutorial/6-viewsets-and-routers/
Otherwise, it would make sense to use plan view class extending APIView and pointing a URL to it.
Viewsets are mainly useful when tied to a router.
I have a base template for when a user is logged in, and on that base template, I need to add user specific options in a drop down menu. This drop down menu with options must be constant across all handlers, i.e., any time the base template is invoked (extended) with a child template.
Other than performing the necessary DB query, assigning the query results to a variable, and passing that variable to every handler (there are many), how can I consolidate this into one query and one variable, which gets passed directly to the base template? I am using jinja2 templates as well.
I would hate to do something so cumbersome in exchange for something far more simple and maintainable.
Any ideas? Thanks.
EDIT
So I still haven't found anything that's exactly what I'm looking for; however, I decided to at least make some headway in the interim. So, I made a custom decorator that takes a view's returned dict() and appends the appropriate data to it. For example:
def get_base_data(func):
def wrapper(request):
d = func(request)
user_id = request.user.id # used in query
contact_group_data = ContactGroups.query.filter(...criteria...).all()
d['contact_group_data'] = contact_group_data
return d
return wrapper
Now, I can at least decorate each method very concisely and simply by putting:
#view_config(...)
#get_base_data
def my_handler(request):
pass # rest of code...
This is one of most inobvious things in Pyramid and took a while to find for me, too.
You can modify the global template context in BeforeRender event.
http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/hooks.html#using-the-before-render-event
Alternatively, you could use class based views, inherit all your views from one base view class which has get_base_data(), then the class instance is passed to the template context to all your views and then you could extract the data with {{ view.get_base_data }}.
http://ruslanspivak.com/2012/03/02/class-based-views-in-pyramid/
I vouch for the latter approach as it is more beautiful, predictable and easier to maintain engineering wise.
In Django you can use the exclude to create SQL similar to not equal. An example could be.
Model.objects.exclude(status='deleted')
Now this works great and exclude is very flexible. Since I'm a bit lazy, I would like to get that functionality when using get_object_or_404, but I haven't found a way to do this, since you cannot use exclude on get_object_or_404.
What I want is to do something like this:
model = get_object_or_404(pk=id, status__exclude='deleted')
But unfortunately this doesn't work as there isn't an exclude query filter or similar. The best I've come up with so far is doing something like this:
object = get_object_or_404(pk=id)
if object.status == 'deleted':
return HttpResponseNotfound('text')
Doing something like that, really defeats the point of using get_object_or_404, since it no longer is a handy one-liner.
Alternatively I could do:
object = get_object_or_404(pk=id, status__in=['list', 'of', 'items'])
But that wouldn't be very maintainable, as I would need to keep the list up to date.
I'm wondering if I'm missing some trick or feature in django to use get_object_or_404 to get the desired result?
Use django.db.models.Q:
from django.db.models import Q
model = get_object_or_404(MyModel, ~Q(status='deleted'), pk=id)
The Q objects lets you do NOT (with ~ operator) and OR (with | operator) in addition to AND.
Note that the Q object must come before pk=id, because keyword arguments must come last in Python.
The most common use case is to pass a Model. However, you can also pass a QuerySet instance:
queryset = Model.objects.exclude(status='deleted')
get_object_or_404(queryset, pk=1)
Django docs example:
https://docs.djangoproject.com/en/1.10/topics/http/shortcuts/#id2
There's another way instead of using Q objects. Instead of passing the model to get_object_or_404 just pass the QuerySet to the function instead:
model = get_object_or_404(MyModel.objects.filter(pk=id).exclude(status='deleted'))
One side effect of this, however, is that it will raise a MultipleObjectsReturned exception if the QuerySet returns multiple results.
get_object_or_404 utilizes the get_queryset method of the object manager. If you override the get_queryset method to only return items that aren't "deleted" then get_object_or_404 will automatically behave as you want. However, overriding get_queryset like this will likely have issues elsewhere (perhaps in the admin pages), but you could add an alternate manager for when you need to access the soft deleted items.
from django.db import models
class ModelManger(models.Manger):
def get_queryset(self):
return super(ModelManger, self).get_queryset().exclude(status='deleted')
class Model(models.Model):
# ... model properties here ...
objects = ModelManager()
all_objects = models.Manager()
So if you need only non-deleted items you can do get_object_or_404(Models, id=id) but if you need all items you can do get_object_or_404(Models.all_objects, id=id).