Django URL dispatcher - Try next view - python

Alright, let me give you guys an example;
We have the following url configuration in Django.
Django will try to match the url with the rules down below. Once it finds a match, it will use the appropriate view and lookup the object in the model.
The thing is, once it finds a match in the URL pattern, it will match the view. But once the object
in the view can't be found, it will return a page not found (404) error.
urls.py
from django.urls import path
from . import views
urlpatterns = [
path('articles/<slug:category>/<slug:editor>/', views.ArticleByThemeView.as_view(), name='articles_by_editor'),
path('articles/<slug:category>/<slug:theme>/', views.ArticleDetailView.as_view(), name='articles_by_theme')
]
views.py
class ArticleByThemeView(ListView):
"""
List all articles by a certain theme; "World War 2".
"""
model = Article
def dispatch(self, request, *args, **kwargs):
try:
# Check if the theme_slug matches a theme
theme = ArticleTheme.objects.get(slug=self.kwargs['theme_slug'])
except ArticleTheme.DoesNotExist:
# Theme does not exist, slug must be an article_slug
return redirect(
'article_detail',
category_slug=category_slug
article_slug=theme_slug
)
return super().dispatch(request, *args, **kwargs)
class ArticleDetailView(DetailView):
"""
Detailview for a certain article
"""
model = Article
def get_object(self):
return get_object_or_404(
Article,
category__slug=self.kwargs['category_slug'],
slug=self.kwargs['article_slug']
)
We have the following url patterns, we can sort articles either by the editor or by theme. We do this to create a logical url structure for SEO purposes.
Is their any way we can redirect to another view once the object isn't found?
Can we modify the dispatch method to return to the url patterns and find the following matching rule?

What about redirection like this:
def articles_by_editor(request, category, editor):
try:
article = Article.objects.get(category=category, editor=editor)
# return article
except Article.DoesNotExist:
# redirect to another view
return redirect('articles_by_theme', category=category)

Alright,
Based on the suggestion from Sunderam Dubey, I'wrote a function view, which uses two differn routes to the same view.
urls.py
from django.urls import path
from . import views
urlpatterns = [
path('articles/<slug:category>/<slug:slug>/', views.article_theme_or_detail_view, name='article_by_theme'),
path('articles/<slug:category>/<slug:slug>/', views.article_theme_or_detail_view, name='article_detail')
]
views.py
def article_theme_or_detail_view(
request,
category_slug,
slug=None
):
"""
This view could either be for a theme view or detailview,
depending on the slug.
"""
try:
# Check if the slug represents a theme
theme = ArticleTheme.objects.get(slug=slug)
article_list = Article.object.filter(theme=theme)
# Add context
context = {
'theme': theme,
'article_list': article_list
}
# Render the template with context
return render(
request,
'article_by_theme.html',
context
)
except ArticleTheme.DoesNotExists:
# The theme does not exist so the slug must be for a detail view
context = {
article = Article.objects.get(slug=slug)
}
return render(
request,
'article_detail.html',
context
)
Todo:
Remove one of the url routes

Related

Customize django admin. Problem with no-model views

Im trying to create a extra view in django admin, on the left navbar. This view will be responsible for uploading a file, which will be parsed in some function (in future i would like to render result of this parsing in admin page). This file wont be saved in database, so there wont be a model. Is there any possibility to add a view to django admin (left navbar) which dont have a model? I was reading a lot, and could find a solution. What i have done for now:
Created a class which inherits from AdminSite. I tried to implement get_app_list method, but variable self._build_app_dict(request) was empty array, and this means, method couldn't find a installed aps. I wanted to add new object to app_list variable, to render it on website.
Tried to override a admin templates, but couldnt render it. I tried to override app_index.html which i put on folder: app_name/templates/admin/app_index.html
Here is my code, which ofc doesnt work:
class MyCustomAdmin(AdminSite):
def get_app_list(self, request):
"""
Return a sorted list of all the installed apps that have been
registered in this site.
"""
app_dict = self._build_app_dict(request)
breakpoint()
app_list = sorted(app_dict.values(), key=lambda x: x['name'].lower())
for app in app_list:
app['models'].sort(key=lambda x: x['name'])
return app_list
def get_urls(self):
from django.conf.urls import url
urls = super(MyCustomAdmin, self).get_urls()
urls += [
url(r'^my_custom_view/$', self.admin_view(MyCustomView.as_view()))
]
return urls
class MyCustomView(View):
template_name = 'admin/app_index.html'
def get(self, request):
print('fefef')
return render(request, self.template_name, {})
def post(self, request):
pass
admin_site = MyCustomAdmin()
admin_site.get_app_list(AdminSite.get_app_list)

