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 %}
+
+ {% for fileobject in filelisting %}
+ - {{ fileobject.filename }}
+ {% endfor %}
+
+ {% endif %}
+
+
+ {% 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" %}
+
+
+ {% 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 %}
+
+ {% include "filebrowser/include/breadcrumbs.html" %}
+
+ {% endif %}
+
+
+ {% block object-tools %}
+
+ {% endblock %}
+
+
+
+ {% 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