Modal Formbuilder for wagtail? - django

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)

Related

How to use several bootstrap modal in one view for CRUD action in Django?

I have a view by several modal and forms like Add_image , Add_birthday and a table for show all objects.
I want for every ROW create button and modal for edit. When user click Edit_button open a modal show form model object and user edit forms and save it.
How to show special object and update it in modal.
I have done precisely this in an application that supports organization of project information.
I suppose there are many different ways to approach this problem but the path that I chose was to override the view's post method. Take, for example, the code in the following abbreviated view:
class HomeView(LoginRequiredMixin, PermissionRequiredMixin, TemplateView):
permission_required = 'app.view_project'
reviewtype = None
template_name = "home.html"
def post(self, request, *args, **kwargs):
""" a) There are two forms for each project accordion button
displayed by this HomeView and,
b) We are displaying those forms in a Bootstrap 5 'modal' (as
opposed to redirecting the user to a new view for the form(s))
Override the POST method for this view so that the forms can be
processed correctly.
"""
# Determine which form is being sent in the post request.
if 'owner' in request.POST:
# 'owner' was in the POST data: NoteForm was sent in
form = NoteForm(data=request.POST)
elif 'changed_by' in request.POST:
# 'changed_by' was in POST data: ReviewStatusForm was sent in
form = ReviewStatusForm(data=request.POST)
elif 'project_company_approval' in request.POST:
# 'project_company_approval' was in POST data: EditFieldForm was sent in
form = EditCompanyApprovalForm(
data=request.POST,
instance=Project.objects.get(project_number=request.POST['project_number']),
)
You'll notice that the view checks to see if the field that the user wanted to edit is present in the POST data and if it is, it fires up the appropriate form and populates the form with the data present in the form.
As far as the modal itself goes, that's handled by the following template:
{% load crispy_forms_tags %}
<!-- Button trigger modal -->
<i type="button" class="bi bi-pencil-square" style="color : green" data-bs-toggle="modal" data-bs-target="#editCompanyApprovalModal{{ project.project_number }}" onclick="open_modal({{ project.project_number }},'_project_approval','{{ project.project_approval }}')" ></i>
<!-- Modal -->
<div class="modal fade" id="editCompanyApprovalModal{{ project.project_number }}" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLabel{{ project.project_number }}" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-body">
{% crispy form %}
</div>
</div>
</div>
</div>
Hopefully that helps answer your question? Let me know if there's anything more you would like to see and I'll see what I can do to get it out with the answer.
Depending on your specific implementation, you might also have to override the view's get_context_data() method, too.
Best of luck to you.

How do I customize the Wagtail page copy experience?

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

Creating links to a page that displays specific rows in my database w/ django?

I created a watered down project based of my more complex project for this question to help deliver my question more effectively. I'll include the code below for future viewers of this post but for ease/convenience here is the gitlab repository url.
I have a model "NotesModel" that models the architecture of a note that you might take during the day. It's quite simple there's a 'title', 'tag' and of course the main part which I call the 'content'. The tag is like a tag you link to your stack overflow post. It's just to help identify the topic that a particular note might be about.
In my function based view I query every row in my database and hand it off to my render(.., .., {'notes': notes}) function as you'll see below. With some html/bootstrap styling I display every item from the 'tag' column in my database as a label which is linkable with the ...a href=""... syntax as such.
{% for note in notes %}
<span class="label label-primary" id="tags">
<a id="theurl" href="this is where my confusion is">
{{note.tag}}
</a>
</span>
{% endfor %}
I entered some basic notes already and taken a screen-shot to help show you what my page looks like in hope that it better explains the behavior I want which I explain next.
Screen-shot
The behavior that I want is to treat this label as a actual tag you might find on some forum website. The behavior is as follows...
1) The user clicks on the label
2) Django directs the user to another page
3) The rows in the database that the attribute 'tag' (being the column) matches the name of the label will be displayed.
Let me explain a bit more. From the picture above you can see there are already four notes (being four rows in database terminology) entered into the database. Therefore, the 'tag' attribute (being the 'tag' column in database terminology) has four instances being starwars, startrek, avp, and another starwars instance. I want to be able to click on the tag starwars and it directs me to another page that displays all notes with the 'tag' starwars. Same goes for the other tags. If I click on startrek then I want this to direct me to another page that displays all notes with the 'tag' startrek.
I did at one point try creating another page in my templates folder and then used a query set filter like the following that I passed off to the template.
queryset = NotesModel.objects.filter(tag__icontains='starwars')
Then I just typed the direct link to that page in the ...a href=""... piece of code. However, there's two problems with this solution...
1 It only works for starwars
2 If i did it this way I would have to create x number of page.html files in my templates folder with an equal amount of x number of function based views with the above queryset. This is more static and not a dynamic way of doing things so how do I achieve such a task?
The following are files in my project as of now. As I said earlier i'm including the gitlab repository url above should you want to pull the project down yourself.
the_app/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.home, name='home'),
]
the_app/models.py
from django.db import models
class NotesModel(models.Model):
title = models.CharField(max_length=100, blank=True)
tag = models.CharField(max_length=100, blank=False)
content = models.CharField(max_length=1000, blank=False)
def __str__(self):
return self.title
the_app/forms.py
from django import forms
from .models import NotesModel
class NotesForm(forms.ModelForm):
class Meta:
model = NotesModel
fields = ['title', 'tag', 'content']
the_app/views.py
from django.shortcuts import render
from .models import NotesModel
from .forms import NotesForm
def home(request):
# Grab the form data
if request.method == 'POST':
form = NotesForm(request.POST)
if form.is_valid():
form.save()
form = NotesForm()
else:
form = NotesForm()
# Grab the data from the database
notes = NotesModel.objects.all()
return render(request, 'the_app/page_home.html', {'form': form, 'notes': notes})
the_app/templates/the_app/base.html
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.0/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="{% static 'mystyle.css' %}">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.0/js/bootstrap.min.js"></script>
</head>
<body>
<div>
{% block notesform %}{% endblock notesform %}
</div>
<div>
{% block notetags %}{% endblock notetags %}
</div>
</body>
</html>
the_app/templates/the_app/page_home.html
{% extends "the_app/base.html" %}
{% block notesform %}
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form }}
<button type="submit" class="btn btn-primary">Submit</button>
</form>
{% endblock notesform %}
{% block notetags %}
{% for note in notes %}
<span class="label label-primary" id="tags">
<!-- I didn't know what to put here so i used google's url as a place holder-->
<a id="theurl" href="https://www.google.com">
{{note.tag}}
</a>
</span>
{% endfor %}
{% endblock notetags %}
Here is the problem, queryset = NotesModel.objects.filter(tag__icontains='starwars') your tag is only containing starwars. So it will not find other tags.
class TagPostView(ListView):
model = NoteModel
template_name = '....'
context_object_name = 'all_notes_of_this_tag'
def get_queryset(self):
result = super(PostTagView, self).get_queryset()
query = self.request.GET.get('q')
if query:
postresult = NoteModel.objects.filter(tag__icontains=query)
result = postresult
else:
result = None
return result
Now you can pass label as q and it will search for that tag and show the result. All you need is only one template for this.
In template you can use,
{% for note in all_notes_of_this_tag %}
{{note.title}}
#..
{% endfor %}
This will solve all three requirements.

