Acquire model instance as view parameter instead of model id - django

In laravel, it's possible to have access to entire models with type-hinting the parameter with a model class. for example:
routes/web.php
Route::get('posts/{post}', function(\App\Post $post) {
/*
$post is now the model with pk in url and
if the pk is wrong it throws an 404 automatically
*/
});
How is that possible in django? (with functions as view)
def posts(request, post_id):
post = get_model_or_404(Post, pk=post_id)
# ...
The second line is completely boilerplate and so much repeated in my code.

You can make a custom path converter [Django-doc] for that. We can for example make model path converter:
# app/converters.py
from django.shortcuts import get_object_or_404
from django.urls.converters import IntConverter
class ModelConverter(IntConverter):
model = None
def to_python(self, value):
return get_object_or_404(self.model, pk=super().to_python(value))
def to_url(self, value):
if instanceof(value, int):
return str(value)
return str(value.pk)
def model_converter(model):
class Converter(ModelConverter):
model = model
return Converter
Then in your urls.py you can make model converters and register these once:
# app/urls.py
from app.converters import model_converter
from app import views
from app.models import Post
from django.urls import path, register_converter
register_converter(model_converter(Post), 'post')
urlpatterns = [
path('posts/<post:post>', views.post, name='post'),
# …
]
Then in the view, you obtain the Post object (given a post for that primary key exists) through the post parameter:
app/views.py
def post(request, post):
# post is here a Post object
# …
In your template, you can then just pass the Post object in the {% url … %} template tag, so:
<a href="{% url 'post' mypostobject %}">

Related

Image response from DetailView in Django

