├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── admin_export ├── __init__.py ├── admin.py ├── models.py ├── templates │ └── admin_export │ │ ├── export.html │ │ └── fields.html ├── urls.py └── views.py ├── pytest.ini ├── requirements.txt ├── setup.py ├── test_requirements.txt └── tests ├── __init__.py ├── models.py ├── settings.py └── test_admin_export.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .cache 41 | nosetests.xml 42 | coverage.xml 43 | 44 | # Translations 45 | *.mo 46 | *.pot 47 | 48 | # Django stuff: 49 | *.log 50 | 51 | # Sphinx documentation 52 | docs/_build/ 53 | 54 | # PyBuilder 55 | target/ 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2014 Burke Software and Consulting LLC 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 3. The name of the author may not be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 16 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include admin_export/templates * 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project is no longer active. Check out a fork at https://github.com/fgmacedo/django-export-action 2 | 3 | 4 | django-admin-export 5 | =================== 6 | 7 | Generic export to XLSX/HTML/CSV action for the Django admin interface. 8 | 9 | Meant for fast and simple exports and lets you choose the data to export. 10 | 11 | Features 12 | -------- 13 | - Drop in application 14 | - Traverse model relations recursively 15 | 16 | django-admin-export is built with [django-report-utils](https://github.com/burke-software/django-report-utils). 17 | For a a full query builder try using [django-report-builder](https://github.com/burke-software/django-report-builder). 18 | 19 | Install 20 | ------- 21 | 1. ``pip install django-admin-export`` 22 | 2. Add ``admin_export`` to INSTALLED_APPS 23 | 3. Add ``url(r'^admin_export/', include("admin_export.urls", namespace="admin_export")),`` to your project's urls.py 24 | 25 | Usage 26 | ----- 27 | Go to any admin page, select fields, then select the export to xls action. Then check off any fields you want to export. 28 | 29 | Running tests 30 | ------------- 31 | 32 | 1. Acquire a checkout of the repository 33 | 2. ``pip install -e . -r test_requirements.txt`` 34 | 3. ``py.test tests`` 35 | 36 | Security 37 | -------- 38 | 39 | This project assumes staff users are trusted. There may be ways for users to manipulate this project to get more data access than they should have. 40 | -------------------------------------------------------------------------------- /admin_export/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burke-software/django-admin-export/65f31edd1149d859888c27cabd60222c227729cf/admin_export/__init__.py -------------------------------------------------------------------------------- /admin_export/admin.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from django.contrib import admin 3 | from django.contrib.contenttypes.models import ContentType 4 | from django.core.urlresolvers import reverse, NoReverseMatch 5 | from django.http import HttpResponseRedirect 6 | 7 | 8 | def export_simple_selected_objects(modeladmin, request, queryset): 9 | selected = list(queryset.values_list('id', flat=True)) 10 | ct = ContentType.objects.get_for_model(queryset.model) 11 | 12 | try: 13 | url = reverse("admin_export:export") 14 | except NoReverseMatch: # Old configuration, maybe? Fall back to old URL scheme. 15 | url = "/admin_export/export_to_xls/" 16 | 17 | if len(selected) > 1000: 18 | session_key = "admin_export_%s" % uuid.uuid4() 19 | request.session[session_key] = selected 20 | return HttpResponseRedirect("%s?ct=%s&session_key=%s" % (url, ct.pk, session_key)) 21 | else: 22 | return HttpResponseRedirect("%s?ct=%s&ids=%s" % (url, ct.pk, ",".join(str(pk) for pk in selected))) 23 | 24 | export_simple_selected_objects.short_description = "Export selected items..." 25 | 26 | admin.site.add_action(export_simple_selected_objects) 27 | -------------------------------------------------------------------------------- /admin_export/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burke-software/django-admin-export/65f31edd1149d859888c27cabd60222c227729cf/admin_export/models.py -------------------------------------------------------------------------------- /admin_export/templates/admin_export/export.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/base_site.html' %} 2 | {% load i18n admin_urls admin_static %} 3 | 4 | {% block extrahead %} 5 | {{ block.super }} 6 | 7 | 28 | {% endblock %} 29 | 30 | {% block breadcrumbs %} 31 | 39 | {% endblock %} 40 | 41 | 42 | {% block content %} 43 |

