I re-use an existing drf model viewset but there are some custom actions (assigned with #action label) that i don't need. How can I hide/remove it from django rest framework without modifying the origional model viewset?
for example
class MyViewSet(viewsets.ModelViewSet):
#action(["get",], detail=False)
def custom_a(self, request):
# some stuff
#action(["get",], detail=False)
def custom_b(self, request):
# some stuff
#action(["get",], detail=False)
def custom_c(self, request):
# some stuff
My router
router = routers.SimpleRouter()
router.register("dummies", views.MyViewSet)
urlpatterns = [
path('', include(router.urls)),
]
Then I will have these endpoints
GET /dummies/
GET /dummies/{id}/
POST /dummies/
PUT /dummies/{id}/
PATCH /dummies/{id}/
DELETE /dummies/{id}/
GET /dummies/custom_a/
GET /dummies/custom_b/
GET /dummies/custom_c/
Now how can I just keep 5 first views and GET /dummies/custom_a/?
Thanks.
There are a few ways to do this, but the "cleanest" seems overriding.
Override & Ignore
Decorators are not inherited, so you can just re-declare the method on your derived class. This new method takes precedence over the base class method, so DRF no longer sees them as #actions.
class View1(viewsets.ModelViewSet):
#action(['get'], detail=False)
def act_up(self, request):
pass
class Meta:
model = Client
fields = "__all__"
class View2(View1):
# redefine and don't add the #action decorator
def act_up(self, request):
pass
class Meta:
model = View1.Meta.model
fields = View1.Meta.fields
Use a router and just removed the methods you don't want
Router URLs are calculated once and then cached. You could make this happen, then filter out the ones you don't want (by name)
router = SimpleRouter()
router.register("view1", View1, basename="v1")
router._urls = [
r for r in router.urls
if not any(r.name.endswith(bad) for bad in ['-act-up', '-other-rt']
]
Manually route to the actions
You can manually create the routes you need, in the same manner that the base SimpleRouter does. This is a lot more work, and most certainly not worth it
list_paths = View1.as_view({"get": "list"})
detail_paths = View1.as_view({"get": "retrieve", "patch": "partial_update"})
urlpatterns = [
path("view1/", list_paths, name="view1-list"),
path("view1/<int:pk>/", detail_paths, name="view1-detail")
]
What this does is bind a url + http method (e.g. get) to a specific DRF "action" (list/retrieve/destroy,etc). So GET view11/ calls your viewset with action=list, which the viewset then dispatches to the correct method internally.
Since you only map the things you want, there is no way for those bad "other" methods to be called.
It is a bit trickier, and confusing, and makes you responsible for the routing which, together, make it more work to maintain & understand. If there is no other way, or if you only want 1 or 2 methods from the ViewSet, then its an OK option.
Related
Let's say I want to use the LoginRequiredMixin and a UserPermissionMixin created by myself and apply them to all the views in an app. This is just an example, I might also have mixins that add some context or do other stuff.
I could do it manually, for example this view:
class MyCreateView(LoginRequiredMixin, UserPermissionMixin, CreateView)
But, since I have many views and I might have other specific mixins for some views, this gets messy and hard to manage.
One solution that came to mind would be to create new classes for the generic views:
class DecoratedCreateView(LoginRequiredMixin, UserPermissionMixin, CreateView):
pass
class DecoratedDetailView(LoginRequiredMixin, UserPermissionMixin, DetailView):
pass
class DecoratedUpdateView(LoginRequiredMixin, UserPermissionMixin, UpdateView):
pass
class DecoratedDeleteView(LoginRequiredMixin, UserPermissionMixin, DeleteView):
pass
and then, use these as my generic views:
class MyCreateView(DecoratedCreateView)
Is this a good approach? Do I have to add any methods in the classes above or do I just leave them blank and it'll work as expected?
Is there any other way to achieve this, maybe in urls.py ?
Your approach is good. I've been doing so for some projects with a slight difference:
myapp/views/generic.py
from django.views.generic import (
CreateView as BaseCreateView,
DetailView as BaseDetailView,
UpdateView as BaseUpdateView,
DeleteView as BaseDeleteView,
)
__all__ = ['MyappMixin', 'CreateView', 'DetailView', 'UpdateView', 'DeleteView']
class MyappMixin(LoginRequiredMixin, UserpermissionMixin):
pass
class CreateView(MyappMixin, BaseCreateView):
pass
class DetailView(MyappMixin, BaseDetailView):
pass
class UpdateView(MyappMixin, BaseUpdateView):
pass
class DeleteView(MyappMixin, BaseDeleteView):
pass
myapp/views/base.py
from .generic import CreateView
class MyCreateView(CreateView):
pass
It works fine, without much hassle, and allows you to easily skip the mixin exceptionally if needed.
According to the usecase, another solution might be to use middlewares or context processors.
class MyMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
is_in_myapp = request.resolver_match.app_name == 'myapp'
if is_in_myapp and not request.user.is_authenticated:
response = HttpResponse("Permission denied", status=403)
else:
response = self.get_response(request)
return response
I will have standard class-based views for CRUD operations that inherit from various generic views like ListView, DetailView and so on.
I will be setting all of their
context_object_name
attribute to the same value.
I was wondering if there is a way to do it more pythonic, to not repeat the operations many times in the code, but to be able to change that variable in one place if necessary?
ps. what comes to my mind is of course further inheritance, but maybe there is some more django-like way?
You can also use a mixin, instead of a middleware app:
class CommonContextMixin(object):
def get_context_data(self, *args, **kwargs):
context = super(CommonContextMixin, self).get_context_data(*args, **kwargs)
context['foo'] = 'bar'
return context
Then use that mixin in your views:
class MyView(TemplateView, CommonContextMixin):
""" This view now has the foo variable as part of its context. """
Relevant Django docs: https://docs.djangoproject.com/en/2.1/topics/class-based-views/mixins/
Middleware can do the trick
class SetContextObjectNameMiddleware:
def process_template_response(self, request, response):
if 'object' in response.context_data:
response.context_data['foo'] = response.context_data['object']
return response
Then add the middleware to your settings.py
It's not really setting the view's context_object_name but it achieves the same outcome.
In Django, when using class based views, it is commonplace to setup class-level variables such as template_name.
class MyView(View):
template_name = 'index.html'
def get(self, request):
...
I am wondering if modifying these variables during runtime will persist across multiple requests or just the current one.
class MyView(View):
template_name = 'index.html'
def get(self, request):
if only_returns_true_once_function():
self.template_name = 'something.html'
...
Each request creates a new instance of that class, handles the request, and destroys it. The reason for class-based views is not to maintain instances, it's to allow inheritance and mixin composition. This makes it substantially easier to create reusable functionality that spans multiple views.
You can change variables at any point in the class' lifetime. The only point that these variables become important is when the request is handled, specifically during the dispatch() method, which other HTTP action methods like get() and post() wrap.
I strongly encourage you to bookmark the Classy Class-based Views site because it offers an incredibly thorough overview of how class-based views are composed and how they inherit. The most appropriate way to change the template names in a class based view is to override the get_template_names() method on a TemplateView.
class MyView(TemplateView):
def get_template_names(self):
if some_contrived_nonce_function():
return 'something.html'
else:
return super(MyView, self).get_template_names()
The above assumes your view either inherits from TemplateView or implements TemplateResponseMixin.
Modifying this as:
self.template_name = 'something.html'
will definitely only last for that request.
Modifying it as:
type(self).template_name = 'something.html'
will cause new instances to inherit your changes.
I'm trying to create a REST-ModelViewSet that has no model predefined, but takes a model when registered with the router. I need this to dynamically add models to my REST-API, without configuring any new viewsets or serializers.
My idea was to pass the model in the kwargs of __init__, but I can't figure out how to correctly do this. Here is what I tried:
//Viewset
class ThemeViewSet(viewsets.ModelViewSet):
def __init__(self, **kwargs):
self.model = kwargs['model']
self.serializer_class = None
super(ThemeViewSet, self).__init__(**kwargs)
def get_serializer_class(self):
if self.serializer_class is not None:
return self.serializer_class
class ThemeSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = self.model
self.serializer_class = ThemeSerializer
return self.serializer_class
//Router:
router = routers.DefaultRouter()
router.register(r'mytheme', ThemeViewSet(model=mytheme), base_name='mytheme')
Now, if I try to print self.model in __init__, it correctly shows <class 'myapp.models.mytheme'> in the console, but Django still returns an error:
AttributeError at /api/mytheme/
This method is available only on the view class.
This error is raised by the classonlymethod-decorator. I don't really know what to make of this, is there any way to pass the model to __init__, or is there a different approach that I can try?
(I know that wq.db.rest has a router that does what I want, but I don't want to use wq. I haven't tried tastypie, would that make it easier/possible?)
Thanks in advance!
Django REST Framework expects that a ViewSet class is passed into the router, not a view instance. This is because the instance has to be created for each request, which prevents a lot of ugly issues with shared state and also follows the standard Django class-based views.
You may have better luck with having a method that creates a customized ViewSet class based on the model that is passed into it:
class ThemeViewSet(viewsets.ModelViewSet):
#classmethod
def create_custom(cls, **kwargs):
class CustomViewSet(cls):
model = kwargs["model"]
queryset = kwargs["model"].objects.all()
return CustomViewSet
Note that I'm also setting the queryset for the view, and DRF no longer accepts just a model since 2.4 was released.
This will create a new class each time it is called, and the model will automatically be set to the model that is passed into it. When registering it with the router, you would do something like:
router.register(r'mytheme', ThemeViewSet.create_custom(model=mytheme), base_name='mytheme')
This way you will still be passing a ViewSet class to the router, but it will be customized for the model that is passed in. You must make sure to set the base_name, or the router won't be able to generate the view names and you will eventually run into errors.
TL;DR
Is there a way to have (namespaced,) well-named views defined when using ModelAdmin.get_urls and ModelAdmins extended by inheritance?
Preferably without resorting to ModelAdmin.model._meta or some other solution of slightly questionable nature.
Pretext
View names added through get_urls get overridden when using and inheriting from custom ModelAdmins.
That is, the view name admin:tighten gets overriden in the following example:
class Screw(models.Model):
"A screw"
class HexCapScrew(Screw):
"A hex cap screw"
class ScrewAdmin(admin.ModelAdmin):
def get_urls(self):
urls = super(ScrewAdmin, self).get_urls()
extra_urls = patterns('',
url(r'^tighten/$', self.tighten, name='tighten'),
)
return extra_urls + urls
def tighten(self, request):
pass
class HexCapScrewAdmin(ScrewAdmin):
pass
admin.site.register(Screw, ScrewAdmin)
admin.site.register(HexCapScrew, HexCapScrewAdmin)
On shell the following happens:
In [1]: reverse('admin:tighten')
Out[1]: u'/admin/parts/hexscrew/tighten/'
This is of course understandable since the registration of HexCapScrewAdmin overides the tighten in ScrewAdmin however now it's impossible to reverse ScrewAdmin.tighten.
A preferred solution
However I would like to be able to
reference both views separatedly and
preferably have views in their own instance namespaces.
Progress so far
The best I've come up with is the following setup (can be copy&pasted directly to some app for testing):
from django.contrib import admin
from django.db import models
class Screw(models.Model):
"A screw"
class Meta:
app_label = 'parts'
class HexCapScrew(Screw):
"A hex cap screw"
class Meta:
app_label = 'parts'
proxy = True
class ScrewAdmin(admin.ModelAdmin):
def tighten(self, request):
pass
def get_urls(self):
urls = super(ScrewAdmin, self).get_urls()
extra_urls = patterns('',
url(r'^tighten/$', self.tighten, name='tighten'),
)
# Find out the slugified name of the model this admin is bound to
# TODO: Feels dirty
model_name = self.model._meta.model_name
# Add the to `extra_urls` to their own namespace
namespaced_extra_urls = patterns('',
url(r'^', include(extra_urls, namespace=model_name, app_name='screw')),
)
return namespaced_extra_urls + urls
class HexCapScrewAdmin(ScrewAdmin):
pass
admin.site.register(Screw, ScrewAdmin)
admin.site.register(HexCapScrew, HexCapScrewAdmin)
Now I have the following:
In [1]: reverse('admin:screw:tighten')
Out[1]: u'/admin/parts/screw/tighten/'
In [2]: reverse('admin:hexscrew:tighten')
Out[2]: u'/admin/parts/hexscrew/tighten/'
In [3]: reverse('admin:screw:tighten', current_app='hexscrew')
Out[3]: u'/admin/parts/hexscrew/tighten/'
which is nice and works but includes a bit of hackery.
Is this the best that's available or am I just missing something? Any suggestions?
(At least one other way would be to do as Django's ModelAdmin.get_urls use ModelAdmin.model._meta to parametrize the view names but then I would use the namespaces.)
If you look at the way the admin does it here, you will see that in addition to defining the url, the model admin also prefixes the app_label and model_name to the url name, thus avoiding the subclassing issue to begin with. It also has the advantage of securing the view against unauthorised users (using the self.admin_site.admin_view decorator). Your get_urls() method would then become:
def get_urls(self):
from django.conf.urls import url
def wrap(view):
def wrapper(*args, **kwargs):
return self.admin_site.admin_view(view)(*args, **kwargs)
return update_wrapper(wrapper, view)
info = self.model._meta.app_label, self.model._meta.model_name
urlpatterns = super(ScrewAdmin, self).get_urls()
urlpatterns.append(
url(r'^tighten/$', wrap(self.tighten), name='%s_%s_tighten' % info))
return urlpatterns
Then, you'd look up your url like: reverse('admin:app_screw_tighten') or reverse('admin:app_hex_screw_tighten').