Skip to content

Commit ac76c85

Browse files
committed
Merge branch 'finder' of https://github.com/django-cms/django-filer into finder
2 parents 4a1c76d + e2d3d56 commit ac76c85

File tree

7 files changed

+107
-36
lines changed

7 files changed

+107
-36
lines changed

README-Finder.md

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,16 @@ while preserving the possibility to perform queries across all tables. This allo
5050
models inheriting from the `AbstractFileModel` without the need of **django-polymorphic**, and thus
5151
a join between two or more tables for each query.
5252

53-
The Admin interface has also been completely rewritten and allows multi-tenant usage out of the box.
54-
For instance, there is no more list view for the `FolderModel` and the `FileModel` (or any
55-
specialized implementation). Instead, there are only details views for each of those models. This is
56-
much nearer to what a user would expect from a file system. Therefore, if a user wants to access the
57-
list view of a folder, he is redirected immediately to the detail view of the root folder of the
58-
given tenant. From there, he can traverse the folder tree in the same manner he's used to from his
59-
operating system.
53+
Each root folder is associated with a *realm*. A realm is a named area inside the file system, which
54+
can be used to separate files and folders of different tenants. Each realm has its own root folder.
55+
By using realms, it is possible to use **django-filer** in a multi-tenant environment.
56+
57+
The Admin interface has also been completely rewritten. For instance, there is no more list view for
58+
the `FolderModel` and the `FileModel` (or any specialized implementation). Instead, there are only
59+
details views for each of those models. This is much nearer to what a user would expect from a file
60+
system. Therefore, if a user wants to access the list view of a folder, he is redirected immediately
61+
to the detail view of the root folder of the given tenant. From there, he can traverse the folder
62+
tree in the same manner he's used to from his operating system.
6063

6164

6265
## New User Interface
@@ -95,8 +98,8 @@ ancestors. This allows to easily move files between those folders.
9598
### Multiple Favrourite Folders
9699

97100
Each user can have multiple favourite folders. This allows him to quickly access those folders from
98-
the navigation bar. It also it pssoble to drag a file from the current view into one of the tabs for
99-
of the favorite folders.
101+
the navigation bar. It is also possible to drag a file from the current view into one of the tabs
102+
for of the favorite folders.
100103

101104

102105
### Implementation Details
@@ -142,7 +145,56 @@ If you use:
142145
* `finder.contrib.audio` or `finder.contrib.video`, assure that `ffmpeg-python` is installed.
143146
* `finder.contrib.image.pil`, assure that `Pillow` is installed.
144147
* `finder.contrib.image.svg`, assure that `reportlab` and `svglib` are installed.
145-
* Postgres as database, install `psycopg2` or `psycopg2-binary` if available for your platform.
148+
* Postgres as a database, install `psycopg2` or `psycopg2-binary` if available for your platform.
149+
150+
Each realm requires two storage backends. One for the public files and one for their thumbnails
151+
and/or samples. Add these storage backends to the `STORAGES` setting in your `settings.py`:
152+
153+
```python
154+
STORAGES = {
155+
'default': {
156+
'BACKEND': 'django.core.files.storage.FileSystemStorage',
157+
},
158+
'staticfiles': {
159+
'BACKEND': 'django.contrib.staticfiles.storage.StaticFilesStorage',
160+
},
161+
'finder_public': {
162+
'BACKEND': 'finder.storage.FinderSystemStorage',
163+
'OPTIONS': {
164+
'location': '/path/to/your/media/filer_public',
165+
'base_url': '/media/filer_public/',
166+
'allow_overwrite': True,
167+
},
168+
},
169+
'finder_public_samples': {
170+
'BACKEND': 'finder.storage.FinderSystemStorage',
171+
'OPTIONS': {
172+
'location': '/path/to/your/media/filer_public_thumbnails',
173+
'base_url': '/media/filer_public_thumbnails/',
174+
'allow_overwrite': True,
175+
},
176+
},
177+
}
178+
```
179+
180+
If instead of using the local file system you want to use another storage backend, such as Amazon S3
181+
or Google Cloud Storage, configure the corresponding storage backend in the `STORAGES` setting as:
182+
183+
```python
184+
STORAGES = {
185+
...
186+
'finder_public': {
187+
'BACKEND': 'storages.backends.s3.S3Storage',
188+
'OPTIONS': {...},
189+
},
190+
'finder_public_samples': {
191+
'BACKEND': 'storages.backends.s3.S3Storage',
192+
'OPTIONS': {...},
193+
},
194+
}
195+
```
196+
197+
Note that multiple realms can share the same storage location or bucket.
146198

