Add a custom button to a Django application's admin page - python

I have an application in Django with a routine which would be available only to the admin. What I want to do is add a button to perform the routine in this application's section of the admin app.
Am I supposed to make a template for it, and if that's the case, how do I add a html template for an app in the admin. Or maybe there's a command to simply add a button?

Messing with the admin forms can be complicated but i've commonly found that adding links, buttons, or extra info is easy and helpful. (Like a list of links to related objects witout making an inline, esp for things that are more viewed than edited).
From Django docs
Because of the modular design of the admin templates, it is usually
neither necessary nor advisable to
replace an entire template. It is
almost always better to override only
the section of the template which you
need to change.
This will add a list over the top of the form.
Place in templates/admin/[your_app]/[template_to_override]:
{% extends "admin/change_form.html" %}
{% block form_top %}
{% for item in original.items %}
{{ item }}
{% endfor %}
{% endblock %}

Django1.10:
1) Override admin/submit_line.html:
{% load i18n admin_urls %}
<div class="submit-row">
{% if extra_buttons %}
{% for button in extra_buttons %}
{{ button }}
{% endfor %}
{% endif %}
{% if show_save %}<input type="submit" value="{% trans 'Save' %}" class="default" name="_save" />{% endif %}
{% if show_delete_link %}
{% url opts|admin_urlname:'delete' original.pk|admin_urlquote as delete_url %}
<p class="deletelink-box">{% trans "Delete" %}</p>
{% endif %}
{% if show_save_as_new %}<input type="submit" value="{% trans 'Save as new' %}" name="_saveasnew" />{% endif %}
{% if show_save_and_add_another %}<input type="submit" value="{% trans 'Save and add another' %}" name="_addanother" />{% endif %}
{% if show_save_and_continue %}<input type="submit" value="{% trans 'Save and continue editing' %}" name="_continue" />{% endif %}
</div>
This assumes, of course, that button's string representation is an appropriate browser input or button element, and is marked safe with django.utils.safestring.mark_safe. Alternatively, you could use the safe template filter or access the attributes of button directly to construct the <input>. In my opinion, it's better to isolate such things to the python level.
2) Override MyModelAdmin.change_view:
def change_view(self, request, object_id, form_url='', extra_context=None):
extra_context = extra_context or self.extra_context()
return super(PollAdmin, self).change_view(
request, object_id, form_url, extra_context=extra_context,
)
This method enables you to add buttons to any ModelAdmin easily. Alternatively to step (1), you could extend admin/change_form.html and override block submit_row. This would be slightly more verbose due to extra tags required in the template.
If you want the extra action available across all of your models (or a specific subset) then subclass ModelAdmin with the desired functionality (an example would be to add archiving to your models. You could even add an override for delete--and the other default buttons--so that the mode is archived instead of deleted; this would require some template modifications)

You can also use django-admin-tools, which allows you to easily customize the admin front page like a dashboard. Using a LinkList, you can point to some view method and check if the user is authenticated. It goes like thies:
# dashboard.py (read more about how to create one on django-admin-tools docs)
class CustomIndexDashboard(Dashboard):
"""
Custom index dashboard for captr.
"""
def init_with_context(self, context):
self.children.append(modules.LinkList(
_('Tasks'),
children=[
['Your task name', '/task']
]
))
# urls.py (mapping uri to your view function)
urlpatterns += patterns('yourapp.views',
(r'^task$', 'task'),
)
# views.py
def task(request):
if request.user.is_authenticated():
update_definitions_task.delay() # do your thing here. in my case I'm using django-celery for messaging
return redirect('/admin')

You might consider adding a custom admin action for this kind of object (similar to the built in 'delete'), if appropriate. Some benefits include: "pure Django", not having to mess with templates, and being able to act on multiple objects at once.
Django’s admin lets you write and register “actions” – simple
functions that get called with a list of objects selected on the
change list page. If you look at any change list in the admin, you’ll
see this feature in action; Django ships with a “delete selected
objects” action available to all models.
https://docs.djangoproject.com/en/dev/ref/contrib/admin/actions/
I got the idea from this article on how to add a custom action button, which is another answer all together. I was able to get by with the simpler built-in actions though.
https://medium.com/#hakibenita/how-to-add-custom-action-buttons-to-django-admin-8d266f5b0d41

