general: add support for slot assets (#24453)

This commit is contained in:
Frédéric Péters 2018-06-13 11:43:41 +02:00
parent ab023ecbab
commit 06c85910e4
12 changed files with 241 additions and 8 deletions

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-06-12 11:42
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Asset',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.CharField(max_length=128, unique=True)),
('asset', models.FileField(upload_to=b'assets')),
],
),
]

View File

View File

@ -0,0 +1,21 @@
# 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/>.
from django.db import models
class Asset(models.Model):
key = models.CharField(max_length=128, unique=True)
asset = models.FileField(upload_to='assets')

View File

@ -34,7 +34,7 @@
<table class="main">
<thead>
<tr>
<th>{% trans "Filename" %}</th>
<th>{% trans "Name" %}</th>
<th>{% trans "Size" %}</th>
<th></th>
<th></th>
@ -43,14 +43,23 @@
<tbody>
{% for asset in object_list %}
<tr class="{{ asset.css_classes }}">
<td><a href="{{ asset.src }}">{{ asset.filename }}</a></td>
<td>{{ asset.size|filesizeformat }}</td>
<td><a href="{{ asset.src }}">{{ asset.name }}</a></td>
<td>{% if asset.size %}{{ asset.size|filesizeformat }}{% else %}-{% endif %}</td>
<td class="image">{% if asset.is_image %}<img data-href="{{ asset.src }}" src="{{ asset.thumb }}"/>{% endif %}</td>
<td class="actions">
{% if asset.key %}{# theme asset #}
<a href="{% url 'combo-manager-slot-asset-upload' key=asset.key %}"
class="overwrite" rel="popup">{% trans 'Overwrite' %}</a>
{% if asset.asset %}
<a href="{% url 'combo-manager-slot-asset-delete' key=asset.key %}"
class="delete" rel="popup">{% trans 'Delete' %}</a>
{% endif %}
{% else %}
<a href="{% url 'combo-manager-asset-overwrite' %}?img={{asset.filepath|iriencode}}"
class="overwrite" rel="popup">{% trans 'Overwrite' %}</a>
<a href="{% url 'combo-manager-asset-delete' %}?img={{asset.filepath|iriencode}}"
class="delete" rel="popup">{% trans 'Delete' %}</a>
{% endif %}
</td>
</td>
</tr>

View File

@ -0,0 +1,47 @@
# 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/>.
from django import template
from django.db.models.fields.files import ImageFieldFile
from ..models import Asset
register = template.Library()
@register.simple_tag
def asset_url(*args):
for asset in args:
if isinstance(asset, ImageFieldFile):
try:
return asset.url
except ValueError: # no associated file
continue
if isinstance(asset, basestring):
try:
asset = Asset.objects.get(key=asset)
except Asset.DoesNotExist:
continue
return asset.asset.url
return ''
@register.assignment_tag
def get_asset(key):
try:
return Asset.objects.get(key=key)
except Asset.DoesNotExist:
return None

View File

@ -25,6 +25,8 @@ assets_manager_urls = [
url(r'^delete$', views.asset_delete, name='combo-manager-asset-delete'),
url(r'^overwrite/$', views.asset_overwrite, name='combo-manager-asset-overwrite'),
url(r'^upload/$', views.asset_upload, name='combo-manager-asset-upload'),
url(r'^upload/(?P<key>[\w_:-]+)/$', views.slot_asset_upload, name='combo-manager-slot-asset-upload'),
url(r'^delete/(?P<key>[\w_:-]+)/$', views.slot_asset_delete, name='combo-manager-slot-asset-delete'),
]
urlpatterns = [

View File

@ -24,16 +24,22 @@ from django.shortcuts import redirect
from django.views.generic import TemplateView, ListView, FormView
import ckeditor
from sorl.thumbnail.shortcuts import get_thumbnail
from .forms import AssetUploadForm
from .models import Asset
class Asset(object):
class CkEditorAsset(object):
def __init__(self, filepath):
self.filepath = filepath
self.filename = os.path.basename(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:
@ -55,16 +61,43 @@ class Asset(object):
return ckeditor.views.is_image(self.src)
class SlotAsset(object):
def __init__(self, key=None, asset=None):
self.key = key
self.name = settings.COMBO_ASSET_SLOTS[key]['label']
self.asset = asset
def is_image(self):
return bool(self.asset)
def size(self):
if self.asset:
return os.stat(self.asset.asset.path).st_size
return None
def src(self):
return self.asset.asset.url if self.asset else ''
def thumb(self):
return get_thumbnail(self.asset.asset, '75x75').url
@classmethod
def get_assets(cls):
assets = dict([(x.key, x) for x in Asset.objects.all() if x.key in settings.COMBO_ASSET_SLOTS])
for key, value in settings.COMBO_ASSET_SLOTS.items():
yield cls(key, asset=assets.get(key))
class Assets(ListView):
template_name = 'combo/manager_assets.html'
paginate_by = 10
def get_queryset(self):
files = [Asset(x) for x in ckeditor.views.get_image_files(self.request.user)]
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.filename.lower()]
files.sort(key=lambda x: getattr(x, 'filename'))
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):
@ -142,3 +175,30 @@ class AssetDelete(TemplateView):
return redirect(reverse('combo-manager-assets'))
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:
asset = Asset.objects.get(key=self.kwargs['key'])
except Asset.DoesNotExist:
asset = Asset(key=self.kwargs['key'])
asset.asset = self.request.FILES['upload']
asset.save()
return super(SlotAssetUpload, self).form_valid(form)
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(reverse('combo-manager-assets'))
slot_asset_delete = SlotAssetDelete.as_view()

View File

@ -199,6 +199,7 @@ p#redirection {
#assets-browser #assets-listing table td.image {
padding: 0;
text-align: center;
}
#assets-browser #assets-listing table td.actions {

