diff --git a/TEKDB/TEKDB/apps.py b/TEKDB/TEKDB/apps.py index 957301f6..6bf8397d 100644 --- a/TEKDB/TEKDB/apps.py +++ b/TEKDB/TEKDB/apps.py @@ -6,3 +6,24 @@ class TEKDBConfig(AppConfig): name = "TEKDB" verbose_name = "Records" + + def ready(self): + from filebrowser.base import FileObject + from .models import Media + + def get_related_media(self): + """ + Returns QuerySet of Media records that reference this file + The file field stores paths relative to MEDIA_ROOT + """ + # self.path gives the relative path from MEDIA_ROOT + # which should match what's stored in the mediafile field + return Media.objects.filter(mediafile=self.path) + + def has_media_record(self): + """Check if this file has any related Media records""" + return self.get_related_media().exists() + + # Add methods to FileObject + FileObject.get_related_media = get_related_media + FileObject.has_media_record = has_media_record diff --git a/TEKDB/TEKDB/settings.py b/TEKDB/TEKDB/settings.py index 588a1fb9..044f08da 100644 --- a/TEKDB/TEKDB/settings.py +++ b/TEKDB/TEKDB/settings.py @@ -47,6 +47,7 @@ INSTALLED_APPS = [ "dal", "dal_select2", + "filebrowser", "django.contrib.contenttypes", "django.contrib.admin", "django.contrib.auth", @@ -182,6 +183,28 @@ STATIC_ROOT = os.path.join(BASE_DIR, "static") +########################################### +## FILEBROWSER ### +########################################### +DIRECTORY = MEDIA_URL + +FILEBROWSER_LIST_PER_PAGE = 20 + +FILEBROWSER_EXCLUDE = [] # Add extensions to exclude from showing in filebrowser. E.g. ['.py',] + +FILEBROWSER_VERSIONS = {} # Empty dict because we are not using versions + +# Apps to exclude from the export `dumpdata` command. This prevents +# dumping third-party apps (like `filebrowser`) that may not have tables +# in the test DB or that you don't want included in project backups. +EXPORT_DUMP_EXCLUDE = [ + "filebrowser", +] + +########################################### +### END FILEBROWSER ### +########################################### + # STATICFILES_DIRS = [ # os.path.join(BASE_DIR, "explore", "static"), # os.path.join(BASE_DIR, "TEKDB", "static"), diff --git a/TEKDB/TEKDB/tekdb_filebrowser.py b/TEKDB/TEKDB/tekdb_filebrowser.py new file mode 100644 index 00000000..d166e6d2 --- /dev/null +++ b/TEKDB/TEKDB/tekdb_filebrowser.py @@ -0,0 +1,269 @@ +import os +from django.core.files.storage import DefaultStorage +from django.core.paginator import Paginator, EmptyPage +from django.template.response import TemplateResponse +from django.contrib import messages +from django.urls import reverse +from django.http import HttpResponseRedirect +from django.conf import settings +from filebrowser import signals +from filebrowser.sites import ( + FileBrowserSite, + filebrowser_view, + get_settings_var, + get_breadcrumbs, + admin_site, +) +from filebrowser.decorators import path_exists +from filebrowser.settings import ( + DEFAULT_SORTING_BY, + DEFAULT_SORTING_ORDER, + VERSIONS_BASEDIR, +) +from filebrowser.templatetags.fb_tags import query_helper + + +def media_matches(media_filter, obj): + """Return True if `obj` satisfies the media-record filter. + + Supported values for media_filter: + - 'has_record' -> keep objects where obj.has_media_record() is True + - 'no_record' -> keep objects where obj.has_media_record() is False + If no media_filter is set, everything matches. + """ + if not media_filter: + return True + try: + has_attr = getattr(obj, "has_media_record", None) + if callable(has_attr): + has = bool(has_attr()) + else: + has = bool(has_attr) + except AttributeError: + has = False + if media_filter == "has_record": + return has + if media_filter == "no_record": + return not has + return True + + +class TekdbFileBrowserSite(FileBrowserSite): + def files_folders_to_ignore(self): + """Return list of filenames/folders to ignore in deleting media without record.""" + return ["__pycache__", "__init__.py", VERSIONS_BASEDIR] + + def get_urls(self): + from django.urls import re_path + + urls = super().get_urls() + + urls += [ + re_path( + r"^delete-media-without-record-confirm/", + path_exists( + self, filebrowser_view(self.delete_media_without_record_confirm) + ), + name="fb_delete_all_media_without_record_confirm", + ), + re_path( + r"^delete-media-without-record/", + path_exists(self, filebrowser_view(self.delete_media_without_record)), + name="fb_delete_all_media_without_record", + ), + ] + return urls + + def browse(self, request): + """Call upstream browse(), then apply server-side filtering for the + `filter_media_record` query parameter. This keeps the UI filter backed + by server logic without changing upstream code. + """ + response = super().browse(request) + + # Only proceed if the TemplateResponse has context data we can + # mutate. + if not hasattr(response, "context_data"): + return response + + media_filter = None + try: + media_filter = request.GET.get("filter_media_record") + except AttributeError: + media_filter = None + + # dict of original paginator -> new paginator + paginator_replacements = {} + + # loop does the following: + # 1. filter data if media_filter is set + # 2. set response.context_data["page"] to filtered page + # 3. build new paginators dict + for key, val in list(response.context_data.items()): + try: + if ( + hasattr(val, "paginator") + and hasattr(val, "number") + and key == "page" + ): + orig_paginator = val.paginator + try: + full_list = list(orig_paginator.object_list) + except (AttributeError, TypeError): + full_list = list(getattr(val, "object_list", [])) + + filtered_full = [ + obj for obj in full_list if media_matches(media_filter, obj) + ] + + max_per_page = getattr(settings, "FILEBROWSER_LIST_PER_PAGE", None) + if not max_per_page: + max_per_page = getattr(orig_paginator, "per_page", 20) + + new_paginator = Paginator(filtered_full, max_per_page) + page_number = getattr(val, "number", 1) + try: + new_page = new_paginator.page(page_number) + except EmptyPage: + new_page = new_paginator.page(new_paginator.num_pages) + response.context_data[key] = new_page + paginator_replacements[orig_paginator] = new_paginator + + except AttributeError: + # Non-fatal: skip entries we can't process + pass + + # Replace response.context_data["p"] with new paginator + if paginator_replacements: + for key, orig_paginator in list(response.context_data.items()): + for orig, new_paginator in list(paginator_replacements.items()): + if orig_paginator is orig: + response.context_data[key] = new_paginator + + # Update results_current and results_total + try: + filelisting = response.context_data.get("filelisting") + if filelisting is not None: + # If filelisting exposes a list of current files, try to + # update results_current/total conservatively. + try: + if hasattr(filelisting, "results_current"): + # get new page set on line 129 + page = response.context_data.get("page") + + if page is not None and hasattr(page, "object_list"): + # update results_current from filtered page + filelisting.results_current = len(list(page.object_list)) + + if hasattr(filelisting, "results_total"): + # derive results_total from a paginator replacement + paginator_new = None + for new_page in paginator_replacements.values(): + paginator_new = new_page + break + + if paginator_new is not None: + try: + total = len(getattr(paginator_new, "object_list", [])) + except AttributeError: + total = 0 + + filelisting.results_total = total + except AttributeError: + pass + except (AttributeError, TypeError): + pass + + return response + + def delete_media_without_record_confirm(self, request): + """Confirm page to delete selected files that do not have associated media records.""" + + query = request.GET + path = "%s" % os.path.join(self.directory, query.get("dir", "")) + + filelisting = self.filelisting_class( + path, + filter_func=lambda fo: not fo.has_media_record() + and fo.filename not in self.files_folders_to_ignore(), + sorting_by=query.get("o", DEFAULT_SORTING_BY), + sorting_order=query.get("ot", DEFAULT_SORTING_ORDER), + site=self, + ) + listings = filelisting.files_listing_filtered() + + return TemplateResponse( + request, + "filebrowser/delete_no_media_record_confirm.html", + dict( + admin_site.each_context(request), + **{ + "filelisting": listings, + "query": query, + "title": "Confirm Deletion of Unused Media Files", + "settings_var": get_settings_var(directory=self.directory), + "breadcrumbs": get_breadcrumbs(query, query.get("dir", "")), + "breadcrumbs_title": "Delete Unused Media Files", + "filebrowser_site": self, + }, + ), + ) + + def delete_media_without_record(self, request): + """Delete selected files that do not have associated media records.""" + query = request.GET + path = "%s" % os.path.join(self.directory, query.get("dir", "")) + + filelisting = self.filelisting_class( + path, + filter_func=lambda fo: not fo.has_media_record() + and fo.filename not in self.files_folders_to_ignore(), + sorting_by=query.get("o", DEFAULT_SORTING_BY), + sorting_order=query.get("ot", DEFAULT_SORTING_ORDER), + site=self, + ) + listing = filelisting.files_listing_filtered() + + deleted_files = [] + for fileobject in listing: + try: + signals.filebrowser_pre_delete.send( + sender=request, + path=fileobject.path, + name=fileobject.filename, + site=self, + ) + # we have disabled versions for TEKDB, + # but keep the call here in case that changes in future + # or in the off chance versions exist + fileobject.delete_versions() + fileobject.delete() + deleted_files.append(fileobject.filename) + signals.filebrowser_post_delete.send( + sender=request, + path=fileobject.path, + name=fileobject.filename, + site=self, + ) + except OSError: + messages.add_message( + request, + messages.ERROR, + f"Error deleting file: {fileobject.filename}", + ) + break + + messages.add_message( + request, messages.SUCCESS, f"Deleted files: {', '.join(deleted_files)}" + ) + + # Redirect back to browse after deletion + redirect_url = reverse( + "filebrowser:fb_browse", current_app=self.name + ) + query_helper(query, "", "filename,filetype") + return HttpResponseRedirect(redirect_url) + + +storage = DefaultStorage() +site = TekdbFileBrowserSite(name="filebrowser", storage=storage) +site.directory = "" diff --git a/TEKDB/TEKDB/templates/filebrowser/delete_no_media_record_confirm.html b/TEKDB/TEKDB/templates/filebrowser/delete_no_media_record_confirm.html new file mode 100644 index 00000000..50e3f70e --- /dev/null +++ b/TEKDB/TEKDB/templates/filebrowser/delete_no_media_record_confirm.html @@ -0,0 +1,35 @@ +{% extends "admin/base_site.html" %} + + +{% load static i18n fb_tags fb_compat %} + + +{% block breadcrumbs %}{% include "filebrowser/include/breadcrumbs.html" %}{% endblock %} + + +{% block content %} + + {% if query.pop %} + {% include "filebrowser/include/breadcrumbs.html" %} + {% endif %} + {% if filelisting|length > 0 %} +