Don't mess with the admin pages.
Create an "application" for this. Yes, your function is just a "routine". That's okay. Many smaller applications are a good thing.
This application has nothing new in models.py. No new model. Zero lines of code.
This application has a useful URL in urls.py. Something that can be used to display this admin page. One URL. Not many lines of code (less than a dozen.)
This application has one view function in views.py. On "GET", this view function presents the form. On "POST", this view function does the "routine". This is the "heart" of your application. The GET -- of course -- simply returns the template for display. The POST does the real work, and returns a final status or something.
This view function is protected with a decorator so that only an admin can execute it.
See http://docs.djangoproject.com/en/1.2/topics/auth/#django.contrib.auth.decorators.user_passes_test. You want to write a test for being an admin. lambda u: u.is_staff is probably it.
This application has one template, presented by the GET and POST. That template has your form with your button. The one you can't add to admin easily.
The tests.py is a test case with two users, one who is an admin and one who is not an admin.
No messing with built-in admin pages.

Related

How to create a "save and add another" button and show on Wagtail admin model page

Was wondering if it is possible to add a custom button within a Wagtail model page that will allow me to save (create) the current data fields and move on to another page. The "save and add another" button was already available in django and I want to have something like that on the Wagtail model page.
Thanks.
Wagtail has a few ways to customise content that is shown in the various Wagtail modeladmin views.
Code Example
Step 1 - create a custom CreateView
This will override the get_context_data method to provide the create_url, we could do this simpler (the context already has access to the instance) but it is nice to be explicit.
Override the get_success_url method to allow for a URL param of next to be set, if that exists the next URL will be this instead of the default behaviour.
products/wagtail_hooks.py
from wagtail.contrib.modeladmin.views import CreateView
# ... other imports
class CustomCreateView(CreateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['create_url'] = self.create_url
return context
def get_success_url(self):
next = self.request.GET.get('next')
if next:
return next
return super().get_success_url()
class ProductsModelAdmin(ModelAdmin):
create_view_class = CustomCreateView
Step 2 - Override the template to add a new button
Wagtail modeladmin uses a template path override approach, you will need to create a new template that aligns with your desired override (for example we may just want to override the product model create view only.
templates/modeladmin/products/products/create.html -> this says to override the create template for the products model within the products app.
Add the code below and check it is working, sometimes the templates path can be a bit tricky so ensure you can see the new button AND that the button has the data-next attribute that should be your create URL.
templates/modeladmin/products/products/create.html
{% extends "modeladmin/create.html" %}
{% load i18n wagtailadmin_tags %}
{% block form_actions %}
{{ block.super }}
<div class="dropdown dropup dropdown-button match-width">
<button type="submit" class="button button-secondary action-save button-longrunning" data-next="{{ create_url }}" data-clicked-text="{% trans 'Saving…' %}">
{% icon name="spinner" %}<em>{% trans 'Save & add another' %}</em>
</button>
</div>
{% endblock %}
Step 3 - Add JS to modify the form action URL on click
The next step requires a bit of JavaScript, we want to attach a different behaviour to the 'save and add another' button.
The JS used here does not require jQuery and should work in IE11
Once the DOM is loaded (which means JS can do things), find all the buttons with the data-next attribute.
Add a listener to the 'click' of each of those buttons which will dynamically update the form action attribute with the extra URL part.
This extra URL part gets read by the get_success_url method in the custom create class but only on a successful save, otherwise you will stay on the page and be able to fix errors.
{% block extra_js %}
{{ block.super}}
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('[data-next]').forEach(function(button) {
button.addEventListener('click', function(event) {
var form = document.querySelector('.content-wrapper form');
form.action = form.action + '?next=' + button.dataset.next;
});
});
})
</script>
{% endblock %}

How can I add a form made by formbuilder to every page in Wagtail?

