combo/combo/apps/assets/views.py

414 lines
14 KiB
Python

# combo - content management system
# Copyright (C) 2017-2018 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import tarfile
from io import BytesIO
import ckeditor
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.core.files.storage import default_storage
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views.generic import FormView, ListView, TemplateView
from sorl.thumbnail.shortcuts import get_thumbnail
from combo.apps.assets.utils import export_assets, import_assets
from combo.apps.maps.models import MapLayer
from combo.data.models import CellBase
from .forms import AssetsImportForm, AssetUploadForm
from .models import Asset
class CkEditorAsset:
def __init__(self, filepath):
self.filepath = filepath
self.name = os.path.basename(filepath)
self.src = ckeditor.utils.get_media_url(filepath)
@classmethod
def get_assets(cls, request):
return [cls(x) for x in ckeditor.views.get_image_files(request.user)]
def css_classes(self):
extension = os.path.splitext(self.filepath)[-1].strip('.')
if extension:
return 'asset-ext-%s' % extension
return ''
def size(self):
return os.stat(default_storage.path(self.filepath)).st_size
def thumb(self):
if getattr(settings, 'CKEDITOR_IMAGE_BACKEND', None):
thumb = ckeditor.utils.get_media_url(ckeditor.utils.get_thumb_filename(self.filepath))
else:
thumb = self.src
return thumb
def is_image(self):
return ckeditor.views.is_image(self.src)
class SlotAsset:
def __init__(self, key=None, name=None, asset_type='image', asset=None):
self.key = key
self.name = name
self.asset_type = asset_type
self.asset = asset
def is_image(self):
return self.asset_type == 'image' and bool(self.asset)
def size(self):
if self.asset:
try:
return os.stat(self.asset.asset.path).st_size
except OSError:
pass
return None
def src(self):
return self.asset.asset.url if self.asset else ''
def thumb(self):
if self.asset.asset.path.lower().endswith('.svg'):
return self.asset.asset.url
else:
return get_thumbnail(self.asset.asset, '75x75').url
@classmethod
def get_assets(cls):
assets = {x.key: x for x in Asset.objects.all()}
uniq_slots = {}
uniq_slots.update(settings.COMBO_ASSET_SLOTS)
cells = CellBase.get_cells(select_related={'__all__': ['page'], 'data_linkcell': ['link_page']})
for cell in cells:
uniq_slots.update(cell.get_asset_slots())
for map_layer in MapLayer.objects.filter(kind='geojson'):
uniq_slots.update(map_layer.get_asset_slots())
for key, value in uniq_slots.items():
yield cls(
key,
name=value.get('label'),
asset_type=value.get('asset-type', 'image'),
asset=assets.get(key),
)
class Assets(ListView):
template_name = 'combo/manager_assets.html'
paginate_by = 10
def get_files(self):
return list(SlotAsset.get_assets()) + CkEditorAsset.get_assets(self.request)
def get_queryset(self):
files = self.get_files()
q = self.request.GET.get('q')
if q:
files = [x for x in files if q.lower() in x.name.lower()]
files.sort(key=lambda x: getattr(x, 'name'))
return files
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['query'] = self.request.GET.get('q') or ''
return context
def get_anchored_url(self, key=None, name=None):
url = reverse('combo-manager-assets')
for i, asset in enumerate(self.get_queryset()):
if key and key != getattr(asset, 'key', None):
continue
if name and name > getattr(asset, 'name', None):
continue
return url + '?page=%s' % ((i // self.paginate_by) + 1)
return url
assets = Assets.as_view()
class AssetsBrowse(Assets):
template_name = 'combo/manager_assets_browse.html'
paginate_by = 7
def get_files(self):
return CkEditorAsset.get_assets(self.request)
browse = AssetsBrowse.as_view()
class AssetUpload(FormView):
form_class = AssetUploadForm
template_name = 'combo/manager_asset_upload.html'
def form_valid(self, form):
# use native ckeditor view so it's available from ckeditor file/image
# dialogs.
ckeditor_upload_view = ckeditor.views.ImageUploadView()
self.request.GET = {'CKEditorFuncNum': '-'} # hack
ckeditor_upload_view.post(self.request)
return super().form_valid(form)
def get_success_url(self):
return Assets(request=self.request).get_anchored_url(name=self.request.FILES['upload'].name)
asset_upload = AssetUpload.as_view()
class AssetOverwrite(FormView):
form_class = AssetUploadForm
template_name = 'combo/manager_asset_overwrite.html'
success_url = reverse_lazy('combo-manager-assets')
def form_valid(self, form):
img_orig = self.request.GET['img']
if '..' in img_orig:
raise PermissionDenied() # better safe than sorry
base_path = settings.CKEDITOR_UPLOAD_PATH
if getattr(settings, 'CKEDITOR_RESTRICT_BY_USER', False):
base_path = os.path.join(base_path, self.request.user.username)
if not img_orig.startswith(base_path):
raise PermissionDenied()
try:
os.stat(default_storage.path(img_orig))
except ValueError:
raise PermissionDenied()
upload = self.request.FILES['upload']
# check that the new file and the original have the same extension
ext_orig = os.path.splitext(img_orig)[1].lower()
ext_upload = os.path.splitext(upload.name)[1].lower()
if ext_orig != ext_upload:
messages.error(
self.request,
_('You have to upload a file with the same extension (%(ext)s).') % {'ext': ext_orig},
)
return super().form_valid(form)
default_storage.delete(img_orig)
if getattr(settings, 'CKEDITOR_IMAGE_BACKEND', None):
thumb = ckeditor.utils.get_thumb_filename(img_orig)
default_storage.delete(thumb)
saved_path = default_storage.save(img_orig, upload)
backend = ckeditor.image_processing.get_backend()
upload.seek(0) # rewind file to be sure
try:
backend.image_verify(upload)
except ckeditor.utils.NotAnImageException:
pass
else:
if backend.should_create_thumbnail(saved_path):
backend.create_thumbnail(saved_path)
return super().form_valid(form)
def get_success_url(self):
img_orig = self.request.GET['img']
return Assets(request=self.request).get_anchored_url(name=os.path.basename(img_orig))
asset_overwrite = AssetOverwrite.as_view()
class AssetDelete(TemplateView):
template_name = 'combo/manager_asset_confirm_delete.html'
def post(self, request):
img_orig = request.GET['img']
if '..' in img_orig:
raise PermissionDenied() # better safe than sorry
base_path = settings.CKEDITOR_UPLOAD_PATH
if getattr(settings, 'CKEDITOR_RESTRICT_BY_USER', False):
base_path = os.path.join(base_path, request.user.username)
if not img_orig.startswith(base_path):
raise PermissionDenied()
try:
os.stat(default_storage.path(img_orig))
except ValueError:
raise PermissionDenied()
default_storage.delete(img_orig)
return redirect(Assets(request=self.request).get_anchored_url(name=os.path.basename(img_orig)))
asset_delete = AssetDelete.as_view()
class SlotAssets(ListView):
template_name = 'combo/manager_slot_assets.html'
def get_assets(self, cell):
asset_slots = cell.get_asset_slots()
assets = {x.key: x for x in Asset.objects.filter(key__in=asset_slots.keys())}
for key, value in asset_slots.items():
yield SlotAsset(
key,
name=value.get('short_label'),
asset_type=value.get('asset-type', 'image'),
asset=assets.get(key),
)
def get_queryset(self):
cell_reference = self.kwargs['cell_reference']
try:
cell = CellBase.get_cell(cell_reference)
except ObjectDoesNotExist:
raise Http404()
return self.get_assets(cell)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['cell_reference'] = self.kwargs['cell_reference']
return context
slot_assets = SlotAssets.as_view()
class SlotAssetUpload(FormView):
form_class = AssetUploadForm
template_name = 'combo/manager_asset_upload.html'
success_url = reverse_lazy('combo-manager-assets')
def form_valid(self, form):
try:
self.asset = Asset.objects.get(key=self.kwargs['key'])
except Asset.DoesNotExist:
self.asset = Asset(key=self.kwargs['key'])
self.asset.asset = self.request.FILES['upload']
self.asset.save()
return super().form_valid(form)
def get_success_url(self):
if self.request.GET.get('cell_reference'):
cell_reference = self.request.GET['cell_reference']
try:
cell = CellBase.get_cell(cell_reference)
except ObjectDoesNotExist:
pass
else:
return (
reverse('combo-manager-page-view', kwargs={'pk': cell.page_id})
+ '#cell-'
+ cell_reference
)
return Assets(request=self.request).get_anchored_url(key=self.kwargs['key'])
slot_asset_upload = SlotAssetUpload.as_view()
class SlotAssetDelete(TemplateView):
template_name = 'combo/manager_asset_confirm_delete.html'
def post(self, request, *args, **kwargs):
Asset.objects.filter(key=kwargs['key']).delete()
if self.request.GET.get('cell_reference'):
cell_reference = self.request.GET['cell_reference']
try:
cell = CellBase.get_cell(cell_reference)
except ObjectDoesNotExist:
pass
else:
return redirect(
reverse('combo-manager-page-view', kwargs={'pk': cell.page_id})
+ '#cell-'
+ cell_reference
)
return redirect(Assets(request=self.request).get_anchored_url(key=kwargs['key']))
slot_asset_delete = SlotAssetDelete.as_view()
class AssetsImport(FormView):
form_class = AssetsImportForm
template_name = 'combo/manager_assets_import.html'
success_url = reverse_lazy('combo-manager-assets')
def form_valid(self, form):
overwrite = form.cleaned_data.get('overwrite')
try:
import_assets(form.cleaned_data['assets_file'], overwrite)
except tarfile.TarError:
messages.error(self.request, _('The assets file is not valid.'))
return super().form_valid(form)
messages.success(self.request, _('The assets file has been imported.'))
return super().form_valid(form)
assets_import = AssetsImport.as_view()
def assets_export(request, *args, **kwargs):
fd = BytesIO()
export_assets(fd)
return HttpResponse(fd.getvalue(), content_type='application/x-tar')
def serve_asset(request, key):
asset = get_object_or_404(Asset, key=key)
if not os.path.exists(asset.asset.path):
raise Http404()
# get options for thumbnail
thumb_options = request.GET.dict()
width = thumb_options.pop('width', None)
height = thumb_options.pop('height', None)
geometry_string = ''
if width:
geometry_string += width
if height:
geometry_string += 'x%s' % height
# no thumbnail whithout geometry_string or for a svg file
if not geometry_string or asset.asset.name.endswith('svg'):
return redirect(asset.asset.url)
# get or create thumbnail and return url
return redirect(get_thumbnail(asset.asset, geometry_string, **thumb_options).url)
class AssetsExportSize(TemplateView):
template_name = 'combo/manager_assets_export_size.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
media_prefix = default_storage.path('')
computed_size = 0
for basedir, dummy, filenames in os.walk(media_prefix):
for filename in filenames:
computed_size += os.stat(os.path.join(basedir, filename)).st_size
context['size'] = computed_size
return context
assets_export_size = AssetsExportSize.as_view()