Export {{ opts.verbose_name_plural }} ({{ queryset.count }})

44 |

45 | {% for object in queryset|slice:":10" %} 46 | {{ object }} 47 | {% if not forloop.last %},{% endif %} 48 | {% endfor %} 49 | {% if queryset.count > 10 %}...{% endif %} 50 |

51 | 52 |
53 |
54 | {% csrf_token %} 55 | 56 | 57 | 60 | 63 | 64 | 65 | {% include "admin_export/fields.html" %} 66 |
58 | 59 | 61 | 62 |
67 |
68 |
69 |
70 | 77 | 78 |
79 |
80 | 81 | {% endblock %} 82 | -------------------------------------------------------------------------------- /admin_export/templates/admin_export/fields.html: -------------------------------------------------------------------------------- 1 | {% if table %}{% endif %} 2 | {% if field_name %} 3 | 4 | 5 | 6 | {% endif %} 7 | 8 | 9 | {% for field in fields %} 10 | 11 | 18 | 25 | 26 | {% endfor %} 27 | 28 | {% for field in related_fields %} 29 | 30 | 32 | 45 | 46 | {% endfor %} 47 | {% if table %}
{{ field_name }}
12 | 17 | 19 | {% if field.verbose_name %} 20 | {{ field.verbose_name }} 21 | {% else %} 22 | {{ field }} 23 | {% endif %} 24 |
31 | 33 | 36 | {% if field.verbose_name %} 37 | {{ field.verbose_name }} 38 | [{{ field.get_internal_type }}] 39 | {% else %} 40 | {{ field.get_accessor_name }} 41 | {% endif %} 42 | → 43 | 44 |
{% endif %} 48 | -------------------------------------------------------------------------------- /admin_export/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url, patterns 2 | from django.contrib.admin.views.decorators import staff_member_required 3 | from .views import AdminExport 4 | 5 | view = staff_member_required(AdminExport.as_view()) 6 | urlpatterns = patterns('', 7 | url(r'^export/$', view, name="export"), 8 | (r'^export_to_xls/$', view), # compatibility for users who upgrade without touching URLs 9 | ) 10 | -------------------------------------------------------------------------------- /admin_export/views.py: -------------------------------------------------------------------------------- 1 | from itertools import chain 2 | from django.contrib import admin 3 | from django.contrib.contenttypes.models import ContentType 4 | from django.http.response import HttpResponse 5 | from django.template import loader, Context 6 | from django.views.generic import TemplateView 7 | import csv 8 | from report_utils.mixins import GetFieldsMixin, DataExportMixin 9 | from report_utils.model_introspection import get_relation_fields_from_model 10 | 11 | HTML_TEMPLATE = r""" 12 | 13 | 14 | 15 | 16 | {{ title }} 17 | 18 | 19 |

{{ title }}

20 | 21 | {% if header %}{% for h in header %}{% endfor %}{% endif %} 22 | 23 | {% for datum in data %} 24 | {% for cell in datum %}{% endfor %} 25 | {% endfor %} 26 | 27 |
{{ h }}
{{ cell|linebreaksbr }}
28 | 29 | 30 | """ 31 | 32 | 33 | class ExtDataExportMixin(DataExportMixin): 34 | 35 | def list_to_html_response(self, data, title='', header=None): 36 | html = loader.get_template_from_string(HTML_TEMPLATE).render(Context(locals())) 37 | return HttpResponse(html) 38 | 39 | def list_to_csv_response(self, data, title='', header=None): 40 | resp = HttpResponse(content_type="text/csv; charset=UTF-8") 41 | cw = csv.writer(resp) 42 | for row in chain([header] if header else [], data): 43 | cw.writerow([unicode(s).encode(resp._charset) for s in row]) 44 | return resp 45 | 46 | 47 | class AdminExport(GetFieldsMixin, ExtDataExportMixin, TemplateView): 48 | 49 | """ Get fields from a particular model """ 50 | template_name = 'admin_export/export.html' 51 | 52 | def get_queryset(self, model_class): 53 | if self.request.GET.get("session_key"): 54 | ids = self.request.session[self.request.GET["session_key"]] 55 | else: 56 | ids = self.request.GET['ids'].split(',') 57 | try: 58 | model_admin = admin.site._registry[model_class] 59 | except KeyError: 60 | raise ValueError("Model %r not registered with admin" % model_class) 61 | queryset = model_admin.get_queryset(self.request).filter(pk__in=ids) 62 | return queryset 63 | 64 | def get_model_class(self): 65 | model_class = ContentType.objects.get(id=self.request.GET['ct']).model_class() 66 | return model_class 67 | 68 | def get_context_data(self, **kwargs): 69 | context = super(AdminExport, self).get_context_data(**kwargs) 70 | field_name = self.request.GET.get('field', '') 71 | model_class = self.get_model_class() 72 | queryset = self.get_queryset(model_class) 73 | path = self.request.GET.get('path', '') 74 | path_verbose = self.request.GET.get('path_verbose', '') 75 | context['opts'] = model_class._meta 76 | context['queryset'] = queryset 77 | context['model_ct'] = self.request.GET['ct'] 78 | context['related_fields'] = get_relation_fields_from_model(model_class) 79 | context.update(self.get_fields(model_class, field_name, path, path_verbose)) 80 | return context 81 | 82 | def post(self, request, **kwargs): 83 | context = self.get_context_data(**kwargs) 84 | fields = [] 85 | for field_name, value in request.POST.items(): 86 | if value == "on": 87 | fields.append(field_name) 88 | data_list, message = self.report_to_list( 89 | context['queryset'], 90 | fields, 91 | self.request.user, 92 | ) 93 | format = request.POST.get("__format") 94 | if format == "html": 95 | return self.list_to_html_response(data_list, header=fields) 96 | elif format == "csv": 97 | return self.list_to_csv_response(data_list, header=fields) 98 | else: 99 | return self.list_to_xlsx_response(data_list, header=fields) 100 | 101 | def get(self, request, *args, **kwargs): 102 | if request.REQUEST.get("related"): # Dispatch to the other view 103 | return AdminExportRelated.as_view()(request=self.request) 104 | return super(AdminExport, self).get(request, *args, **kwargs) 105 | 106 | 107 | class AdminExportRelated(GetFieldsMixin, TemplateView): 108 | template_name = 'admin_export/fields.html' 109 | 110 | def get(self, request, **kwargs): 111 | context = self.get_context_data(**kwargs) 112 | model_class = ContentType.objects.get(id=self.request.GET['model_ct']).model_class() 113 | field_name = request.GET['field'] 114 | path = request.GET['path'] 115 | field_data = self.get_fields(model_class, field_name, path, '') 116 | context['related_fields'], model_ct, context['path'] = self.get_related_fields(model_class, field_name, path) 117 | context['model_ct'] = model_ct.id 118 | context['field_name'] = field_name 119 | context['table'] = True 120 | context = dict(context.items() + field_data.items()) 121 | return self.render_to_response(context) 122 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE=tests.settings 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django 2 | django-report-utils>=0.2 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name = "django-admin-export", 5 | version = "2.0", 6 | author = "David Burke", 7 | author_email = "david@burkesoftware.com", 8 | description = ("Generic export action for Django admin interface"), 9 | license = "BSD", 10 | keywords = "django admin", 11 | url = "https://github.com/burke-software/django-admin-export", 12 | packages=find_packages(exclude=("tests",)), 13 | include_package_data=True, 14 | classifiers=[ 15 | "Development Status :: 4 - Beta", 16 | 'Environment :: Web Environment', 17 | 'Framework :: Django', 18 | 'Programming Language :: Python', 19 | 'Intended Audience :: Developers', 20 | 'Intended Audience :: System Administrators', 21 | "License :: OSI Approved :: BSD License", 22 | ], 23 | install_requires=['django-report-utils'], 24 | ) 25 | -------------------------------------------------------------------------------- /test_requirements.txt: -------------------------------------------------------------------------------- 1 | pytest==2.6.4 2 | pytest-django==2.8.0 3 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -- encoding: UTF-8 -- 2 | -------------------------------------------------------------------------------- /tests/models.py: -------------------------------------------------------------------------------- 1 | # -- encoding: UTF-8 -- 2 | from django.db import models 3 | 4 | 5 | class TestModel(models.Model): 6 | value = models.IntegerField(unique=True) 7 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | # -- encoding: UTF-8 -- 2 | 3 | SECRET_KEY = "hi" 4 | 5 | DATABASES = { 6 | "default": dict(ENGINE='django.db.backends.sqlite3', NAME=':memory:') 7 | } 8 | 9 | INSTALLED_APPS = ( 10 | 'django.contrib.admin', 11 | 'django.contrib.auth', 12 | 'django.contrib.contenttypes', 13 | 'django.contrib.sessions', 14 | 'django.contrib.messages', 15 | 'django.contrib.staticfiles', 16 | 'admin_export', 17 | 'tests', 18 | ) 19 | 20 | MIDDLEWARE_CLASSES = ( 21 | 'django.contrib.sessions.middleware.SessionMiddleware', 22 | 'django.middleware.common.CommonMiddleware', 23 | 'django.middleware.csrf.CsrfViewMiddleware', 24 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 25 | 'django.contrib.messages.middleware.MessageMiddleware', 26 | ) 27 | -------------------------------------------------------------------------------- /tests/test_admin_export.py: -------------------------------------------------------------------------------- 1 | # -- encoding: UTF-8 -- 2 | import random 3 | from admin_export.views import AdminExport 4 | from django.contrib import admin 5 | from django.contrib.contenttypes.models import ContentType 6 | import pytest 7 | from tests.models import TestModel 8 | 9 | 10 | class TestModelAdmin(admin.ModelAdmin): 11 | def get_queryset(self, request): 12 | return super(TestModelAdmin, self).get_queryset(request).filter(value__lt=request.magic) 13 | 14 | 15 | def queryset_valid(request, queryset): 16 | return all(x.value < request.magic for x in queryset) 17 | 18 | 19 | @pytest.mark.django_db 20 | def test_queryset_from_admin(rf, admin_user): 21 | for x in range(100): 22 | TestModel.objects.get_or_create(value=x) 23 | assert TestModel.objects.count() >= 100 24 | 25 | request = rf.get("/") 26 | request.user = admin_user 27 | request.magic = random.randint(10, 90) 28 | request.GET = { 29 | "ct": ContentType.objects.get_for_model(TestModel).pk, 30 | "ids": ",".join(str(id) for id in TestModel.objects.all().values_list("pk", flat=True)) 31 | } 32 | 33 | old_registry = admin.site._registry 34 | admin.site._registry = {} 35 | admin.site.register(TestModel, TestModelAdmin) 36 | assert queryset_valid(request, admin.site._registry[TestModel].get_queryset(request)) 37 | assert not queryset_valid(request, TestModel.objects.all()) 38 | 39 | admin_export_view = AdminExport() 40 | admin_export_view.request = request 41 | admin_export_view.args = () 42 | admin_export_view.kwargs = {} 43 | assert admin_export_view.get_model_class() == TestModel 44 | assert queryset_valid(request, admin_export_view.get_queryset(TestModel)) 45 | admin.site._registry = old_registry 46 | --------------------------------------------------------------------------------