general: add support for slot assets (#24453)
This commit is contained in:
parent
ab023ecbab
commit
06c85910e4
|
@ -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')),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -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')
|
|
@ -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>
|
||||
|
|
|
@ -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
|
|
@ -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 = [
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Reference in New Issue