Is it possible to add 2nd slug to URL path in Django? - 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']
)

Related

'Post' object has no attribute 'get_absolute_url' Django

I am trying to create a sitemap in Django but I am getting an error
'Post' object has no attribute 'get_absolute_url'
Here is my anotherfile/sitemap.py
from django.contrib.sitemaps import Sitemap
from somefile.models import Post
class site_map(Sitemap):
changefreq = "daily"
priority = 0.8
def items(self):
return Post.objects.all()
def lastmod(self, obj):
return obj.time_stamp
and here is my somefile/models.py
class Post(models.Model):
number=models.AutoField(primary_key=True)
slug=models.CharField(max_length=130)
time_stamp=models.DateTimeField(blank=True)
def __str__(self):
return self.number
In order to determine the paths in the sitemap, you need to implement a get_absolute_url for the model(s) for which you make a sitemap, so:
from django.urls import reverse
class Post(models.Model):
number=models.AutoField(primary_key=True)
slug=models.CharField(max_length=130)
time_stamp=models.DateTimeField(blank=True)
def get_absolute_url(self):
return reverse('name-of-some-view', kwargs={'para': 'meters'})
def __str__(self):
return self.number
With reverse(…) [Django-doc] you can calculate the URL based on the name of the view, and parameters that the corresponding path needs.
I've never used it, but it simply sounds like your Post model doesn't have a get_absolute_url method.
http://docs.djangoproject.com/en/dev/ref/contrib/sitemaps/#django.contrib.sitemaps.Sitemap.location
If location isn't provided, the framework will call the
get_absolute_url() method on each object as returned by items().
Model.get_absolute_url()
Define a get_absolute_url() method to tell Django how to calculate the canonical URL for an object. To callers, this method should appear to return a string that can be used to refer to the object over HTTP.
For example:
def get_absolute_url(self):
return "/post/%i/" % self.id

forking django-oscar views shows an error

