Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
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.5.0.pbs.1'
45 changes: 35 additions & 10 deletions filer/admin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,43 @@
#-*- coding: utf-8 -*-
from django.contrib import admin
from filer.admin.clipboardadmin import ClipboardAdmin
from filer.admin.fileadmin import FileAdmin
from filer.admin.folderadmin import FolderAdmin
from filer.admin.imageadmin import ImageAdmin
from filer.admin.archiveadmin import ArchiveAdmin
from filer.models import (Folder, File, Clipboard, Image,
Archive)

from filer.admin.trashadmin import Trash, TrashAdmin
from ..models import Clipboard, File, Folder, FolderPermission
from ..settings import FILER_IMAGE_MODEL
from ..utils.loader import load_model
from .clipboardadmin import ClipboardAdmin
from .fileadmin import FileAdmin
from .folderadmin import FolderAdmin
from .imageadmin import ImageAdmin
from .permissionadmin import PermissionAdmin

# PBS-specific: ThumbnailOption model may not exist in PBS migrations yet
try:
from ..models import ThumbnailOption
from .thumbnailoptionadmin import ThumbnailOptionAdmin
_has_thumbnail_option = True
except ImportError:
_has_thumbnail_option = False

# PBS-specific: Trash admin
from ..admin.trashadmin import Trash, TrashAdmin

Image = load_model(FILER_IMAGE_MODEL)

admin.site.register(Folder, FolderAdmin)
admin.site.register(File, FileAdmin)
admin.site.register(Clipboard, ClipboardAdmin)
admin.site.register(Image, ImageAdmin)
admin.site.register(Archive, ArchiveAdmin)
admin.site.register(FolderPermission, PermissionAdmin)

if _has_thumbnail_option:
admin.site.register(ThumbnailOption, ThumbnailOptionAdmin)

# PBS-specific: register Trash admin
admin.site.register([Trash], TrashAdmin)

