###   MODEL: Include something like this in your model   ###
class Person(models.Model):

    class Admin:
        list_display = ['name', 'select']

    name = models.CharField(maxlength=100)

    def select(self):
        return '<input type="checkbox" name="person" value="%s"/>' % self.id
    select.allow_tags = True
    select.short_description = ''



###   TEMPLATE: 'change_list.html'   ###
###   Put it in either admin/change_list.html, admin/<app>/<model>/change_list.html, etc   ###
{% extends "admin/change_list.html" %}

{% block result_list %}
<form action="delete/" method="post">
    {{ block.super }}
    {% for r in cl.get_query_set %}
        <input type="hidden" name="qs_obj" value="{{ r.id }}" id="qs_obj_{{ r.id }}">
    {% endfor %}
    <p>
        <input type="submit" name="delete_selected" id="id_delete_selected" value="Delete selected" />
        <input type="submit" name="delete_shown" id="id_delete_shown" value="Delete shown" />               
        <input type="submit" name="delete_all" id="id_delete_all" value="Delete all" /> 
    </p>
</form> 
{% endblock %}



###  VIEW: I just tossed this in 'admin.py' in the project root   ###
from django.contrib.admin.views.main import *

def delete(request, app_label, model_name):
    
    model = models.get_model(app_label, model_name)
    opts = model._meta  
    if model is None:
        raise Http404("App %r, model %r, not found" % (app_label, model_name))
    if not request.user.has_perm(app_label + '.' + model._meta.get_change_permission()):
        raise PermissionDenied  
    try:
        cl = ChangeList(request, model)
    except IncorrectLookupParameters:
        if ERROR_FLAG in request.GET.keys():
            return render_to_response('admin/invalid_setup.html', {'title': _('Database error')})
        return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')

    if 'delete_selected' in request.POST and model_name in request.POST:
        deleted = []
        for obj in cl.get_query_set().filter(id__in=request.POST.getlist(model_name)):
            obj.delete()
            deleted.append('"%s"' % str(obj))
        request.user.message_set.create(message=_('The %(name)s %(obj)s were deleted successfully.') % {'name': opts.verbose_name_plural, 'obj': ", ".join(deleted)})

    if 'delete_shown' in request.POST and 'qs_obj' in request.POST:
        deleted = []
        for obj in cl.get_query_set().filter(id__in=request.POST.getlist('qs_obj')):
            obj.delete()
            deleted.append('"%s"' % str(obj))
        request.user.message_set.create(message=_('The %(name)s %(obj)s were deleted successfully.') % {'name': opts.verbose_name_plural, 'obj': ", ".join(deleted)})
        
    if 'delete_all' in request.POST and cl.get_query_set().count() > 0:
        for obj in cl.get_query_set():
            obj.delete()
        request.user.message_set.create(message=_('All %(name)s were deleted successfully.') % {'name': opts.verbose_name_plural})      
    
    return HttpResponseRedirect('..')
change_list = staff_member_required(never_cache(change_list))



###   URLS: Add this line to your urls BEFORE the normal admin urls line   ###
    ('^admin/([^/]+)/([^/]+)/delete/$', 'admin.delete'),