Trouble with Django Logout Page, django-registration logout.html Template Form Fields

I am using django-registration. After getting it working, I went on extending views and forms for field customization using Bootstrap.
I have this working for everything except the logout page. In the default method, I simply built the form by scratch and it worked OK. But when trying to use Django's forms, it's not showing the fields at all. Both the login and logout templates use this basic form:
<div class="auth-form">
<form class="form-signin" method="post" action="/accounts/login/">
{% csrf_token %}
<h2 class="form-signin-heading">Please log in</h2>
<div class="font-16px top-mar-1em">
<label for="id_username"></label>
{{ form.username }}
<label for="id_password1"></label>
{{ form.password }}
<div class="form-group">
<div class="col-sm-10">
<div class="level-checkbox">
<label for="checkbox">
{{ form.remember_me }} Remember me
</label>
</div>
</div>
</div>
<button class="btn btn-primary" type="submit">Log in</button>
</div>
</form>
</div>
This works fine for the login page, but for the logout page it doesn't (the fields are missing). It's like the form is not available to this page. I'm sure I missed something simple but I can't figure it out.
My urls.py section look like this:
url(r'^accounts/login/$','django.contrib.auth.views.login', {
'template_name': 'registration/login.html',
'authentication_form': UserAuthenticationForm,
}),
I've tried adding a logout section to urls.py similar to the login one, but that hasn't worked. I've tried using logout_then_login, using login.html as import template on logout.html and other miscellaneous things to no avail.
Apparently, there is no form associated with the default logout view, so this is probably the issue but I'm not sure the best way to implement a standard form in the logout view.
You should be able to add the following to your urls.py and have everything work.
url(r'^accounts/logout/$','django.contrib.auth.views.logout_then_login')
Make sure you have LOGIN_URL defined your settings.py. Then when a user clicks a logout link they will hit the logout view which takes care of redirecting to the login view which contains your form.
Thanks to #tsurantino I was able to get this working using the messages framework. I always like when I have to implement a new feature using a part of Django I haven't used before. Adding to the toolkit, so to speak. Here is what I did:
Since I had a signal_connectors.py file already for sending email based on user registration/activation I just added to it:
#receiver(user_logged_out)
def user_logged_out_message(sender, request, **kwargs):
messages.add_message(
request,
messages.INFO,
'You\'ve been successfully logged out. You can log in again below.'
)
Then in my common app's __init__.py I imported it:
from common.signal_connectors import user_logged_out_message
Then in my login.html template:
{% if messages %}
<div class="alert-success align-ctr font-16px top-mar-2em">
{% for message in messages %}
{{ message }}
{% endfor %}
</div>
{% endif %}
Finally, to redirect to login page after logout (urls.py):
url(r'^accounts/logout/$','django.contrib.auth.views.logout_then_login'),
Thanks for everyone’s help and comments.

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 %}