# PBS-specific: Archive (backward compat)
try:
from ..models import Archive
from ..admin.archiveadmin import ArchiveAdmin
admin.site.register(Archive, ArchiveAdmin)
except ImportError:
pass
294 changes: 136 additions & 158 deletions filer/admin/clipboardadmin.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
#-*- coding: utf-8 -*-
from django.contrib import admin, messages
from django.core.exceptions import ValidationError
from django.forms.models import modelform_factory
from django.core.exceptions import PermissionDenied
from django.contrib import admin
from django.http import HttpResponse, HttpResponseRedirect
from django.http import JsonResponse
from django.urls import path, reverse
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from django.urls import re_path
from filer import settings as filer_settings
from filer.models import Clipboard, ClipboardItem, Folder, tools
from filer.utils.files import (
handle_upload, UploadException, matching_file_subtypes, truncate_filename
)
from filer.views import (
popup_param, selectfolder_param, current_site_param,
file_type_param

from .. import settings as filer_settings
from ..models import Clipboard, ClipboardItem, Folder
from ..settings import FILER_THUMBNAIL_ICON_SIZE
from ..utils.files import handle_request_files_upload, handle_upload
from ..utils.loader import load_model
from ..validation import validate_upload
from . import views


NO_PERMISSIONS = _("You do not have permission to upload files.")
NO_FOLDER_ERROR = _("Can't find folder to upload. Please refresh and try again")
NO_PERMISSIONS_FOR_FOLDER = _(
"Can't use this folder, Permission Denied. Please select another folder."
)
from filer.admin.tools import is_valid_destination
from filer.utils.is_ajax import is_ajax
import json

# even though the CharField is limited at 255 characters, the filename is used in
# thumbnail creation, which remembers the path and also post-fixes the name with
# '__32x32_q85_crop_subsampling-2_upscale.jpg'-like strings
FILENAME_LIMIT = 100 # larger values cause DataError

Image = load_model(filer_settings.FILER_IMAGE_MODEL)


# ModelAdmins
class ClipboardItemInline(admin.TabularInline):
Expand All @@ -31,149 +33,30 @@ class ClipboardItemInline(admin.TabularInline):
class ClipboardAdmin(admin.ModelAdmin):
model = Clipboard
inlines = [ClipboardItemInline]
# filter_horizontal = ('files',)
raw_id_fields = ('user',)
verbose_name = "DEBUG Clipboard"
verbose_name_plural = "DEBUG Clipboards"
messages = {
'already-exists': 'A file named {} already exists in the clipboard',
'request-invalid': "AJAX request not valid: form invalid '{}'"
}

def get_urls(self):
urls = super(ClipboardAdmin, self).get_urls()
url_patterns = [
re_path(r'^operations/paste_clipboard_to_folder/$',
self.admin_site.admin_view(self.paste_clipboard_to_folder),
name='filer-paste_clipboard_to_folder'),
re_path(r'^operations/discard_clipboard/$',
self.admin_site.admin_view(self.discard_clipboard),
name='filer-discard_clipboard'),
re_path(r'^operations/delete_clipboard/$',
self.admin_site.admin_view(self.delete_clipboard),
name='filer-delete_clipboard'),
# upload does it's own permission stuff (because of the stupid
# flash missing cookie stuff)
re_path(r'^operations/upload/$',
self.ajax_upload,
name='filer-ajax_upload'),
]
url_patterns.extend(urls)
return url_patterns

def get_clipboard(self, request):
return Clipboard.objects.get(id=request.POST.get('clipboard_id'))

def make_clipboard_redirect(self, request):
return HttpResponseRedirect('%s%s%s%s%s' % (
request.POST.get('redirect_to', ''),
popup_param(request),
selectfolder_param(request),
current_site_param(request),
file_type_param(request)))

def paste_clipboard_to_folder(self, request):
if request.method == 'POST':
folder_id = request.POST.get('folder_id')
if not folder_id:
raise PermissionDenied
folder = Folder.objects.get(id=folder_id)
if not is_valid_destination(request, folder):
raise PermissionDenied

clipboard = self.get_clipboard(request)
files_moved = tools.move_files_from_clipboard_to_folder(
request, clipboard, folder)
tools.discard_clipboard_files(clipboard, files_moved)
return self.make_clipboard_redirect(request)

def discard_clipboard(self, request):
if request.method == 'POST':
clipboard = self.get_clipboard(request)
tools.discard_clipboard(clipboard)
return self.make_clipboard_redirect(request)

def delete_clipboard(self, request):
if request.method == 'POST':
tools.delete_clipboard(self.get_clipboard(request))
return self.make_clipboard_redirect(request)

def clone_files_from_clipboard_to_folder(self, request):
if request.method == 'POST':
folder_id = request.POST.get('folder_id')
if not folder_id:
raise PermissionDenied
folder = Folder.objects.get(id=folder_id)
if not is_valid_destination(request, folder):
raise PermissionDenied
tools.clone_files_from_clipboard_to_folder(
self.get_clipboard(request), folder)
return self.make_clipboard_redirect(request)

@csrf_exempt
def ajax_upload(self, request, folder_id=None):
"""
receives an upload from the uploader. Receives only one file at the time.
"""
mimetype = "application/json" if is_ajax(request) else "text/html"
upload, file_obj, clipboard_item = None, None, None
try:
upload, original_filename, _ = handle_upload(request)
filename = truncate_filename(upload, maxlen=FILENAME_LIMIT)
upload.name = filename # the upload raw has also the title saved in a CharField

# Get clipboad
clipboard = Clipboard.objects.get_or_create(user=request.user)[0]
if any(f for f in clipboard.files.all() if f.original_filename == filename):
raise UploadException(self.messages['already-exists'].format(filename))
matched_file_types = matching_file_subtypes(filename, upload, request)
FileForm = modelform_factory(
model=matched_file_types[0],
fields=('original_filename', 'owner', 'file')
)
uploadform = FileForm({'original_filename': filename,
'owner': request.user.pk},
{'file': upload})
if uploadform.is_valid():
file_obj = uploadform.save(commit=False)
# Enforce the FILER_IS_PUBLIC_DEFAULT
file_obj.is_public = filer_settings.FILER_IS_PUBLIC_DEFAULT
file_obj.save()
clipboard_item = ClipboardItem(
clipboard=clipboard, file=file_obj)
clipboard_item.save()
json_response = {
'thumbnail': file_obj.icons['32'],
'alt_text': '',
'label': str(file_obj),
}
return HttpResponse(json.dumps(json_response),
content_type=mimetype)
else:
form_errors = '; '.join(['%s: %s' % (
field,
', '.join(errors)) for field, errors in list(uploadform.errors.items())
])
raise UploadException(self.messages['request-invalid'].format(form_errors))
except UploadException as exception:
return HttpResponse(json.dumps({'error': str(exception)}),
content_type=mimetype)
except Exception as error: # no matter the error, we don't return a 500 code
# an error occurred trying to build the file obj and the clipboard item
# since they are interconnected, we'll delete both to cleanup
if clipboard_item:
clipboard_item.file.file.close()
clipboard_item.file.file.delete()
clipboard_item.delete()
return HttpResponse(json.dumps({'error': str(error)}),
content_type=mimetype)
finally:
if upload:
upload.close()
if file_obj and file_obj.file:
file_obj.file.close()

def get_model_perms(self, request):
return [
path('operations/paste_clipboard_to_folder/',
self.admin_site.admin_view(views.paste_clipboard_to_folder),
name='filer-paste_clipboard_to_folder'),
path('operations/discard_clipboard/',
self.admin_site.admin_view(views.discard_clipboard),
name='filer-discard_clipboard'),
path('operations/delete_clipboard/',
self.admin_site.admin_view(views.delete_clipboard),
name='filer-delete_clipboard'),
path('operations/upload/<int:folder_id>/',
ajax_upload,
name='filer-ajax_upload'),
path('operations/upload/no_folder/',
ajax_upload,
name='filer-ajax_upload'),
] + super().get_urls()

def get_model_perms(self, *args, **kwargs):
"""
It seems this is only used for the list view. NICE :-)
"""
Expand All @@ -182,3 +65,98 @@ def get_model_perms(self, request):
'change': False,
'delete': False,
}