Django Redirect by Name - No Reverse Match Error

My django project name is 'exercises' and the app name is 'practice'. This basic application allows a user can navigate to different pages with a button click. The reference to each page uses the following href:
{% url 'practice:redirector' project.pk %}
Where practice:redirector points to my app (practice) url path named redirector. It is parameterized with the primary key of the page which I am trying to redirect to. See below
from django.urls import path
from practice import views
app_name = "practice"
urlpatterns = [
path('', views.all_sites, name="all_sites"),
path('<int:pk>', views.redirector, name='redirector'),
path('clickToAddItems', views.clickToAddItems, name='clickToAddItems'),
path('displayEvens', views.displayEvens, name='displayEvens'),
path('displayMousePosition', views.displayMousePosition, name='displayMousePosition'),
path('luckySevens', views.luckySevens, name='luckySevens'),
]
This url points to the views within practice. There, I have a view set up for each page that corresponds to the name that the page will display. I also have a re director view that uses the pk passed in the url to pull the relevant page from my database and use the name as the argument in the redirect() shortcut:
from django.shortcuts import render, redirect
from practice.models import Project
app_name = "practice"
# Create your views here.
# Main list of javascript sites
def all_sites(request):
# query my project model to return all site objects
projects = Project.objects.all()
# return a rendered template with the site information passed into it
return render(request, 'practice/all_sites.html', {'projects':projects})
def clickToAddItems(request):
return render(request, 'practice/clickToAddItems')
def displayEvens(request):
return render(request, 'practice/displayEvens')
def displayMousePosition(request):
return render(request, 'practice/displayMousePosition')
def luckySevens(request):
return render(request, 'practice/luckySevens')
def redirector(request, pk):
project = Project.objects.get(pk=pk)
site_name=project.title
return redirect(site_name, permanent=True)
I am expecting django to use the redirector view to redirect depending on the provided name. Using the debugger, I can see that the database is correctly queried and the correct name is passed to the redirect with each url:
C:\Users\User\my_repository\my_repository\Dev\small-projects\practice\views.py, line 29, in redirector
def luckySevens(request):
return render(request, 'practice/luckySevens')
def redirector(request, pk):
project = Project.objects.get(pk=pk)
site_name=project.title
return redirect(site_name, permanent=True)
# Local vars:
#(These are the local variables tracked by the debugger, not project code)
pk = 1
project = <Project: Project object (1)>
request = <WSGIRequest: GET '/practice/1'>
site_name = 'clickToAddItems'
However, I get a NoReverseMatch error because django is using the project url file instead of my application url file to find the corresponding view.
C:\Users\User\my_repository\my_repository\Dev\small-projects\venv\lib\site-packages\django\urls\base.py, line 88, in reverse
else:
raise NoReverseMatch("%s is not a registered namespace" % key)
if ns_pattern:
resolver = get_ns_resolver(
ns_pattern, resolver, tuple(ns_converters.items())
)
return resolver._reverse_with_prefix(view, prefix, *args, **kwargs) …
reverse_lazy = lazy(reverse, str)
def clear_url_caches():
# Local vars:
#(These are the local variables tracked by the debugger, not project code)
args = []
current_app = None
current_path = None
kwargs = {}
ns_converters = {}
ns_pattern = ''
path = []
prefix = '/'
resolved_path = []
resolver = <URLResolver 'exercises.urls' (None:None) '^/'>
urlconf = 'exercises.url'
view = 'clickToAddItems'
viewname = 'clickToAddItems'
I need the resolver to look at 'practice.urls' instead of 'exercises.urls'. How can I do this? I have tried parameterizing urlconf=practice.urls within the redirect shortcut of the redirector view, but that does not work. Any and all feed back is very much appreciated! Thank you :)
if you want to redirect to
site_name = 'clickToAddItems'
as in your debug example, you should name the app:
site_name = 'practice:clickToAddItems'

