mirror of
https://github.com/jazzband/django-admin2.git
synced 2026-03-24 01:50:25 +00:00
Merge branch 'develop' of https://github.com/twoscoops/django-admin2 into get_example_proj_to_work
This commit is contained in:
commit
7e86c1172e
10 changed files with 206 additions and 77 deletions
|
|
@ -18,10 +18,11 @@ Developers
|
|||
* Chris Lawlor (@chrislawlor)
|
||||
* Ben Tappin <ben@mrben.co.uk>
|
||||
* Allison Kapture (@akapture)
|
||||
* Roman Gladkov (@d1ffuz0r / d1fffuz0r@gmail.com)
|
||||
* Roman Gladkov (@d1ffuz0r / <d1fffuz0r@gmail.com>)
|
||||
* Pau Rosello Van Schoor (@paurosello)
|
||||
* Wade Austin (@waustin)
|
||||
* the5fire (@the5fire)
|
||||
* Andrews Medina (@andrewsmedina / andrewsmedina@gmail.com)
|
||||
* Andrews Medina (@andrewsmedina / <andrewsmedina@gmail.com>)
|
||||
* Wade Austin (@waustin)
|
||||
* Douglas Miranda
|
||||
* Ethan Soergel (@esoergel / <esoergel@gmail.com>)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
from django.contrib import messages
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.template.response import TemplateResponse
|
||||
from django.views.generic import TemplateView
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.text import capfirst
|
||||
from django.utils.translation import ugettext_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from . import utils
|
||||
from . import permissions, utils
|
||||
from .viewmixins import AdminModel2Mixin
|
||||
|
||||
|
||||
def get_description(action):
|
||||
|
|
@ -16,45 +16,55 @@ def get_description(action):
|
|||
return capfirst(action.__name__.replace('_', ' '))
|
||||
|
||||
|
||||
class BaseListAction(object):
|
||||
class BaseListAction(AdminModel2Mixin, TemplateView):
|
||||
|
||||
def __init__(self, request, queryset):
|
||||
self.request = request
|
||||
permission_classes = (permissions.IsStaffPermission,)
|
||||
|
||||
empty_message = 'Items must be selected in order to perform actions on them. No items have been changed.'
|
||||
success_message = 'Successfully deleted %d %s'
|
||||
|
||||
queryset = None
|
||||
|
||||
def __init__(self, queryset, *args, **kwargs):
|
||||
self.queryset = queryset
|
||||
self.model = queryset.model
|
||||
self.options = utils.model_options(self.model)
|
||||
|
||||
options = utils.model_options(self.model)
|
||||
|
||||
self.app_label = options.app_label
|
||||
self.model_name = options.module_name
|
||||
|
||||
self.item_count = len(queryset)
|
||||
|
||||
if self.item_count <= 1:
|
||||
objects_name = self.options.verbose_name
|
||||
objects_name = options.verbose_name
|
||||
else:
|
||||
objects_name = self.options.verbose_name_plural
|
||||
objects_name = options.verbose_name_plural
|
||||
self.objects_name = unicode(objects_name)
|
||||
|
||||
@property
|
||||
def permission_name(self):
|
||||
return None
|
||||
super(BaseListAction, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
""" Replaced `get_queryset` from `AdminModel2Mixin`"""
|
||||
return self.queryset
|
||||
|
||||
def description(self):
|
||||
raise NotImplementedError("List action classes require a description attribute.")
|
||||
|
||||
def render_or_none(self):
|
||||
""" Returns either:
|
||||
Http response (anything)
|
||||
None object (shows the list)
|
||||
"""
|
||||
raise NotImplementedError("List action classes require a render_or_none method that returns either a None or HTTP response object.")
|
||||
raise NotImplementedError("List action classes require"
|
||||
" a description attribute.")
|
||||
|
||||
@property
|
||||
def template_for_display_nested_response(self):
|
||||
""" This is a required attribute for when using the `display_nested_response` method. """
|
||||
raise NotImplementedError("List actions classes using display_nested_response require a template")
|
||||
def default_template_name(self):
|
||||
raise NotImplementedError(
|
||||
"List actions classes using display_nested_response"
|
||||
" require a template"
|
||||
)
|
||||
|
||||
def display_nested_response(self):
|
||||
def get_context_data(self, **kwargs):
|
||||
""" Utility method when you want to display nested objects
|
||||
(such as during a bulk update/delete
|
||||
(such as during a bulk update/delete)
|
||||
"""
|
||||
context = super(BaseListAction, self).get_context_data()
|
||||
|
||||
def _format_callback(obj):
|
||||
opts = utils.model_options(obj)
|
||||
return '%s: %s' % (force_text(capfirst(opts.verbose_name)),
|
||||
|
|
@ -63,27 +73,41 @@ class BaseListAction(object):
|
|||
collector = utils.NestedObjects(using=None)
|
||||
collector.collect(self.queryset)
|
||||
|
||||
context = {
|
||||
'queryset': self.queryset,
|
||||
context.update({
|
||||
'view': self,
|
||||
'objects_name': self.objects_name,
|
||||
'queryset': self.queryset,
|
||||
'deletable_objects': collector.nested(_format_callback),
|
||||
}
|
||||
return TemplateResponse(self.request, self.template_for_display_nested_response, context)
|
||||
})
|
||||
|
||||
def __call__(self):
|
||||
# We check whether the user has permission to delete the objects in the
|
||||
# queryset.
|
||||
if self.permission_name and not self.request.user.has_perm(self.permission_name):
|
||||
message = _("Permission to '%s' denied" % force_text(self.description))
|
||||
messages.add_message(self.request, messages.INFO, message)
|
||||
return None
|
||||
return context
|
||||
|
||||
def get(self, request):
|
||||
if self.item_count > 0:
|
||||
return self.render_or_none()
|
||||
return super(BaseListAction, self).get(request)
|
||||
|
||||
message = _(self.empty_message)
|
||||
messages.add_message(request, messages.INFO, message)
|
||||
|
||||
return None
|
||||
|
||||
def post(self, request):
|
||||
if request.POST.get('confirmed'):
|
||||
if self.process_queryset() is None:
|
||||
|
||||
message = _(self.success_message % (
|
||||
self.item_count, self.objects_name)
|
||||
)
|
||||
messages.add_message(request, messages.INFO, message)
|
||||
|
||||
return None
|
||||
else:
|
||||
message = _("Items must be selected in order to perform actions on them. No items have been changed.")
|
||||
messages.add_message(self.request, messages.INFO, message)
|
||||
return None
|
||||
# The user has not confirmed that they want to delete the objects, so
|
||||
# render a template asking for their confirmation.
|
||||
return self.get(request)
|
||||
|
||||
def process_queryset(self):
|
||||
raise NotImplementedError('Must be provided to do some actions with queryset')
|
||||
|
||||
|
||||
class DeleteSelectedAction(BaseListAction):
|
||||
|
|
@ -91,31 +115,13 @@ class DeleteSelectedAction(BaseListAction):
|
|||
# `get_deleted_objects` in contrib.admin.util for how this is currently
|
||||
# done. (Hint: I think we can do better.)
|
||||
|
||||
default_template_name = "actions/delete_selected_confirmation.html"
|
||||
|
||||
description = ugettext_lazy("Delete selected items")
|
||||
permission_classes = BaseListAction.permission_classes + (
|
||||
permissions.ModelDeletePermission,
|
||||
)
|
||||
|
||||
@property
|
||||
def permission_name(self):
|
||||
return '%s.delete.%s' \
|
||||
% (self.options.app_label, self.options.object_name.lower())
|
||||
|
||||
def render_or_none(self):
|
||||
|
||||
if self.request.POST.get('confirmed'):
|
||||
# The user has confirmed that they want to delete the objects.
|
||||
num_objects_deleted = len(self.queryset)
|
||||
self.queryset.delete()
|
||||
message = _("Successfully deleted %d %s" % \
|
||||
(num_objects_deleted, self.objects_name))
|
||||
messages.add_message(self.request, messages.INFO, message)
|
||||
return None
|
||||
else:
|
||||
# The user has not confirmed that they want to delete the objects, so
|
||||
# render a template asking for their confirmation.
|
||||
return self.display_nested_response()
|
||||
|
||||
@property
|
||||
def template_for_display_nested_response(self):
|
||||
# TODO - power this off the ADMIN2_THEME_DIRECTORY setting
|
||||
return "admin2/bootstrap/actions/delete_selected_confirmation.html"
|
||||
|
||||
|
||||
def process_queryset(self):
|
||||
# The user has confirmed that they want to delete the objects.
|
||||
self.get_queryset().delete()
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
{% extends "admin2/bootstrap/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load admin2_tags i18n %}
|
||||
|
||||
{% block title %}Are you sure?{% endblock title %}
|
||||
|
||||
{% block page_title %}Are you sure?{% endblock page_title %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<li><a href="{% url "admin2:dashboard" %}">Home</a> <span class="divider">/</span></li>
|
||||
<li><a href="{% url "admin2:app_index" app_label=app_label %}">{{ app_label|title }}</a> <span class="divider">/</span></li>
|
||||
<li><a href="{% url view|admin2_urlname:"index" %}">{{ model_name_pluralized|title }}</a> <span class="divider">/</span></li>
|
||||
<li class="active">{% trans "Delete" %}</li>
|
||||
{% endblock breadcrumbs %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
<p>{% blocktrans with objects_name=objects_name %}Are you sure you want to delete the selected {{ objects_name }}? All of the following items will be deleted:{% endblocktrans %}</p>
|
||||
|
|
|
|||
|
|
@ -63,11 +63,12 @@ class ModelListView(AdminModel2Mixin, generic.ListView):
|
|||
|
||||
# If action_callable is a class subclassing from actions.BaseListAction
|
||||
# then we generate the callable object.
|
||||
if hasattr(action_callable, "render_or_none"):
|
||||
response = action_callable(request, queryset)()
|
||||
if hasattr(action_callable, "process_queryset"):
|
||||
response = action_callable.as_view(queryset=queryset)(request)
|
||||
else:
|
||||
# generate the reponse if a function.
|
||||
response = action_callable(request, queryset)
|
||||
|
||||
if response is None:
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
else:
|
||||
|
|
@ -85,7 +86,6 @@ class ModelListView(AdminModel2Mixin, generic.ListView):
|
|||
else:
|
||||
return "%s__icontains" % field_name
|
||||
|
||||
|
||||
use_distinct = False
|
||||
|
||||
orm_lookups = [construct_search(str(search_field))
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ The basic workflow of Django’s admin is, in a nutshell, “select an object, t
|
|||
|
||||
In these cases, 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. Using our sample models, let's pretend we wrote a blog article about Django and our mother put in a whole bunch of embarressing comments. Rather than cherry-pick the comments, we want to delete the whole batch.
|
||||
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. Using our sample models, let's pretend we wrote a blog article about Django and our mother put in a whole bunch of embarressing comments. Rather than cherry-pick the comments, we want to delete the whole batch.
|
||||
|
||||
In our blog/admin.py module we write:
|
||||
|
||||
|
|
@ -54,11 +54,23 @@ In our blog/admin.py module we write:
|
|||
from .models import Post, Comment
|
||||
|
||||
class DeleteAllComments(djadmin2.actions.BaseListAction):
|
||||
description = "Delete selected items"
|
||||
template = "blog/actions/delete_all_comments_confirmation.html
|
||||
|
||||
description = 'Delete selected items'
|
||||
default_template_name = 'actions/delete_all_comments_confirmation.html'
|
||||
success_message = 'Successfully deleted %d %s' # first argument - items count, second - verbose_name[_plural]
|
||||
|
||||
def process_query(self):
|
||||
"""Every action must provide this method"""
|
||||
self.get_queryset().delete()
|
||||
|
||||
|
||||
def custom_function_action(request, queryset):
|
||||
print(queryset.count())
|
||||
|
||||
custom_function_action.description = 'Do other action'
|
||||
|
||||
class PostAdmin(djadmin2.ModelAdmin2):
|
||||
actions = [DeleteAllComments]
|
||||
actions = [DeleteAllComments, custom_function_action]
|
||||
|
||||
djadmin2.default.register(Post, PostAdmin)
|
||||
djadmin2.default.register(Comment)
|
||||
|
|
@ -75,4 +87,4 @@ In our blog/admin.py module we write:
|
|||
.. _`QuerySet.delete()`: https://docs.djangoproject.com/en/dev/ref/models/querysets/#django.db.models.query.QuerySet.delete
|
||||
.. _`Object deletion`: https://docs.djangoproject.com/en/dev/topics/db/queries/#topics-db-queries-delete
|
||||
|
||||
Read on to find out how to add your own actions to this list.
|
||||
Read on to find out how to add your own actions to this list.
|
||||
|
|
|
|||
19
example/blog/actions.py
Normal file
19
example/blog/actions.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
from djadmin2.actions import BaseListAction
|
||||
from djadmin2 import permissions
|
||||
|
||||
from django.utils.translation import ugettext_lazy
|
||||
|
||||
|
||||
class CustomPublishAction(BaseListAction):
|
||||
|
||||
permission_classes = BaseListAction.permission_classes + (
|
||||
permissions.ModelChangePermission,
|
||||
)
|
||||
|
||||
description = ugettext_lazy('Publish selected items')
|
||||
success_message = 'Successfully published %d %s'
|
||||
|
||||
default_template_name = "actions/publish_selected_items.html"
|
||||
|
||||
def process_queryset(self):
|
||||
self.get_queryset().update(published=True)
|
||||
|
|
@ -1,12 +1,15 @@
|
|||
# Import your custom models
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.contrib import messages
|
||||
|
||||
from rest_framework.relations import PrimaryKeyRelatedField
|
||||
|
||||
import djadmin2
|
||||
from djadmin2.actions import DeleteSelectedAction
|
||||
from djadmin2.forms import UserCreationForm, UserChangeForm
|
||||
from djadmin2.apiviews import Admin2APISerializer
|
||||
|
||||
from .actions import CustomPublishAction
|
||||
from .models import Post, Comment
|
||||
|
||||
|
||||
|
|
@ -33,10 +36,19 @@ class CommentInline(djadmin2.Admin2Inline):
|
|||
model = Comment
|
||||
|
||||
|
||||
def unpublish_items(request, queryset):
|
||||
queryset.update(published=False)
|
||||
messages.add_message(request, messages.INFO, u'Items unpublished')
|
||||
|
||||
unpublish_items.description = 'Unpublish selected items'
|
||||
|
||||
|
||||
class PostAdmin(djadmin2.ModelAdmin2):
|
||||
list_actions = [DeleteSelectedAction, CustomPublishAction, unpublish_items]
|
||||
inlines = [CommentInline]
|
||||
search_fields = ('title', '^body')
|
||||
|
||||
|
||||
class CommentAdmin(djadmin2.ModelAdmin2):
|
||||
search_fields = ('body', '=post__title')
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from django.utils.encoding import python_2_unicode_compatible
|
|||
class Post(models.Model):
|
||||
title = models.CharField(max_length=255)
|
||||
body = models.TextField()
|
||||
published = models.BooleanField(default=False)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.title
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
{% extends "admin2/bootstrap/base.html" %}
|
||||
{% load admin2_tags i18n %}
|
||||
|
||||
{% block title %}Are you sure?{% endblock title %}
|
||||
|
||||
{% block page_title %}Are you sure?{% endblock page_title %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<li><a href="{% url "admin2:dashboard" %}">Home</a> <span class="divider">/</span></li>
|
||||
<li><a href="{% url "admin2:app_index" app_label=app_label %}">{{ app_label|title }}</a> <span class="divider">/</span></li>
|
||||
<li><a href="{% url view|admin2_urlname:"index" %}">{{ model_name_pluralized|title }}</a> <span class="divider">/</span></li>
|
||||
<li class="active">{% trans "Publish" %}</li>
|
||||
{% endblock breadcrumbs %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
<p>{% blocktrans with objects_name=objects_name %}Are you sure you want to publish the selected {{ objects_name }}? All of the following items will be published:{% endblocktrans %}</p>
|
||||
|
||||
<ul>
|
||||
{{ deletable_objects|unordered_list }}
|
||||
</ul>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="confirmed" value="yes" />
|
||||
<input type="hidden" name="action" value="CustomPublishAction" />
|
||||
{% for item in queryset %}
|
||||
<input type="hidden" name="selected_model_pk" value="{{ item.pk }}" />
|
||||
{% endfor %}
|
||||
<button class="btn btn-small btn-danger" type="submit">{% trans "Publish" %}</button>
|
||||
</form>
|
||||
|
||||
{% endblock content %}
|
||||
|
|
@ -89,6 +89,42 @@ class PostListTest(BaseIntegrationTest):
|
|||
self.assertNotContains(response, "a_post_title")
|
||||
|
||||
|
||||
class PostListTestCustomAction(BaseIntegrationTest):
|
||||
|
||||
def test_publish_action_displayed_in_list(self):
|
||||
response = self.client.get(reverse("admin2:blog_post_index"))
|
||||
self.assertInHTML('<a tabindex="-1" href="#" data-name="action" data-value="CustomPublishAction">Publish selected items</a>', response.content)
|
||||
|
||||
def test_publish_selected_items(self):
|
||||
post = Post.objects.create(title="a_post_title",
|
||||
body="body",
|
||||
published=False)
|
||||
self.assertEqual(Post.objects.filter(published=True).count(), 0)
|
||||
|
||||
params = {'action': 'CustomPublishAction',
|
||||
'selected_model_pk': str(post.pk),
|
||||
'confirmed': 'yes'}
|
||||
response = self.client.post(reverse("admin2:blog_post_index"), params)
|
||||
self.assertRedirects(response, reverse("admin2:blog_post_index"))
|
||||
|
||||
self.assertEqual(Post.objects.filter(published=True).count(), 1)
|
||||
|
||||
def test_unpublish_action_displayed_in_list(self):
|
||||
response = self.client.get(reverse("admin2:blog_post_index"))
|
||||
self.assertInHTML('<a tabindex="-1" href="#" data-name="action" data-value="unpublish_items">Unpublish selected items</a>', response.content)
|
||||
|
||||
def test_unpublish_selected_items(self):
|
||||
post = Post.objects.create(title="a_post_title",
|
||||
body="body",
|
||||
published=True)
|
||||
self.assertEqual(Post.objects.filter(published=True).count(), 1)
|
||||
|
||||
params = {'action': 'unpublish_items',
|
||||
'selected_model_pk': str(post.pk)}
|
||||
response = self.client.post(reverse("admin2:blog_post_index"), params)
|
||||
self.assertRedirects(response, reverse("admin2:blog_post_index"))
|
||||
|
||||
self.assertEqual(Post.objects.filter(published=True).count(), 0)
|
||||
|
||||
|
||||
class PostDetailViewTest(BaseIntegrationTest):
|
||||
|
|
|
|||
Loading…
Reference in a new issue