pwa: add management of navigation entries (#29362)
This commit is contained in:
parent
1ebadd603c
commit
d9367275de
|
@ -15,9 +15,15 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse_lazy
|
from django.core.urlresolvers import reverse_lazy
|
||||||
from django.views.generic import UpdateView
|
from django.db.models import Max
|
||||||
|
from django import forms
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.views.generic import CreateView, UpdateView, DeleteView
|
||||||
|
|
||||||
from .models import PwaSettings
|
from combo.data.forms import get_page_choices
|
||||||
|
|
||||||
|
from .models import PwaSettings, PwaNavigationEntry
|
||||||
|
|
||||||
|
|
||||||
class ManagerHomeView(UpdateView):
|
class ManagerHomeView(UpdateView):
|
||||||
|
@ -28,3 +34,59 @@ class ManagerHomeView(UpdateView):
|
||||||
|
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
return PwaSettings.singleton()
|
return PwaSettings.singleton()
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(ManagerHomeView, self).get_context_data(**kwargs)
|
||||||
|
context['navigation_entries'] = PwaNavigationEntry.objects.all()
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class ManagerNavigationEntryMixin(object):
|
||||||
|
model = PwaNavigationEntry
|
||||||
|
fields = ['label', 'url', 'link_page', 'icon', 'extra_css_class',
|
||||||
|
'notification_count', 'use_user_name_as_label']
|
||||||
|
template_name = 'combo/pwa/manager_form.html'
|
||||||
|
success_url = reverse_lazy('pwa-manager-homepage')
|
||||||
|
|
||||||
|
def get_form_class(self):
|
||||||
|
form_class = forms.models.modelform_factory(self.model,
|
||||||
|
fields=self.fields)
|
||||||
|
form_class.base_fields['link_page'].choices = [(None, '-----')] + get_page_choices()
|
||||||
|
return form_class
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
if form.instance.order is None:
|
||||||
|
max_order = self.model.objects.all().aggregate(Max('order'))
|
||||||
|
form.instance.order = (max_order['order__max'] or 0) + 1
|
||||||
|
if form.instance.link_page_id is None:
|
||||||
|
if not form.instance.label:
|
||||||
|
form.add_error('label', _('A label is required when no page is selected.'))
|
||||||
|
if not form.instance.url:
|
||||||
|
form.add_error('url', _('An URL is required when no page is selected.'))
|
||||||
|
elif form.instance.url:
|
||||||
|
form.add_error('url', _('An URL cannot be specified when a page is selected.'))
|
||||||
|
if form.errors:
|
||||||
|
return super(ManagerNavigationEntryMixin, self).form_invalid(form)
|
||||||
|
return super(ManagerNavigationEntryMixin, self).form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
|
class ManagerAddNavigationEntry(ManagerNavigationEntryMixin, CreateView):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ManagerEditNavigationEntry(ManagerNavigationEntryMixin, UpdateView):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ManagerDeleteNavigationEntry(DeleteView):
|
||||||
|
model = PwaNavigationEntry
|
||||||
|
success_url = reverse_lazy('pwa-manager-homepage')
|
||||||
|
template_name = 'combo/generic_confirm_delete.html'
|
||||||
|
|
||||||
|
|
||||||
|
def manager_navigation_order(request, *args, **kwargs):
|
||||||
|
new_order = {int(x): i for i, x in enumerate(request.GET['new-order'].split(','))}
|
||||||
|
for entry in PwaNavigationEntry.objects.all():
|
||||||
|
entry.order = new_order[entry.id]
|
||||||
|
entry.save()
|
||||||
|
return JsonResponse({'err': 0})
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.12 on 2018-12-27 14:27
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('data', '0036_page_sub_slug'),
|
||||||
|
('pwa', '0002_pwasettings'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PwaNavigationEntry',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('label', models.CharField(blank=True, max_length=150, verbose_name='Label')),
|
||||||
|
('url', models.CharField(blank=True, max_length=200, verbose_name='URL')),
|
||||||
|
('icon', models.FileField(blank=True, null=True, upload_to=b'pwa', verbose_name='Icon')),
|
||||||
|
('extra_css_class', models.CharField(blank=True, max_length=100, verbose_name='Extra classes for CSS styling')),
|
||||||
|
('order', models.PositiveIntegerField()),
|
||||||
|
('notification_count', models.BooleanField(default=False, verbose_name='Display notification count')),
|
||||||
|
('use_user_name_as_label', models.BooleanField(default=False, verbose_name='Use user name as label')),
|
||||||
|
('link_page', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='data.Page', verbose_name='Internal link')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ('order',),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -14,15 +14,21 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import base64
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import serializers
|
from django.core import serializers
|
||||||
|
from django.core.files.storage import default_storage
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils import six
|
||||||
|
from django.utils.encoding import force_text, force_bytes
|
||||||
|
from django.utils.six import BytesIO
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from jsonfield import JSONField
|
from jsonfield import JSONField
|
||||||
from combo.data.fields import RichTextField
|
from combo.data.fields import RichTextField
|
||||||
|
from combo import utils
|
||||||
|
|
||||||
|
|
||||||
class PwaSettings(models.Model):
|
class PwaSettings(models.Model):
|
||||||
|
@ -56,6 +62,80 @@ class PwaSettings(models.Model):
|
||||||
obj.save()
|
obj.save()
|
||||||
|
|
||||||
|
|
||||||
|
class PwaNavigationEntry(models.Model):
|
||||||
|
label = models.CharField(verbose_name=_('Label'), max_length=150, blank=True)
|
||||||
|
url = models.CharField(verbose_name=_('External URL'), max_length=200, blank=True)
|
||||||
|
link_page = models.ForeignKey('data.Page', blank=True,
|
||||||
|
null=True, verbose_name=_('Internal link'))
|
||||||
|
icon = models.FileField(_('Icon'), upload_to='pwa', blank=True, null=True)
|
||||||
|
extra_css_class = models.CharField(_('Extra classes for CSS styling'), max_length=100, blank=True)
|
||||||
|
order = models.PositiveIntegerField()
|
||||||
|
|
||||||
|
notification_count = models.BooleanField(
|
||||||
|
verbose_name=_('Display notification count'),
|
||||||
|
default=False)
|
||||||
|
use_user_name_as_label = models.BooleanField(
|
||||||
|
verbose_name=_('Use user name as label'),
|
||||||
|
default=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('order',)
|
||||||
|
|
||||||
|
def get_label(self):
|
||||||
|
return self.label or self.link_page.title
|
||||||
|
|
||||||
|
def get_url(self):
|
||||||
|
if self.link_page:
|
||||||
|
return self.link_page.get_online_url()
|
||||||
|
else:
|
||||||
|
return utils.get_templated_url(self.url)
|
||||||
|
|
||||||
|
def css_class_names(self):
|
||||||
|
css_class_names = self.extra_css_class or ''
|
||||||
|
if self.link_page:
|
||||||
|
css_class_names += ' page-%s' % self.link_page.slug
|
||||||
|
return css_class_names
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_all_for_json(cls):
|
||||||
|
return [x.get_as_serialized_object() for x in cls.objects.all()]
|
||||||
|
|
||||||
|
def get_as_serialized_object(self):
|
||||||
|
serialized_entry = json.loads(serializers.serialize('json', [self],
|
||||||
|
use_natural_foreign_keys=True, use_natural_primary_keys=True))[0]
|
||||||
|
if self.icon:
|
||||||
|
encode = base64.encodestring if six.PY2 else base64.encodebytes
|
||||||
|
serialized_entry['icon:base64'] = force_text(encode(self.icon.read()))
|
||||||
|
del serialized_entry['model']
|
||||||
|
del serialized_entry['pk']
|
||||||
|
return serialized_entry
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load_serialized_objects(cls, json_site):
|
||||||
|
for json_entry in json_site:
|
||||||
|
cls.load_serialized_object(json_entry)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load_serialized_object(cls, json_entry):
|
||||||
|
json_entry['model'] = 'pwa.pwanavigationentry'
|
||||||
|
# deserialize once to get link_page by natural key
|
||||||
|
fake_entry = [x for x in serializers.deserialize('json', json.dumps([json_entry]))][0]
|
||||||
|
entry, created = cls.objects.get_or_create(
|
||||||
|
label=json_entry['fields']['label'],
|
||||||
|
url=json_entry['fields']['url'],
|
||||||
|
link_page=fake_entry.object.link_page,
|
||||||
|
defaults={'order': 0})
|
||||||
|
json_entry['pk'] = entry.id
|
||||||
|
entry = [x for x in serializers.deserialize('json', json.dumps([json_entry]))][0]
|
||||||
|
entry.save()
|
||||||
|
if json_entry.get('icon:base64'):
|
||||||
|
decode = base64.decodestring if six.PY2 else base64.decodebytes
|
||||||
|
decoded_icon = decode(force_bytes(json_entry['icon:base64']))
|
||||||
|
if not default_storage.exists(entry.object.icon.name) or entry.object.icon.read() != decoded_icon:
|
||||||
|
# save new file
|
||||||
|
entry.object.icon.save(entry.object.icon.name, BytesIO(decoded_icon))
|
||||||
|
|
||||||
|
|
||||||
class PushSubscription(models.Model):
|
class PushSubscription(models.Model):
|
||||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||||
subscription_info = JSONField()
|
subscription_info = JSONField()
|
||||||
|
|
|
@ -82,3 +82,30 @@ div#mobile-case {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.section.navigation {
|
||||||
|
ul.navigation-entries {
|
||||||
|
margin-bottom: 0;
|
||||||
|
+ ul {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
span.handle {
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
width: 2em;
|
||||||
|
+ a {
|
||||||
|
padding-left: 4ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
li a.add {
|
||||||
|
padding-left: 0;
|
||||||
|
&::before {
|
||||||
|
content: "\f055"; // circle-plus
|
||||||
|
font-family: FontAwesome;
|
||||||
|
width: 2.2em;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
{% extends "combo/pwa/manager_base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block appbar %}
|
||||||
|
<h2></h2>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="submit-button">{% trans "Save" %}</button>
|
||||||
|
<a class="cancel" href="{% url 'pwa-manager-homepage' %}">{% trans 'Cancel' %}</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
|
@ -20,6 +20,36 @@
|
||||||
|
|
||||||
<div class="sections">
|
<div class="sections">
|
||||||
|
|
||||||
|
<div class="section navigation">
|
||||||
|
<h3>{% trans "Navigation" %}</h3>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
{% if navigation_entries|length %}
|
||||||
|
<p class="hint">
|
||||||
|
{% blocktrans %}
|
||||||
|
Use drag and drop with the ⣿ handles to reorder navigation entries.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<ul class="objects-list single-links navigation-entries"
|
||||||
|
data-order-url="{% url 'pwa-manager-navigation-order' %}">
|
||||||
|
{% for entry in navigation_entries %}
|
||||||
|
<li data-pk="{{entry.pk}}"><span class="handle">⣿</span>
|
||||||
|
<a rel="popup" href="{% url 'pwa-manager-navigation-edit' pk=entry.pk %}">{{ entry.get_label }}</a>
|
||||||
|
<a rel="popup" class="delete" href="{% url 'pwa-manager-navigation-delete' pk=entry.pk %}">{% trans "remove" %}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% if navigation_entries|length < 5 %}
|
||||||
|
<ul class="objects-list single-links">
|
||||||
|
<li><a class="add" rel="popup" href="{% url 'pwa-manager-navigation-add' %}">{% trans 'Add a navigation entry' %}</a></li>
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="section settings">
|
<div class="section settings">
|
||||||
<h3>{% trans "Settings" %}</h3>
|
<h3>{% trans "Settings" %}</h3>
|
||||||
<div>
|
<div>
|
||||||
|
@ -48,6 +78,17 @@ $(function() {
|
||||||
$('.mobile-app-content iframe').attr('src', '/');
|
$('.mobile-app-content iframe').attr('src', '/');
|
||||||
$('.mobile-app-content').addClass('splash-off');
|
$('.mobile-app-content').addClass('splash-off');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('.navigation-entries').sortable({
|
||||||
|
handle: '.handle',
|
||||||
|
update: function(event, ui) {
|
||||||
|
var new_order = $('.navigation-entries li').map(function() { return $(this).data('pk'); }).get().join();
|
||||||
|
$.ajax({
|
||||||
|
url: $('.navigation-entries').data('order-url'),
|
||||||
|
data: {'new-order': new_order}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
{% load combo %}
|
||||||
|
<div class="pwa-navigation" id="pwa-navigation">
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
{% for entry in entries %}
|
||||||
|
<li class="{{ entry.css_class_names }}" data-entry-pk="{{ entry.pk }}"
|
||||||
|
{% if entry.notification_count %}data-notification-count-url="{{site_base}}/api/notification/count/"{% endif %}
|
||||||
|
{% if entry.use_user_name_as_label %}data-pwa-user-name="{% skeleton_extra_placeholder user-name %}{{user.get_full_name}}{% end_skeleton_extra_placeholder %}"{% endif %}>
|
||||||
|
<a href="{{ entry.get_url }}"
|
||||||
|
{% if entry.icon %}style="background-image: url({{site_base}}{{entry.icon.url}});"{% endif %}
|
||||||
|
><span>{{ entry.get_label }}</span></a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
$('li[data-pwa-user-name]').each(function(idx, elem) {
|
||||||
|
var user_name = $(this).data('pwa-user-name');
|
||||||
|
if (user_name) {
|
||||||
|
$(this).find('span').text(user_name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$('body.authenticated-user li[data-notification-count-url]').each(function(idx, elem) {
|
||||||
|
var $entry = $(this);
|
||||||
|
$.ajax({
|
||||||
|
url: $entry.data('notification-count-url'),
|
||||||
|
xhrFields: { withCredentials: true },
|
||||||
|
async: true,
|
||||||
|
dataType: 'json',
|
||||||
|
crossDomain: true,
|
||||||
|
success: function(data) {
|
||||||
|
if (data.new) {
|
||||||
|
$entry.find('span').append(' <span class="badge">' + data.new + '</span>');
|
||||||
|
}
|
||||||
|
}});
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,36 @@
|
||||||
|
# combo - content management system
|
||||||
|
# Copyright (C) 2015-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.conf import settings
|
||||||
|
|
||||||
|
from combo.apps.pwa.models import PwaNavigationEntry
|
||||||
|
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=True)
|
||||||
|
def pwa_navigation(context):
|
||||||
|
if settings.TEMPLATE_VARS.get('pwa_display') not in ('standalone', 'fullscreen'):
|
||||||
|
return ''
|
||||||
|
pwa_navigation_template = template.loader.get_template('combo/pwa/navigation.html')
|
||||||
|
context = {
|
||||||
|
'entries': PwaNavigationEntry.objects.all(),
|
||||||
|
'user': context.get('user'),
|
||||||
|
'render_skeleton': context.get('render_skeleton'),
|
||||||
|
'site_base': context['request'].build_absolute_uri('/')[:-1],
|
||||||
|
}
|
||||||
|
return pwa_navigation_template.render(context)
|
|
@ -20,6 +20,10 @@ from combo.urls_utils import decorated_includes, manager_required
|
||||||
|
|
||||||
from .manager_views import (
|
from .manager_views import (
|
||||||
ManagerHomeView,
|
ManagerHomeView,
|
||||||
|
ManagerAddNavigationEntry,
|
||||||
|
ManagerEditNavigationEntry,
|
||||||
|
ManagerDeleteNavigationEntry,
|
||||||
|
manager_navigation_order,
|
||||||
)
|
)
|
||||||
from .views import (
|
from .views import (
|
||||||
manifest_json,
|
manifest_json,
|
||||||
|
@ -32,6 +36,18 @@ from .views import (
|
||||||
|
|
||||||
pwa_manager_urls = [
|
pwa_manager_urls = [
|
||||||
url('^$', ManagerHomeView.as_view(), name='pwa-manager-homepage'),
|
url('^$', ManagerHomeView.as_view(), name='pwa-manager-homepage'),
|
||||||
|
url('^navigation/add/$',
|
||||||
|
ManagerAddNavigationEntry.as_view(),
|
||||||
|
name='pwa-manager-navigation-add'),
|
||||||
|
url('^navigation/edit/(?P<pk>\w+)/$',
|
||||||
|
ManagerEditNavigationEntry.as_view(),
|
||||||
|
name='pwa-manager-navigation-edit'),
|
||||||
|
url('^navigation/delete/(?P<pk>\w+)/$',
|
||||||
|
ManagerDeleteNavigationEntry.as_view(),
|
||||||
|
name='pwa-manager-navigation-delete'),
|
||||||
|
url('^navigation/order/$',
|
||||||
|
manager_navigation_order,
|
||||||
|
name='pwa-manager-navigation-order'),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
|
@ -22,7 +22,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from combo.apps.assets.models import Asset
|
from combo.apps.assets.models import Asset
|
||||||
from combo.apps.maps.models import MapLayer
|
from combo.apps.maps.models import MapLayer
|
||||||
from combo.apps.pwa.models import PwaSettings
|
from combo.apps.pwa.models import PwaSettings, PwaNavigationEntry
|
||||||
from .models import Page
|
from .models import Page
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,6 +42,7 @@ def export_site():
|
||||||
'assets': Asset.export_all_for_json(),
|
'assets': Asset.export_all_for_json(),
|
||||||
'pwa': {
|
'pwa': {
|
||||||
'settings': PwaSettings.export_for_json(),
|
'settings': PwaSettings.export_for_json(),
|
||||||
|
'navigation': PwaNavigationEntry.export_all_for_json(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,6 +75,7 @@ def import_site(data, if_empty=False, clean=False):
|
||||||
Asset.objects.all().delete()
|
Asset.objects.all().delete()
|
||||||
Page.objects.all().delete()
|
Page.objects.all().delete()
|
||||||
PwaSettings.objects.all().delete()
|
PwaSettings.objects.all().delete()
|
||||||
|
PwaNavigationEntry.objects.all().delete()
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
MapLayer.load_serialized_objects(data.get('map-layers') or [])
|
MapLayer.load_serialized_objects(data.get('map-layers') or [])
|
||||||
|
@ -84,5 +86,8 @@ def import_site(data, if_empty=False, clean=False):
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
Page.load_serialized_pages(data.get('pages') or [])
|
Page.load_serialized_pages(data.get('pages') or [])
|
||||||
|
|
||||||
with transaction.atomic():
|
if data.get('pwa'):
|
||||||
PwaSettings.load_serialized_settings((data.get('pwa') or {}).get('settings'))
|
with transaction.atomic():
|
||||||
|
PwaSettings.load_serialized_settings(data['pwa'].get('settings'))
|
||||||
|
with transaction.atomic():
|
||||||
|
PwaNavigationEntry.load_serialized_objects(data['pwa'].get('navigation'))
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import base64
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
@ -9,12 +10,12 @@ import pytest
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from django.utils.encoding import force_bytes
|
from django.utils.encoding import force_bytes, force_text
|
||||||
from django.utils.six import BytesIO, StringIO
|
from django.utils.six import BytesIO, StringIO
|
||||||
|
|
||||||
from combo.apps.assets.models import Asset
|
from combo.apps.assets.models import Asset
|
||||||
from combo.apps.maps.models import MapLayer, Map
|
from combo.apps.maps.models import MapLayer, Map
|
||||||
from combo.apps.pwa.models import PwaSettings
|
from combo.apps.pwa.models import PwaSettings, PwaNavigationEntry
|
||||||
from combo.data.models import Page, TextCell
|
from combo.data.models import Page, TextCell
|
||||||
from combo.data.utils import export_site, import_site, MissingGroups
|
from combo.data.utils import export_site, import_site, MissingGroups
|
||||||
|
|
||||||
|
@ -213,3 +214,39 @@ def test_import_export_pwa_settings(app):
|
||||||
import_site(data=json.loads(output))
|
import_site(data=json.loads(output))
|
||||||
assert PwaSettings.singleton().offline_retry_button is False
|
assert PwaSettings.singleton().offline_retry_button is False
|
||||||
assert PwaSettings.singleton().offline_text == 'Hello world'
|
assert PwaSettings.singleton().offline_text == 'Hello world'
|
||||||
|
|
||||||
|
def test_import_export_pwa_navigation(app, some_data):
|
||||||
|
page = Page.objects.get(slug='one')
|
||||||
|
entry1 = PwaNavigationEntry(label='a', url='/', order=0)
|
||||||
|
entry2 = PwaNavigationEntry(link_page=page, order=1, icon=File(BytesIO(b'te\30st'), 'test.png'))
|
||||||
|
entry1.save()
|
||||||
|
entry2.save()
|
||||||
|
output = get_output_of_command('export_site')
|
||||||
|
import_site(data={}, clean=True)
|
||||||
|
assert PwaNavigationEntry.objects.all().count() == 0
|
||||||
|
|
||||||
|
import_site(data=json.loads(output))
|
||||||
|
assert PwaNavigationEntry.objects.all().count() == 2
|
||||||
|
# check identical file was not touched
|
||||||
|
assert os.path.basename(PwaNavigationEntry.objects.get(order=1).icon.file.name) == 'test.png'
|
||||||
|
assert PwaNavigationEntry.objects.get(order=1).icon.read() == b'te\30st'
|
||||||
|
|
||||||
|
# check a second import doesn't create additional entries
|
||||||
|
import_site(data=json.loads(output))
|
||||||
|
assert PwaNavigationEntry.objects.all().count() == 2
|
||||||
|
|
||||||
|
# check with a change in icon file content
|
||||||
|
data = json.loads(output)
|
||||||
|
data['pwa']['navigation'][1]['icon:base64'] = force_text(base64.encodestring(b'TEST'))
|
||||||
|
import_site(data=data)
|
||||||
|
assert PwaNavigationEntry.objects.all().count() == 2
|
||||||
|
assert PwaNavigationEntry.objects.get(order=1).icon.read() == b'TEST'
|
||||||
|
|
||||||
|
# check with a change in icon file name
|
||||||
|
data = json.loads(output)
|
||||||
|
data['pwa']['navigation'][1]['fields']['icon'] = 'pwa/test2.png'
|
||||||
|
data['pwa']['navigation'][1]['icon:base64'] = force_text(base64.encodestring(b'TEST2'))
|
||||||
|
import_site(data=data)
|
||||||
|
assert PwaNavigationEntry.objects.all().count() == 2
|
||||||
|
assert os.path.basename(PwaNavigationEntry.objects.get(order=1).icon.file.name) == 'test2.png'
|
||||||
|
assert PwaNavigationEntry.objects.get(order=1).icon.read() == b'TEST2'
|
||||||
|
|
|
@ -9,11 +9,16 @@ except ImportError:
|
||||||
pywebpush = None
|
pywebpush = None
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.files import File
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.template import Context, Template
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
from django.utils.six import BytesIO
|
||||||
|
|
||||||
from combo.apps.notifications.models import Notification
|
from combo.apps.notifications.models import Notification
|
||||||
from combo.apps.pwa.models import PushSubscription, PwaSettings
|
from combo.apps.pwa.models import PushSubscription, PwaSettings, PwaNavigationEntry
|
||||||
|
from combo.data.models import Page
|
||||||
|
|
||||||
from .test_manager import login
|
from .test_manager import login
|
||||||
|
|
||||||
|
@ -78,6 +83,64 @@ def test_pwa_manager(app, admin_user):
|
||||||
assert resp.form['offline_text'].value == 'You are offline.'
|
assert resp.form['offline_text'].value == 'You are offline.'
|
||||||
assert resp.form['offline_retry_button'].checked is False
|
assert resp.form['offline_retry_button'].checked is False
|
||||||
|
|
||||||
|
resp = app.get('/manage/pwa/')
|
||||||
|
resp = resp.click('Add a navigation entry')
|
||||||
|
resp.form['label'] = 'Hello'
|
||||||
|
resp.form['url'] = 'https://www.example.net'
|
||||||
|
resp = resp.form.submit().follow()
|
||||||
|
assert PwaNavigationEntry.objects.all().count() == 1
|
||||||
|
|
||||||
|
page = Page(title='test', slug='test')
|
||||||
|
page.save()
|
||||||
|
|
||||||
|
resp = resp.click('Add a navigation entry')
|
||||||
|
resp.form['link_page'] = page.id
|
||||||
|
resp = resp.form.submit().follow()
|
||||||
|
assert PwaNavigationEntry.objects.all().count() == 2
|
||||||
|
|
||||||
|
for i in range(3):
|
||||||
|
resp = resp.click('Add a navigation entry')
|
||||||
|
resp.form['label'] = 'Hello %s' % i
|
||||||
|
resp.form['url'] = 'https://www.example.net'
|
||||||
|
resp = resp.form.submit().follow()
|
||||||
|
|
||||||
|
# max 5 items
|
||||||
|
assert 'Add a navigation entry' not in resp.text
|
||||||
|
|
||||||
|
# reorder items, reverse them all
|
||||||
|
entries = PwaNavigationEntry.objects.all()
|
||||||
|
app.get('/manage/pwa/navigation/order/?new-order=%s' %
|
||||||
|
','.join(reversed([str(x.id) for x in entries])))
|
||||||
|
entries = PwaNavigationEntry.objects.all()
|
||||||
|
assert entries[0].label == 'Hello 2'
|
||||||
|
|
||||||
|
# remove first item
|
||||||
|
resp = app.get('/manage/pwa/')
|
||||||
|
resp = resp.click(href='delete', index=0)
|
||||||
|
resp = resp.form.submit().follow()
|
||||||
|
assert 'Hello 2' not in resp.text
|
||||||
|
assert 'Add a navigation entry' in resp.text
|
||||||
|
|
||||||
|
# rename item
|
||||||
|
resp = resp.click('Hello 1')
|
||||||
|
resp.form['label'] = 'Hello 12'
|
||||||
|
resp = resp.form.submit().follow()
|
||||||
|
assert PwaNavigationEntry.objects.all()[0].label == 'Hello 12'
|
||||||
|
|
||||||
|
# check error handling
|
||||||
|
resp = resp.click('Hello 12')
|
||||||
|
resp.form['label'] = ''
|
||||||
|
resp.form['url'] = ''
|
||||||
|
resp = resp.form.submit()
|
||||||
|
assert 'A label is required' in resp.text
|
||||||
|
assert 'An URL is required' in resp.text
|
||||||
|
|
||||||
|
resp.form['url'] = 'foobar'
|
||||||
|
resp.form['link_page'] = page.id
|
||||||
|
resp = resp.form.submit()
|
||||||
|
assert 'An URL cannot be specified' in resp.text
|
||||||
|
|
||||||
|
|
||||||
def test_pwa_offline_page(app):
|
def test_pwa_offline_page(app):
|
||||||
PwaSettings.objects.all().delete()
|
PwaSettings.objects.all().delete()
|
||||||
resp = app.get('/__pwa__/offline/')
|
resp = app.get('/__pwa__/offline/')
|
||||||
|
@ -91,3 +154,27 @@ def test_pwa_offline_page(app):
|
||||||
resp = app.get('/__pwa__/offline/')
|
resp = app.get('/__pwa__/offline/')
|
||||||
assert 'You are offline.' in resp.text
|
assert 'You are offline.' in resp.text
|
||||||
assert 'Retry' not in resp.text
|
assert 'Retry' not in resp.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_pwa_navigation_templatetag(app):
|
||||||
|
page = Page(title='One', slug='one')
|
||||||
|
page.save()
|
||||||
|
entry1 = PwaNavigationEntry(label='a', url='/', notification_count=True,
|
||||||
|
use_user_name_as_label=True, order=0)
|
||||||
|
entry2 = PwaNavigationEntry(link_page=page, order=1, icon=File(BytesIO(b'te\30st'), 'test.png'))
|
||||||
|
entry1.save()
|
||||||
|
entry2.save()
|
||||||
|
t = Template('{% load pwa %}{% pwa_navigation %}')
|
||||||
|
assert t.render(Context({})) == ''
|
||||||
|
|
||||||
|
with override_settings(TEMPLATE_VARS={'pwa_display': 'standalone'}):
|
||||||
|
request = RequestFactory().get('/')
|
||||||
|
nav = t.render(Context({'request': request}))
|
||||||
|
assert '<span>a</span>' in nav
|
||||||
|
assert '<span>One</span>' in nav
|
||||||
|
assert nav.count('background-image') == 1
|
||||||
|
assert nav.count('data-notification-count-url=') == 1
|
||||||
|
assert nav.count('data-pwa-user-name=""') == 1
|
||||||
|
|
||||||
|
nav = t.render(Context({'request': request, 'render_skeleton': True}))
|
||||||
|
assert 'data-pwa-user-name="{% block placeholder-user-name %}' in nav
|
||||||
|
|
Loading…
Reference in New Issue