diff --git a/combo/apps/gallery/__init__.py b/combo/apps/gallery/__init__.py
new file mode 100644
index 00000000..8c8ef1eb
--- /dev/null
+++ b/combo/apps/gallery/__init__.py
@@ -0,0 +1,26 @@
+# combo - content management system
+# Copyright (C) 2015 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 .
+
+import django.apps
+
+class AppConfig(django.apps.AppConfig):
+ name = 'combo.apps.gallery'
+
+ def get_after_manager_urls(self):
+ from . import urls
+ return urls.gallery_manager_urls
+
+default_app_config = 'combo.apps.gallery.AppConfig'
diff --git a/combo/apps/gallery/forms.py b/combo/apps/gallery/forms.py
new file mode 100644
index 00000000..c4a1420b
--- /dev/null
+++ b/combo/apps/gallery/forms.py
@@ -0,0 +1,31 @@
+# combo - content management system
+# Copyright (C) 2015 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 .
+
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+
+from .models import Image
+
+class ImageAddForm(forms.ModelForm):
+ class Meta:
+ model = Image
+ fields = ('image', 'title',)
+
+
+class ImageEditForm(forms.ModelForm):
+ class Meta:
+ model = Image
+ fields = ('title',)
diff --git a/combo/apps/gallery/migrations/0001_initial.py b/combo/apps/gallery/migrations/0001_initial.py
new file mode 100644
index 00000000..cd460bc2
--- /dev/null
+++ b/combo/apps/gallery/migrations/0001_initial.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('auth', '0001_initial'),
+ ('data', '0005_auto_20150226_0903'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='GalleryCell',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('placeholder', models.CharField(max_length=20)),
+ ('order', models.PositiveIntegerField()),
+ ('extra_css_class', models.CharField(max_length=100, verbose_name='Extra classes for CSS styling', blank=True)),
+ ('slug', models.SlugField(verbose_name='Slug', blank=True)),
+ ('public', models.BooleanField(default=True, verbose_name='Public')),
+ ('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
+ ('last_update_timestamp', models.DateTimeField(auto_now=True)),
+ ('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
+ ('page', models.ForeignKey(to='data.Page')),
+ ],
+ options={
+ 'verbose_name': 'Gallery',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Image',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('image', models.ImageField(upload_to=b'uploads/gallery/%Y/%m/', verbose_name='Image')),
+ ('order', models.PositiveIntegerField()),
+ ('gallery', models.ForeignKey(verbose_name='Gallery', to='gallery.GalleryCell')),
+ ],
+ options={
+ 'ordering': ['order'],
+ },
+ bases=(models.Model,),
+ ),
+ ]
diff --git a/combo/apps/gallery/migrations/0002_image_title.py b/combo/apps/gallery/migrations/0002_image_title.py
new file mode 100644
index 00000000..32bd732e
--- /dev/null
+++ b/combo/apps/gallery/migrations/0002_image_title.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('gallery', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='image',
+ name='title',
+ field=models.CharField(max_length=50, verbose_name='Title', blank=True),
+ preserve_default=True,
+ ),
+ ]
diff --git a/combo/apps/gallery/migrations/0003_gallerycell_title.py b/combo/apps/gallery/migrations/0003_gallerycell_title.py
new file mode 100644
index 00000000..57cb09c3
--- /dev/null
+++ b/combo/apps/gallery/migrations/0003_gallerycell_title.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('gallery', '0002_image_title'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='gallerycell',
+ name='title',
+ field=models.CharField(max_length=50, null=True, verbose_name='Title', blank=True),
+ preserve_default=True,
+ ),
+ ]
diff --git a/combo/apps/gallery/migrations/__init__.py b/combo/apps/gallery/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/combo/apps/gallery/models.py b/combo/apps/gallery/models.py
new file mode 100644
index 00000000..c6b1a89d
--- /dev/null
+++ b/combo/apps/gallery/models.py
@@ -0,0 +1,66 @@
+# combo - content management system
+# Copyright (C) 2015 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 .
+
+import json
+
+from django import template
+from django.db import models
+from django.core import serializers
+from django.forms import models as model_forms
+from django.utils.translation import ugettext_lazy as _
+
+from combo.data.models import CellBase
+from combo.data.library import register_cell_class
+
+@register_cell_class
+class GalleryCell(CellBase):
+ title = models.CharField(_('Title'), max_length=50, blank=True, null=True)
+ template_name = 'combo/gallerycell.html'
+ manager_form_template = 'combo/gallery_manager.html'
+
+ class Meta:
+ verbose_name = _('Gallery')
+
+ def get_additional_label(self):
+ if self.title:
+ return self.title
+ return ''
+
+ def export_subobjects(self):
+ return {'images': [x.get_as_serialized_object() for x in self.image_set.all()]}
+
+ def import_subobjects(self, cell_json):
+ for image in cell_json['images']:
+ image['fields']['gallery_id'] = self.id
+ for image in serializers.deserialize('json', json.dumps(cell_json['images'])):
+ image.save()
+
+
+class Image(models.Model):
+ gallery = models.ForeignKey(GalleryCell, verbose_name=_('Gallery'))
+ image = models.ImageField(_('Image'),
+ upload_to='uploads/gallery/%Y/%m/')
+ order = models.PositiveIntegerField()
+ title = models.CharField(_('Title'), max_length=50, blank=True)
+
+ class Meta:
+ ordering = ['order']
+
+ def get_as_serialized_object(self):
+ serialized_image = json.loads(serializers.serialize('json', [self], use_natural_foreign_keys=True, use_natural_primary_keys=True))[0]
+ del serialized_image['fields']['gallery']
+ del serialized_image['pk']
+ return serialized_image
diff --git a/combo/apps/gallery/static/js/combo.gallery.js b/combo/apps/gallery/static/js/combo.gallery.js
new file mode 100644
index 00000000..2c9609c0
--- /dev/null
+++ b/combo/apps/gallery/static/js/combo.gallery.js
@@ -0,0 +1,29 @@
+function gallery(element) {
+ var element_id = '#' + $(element).attr('id');
+ $(element).sortable({
+ items: '> li[data-object-id]',
+ containment: 'parent',
+ placeholder: 'empty-image',
+ update: function(event, ui) {
+ var new_order = $(element).find('> li').map(function() { return $(this).data('object-id'); }).get().join();
+ $.ajax({
+ url: $(element).data('order-url'),
+ data: {'new-order': new_order},
+ success: function(data, status) {
+ $(element).replaceWith($(data).find(element_id));
+ gallery($(element_id));
+ }
+ });
+ }
+ });
+ $('.image-delete').on('click', function() {
+ $.ajax({
+ url: $(this).attr('href'),
+ success: function(data, status) {
+ $(element).replaceWith($(data).find(element_id));
+ gallery($(element_id));
+ }
+ });
+ return false;
+ });
+};
diff --git a/combo/apps/gallery/templates/combo/gallery_image_form.html b/combo/apps/gallery/templates/combo/gallery_image_form.html
new file mode 100644
index 00000000..e38d1016
--- /dev/null
+++ b/combo/apps/gallery/templates/combo/gallery_image_form.html
@@ -0,0 +1,26 @@
+{% extends "combo/manager_base.html" %}
+{% load i18n %}
+
+{% block appbar %}
+{% if object.id %}
+
{% trans "Edit Image" %}
+{% else %}
+{% trans "New Image" %}
+{% endif %}
+{% endblock %}
+
+{% block content %}
+
+
+{% endblock %}
diff --git a/combo/apps/gallery/templates/combo/gallery_manager.html b/combo/apps/gallery/templates/combo/gallery_manager.html
new file mode 100644
index 00000000..9cb7d000
--- /dev/null
+++ b/combo/apps/gallery/templates/combo/gallery_manager.html
@@ -0,0 +1,23 @@
+{% extends 'combo/cell_form.html' %}
+{% load static thumbnail i18n %}
+
+{% block cell-form %}
+
+{% for image in cell.image_set.all %}
+-
+{% thumbnail image.image "120x120" crop="50% 25%" as im %}
+
+{% endthumbnail %}
+
+
+
+
+
+{% endfor %}
+- {% trans 'Add an Image' %}
+
+
+
+
+
+{% endblock %}
diff --git a/combo/apps/gallery/templates/combo/gallerycell.html b/combo/apps/gallery/templates/combo/gallerycell.html
new file mode 100644
index 00000000..b98755b4
--- /dev/null
+++ b/combo/apps/gallery/templates/combo/gallerycell.html
@@ -0,0 +1,34 @@
+{% load thumbnail %}
+
+{% block cell-content %}
+
+{% for image in cell.image_set.all %}
+ {% if forloop.first %}
+{% thumbnail image.image "640x480" crop="50% 25%" as im %}
+
+
+
+
+{% endthumbnail %}
+ {% endif %}
+{% thumbnail image.image "60x60" crop="50% 25%" as im %}
+{% thumbnail image.image "640x480" crop="50% 25%" as im_large %}
+
+{% endthumbnail %}
+{% endthumbnail %}
+{% endfor %}
+
+
+
+{% endblock %}
diff --git a/combo/apps/gallery/urls.py b/combo/apps/gallery/urls.py
new file mode 100644
index 00000000..7e91b9bb
--- /dev/null
+++ b/combo/apps/gallery/urls.py
@@ -0,0 +1,30 @@
+# combo - content management system
+# Copyright (C) 2015 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 .
+
+from django.conf.urls import url
+
+from . import views
+
+gallery_manager_urls = [
+ url('^gallery/(?P\w+)/images/add/$', views.image_add,
+ name='combo-gallery-image-add'),
+ url('^gallery/(?P\w+)/order$', views.image_order,
+ name='combo-gallery-image-order'),
+ url('^gallery/(?P\w+)/images/(?P\w+)/edit$', views.image_edit,
+ name='combo-gallery-image-edit'),
+ url('^gallery/(?P\w+)/images/(?P\w+)/delete$', views.image_delete,
+ name='combo-gallery-image-delete'),
+]
diff --git a/combo/apps/gallery/views.py b/combo/apps/gallery/views.py
new file mode 100644
index 00000000..76ca3517
--- /dev/null
+++ b/combo/apps/gallery/views.py
@@ -0,0 +1,68 @@
+# combo - content management system
+# Copyright (C) 2015 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 .
+
+from django.core.urlresolvers import reverse, reverse_lazy
+from django.shortcuts import redirect
+from django.views.generic import (TemplateView, RedirectView, DetailView,
+ CreateView, UpdateView, ListView, DeleteView, FormView)
+
+from .models import Image, GalleryCell
+from .forms import ImageAddForm, ImageEditForm
+
+class ImageAddView(CreateView):
+ model = Image
+ template_name = 'combo/gallery_image_form.html'
+ form_class = ImageAddForm
+
+ def form_valid(self, form):
+ form.instance.gallery_id = self.kwargs.get('gallery_pk')
+ other_images = form.instance.gallery.image_set.all()
+ if other_images:
+ form.instance.order = max([x.order for x in other_images]) + 1
+ else:
+ form.instance.order = 0
+ return super(ImageAddView, self).form_valid(form)
+
+ def get_success_url(self):
+ return reverse('combo-manager-page-view', kwargs={'pk': self.object.gallery.page.id})
+
+image_add = ImageAddView.as_view()
+
+
+class ImageEditView(UpdateView):
+ model = Image
+ template_name = 'combo/gallery_image_form.html'
+ form_class = ImageEditForm
+
+ def get_success_url(self):
+ return reverse('combo-manager-page-view', kwargs={'pk': self.object.gallery.page.id})
+
+image_edit = ImageEditView.as_view()
+
+
+def image_delete(request, gallery_pk, pk):
+ gallery = GalleryCell.objects.get(id=gallery_pk)
+ Image.objects.get(id=pk).delete()
+ return redirect(reverse('combo-manager-page-view', kwargs={'pk': gallery.page.id}))
+
+
+def image_order(request, gallery_pk):
+ gallery = GalleryCell.objects.get(id=gallery_pk)
+ new_order = [int(x) for x in request.GET['new-order'].split(',')]
+ for image in gallery.image_set.all():
+ image.order = new_order.index(image.id)+1
+ image.save()
+ return redirect(reverse('combo-manager-page-view', kwargs={'pk': gallery.page.id}))
diff --git a/combo/data/models.py b/combo/data/models.py
index f0692cd9..7f86c387 100644
--- a/combo/data/models.py
+++ b/combo/data/models.py
@@ -359,6 +359,8 @@ class Page(models.Model):
del serialized_page['fields']['related_cells']
serialized_page['cells'] = json.loads(serializers.serialize('json',
cells, use_natural_foreign_keys=True, use_natural_primary_keys=True))
+ for index, cell in enumerate(cells):
+ serialized_page['cells'][index].update(cell.export_subobjects())
serialized_page['fields']['groups'] = [x[0] for x in serialized_page['fields']['groups']]
for cell in serialized_page['cells']:
del cell['pk']
@@ -385,6 +387,7 @@ class Page(models.Model):
else:
cell['fields']['page'] = page.object.natural_key()
+
# if there were cells, remove them
for cell in CellBase.get_cells(page_id=page.object.id):
cell.delete()
@@ -393,10 +396,11 @@ class Page(models.Model):
@classmethod
def load_serialized_cells(cls, cells):
# load new cells
- for cell in serializers.deserialize('json', json.dumps(cells)):
+ for index, cell in enumerate(serializers.deserialize('json', json.dumps(cells))):
cell.save()
# will populate cached_* attributes
cell.object.save()
+ cell.object.import_subobjects(cells[index])
@classmethod
def load_serialized_pages(cls, json_site):
@@ -718,6 +722,11 @@ class CellBase(six.with_metaclass(CellMeta, models.Model)):
def get_external_links_data(self):
return []
+ def export_subobjects(self):
+ return {}
+
+ def import_subobjects(self, cell_json):
+ pass
@register_cell_class
class TextCell(CellBase):
diff --git a/combo/manager/static/css/combo.manager.css b/combo/manager/static/css/combo.manager.css
index 5ac04df9..489b7a36 100644
--- a/combo/manager/static/css/combo.manager.css
+++ b/combo/manager/static/css/combo.manager.css
@@ -112,6 +112,7 @@ div.cell-list button.save {
div.cell div.buttons {
+ clear: both;
margin-top: 2em;
margin-bottom: 1ex;
}
@@ -188,6 +189,9 @@ p#redirection {
float: right;
}
+.icon-eye-open:before { content: "\f06e "; }
+.icon-edit:before { content: "\f044"; }
+
#assets-browser {
display: flex;
}
@@ -380,3 +384,51 @@ span.error {
#id_sub_slug + span.helptext {
max-width: 35rem;
}
+
+div.gallerycell div.buttons button {
+ display: none;
+}
+
+ul.gallery {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+ul.gallery li {
+ float: left;
+ margin: 0 0.5rem 0.5rem 0;
+ position: relative;
+}
+
+ul.gallery li img {
+ border: 1px solid #aaa;
+ background: white;
+ padding: 2px;
+}
+
+ul.gallery li.empty-image {
+ width: 122px;
+}
+
+ul.gallery li span.image-actions {
+ position: absolute;
+ bottom: 5px;
+ right: 5px;
+ font-size: 150%;
+}
+
+ul.gallery li:last-child {
+ height: 124px;
+ width: 124px;
+ margin-bottom: 1rem;
+}
+
+ul.gallery li:last-child a {
+ display: block;
+ line-height: 120px;
+ padding: 0;
+ height: 100%;
+ width: 100%;
+ text-align: center;
+}
diff --git a/combo/settings.py b/combo/settings.py
index efd582e6..a9547ea6 100644
--- a/combo/settings.py
+++ b/combo/settings.py
@@ -77,6 +77,7 @@ INSTALLED_APPS = (
'combo.apps.maps',
'combo.apps.calendar',
'combo.apps.pwa',
+ 'combo.apps.gallery',
'haystack',
'xstatic.pkg.josefinsans',
'xstatic.pkg.leaflet',
diff --git a/tests/test_gallery_cell.py b/tests/test_gallery_cell.py
new file mode 100644
index 00000000..463faf15
--- /dev/null
+++ b/tests/test_gallery_cell.py
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+import os
+import pytest
+
+from django.core.urlresolvers import reverse
+from combo.data.models import Page
+from combo.apps.gallery.models import GalleryCell, Image
+
+from .test_manager import login
+
+TESTS_DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
+
+pytestmark = pytest.mark.django_db
+
+
+def test_adding_gallery_images(app, admin_user):
+ page = Page(title='Pictures', slug='test_gallery_cell', template_name='standard')
+ page.save()
+ cell = GalleryCell(page=page, placeholder='content', order=0)
+ cell.save()
+
+ app.post(reverse('combo-gallery-image-add', kwargs={'gallery_pk': cell.id}), params={'image': ['foo'], 'title': 'white'}, status=403)
+
+ app = login(app)
+ mgr = app.get(reverse('combo-manager-page-edit-cell', kwargs={'page_pk': page.id, 'cell_reference': cell.get_reference()}), status=200)
+ assert '