Are you sure you want to delete all media files without media records? All of the following related items will be deleted:

+ {% if filelisting %} + + {% endif %} + +
+ {% csrf_token %} +
+ +
+
+ {% endif %} + {% if filelisting|length == 0 %} +

There are no media files without media records to delete.

+ {% endif %} +{% endblock %} \ No newline at end of file diff --git a/TEKDB/TEKDB/templates/filebrowser/detail.html b/TEKDB/TEKDB/templates/filebrowser/detail.html new file mode 100644 index 00000000..d5f54292 --- /dev/null +++ b/TEKDB/TEKDB/templates/filebrowser/detail.html @@ -0,0 +1,108 @@ +{% extends "admin/base_site.html" %} +{% comment %} +Copied from https://github.com/smacker/django-filebrowser-no-grappelli/blob/master/filebrowser/templates/filebrowser/detail.html +{% endcomment %} + + +{% load static i18n fb_tags fb_compat %} + + +{% block extrastyle %} + {{ block.super }} + +{% endblock %} + + +{% block extrahead %} + {{ block.super }} + + +{% endblock %} + + + +{% block breadcrumbs %}{% include "filebrowser/include/breadcrumbs.html" %}{% endblock %} + + +{% block content %} +
+ + {% if query.pop %} + {% include "filebrowser/include/breadcrumbs.html" %} + {% endif %} +
+ {% if fileobject.filetype != "Folder" %} +
+