I forked customer app , to add a tab in http://oscar/accounts/...(example products)
to edit/show catalogue Views (Dashboard>Catalogue)
Error that I get to use that view is
django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet.
I used views with the same method for payment checkout, but this one runs into errors.
# yourappsfolder.customer.apps.py
import oscar.apps.customer.apps as apps
from oscar.apps.dashboard.catalogue import apps as dapps
from django.views import generic
from django.conf.urls import url
from oscar.core.loading import get_class
from .views import ProductListView
class CustomerConfig(apps.CustomerConfig):
name = 'yourappsfolder.customer'
def ready(self):
super().ready()
self.extra_view =ProductListView
def get_urls(self):
urls = super().get_urls()
urls += [
url(r'products/',self.extra_view.as_view(),name='Products'),
]
return self.post_process_urls(urls)
This is the view I copied from oscar.apps.dashboard.catalogue
# yourappsfolder.customer.views
from django.shortcuts import render
from django.http import HttpResponse
# Create your views here.
from django.views import generic
from oscar.apps.dashboard.catalogue.views import ProductListView as UserProductListView
class ProductListView(UserProductListView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['form'] = self.form
ctx['productclass_form'] = self.productclass_form_class()
return ctx
def get_description(self, form):
if form.is_valid() and any(form.cleaned_data.values()):
return _('Product search results')
return _('Products')
def get_table(self, **kwargs):
if 'recently_edited' in self.request.GET:
kwargs.update(dict(orderable=False))
table = super().get_table(**kwargs)
table.caption = self.get_description(self.form)
return table
def get_table_pagination(self, table):
return dict(per_page=20)
def filter_queryset(self, queryset):
"""
Apply any filters to restrict the products that appear on the list
"""
return filter_products(queryset, self.request.user)
def get_queryset(self):
"""
Build the queryset for this list
"""
queryset = Product.objects.browsable_dashboard().base_queryset()
queryset = self.filter_queryset(queryset)
queryset = self.apply_search(queryset)
return queryset
def apply_search(self, queryset):
"""
Search through the filtered queryset.
We must make sure that we don't return search results that the user is not allowed
to see (see filter_queryset).
"""
self.form = self.form_class(self.request.GET)
if not self.form.is_valid():
return queryset
data = self.form.cleaned_data
if data.get('upc'):
# Filter the queryset by upc
# For usability reasons, we first look at exact matches and only return
# them if there are any. Otherwise we return all results
# that contain the UPC.
# Look up all matches (child products, products not allowed to access) ...
matches_upc = Product.objects.filter(upc__iexact=data['upc'])
# ... and use that to pick all standalone or parent products that the user is
# allowed to access.
qs_match = queryset.filter(
Q(id__in=matches_upc.values('id')) | Q(id__in=matches_upc.values('parent_id')))
if qs_match.exists():
# If there's a direct UPC match, return just that.
queryset = qs_match
else:
# No direct UPC match. Let's try the same with an icontains search.
matches_upc = Product.objects.filter(upc__icontains=data['upc'])
queryset = queryset.filter(
Q(id__in=matches_upc.values('id')) | Q(id__in=matches_upc.values('parent_id')))
if data.get('title'):
queryset = queryset.filter(title__icontains=data['title'])
return queryset
You have a circular import - move the import of the list view into the ready() method of the app config:
class CustomerConfig(apps.CustomerConfig):
name = 'yourappsfolder.customer'
def ready(self):
super().ready()
from .views import ProductListView
self.extra_view =ProductListView

Django Admin - Custom changelist view

I need to add a custom view to the Django Admin. This should be similar to a standard ChangeList view for a certain model, but with a custom result set. (I need to display all models having some date or some other date less than today, but this is not really relevant).
One way I can do this is by using the Admin queryset method, like
class CustomAdmin(admin.ModelAdmin):
...
def queryset(self, request):
qs = super(CustomAdmin, self).queryset(request)
if request.path == 'some-url':
today = date.today()
# Return a custom queryset
else:
return qs
This makes sure that ...
The problem is that I do not know how to tie some-url to a standard ChangeList view.
So you want a second URL that goes to the changelist view so you can check which of the two it was by the requested URL and then change the queryset accordingly?
Just mimick what django.contrib.admin.options does and add another URL to the ModelAdmin.
Should look something like this:
class CustomAdmin(admin.ModelAdmin):
def get_urls(self):
def wrap(view):
def wrapper(*args, **kwargs):
kwargs['admin'] = self # Optional: You may want to do this to make the model admin instance available to the view
return self.admin_site.admin_view(view)(*args, **kwargs)
return update_wrapper(wrapper, view)
# Optional: only used to construct name - see below
info = self.model._meta.app_label, self.model._meta.module_name
urlpatterns = patterns('',
url(r'^my_changelist/$', # to your liking
wrap(self.changelist_view),
name='%s_%s_my_changelist' % info)
)
urlpatterns += super(CustomAdmin, self).get_urls()
return urlpatterns

Django Admin linking to related objects

My app has users who create pages. In the Page screen of the admin, I'd like to list the User who created the page, and in that list, I'd like the username to have a link that goes to the user page in admin (not the Page).
class PageAdmin(admin.ModelAdmin):
list_display = ('name', 'user', )
list_display_links = ('name','user',)
admin.site.register(Page, PageAdmin)
I was hoping that by making it a link in the list_display it would default to link to the actual user object, but it still goes to Page.
I'm sure I'm missing something simple here.
Modifying your model isn't necessary, and it's actually a bad practice (adding admin-specific view-logic into your models? Yuck!) It may not even be possible in some scenarios.
Luckily, it can all be achieved from the ModelAdmin class:
from django.urls import reverse
from django.utils.safestring import mark_safe
class PageAdmin(admin.ModelAdmin):
# Add it to the list view:
list_display = ('name', 'user_link', )
# Add it to the details view:
readonly_fields = ('user_link',)
def user_link(self, obj):
return mark_safe('{}'.format(
reverse("admin:auth_user_change", args=(obj.user.pk,)),
obj.user.email
))
user_link.short_description = 'user'
admin.site.register(Page, PageAdmin)
Edit 2016-01-17:
Updated answer to use make_safe, since allow_tags is now deprecated.
Edit 2019-06-14:
Updated answer to use django.urls, since as of Django 1.10 django.core.urls has been deprecated.
Add this to your model:
def user_link(self):
return '%s' % (reverse("admin:auth_user_change", args=(self.user.id,)) , escape(self.user))
user_link.allow_tags = True
user_link.short_description = "User"
You might also need to add the following to the top of models.py:
from django.template.defaultfilters import escape
from django.core.urls import reverse
In admin.py, in list_display, add user_link:
list_display = ('name', 'user_link', )
No need for list_display_links.
You need to use format_html for modern versions of django
#admin.register(models.Foo)
class FooAdmin(admin.ModelAdmin):
list_display = ('ts', 'bar_link',)
def bar_link(self, item):
from django.shortcuts import resolve_url
from django.contrib.admin.templatetags.admin_urls import admin_urlname
url = resolve_url(admin_urlname(models.Bar._meta, 'change'), item.bar.id)
return format_html(
'{name}'.format(url=url, name=str(item.bar))
)
I ended up with a simple helper:
from django.shortcuts import resolve_url
from django.utils.safestring import SafeText
from django.contrib.admin.templatetags.admin_urls import admin_urlname
from django.utils.html import format_html
def model_admin_url(obj: Model, name: str = None) -> str:
url = resolve_url(admin_urlname(obj._meta, SafeText("change")), obj.pk)
return format_html('{}', url, name or str(obj))
Then you can use the helper in your model-admin:
class MyAdmin(admin.ModelAdmin):
readonly_field = ["my_link"]
def my_link(self, obj):
return model_admin_url(obj.my_foreign_key)
I needed this for a lot of my admin pages, so I created a mixin for it that handles different use cases:
pip install django-admin-relation-links
Then:
from django.contrib import admin
from django_admin_relation_links import AdminChangeLinksMixin
#admin.register(Group)
class MyModelAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
# ...
change_links = ['field_name']
See the GitHub page for more info. Try it out and let me know how it works out!
https://github.com/gitaarik/django-admin-relation-links
I decided to make a simple admin mixin that looks like this (see docstring for usage):
from django.contrib.contenttypes.models import ContentType
from django.utils.html import format_html
from rest_framework.reverse import reverse
class RelatedObjectLinkMixin(object):
"""
Generate links to related links. Add this mixin to a Django admin model. Add a 'link_fields' attribute to the admin
containing a list of related model fields and then add the attribute name with a '_link' suffix to the
list_display attribute. For Example a Student model with a 'teacher' attribute would have an Admin class like this:
class StudentAdmin(RelatedObjectLinkMixin, ...):
link_fields = ['teacher']
list_display = [
...
'teacher_link'
...
]
"""
link_fields = []
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.link_fields:
for field_name in self.link_fields:
func_name = field_name + '_link'
setattr(self, func_name, self._generate_link_func(field_name))
def _generate_link_func(self, field_name):
def _func(obj, *args, **kwargs):
related_obj = getattr(obj, field_name)
if related_obj:
content_type = ContentType.objects.get_for_model(related_obj.__class__)
url_name = 'admin:%s_%s_change' % (content_type.app_label, content_type.model)
url = reverse(url_name, args=[related_obj.pk])
return format_html('{}', url, str(related_obj))
else:
return None
return _func
If anyone is trying to do this with inline admin, consider a property called show_change_link since Django 1.8.
Your code could then look like this:
class QuestionInline(admin.TabularInline):
model = Question
extra = 1
show_change_link = True
class TestAdmin(admin.ModelAdmin):
inlines = (QuestionInline,)
admin.site.register(Test, TestAdmin)
This will add a change/update link for each foreign key relationship in the admin's inline section.

How Do I Show Django Admin Change List View of foreign key children?

I'm working on an app with a Model hierarchy of Campaign > Category > Account. Ideally, I'd like users to be able to click on a link in the campaign admin list view and go to a URL like "/admin/myapp/campaign/2/accounts/" which will show a Django admin view with all the handy ChangeList amenities but which is filtered to show just the accounts in categories in the specified campaign (ie. Account.object.filter(category__campaign__id = 2)). (Note, categories themselves I'm happy to just be "filters" on this accounts list view).
I can't seem to find any reference to a way to mimic this item-click-goes-to-list-of-foriegn-key-children approach that is common in many other frameworks.
Is it possible? Is there a "better" approach in the django paradigm?
thanks for any help!
This was an interesting question so I whipped up a sample app to figure it out.
# models.py
from django.db import models
class Campaign(models.Model):
name = models.CharField(max_length=20)
def __unicode__(self):
return unicode(self.name)
class Category(models.Model):
campaign = models.ForeignKey(Campaign)
name = models.CharField(max_length=20)
def __unicode__(self):
return unicode(self.name)
class Account(models.Model):
category = models.ForeignKey(Category)
name = models.CharField(max_length=20)
def __unicode__(self):
return unicode(self.name)
# admin.py
from django.contrib import admin
from models import Campaign, Category, Account
class CampaignAdmin(admin.ModelAdmin):
list_display = ('name', 'related_accounts', )
def related_accounts(self, obj):
from django.core import urlresolvers
url = urlresolvers.reverse("admin:<yourapp>_account_changelist")
lookup = u"category__campaign__exact"
text = u"View Accounts"
return u"<a href='%s?%s=%d'>%s</a>" % (url, lookup, obj.pk, text)
related_accounts.allow_tags = True
admin.site.register(Campaign, CampaignAdmin)
admin.site.register(Category)
class AccountAdmin(admin.ModelAdmin):
list_display = ('category', 'name')
list_filter = ('category__campaign',)
admin.site.register(Account, AccountAdmin)
You'll need to replace with the name of your app where the Account ModelAdmin lives.
Note: the list_filter on the AccountAdmin is required since Django 1.2.4, Django 1.1.3 and Django 1.3 beta 1, which introduced protection from arbitrary filtering via URL parameter in the admin.
If i understand you correctly, you want to add a custom field (a callable in your ModelAdmin's list_display) to your CampaignAdmin change_list view.
Your custom field would be a link that takes the category.id of each category in your change_list and generates a link to the desired, filtered admin view, which seems to be the account-change_list in your case:
admin/yourproject/account/?category__id__exact=<category.id>
Assuming category is a field on your Campaign-Model you could add the follwoing method to your CampaignAdmin:
def account_link(self, obj):
return 'Accounts' % (obj.category.id)
account_link.allow_tags = True
And then you add it to the admin's list_display option:
list_display = ('account_link', ...)
It depends a bit on your data model though.
If you want to create a permanent, filtered change_list view that suits your needs, you may take a look at this article: http://lincolnloop.com/blog/2011/jan/11/custom-filters-django-admin/
The other solutions don't pay attention to the filters you already have applied. They are part of the query string and I wanted to retain them as well.
First you need to get a reference to the request, you can do that by wrapping changelist_view or queryset as I did:
class AccountAdmin(ModelAdmin):
model = Account
list_display = ('pk', 'campaign_changelist')
# ...
def queryset(self, request):
self._get_params = request.GET
return super(AccountAdmin, self).queryset(request)
def campaign_changelist(self, obj):
url = reverse('admin:yourapp_account_changelist')
querystring = self._get_params.copy()
querystring['campaign__id__exact'] = obj.campaign.pk
return u'{2}'.format(
url, querystring.urlencode(), obj.campaign)
campaign_changelist.allow_tags = True
And something like that will give you a filter inside the changelist rows. Really helpful. :-)
These are good solutions. I wasn't aware of the auto-filter by url paradigm. Here's another I've discovered which allows you use a custom url scheme:
from consensio.models import Account
from django.contrib import admin
from django.conf.urls.defaults import patterns, include, url
class AccountAdmin(admin.ModelAdmin):
campaign_id = 0;
def campaign_account_list(self, request, campaign_id, extra_context=None):
'''
First create your changelist_view wrapper which grabs the extra
pattern matches
'''
self.campaign_id = int(campaign_id)
return self.changelist_view(request, extra_context)
def get_urls(self):
'''
Add your url patterns to get the foreign key
'''
urls = super(AccountAdmin, self).get_urls()
my_urls = patterns('',
(r'^bycampaign/(?P<campaign_id>\d+)/$', self.admin_site.admin_view(self.campaign_account_list))
)
return my_urls + urls
def queryset(self, request):
'''
Filter the query set based on the additional param if set
'''
qs = super(AccountAdmin, self).queryset(request)
if (self.campaign_id > 0):
qs = qs.filter(category__campaign__id = self.campaign_id)
return qs
And plus you'd need to incorporate the URL link into CampaignAdmin's list view...