Merge branch 'develop' of https://github.com/twoscoops/django-admin2 into get_example_proj_to_work

This commit is contained in:
Ethan Soergel 2013-06-18 13:18:26 -04:00
commit 7e86c1172e
10 changed files with 206 additions and 77 deletions

View file

@ -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>)

View file

@ -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()

View file

@ -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>

View file

@ -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))

View file

@ -43,7 +43,7 @@ The basic workflow of Djangos admin is, in a nutshell, “select an object, t
In these cases, Djangos 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, youll 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, youll 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
View 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)

View file

@ -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')

View file

@ -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

View file

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

View file

@ -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):