How do I customize the Wagtail page copy experience? - customization

I have some custom logic (complex unique constraint validation) I would like to check when a user attempts to copy (or move) a certain type of Page in Wagtail. I would also like to give the user an opportunity to change the fields associated with the validation check.
I am aware of the fact that Wagtail exposes a way of customizing the copy (and move) experiences through hooks (http://docs.wagtail.io/en/stable/reference/hooks.html#before-copy-page), but the best I can come up with using that tool is to create a completely new interface and return it in a HttpResponse. Is there a way to merely customize the existing copy (and move) interface for a specific page type?
#hooks.register('before-copy-page')
def before-copy-page(request, page):
return HttpResponse("New copy interface", content_type="text/plain")

These three approaches get you deeper into a customisation for the Wagtail page copy view and validation. You may not need to do all three but the example code below assumes all changes have been done to some extent.
There might be better ways to do the exact thing you want but hopefully this gives you a few ways to customise parts of the entire copy view/form interaction.
These approaches should work for the move pages interaction but that has a few more forms and views.
Overview
1. Override the page copy template
Wagtail provides a way to easily override any admin templates.
Adding a template at templates/wagtailadmin/pages/copy.html will override the copy page form template.
We can also easily extend the original template for the copy page by adding {% extends "wagtailadmin/pages/copy.html" %} at the top, this saves us having to copy/past most of the page and only customise the blocks we need.
Remember {{ block.super }} could come in handy here if you only wanted to add something to the start or end of a block within the template.
In the example code below I have copied the entire content block (will need to be maintained for future releases) and added a custom field.
2. Override the URL and view for page copy
In your urls.py which should be configured to include the Wagtail urls.
Add a new URL path above the admin/ urls, this will be accessed first.
For example url(r'^admin/pages/(\d+)/copy/$', base_views.customCopy, name='copy'),, this will direct the admin copy page to our customCopy view.
This view can be a function or class view and either completely customise the entire view (and template) or just parts of it.
The Wagtail view used here is a function view so it cannot be easily copied, so your customisations are a bit restricted here.
You can see the source for this view in admin/views/pages.py.
3. Monkey patch the Wagtail CopyForm
This may not be ideal, but you can always monkey patch the CopyForm and customise its __init__ or clean methods (or any others as needed).
You can view the source of CopyForm to see what you need to modify, if you wanted to add fields to the form, this (along with the template changes) will be needed.
Code
(1) templates/wagtailadmin/pages/copy.html
{% extends "wagtailadmin/pages/copy.html" %}
{% load i18n %}
{% block content %}
{% comment %} source - wagtail/admin/templates/wagtailadmin/pages/copy.html {% endcomment %}
{% trans "Copy" as copy_str %}
{% include "wagtailadmin/shared/header.html" with title=copy_str subtitle=page.get_admin_display_title icon="doc-empty-inverse" %}
<div class="nice-padding">
<form action="{% url 'wagtailadmin_pages:copy' page.id %}" method="POST" novalidate>
{% csrf_token %}
<input type="hidden" name="next" value="{{ next }}" />
<ul class="fields">
{% include "wagtailadmin/shared/field_as_li.html" with field=form.new_title %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.new_slug %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.new_parent_page %}
{% if form.copy_subpages %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.copy_subpages %}
{% endif %}
{% if form.publish_copies %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.publish_copies %}
{% endif %}
{% comment %} BEGIN CUSTOM CONTENT {% endcomment %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.other %}
{% comment %} END CUSTOM CONTENT {% endcomment %}
</ul>
<input type="submit" value="{% trans 'Copy this page' %}" class="button">
</form>
</div>
{% endblock %}
(2) urls.py
from django.conf.urls import include, url
from django.contrib import admin
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.admin.views import pages
from wagtail.documents import urls as wagtaildocs_urls
from wagtail.core import urls as wagtail_urls
from myapp.base import views as base_views # added
urlpatterns = [
url(r'^django-admin/', admin.site.urls),
url(r'^admin/pages/(\d+)/copy/$', base_views.customCopy, name='copy'), # added
url(r'^admin/', include(wagtailadmin_urls)),
url(r'^documents/', include(wagtaildocs_urls)),
url(r'', include(wagtail_urls)),
]
(2 & 3) views.py
from django import forms
from django.core.exceptions import PermissionDenied
from wagtail.admin.forms.pages import CopyForm
from wagtail.admin.views import pages
from wagtail.core.models import Page
# BEGIN monkey patch of CopyForm
# See: wagtail/admin/forms/pages.py
original_form_init = CopyForm.__init__
original_form_clean = CopyForm.clean
def custom_form_init(self, *args, **kwargs):
# note - the template will need to be overridden to show additional fields
original_form_init(self, *args, **kwargs)
self.fields['other'] = forms.CharField(initial="will fail", label="Other", required=False)
def custom_form_clean(self):
cleaned_data = original_form_clean(self)
other = cleaned_data.get('other')
if other == 'will fail':
self._errors['other'] = self.error_class(["This field failed due to custom form validation"])
del cleaned_data['other']
return cleaned_data
CopyForm.__init__ = custom_form_init
CopyForm.clean = custom_form_clean
# END monkey patch of CopyForm
def customCopy(request, page_id):
"""
here we can inject any custom code for the response as a whole
the template is a view function so we cannot easily customise it
we can respond to POST or GET with any customisations though
See: wagtail/admin/views/pages.py
"""
page = Page.objects.get(id=page_id)
# Parent page defaults to parent of source page
parent_page = page.get_parent()
# Check if the user has permission to publish subpages on the parent
can_publish = parent_page.permissions_for_user(request.user).can_publish_subpage()
# Create the form
form = CopyForm(request.POST or None, user=request.user, page=page, can_publish=can_publish)
if request.method == 'POST':
if form.is_valid():
# if the form has been validated (using the form clean above)
# we get another chance here to fail the request, or redirect to another page
# we can also easily access the specific page's model for any Page model methods
try:
if not page.specific.can_copy_check():
raise PermissionDenied
except AttributeError:
# continue through to the normal behaviour
pass
response = pages.copy(request, page_id)
return response

Related

Modal Formbuilder for wagtail?

Hello im new to wagtail and its been really awesome so far. However im facing an issue trying to create a modal version of the formbuilder. My intentions is to create an action button within the base.html of which the user can click at any point in time and enter a modal pop up form to leave a feed back . Is there a way of accomplishing this?
This is very doable, you will need to work out how you want your modals to look and work by finding a suitable modal library.
Once you have that, you will need to determine how your admin interface will provide the setting of which FormPage will be used on the base template to render the modal. Wagtail's site settings is a good option.
From here, you then need to get the linked form and use it's get_form() method (all pages that extend AbstractForm or AbstractEmailForm have this). If you want to understand how the form is processed you can see the code here.
The simplest way to handle form submissions is to POST to the original form, this way there does not need to be any additional handling elsewhere. You also get the 'success' page as per normal without having to render that inside the modal.
Below is a basic code example that should get you started.
Example Code Walk-through
1. install wagtail settings
This will enable us to have some site wide settings to configure which form will be used in the modal
Docs - https://docs.wagtail.io/en/stable/reference/contrib/settings.html#site-settings
# settings.py
INSTALLED_APPS += [
'wagtail.contrib.settings',
]
2. Set up a settings model
This will contain a relationship to any FormPage, you will need to create the form page separately.
Note: you may want to uncheck 'show in menus' on the form page, unless you want users to be able to go to that form on its own URL also
Docs - https://docs.wagtail.io/en/stable/reference/contrib/settings.html#defining-settings
Remember to run makemigrations & migrate
from django.db import models
from wagtail.contrib.settings.models import BaseSetting, register_setting
from wagtail.admin.edit_handlers PageChooserPanel
# ... other models & imports
#register_setting
class MyAppSettings(BaseSetting):
# relationship to a single form page (one per site)
modal_form_page = models.ForeignKey(
'wagtailcore.Page',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
verbose_name='Modal Form'
)
panels = [
# note the kwarg - this will only allow form pages to be selected (replace base with your app)
PageChooserPanel('modal_form_page', page_type='base.FormPage')
]
3. Install a basic modal library
This will be up to you, if you use bootsrap for example it comes with a modal library
For this example I have used https://micromodal.now.sh/#installation
There are lots of ways to do this, this is the simplest and does not require any async background calls to the server
3a. Add the css in your static folder (e.g. my-app/static/css/micromodal.css) & then import it in your central header or layout template.
<head>
<!-- ALL OTHER ITEMS -->
<!-- Modal CSS -->
<link href="{% static 'css/micromodal.css' %}" rel="stylesheet" type="text/css">
</head>
3b. Add the JS & init call in your base template, best to do this as the last item before the closing body tag
<!-- Modal JS -->
<script src="https://unpkg.com/micromodal/dist/micromodal.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
MicroModal.init();
});
</script>
4. Set up a template tag
There are a few ways to do this, but basically we want our base.html template to have a convenient way to place the modal & modal trigger. Django's template tags are a great way to do this.
Docs - https://docs.djangoproject.com/en/3.0/howto/custom-template-tags/#inclusion-tags
Using the instructions in the Wagtail site settings page we can import our settings model and access the related form page, from here we can generate the form object.
The template code below is the bare minimum, you will want to do more styling probably, it assumes the trigger will just be rendered along with the modal content.
The template contains some basic logic to allow for tracking of the page the form was submitted on source-page-id so we can redirect the user to their source page and request.session reading to show a success message.
# my-app/templatetags/modal_tags.py
from django import template
from models import MyAppSettings
register = template.Library()
# reminder - you will need to restart Django when adding a template tag
#register.inclusion_tag('tags/form_modal.html', takes_context=True)
def form_modal(context):
request = context['request'] # important - you must have the request in context
settings = MyAppSettings.for_request(request)
form_page = settings.modal_form_page
if not form_page:
return context
form_page = form_page.specific
# this will provide the parts needed to render the form
# this does NOT handle the submission of the form - that still goes to the form page
# this does NOT handle anything to do with rendering the 'thank you' message
context['form_page'] = form_page
context['form'] = form_page.get_form(page=form_page, user=request.user)
return context
{% comment %} e.g. my-app/templates/tags/form_modal.html {% endcomment %}
{% load wagtailcore_tags %}
{% if request.session.form_page_success %}
Thanks for submitting the form!
{% endif %}
{% if form %}
<button data-micromodal-trigger="modal-1">Open {{ form_page.title }} Modal</button>
<div class="modal micromodal-slide" id="modal-1" aria-hidden="true">
<div class="modal__overlay" tabindex="-1" data-micromodal-close>
<div class="modal__container" role="dialog" aria-modal="true" aria-labelledby="modal-1-title">
<header class="modal__header">
<h2 class="modal__title" id="modal-1-title">
{{ form_page.title }}
</h2>
<button class="modal__close" aria-label="Close modal" data-micromodal-close></button>
</header>
<form action="{% pageurl form_page %}" method="POST" role="form">
<main class="modal__content" id="modal-1-content">
{% csrf_token %}
{{ form.as_p }}
{% if page.pk != form_page.pk %}
{% comment %} only provide the source page if not on the actual form page {% endcomment %}
<input name="source-page-id" type="hidden" value="{{ page.pk }}">
{% endif %}
</main>
<footer class="modal__footer">
<input class="modal__btn modal__btn-primary" type="submit">
<button class="modal__btn" data-micromodal-close aria-label="Close this dialog window">Close</button>
</footer>
</form>
</div>
</div>
</div>
{% endif %}
5. Use the template tag where you want the modal trigger
Now you can add the modal (along with its trigger)
Important: for the template tag above to have access to the request via context, you may need to add this (depending on your setup).
<!-- Footer -->
<footer>
{% form_modal %}
{% include "includes/footer.html" %}
</footer>
6. Redirecting back to the source page
As a final step we can redirect the user back to their source page by reading the source-page-id from the request.POST.
From this ID we can get the original URL and use Django's redirect shortcut.
Before we redirect, we updated request.session with some data so we can show a success message.
This can be done by overriding the render_landing_page on your FormPage
Docs - https://docs.wagtail.io/en/latest/reference/contrib/forms/customisation.html#custom-landing-page-redirect
# models.py
class FormPage(AbstractEmailForm):
# .. fields etc
def render_landing_page(self, request, form_submission=None, *args, **kwargs):
source_page_id = request.POST.get('source-page-id')
source_page = Page.objects.get(pk=source_page_id)
if source_page:
request.session['form_page_success'] = True
return redirect(source_page.url, permanent=False)
# if no source_page is set, render default landing page
return super().render_landing_page(request, form_submission, *args, **kwargs)