I'd like to have a DetailView respond with an image file that is saved to the model/object.
I have seen this related question but what I am doing is a little different.
I have the following model/views/urls - this allows me to display the image in an img tag inside a template when accessing the url localhost/MY-SKU-FIELD. I'd rather not use a template and just respond with the image directly.
# models.py
from django.db import models
class SignModel(models.Model):
sku = models.CharField('SKU', max_length=50)
sign_image = models.ImageField(upload_to='images/sign')
# views.py
from django.views.generic import DetailView
from signs.models import SignModel
class SignDetailView(DetailView):
model = SignModel
template_name = 'signs/sign_detail.html'
slug_url_kwarg = 'sku'
slug_field = 'sku'
# urls.py
from django.urls import path
from signs.views import SignDetailView
urlpatterns = [
path('<slug:sku>', SignDetailView.as_view(), name='sign-detail'),
]
{# 'signs/templates/sign_detail.html #}
<img src="{{object.sign_image.url}}" alt="">
Based on the related question/answer, I figure I need to define my own get() method, then fetch the image, and then respond accordingly, but I'm not quite sure how to fetch the image based on the object fetched in DetailView. I've tried fetching with r = requests.get(self.sign_image.url) but that is clearly not working.
How do I get the image and respond correctly?
I haven't done this before but I was able to quickly check, and you can give the value of the field to a django.http.FileResponse object. As for the view, I think you want to overwrite the render_to_response method.
from django.http import FileResponse
class SignDetailView(DetailView):
model = SignModel
template_name = 'signs/sign_detail.html'
slug_url_kwarg = 'sku'
slug_field = 'sku'
def render_to_response(self, context, **kwargs):
return FileResponse(self.object.sign_image)

How to redirect an UpdateView upon success?

I created a small Django application to manage data that fits a simple a model. For now I only need two views: one to list all records and another to edit a record with a generic form. Everything functions as expected, except the redirection from the edit view upon a successful update. In urls.py are the following contents:
from django.urls import path
from . import views
app_name = 'reqs'
urlpatterns = [
path('', views.IndexView.as_view(), name='index'),
path('<int:pk>/', views.ReqUpdateView.as_view(), name='update'),
]
In forms.py:
from django.forms import ModelForm
from .models import Requirement
class RequirementForm(ModelForm):
class Meta:
model = Requirement
fields = ['name', 'priority', 'source' , 'rationale']
And the templeate requirement_form.html:
<h1>{{ requirement.id }} - {{ requirement.name }}</h1>
<form method="post" novalidate>
{% csrf_token %}
<table>
{{ form.as_table }}
<tr><td></td><td><button type="submit">Save</button></td></tr>
</table>
</form>
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
<br><br>
Back to list
Finally views.py, on a first attempt to redirect the update to the list:
from django.views.generic import ListView, UpdateView
from django.urls import reverse_lazy
from .models import Requirement
from .forms import RequirementForm
class IndexView(ListView):
template_name = 'reqs/index.html'
context_object_name = 'requirements_list'
def get_queryset(self):
return Requirement.objects.order_by('subject')
class ReqUpdateView(UpdateView):
model = Requirement
form_class = RequirementForm
success_url = reverse_lazy('/')
With this formulation the Save button produces this error:
Reverse for '/' not found. '/' is not a valid view function or pattern name.
I also tried an empty string as argument to reverse_lazy, as well as the path name index, but a similar error message is produced.
On a second attempt I tried to redirect to the same page, redefining the get_success_url method to do nothing:
class ReqUpdateView(UpdateView):
model = Requirement
form_class = RequirementForm
context_object_name = 'requirement_update'
def get_success_url(self):
pass #return the appropriate success url
This returns a 404 error trying to redirect the browser to /reqs/1/None.
A third attempt to redirect to the form with the same record:
class ReqUpdateView(UpdateView):
model = Requirement
form_class = RequirementForm
context_object_name = 'requirement_update'
def get_success_url(self):
pk = self.kwargs["pk"]
return reverse("update", kwargs={"pk": pk})
Which complains about not finding the view:
Reverse for 'update' not found. 'update' is not a valid view function or pattern name.
How can I redirect success to a valid URL? It can either be the items list or the item update view, as long as it works.
There are few misconception that you did
reverse parameter should be as documented
URL pattern name or the callable view object
You have set namespace but you are not reversing with namespace as documented
So in your case
def get_success_url(self):
pk = self.kwargs["pk"]
return reverse("reqs:update", kwargs={"pk": pk})
reverse / reverse_lazy are used to get the url using view name or pattern name. If you want to use a url directly just write:
success_url = '/'
For the case of return reverse("update", kwargs={"pk": pk}) not working since you set app_name = 'reqs' you should be using return reverse("reqs:update", kwargs={"pk": pk}) instead.

Is it possible to add 2nd slug to URL path in Django?

I'm using Django version 2.1.
I want to create this type of URL Path in my Project:
www.example.com/bachelor/germany/university-of-frankfurt/corporate-finance
Is it possible to do it in Django?
Yes, say for example that you have a slug for an Author, and one for a Book, you can define it as:
# app/urls.py
from django.urls import path
from app.views import book_details
urlpatterns = [
path('book/<slug:author_slug>/<slug:book_slug>/', book_details),
]
Then the view looks like:
# app/views.py
from django.http import HttpResponse
def book_details(request, author_slug, book_slug):
# ...
return HttpResponse()
The view thus takes two extra parameters author_slug (the slug for the author), and book_slug (the slug for the book).
If you thus query for /book/shakespeare/romeo-and-juliet, then author_slug will contains 'shakespeare', and book_slug will contain 'romeo-and-juliet'.
We can for example look up that specific book with:
def book_details(request, author_slug, book_slug):
my_book = Book.objects.get(author__slug=author_slug, slug=book_slug)
return HttpResponse()
Or in a DetailView, by overriding the get_object(..) method [Django-doc]:
class BookDetailView(DetailView):
model = Book
def get_object(self, queryset=None):
super(BookDetailView, self).get_object(queryset=queryset)
return qs.get(
author__slug=self.kwargs['author_slug'],
slug=self.kwargs['book_slug']
)
or for all views (including the DetailView), by overriding the get_queryset method:
class BookDetailView(DetailView):
model = Book
def get_queryset(self):
qs = super(BookDetailView, self).get_queryset()
return qs.filter(
author__slug=self.kwargs['author_slug'],
slug=self.kwargs['book_slug']
)

How to create an instance created from class-based views

I am learning Django's class-based views.
I don't understand what's the instance created from class Views
For example:
from django.http import HttpResponse
from django.views import View
# views.py
class MyView(View):
def get(self, request):
# <view logic>
return HttpResponse('result')
# urls.py
from django.conf.urls import url
from myapp.views import MyView
urlpatterns = [
url(r'^about/$', MyView.as_view()),
]
What's the instance of class 'MyView' during this request-and-respond process?
Although your question is not very clear, I think that I get what you are asking:
Take a look at the source code of url() method:
def url(regex, view, kwargs=None, name=None):
if isinstance(view, (list, tuple)):
# For include(...) processing.
urlconf_module, app_name, namespace = view
return RegexURLResolver(
regex, urlconf_module, kwargs,
app_name=app_name, namespace=namespace
)
...
And a look at the as_view() method documentation:
Returns a callable view that takes a request and returns a response:
response = MyView.as_view()(request)
The returned view has view_class and view_initkwargs attributes.
When the view is called during the request/response cycle, the HttpRequest is assigned to the view’s request attribute.
Therefore because you are passing in the url method the MyView.as_view() argument you are giving the instruction to instantiate the MyView class when a request object gets passed to the url() method.

Django custom tag no longer works

I made a custom tag to display a list of categories and its url which worked until I made a category detail view which will only display articles based on the category.
Here is the category view:
from blog.models import Category, Entry
from django.shortcuts import render_to_response, get_object_or_404
from django.views.generic import list_detail
#for template tag to display all categories
def all_categories(request):
return render_to_response('category_detail.html', {
'categories': Category.objects.all()
})
def category_detail(request, slug):
"""
object_list
List of posts specific to the given category.
category
Given category.
"""
return list_detail.object_list(
request,
queryset = Entry.objects.filter(categories = Category.objects.filter(slug = slug)),
extra_context = {'category': Category.objects.filter(slug = slug)},
template_name = 'blog/category_detail.html',
)
Categories url:
from django.conf.urls.defaults import *
from django.views.generic.list_detail import object_list, object_detail
from blog.views.category import category_detail
from blog.models import Category, Entry
# for category detail, include all entries that belong to the category
category_info = {
'queryset' : Category.objects.all(),
'template_object_name' : 'category',
'extra_context' : { 'entry_list' : Entry.objects.all }
}
urlpatterns = patterns('',
url(r'^$', 'django.views.generic.list_detail.object_list', {'queryset': Category.objects.all() }, 'blog_category_list'),
url(r'^(?P<slug>[-\w]+)/$', category_detail),
)
and the custom category tag:
from django import template
from blog.models import Category
def do_all_categories(parser, token):
return AllCategoriesNode()
class AllCategoriesNode(template.Node):
def render(self, context):
context['all_categories'] = Category.objects.all()
return ''
register = template.Library()
register.tag('get_all_categories', do_all_categories)
Also here is how i am using the custom tag in base.html:
{% load blog_tags %}
<p>
{% get_all_categories %}
<ul>
{% for cat in all_categories %}
<li>{{ cat.title }}</li>
{% endfor %}
</ul>
</p>
Before I added category_detail in the view, the custom tag would display the url correctly like: /categories/news. However, now all of the links from the custom tag just displays the url or the current page your on. Whats weird is that it displays the category name correctly.
Does anyone know how to get the urls to work again?
EDIT
here is my category model, maybe something is wrong with my get_absolute_url():
import datetime
from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import post_save
class Category(models.Model):
title = models.CharField(max_length = 100)
slug = models.SlugField(unique = True)
class Meta:
ordering = ['title']
verbose_name_plural = "Categories"
def __unicode__(self):
return self.title
#models.permalink
def get_absolute_url(self):
return ('category_detail', (), {'slug': self.slug })
EDIT: updated get_absolute_url for Category model, however, that did not fix my problem. Also if i wasnt clear earlier, the category url worked even before changing the get_absolute_url
I am going to bet that get_absolute_url is actually returning an empty string. This would make the links reload the current page. Check out your HTML and look for something like this:
Category Title Example
If the URLs are actually blank, there is probably an error in get_absolute_url. When a Django template encounters an error when outputting a template variable it returns an empty string. Try calling get_absolute_url from a Django shell and see if it returns properly:
Category.objects.all()[0].get_absolute_url()
It looks like you renamed your view from blog_category_detail to category_detail, but forgot to update the reference in get_absolute_url.
Update: It doesn't look like the reverse URL lookup for 'category_detail' would succeed. Your urls file does not name the category_detail URL. You should either change the get_absolute_url reference to app_name.views.category_detail (or wherever it is stored) or name the URL by replacing that last line in your urls file with:
url(r'^(?P<slug>[-\w]+)/$', category_detail, name='category_detail'),
To find the source of the problem, you should debug this code from a command line. Something like this should do:
$ python manage.py shell
>>> from blog.mobels import Category
>>> cat = Category.objects.all()[0]
>>> cat.get_absolute_url()