Is there any way to add form (for example feedback form) to every page in CMS? I really like to use Wagtail FormBuilder so Editor guy can change fields.
My first idea is to create custom form page (inherited from AbstractEmailForm) as site root child and load it to base.html trough template tag. I can access page properties this way but I cant render the form.
Here is my template tag:
#register.assignment_tag(takes_context=True)
def get_feedback_form(context):
return context['request'].site.root_page.get_children().type(FeedbackFormPage).first()
And this is how I use it from base.html:
{% get_feedback_form as feedback_form %}
...
{{ feedback_form.specific.title }} <-- this works
{{ feedback_form.specific.form.as_p }} <-- this doesnt work
It would be nice somehow to create a form as snippet or add it to Site Settings, but I didnt find how to do that.
The main issue is how you are generating the form in the template with .form.as_p.
You will need to generate the form with the .get_form function, but you are best to do it within your template as the current user and page needs to be past in as arguments like this.
form = feedback_form_page.get_form(
page=feedback_form_page, user=request.user)
You can see how the form is built for the AbstractForm model here:
https://github.com/wagtail/wagtail/blob/master/wagtail/wagtailforms/models.py#L278
Full detailed example below, along with how you could work the form selection into the Site Settings module.
Link to a Form in Site Settings
Assuming you are referring to the Site Settings contrib module:
http://docs.wagtail.io/en/v1.13/reference/contrib/settings.html
The 'Edit Handlers' section of the documentation explains a great way to link to a page inside of your site settings.
http://docs.wagtail.io/en/v1.13/reference/contrib/settings.html?highlight=site%20settings#edit-handlers
Example (in models.py):
from wagtail.contrib.settings.models import BaseSetting, register_setting
# ...
#register_setting
class MyCustomSettings(BaseSetting):
feedback_form_page = models.ForeignKey(
'wagtailcore.Page', null=True, on_delete=models.SET_NULL)
panels = [
# note the page type declared within the pagechooserpanel
PageChooserPanel('feedback_form_page', ['base.FormPage']),
]
Once you set this model up, you will need to do makemigration and migrate for the changes to work in admin. You will then see inside the settings menu a sub-menu titled 'My Custom Settings'
Adding linked Form to every page
Add a block (so it can be overridden in templates) that has an include in your base template (eg. myapp/templates/base.html).
<!-- Footer -->
<footer>
{% block feedback_form %}{% include "includes/feedback_form.html" %}{% endblock feedback_form %}
{% include "includes/footer.html" %}
</footer>
Create an include template (eg. myapp/templates/includes/feedback_form.html)
{% load feedback_form_tags wagtailcore_tags %}
{% get_feedback_form as feedback_form %}
<form action="{% pageurl feedback_form.page %}" method="POST" role="form">
<h3>{{ feedback_form.page.title}}</h3>
{% csrf_token %}
{{ feedback_form.form.as_p }}
<input type="submit">
</form>
Build a Template Tag to get the form and page
Your template tag needs to build the form with the page's self.get_form() function. Eg. you your template tag (base/templatetags/feedback_form)
from django import template
from myapp.models import MyCustomSettings
register = template.Library()
# https://docs.djangoproject.com/en/1.9/howto/custom-template-tags/
#register.assignment_tag(takes_context=True)
def get_feedback_form(context):
request = context['request']
my_custom_settings = MyCustomSettings.for_site(request.site)
feedback_form_page = my_custom_settings.feedback_form_page.specific
form = feedback_form_page.get_form(
page=feedback_form_page, user=request.user)
return {'page': feedback_form_page, 'form': form}
This still works in wagtail 2.3 just need to replace
#register.assignment_tag(takes_context=True)
with
#register.simple_tag(takes_context=True) to conform with django 2.2
Also {% load feedback_form_tags wagtailcore_tags %} assumes your file inside of templates tags is named feedback_form_tags.py. I also added an __init__.py in the template tags folder although I'm not sure that was actually necessary.

Sending POST data from inside a Django template 'for loop'

