├── Makefile
├── requirements.txt
├── bulk_admin
├── models.py
├── __init__.py
├── templates
│ └── bulk_admin
│ │ ├── bulk_change_list.html
│ │ ├── bulk_popup_response.html
│ │ └── bulk_change_form.html
├── static
│ └── bulk_admin
│ │ └── js
│ │ ├── bulk-related.js
│ │ └── bulk.js
└── admin.py
├── example_project
├── views.py
├── __init__.py
├── wsgi.py
├── admin.py
├── models.py
├── urls.py
├── settings.py
└── tests.py
├── screenshots
├── bulk_add_1.png
├── bulk_add_2.png
├── bulk_edit_1.png
├── bulk_select_1.png
├── bulk_select_2.png
├── bulk_select_3.png
├── bulk_upload_1.png
└── bulk_upload_2.png
├── .gitignore
├── MANIFEST.in
├── manage.py
├── tox.ini
├── CHANGES.rst
├── setup.py
├── LICENSE
└── README.rst
/Makefile:
--------------------------------------------------------------------------------
1 | test:
2 | ./manage.py test
3 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Django==1.8.4
2 | tox==2.1.1
3 |
--------------------------------------------------------------------------------
/bulk_admin/models.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
--------------------------------------------------------------------------------
/example_project/views.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
--------------------------------------------------------------------------------
/example_project/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
--------------------------------------------------------------------------------
/screenshots/bulk_add_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/purelabs/django-bulk-admin/HEAD/screenshots/bulk_add_1.png
--------------------------------------------------------------------------------
/screenshots/bulk_add_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/purelabs/django-bulk-admin/HEAD/screenshots/bulk_add_2.png
--------------------------------------------------------------------------------
/screenshots/bulk_edit_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/purelabs/django-bulk-admin/HEAD/screenshots/bulk_edit_1.png
--------------------------------------------------------------------------------
/screenshots/bulk_select_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/purelabs/django-bulk-admin/HEAD/screenshots/bulk_select_1.png
--------------------------------------------------------------------------------
/screenshots/bulk_select_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/purelabs/django-bulk-admin/HEAD/screenshots/bulk_select_2.png
--------------------------------------------------------------------------------
/screenshots/bulk_select_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/purelabs/django-bulk-admin/HEAD/screenshots/bulk_select_3.png
--------------------------------------------------------------------------------
/screenshots/bulk_upload_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/purelabs/django-bulk-admin/HEAD/screenshots/bulk_upload_1.png
--------------------------------------------------------------------------------
/screenshots/bulk_upload_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/purelabs/django-bulk-admin/HEAD/screenshots/bulk_upload_2.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | db.sqlite3
2 | env/
3 | media/
4 | *.pyc
5 | .DS_Store
6 | .python-version
7 | .tox
8 | django_bulk_admin.egg-info/
9 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE
2 | include README.rst
3 | recursive-include bulk_admin/locale *
4 | recursive-include bulk_admin/static *
5 | recursive-include bulk_admin/templates *
6 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings")
7 |
8 | from django.core.management import execute_from_command_line
9 |
10 | execute_from_command_line(sys.argv)
11 |
--------------------------------------------------------------------------------
/bulk_admin/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
3 | from bulk_admin.admin import (
4 | BulkModelAdmin,
5 | StackedBulkInlineModelAdmin,
6 | TabularBulkInlineModelAdmin,
7 | )
8 |
9 | __all__ = [
10 | BulkModelAdmin,
11 | StackedBulkInlineModelAdmin,
12 | TabularBulkInlineModelAdmin,
13 | ]
14 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist =
3 | py{27,32,33,34}-django{17,18}
4 |
5 | [testenv]
6 | basepython =
7 | py27: python2.7
8 | py32: python3.2
9 | py33: python3.3
10 | py34: python3.4
11 | commands = make test
12 | deps =
13 | django17: Django>=1.7,<1.8
14 | django18: Django>=1.8,<1.9
15 | whitelist_externals = make
16 |
--------------------------------------------------------------------------------
/CHANGES.rst:
--------------------------------------------------------------------------------
1 | Changelog
2 | =========
3 |
4 | 0.1.1
5 | -----
6 |
7 | * Enforce the use of the same model inside BulkInlineModelAdmin and BulkModelAdmin
8 | * Added errors to change form context
9 | * Documented caveat: No admin logs are generated for bulk operations
10 | * Compatibility with python 2.7, 3.2, 3.3, 3.4 and Django 1.7 and 1.8
11 | * Added tox
12 | * Removed dependency to django-sortedm2m
13 | * Removed migrations in example_project
14 |
--------------------------------------------------------------------------------
/bulk_admin/templates/bulk_admin/bulk_change_list.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin/change_list.html' %}
2 |
3 | {% load i18n admin_urls %}
4 |
5 | {% block object-tools-items %}
6 |
7 | {% url cl.opts|admin_urlname:'bulk' as bulk_url %}
8 |
9 | {% blocktrans with cl.opts.verbose_name_plural as name %}Bulk add {{ name }}{% endblocktrans %}
10 |
11 |
12 | {{ block.super }}
13 | {% endblock %}
14 |
--------------------------------------------------------------------------------
/example_project/wsgi.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
3 | """
4 | WSGI config for example_project project.
5 |
6 | It exposes the WSGI callable as a module-level variable named ``application``.
7 |
8 | For more information on this file, see
9 | https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/
10 | """
11 |
12 | import os
13 |
14 | from django.core.wsgi import get_wsgi_application
15 |
16 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings")
17 |
18 | application = get_wsgi_application()
19 |
--------------------------------------------------------------------------------
/bulk_admin/templates/bulk_admin/bulk_popup_response.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ media }}
6 |
7 |
8 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/example_project/admin.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
3 | from django.contrib import admin
4 | from example_project import models
5 |
6 | import bulk_admin
7 |
8 |
9 | class ProjectInline(bulk_admin.StackedBulkInlineModelAdmin):
10 | model = models.Project
11 | raw_id_fields = ('images',)
12 |
13 |
14 | @admin.register(models.Image)
15 | class ImageAdmin(bulk_admin.BulkModelAdmin):
16 | search_fields = ('title',)
17 |
18 |
19 | @admin.register(models.Project)
20 | class ProjectAdmin(bulk_admin.BulkModelAdmin):
21 | raw_id_fields = ('images',)
22 | bulk_inline = ProjectInline
23 |
--------------------------------------------------------------------------------
/example_project/models.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
3 | from django.db import models
4 | from django.utils.encoding import python_2_unicode_compatible
5 |
6 |
7 | @python_2_unicode_compatible
8 | class Image(models.Model):
9 | title = models.CharField(max_length=255, unique=True)
10 | data = models.FileField(null=True, blank=True)
11 |
12 | def __str__(self):
13 | return self.title
14 |
15 |
16 | @python_2_unicode_compatible
17 | class Project(models.Model):
18 | title = models.CharField(max_length=255, unique=True)
19 | images = models.ManyToManyField(Image, blank=True)
20 |
21 | def __str__(self):
22 | return self.title
23 |
--------------------------------------------------------------------------------
/example_project/urls.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
3 | """example_project URL Configuration
4 |
5 | The `urlpatterns` list routes URLs to views. For more information please see:
6 | https://docs.djangoproject.com/en/1.8/topics/http/urls/
7 | Examples:
8 | Function views
9 | 1. Add an import: from my_app import views
10 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
11 | Class-based views
12 | 1. Add an import: from other_app.views import Home
13 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
14 | Including another URLconf
15 | 1. Add an import: from blog import urls as blog_urls
16 | 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls))
17 | """
18 | from django.conf.urls import include, url
19 | from django.contrib import admin
20 |
21 | urlpatterns = [
22 | url(r'^admin/', include(admin.site.urls)),
23 | ]
24 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 | from setuptools import setup, find_packages
3 |
4 | with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme:
5 | README = readme.read()
6 |
7 | # allow setup.py to be run from any path
8 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
9 |
10 | setup(
11 | name='django-bulk-admin',
12 | version='0.1.1',
13 | packages=find_packages(exclude=('example_project*', 'screenshots',)),
14 | include_package_data=True,
15 | license='BSD',
16 | description='Django bulk admin enables you to bulk add, bulk edit, bulk upload and bulk select in django admin.',
17 | long_description=README,
18 | url='https://github.com/purelabs/django-bulk-admin',
19 | author='Ruben Grill',
20 | author_email='ruben.grill@gmail.com',
21 | install_requires=[
22 | 'Django>=1.7',
23 | ],
24 | classifiers=[
25 | 'Environment :: Web Environment',
26 | 'Framework :: Django',
27 | 'Intended Audience :: Developers',
28 | 'License :: OSI Approved :: BSD License',
29 | 'Operating System :: OS Independent',
30 | 'Programming Language :: Python',
31 | 'Programming Language :: Python :: 2',
32 | 'Programming Language :: Python :: 2.7',
33 | 'Programming Language :: Python :: 3',
34 | 'Programming Language :: Python :: 3.2',
35 | 'Programming Language :: Python :: 3.3',
36 | 'Programming Language :: Python :: 3.4',
37 | 'Topic :: Software Development :: Libraries :: Python Modules',
38 | ],
39 | )
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) Ruben Grill and individual contributors.
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification,
5 | are permitted provided that the following conditions are met:
6 |
7 | 1. Redistributions of source code must retain the above copyright notice,
8 | this list of conditions and the following disclaimer.
9 |
10 | 2. Redistributions in binary form must reproduce the above copyright
11 | notice, this list of conditions and the following disclaimer in the
12 | documentation and/or other materials provided with the distribution.
13 |
14 | 3. Neither the name of Django nor the names of its contributors may be used
15 | to endorse or promote products derived from this software without
16 | specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/bulk_admin/static/bulk_admin/js/bulk-related.js:
--------------------------------------------------------------------------------
1 | (function($) {
2 | 'use strict';
3 |
4 | function windowname_to_id(text) {
5 | text = text.replace(/__dot__/g, '.');
6 | text = text.replace(/__dash__/g, '-');
7 | return text;
8 | }
9 |
10 | function dismissAddRelatedObjectPopup(openerWindow, newIds, newReprs) {
11 | var name = windowname_to_id(window.name);
12 | var elem = openerWindow.document.getElementById(name);
13 | if (elem) {
14 | var elemName = elem.nodeName.toUpperCase();
15 | if (elemName === 'SELECT') {
16 | for (var i = 0; i < newIds.length; i++) {
17 | elem.options[elem.options.length] = new Option(newReprs[i], newIds[i], true, true);
18 | }
19 | } else if (elemName === 'INPUT') {
20 | if (elem.className.indexOf('vManyToManyRawIdAdminField') !== -1 && elem.value) {
21 | elem.value += ',' + newIds.join(',');
22 | } else {
23 | elem.value = newIds.join(',');
24 | }
25 | }
26 | // Trigger a change event to update related links if required.
27 | openerWindow.django.jQuery(elem).trigger('change');
28 | } else {
29 | var toId = name + "_to";
30 |
31 | for (var i = 0; i < newIds.length; i++) {
32 | openerWindow.SelectBox.add_to_cache(toId, new Option(newRepr, newId));
33 | openerWindow.SelectBox.redisplay(toId);
34 | }
35 | }
36 | window.close();
37 | }
38 |
39 | window.dismissAddRelatedObjectPopup = dismissAddRelatedObjectPopup;
40 |
41 | })();
42 |
--------------------------------------------------------------------------------
/bulk_admin/templates/bulk_admin/bulk_change_form.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin/change_form.html' %}
2 |
3 | {% load i18n admin_urls %}
4 |
5 | {% if not is_popup %}
6 | {% block breadcrumbs %}
7 |
13 | {% endblock %}
14 | {% endif %}
15 |
16 | {% block object-tools %}
17 | {% trans "Files are being uploaded..." as submitting_message %}
18 | {% if bulk %}
19 |
28 |
43 | {% else %}
44 | {{ block.super }}
45 | {% endif %}
46 | {% endblock %}
47 |
--------------------------------------------------------------------------------
/example_project/settings.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
3 | """
4 | Django settings for example_project project.
5 |
6 | Generated by 'django-admin startproject' using Django 1.8.4.
7 |
8 | For more information on this file, see
9 | https://docs.djangoproject.com/en/1.8/topics/settings/
10 |
11 | For the full list of settings and their values, see
12 | https://docs.djangoproject.com/en/1.8/ref/settings/
13 | """
14 |
15 | import django
16 |
17 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
18 | import os
19 |
20 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
21 |
22 |
23 | # Quick-start development settings - unsuitable for production
24 | # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/
25 |
26 | # SECURITY WARNING: keep the secret key used in production secret!
27 | SECRET_KEY = 'h70^2zl-an7*k3*x4+mf3jc5)pzxt+p9i6uztrrb!_u_+6q38y'
28 |
29 | # SECURITY WARNING: don't run with debug turned on in production!
30 | DEBUG = True
31 |
32 | ALLOWED_HOSTS = []
33 |
34 |
35 | # Application definition
36 |
37 | INSTALLED_APPS = (
38 | 'django.contrib.admin',
39 | 'django.contrib.auth',
40 | 'django.contrib.contenttypes',
41 | 'django.contrib.sessions',
42 | 'django.contrib.messages',
43 | 'django.contrib.staticfiles',
44 | 'bulk_admin',
45 | 'example_project',
46 | )
47 |
48 | MIDDLEWARE_CLASSES = (
49 | 'django.contrib.sessions.middleware.SessionMiddleware',
50 | 'django.middleware.common.CommonMiddleware',
51 | 'django.middleware.csrf.CsrfViewMiddleware',
52 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
53 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
54 | 'django.contrib.messages.middleware.MessageMiddleware',
55 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
56 | )
57 |
58 | ROOT_URLCONF = 'example_project.urls'
59 |
60 | if django.VERSION < (1, 8):
61 | TEMPLATE_DEBUG = True
62 |
63 | TEMPLATES = [
64 | {
65 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
66 | 'DIRS': [],
67 | 'APP_DIRS': True,
68 | 'OPTIONS': {
69 | 'context_processors': [
70 | 'django.template.context_processors.debug',
71 | 'django.template.context_processors.request',
72 | 'django.contrib.auth.context_processors.auth',
73 | 'django.contrib.messages.context_processors.messages',
74 | ],
75 | },
76 | },
77 | ]
78 |
79 | WSGI_APPLICATION = 'example_project.wsgi.application'
80 |
81 |
82 | # Database
83 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases
84 |
85 | DATABASES = {
86 | 'default': {
87 | 'ENGINE': 'django.db.backends.sqlite3',
88 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
89 | }
90 | }
91 |
92 |
93 | # Internationalization
94 | # https://docs.djangoproject.com/en/1.8/topics/i18n/
95 |
96 | LANGUAGE_CODE = 'en-us'
97 |
98 | TIME_ZONE = 'UTC'
99 |
100 | USE_I18N = True
101 |
102 | USE_L10N = True
103 |
104 | USE_TZ = True
105 |
106 |
107 | # Static files (CSS, JavaScript, Images)
108 | # https://docs.djangoproject.com/en/1.8/howto/static-files/
109 |
110 | STATIC_URL = '/static/'
111 |
112 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')
113 |
--------------------------------------------------------------------------------
/bulk_admin/static/bulk_admin/js/bulk.js:
--------------------------------------------------------------------------------
1 | (function($) {
2 | 'use strict';
3 |
4 | $.fn.bulkUpload = function(opts) {
5 | var options = $.extend({}, $.fn.bulkUpload.defaults, opts);
6 | var $this = $(this);
7 | var submitted = false;
8 |
9 | $this.click(function() {
10 | var field = $this.data('field');
11 |
12 | var $form = $('