Skip to content

Commit 2e74152

Browse files
authored
Merge branch 'master' into test/django51
2 parents cfe975f + b38ef8f commit 2e74152

26 files changed

+487
-65
lines changed

.github/workflows/test.yml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
strategy:
99
fail-fast: false
1010
matrix:
11-
python-version: ['3.10', '3.11', '3.12']
11+
python-version: [3.8, 3.9, '3.10', '3.11', '3.12']
1212
requirements-file: [
1313
django-4.2.txt,
1414
django-5.0.txt,
@@ -19,6 +19,15 @@ jobs:
1919
os: [
2020
ubuntu-20.04,
2121
]
22+
exclude:
23+
- requirements-file: django-5.0.txt
24+
python-version: 3.8
25+
- requirements-file: django-5.0.txt
26+
python-version: 3.9
27+
- requirements-file: django-main.txt
28+
python-version: 3.8
29+
- requirements-file: django-main.txt
30+
python-version: 3.9
2231

2332
steps:
2433
- uses: actions/checkout@v1
@@ -35,7 +44,7 @@ jobs:
3544
run: |
3645
python -m pip install --upgrade pip
3746
pip install -r tests/requirements/${{ matrix.requirements-file }}
38-
python setup.py install
47+
pip install -e .
3948
- name: Enable the custom image model
4049
run: echo "CUSTOM_IMAGE=custom_image.Image" >> $GITHUB_ENV
4150
if: ${{ matrix.custom-image-model }}

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ repos:
2121
# args: [--target-version, "2.2"]
2222

2323
- repo: https://github.com/PyCQA/flake8
24-
rev: 7.0.0
24+
rev: 7.1.0
2525
hooks:
2626
- id: flake8
2727

CHANGELOG.rst

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,39 @@
22
CHANGELOG
33
=========
44

5-
3.1.3 (2025-05-17)
5+
3.2.1 (2024-09-05)
6+
==================
7+
8+
* fix: Restore python 3.8 and python 3.9 compatibility
9+
10+
3.2.0 (2024-08-23)
11+
==================
12+
13+
* feat: Add cache for permission checks by @fsbraun in https://github.com/django-cms/django-filer/pull/1486
14+
* fix: Reduce number of thumbnails created for admin, avoid admin thumbnails for svg files by @fsbraun in https://github.com/django-cms/django-filer/pull/1490
15+
* fix: Allow ``Image.MAX_IMAGE_PIXELS`` to be ``None`` by @fsbraun in https://github.com/django-cms/django-filer/pull/1475
16+
* docs: Update extending_filer.rst by @DmytroLitvinov in https://github.com/django-cms/django-filer/pull/1488
17+
18+
**New contributor:**
19+
20+
* @DmytroLitvinov made their first contribution in https://github.com/django-cms/django-filer/pull/1488
21+
22+
3.1.4 (2024-07-15)
23+
==================
24+
25+
* feat: Accept new `STORAGES` setting, introduced in Django 4.2 by @fsbraun in https://github.com/django-cms/django-filer/pull/1472
26+
* feat: Replace `render` with `TemplateResponse` in admin views by @fsbraun in https://github.com/django-cms/django-filer/pull/1473
27+
* fix: File expand url incorrect and worked not with custom image models by @fsbraun in https://github.com/django-cms/django-filer/pull/1471
28+
* fix: Crash when moving files from a filtered directory listing by @W1ldPo1nter in https://github.com/django-cms/django-filer/pull/1482
29+
* ci: pre-commit autoupdate by @pre-commit-ci in https://github.com/django-cms/django-filer/pull/1477
30+
31+
32+
3.1.3 (2024-05-17)
633
==================
734
* Fix: Folder select widget did not render correctly with standard Django admin
835
styles.
936

10-
3.1.2 (2025-05-17)
37+
3.1.2 (2024-05-17)
1138
==================
1239

1340
* Made the filer check command compatible with custom image models.

docs/extending_filer.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ field. You can override this behavior by subclassing the
214214
You can also override the search behavior for :py:class:`Folders<filer.models.foldermodels.Folder>`.
215215
Just override :py:attr:`~filer.admin.folderadmin.FolderAdmin.search_fields` by subclassing
216216
the :py:class:`filer.admin.folderadmin.FolderAdmin`. It works as described in
217-
`Django's docs <https://docs.djangoproject.com/en/1.8/ref/contrib/admin/#django.contrib.admin.ModelAdmin.search_fields>`_. E.g.:
217+
`Django's docs <https://docs.djangoproject.com/en/4.2/ref/contrib/admin/#django.contrib.admin.ModelAdmin.search_fields>`_. E.g.:
218218

219219

220220
.. code-block:: python
@@ -230,7 +230,7 @@ Providing custom Image model
230230
----------------------------
231231

232232
As the ``Image`` model is special, a different way to implement custom Image model is required, which uses the Django
233-
`swappable models <https://docs.djangoproject.com/en/2.0/topics/auth/customizing/#substituting-a-custom-user-model>`_ interface.
233+
`swappable models <https://docs.djangoproject.com/en/4.2/topics/auth/customizing/#substituting-a-custom-user-model>`_ interface.
234234

235235
Defining the model
236236
..................

filer/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@
1313
8. Publish the release and it will automatically release to pypi
1414
"""
1515

16-
__version__ = '3.1.3'
16+
__version__ = '3.2.1'

filer/admin/clipboardadmin.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
from django.core.exceptions import ValidationError
33
from django.forms.models import modelform_factory
44
from django.http import JsonResponse
5-
from django.urls import path
5+
from django.urls import path, reverse
66
from django.utils.translation import gettext_lazy as _
77
from django.views.decorators.csrf import csrf_exempt
88

99
from .. import settings as filer_settings
1010
from ..models import Clipboard, ClipboardItem, Folder
11+
from ..settings import FILER_THUMBNAIL_ICON_SIZE
1112
from ..utils.files import handle_request_files_upload, handle_upload
1213
from ..utils.loader import load_model
1314
from ..validation import validate_upload
@@ -141,14 +142,10 @@ def ajax_upload(request, folder_id=None):
141142
}
142143
# prepare preview thumbnail
143144
if isinstance(file_obj, Image):
144-
thumbnail_180_options = {
145-
'size': (180, 180),
146-
'crop': True,
147-
'upscale': True,
148-
}
149-
thumbnail_180 = file_obj.file.get_thumbnail(
150-
thumbnail_180_options)
151-
data['thumbnail_180'] = thumbnail_180.url
145+
data['thumbnail_180'] = reverse(
146+
f"admin:filer_{file_obj._meta.model_name}_fileicon",
147+
args=(file_obj.pk, FILER_THUMBNAIL_ICON_SIZE),
148+
)
152149
data['original_image'] = file_obj.url
153150
return JsonResponse(data)
154151
except Exception as error:

filer/admin/folderadmin.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@
2828
from easy_thumbnails.models import Thumbnail
2929

3030
from .. import settings
31+
from ..cache import clear_folder_permission_cache
3132
from ..models import File, Folder, FolderPermission, FolderRoot, ImagesWithMissingData, UnsortedImages, tools
32-
from ..settings import FILER_IMAGE_MODEL, FILER_PAGINATE_BY, TABLE_LIST_TYPE
33+
from ..settings import (
34+
FILER_IMAGE_MODEL, FILER_PAGINATE_BY, FILER_TABLE_ICON_SIZE, FILER_THUMBNAIL_ICON_SIZE, TABLE_LIST_TYPE,
35+
)
3336
from ..thumbnail_processors import normalize_subject_location
3437
from ..utils.compatibility import get_delete_permission
3538
from ..utils.filer_easy_thumbnails import FilerActionThumbnailer
@@ -107,6 +110,9 @@ def save_form(self, request, form, change):
107110
Given a ModelForm return an unsaved instance. ``change`` is True if
108111
the object is being changed, and False if it's being added.
109112
"""
113+
if not change:
114+
# New folder invalidates the folder permission cache (or it will not be visible)
115+
clear_folder_permission_cache(request.user)
110116
r = form.save(commit=False)
111117
parent_id = request.GET.get('parent_id', None)
112118
if not parent_id:
@@ -267,11 +273,13 @@ def directory_listing(self, request, folder_id=None, viewtype=None):
267273

268274
list_type = get_directory_listing_type(request) or settings.FILER_FOLDER_ADMIN_DEFAULT_LIST_TYPE
269275
if list_type == TABLE_LIST_TYPE:
270-
size = "40x40" # Prefetch thumbnails for listing
271-
size_x2 = "80x80"
276+
# Prefetch thumbnails for table view
277+
size = f"{FILER_TABLE_ICON_SIZE}x{FILER_TABLE_ICON_SIZE}"
278+
size_x2 = f"{2 * FILER_TABLE_ICON_SIZE}x{2 * FILER_TABLE_ICON_SIZE}"
272279
else:
273-
size = "160x160" # Prefetch thumbnails for thumbnail view
274-
size_x2 = "320x320"
280+
# Prefetch thumbnails for thumbnail view
281+
size = f"{FILER_THUMBNAIL_ICON_SIZE}x{FILER_THUMBNAIL_ICON_SIZE}"
282+
size_x2 = f"{2 * FILER_THUMBNAIL_ICON_SIZE}x{2 * FILER_THUMBNAIL_ICON_SIZE}"
275283

276284
# Check actions to see if any are available on this changelist
277285
actions = self.get_actions(request)
@@ -321,14 +329,13 @@ def directory_listing(self, request, folder_id=None, viewtype=None):
321329
order_by = request.GET.get('order_by', None)
322330
order_by_annotation = None
323331
if order_by is None:
324-
file_qs = file_qs.annotate(coalesce_sort_field=Coalesce(
332+
order_by_annotation = Lower(Coalesce(
325333
Case(
326334
When(name__exact='', then=None),
327335
When(name__isnull=False, then='name')
328336
),
329337
'original_filename'
330338
))
331-
order_by_annotation = Lower('coalesce_sort_field')
332339

333340
order_by = order_by.split(',') if order_by else []
334341
order_by = [field for field in order_by
@@ -464,6 +471,7 @@ def directory_listing(self, request, folder_id=None, viewtype=None):
464471
'show_result_count': show_result_count,
465472
'folder_children': folder_qs,
466473
'folder_files': file_qs,
474+
'thumbnail_size': FILER_TABLE_ICON_SIZE if list_type == TABLE_LIST_TYPE else FILER_THUMBNAIL_ICON_SIZE,
467475
'limit_search_to_folder': limit_search_to_folder,
468476
'is_popup': popup_status(request),
469477
'filer_admin_context': AdminContext(request),

filer/admin/permissionadmin.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from django.utils.translation import gettext_lazy as _
33

44
from .. import settings
5+
from ..cache import clear_folder_permission_cache
56

67

78
class PermissionAdmin(admin.ModelAdmin):
@@ -31,3 +32,11 @@ def get_model_perms(self, request):
3132
'change': enable_permissions,
3233
'delete': enable_permissions,
3334
}
35+
36+
def save_model(self, request, obj, form, change):
37+
clear_folder_permission_cache(request.user)
38+
super().save_model(request, obj, form, change)
39+
40+
def delete_model(self, request, obj):
41+
clear_folder_permission_cache(request.user)
42+
super().delete_model(request, obj)

filer/cache.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import typing
2+
3+
from django.contrib.auth import get_user_model
4+
from django.core.cache import cache
5+
6+
7+
User = get_user_model()
8+
9+
10+
def get_folder_perm_cache_key(user: User, permission: str) -> str:
11+
"""
12+
Generates a unique cache key for a given user and permission.
13+
14+
The key is a string in the format "filer:perm:<permission>", i.e. it does not
15+
contain the user id. This will be sufficient for most use cases.
16+
17+
Patch this method to include the user id in the cache key if necessary, e.g.,
18+
for far more than 1,000 admin users to make the cached unit require less memory.
19+
20+
Parameters:
21+
user (User): The user for whom the cache key is being generated.
22+
permission (str): The permission for which the cache key is being generated.
23+
24+
Returns:
25+
str: The generated cache key.
26+
"""
27+
return f"filer:perm:{permission}"
28+
29+
30+
def get_folder_permission_cache(user: User, permission: str) -> typing.Optional[dict]:
31+
"""
32+
Retrieves the cached folder permissions for a given user and permission.
33+
34+
If the cache value exists, it returns the permissions for the user.
35+
If the cache value does not exist, it returns None.
36+
37+
Parameters:
38+
user (User): The user for whom the permissions are being retrieved.
39+
permission (str): The permission for which the permissions are being retrieved.
40+
41+
Returns:
42+
dict or None: The permissions for the user, or None if no cache value exists.
43+
"""
44+
cache_value = cache.get(get_folder_perm_cache_key(user, permission))
45+
if cache_value:
46+
return cache_value.get(user.pk, None)
47+
return None
48+
49+
50+
def clear_folder_permission_cache(user: User, permission: typing.Optional[str] = None) -> None:
51+
"""
52+
Clears the cached folder permissions for a given user.
53+
54+
If a specific permission is provided, it clears the cache for that permission only.
55+
If no specific permission is provided, it clears the cache for all permissions.
56+
57+
Parameters:
58+
user (User): The user for whom the permissions are being cleared.
59+
permission (str, optional): The specific permission to clear. Defaults to None.
60+
"""
61+
if permission is None:
62+
for perm in ['can_read', 'can_edit', 'can_add_children']:
63+
cache.delete(get_folder_perm_cache_key(user, perm))
64+
else:
65+
cache.delete(get_folder_perm_cache_key(user, permission))
66+
67+
68+
def update_folder_permission_cache(user: User, permission: str, id_list: typing.List[int]) -> None:
69+
"""
70+
Updates the cached folder permissions for a given user and permission.
71+
72+
It first retrieves the current permissions from the cache (or an empty dictionary if none exist).
73+
Then it updates the permissions for the user with the provided list of IDs.
74+
Finally, it sets the updated permissions back into the cache.
75+
76+
Parameters:
77+
user (User): The user for whom the permissions are being updated.
78+
permission (str): The permission to update.
79+
id_list (list): The list of IDs to set as the new permissions.
80+
"""
81+
perms = get_folder_permission_cache(user, permission) or {}
82+
perms[user.pk] = id_list
83+
cache.set(get_folder_perm_cache_key(user, permission), perms)

0 commit comments

Comments
 (0)