With this HTML:
...
{% for thing in things %}
<form method="post">
{% csrf_token %}
{{ thing.name }}
{{ form.value }}
<input type="submit" value="Submit" />
</form>
{% endfor %}
...
My website lists multiple 'things' from my database, so there can be many forms generated on the one page. How can I somehow determine in my views.py, which 'thing's' form is being submitted?
More elaboration:
Imagine you have a page of objects listed one after the other, and each object has a like button associated with it, that adds a like to the object it is next to. That's essentially what I'm trying to do here.
The problem is, I have a form that can process the like, but how do I take that like and add it to the object that it's displayed next to on the page? (by the aforementioned 'for loop')
I'm completely confused on how to go about this, am I looking at the problem the wrong way, or is there a standard idiom around this problem that I don't know about?
Thank you :)
The most common design pattern for model instance updates is to provide the primary key of an object in the url where you are submitting your post data.
# urls.py
from django.conf.urls import *
from library.views import UpdateThing
urlpatterns = patterns('',
url('^update_thing/(?P<pk>[\w-]+)$', UpdateThing.as_view(), name='update_thing'),
# views.py
def my_view(request, pk=None):
if pk:
object = thing.objects.get(pk=pk)
form = MyModelForm(data=request.POST or None, instance=object)
if form.is_valid():
...
Now, let's specify (using Django's url template tag) that we want to submit post data for each object to the correct url.
{% for thing in things %}
<form method="post" action="{% url 'update_thing' thing.pk %}">
{% csrf_token %}
{{ thing.name }}
{{ form.value }}
<input type="submit" value="Submit" />
</form>
{% endfor %}
The url tag does a reverse lookup through your urls for the name kwarg supplied for a given url, and accepting positional arguments (such as, in this case, thing.pk) and, when needed, keyword arguments.
The standard way to handle multiple forms of the same kind on one page with Django is to use Formsets.
It handles the annoying details like displaying errors on one form while preserving the input on others etc.
However, in your specific case that might be overkill. If you just want to create a like for an object, there isn't really any user input that needs to be validated, so you don't really need a form. Just perform a POST to a specified URL, maybe with Javascript. If the user messes with the URL, you display a 404.

Creating a "Recent Posts" list in a sidebar.

