From 24af3c8a2f9217aaa3f7ca7837218eb1c041f7d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= Date: Mon, 25 May 2015 12:59:41 +0200 Subject: [PATCH] add a gallery cell type (#7344) Signed-off-by: Christophe Siraut --- combo/apps/gallery/__init__.py | 26 +++++++ combo/apps/gallery/forms.py | 31 +++++++++ combo/apps/gallery/migrations/0001_initial.py | 47 +++++++++++++ .../gallery/migrations/0002_image_title.py | 20 ++++++ .../migrations/0003_gallerycell_title.py | 20 ++++++ combo/apps/gallery/migrations/__init__.py | 0 combo/apps/gallery/models.py | 66 ++++++++++++++++++ combo/apps/gallery/static/js/combo.gallery.js | 29 ++++++++ .../templates/combo/gallery_image_form.html | 26 +++++++ .../templates/combo/gallery_manager.html | 23 +++++++ .../gallery/templates/combo/gallerycell.html | 34 ++++++++++ combo/apps/gallery/urls.py | 30 ++++++++ combo/apps/gallery/views.py | 68 +++++++++++++++++++ combo/data/models.py | 11 ++- combo/manager/static/css/combo.manager.css | 52 ++++++++++++++ combo/settings.py | 1 + tests/test_gallery_cell.py | 53 +++++++++++++++ tests/test_import_export.py | 18 +++++ 18 files changed, 554 insertions(+), 1 deletion(-) create mode 100644 combo/apps/gallery/__init__.py create mode 100644 combo/apps/gallery/forms.py create mode 100644 combo/apps/gallery/migrations/0001_initial.py create mode 100644 combo/apps/gallery/migrations/0002_image_title.py create mode 100644 combo/apps/gallery/migrations/0003_gallerycell_title.py create mode 100644 combo/apps/gallery/migrations/__init__.py create mode 100644 combo/apps/gallery/models.py create mode 100644 combo/apps/gallery/static/js/combo.gallery.js create mode 100644 combo/apps/gallery/templates/combo/gallery_image_form.html create mode 100644 combo/apps/gallery/templates/combo/gallery_manager.html create mode 100644 combo/apps/gallery/templates/combo/gallerycell.html create mode 100644 combo/apps/gallery/urls.py create mode 100644 combo/apps/gallery/views.py create mode 100644 tests/test_gallery_cell.py 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 %} + +
+ {% csrf_token %} + {{ form.as_p }} +
+ + {% if object.id %} + {% trans 'Cancel' %} + {% else %} + {% trans 'Cancel' %} + {% endif %} +
+
+{% 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 %} + + + + + +{% 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 %} + + +{% 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 '