{% trans "File Information" %}

+ {% if fileobject.filetype == "Image" %} +
+
+ +

+ +

+
+
+ {% endif %} +
+
+ +

+ {{ fileobject.url }} +

+
+
+
+
+ +

+ {{ fileobject.filesize|filesizeformat }} +

+
+
+
+
+ +

+ {{ fileobject.datetime|date:"N j, Y" }} +

+
+
+ {% if fileobject.filetype == "Image" %} +
+
+ +

+ {{ fileobject.width }} x {{ fileobject.height }} px +

+
+
+ {% endif %} +
+
+

{% trans "Related Media Records" %}

+ {% if fileobject.has_media_record %} + {% for media in fileobject.get_related_media %} +
+
+

{{ media }}

+
+
+ {% endfor %} + {% else %} +
+

{% trans "No related media records." %}

+
+ {% endif %} +
+ {% endif %} + {% if not fileobject.has_media_record %} + + {% endif %} +
+
+{% endblock %} \ No newline at end of file diff --git a/TEKDB/TEKDB/templates/filebrowser/filelisting.html b/TEKDB/TEKDB/templates/filebrowser/filelisting.html new file mode 100644 index 00000000..cc1d3a00 --- /dev/null +++ b/TEKDB/TEKDB/templates/filebrowser/filelisting.html @@ -0,0 +1,56 @@ +{% load i18n fb_tags fb_compat %} +{% comment %} +Copied from https://github.com/smacker/django-filebrowser-no-grappelli/blob/master/filebrowser/templates/filebrowser/include/filelisting.html +{% endcomment %} + +{% for fileobject in page.object_list %} + + + + + + + {% if fileobject.filetype %} + {% trans fileobject.filetype %} + {% else %} + + {% endif %} + + + + + {% if fileobject.filetype == "Image" %} + + {% endif %} + + + + {% if fileobject.is_folder %} + {{ fileobject.filename }} + {% else %} + + {{ fileobject.filename }} + {% if fileobject.dimensions %} +
{{ fileobject.dimensions.0 }} x {{ fileobject.dimensions.1 }} px + {% endif %} + + {% endif %} + + + {% if query.q and settings_var.SEARCH_TRAVERSE %} + {{ fileobject.folder }} + {% endif %} + + + {% if fileobject.filesize %}{{ fileobject.filesize|filesizeformat }}{% else %}—{% endif %} + + + {{ fileobject.datetime|date:"N j, Y" }} + + + + {% trans "Change" %} + + + +{% endfor %} \ No newline at end of file diff --git a/TEKDB/TEKDB/templates/filebrowser/filter.html b/TEKDB/TEKDB/templates/filebrowser/filter.html new file mode 100644 index 00000000..e22bd087 --- /dev/null +++ b/TEKDB/TEKDB/templates/filebrowser/filter.html @@ -0,0 +1,44 @@ +{% load i18n fb_tags %} +{% comment %} +Copied from https://github.com/smacker/django-filebrowser-no-grappelli/blob/master/filebrowser/templates/filebrowser/include/filter.html +{% endcomment %} + +
+