I'm working on a simple blog app in Django, and i'm having trouble figuring out how to dynamically generate the five most recent posts in a side bar. Each of my views are class based and they extend a generic template, each view maps to one template which I believe is the correct way to do it. I've looked for a way to do this using template tags, but it seems Django doesn't like you to put any logic inside of your templates.
The problem I believe is that I want this to exist within my base.html because I want the recent posts to be displayed site-wide, is a view even supposed to map to your base.html or does that cause problems, i'm pretty new with this. I don't know how to approach this, whether i'm supposed to create a new view for base.html or if I should use my template tags, or if I should extend an existing view(but if I do that it won't be site wide?).
I essentially want the following(they're ordered in reverse chronological order)
{% for post in post_list[:4] %}
{{ post.title }}
{% endfor %}
You can use a template tag. More specifically, an inclusion tag is what you need. This allows you to insert a rendered snippet anywhere inside your template via a small view-like piece of code.
For example, create a templatetags/blog_tags.py file (it's important that you create the templatetags folder within your app; Django searches for them here by default) in your blog app and add the following:
from django import template
register = template.Library()
#register.inclusion_tag('blog/snippets/recent_posts.html')
def render_recent_blogposts():
return {
# This is just an example query, your actual models may vary
'post_list': BlogPost.objects.all().order_by("published_on")[:4]
}
now create a blog/snippets/recent_posts.html template (it can be anywhere as long as it mathecs the #register.inclusion_tag(...) above.):
<ul>
{% for post in post_list %}
<li> {{ post.title }}</li>
...
{% endfor %}
</ul>
finally, in your original template, you can now render your template tags:
<aside>
{% load blog_tags %}
{% render_recent_blogposts %}
</aside>

Possible to limit filters ManyToMany/Foreign Key in Django admin for a model where the relationship is defined on the other model?

So the title is a bit obtuse, I know, but I couldn't think of a more succinct way to state it. Here's the issue:
I've created two proxy models for "user types", both inheriting from django.contrib.auth.User. Each has a custom manager limiting the queryset to items belonging to a particular Group. Specifically, there's a PressUser which is any user belonging to the "Press" group and StaffUser which is any user in any other group than "Press".
The issue is that when I add 'groups' to list_filters on my StaffUsers modeladmin, the resulting filter options are every group available, including "Press", and not just groups available to StaffUsers.
I've research a bit online and came up with a custom filterspec that should produce the behavior I want, but the problem is that the User model's 'groups' attribute is actually a related_name applied from the Group model. As a result, I can't attach my filterspec to 'groups' in my proxy model.
Is there any other way to apply the filterspec? Alternatively, is there a better approach to filtering the items returned by the default filterspec?
So, I was able to solve my own problem. For those that might run into a similar situation, here are the steps:
The approach I took is to modify the change_list.html template and manually filter out the items I didn't want to be included. There's quite a number of changes to make, though.
First, add a changelist_view method to your ModelAdmin:
# myproject/account/admin.py
class StaffUserAdmin(models.ModelAdmin):
...
def changelist_view(self, request, extra_context=None):
groups = Group.objects.exclude(name__in=['Press',]).values_list('name')
extra_context = {
'groups': [x[0] for x in groups],
}
return super(StaffUserAdmin, self).changelist_view(request,
extra_context=extra_context)
Basically, all we're doing here is passing in the filtered list of Groups we want to use into the context for the template.
Second, create a change_list.html template for your app.
# myproject/templates/admin/auth/staffuser/change_list.html
{% extends "admin/change_list.html" %}
{% load admin_list %}
{% load i18n %}
{% load account_admin %}
{% block filters %}
{% if cl.has_filters %}
<div id="changelist-filter">
<h2>{% trans 'Filter' %}</h2>
{% for spec in cl.filter_specs %}
{% ifequal spec.title 'group' %}
{% admin_list_group_filter cl spec groups %}
{% else %}
{% admin_list_filter cl spec %}
{% endifequal %}
{% endfor %}
</div>
{% endif %}
{% endblock filters %}
This one deserves a little explanation. First, the template tag loads: admin_list is used for the default Django template tag responsible for rendering the filters, admin_list_filter, i18n is used for trans, and account_admin is for my custom template tag (discussed in a sec), admin_list_group_filter.
The variable spec.title holds the title of the field that's being filtered on. Since I'm trying to alter how the Groups filter is displayed, I'm checking if it equals 'groups'. If it does, then I use my custom template tag, otherwise, it falls back to the default Django template tag.
Third, we create the template tag. I basically just copied the default Django template tag and made the necessary modifications.
# myproject/account/templatetags/account_admin.py
from django.template import Library
register = Library()
def admin_list_group_filter(cl, spec, groups):
return {'title': spec.title, 'choices' : list(spec.choices(cl)), 'groups': groups }
admin_list_group_filter = register.inclusion_tag('admin/auth/group_filter.html')(admin_list_group_filter)
The only things that I've changed here are adding a new argument to the method called 'groups' so I can pass in my filtered list of groups from before, as well as adding a new key to the dictionary to pass that list into the context for the template tag. I've also changed the template the tag uses to a new one that we're about to create now.
Fourth, create the template for the template tag.
# myproject/templates/admin/auth/group_filter.html
{% load i18n %}
<h3>{% blocktrans with title as filter_title %} By {{ filter_title }} {% endblocktrans %}</h3>
<ul>
{% for choice in choices %}
{% if choice.display in groups %}
<li{% if choice.selected %} class="selected"{% endif %}>
{{ choice.display }}</li>
{% endif %}
{% endfor %}
</ul>
No big surprises here. All we're doing is putting all the pieces together. Each choice is a dictionary with all the values needed to construct the filter link. Specifically, choice.display holds the actual name of the instance that will be filtered by. Obviously enough, I've set up a check to see if this value is in my filtered list of groups I want to show, and only render the link if it is.
So, it's a bit involved but works remarkably well. Just like that, you have a list of filters that is exactly what you want instead of the default ones generated by Django.
I'm going to tell you off the bat that I've never done this before myself, so take it with a grain of salt.
What I'd suggest would be to override get_changelist on your ModelAdmin, to return a custom ChangeList class, which you can define somewhere in your admin module.
Your custom ChangeList class would simply override get_filters, so you can map your custom FilterSpec for the group field.
Another thing that might interest you are patches from the feature request ticket for specifying custom filter specs. The latest patch doesn't work for Django 1.3rc1 yet, although #bendavis78 recently posted that he's working on a new one, but depending on your version of Django it may apply cleanly.
It looks like it barely missed the cut to get included into the 1.3 milestone, so I figure it's going to make it into the trunk as soon as work beings on Django 1.4.

Categories