Django Urls - two views with same regular expression

I have two views with the same regular expression, as you can see below. It's a Category and an Article view, their entry slugs will never be the same so it should be no problem. But at the moment it doesn't work well, as you prolly know the category-view will get triggered.
Please do not suggest to make the url structure unique, the slugs of categories and articles will never be the same. It should be as short as possible.
urls.py
urlpatterns = [
url(r'^index', index.Index.as_view(), name='index'),
url(r'^search', search.Index.as_view(), name='search'),
url(r'^(?P<slug>.+)$', category.Index.as_view(), name='category'),
url(r'^(?P<slug>.+)$', article.Index.as_view(), name='article'),
]
I tried to reverse from views.category back to urls.py if there is no category to find like this:
views.category.py
class Index(View):
def get(self, request, slug):
category = CategoryModel.objects.get(slug=slug)
if category is None:
return HttpResponseRedirect(reverse('article', args=[slug]))
context = {
'category': category
}
return render(request, 'category/index.html', context)
The error (but there is a article with slug 'test123'):
NoReverseMatch at /wiki/test123
Reverse for 'article' with arguments '('test123',)' and keyword arguments '{}' not found. 0 pattern(s) tried: []
Using Python 3.6
Why dont you try differentiating the URLs like so
urlpatterns = [
url(r'^index', index.Index.as_view(), name='index'),
url(r'^search', search.Index.as_view(), name='search'),
url(r'^category/(?P<slug>.+)$', category.Index.as_view(), name='category'),
url(r'^article/(?P<slug>.+)$', article.Index.as_view(), name='article'),
]
You get to use the same regular expressions without the URL being ambiguous.
You can remove the article.Index view and rather than trying to redirect when there's no object for Category, you can call the method you defined in article.Index with the same parameters as the get method takes in article.Index view.
Example:
urls.py
urlpatterns = [
url(r'^index', index.Index.as_view(), name='index'),
url(r'^search', search.Index.as_view(), name='search'),
url(r'^(?P<slug>.+)$', category.Index.as_view(), name='category'),
# article url removed
]
views.category.py
from path.to.article import Index as ArticleIndexView
class Index(View):
def get(self, request, slug):
category = CategoryModel.objects.get(slug=slug)
if category is None:
# calling article app's Index get method
article_index_view_obj = ArticleIndexView()
return article_index_view_obj.get(request, slug)
context = {
'category': category
}
return render(request, 'category/index.html', context)
If you make the article.Index class view as function-based view.
You can import from path.to.article import index as article_index
and then, instead of instantiating the object, you can directly call article_index(request, slug).
There are several issues here.
(1) You call reverse with args but you have specified a kwarg.
if category is None:
return HttpResponseRedirect(reverse('article', args=[slug]))
Reverse for 'article' with arguments '('test123',)' and keyword arguments '{}' not found.
Says exactly that - as you did not provide the keyword argument, it could not find the matching URL pattern. This would be correct:
if category is None:
return HttpResponseRedirect(reverse('article', kwargs={'slug':slug}))
(2) You will end up in that same code again and again - is what I expect to happen once you fix (1). Because if reverse really does reverse the URL - the result of reverse will of course also match the category URL which means it will simply call the category.Index view - again.
I think your URL setup could actually work because the resolver does try all URLs sequentially until one comes along that matches. I'm just not sure if you can make the view return something that will lead to the URL resolver to kick in and decide to take the next URL (article) instead of category which just resolved. Probably not.
In this case, if you are fine with redirects, you could just define 3 URL patterns. 1 for the view that will operate as a switch and redirect to the CategoryView or ArticleView respectively.
Otherwise go with Sachin Kukreja's solution of handling both in one view.
Finally I made it like this. Actually I wanna use 2 different views but I guess it's fine too. Does someone see a mistake?
class Index(View):
def get(self, request, slug):
self.request = request
self.slug = slug
self.item = None
is_article = ArticleModel.objects.filter(slug=self.slug).exists()
if is_article:
self.item = 'article'
return self.article()
is_category = CategoryModel.objects.filter(slug=self.slug).exists()
if is_category:
self.item = 'category'
return self.category()
self.item = None
# 404 here
return HttpResponse(self.item)
def article(self):
article = ArticleModel.objects.get(slug=self.slug)
context = {
'article': article
}
return render(self.request, 'article/index.html', context)
def category(self):
category = CategoryModel.objects.get(slug=self.slug)
context = {
'category': category
}
return render(self.request, 'article/index.html', context)

