I've got a class-based-view and I'm trying to implement breadcrumbs into it.
I'm using django-bootstrap-breadcrumbs & django-view-breadcrumbs, but because of our unique URL structure we have an ID in the URL of almost all of the urls.
I've setup the breadcrumbs appropriately, but need to get the person_id kwarg into the 'crumbs' attribute on the ListView
URLs look like this:
path(
"<person_id>/trees/",
views.TreeListView.as_view(),
name="tree_list",
),
path(
"<person_id>/trees/<pk>/",
views.TreeDetailView.as_view(),
name="tree_view",
),
path(
"<person_id>/trees/<tree_id>/planes/",
views.PlaneListView.as_view(),
name="plane_list",
),
path(
"<person_id>/trees/<tree_id>/cord/<pk>/",
views.CordDetailView.as_view(),
name="cord_view",
),
I've setup my view according to the documentation:
class TreeListView(
LoginRequiredMixin,
UserPassesTestMixin,
ListBreadcrumbMixin,
ListView
):
# pylint: disable=too-many-ancestors
login_url = "/login/"
model = Tree
template_name = "tree_list"
crumbs = [('My Test Breadcrumb', reverse('tree_list', args=[self.kwargs["person_id"]]))]
As you can see in the last line - the crumbs is what is supposed to create the breadcrumbs that display on the page.
The issue is that I get an error (which seem pretty obvious) that there is no 'self' item with that object.
My question is - how do I get that person_id from the URL so I can pass it as the argument to the URL?
You can define crumbs as property:
class TreeListView(
LoginRequiredMixin,
UserPassesTestMixin,
ListBreadcrumbMixin,
ListView
):
# pylint: disable=too-many-ancestors
login_url = "/login/"
model = Tree
template_name = "tree_list"
#property
def crumbs(self):
return [('My Test Breadcrumb', reverse('tree_list', args=[self.kwargs["person_id"]]))]
Related
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.
The test_func doesn't appear to run at all. Right now I'm just checking if the user is staff, as an easy test. Here's the code:
class MyModelCreate(CreateView, UserPassesTestMixin):
def test_func(self):
print("checking if user passes test....")
return self.request.user.is_staff
model = MyModel
....
However, when I go to the create page for this model, anyone can edit it, and there is no output to the python console showing that the test_func runs.
The view is called:
urlpatterns = [
...
url(r'^create/$', views.MyModelCreate.as_view(), name='my_model_create'),
]
In python the order of the classes matters because their methods override each other
Your class based view should look like this:
class MyModelCreate(UserPassesTestMixin, CreateView):
def test_func(self):
print("checking if user passes test....")
return self.request.user.is_staff
model = MyModel
....
I am trying to set GET parameters on a LinkColumn based on Accessors with django-tables2.
Let's say:
urls.py
urlpatterns = [
...
url(r'^rqGET$', views.rqGET, name='rqGET'),
...
]
views.py
def rqGET(request):
#... do something with request.GET
tables.py
class MyTable(tables.Table):
id = LinkColumn('rqGet',text='Link') # do something with Accessors to make a GET string, maybe ?id=A('pk')
class Meta:
model = MyModel #(has field 'id')
I want to use reverse to get the correct url, then construct the GET parameter string. For example /rqGET?id=1 ('1' would be different in each row).
That's not really how Accessors work in django-tables2. Django-tables2 uses django's reverse to generate urls. If you want reverse to be able to generate the url, you need to define the parameters in your urls, which will be passed as arguments to your view function:
# urls.py
urlpatterns = [
...
url(r'^rqGET/(?P<id>\d+)$', views.rqGET, name='rqGET'),
...
]
# views.py
def rqGET(request, id):
# do something with request/id.
If you don't want to change the way your url is formatted, you can use a custom render_ function on your MyTable like this:
# tables.py
from django.core.urlresolvers import reverse
class MyTable(tables.Table):
id = LinkColumn('rqGet', text='Link') # do something with Accessors to make a GET string, maybe ?id=A('pk')
class Meta:
model = MyModel
def render_id(self, record):
url = reverse('rqGET')
return format_html('{}', url, record.id, 'Link')
This will render a link with format /rqGET/?id=<id>.
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').
I'm pretty new at Django and wondering what is the difference between defining model vs queryset in a generic view like ListView. Here's my code example in my urls.py file for the project:
urlpatterns = patterns('',
url(r'^$', ListView.as_view(
model=Person,
context_object_name='people',
template_name='index.html',
)),
)
I've also used the same this:
urlpatterns = patterns('',
url(r'^$', ListView.as_view(
queryset=Person.objects.all,
context_object_name='people',
template_name='index.html',
)),
)
And received the same result on my view. I'm assuming there are different things you can do with a queryset?
Using model=Person or queryset=Person.objects.all give the same result.
Let's look at the code. A ListView has the following method:
def get_queryset(self):
"""
Get the list of items for this view. This must be an interable, and may
be a queryset (in which qs-specific behavior will be enabled).
"""
if self.queryset is not None:
queryset = self.queryset
if hasattr(queryset, '_clone'):
queryset = queryset._clone()
elif self.model is not None:
queryset = self.model._default_manager.all()
else:
raise ImproperlyConfigured(u"'%s' must define 'queryset' or 'model'"
% self.__class__.__name__)
return queryset
As you can see, it first looks for self.queryset and, if that does not exist, for self.model. So there are two possibilities to specify a list: you can provide a queryset yourself or you can specify a model class (in which case Django will call the all() method of the default manager, which is objects).
I'm assuming there are different things you can do with a queryset?
Yes. If you specify a model, then you get all instances by default. But if you specify a queryset, you can also call other methods of a model manager, such as Person.objects.children() which could return only persons with age <= 12.