147199
Run the migrations for app `finder`:
148200

client/browser/FileSelectDialog.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,12 @@ function ScrollSpy(props) {
7171

7272

7373
const FilesList = memo((props: any) => {
74-
const {structure, setDirty, selectFile, selectedFileId, webAudio} = props;
74+
const {structure, setDirty, isDirty, selectFile, selectedFileId, webAudio} = props;
7575

7676
return (
7777
<ul className="files-browser">{
7878
structure.files.length === 0 ?
79-
<li className="status">{gettext("Empty folder")}</li> : (
79+
(!isDirty && <li className="status">{gettext("Empty folder")}</li>) : (
8080
<>{structure.files.map(file => (
8181
<li key={file.id} onClick={() => selectFile(file)}>
8282
<Figure {...file} webAudio={webAudio} isSelected={file.id === selectedFileId} />
@@ -197,6 +197,13 @@ const FileSelectDialog = forwardRef((props: any, forwardedRef) => {
197197
const response = await fetch(`${baseUrl}structure/${realm}${params.size === 0 ? '' : `?${params.toString()}`}`);
198198
if (response.ok) {
199199
setStructure(await response.json());
200+
window.setTimeout(() => {
201+
// first show the structure from the root for orientation, then scroll to the current folder
202+
const currentListItem = ref.current.querySelector('ul[role="navigation"] li:has(>[aria-current="true"]');
203+
if (currentListItem) {
204+
currentListItem.scrollIntoView({behavior: 'smooth', block: 'center'});
205+
}
206+
}, 999);
200207
} else {
201208
console.error(response);
202209
}
@@ -324,17 +331,20 @@ const FileSelectDialog = forwardRef((props: any, forwardedRef) => {
324331
handleUpload={handleUpload}
325332
ref={uploaderRef}
326333
settings={{csrf_token: csrfToken, base_url: baseUrl}}
327-
>{
328-
structure.files === null ?
329-
<div className="status">{gettext("Loading files…")}</div> :
334+
>{Array.isArray(structure.files) ?
330335
<FilesList
331336
structure={structure}
332337
setDirty={setDirty}
338+
isDirty={isDirty}
333339
selectFile={selectFile}
334340
selectedFileId={props.selectedFileId}
335341
webAudio={webAudio}
336342
/>
337-
}</FileUploader>
343+
:
344+
<div className="status">{gettext("Loading structure…")}</div>
345+
}
346+
{isDirty && <div className="status">{gettext("Loading files…")}</div>}
347+
</FileUploader>
338348
</div>
339349
</>}
340350
</div>

client/scss/finder-browser.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ dialog {
8484
box-shadow: 0 0 0.5rem #c8c8c8;
8585
width: 80%;
8686
height: 80%;
87-
max-width: 900px;
87+
max-width: 1200px;
8888
padding: 2rem;
8989

9090
.wrapper {

finder/browser/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class BrowserView(View):
3030
The view for web component <finder-browser>.
3131
"""
3232
action = None
33-
limit = 50
33+
limit = 100
3434

3535
def dispatch(self, request, *args, **kwargs):
3636
if self.action is None:

finder/management/commands/filer_to_finder.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def handle(self, verbosity, *args, **options):
2424

2525
def forward(self):
2626
site = Site.objects.get(id=settings.SITE_ID)
27-
owner = FilerFolder.objects.filter(parent__isnull=True, owner__isnull=False).first().owner
27+
owner = FilerFolder.objects.filter(parent__isnull=True).first().owner
2828
try:
2929
realm = FinderRealmModel.objects.get(site=site, slug=admin_site.name)
3030
except FinderRealmModel.DoesNotExist:
@@ -35,16 +35,17 @@ def forward(self):
3535
self.migrate_folder(filer_folder, realm.root_folder)
3636

3737
def migrate_folder(self, filer_folder, finder_parent):
38-
finder_folder = finder_parent.listdir(name=filer_folder.name, is_folder=True).first()
39-
if finder_folder is None:
38+
if finder_folder := finder_parent.listdir(name=filer_folder.name, is_folder=True).first():
39+
finder_folder = FinderFolder.objects.get(id=finder_folder['id'])
40+
else:
4041
finder_folder = FinderFolder.objects.create(
4142
name=filer_folder.name,
4243
parent=finder_parent,
4344
created_at=filer_folder.created_at,
4445
last_modified_at=filer_folder.modified_at,
4546
owner_id=filer_folder.owner_id,
4647
)
47-
self.stdout.write(f"Create folder {finder_folder} in {finder_parent}")
48+
self.stdout.write(f"Create folder {finder_folder} in {finder_parent}”.")
4849

4950
allowed_image_types = ['image/gif', 'image/jpeg', 'image/png', 'image/webp', 'image/svg+xml']
5051
for filer_file in filer_folder.files.all():
@@ -60,8 +61,11 @@ def migrate_file(self, filer_file, finder_parent):
6061
path = Path(filer_file.file.name)
6162
inode_id = path.parent.stem
6263
try:
63-
finder_file = FinderFile.objects.get_inode(id=inode_id)
64+
finder_file = FinderFile.objects.get(id=inode_id)
6465
except FinderFile.DoesNotExist:
66+
if not filer_file._file_size:
67+
self.stderr.write(f"File {filer_file} has no file size.")
68+
return
6569
FinderFile.objects.create(
6670
id=inode_id,
6771
name=filer_file.name if filer_file.name else filer_file.original_filename,
@@ -91,15 +95,19 @@ def migrate_image(self, filer_image, finder_parent):
9195
meta_data = {'alt_text': getattr(filer_image, 'default_alt_text', '')}
9296
try:
9397
center_x, center_y = map(float, filer_image.subject_location.split(','))
94-
# since Filer does not store the area of interest, we assume it is 10px
95-
meta_data['crop_x'] = center_x - 5
96-
meta_data['crop_y'] = center_y - 5
97-
meta_data['crop_size'] = 10
98+
# since Filer does not store the area of interest, we assume it's 20% of its width or height
99+
crop_size = int(round(max(filer_image.width, filer_image.height) * 0.2))
100+
meta_data['crop_x'] = center_x - crop_size / 2
101+
meta_data['crop_y'] = center_y - crop_size / 2
102+
meta_data['crop_size'] = crop_size
98103
except ValueError:
99104
pass
100105
try:
101-
finder_image = FinderImage.objects.get_inode(id=inode_id)
102-
except FinderFile.DoesNotExist:
106+
finder_image = FinderImage.objects.get(id=inode_id)
107+
except FinderImage.DoesNotExist:
108+
if not filer_image._file_size:
109+
self.stderr.write(f"Image {filer_image} has no file size.")
110+
return
103111
FinderImage.objects.create(
104112
id=inode_id,
105113
name=filer_image.name if filer_image.name else filer_image.original_filename,

finder/management/commands/finder.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ def handle(self, verbosity, subcommand, *args, **options):
2020
self.reorganize()
2121
elif subcmd == 'reorder':
2222
self.reorder()
23-
elif subcmd == 'migrate-filer':
24-
call_command('filer_to_finder', verbosity=verbosity)
2523
else:
2624
self.stderr.write(f"Unknown subcommand ‘{subcmd}’")
2725

finder/models/inode.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from operator import and_, or_
55

66
from django.conf import settings
7-
from django.core.exceptions import ImproperlyConfigured, ValidationError
7+
from django.core.exceptions import ImproperlyConfigured, ValidationError, ObjectDoesNotExist
88
from django.db import connections, models
99
from django.db.models.aggregates import Aggregate
1010
from django.db.models.expressions import F, Value, Q
@@ -166,10 +166,13 @@ def get_inode(self, **lookup):
166166
folder_qs = FolderModel.objects.none()
167167
elif (folder_qs := FolderModel.objects.filter(**lookup)).exists():
168168
return folder_qs.get()
169-
values = folder_qs.values('id', mime_type=Value(None, output_field=models.CharField())).union(*[
170-
model.objects.values('id', 'mime_type').filter(self.get_query(model, lookup))
171-
for model in FileModel.get_models()
172-
]).get()
169+
try:
170+
values = folder_qs.values('id', mime_type=Value(None, output_field=models.CharField())).union(*[
171+
model.objects.values('id', 'mime_type').filter(self.get_query(model, lookup))
172+
for model in FileModel.get_models()
173+
]).get()
174+
except FolderModel.DoesNotExist:
175+
raise self.model.DoesNotExist(f"No inode found matching the given lookup: {lookup}.")
173176
return FileModel.objects.get_model_for(values['mime_type']).objects.get(id=values['id'])
174177

175178
@classmethod

0 commit comments

Comments
 (0)