338 lines
12 KiB
Python
338 lines
12 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 json
|
|
import tarfile
|
|
import os
|
|
|
|
from django.conf import settings
|
|
from django.contrib import messages
|
|
from django.core.exceptions import PermissionDenied
|
|
from django.core.files.storage import default_storage
|
|
from django.core.urlresolvers import reverse, reverse_lazy
|
|
from django.http import Http404, HttpResponse
|
|
from django.shortcuts import get_object_or_404
|
|
from django.shortcuts import redirect
|
|
from django.utils.six import BytesIO
|
|
from django.utils.translation import ugettext_lazy as _
|
|
from django.views.generic import TemplateView, ListView, FormView
|
|
|
|
import ckeditor
|
|
from sorl.thumbnail.shortcuts import get_thumbnail
|
|
|
|
from combo.data.models import CellBase
|
|
from combo.data.utils import import_site
|
|
|
|
from .forms import AssetUploadForm, AssetsImportForm
|
|
from .models import Asset
|
|
|
|
|
|
class CkEditorAsset(object):
|
|
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(object):
|
|
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 = dict([(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 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_queryset(self):
|
|
files = list(SlotAsset.get_assets()) + CkEditorAsset.get_assets(self.request)
|
|
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(Assets, self).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 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(AssetUpload, self).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()
|
|
|
|
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(AssetOverwrite, self).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(AssetOverwrite, self).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()
|
|
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 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(SlotAssetUpload, self).form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
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()
|
|
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:
|
|
assets = tarfile.open(fileobj=form.cleaned_data['assets_file'])
|
|
except tarfile.TarError:
|
|
messages.error(self.request, _('The assets file is not valid.'))
|
|
return super(AssetsImport, self).form_valid(form)
|
|
media_prefix = default_storage.path('')
|
|
for tarinfo in assets.getmembers():
|
|
filepath = default_storage.path(tarinfo.name)
|
|
if not overwrite and os.path.exists(filepath):
|
|
continue
|
|
if tarinfo.name == '_assets.json':
|
|
json_assets = assets.extractfile(tarinfo).read()
|
|
import_site(json.loads(json_assets.decode('utf-8')))
|
|
else:
|
|
assets.extract(tarinfo, path=media_prefix)
|
|
messages.success(self.request, _('The assets file has been imported.'))
|
|
return super(AssetsImport, self).form_valid(form)
|
|
|
|
assets_import = AssetsImport.as_view()
|
|
|
|
|
|
def assets_export(request, *args, **kwargs):
|
|
fd = BytesIO()
|
|
assets_file = tarfile.open('assets.tar', 'w', fileobj=fd)
|
|
media_prefix = default_storage.path('')
|
|
for basedir, dirnames, filenames in os.walk(media_prefix):
|
|
for filename in filenames:
|
|
assets_file.add(
|
|
os.path.join(basedir, filename),
|
|
os.path.join(basedir, filename)[len(media_prefix):])
|
|
if Asset.objects.exists():
|
|
json_file = tarfile.TarInfo('_assets.json')
|
|
json_fd = BytesIO()
|
|
export = {'assets': Asset.export_all_for_json(),}
|
|
json_fd.write(json.dumps(export).encode('utf-8'))
|
|
json_file.size = json_fd.tell()
|
|
json_fd.seek(0)
|
|
assets_file.addfile(json_file, fileobj=json_fd)
|
|
assets_file.close()
|
|
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)
|