Trying to include a form on another app's template with {% include %}, but getting "TemplateDoesNotExist"

Currently, I can leave a comment about a post, but I have to go to a separate comment create page. I want to include the comment form right under the post in the group detail page. I have tried to use {% include %}, but it cant seem to find the form. This is probabaly because I am trying to render the form on a different app's template than where the form.py and comment_form.html are (I create the form and the template for the form in the 'comments' app, and I'm trying to include the form in the the 'groups' app on the detail page. here are the relevant files.
comments/forms.py
from django import forms
from .models import Comment
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = ('body',)
comments/comment_form.html
<h2>this is the comment form</h2>
<form class="post-form" method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="save btn btn-default">Save</button>
</form>
groups/views.detail :
def detail(request, group_id):
group = get_object_or_404(Group, pk= group_id)
posts = Post.objects.filter(group__id = group_id)
form = CommentForm()
return render(request, 'groups/detail.html', {'group': group, 'posts':posts, 'form':form})
groups/detail.html:
{% include form %}
this is the url that takes care of creating a comment (comments/urls.py):
from . import views
from django.urls import path
app_name = 'comments'
urlpatterns = [
path('<int:post_id>/create/', views.create, name='create'),
path('delete/<int:group_id>/<int:post_id>/<int:comment_id>', views.delete, name='delete'),
]
again, it can't find the template, probabaly because I have to make the 'groups' app aware of 'comment_form.html' 's existence. how do I accomplish this?
If you want to include comments/comment_form.html you should do:
{% include 'comments/comment_form.html' %}
Currently, you have {% include form %} which will use the variable form - that doesn't make sense, since form is a form instance CommentForm(), it is not a template name.