@csrf_exempt
def ajax_upload(request, folder_id=None):
"""
Receives an upload from the uploader. Receives only one file at a time.
"""

if not request.user.has_perm("filer.add_file"):
messages.error(request, NO_PERMISSIONS)
return JsonResponse({'error': NO_PERMISSIONS})

if folder_id:
try:
# Get folder
folder = Folder.objects.get(pk=folder_id)
except Folder.DoesNotExist:
messages.error(request, NO_FOLDER_ERROR)
return JsonResponse({'error': NO_FOLDER_ERROR})
else:
folder = Folder.objects.filter(pk=request.session.get('filer_last_folder_id', 0)).first()

# check permissions
if folder and not folder.has_add_children_permission(request):
messages.error(request, NO_PERMISSIONS_FOR_FOLDER)
return JsonResponse({'error': NO_PERMISSIONS_FOR_FOLDER})

if len(request.FILES) == 1:
# don't check if request is ajax or not, just grab the file
upload, filename, is_raw, mime_type = handle_request_files_upload(request)
else:
# else process the request as usual
upload, filename, is_raw, mime_type = handle_upload(request)
# TODO: Deprecated/refactor
# Get clipboad
# clipboard = Clipboard.objects.get_or_create(user=request.user)[0]

# find the file type
for filer_class in filer_settings.FILER_FILE_MODELS:
FileSubClass = load_model(filer_class)
# TODO: What if there are more than one that qualify?
if FileSubClass.matches_file_type(filename, upload, mime_type):
FileForm = modelform_factory(
model=FileSubClass,
fields=('original_filename', 'owner', 'file')
)
break
uploadform = FileForm({'original_filename': filename, 'owner': request.user.pk},
{'file': upload})
uploadform.request = request
uploadform.instance.mime_type = mime_type
if uploadform.is_valid():
try:
validate_upload(filename, upload, request.user, mime_type)
file_obj = uploadform.save(commit=False)
# Enforce the FILER_IS_PUBLIC_DEFAULT
file_obj.is_public = filer_settings.FILER_IS_PUBLIC_DEFAULT
except ValidationError as error:
messages.error(request, str(error))
return JsonResponse({'error': str(error)})
file_obj.folder = folder
file_obj.save()
# TODO: Deprecated/refactor
# clipboard_item = ClipboardItem(
# clipboard=clipboard, file=file_obj)
# clipboard_item.save()

try:
thumbnail = None
data = {
'thumbnail': thumbnail,
'alt_text': '',
'label': str(file_obj),
'file_id': file_obj.pk,
}
# prepare preview thumbnail
if isinstance(file_obj, Image):
data['thumbnail_180'] = reverse(
f"admin:filer_{file_obj._meta.model_name}_fileicon",
args=(file_obj.pk, FILER_THUMBNAIL_ICON_SIZE),
)
data['original_image'] = file_obj.url
return JsonResponse(data)
except Exception as error:
messages.error(request, str(error))
return JsonResponse({"error": str(error)})
else:
for key, error_list in uploadform.errors.items():
for error in error_list:
messages.error(request, error)

form_errors = '; '.join(['{}'.format(
', '.join(errors)) for errors in list(uploadform.errors.values())
])
return JsonResponse({'error': str(form_errors)}, status=200)
Loading
Loading