View File

@ -303,6 +303,10 @@ REQUESTS_TIMEOUT = 28
# default duration of notifications (in days)
COMBO_DEFAULT_NOTIFICATION_DURATION = 3
# predefined slots for assets
# example: {'banner': {'label': 'Banner image'}}
COMBO_ASSET_SLOTS = {}
# hide work-in-progress/experimental/whatever cells for now
BOOKING_CALENDAR_CELL_ENABLED = False
NEWSLETTERS_CELL_ENABLED = False

View File

@ -20,6 +20,7 @@ from webtest import Upload
from combo.wsgi import application
from combo.data.models import Page, CellBase, TextCell, LinkCell, ConfigJsonCell, JsonCell, PageSnapshot
from combo.apps.assets.models import Asset
from combo.apps.family.models import FamilyInfosCell
from combo.apps.search.models import SearchCell
@ -652,6 +653,42 @@ def test_asset_management_search(app, admin_user):
resp = resp.form.submit()
assert resp.body.count('<tr class="asset') == 2
def test_asset_slots_management(app, admin_user):
app = login(app)
assert Asset.objects.count() == 0
with override_settings(COMBO_ASSET_SLOTS={'collectivity:banner': {'label': 'Banner'}}):
resp = app.get('/manage/assets/')
assert '>Banner<' in resp.body
assert '>Delete<' not in resp.body
resp = resp.click('Overwrite')
resp.form['upload'] = Upload('test.png',
base64.decodestring('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAgAABAADRWoApgAA\nAABJRU5ErkJggg=='),
'image/png')
resp = resp.form.submit().follow()
assert 'test.png' in resp.body
assert '>Delete<' in resp.body
assert Asset.objects.filter(key='collectivity:banner').count() == 1
# upload a new version of image
resp = resp.click('Overwrite')
resp.form['upload'] = Upload('test2.png',
base64.decodestring('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAgAABAADRWoApgAA\nAABJRU5ErkJggg=='),
'image/png')
resp = resp.form.submit().follow()
assert 'test2.png' in resp.body
assert '>Delete<' in resp.body
assert Asset.objects.filter(key='collectivity:banner').count() == 1
resp = resp.click('Delete')
resp = resp.form.submit().follow()
assert '>Banner<' in resp.body
assert '>Delete<' not in resp.body
assert Asset.objects.filter(key='collectivity:banner').count() == 0
def test_menu_json(app, admin_user):
app.get('/manage/menu.json', status=302)

View File

@ -1,9 +1,16 @@
from StringIO import StringIO
import pytest
from django.core.files import File
from django.template import Context, Template
from django.test import override_settings
from django.test.client import RequestFactory
from django.contrib.auth.models import User, Group, AnonymousUser
from combo.data.models import Page
from combo.apps.assets.models import Asset
pytestmark = pytest.mark.django_db
@ -121,3 +128,24 @@ def test_get_group():
t = Template('{% load combo %}{% regroup cities by country as country_list %}'
'{% for c in country_list|get_group:"USA" %}{{c.name}},{% endfor %}')
assert t.render(context) == 'New York,Chicago,'
def test_asset_template_tags():
with override_settings(COMBO_ASSET_SLOTS={'collectivity:banner': {'label': 'Banner'}}):
t = Template('''{% load assets %}{% get_asset "collectivity:banner" as banner %}{% if banner %}BANNER{% endif %}''')
assert t.render(Context()) == ''
Asset(key='collectivity:banner', asset=File(StringIO('test'), 'test.png')).save()
assert t.render(Context()) == 'BANNER'
t = Template('''{% load assets %}{% asset_url "collectivity:banner" %}''')
assert t.render(Context()) == '/media/assets/test.png'
page = Page(title='Home', slug='index', template_name='standard')
page.save()
t = Template('''{% load assets %}{% asset_url page.picture "collectivity:banner" %}''')
assert t.render(Context()) == '/media/assets/test.png'
page.picture = File(StringIO('test'), 'test2.png')
page.save()
assert t.render(Context({'page': page})) == '/media/page-pictures/test2.png'