How can I add a button into django admin change list view page

I would like to add a button next to "add" button in list view in model for my model and then create a view function where I will do my stuff and then redirect user back to list view.
I've checked how to overload admin template, but I still dont know, where should I put my view function where I will do my stuff, and how can I register that view into admin urls.
There is also question about security. I would like to have that action inside admin, so if u r not logged in, u cannot use it.
I've found this, but I don't know if it's the right way: http://www.stavros.io/posts/how-to-extend-the-django-admin-site-with-custom/
When several applications provide different versions of the same
resource (template, static file, management command, translation), the
application listed first in INSTALLED_APPS has precedence.
- Django documentation on INSTALLED_APPS
Make sure your app is listed before 'django.contrib.admin' in INSTALLED_APPS.
Create a change_list.html template in one of the following directories:
# Template applies to all change lists.
myproject/myapp/templates/admin/change_list.html
# Template applies to change lists in myapp.
myproject/myapp/templates/admin/myapp/change_list.html
# Template applies to change list in myapp and only to the Foo model.
myproject/myapp/templates/admin/myapp/foo/change_list.html
The template should be picked up automatically, but in case it is not on one of paths listed above, you can also point to it via an admin model attribute:
class MyModelAdmin(admin.ModelAdmin):
#...
change_list_template = "path/to/change_list.html"
You can lookup the contents of the original change_list.html it lives in path/to/your/site-packages/django/contrib/admin/templates/admin/change_list.html. The other answer also shows you how to format the template. Nikolai Saiko shows you how to override the relevant parts using 'extends' and 'super'. Summary:
{% extends "admin/change_list.html" %} {% load i18n %}
{% block object-tools-items %}
{{ block.super }}
<li>
<a class="historylink" href="...">My custom admin page</a>
</li>
{% endblock %}
Let's fill href="..." with an url. The admin url names are in the namespace 'admin' and can be looked up like this:
{% url 'admin:custom_view' %}
When you are adding a button to change_form.html you maybe want to pass in the current object id:
{% url 'admin:custom_view' original.pk %}
Now create a custom view. This can be a regular view (just like other pages on your website) or a custom admin view in admin.py. The get_urls method on a ModelAdmin returns the URLs to be used for that ModelAdmin in the same way as a URLconf. Therefore you can extend them as documented in URL dispatcher:
class MyModelAdmin(admin.ModelAdmin):
def get_urls(self):
urls = super(MyModelAdmin, self).get_urls()
my_urls = patterns('',
url(r'^my_view/$', self.my_view, name="custom_view")
)
return my_urls + urls
def my_view(self, request):
# custom view which should return an HttpResponse
pass
# In case your template resides in a non-standard location
change_list_template = "path/to/change_list.html"
Read the docs on how to set permissions on a view in ModelAdmin: https://docs.djangoproject.com/en/1.5/ref/contrib/admin/#django.contrib.admin.ModelAdmin.get_urls
You can protect your view and only give access to users with staff status:
from django.contrib.admin.views.decorators import staff_member_required
#staff_member_required
def my_view(request):
...
You might also want to check request.user.is_active and handle inactive users.
Update: Take advantage of the framework and customise as little as possible. Many times actions can be a good alternative: https://docs.djangoproject.com/en/1.5/ref/contrib/admin/actions/
Update 2: I removed a JS example to inject a button client side. If you need it, see the revisions.
Here is another solution , without using of jQuery (like one provided by allcaps). Also this solution provides object's pk with more intuitive way :)
I'll give my source code based on that link (follow link above for more info):
I have an app Products with model Product. This code adds button "Do Evil", which executes ProductAdmin.do_evil_view()
File products/models.py:
class ProductAdmin(admin.ModelAdmin):
def get_urls(self):
urls = super().get_urls()
my_urls = patterns('',
(r'^(?P<pk>\d+)/evilUrl/$', self.admin_site.admin_view(self.do_evil_view))
)
return my_urls + urls
def do_evil_view(self, request, pk):
print('doing evil with', Product.objects.get(pk=int(pk)))
return redirect('/admin/products/product/%s/' % pk)
self.admin_site.admin_view is needed to ensure that user was logged as administrator.
And this is template extention of standard Django admin page for changing entry:
File: {template_dir}/admin/products/product/change_form.html
In Django >= 1.8 (thanks to #jenniwren for this info):
{% extends "admin/change_form.html" %}
{% load i18n %}
{% block object-tools-items %}
{{ block.super }}
<li><a class="historylink" href="evilUrl/">{% trans "Do Evil" %}</a></li>
{% endblock %}
If your Django version is lesser than 1.8, you have to write some more code:
{% extends "admin/change_form.html" %}
{% load i18n %}
{% block object-tools %}
{% if change %}{% if not is_popup %}
<ul class="object-tools">
<li><a class="historylink" href="history/">{% trans "History" %}</a></li>
<li><a class="historylink" href="evilUrl/">{% trans "Do Evil" %}</a></li>
{% if has_absolute_url %}
<li><a class="viewsitelink" href="../../../r/{{ content_type_id }}/{{ object_id }}/">{% trans "View on site" %}</a></li>
{% endif%}</ul>
{% endif %}{% endif %}
{% endblock %}

Is it possible acess request data into a django generic list view?

I need to have access to some request data (username) into a template used in a generic list view. I serched django documentation but this subject is not so clear (to me) at a first glance.
My situation is like this:
**urls.py:**
from django.conf.urls.defaults import patterns
from django.views.generic.base import TemplateView
...
urlpatterns = patterns('',
...
(r'^pattern.html', TemplateView.as_view(template_name="template_name.html")),
...
)
** template_name.html **
{% extends "base.html" %}
{% block title %}My title{% endblock %}
{% block content %}
<h2>My heading</h2>
link text 1 |
link text 2 |
link for {{username}} |
{% endblock %}
Here (template_name.html) is where I want to use the request data. To compose a link to another view passing as parameter request.user
This post says tels somthing about generics but its not exactly what i need.
{{ user.username }} is always available in a generic view, because of the auth context processor. If you need anything else from the request, you'll need to add the request context processor.

How to implement breadcrumbs in a Django template?

Some solutions provided on doing a Google search for "Django breadcrumbs" include using templates and block.super, basically just extending the base blocks and adding the current page to it. http://www.martin-geber.com/thought/2007/10/25/breadcrumbs-django-templates/
http://www.djangosnippets.org/snippets/1289/ - provides a template tag but I'm not sure this would work if you don't have your urls.py properly declared.
I'm wondering what's the best way? And if you have implemented breadcrumbs before how did you do it?
--- Edit --
My question was meant to be: is there a general accepted method of doing breadcrumbs in Django, but from the answers I see there is not, and there are many different solutions, I'm not sure who to award the correct answer to, as I used a variation of using the block.super method, while all the below answers would work.
I guess then this is too much of a subjective question.
Note: I provide the full snippet below, since djangosnippets has been finicky lately.
Cool, someone actually found my snippet :-) The use of my template tag is rather simple.
To answer your question there is no "built-in" django mechanism for dealing with breadcrumbs, but it does provide us with the next best thing: custom template tags.
Imagine you want to have breadcrumbs like so:
Services -> Programming
Services -> Consulting
Then you will probably have a few named urls: "services", and "programming", "consulting":
(r'^services/$',
'core.views.services',
{},
'services'),
(r'^services/programming$',
'core.views.programming',
{},
'programming'),
(r'^services/consulting$',
'core.views.consulting',
{},
'consulting'),
Now inside your html template (lets just look at consulting page) all you have to put is:
//consulting.html
{% load breadcrumbs %}
{% block breadcrumbs %}
{% breadcrumb_url 'Services' services %}
{% breadcrumb_url 'Consulting' consulting %}
{% endblock %}
If you want to use some kind of custom text within the breadcrumb, and don't want to link it, you can use breadcrumb tag instead.
//consulting.html
{% load breadcrumbs %}
{% block breadcrumbs %}
{% breadcrumb_url 'Services' services %}
{% breadcrumb_url 'Consulting' consulting %}
{% breadcrumb 'We are great!' %}
{% endblock %}
There are more involved situations where you might want to include an id of a particular object, which is also easy to do. This is an example that is more realistic:
{% load breadcrumbs %}
{% block breadcrumbs %}
{% breadcrumb_url 'Employees' employee_list %}
{% if employee.id %}
{% breadcrumb_url employee.company.name company_detail employee.company.id %}
{% breadcrumb_url employee.full_name employee_detail employee.id %}
{% breadcrumb 'Edit Employee ' %}
{% else %}
{% breadcrumb 'New Employee' %}
{% endif %}
{% endblock %}
DaGood breadcrumbs snippet
Provides two template tags to use in your HTML templates: breadcrumb and breadcrumb_url. The first allows creating of simple url, with the text portion and url portion. Or only unlinked text (as the last item in breadcrumb trail for example). The second, can actually take the named url with arguments! Additionally it takes a title as the first argument.
This is a templatetag file that should go into your /templatetags directory.
Just change the path of the image in the method create_crumb and you are good to go!
Don't forget to {% load breadcrumbs %} at the top of your html template!
from django import template
from django.template import loader, Node, Variable
from django.utils.encoding import smart_str, smart_unicode
from django.template.defaulttags import url
from django.template import VariableDoesNotExist
register = template.Library()
#register.tag
def breadcrumb(parser, token):
"""
Renders the breadcrumb.
Examples:
{% breadcrumb "Title of breadcrumb" url_var %}
{% breadcrumb context_var url_var %}
{% breadcrumb "Just the title" %}
{% breadcrumb just_context_var %}
Parameters:
-First parameter is the title of the crumb,
-Second (optional) parameter is the url variable to link to, produced by url tag, i.e.:
{% url person_detail object.id as person_url %}
then:
{% breadcrumb person.name person_url %}
#author Andriy Drozdyuk
"""
return BreadcrumbNode(token.split_contents()[1:])
#register.tag
def breadcrumb_url(parser, token):
"""
Same as breadcrumb
but instead of url context variable takes in all the
arguments URL tag takes.
{% breadcrumb "Title of breadcrumb" person_detail person.id %}
{% breadcrumb person.name person_detail person.id %}
"""
bits = token.split_contents()
if len(bits)==2:
return breadcrumb(parser, token)
# Extract our extra title parameter
title = bits.pop(1)
token.contents = ' '.join(bits)
url_node = url(parser, token)
return UrlBreadcrumbNode(title, url_node)
class BreadcrumbNode(Node):
def __init__(self, vars):
"""
First var is title, second var is url context variable
"""
self.vars = map(Variable,vars)
def render(self, context):
title = self.vars[0].var
if title.find("'")==-1 and title.find('"')==-1:
try:
val = self.vars[0]
title = val.resolve(context)
except:
title = ''
else:
title=title.strip("'").strip('"')
title=smart_unicode(title)
url = None
if len(self.vars)>1:
val = self.vars[1]
try:
url = val.resolve(context)
except VariableDoesNotExist:
print 'URL does not exist', val
url = None
return create_crumb(title, url)
class UrlBreadcrumbNode(Node):
def __init__(self, title, url_node):
self.title = Variable(title)
self.url_node = url_node
def render(self, context):
title = self.title.var
if title.find("'")==-1 and title.find('"')==-1:
try:
val = self.title
title = val.resolve(context)
except:
title = ''
else:
title=title.strip("'").strip('"')
title=smart_unicode(title)
url = self.url_node.render(context)
return create_crumb(title, url)
def create_crumb(title, url=None):
"""
Helper function
"""
crumb = """<span class="breadcrumbs-arrow">""" \
"""<img src="/media/images/arrow.gif" alt="Arrow">""" \
"""</span>"""
if url:
crumb = "%s<a href='%s'>%s</a>" % (crumb, url, title)
else:
crumb = "%s %s" % (crumb, title)
return crumb
The Django admin view modules have automatic breadcumbs, which are implemented like this:
{% block breadcrumbs %}
<div class="breadcrumbs">
{% trans 'Home' %}
{% block crumbs %}
{% if title %} › {{ title }}{% endif %}
{% endblock %}
</div>
{% endblock %}
So there is some kind of built-in support for this..
My view functions emit the breadcrumbs as a simple list.
Some information is kept in the user's session. Indirectly, however, it comes from the URL's.
Breadcrumbs are not a simple linear list of where they've been -- that's what browser history is for. A simple list of where they've been doesn't make a good breadcrumb trail because it doesn't reflect any meaning.
For most of our view functions, the navigation is pretty fixed, and based on template/view/URL design. In our cases, there's a lot of drilling into details, and the breadcrumbs reflect that narrowing -- we have a "realm", a "list", a "parent" and a "child". They form a simple hierarchy from general to specific.
In most cases, a well-defined URL can be trivially broken into a nice trail of breadcrumbs. Indeed, that's one test for good URL design -- the URL can be interpreted as breadcrumbs and displayed meaningfully to the users.
For a few view functions, where we present information that's part of a "many-to-many" join, for example, there are two candidate parents. The URL may say one thing, but the session's context stack says another.
For that reason, our view functions have to leave context clues in the session so we can emit breadcrumbs.
Try django-breadcrumbs — a pluggable middleware that add a breadcrumbs callable/iterable in your request object.
It supports simple views, generic views and Django FlatPages app.
I had the same issue and finally I've made simple django tempalate tag for it: https://github.com/prymitive/bootstrap-breadcrumbs
http://www.djangosnippets.org/snippets/1289/ - provides a template tag but i'm not sure this would work if you don't have your urls.py properly declared.
Nothing will work if you don't have your urls.py properly declared. Having said that, it doesn't look as though it imports from urls.py. In fact, it looks like to properly use that tag, you still have to pass the template some variables. Okay, that's not quite true: indirectly through the default url tag, which the breadcrumb tag calls. But as far as I can figure, it doesn't even actually call that tag; all occurrences of url are locally created variables.
But I'm no expert at parsing template tag definitions. So say somewhere else in the code it magically replicates the functionality of the url tag. The usage seems to be that you pass in arguments to a reverse lookup. Again, no matter what your project is, you urls.py should be configured so that any view can be reached with a reverse lookup. This is especially true with breadcrumbs. Think about it:
home > accounts > my account
Should accounts, ever hold an arbitrary, hardcoded url? Could "my account" ever hold an arbitrary, hardcoded url? Some way, somehow you're going to write breadcrumbs in such a way that your urls.py gets reversed. That's really only going to happen in one of two places: in your view, with a call to reverse, or in the template, with a call to a template tag that mimics the functionality of reverse. There may be reasons to prefer the former over the latter (into which the linked snippet locks you), but avoiding a logical configuration of your urls.py file is not one of them.
Try django-mptt.
Utilities for implementing Modified Preorder Tree Traversal (MPTT) with your Django Model classes and working with trees of Model instances.
This answer is just the same as #Andriy Drozdyuk (link). I just want to edit something so it works in Django 3.2 (in my case) and good in bootstrap too.
for create_crumb function (Remove the ">" bug in the current code)
def create_crumb(title, url=None):
"""
Helper function
"""
if url:
crumb = '<li class="breadcrumb-item">{}</li>'.format(url, title)
else:
crumb = '<li class="breadcrumb-item active" aria-current="page">{}</li>'.format(title)
return crumb
And for __init__ in BreadcrumbNode, add list() to make it subscriptable. And change smart_unicode to smart_text in render method
from django.utils.encoding import smart_text
class BreadcrumbNode(Node):
def __init__(self, vars):
"""
First var is title, second var is url context variable
"""
self.vars = list(map(Variable, vars))
def render(self, context):
title = self.vars[0].var
if title.find("'")==-1 and title.find('"')==-1:
try:
val = self.vars[0]
title = val.resolve(context)
except:
title = ''
else:
title=title.strip("'").strip('"')
title=smart_text(title)
And add this in base.html for the view for Bootstrap. Check the docs
<nav style="--bs-breadcrumb-divider: '>';" aria-label="breadcrumb">
<ol class="breadcrumb">
{% block breadcrumbs %}
{% endblock breadcrumbs %}
</ol>
</nav>
Obviously, no one best answer, but for practical reason I find that it is worth considering the naïve way. Just overwrite and rewrite the whole breadcrumb... (at least until the official django.contrib.breadcrumb released )
Without being too fancy, it is better to keep things simple. It helps the newcomer to understand. It is extremely customizable (e.g. permission checking, breadcrumb icon, separator characters, active breadcrumb, etc...)
Base Template
<!-- File: base.html -->
<html>
<body>
{% block breadcrumb %}
<ul class="breadcrumb">
<li>Dashboard</li>
</ul>
{% endblock breadcrumb %}
{% block content %}{% endblock content %}
</body>
</html>
Implementation Template
Later on each pages we rewrite and overwrite the whole breadcrumb block.
<!-- File: page.html -->
{% extends 'base.html' %}
{% block breadcrumb %}
<ul class="breadcrumb">
<li>Dashboard</li>
<li>Level 1</li>
<li class="active">Level 2</li>
</ul>
{% endblock breadcrumb %}
Practicallity
Realworld use cases:
Django Oscar: base template, simple bread
Django Admin: base template, simple bread, permission check breadcrumb
You could also reduce the boiler plate required to manage breadcrumbs using django-view-breadcrumbs, by adding a crumbs property to the view.
urls.py
urlpatterns = [
...
path('posts/<slug:slug>', views.PostDetail.as_view(), name='post_detail'),
...
]
views.py
from django.views.generic import DetailView
from view_breadcrumbs import DetailBreadcrumbMixin
class PostDetail(DetailBreadcrumbMixin, DetailView):
model = Post
template_name = 'app/post/detail.html'
base.html
{% load django_bootstrap_breadcrumbs %}
{% block breadcrumbs %}
{% render_breadcrumbs %}
{% endblock %}
Something like this may work for your situation:
Capture the entire URL in your view and make links from it. This will require modifying your urls.py, each view that needs to have breadcrumbs, and your templates.
First you would capture the entire URL in your urls.py file
original urls.py
...
(r'^myapp/$', 'myView'),
(r'^myapp/(?P<pk>.+)/$', 'myOtherView'),
...
new urls.py
...
(r'^(?P<whole_url>myapp/)$', 'myView'),
(r'^(?P<whole_url>myapp/(?P<pk>.+)/)$', 'myOtherView'),
...
Then in your view something like:
views.py
...
def myView(request, whole_url):
# dissect the url
slugs = whole_url.split('/')
# for each 'directory' in the url create a piece of bread
breadcrumbs = []
url = '/'
for slug in slugs:
if slug != '':
url = '%s%s/' % (url, slug)
breadcrumb = { 'slug':slug, 'url':url }
breadcrumbs.append(breadcrumb)
objects = {
'breadcrumbs': breadcrumbs,
}
return render_to_response('myTemplate.html', objects)
...
Which should be pulled out into a function that gets imported into the views that need it
Then in your template print out the breadcrumbs
myTemplate.html
...
<div class="breadcrumb-nav">
<ul>
{% for breadcrumb in breadcrumbs %}
<li>{{ breadcrumb.slug }}</li>
{% endfor %}
</ul>
</div>
...
One shortcoming of doing it this way is that as it stands you can only show the 'directory' part of the url as the link text. One fix for this off the top of my head (probably not a good one) would be to keep a dictionary in the file that defines the breadcrumb function.
Anyways that's one way you could accomplish breadcrumbs, cheers :)
You might want to try django-headcrumbs (don’t worry, they are not going to eat your brains).
It’s very lightweight and absolutely straightforward to use, all you have to do is annotate your views (because defining crumbs structure in templates sounds crazy to me) with a decorator that explains how to get back from the given view.
Here is an example from the documentation:
from headcrumbs.decorators import crumb
from headcrumbs.util import name_from_pk
#crumb('Staff') # This is the root crumb -- it doesn’t have a parent
def index(request):
# In our example you’ll fetch the list of divisions (from a database)
# and output it.
#crumb(name_from_pk(Division), parent=index)
def division(request, slug):
# Here you find all employees from the given division
# and list them.
There are also some utility functions (e.g. name_from_pk you can see in the example) that automagically generate nice names for your crumbs without you having to wright lots of code.
I've created template filter for this.
Apply your custom filter (I've named it 'makebreadcrumbs') to the request.path like this:
{% with request.resolver_match.namespace as name_space %}
{{ request.path|makebreadcrumbs:name_space|safe }}
{% endwith %}
We need to pass url namespace as an arg to our filter.
Also use safe filter, because our filter will be returning string that needs to be resolved as html content.
Custom filter should look like this:
#register.filter
def makebreadcrumbs(value, arg):
my_crumbs = []
crumbs = value.split('/')[1:-1] # slice domain and last empty value
for index, c in enumerate(crumbs):
if c == arg and len(crumbs) != 1:
# check it is a index of the app. example: /users/user/change_password - /users/ is the index.
link = '{}'.format(reverse(c+':index'), c)
else:
if index == len(crumbs)-1:
link = '<span>{}</span>'.format(c)
# the current bread crumb should not be a link.
else:
link = '{}'.format(reverse(arg+':' + c), c)
my_crumbs.append(link)
return ' > '.join(my_crumbs)
# return whole list of crumbs joined by the right arrow special character.
Important:
splited parts of the 'value' in our filter should be equal to the namespace in the urls.py, so the reverse method can be called.
Hope it helped.
A generic way, to collect all callable paths of the current url could be resolved by the following code snippet:
from django.urls import resolve, Resolver404
path_items = request.path.split("/")
path_items.pop(0)
path_tmp = ""
breadcrumb_config = OrderedDict()
for path_item in path_items:
path_tmp += "/" + path_item
try:
resolve(path_tmp)
breadcrumb_config[path_item] = {'is_representative': True, 'current_path': path_tmp}
except Resolver404:
breadcrumb_config[path_item] = {'is_representative': False, 'current_path': path_tmp}
If the resolve function can't get a real path from any urlpattern, the Resolver404 exception will be thrown. For those items we set the is_representative flag to false. The OrderedDict breadcrumb_config holds after that the breadcrumb items with there configuration.
For bootstrap 4 breadcrumb for example, you can do something like the following in your template:
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
{% for crumb, values in BREADCRUMB_CONFIG.items %}
<li class="breadcrumb-item {% if forloop.last or not values.is_representative %}active{% endif %}" {% if forloop.last %}aria-current="page"{% endif %}>
{% if values.is_representative %}
<a href="{{values.current_path}}">
{{crumb}}
</a>
{% else %}
{{crumb}}
{% endif %}
</li>
{% endfor %}
</ol>
</nav>
Only the links which won't raises a 404 are clickable.
I believe there is nothing simpler than that (django 3.2):
def list(request):
return render(request, 'list.html', {
'crumbs' : [
("Today", "https://www.python.org/"),
("Is", "https://www.python.org/"),
("Sunday", "https://www.djangoproject.com/"),
]
})
Breadcrumbs.html
<div class="page-title-right">
<ol class="breadcrumb m-0">
{% if crumbs %}
{% for c in crumbs %}
<li class="breadcrumb-item {{c.2}}">{{c.0}}</li>
{% endfor %}
{% endif %}
</ol>
</div>
css:
.m-0 {
margin: 0!important;
}
.breadcrumb {
display: flex;
flex-wrap: wrap;
padding: 0 0;
margin-bottom: 1rem;
list-style: none;
border-radius: .25rem;
}
dl, ol, ul {
margin-top: 0;
margin-bottom: 1rem;
}
ol, ul {
padding-left: 2rem;
}