0001-general-add-support-for-slot-assets-24453.patch
combo/apps/assets/migrations/0001_initial.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# Generated by Django 1.11.12 on 2018-06-12 11:42 |
|
3 |
from __future__ import unicode_literals |
|
4 | ||
5 |
from django.db import migrations, models |
|
6 | ||
7 | ||
8 |
class Migration(migrations.Migration): |
|
9 | ||
10 |
initial = True |
|
11 | ||
12 |
dependencies = [ |
|
13 |
] |
|
14 | ||
15 |
operations = [ |
|
16 |
migrations.CreateModel( |
|
17 |
name='Asset', |
|
18 |
fields=[ |
|
19 |
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
|
20 |
('key', models.CharField(max_length=128, unique=True)), |
|
21 |
('asset', models.FileField(upload_to=b'assets')), |
|
22 |
], |
|
23 |
), |
|
24 |
] |
combo/apps/assets/models.py | ||
---|---|---|
1 |
# combo - content management system |
|
2 |
# Copyright (C) 2017-2018 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
from django.db import models |
|
18 | ||
19 |
class Asset(models.Model): |
|
20 |
key = models.CharField(max_length=128, unique=True) |
|
21 |
asset = models.FileField(upload_to='assets') |
combo/apps/assets/templates/combo/manager_assets.html | ||
---|---|---|
34 | 34 |
<table class="main"> |
35 | 35 |
<thead> |
36 | 36 |
<tr> |
37 |
<th>{% trans "Filename" %}</th>
|
|
37 |
<th>{% trans "Name" %}</th>
|
|
38 | 38 |
<th>{% trans "Size" %}</th> |
39 | 39 |
<th></th> |
40 | 40 |
<th></th> |
... | ... | |
43 | 43 |
<tbody> |
44 | 44 |
{% for asset in object_list %} |
45 | 45 |
<tr class="{{ asset.css_classes }}"> |
46 |
<td><a href="{{ asset.src }}">{{ asset.filename }}</a></td>
|
|
47 |
<td>{{ asset.size|filesizeformat }}</td>
|
|
46 |
<td><a href="{{ asset.src }}">{{ asset.name }}</a></td> |
|
47 |
<td>{% if asset.size %}{{ asset.size|filesizeformat }}{% else %}-{% endif %}</td>
|
|
48 | 48 |
<td class="image">{% if asset.is_image %}<img data-href="{{ asset.src }}" src="{{ asset.thumb }}"/>{% endif %}</td> |
49 | 49 |
<td class="actions"> |
50 |
{% if asset.key %}{# theme asset #} |
|
51 |
<a href="{% url 'combo-manager-slot-asset-upload' key=asset.key %}" |
|
52 |
class="overwrite" rel="popup">{% trans 'Overwrite' %}</a> |
|
53 |
{% if asset.asset %} |
|
54 |
<a href="{% url 'combo-manager-slot-asset-delete' key=asset.key %}" |
|
55 |
class="delete" rel="popup">{% trans 'Delete' %}</a> |
|
56 |
{% endif %} |
|
57 |
{% else %} |
|
50 | 58 |
<a href="{% url 'combo-manager-asset-overwrite' %}?img={{asset.filepath|iriencode}}" |
51 | 59 |
class="overwrite" rel="popup">{% trans 'Overwrite' %}</a> |
52 | 60 |
<a href="{% url 'combo-manager-asset-delete' %}?img={{asset.filepath|iriencode}}" |
53 | 61 |
class="delete" rel="popup">{% trans 'Delete' %}</a> |
62 |
{% endif %} |
|
54 | 63 |
</td> |
55 | 64 |
</td> |
56 | 65 |
</tr> |
combo/apps/assets/templatetags/assets.py | ||
---|---|---|
1 |
# combo - content management system |
|
2 |
# Copyright (C) 2017-2018 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
from django import template |
|
18 |
from django.db.models.fields.files import ImageFieldFile |
|
19 | ||
20 |
from ..models import Asset |
|
21 | ||
22 |
register = template.Library() |
|
23 | ||
24 | ||
25 |
@register.simple_tag |
|
26 |
def asset_url(*args): |
|
27 |
for asset in args: |
|
28 |
if isinstance(asset, ImageFieldFile): |
|
29 |
try: |
|
30 |
return asset.url |
|
31 |
except ValueError: # no associated file |
|
32 |
continue |
|
33 |
if isinstance(asset, basestring): |
|
34 |
try: |
|
35 |
asset = Asset.objects.get(key=asset) |
|
36 |
except Asset.DoesNotExist: |
|
37 |
continue |
|
38 |
return asset.asset.url |
|
39 |
return '' |
|
40 | ||
41 | ||
42 |
@register.assignment_tag |
|
43 |
def get_asset(key): |
|
44 |
try: |
|
45 |
return Asset.objects.get(key=key) |
|
46 |
except Asset.DoesNotExist: |
|
47 |
return None |
combo/apps/assets/urls.py | ||
---|---|---|
25 | 25 |
url(r'^delete$', views.asset_delete, name='combo-manager-asset-delete'), |
26 | 26 |
url(r'^overwrite/$', views.asset_overwrite, name='combo-manager-asset-overwrite'), |
27 | 27 |
url(r'^upload/$', views.asset_upload, name='combo-manager-asset-upload'), |
28 |
url(r'^upload/(?P<key>[\w_:-]+)/$', views.slot_asset_upload, name='combo-manager-slot-asset-upload'), |
|
29 |
url(r'^delete/(?P<key>[\w_:-]+)/$', views.slot_asset_delete, name='combo-manager-slot-asset-delete'), |
|
28 | 30 |
] |
29 | 31 | |
30 | 32 |
urlpatterns = [ |
combo/apps/assets/views.py | ||
---|---|---|
24 | 24 |
from django.views.generic import TemplateView, ListView, FormView |
25 | 25 | |
26 | 26 |
import ckeditor |
27 |
from sorl.thumbnail.shortcuts import get_thumbnail |
|
27 | 28 | |
28 | 29 |
from .forms import AssetUploadForm |
30 |
from .models import Asset |
|
29 | 31 | |
30 | 32 | |
31 |
class Asset(object): |
|
33 |
class CkEditorAsset(object):
|
|
32 | 34 |
def __init__(self, filepath): |
33 | 35 |
self.filepath = filepath |
34 |
self.filename = os.path.basename(filepath)
|
|
36 |
self.name = os.path.basename(filepath) |
|
35 | 37 |
self.src = ckeditor.utils.get_media_url(filepath) |
36 | 38 | |
39 |
@classmethod |
|
40 |
def get_assets(cls, request): |
|
41 |
return [cls(x) for x in ckeditor.views.get_image_files(request.user)] |
|
42 | ||
37 | 43 |
def css_classes(self): |
38 | 44 |
extension = os.path.splitext(self.filepath)[-1].strip('.') |
39 | 45 |
if extension: |
... | ... | |
55 | 61 |
return ckeditor.views.is_image(self.src) |
56 | 62 | |
57 | 63 | |
64 |
class SlotAsset(object): |
|
65 |
def __init__(self, key=None, asset=None): |
|
66 |
self.key = key |
|
67 |
self.name = settings.COMBO_ASSET_SLOTS[key]['label'] |
|
68 |
self.asset = asset |
|
69 | ||
70 |
def is_image(self): |
|
71 |
return bool(self.asset) |
|
72 | ||
73 |
def size(self): |
|
74 |
if self.asset: |
|
75 |
return os.stat(self.asset.asset.path).st_size |
|
76 |
return None |
|
77 | ||
78 |
def src(self): |
|
79 |
return self.asset.asset.url if self.asset else '' |
|
80 | ||
81 |
def thumb(self): |
|
82 |
return get_thumbnail(self.asset.asset, '75x75').url |
|
83 | ||
84 |
@classmethod |
|
85 |
def get_assets(cls): |
|
86 |
assets = dict([(x.key, x) for x in Asset.objects.all() if x.key in settings.COMBO_ASSET_SLOTS]) |
|
87 |
for key, value in settings.COMBO_ASSET_SLOTS.items(): |
|
88 |
yield cls(key, asset=assets.get(key)) |
|
89 | ||
90 | ||
58 | 91 |
class Assets(ListView): |
59 | 92 |
template_name = 'combo/manager_assets.html' |
60 | 93 |
paginate_by = 10 |
61 | 94 | |
62 | 95 |
def get_queryset(self): |
63 |
files = [Asset(x) for x in ckeditor.views.get_image_files(self.request.user)]
|
|
96 |
files = list(SlotAsset.get_assets()) + CkEditorAsset.get_assets(self.request)
|
|
64 | 97 |
q = self.request.GET.get('q') |
65 | 98 |
if q: |
66 |
files = [x for x in files if q.lower() in x.filename.lower()]
|
|
67 |
files.sort(key=lambda x: getattr(x, 'filename'))
|
|
99 |
files = [x for x in files if q.lower() in x.name.lower()] |
|
100 |
files.sort(key=lambda x: getattr(x, 'name')) |
|
68 | 101 |
return files |
69 | 102 | |
70 | 103 |
def get_context_data(self, **kwargs): |
... | ... | |
142 | 175 |
return redirect(reverse('combo-manager-assets')) |
143 | 176 | |
144 | 177 |
asset_delete = AssetDelete.as_view() |
178 | ||
179 | ||
180 |
class SlotAssetUpload(FormView): |
|
181 |
form_class = AssetUploadForm |
|
182 |
template_name = 'combo/manager_asset_upload.html' |
|
183 |
success_url = reverse_lazy('combo-manager-assets') |
|
184 | ||
185 |
def form_valid(self, form): |
|
186 |
try: |
|
187 |
asset = Asset.objects.get(key=self.kwargs['key']) |
|
188 |
except Asset.DoesNotExist: |
|
189 |
asset = Asset(key=self.kwargs['key']) |
|
190 |
asset.asset = self.request.FILES['upload'] |
|
191 |
asset.save() |
|
192 |
return super(SlotAssetUpload, self).form_valid(form) |
|
193 | ||
194 |
slot_asset_upload = SlotAssetUpload.as_view() |
|
195 | ||
196 | ||
197 |
class SlotAssetDelete(TemplateView): |
|
198 |
template_name = 'combo/manager_asset_confirm_delete.html' |
|
199 | ||
200 |
def post(self, request, *args, **kwargs): |
|
201 |
Asset.objects.filter(key=kwargs['key']).delete() |
|
202 |
return redirect(reverse('combo-manager-assets')) |
|
203 | ||
204 |
slot_asset_delete = SlotAssetDelete.as_view() |
combo/manager/static/css/combo.manager.css | ||
---|---|---|
199 | 199 | |
200 | 200 |
#assets-browser #assets-listing table td.image { |
201 | 201 |
padding: 0; |
202 |
text-align: center; |
|
202 | 203 |
} |
203 | 204 | |
204 | 205 |
#assets-browser #assets-listing table td.actions { |
combo/settings.py | ||
---|---|---|
303 | 303 |
# default duration of notifications (in days) |
304 | 304 |
COMBO_DEFAULT_NOTIFICATION_DURATION = 3 |
305 | 305 | |
306 |
# predefined slots for assets |
|
307 |
COMBO_ASSET_SLOTS = {} |
|
308 | ||
306 | 309 |
# hide work-in-progress/experimental/whatever cells for now |
307 | 310 |
BOOKING_CALENDAR_CELL_ENABLED = False |
308 | 311 |
NEWSLETTERS_CELL_ENABLED = False |
tests/test_manager.py | ||
---|---|---|
20 | 20 | |
21 | 21 |
from combo.wsgi import application |
22 | 22 |
from combo.data.models import Page, CellBase, TextCell, LinkCell, ConfigJsonCell, JsonCell, PageSnapshot |
23 |
from combo.apps.assets.models import Asset |
|
23 | 24 |
from combo.apps.family.models import FamilyInfosCell |
24 | 25 |
from combo.apps.search.models import SearchCell |
25 | 26 | |
... | ... | |
652 | 653 |
resp = resp.form.submit() |
653 | 654 |
assert resp.body.count('<tr class="asset') == 2 |
654 | 655 | |
656 |
def test_asset_slots_management(app, admin_user): |
|
657 |
app = login(app) |
|
658 | ||
659 |
assert Asset.objects.count() == 0 |
|
660 | ||
661 |
with override_settings(COMBO_ASSET_SLOTS={'collectivity:banner': {'label': 'Banner'}}): |
|
662 |
resp = app.get('/manage/assets/') |
|
663 |
assert '>Banner<' in resp.body |
|
664 |
assert '>Delete<' not in resp.body |
|
665 | ||
666 |
resp = resp.click('Overwrite') |
|
667 |
resp.form['upload'] = Upload('test.png', |
|
668 |
base64.decodestring('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAgAABAADRWoApgAA\nAABJRU5ErkJggg=='), |
|
669 |
'image/png') |
|
670 |
resp = resp.form.submit().follow() |
|
671 |
assert 'test.png' in resp.body |
|
672 |
assert '>Delete<' in resp.body |
|
673 |
assert Asset.objects.filter(key='collectivity:banner').count() == 1 |
|
674 | ||
675 |
# upload a new version of image |
|
676 |
resp = resp.click('Overwrite') |
|
677 |
resp.form['upload'] = Upload('test2.png', |
|
678 |
base64.decodestring('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAgAABAADRWoApgAA\nAABJRU5ErkJggg=='), |
|
679 |
'image/png') |
|
680 |
resp = resp.form.submit().follow() |
|
681 |
assert 'test2.png' in resp.body |
|
682 |
assert '>Delete<' in resp.body |
|
683 |
assert Asset.objects.filter(key='collectivity:banner').count() == 1 |
|
684 | ||
685 |
resp = resp.click('Delete') |
|
686 |
resp = resp.form.submit().follow() |
|
687 |
assert '>Banner<' in resp.body |
|
688 |
assert '>Delete<' not in resp.body |
|
689 |
assert Asset.objects.filter(key='collectivity:banner').count() == 0 |
|
690 | ||
691 | ||
655 | 692 |
def test_menu_json(app, admin_user): |
656 | 693 |
app.get('/manage/menu.json', status=302) |
657 | 694 |
tests/test_public_templatetags.py | ||
---|---|---|
1 |
from StringIO import StringIO |
|
2 | ||
1 | 3 |
import pytest |
2 | 4 | |
5 |
from django.core.files import File |
|
3 | 6 |
from django.template import Context, Template |
7 |
from django.test import override_settings |
|
4 | 8 |
from django.test.client import RequestFactory |
5 | 9 |
from django.contrib.auth.models import User, Group, AnonymousUser |
6 | 10 | |
11 |
from combo.data.models import Page |
|
12 |
from combo.apps.assets.models import Asset |
|
13 | ||
7 | 14 |
pytestmark = pytest.mark.django_db |
8 | 15 | |
9 | 16 | |
... | ... | |
121 | 128 |
t = Template('{% load combo %}{% regroup cities by country as country_list %}' |
122 | 129 |
'{% for c in country_list|get_group:"USA" %}{{c.name}},{% endfor %}') |
123 | 130 |
assert t.render(context) == 'New York,Chicago,' |
131 | ||
132 |
def test_asset_template_tags(): |
|
133 |
with override_settings(COMBO_ASSET_SLOTS={'collectivity:banner': {'label': 'Banner'}}): |
|
134 |
t = Template('''{% load assets %}{% get_asset "collectivity:banner" as banner %}{% if banner %}BANNER{% endif %}''') |
|
135 |
assert t.render(Context()) == '' |
|
136 | ||
137 |
Asset(key='collectivity:banner', asset=File(StringIO('test'), 'test.png')).save() |
|
138 |
assert t.render(Context()) == 'BANNER' |
|
139 | ||
140 |
t = Template('''{% load assets %}{% asset_url "collectivity:banner" %}''') |
|
141 |
assert t.render(Context()) == '/media/assets/test.png' |
|
142 | ||
143 |
page = Page(title='Home', slug='index', template_name='standard') |
|
144 |
page.save() |
|
145 | ||
146 |
t = Template('''{% load assets %}{% asset_url page.picture "collectivity:banner" %}''') |
|
147 |
assert t.render(Context()) == '/media/assets/test.png' |
|
148 | ||
149 |
page.picture = File(StringIO('test'), 'test2.png') |
|
150 |
page.save() |
|
151 |
assert t.render(Context({'page': page})) == '/media/page-pictures/test2.png' |
|
124 |
- |