Django 1.9 : Passing arguments from urls.py to a class view

I am creating a small web application as a mini project of mine to learn the Django framework. I'm on Version 1.9.4, on OS X. I'm trying to pass a string in the URL that will be sent to a class-based view, and it will return a different template based on the URL. To my knowledge, doing (?P) will allow the input of dynamic text. \w is for characters, and writing <name> will pass it as a variable. Is this configured right, or is this is not the correct way to do it?
The reason I'm concerned is that the Django documentation uses method views, while I am using class-based views.
urls.py
from django.conf.urls import url
from . import views
app_name = 'xyz'
urlpatterns = [
url(r'^create/(?P<ty>\w+)$', views.ArticleView.as_view(), name='article-form'), #.as_view() to turn Class into View
]
views.py
class ArticleCreate(View):
l = {
'weapon': WeaponForm,
'map': MapForm,
'operator': OperatorForm,
'gadget': GadgetForm,
'skin': SkinForm
}
ty = ty.lower()
template_name = 'xyz/create_article_form.html'
def get(self, request):
return render(request, self.template_name)
def post(self, request):
pass
The arguments that are being passed to the url should be "catched" within the view inside the relevant function, for example:
def get(self, request, ty):
ty = ty.lower()
return render(request, self.template_name)

How to convert a Django ListView to work on the admin site?

I have a list view (for the admin site) that uses a template as follows:
class UserImageListPendingView(ListView):
model = UserImage
queryset = UserImage.objects.filter(status=ImageBase.PENDING)
template_name = 'userimage_list_pending.html'
context_object_name = 'userimage_list'
paginate_by = 5
#method_decorator(staff_member_required)
def dispatch(self, *args, **kwargs):
return super(UserImageListPendingView, self).dispatch(*args, **kwargs)
Although this works there are problems with putting the URL in urls.py:
urlpatterns = [
url(r'^admin/app/pendinguserimages/?$', login_required(
UserImageListPendingView.as_view()),
name='pendinguserimages'),
...
]
...as this stops the redirection working properly.
I did try to define the URL through admin.py:
def get_admin_urls(urls):
def get_urls():
return patterns('',
url(r'^app/pendinguserimages/?$',
UserImageListPendingView.as_view(), name='pendinguserimages'),
url(r'^app/checkuserimage/(?P<pk>[0-9]+)/?$',
userimage_check, name='checkuserimage'),
...
) + urls
return get_urls
admin_urls = get_admin_urls(admin.site.get_urls())
admin.site.get_urls = admin_urls
... but there was an error when reversing the checkuserimage URL.
How would I go about converting this view to fit in better with the admin site, but still use the template?
I didn't need to rewrite the ListView afterall. After defining the URLs in admin.py instead of in urls.py, all I needed to do was put "admin:" in front of the name when reversing the URL in the template, as follows:
check

Categories