diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 98c452bd1..d8e2bb2fe 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,30 +8,31 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.8, 3.9, '3.10', '3.11', '3.12'] + python-version: [3.9, '3.10', '3.11', '3.12', '3.13'] requirements-file: [ django-4.2.txt, django-5.0.txt, django-5.1.txt, + django-5.2.txt, django-main.txt, ] custom-image-model: [false, true] os: [ - ubuntu-20.04, + ubuntu-latest, ] exclude: - - requirements-file: django-5.0.txt - python-version: 3.8 - requirements-file: django-5.0.txt python-version: 3.9 - - requirements-file: django-5.1.txt - python-version: 3.8 - requirements-file: django-5.1.txt python-version: 3.9 - - requirements-file: django-main.txt - python-version: 3.8 + - requirements-file: django-5.2.txt + python-version: 3.9 - requirements-file: django-main.txt python-version: 3.9 + - requirements-file: django-main.txt + python-version: 3.10 + - requirements-file: django-main.txt + python-version: 3.11 steps: - uses: actions/checkout@v1 @@ -40,7 +41,7 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: library prerequisites - run: sudo apt-get install python-dev libpq-dev libmagic1 gcc libxml2-dev libxslt1-dev libjpeg62 libopenjp2-7 -y + run: sudo apt-get install python-dev-is-python3 libpq-dev libmagic1 gcc libxml2-dev libxslt1-dev libjpeg62 libopenjp2-7 -y - name: Install extra dependencies run: pip install lxml if: matrix.python-version == '3.10' @@ -53,6 +54,6 @@ jobs: run: echo "CUSTOM_IMAGE=custom_image.Image" >> $GITHUB_ENV if: ${{ matrix.custom-image-model }} - name: Run coverage - run: coverage run ./tests/settings.py + run: coverage run ./tests/settings.py - name: Upload Coverage to Codecov uses: codecov/codecov-action@v1 diff --git a/filer/admin/fileadmin.py b/filer/admin/fileadmin.py index 517f57380..9c38824ee 100644 --- a/filer/admin/fileadmin.py +++ b/filer/admin/fileadmin.py @@ -60,7 +60,7 @@ class FileAdmin(PrimitivePermissionAwareModelAdmin): list_display = ('label',) list_per_page = 10 search_fields = ['name', 'original_filename', 'sha1', 'description'] - autocomplete_fields = ('owner',) + autocomplete_fields = ['owner'] readonly_fields = ('sha1', 'display_canonical') form = FileAdminChangeFrom diff --git a/filer/admin/folderadmin.py b/filer/admin/folderadmin.py index 8b3190f8e..1bc486417 100644 --- a/filer/admin/folderadmin.py +++ b/filer/admin/folderadmin.py @@ -5,6 +5,7 @@ from urllib.parse import quote as urlquote from urllib.parse import unquote as urlunquote +from django import VERSION as DJANGO_VERSION from django import forms from django.conf import settings as django_settings from django.contrib import messages @@ -777,9 +778,15 @@ def delete_files_or_folders(self, request, files_queryset, folders_queryset): n = files_queryset.count() + folders_queryset.count() if n: # delete all explicitly selected files - for f in files_queryset: - self.log_deletion(request, f, force_str(f)) - f.delete() + if DJANGO_VERSION >= (5, 1): + self.log_deletions(request, files_queryset) + # Still need to delete files individually (not only the database entries) + for f in files_queryset: + f.delete() + else: + for f in files_queryset: + self.log_deletion(request, f, force_str(f)) + f.delete() # delete all files in all selected folders and their children # This would happen automatically by ways of the delete # cascade, but then the individual .delete() methods won't be @@ -788,13 +795,24 @@ def delete_files_or_folders(self, request, files_queryset, folders_queryset): for folder in folders_queryset: folder_ids.add(folder.id) folder_ids.update(folder.get_descendants_ids()) - for f in File.objects.filter(folder__in=folder_ids): - self.log_deletion(request, f, force_str(f)) - f.delete() + if DJANGO_VERSION >= (5, 1): + qs = File.objects.filter(folder__in=folder_ids) + self.log_deletions(request, qs) + # Still need to delete files individually (not only the database entries) + for f in qs: + f.delete() + else: + for f in File.objects.filter(folder__in=folder_ids): + self.log_deletion(request, f, force_str(f)) + f.delete() # delete all folders - for f in folders_queryset: - self.log_deletion(request, f, force_str(f)) - f.delete() + if DJANGO_VERSION >= (5, 1): + self.log_deletions(request, files_queryset) + folders_queryset.delete() + else: + for f in folders_queryset: + self.log_deletion(request, f, force_str(f)) + f.delete() self.message_user(request, _("Successfully deleted %(count)d files and/or folders.") % {"count": n, }) # Return None to display the change list page again. return None diff --git a/filer/admin/permissionadmin.py b/filer/admin/permissionadmin.py index 1e8d75d54..7c6df458f 100644 --- a/filer/admin/permissionadmin.py +++ b/filer/admin/permissionadmin.py @@ -1,4 +1,6 @@ +from django import VERSION as django_version from django.contrib import admin +from django.contrib.auth import get_user_model from django.utils.translation import gettext_lazy as _ from .. import settings @@ -19,6 +21,18 @@ class PermissionAdmin(admin.ModelAdmin): class Media: css = {'all': ['filer/css/admin_folderpermissions.css']} + def get_autocomplete_fields(self, request): + """Remove "owner" from autocomplete_fields is User model has no search_fields""" + + autocomplete_fields = super().get_autocomplete_fields(request) + if django_version >= (5, 0): + user_admin = self.admin_site.get_model_admin(get_user_model()) + else: + user_admin = self.admin_site._registry[get_user_model()] + if not user_admin.get_search_fields(request): + autocomplete_fields.remove('user') + return autocomplete_fields + def get_queryset(self, request): qs = super().get_queryset(request) return qs.prefetch_related("group", "folder") diff --git a/filer/admin/permissions.py b/filer/admin/permissions.py index a060ea924..b3e7d00a7 100644 --- a/filer/admin/permissions.py +++ b/filer/admin/permissions.py @@ -1,8 +1,22 @@ +from django import VERSION as django_version from django.contrib import admin +from django.contrib.auth import get_user_model from django.urls import reverse class PrimitivePermissionAwareModelAdmin(admin.ModelAdmin): + def get_autocomplete_fields(self, request): + """Remove "owner" from autocomplete_fields is User model has no search_fields""" + + autocomplete_fields = super().get_autocomplete_fields(request) + if django_version >= (5, 0): + user_admin = self.admin_site.get_model_admin(get_user_model()) + else: + user_admin = self.admin_site._registry[get_user_model()] + if not user_admin.get_search_fields(request) and 'owner' in autocomplete_fields: + autocomplete_fields.remove('owner') + return autocomplete_fields + def has_add_permission(self, request): # we don't have a "add" permission... but all adding is handled # by special methods that go around these permissions anyway diff --git a/tests/requirements/django-5.2.txt b/tests/requirements/django-5.2.txt new file mode 100644 index 000000000..5286d7dc8 --- /dev/null +++ b/tests/requirements/django-5.2.txt @@ -0,0 +1,4 @@ +-r base.txt + +django>=5.2a1,<5.3 +django_polymorphic>=3.1 diff --git a/tox.ini b/tox.ini index d04a03bd2..3fba0702f 100644 --- a/tox.ini +++ b/tox.ini @@ -4,13 +4,15 @@ envlist = isort docs frontend - py{310,311,312}-{dj42,dj50,djmain}-{swap,noswap} + py{310,311,312}-{dj42,dj50,dj51,dj52}-{swap,noswap} + py{312,313,312}-{djmain}-{swap,noswap} [gh-actions] python = 3.10: py310 3.11: py311 3.12: py312 + 3.13: py313 skip_missing_interpreters=True @@ -21,11 +23,13 @@ allowlist_externals = deps = dj42: -r tests/requirements/django-4.2.txt dj50: -r tests/requirements/django-5.0.txt + dj51: -r tests/requirements/django-5.1.txt + dj52: -r tests/requirements/django-5.2.txt djmain: -r tests/requirements/django-main.txt commands = {envpython} --version {env:COMMAND:coverage} erase - {env:COMMAND:coverage} run setup.py test + {env:COMMAND:coverage} run tests/settings.py {env:COMMAND:coverage} report setenv = swap: CUSTOM_IMAGE=custom_image.Image