{% trans 'Filter' %}

+ +

{% trans "By Date" %}

+ + +

{% trans "By Type" %}

+ + +

{% trans "By Media Record" %}

+ +
\ No newline at end of file diff --git a/TEKDB/TEKDB/templates/filebrowser/index.html b/TEKDB/TEKDB/templates/filebrowser/index.html new file mode 100644 index 00000000..3603052a --- /dev/null +++ b/TEKDB/TEKDB/templates/filebrowser/index.html @@ -0,0 +1,88 @@ +{% extends "admin/base_site.html" %} +{% comment %} +Copied from https://github.com/smacker/django-filebrowser-no-grappelli/blob/master/filebrowser/templates/filebrowser/index.html +{% endcomment %} + + +{% load static i18n fb_tags fb_pagination fb_compat %} + + +{% block extrastyle %} + {{ block.super }} + + +{% endblock %} + + +{% block bodyclass %}change-list filebrowser {% if query.pop %} popup{% endif %}{% endblock %} +{% block coltype %}flex{% endblock %} + + +{% block breadcrumbs %}{% include "filebrowser/include/breadcrumbs.html" %}{% endblock %} + + +{% block content %} +
+ + {% if query.pop %} + + {% endif %} + + + {% block object-tools %} + + {% endblock %} + +
+
+ + {% block search %} +
+ + {% endblock %} + +
{% csrf_token %} + + {% if filelisting.results_current %} +
+ + {% include "filebrowser/include/tableheader.html" %} + + {% include "filebrowser/filelisting.html" %} + +
+
+ {% endif %} + + {% pagination %} +
+
+ {% block filters %} + + {% include "filebrowser/filter.html" %} + {% endblock %} +
+
+{% endblock %} \ No newline at end of file diff --git a/TEKDB/TEKDB/tests/test_custom_filebrowser.py b/TEKDB/TEKDB/tests/test_custom_filebrowser.py new file mode 100644 index 00000000..fce88b64 --- /dev/null +++ b/TEKDB/TEKDB/tests/test_custom_filebrowser.py @@ -0,0 +1,210 @@ +from base64 import b64encode +from os.path import join +from django.conf import settings +from django.test import TestCase, RequestFactory +from django.urls import reverse +from unittest.mock import patch, Mock + +from TEKDB import tekdb_filebrowser +from TEKDB.tests.test_views import import_fixture_file + + +class MockFileObject: + def __init__(self, val): + # val can be bool or callable returning bool + self._val = val + + def has_media_record(self): + if callable(self._val): + return self._val() + return self._val + + +class CustomFileBrowserTests(TestCase): + def setUp(self): + import_fixture_file( + join(settings.BASE_DIR, "TEKDB", "fixtures", "all_dummy_data.json") + ) + self.factory = RequestFactory() + self.credentials = b64encode(b"admin:admin").decode("ascii") + + def test_media_matches_no_filter(self): + obj = MockFileObject(True) + + self.assertTrue(tekdb_filebrowser.media_matches(None, obj)) + + def test_media_matches_has_record(self): + obj_true = MockFileObject(True) + obj_false = MockFileObject(False) + self.assertTrue(tekdb_filebrowser.media_matches("has_record", obj_true)) + self.assertFalse(tekdb_filebrowser.media_matches("has_record", obj_false)) + + def test_media_matches_no_record(self): + obj_true = MockFileObject(True) + obj_false = MockFileObject(False) + self.assertFalse(tekdb_filebrowser.media_matches("no_record", obj_true)) + self.assertTrue(tekdb_filebrowser.media_matches("no_record", obj_false)) + + def test_media_matches_callable_attribute(self): + obj = MockFileObject(lambda: True) + self.assertTrue(tekdb_filebrowser.media_matches("has_record", obj)) + + def test_get_urls(self): + confirm_url = reverse("filebrowser:fb_delete_all_media_without_record_confirm") + delete_url = reverse("filebrowser:fb_delete_all_media_without_record") + + self.assertIn("delete-media-without-record-confirm", confirm_url) + self.assertIn("delete-media-without-record", delete_url) + + def test_browse_media_filter_has_record(self): + from TEKDB.models import Users + + user = Users.objects.get(username="admin") + self.client.force_login(user) + + url = reverse("filebrowser:fb_browse") + unfiltered_response = self.client.get( + url, headers={"Authorization": f"Basic {self.credentials}"} + ) + self.assertEqual(unfiltered_response.status_code, 200) + + filter_response = self.client.get( + url, + headers={"Authorization": f"Basic {self.credentials}"}, + QUERY_STRING="&filter_media_record=has_record", + ) + self.assertEqual(filter_response.status_code, 200) + self.assertEqual( + filter_response.context_data["query"]["filter_media_record"], "has_record" + ) + + for item in filter_response.context_data["page"]: + self.assertTrue(item.has_media_record()) + filtered_filelisting = filter_response.context_data.get("filelisting") + unfiltered_filelisting = unfiltered_response.context_data.get("filelisting") + + filtered_results_current = filtered_filelisting.results_current + unfiltered_results_current = unfiltered_filelisting.results_current + + filtered_results_total = filtered_filelisting.results_total + unfiltered_results_total = unfiltered_filelisting.results_total + + self.assertLess(filtered_results_current, unfiltered_results_current) + self.assertLess(filtered_results_total, unfiltered_results_total) + + def test_browse_media_filter_no_record(self): + from TEKDB.models import Users + + user = Users.objects.get(username="admin") + self.client.force_login(user) + + url = reverse("filebrowser:fb_browse") + response = self.client.get( + url, + headers={"Authorization": f"Basic {self.credentials}"}, + QUERY_STRING="&filter_media_record=no_record", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.context_data["query"]["filter_media_record"], "no_record" + ) + + for item in response.context_data["page"]: + self.assertFalse(item.has_media_record()) + + def test_browse_media_filter_multiple_filters(self): + from TEKDB.models import Users + + user = Users.objects.get(username="admin") + self.client.force_login(user) + + url = reverse("filebrowser:fb_browse") + response = self.client.get( + url, + headers={"Authorization": f"Basic {self.credentials}"}, + QUERY_STRING="&filter_media_record=has_record&filter_type=Image", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.context_data["query"]["filter_media_record"], "has_record" + ) + self.assertEqual(response.context_data["query"]["filter_type"], "Image") + + for item in response.context_data["page"]: + self.assertEqual(item.filetype, "Image") + self.assertTrue(item.has_media_record()) + + def test_browse_media_filter_invalid(self): + from TEKDB.models import Users + + user = Users.objects.get(username="admin") + self.client.force_login(user) + + url = reverse("filebrowser:fb_browse") + response = self.client.get( + url, + headers={"Authorization": f"Basic {self.credentials}"}, + QUERY_STRING="&filter_media_record=invalid_value", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.context_data["query"]["filter_media_record"], "invalid_value" + ) + + # With an invalid filter, all items should be returned + all_items = list(response.context_data["page"]) + self.assertGreater(len(all_items), 0) + + def test_delete_media_without_record_confirm(self): + from TEKDB.models import Users + + user = Users.objects.get(username="admin") + self.client.force_login(user) + + # media file that has a related Media record + media_file_name = "gumboot_chiton_fT5Kxtm.jpg" + + url = reverse("filebrowser:fb_delete_all_media_without_record_confirm") + response = self.client.post( + url, + headers={"Authorization": f"Basic {self.credentials}"}, + ) + self.assertEqual(response.status_code, 200) + + filelisting = response.context_data["filelisting"] + self.assertNotIn(media_file_name, [f.filename for f in filelisting]) + + def test_delete_media_without_record(self): + from TEKDB.models import Users + + user = Users.objects.get(username="admin") + self.client.force_login(user) + + delete_url = reverse("filebrowser:fb_delete_all_media_without_record") + + confirm_url = reverse("filebrowser:fb_delete_all_media_without_record_confirm") + confirm_resp = self.client.post( + confirm_url, + headers={"Authorization": f"Basic {self.credentials}"}, + ) + self.assertEqual(confirm_resp.status_code, 200) + + filelisting = confirm_resp.context_data.get("filelisting", []) + if filelisting: + file_obj_class = filelisting[0].__class__ + else: + file_obj_class = None + + if file_obj_class is not None: + with ( + patch.object( + file_obj_class, "delete_versions", new=Mock(return_value=None) + ), + patch.object(file_obj_class, "delete", new=Mock(return_value=None)), + ): + response = self.client.post( + delete_url, + headers={"Authorization": f"Basic {self.credentials}"}, + ) + + self.assertEqual(response.status_code, 302) diff --git a/TEKDB/TEKDB/urls.py b/TEKDB/TEKDB/urls.py index d28612d1..ef2e60f3 100644 --- a/TEKDB/TEKDB/urls.py +++ b/TEKDB/TEKDB/urls.py @@ -27,12 +27,16 @@ from django.urls import include, path, re_path from django.contrib import admin from django.contrib.auth import views as auth_views + from login import views as login_views from . import views +from .tekdb_filebrowser import site as tekdb_filebrowser + urlpatterns = [ # url(r'^login/', include('login.urls')), + path("admin/filebrowser/", tekdb_filebrowser.urls), path("login/", login_views.login, name="login"), path("login_async/", login_views.login_async, name="login_async"), path("logout/", auth_views.LogoutView.as_view(next_page="/"), name="logout"), diff --git a/TEKDB/TEKDB/views.py b/TEKDB/TEKDB/views.py index 915b48cd..60329273 100644 --- a/TEKDB/TEKDB/views.py +++ b/TEKDB/TEKDB/views.py @@ -78,7 +78,13 @@ def ExportDatabase(request, test=False): dumpfile = "{}_backup.json".format(datestamp) dumpfile_location = os.path.join(tmp_dir, dumpfile) with open(dumpfile_location, "w") as of: - management.call_command("dumpdata", "--indent=2", stdout=of) + excludes = getattr(settings, "EXPORT_DUMP_EXCLUDE", []) + management.call_command( + "dumpdata", + exclude=excludes, + indent=2, + stdout=of, + ) # zip up: # * Data Dump file # * Media files @@ -88,7 +94,7 @@ def ExportDatabase(request, test=False): zip.write(media_file) response = FileResponse(open(tmp_zip.name, "rb")) - + finally: try: if not test: @@ -97,7 +103,7 @@ def ExportDatabase(request, test=False): except (PermissionError, NotADirectoryError): response.set_cookie("export_status", "error") pass - return response + return response def getDBTruncateCommand(): diff --git a/TEKDB/requirements.txt b/TEKDB/requirements.txt index f3768a1a..cdf6722b 100644 --- a/TEKDB/requirements.txt +++ b/TEKDB/requirements.txt @@ -12,6 +12,7 @@ django-reversion django-tinymce pillow psycopg2-binary +django-filebrowser-no-grappelli>=4.0.0,<5.0.0 XlsxWriter #-e git+https://github.com/dominno/django-moderation.git@master#egg=moderation