Skip to content
Open
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
4565069
Phase 1-2: Foundation, build system, and non-conflicting upstream add…
Copilot Apr 30, 2026
d901d18
Phase 3+5: Merge core models (filemodels, foldermodels, mixins, image…
Copilot Apr 30, 2026
602b3f4
Phase 4+6+7: Merge admin, fields, and utilities with PBS customizatio…
Copilot Apr 30, 2026
848acee
Phase 8: Generate migration 0008 for upstream schema changes (Thumbna…
Copilot Apr 30, 2026
ea8f300
Phase 9-10: Adopt upstream templates/static/locale, fix import issues…
Copilot Apr 30, 2026
8efb149
Remove accidentally committed test.zip
Copilot Apr 30, 2026
2a29e5e
BEN-2954: fix test error
Apr 30, 2026
fac8419
BEN-2954: fix tests
Apr 30, 2026
d140d5b
BEN-2954: more test fixes
Apr 30, 2026
1d8f171
BEN-2954: PBS fixes
Apr 30, 2026
f2283df
BEN-2954: set version 3.4.4
Apr 30, 2026
ae5bf9d
BEN-2954: fix cdn
Apr 30, 2026
649d9a0
Merge branch 'BEN-3407' into BEN-2954
May 15, 2026
15bea78
Merge branch 'master_pbs' into BEN-2954
May 28, 2026
77b78d8
BEN-2954: use bento3 patch versioning
May 28, 2026
857efaa
Ben-2954: test
May 28, 2026
cca5ae4
BEN-2954: font awesome upgrade and debug
May 28, 2026
e23579a
BEN-2954: fix new line error
Jun 8, 2026
d7cd0c5
BEN-2954: fix tests
Jun 8, 2026
d29f4a6
BEN-2954: test
Jun 8, 2026
78dd965
BEN-2954: fix clipboard
Jun 8, 2026
98d367e
BEN-2954: fix dropzone
Jun 8, 2026
6d8486a
BEN-2954: use filer tree
Jun 8, 2026
6ff4b23
BEN-2954: extract zip files check
Jun 9, 2026
ff064cb
BEN-2954: remove clipboard
Jun 9, 2026
53de0b6
BEN-2954: filer archive model added
Jun 9, 2026
f504aef
BEn-2954: test archive and browser reload after drag file upload
Jun 9, 2026
45a4d02
BEN-2954: debug logs added
Jun 9, 2026
5964cd5
BEN-2954: test
Jun 9, 2026
8ab90d4
BEN-2954: debug
Jun 9, 2026
13ec1b9
BEN-2954: test
Jun 9, 2026
a92d981
BEN-2954: setup.py removed
Jun 9, 2026
badec15
BEN-2954: remove logs
Jun 9, 2026
8abf6c6
BEN-2954: fix file uploads and edit
Jun 10, 2026
c52912f
BEN-2954: rename filer versions
Jun 10, 2026
bed9d6d
BEN-2954: add button
Jun 10, 2026
dc8739a
BEN-2954: test upload
Jun 10, 2026
5e8e393
BEN-2954: bump-my-version added and configed with makefile
Jun 11, 2026
3deb34f
BEN-2954: fix version in .bumpversion.toml
Jun 11, 2026
913a81a
BEN-2954: use short sha
Jun 11, 2026
ea07107
BEN-2954: nexus publish commands
Jun 11, 2026
7f83356
BEN-2954: fix makefile for second prekind bump version
Jun 11, 2026
1c4c079
BEN-2954: fix prekind second attempt from a different github hash
Jun 11, 2026
68a0189
BEN-2954: debug file upload
Jun 11, 2026
b0545da
BEN-2954: use sha param when executed from GHA in order to create pac…
Jun 11, 2026
a5e264f
BEN-2954: use print
Jun 11, 2026
c52b9ba
BEN-2954: fix stale clipboard
Jun 11, 2026
0c382f2
BEN-2954: remove debug logs
Jun 11, 2026
840997d
BEN-2954: lazy load folder tree
Jun 11, 2026
a1ba3a6
BEN-2954: fallback improved
Jun 15, 2026
df51be8
BEN-2954: make uploads looser due to pillow limitations
Jun 16, 2026
b8be6e2
BEN-2954: role restriction
Jun 16, 2026
4811f1f
BEN-2954: developer guide readme
Jun 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .bumpversion.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[tool.bumpversion]
current_version = "3.4.4+pbs.1"
commit = false
tag = false
parse = """(?x)
(?P<major>0|[1-9]\\d*)\\.
(?P<minor>0|[1-9]\\d*)\\.
(?P<patch>0|[1-9]\\d*)
\\+pbs\\.
(?P<pbs>0|[1-9]\\d*)
(?:
\\.(?P<prekind>dev)
\\.g[0-9a-f]{7,40}
\\.\\d{8}
)?
"""

serialize = [
"{major}.{minor}.{patch}+pbs.{pbs}.{prekind}.g{$GITHUB_SHA}.{now:%Y%m%d}",
"{major}.{minor}.{patch}+pbs.{pbs}"
]


[tool.bumpversion.parts.prekind]
values = [
"",
"dev"
]
optional_value = ""


[[tool.bumpversion.files]]
filename = "filer/__init__.py"
search = "{current_version}"
replace = "{new_version}"
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
.installed.cfg
bin
develop-eggs
dist
/dist
downloads
eggs
parts
Expand All @@ -14,6 +14,7 @@ tmp
*.egg
.DS_Store
.sass-cache
node_modules
.project
.pydevproject
.settings
Expand Down
56 changes: 46 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,68 @@ help: ## This help.
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n\nTargets:\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-10s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST)
.DEFAULT_GOAL := help

.PHONY: bump-pbs bump-prekind build publish-nexus publish-nexus-dry-run

IMAGE_NAME := django-filer-test
CONTAINER_NAME := django-filer-test-run
SHELL:=/bin/bash

## Build the test Docker image
test-build:
test-build: ## Build the test Docker image
docker build -f Dockerfile.test -t $(IMAGE_NAME) .

## Run unit tests in a Docker container
test:
test: ## Run unit tests in a Docker container
@if [ -z "$$(docker images -q $(IMAGE_NAME) 2>/dev/null)" ]; then \
echo "Image not found, building..."; \
$(MAKE) test-build; \
fi
docker run --rm --name $(CONTAINER_NAME) $(IMAGE_NAME)

## Run tests with verbose output and stop on first failure
test-verbose: test-build
test-verbose: test-build ## Run tests with verbose output and stop on first failure
docker run --rm --name $(CONTAINER_NAME) $(IMAGE_NAME) \
pytest -vx --ds=filer.test_settings --pyargs filer.tests

## Open a shell in the test container (useful for debugging)
test-shell: test-build
test-shell: test-build ## Open a shell in the test container (useful for debugging)
docker run --rm -it --name $(CONTAINER_NAME) $(IMAGE_NAME) /bin/bash

## Remove the test Docker image
test-clean:
test-clean: ## Remove the test Docker image
-docker rmi $(IMAGE_NAME)


bump-pbs: ## Bump PBS number: 3.4.4+pbs.3 -> 3.4.4+pbs.4
bump-my-version bump --allow-dirty pbs

bump-prekind: ## Bump prekind dev version: 3.4.4+pbs.3 -> 3.4.4+pbs.3.dev.g<sha>.YYYYMMDD. Pass sha=<commit> to override.
@current=$$(grep "^__version__" filer/__init__.py | sed "s/^__version__ = '//;s/'.*//"); \
sha="$${sha:-$$(git rev-parse --short=8 HEAD)}"; \
sha8="$${sha:0:8}"; \
if echo "$$current" | grep -q '\.dev\.'; then \
current_sha=$$(echo "$$current" | sed -nE "s/.*\.dev\.g([0-9a-f]{7,40})\.[0-9]{8}/\1/p"); \
current_sha8="$${current_sha:0:8}"; \
if [ "$$current_sha8" = "$$sha8" ]; then \
echo "Nothing to bump: '$$current' already targets commit '$$sha8'. Create a new commit first or run 'make bump-pbs'."; \
exit 1; \
fi; \
base=$$(echo "$$current" | sed -E "s/\.dev\.g[0-9a-f]+\.[0-9]{8}$$//"); \
new_version="$$base.dev.g$$sha8.$$(date +%Y%m%d)"; \
else \
base="$$current"; \
new_version="$$base.dev.g$$sha8.$$(date +%Y%m%d)"; \
fi; \
echo "Bumping: $$current -> $$new_version"; \
sed -i.bak "s/__version__ = '$$current'/__version__ = '$$new_version'/" filer/__init__.py && rm -f filer/__init__.py.bak; \
sed -i.bak -E "s/^current_version = \"[^\"]+\"/current_version = \"$$new_version\"/" .bumpversion.toml && rm -f .bumpversion.toml.bak


build: ## Build distribution packages (sdist and wheel)
rm -rf dist/ build/ *.egg-info
python3 -m pip install --upgrade --break-system-packages build
python3 -m build

publish-nexus: build ## Publish to Nexus using ~/.pypirc config
python3 -m pip install --upgrade --break-system-packages twine
python3 -m twine upload -r nexus dist/*

publish-nexus-dry-run: build ## Validate packages ready for Nexus (without uploading)
python3 -m pip install --upgrade --break-system-packages twine
python3 -m twine check dist/*

207 changes: 207 additions & 0 deletions UPSTREAM_MERGE_FIXES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
# Upstream Merge Fix Progress

## Overview

**Upstream source:** `https://github.com/django-cms/django-filer` (master branch, v3.4.4)
**Fork divergence:** Forked at tag `0.9` (commit `a9364960`), ~790 PBS vs ~1,977 upstream commits, 603 files changed.
**Target:** Django 5.1 / Python 3.12+

### Test Results Timeline

| Stage | Failed | Passed | Skipped |
|-------|--------|--------|---------|
| Pre-merge (PBS baseline) | 0 | 153 | 68 |
| After upstream merge | 73 | ~80 | 68 |
| After Session 1 fixes | 67 | 86 | 68 |
| After Session 2 fixes | 59 | 94 | 68 |
| **After all fixes (final)** | **0** | **153** | **68** |

---

## Fixes Applied

### 1. `setup.py` — Build-time import error (GHA blocker)

- **Problem:** `__import__('filer').__version__` fails because Django isn't installed in the build environment
- **Fix:** Replaced with regex-based `get_version()` that reads `filer/__init__.py` without importing
- **Also:** Removed deprecated `setuptools.command.test` references and `test_suite`/`tests_require`
- **Code:**
```python
def get_version():
"""Read version from filer/__init__.py without importing the package."""
init_py = os.path.join(os.path.dirname(__file__), 'filer', '__init__.py')
with open(init_py) as f:
match = re.search(r"^__version__\s*=\s*['\"]([^'\"]+)['\"]", f.read(), re.M)
if not match:
raise RuntimeError("Cannot find __version__ in filer/__init__.py")
return match.group(1)
```

### 2. `filer/__init__.py` — PEP 440 version

- **Problem:** `3.5.0.pbs.1` is invalid per PEP 440
- **Fix:** Changed to `3.5.0+pbs.1` (local version identifier)

### 3. `filer/models/abstract.py` — VILImage import

- **Problem:** `from easy_thumbnails.VIL import Image` fails without `reportlab`
- **Fix:** Wrapped in try/except, set `VILImage = None` as fallback
- **Code:**
```python
try:
from easy_thumbnails.VIL import Image as VILImage
except (ImportError, ModuleNotFoundError):
VILImage = None
```

### 4. `filer/admin/fileadmin.py` — `display_canonical` FieldError

- **Problem:** `get_readonly_fields` returned only model field names for restricted/core files, but fieldsets contain `display_canonical` (an admin method, not a model field)
- **Fix:** Appended `'display_canonical'` to the readonly fields list in that code path
- **Error:** `FieldError: Unknown field(s) (display_canonical) specified for File`

### 5. `filer/templates/admin/filer/submit_line.html` — Delete URL with empty pk

- **Problem:** `{% url opts|admin_urlname:'delete' obj.pk %}` failed when `obj.pk` was empty
- **Fix:** Changed to `original.pk` and added `and original.pk` guard

### 6. `filer/admin/folderadmin.py` — Multiple fixes

#### 6a. `destination_folders` AJAX view
- **Problem:** `NoReverseMatch: 'filer-destination_folders' not found` — the fancytree folder picker widget needed this URL
- **Fix:** Added AJAX view and URL pattern to `FolderAdmin.get_urls()`

#### 6b. `move_file_to_clipboard` signature mismatch
- **Problem:** Both call sites passed wrong number of arguments
- **Fix:** Added missing `request` argument at both call sites

#### 6c. `KeyError: 'name'` in folder form clean
- **Problem:** Folder form clean method assumed `'name'` was always in `cleaned_data`
- **Fix:** Added guard `if 'name' not in cleaned_data: return cleaned_data`

#### 6d. `exclude` tuple incomplete
- **Problem:** Upstream had `exclude = ('parent',)` but PBS needs `('parent', 'owner', 'folder_type')`
- **Fix:** Restored full PBS exclude tuple

#### 6e. `get_form()` — PBS field visibility logic
- **Problem:** Upstream `get_form()` didn't handle PBS-specific dynamic field visibility
- **Fix:** Restored full PBS logic:
- Hide `site`/`shared` for child folders and core folders
- Only show `shared` to superusers
- Pop `restricted` for add view
- Set `owner`/`parent` in clean method

#### 6f. `_move_files_and_folders_impl` — MPTT corruption
- **Problem:** Upstream used bulk `update()` which bypasses MPTT tree updates and model signals
- **Fix:** Changed to individual `save()` calls to trigger MPTT tree recalculation

#### 6g. `move_files_and_folders()` — PBS validation
- **Problem:** PBS site validation checks were missing from upstream's move implementation
- **Fix:** Restored:
- Root folder move prevention
- Site consistency checks (all moved items must belong to same site as destination)
- **Code:**
```python
# PBS: prevent moving root folders
if folders_queryset.filter(parent=None).exists():
messages.error(request, "To prevent potential problems, users "
"are not allowed to move root folders.")
return
# PBS: site consistency checks
sites_from_folders = \
set(folders_queryset.values_list('site_id', flat=True)) | \
set(files_queryset.exclude(folder__isnull=True).\
values_list('folder__site_id', flat=True))
if sites_from_folders and None in sites_from_folders:
messages.error(request, "Some of the selected files/folders "
"do not belong to any site.")
return
```

#### 6h. `log_deletion` deprecation
- **Problem:** Dead `else` branches for Django < 5.1; bug where `files_queryset` was logged instead of `folders_queryset`
- **Fix:** Removed dead branches, fixed queryset reference

### 7. `filer/server/backends/` — Server backend fixes (3 files)

- **Files:** `default.py`, `nginx.py`, `xsendfile.py`
- **Problem 1:** `filer_file.mime_type` — `filer_file` is a `FieldFile` (not the `File` model), so it has no `mime_type` attribute
- **Fix 1:** Added `mimetypes.guess_type()` fallback
- **Problem 2:** `file_obj=filer_file.file` — `filer_file` is already the `FieldFile`, `.file` is the raw Python file object
- **Fix 2:** Changed to `file_obj=filer_file`
- **Error:** `AttributeError: 'MultiStorageFieldFile' has no attribute 'mime_type'`

### 8. `filer/admin/views.py` — NewFolderForm missing `site` field

- **Problem:** Form only had `fields = ('name',)`, ignoring PBS `site` field from POST data
- **Fix:** Added `'site'` to form fields: `fields = ('name', 'site')`

### 9. `filer/templatetags/filermedia.py` — Deleted by upstream merge

- **Problem:** Upstream deleted `filermedia.py` but 8+ templates still reference `{% load filermedia %}`
- **Fix:** Restored file with `filer_staticmedia_prefix` simple tag
- **Error:** `TemplateSyntaxError: 'filermedia' is not a registered tag library`

### 10. `filer/admin/clipboardadmin.py` — Clipboard not created on upload

- **Problem:** Upstream commented out `Clipboard.objects.get_or_create(user=request.user)` in `ajax_upload`
- **Fix:** Restored the get_or_create call so clipboard is created during upload
- **Error:** `Clipboard.DoesNotExist`

### 11. `filer/tests/admin.py` — Test compatibility fixes

- **Clipboard access:** Replaced `self.superuser.filer_clipboard` with `Clipboard.objects.get(user=self.superuser)`
- **Return values:** Removed `return folders, files` from test methods

### 12. `filer/utils/cdn.py` — `modified_at` None guard

- **Problem:** `get_cdn_url()` crashes with `TypeError: unsupported operand type(s) for +: 'NoneType' and 'datetime.timedelta'` when `file_obj.modified_at` is `None`
- **Fix:** Added early return `if file_obj.modified_at is None: return url` before the timedelta arithmetic
- **Error:** `TypeError: unsupported operand type(s) for +: 'NoneType' and 'datetime.timedelta'`

---

## PBS-Specific Features Preserved

These features exist in the PBS fork but not in upstream django-filer:

| Feature | Description |
|---------|-------------|
| **Trash / Soft-delete** | Files/folders are soft-deleted to trash before permanent deletion |
| **Site-based permissions** | Folders belong to Django sites; users have site-scoped access |
| **Folder types** | `CORE_FOLDER`, `SITE_FOLDER` — restricts operations on system folders |
| **Restricted flag** | Files/folders can be marked restricted; cascades to children |
| **Shared folders** | M2M relationship allowing folders shared across sites |
| **CDN invalidation** | URL hashing and CDN cache invalidation on file changes |
| **S3/Botocore storage** | Multi-storage backend with S3 support |
| **Clipboard model** | Per-user clipboard for file operations |
| **Folder-affects-URL** | Hash-based filenames tied to folder structure |

---

## Test Results — ✅ All Passing

**Final result: 153 passed, 68 skipped, 0 failed** (matches PBS baseline)

All 59 previously failing tests have been resolved. The upstream merge is fully compatible with the PBS test suite.

---

## Files Modified (Summary)

| File | Type of Change |
|------|---------------|
| `setup.py` | Build fix (regex version reader) |
| `filer/__init__.py` | PEP 440 version fix |
| `filer/models/abstract.py` | Import guard for VILImage |
| `filer/admin/fileadmin.py` | Readonly fields fix |
| `filer/admin/folderadmin.py` | 8 separate fixes (AJAX view, move/copy validation, MPTT, field visibility) |
| `filer/admin/views.py` | NewFolderForm site field |
| `filer/admin/clipboardadmin.py` | Clipboard creation restored |
| `filer/server/backends/default.py` | mime_type + file_obj fixes |
| `filer/server/backends/nginx.py` | mime_type + file_obj fixes |
| `filer/server/backends/xsendfile.py` | mime_type + file_obj fixes |
| `filer/templatetags/filermedia.py` | Restored deleted file |
| `filer/templates/admin/filer/submit_line.html` | Delete URL pk guard |
| `filer/tests/admin.py` | Test compatibility fixes |
| `filer/utils/cdn.py` | `modified_at` None guard for CDN URL |
2 changes: 2 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@
collect_ignore = [
os.path.join(os.path.dirname(__file__), "filer", "tests", "utils"),
os.path.join(os.path.dirname(__file__), "filer", "tests", "__init__.py"),
os.path.join(os.path.dirname(__file__), "filer", "contrib"),
os.path.join(os.path.dirname(__file__), "filer", "management"),
]

17 changes: 14 additions & 3 deletions filer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
#-*- coding: utf-8 -*-
# version string following pep-0440
__version__ = '0.9.123' # pragma: nocover
"""
See PEP 386 (https://www.python.org/dev/peps/pep-0386/)

Release logic:
1. Increase version number (change __version__ below).
2. Check that all changes have been documented in CHANGELOG.rst.
3. git add filer/__init__.py CHANGELOG.rst
4. git commit -m 'Bump to {new version}'
5. git push
6. Assure that all tests pass on CI
7. Create a new release on github.
"""

__version__ = '3.4.4+pbs.1'
Loading