0001-pwa-add-management-of-navigation-entries-29362.patch
combo/apps/pwa/manager_views.py | ||
---|---|---|
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 | 17 |
from django.core.urlresolvers import reverse_lazy |
18 |
from django.views.generic import UpdateView |
|
18 |
from django.db.models import Max |
|
19 |
from django import forms |
|
20 |
from django.http import JsonResponse |
|
21 |
from django.utils.translation import ugettext_lazy as _ |
|
22 |
from django.views.generic import CreateView, UpdateView, DeleteView |
|
19 | 23 | |
20 |
from .models import PwaSettings |
|
24 |
from combo.data.forms import get_page_choices |
|
25 | ||
26 |
from .models import PwaSettings, PwaNavigationEntry |
|
21 | 27 | |
22 | 28 | |
23 | 29 |
class ManagerHomeView(UpdateView): |
... | ... | |
28 | 34 | |
29 | 35 |
def get_object(self): |
30 | 36 |
return PwaSettings.singleton() |
37 | ||
38 |
def get_context_data(self, **kwargs): |
|
39 |
context = super(ManagerHomeView, self).get_context_data(**kwargs) |
|
40 |
context['navigation_entries'] = PwaNavigationEntry.objects.all() |
|
41 |
return context |
|
42 | ||
43 | ||
44 |
class ManagerNavigationEntryMixin(object): |
|
45 |
model = PwaNavigationEntry |
|
46 |
fields = ['label', 'url', 'link_page', 'icon', 'extra_css_class', |
|
47 |
'notification_count', 'use_user_name_as_label'] |
|
48 |
template_name = 'combo/pwa/manager_form.html' |
|
49 |
success_url = reverse_lazy('pwa-manager-homepage') |
|
50 | ||
51 |
def get_form_class(self): |
|
52 |
form_class = forms.models.modelform_factory(self.model, |
|
53 |
fields=self.fields) |
|
54 |
form_class.base_fields['link_page'].choices = [(None, '-----')] + get_page_choices() |
|
55 |
return form_class |
|
56 | ||
57 |
def form_valid(self, form): |
|
58 |
if form.instance.order is None: |
|
59 |
max_order = self.model.objects.all().aggregate(Max('order')) |
|
60 |
form.instance.order = (max_order['order__max'] or 0) + 1 |
|
61 |
if form.instance.link_page_id is None: |
|
62 |
if not form.instance.label: |
|
63 |
form.add_error('label', _('A label is required when no page is selected.')) |
|
64 |
if not form.instance.url: |
|
65 |
form.add_error('url', _('An URL is required when no page is selected.')) |
|
66 |
elif form.instance.url: |
|
67 |
form.add_error('url', _('An URL cannot be specified when a page is selected.')) |
|
68 |
if form.errors: |
|
69 |
return super(ManagerNavigationEntryMixin, self).form_invalid(form) |
|
70 |
return super(ManagerNavigationEntryMixin, self).form_valid(form) |
|
71 | ||
72 | ||
73 |
class ManagerAddNavigationEntry(ManagerNavigationEntryMixin, CreateView): |
|
74 |
pass |
|
75 | ||
76 | ||
77 |
class ManagerEditNavigationEntry(ManagerNavigationEntryMixin, UpdateView): |
|
78 |
pass |
|
79 | ||
80 | ||
81 |
class ManagerDeleteNavigationEntry(DeleteView): |
|
82 |
model = PwaNavigationEntry |
|
83 |
success_url = reverse_lazy('pwa-manager-homepage') |
|
84 |
template_name = 'combo/generic_confirm_delete.html' |
|
85 | ||
86 | ||
87 |
def manager_navigation_order(request, *args, **kwargs): |
|
88 |
new_order = {int(x): i for i, x in enumerate(request.GET['new-order'].split(','))} |
|
89 |
for entry in PwaNavigationEntry.objects.all(): |
|
90 |
entry.order = new_order[entry.id] |
|
91 |
entry.save() |
|
92 |
return JsonResponse({'err': 0}) |
combo/apps/pwa/migrations/0003_pwanavigationentry.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# Generated by Django 1.11.12 on 2018-12-27 14:27 |
|
3 |
from __future__ import unicode_literals |
|
4 | ||
5 |
from django.db import migrations, models |
|
6 |
import django.db.models.deletion |
|
7 | ||
8 | ||
9 |
class Migration(migrations.Migration): |
|
10 | ||
11 |
dependencies = [ |
|
12 |
('data', '0036_page_sub_slug'), |
|
13 |
('pwa', '0002_pwasettings'), |
|
14 |
] |
|
15 | ||
16 |
operations = [ |
|
17 |
migrations.CreateModel( |
|
18 |
name='PwaNavigationEntry', |
|
19 |
fields=[ |
|
20 |
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
|
21 |
('label', models.CharField(blank=True, max_length=150, verbose_name='Label')), |
|
22 |
('url', models.CharField(blank=True, max_length=200, verbose_name='URL')), |
|
23 |
('icon', models.FileField(blank=True, null=True, upload_to=b'pwa', verbose_name='Icon')), |
|
24 |
('extra_css_class', models.CharField(blank=True, max_length=100, verbose_name='Extra classes for CSS styling')), |
|
25 |
('order', models.PositiveIntegerField()), |
|
26 |
('notification_count', models.BooleanField(default=False, verbose_name='Display notification count')), |
|
27 |
('use_user_name_as_label', models.BooleanField(default=False, verbose_name='Use user name as label')), |
|
28 |
('link_page', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='data.Page', verbose_name='Internal link')), |
|
29 |
], |
|
30 |
options={ |
|
31 |
'ordering': ('order',), |
|
32 |
}, |
|
33 |
), |
|
34 |
] |
combo/apps/pwa/models.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU Affero General Public License |
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 |
import base64 |
|
17 | 18 |
import json |
18 | 19 | |
19 | 20 |
from django.conf import settings |
20 | 21 |
from django.core import serializers |
22 |
from django.core.files.storage import default_storage |
|
21 | 23 |
from django.db import models |
24 |
from django.utils import six |
|
25 |
from django.utils.encoding import force_text, force_bytes |
|
26 |
from django.utils.six import BytesIO |
|
22 | 27 |
from django.utils.translation import ugettext_lazy as _ |
23 | 28 | |
24 | 29 |
from jsonfield import JSONField |
25 | 30 |
from combo.data.fields import RichTextField |
31 |
from combo import utils |
|
26 | 32 | |
27 | 33 | |
28 | 34 |
class PwaSettings(models.Model): |
... | ... | |
56 | 62 |
obj.save() |
57 | 63 | |
58 | 64 | |
65 |
class PwaNavigationEntry(models.Model): |
|
66 |
label = models.CharField(verbose_name=_('Label'), max_length=150, blank=True) |
|
67 |
url = models.CharField(verbose_name=_('External URL'), max_length=200, blank=True) |
|
68 |
link_page = models.ForeignKey('data.Page', blank=True, |
|
69 |
null=True, verbose_name=_('Internal link')) |
|
70 |
icon = models.FileField(_('Icon'), upload_to='pwa', blank=True, null=True) |
|
71 |
extra_css_class = models.CharField(_('Extra classes for CSS styling'), max_length=100, blank=True) |
|
72 |
order = models.PositiveIntegerField() |
|
73 | ||
74 |
notification_count = models.BooleanField( |
|
75 |
verbose_name=_('Display notification count'), |
|
76 |
default=False) |
|
77 |
use_user_name_as_label = models.BooleanField( |
|
78 |
verbose_name=_('Use user name as label'), |
|
79 |
default=False) |
|
80 | ||
81 |
class Meta: |
|
82 |
ordering = ('order',) |
|
83 | ||
84 |
def get_label(self): |
|
85 |
return self.label or self.link_page.title |
|
86 | ||
87 |
def get_url(self): |
|
88 |
if self.link_page: |
|
89 |
return self.link_page.get_online_url() |
|
90 |
else: |
|
91 |
return utils.get_templated_url(self.url) |
|
92 | ||
93 |
def css_class_names(self): |
|
94 |
css_class_names = self.extra_css_class or '' |
|
95 |
if self.link_page: |
|
96 |
css_class_names += ' page-%s' % self.link_page.slug |
|
97 |
return css_class_names |
|
98 | ||
99 |
@classmethod |
|
100 |
def export_all_for_json(cls): |
|
101 |
return [x.get_as_serialized_object() for x in cls.objects.all()] |
|
102 | ||
103 |
def get_as_serialized_object(self): |
|
104 |
serialized_entry = json.loads(serializers.serialize('json', [self], |
|
105 |
use_natural_foreign_keys=True, use_natural_primary_keys=True))[0] |
|
106 |
if self.icon: |
|
107 |
encode = base64.encodestring if six.PY2 else base64.encodebytes |
|
108 |
serialized_entry['icon:base64'] = force_text(encode(self.icon.read())) |
|
109 |
del serialized_entry['model'] |
|
110 |
del serialized_entry['pk'] |
|
111 |
return serialized_entry |
|
112 | ||
113 |
@classmethod |
|
114 |
def load_serialized_objects(cls, json_site): |
|
115 |
for json_entry in json_site: |
|
116 |
cls.load_serialized_object(json_entry) |
|
117 | ||
118 |
@classmethod |
|
119 |
def load_serialized_object(cls, json_entry): |
|
120 |
json_entry['model'] = 'pwa.pwanavigationentry' |
|
121 |
# deserialize once to get link_page by natural key |
|
122 |
fake_entry = [x for x in serializers.deserialize('json', json.dumps([json_entry]))][0] |
|
123 |
entry, created = cls.objects.get_or_create( |
|
124 |
label=json_entry['fields']['label'], |
|
125 |
url=json_entry['fields']['url'], |
|
126 |
link_page=fake_entry.object.link_page, |
|
127 |
defaults={'order': 0}) |
|
128 |
json_entry['pk'] = entry.id |
|
129 |
entry = [x for x in serializers.deserialize('json', json.dumps([json_entry]))][0] |
|
130 |
entry.save() |
|
131 |
if json_entry.get('icon:base64'): |
|
132 |
decode = base64.decodestring if six.PY2 else base64.decodebytes |
|
133 |
decoded_icon = decode(force_bytes(json_entry['icon:base64'])) |
|
134 |
if not default_storage.exists(entry.object.icon.name) or entry.object.icon.read() != decoded_icon: |
|
135 |
# save new file |
|
136 |
entry.object.icon.save(entry.object.icon.name, BytesIO(decoded_icon)) |
|
137 | ||
138 | ||
59 | 139 |
class PushSubscription(models.Model): |
60 | 140 |
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) |
61 | 141 |
subscription_info = JSONField() |
combo/apps/pwa/static/css/combo.manager.pwa.scss | ||
---|---|---|
82 | 82 |
} |
83 | 83 |
} |
84 | 84 |
} |
85 | ||
86 |
div.section.navigation { |
|
87 |
ul.navigation-entries { |
|
88 |
margin-bottom: 0; |
|
89 |
+ ul { |
|
90 |
margin-top: 0; |
|
91 |
} |
|
92 |
span.handle { |
|
93 |
padding: 0; |
|
94 |
position: absolute; |
|
95 |
width: 2em; |
|
96 |
+ a { |
|
97 |
padding-left: 4ex; |
|
98 |
} |
|
99 |
} |
|
100 |
} |
|
101 |
li a.add { |
|
102 |
padding-left: 0; |
|
103 |
&::before { |
|
104 |
content: "\f055"; // circle-plus |
|
105 |
font-family: FontAwesome; |
|
106 |
width: 2.2em; |
|
107 |
display: inline-block; |
|
108 |
text-align: center; |
|
109 |
} |
|
110 |
} |
|
111 |
} |
combo/apps/pwa/templates/combo/pwa/manager_form.html | ||
---|---|---|
1 |
{% extends "combo/pwa/manager_base.html" %} |
|
2 |
{% load i18n %} |
|
3 | ||
4 |
{% block appbar %} |
|
5 |
<h2></h2> |
|
6 |
{% endblock %} |
|
7 | ||
8 |
{% block content %} |
|
9 |
<form method="post" enctype="multipart/form-data"> |
|
10 |
{% csrf_token %} |
|
11 |
{{ form.as_p }} |
|
12 |
<div class="buttons"> |
|
13 |
<button class="submit-button">{% trans "Save" %}</button> |
|
14 |
<a class="cancel" href="{% url 'pwa-manager-homepage' %}">{% trans 'Cancel' %}</a> |
|
15 |
</div> |
|
16 |
</form> |
|
17 |
{% endblock %} |
combo/apps/pwa/templates/combo/pwa/manager_home.html | ||
---|---|---|
20 | 20 | |
21 | 21 |
<div class="sections"> |
22 | 22 | |
23 |
<div class="section navigation"> |
|
24 |
<h3>{% trans "Navigation" %}</h3> |
|
25 |
<div> |
|
26 | ||
27 |
{% if navigation_entries|length %} |
|
28 |
<p class="hint"> |
|
29 |
{% blocktrans %} |
|
30 |
Use drag and drop with the ⣿ handles to reorder navigation entries. |
|
31 |
{% endblocktrans %} |
|
32 |
</p> |
|
33 |
{% endif %} |
|
34 | ||
35 |
<ul class="objects-list single-links navigation-entries" |
|
36 |
data-order-url="{% url 'pwa-manager-navigation-order' %}"> |
|
37 |
{% for entry in navigation_entries %} |
|
38 |
<li data-pk="{{entry.pk}}"><span class="handle">⣿</span> |
|
39 |
<a rel="popup" href="{% url 'pwa-manager-navigation-edit' pk=entry.pk %}">{{ entry.get_label }}</a> |
|
40 |
<a rel="popup" class="delete" href="{% url 'pwa-manager-navigation-delete' pk=entry.pk %}">{% trans "remove" %}</a> |
|
41 |
</li> |
|
42 |
{% endfor %} |
|
43 |
</ul> |
|
44 |
{% if navigation_entries|length < 5 %} |
|
45 |
<ul class="objects-list single-links"> |
|
46 |
<li><a class="add" rel="popup" href="{% url 'pwa-manager-navigation-add' %}">{% trans 'Add a navigation entry' %}</a></li> |
|
47 |
</ul> |
|
48 |
{% endif %} |
|
49 | ||
50 |
</div> |
|
51 |
</div> |
|
52 | ||
23 | 53 |
<div class="section settings"> |
24 | 54 |
<h3>{% trans "Settings" %}</h3> |
25 | 55 |
<div> |
... | ... | |
48 | 78 |
$('.mobile-app-content iframe').attr('src', '/'); |
49 | 79 |
$('.mobile-app-content').addClass('splash-off'); |
50 | 80 |
}); |
81 | ||
82 |
$('.navigation-entries').sortable({ |
|
83 |
handle: '.handle', |
|
84 |
update: function(event, ui) { |
|
85 |
var new_order = $('.navigation-entries li').map(function() { return $(this).data('pk'); }).get().join(); |
|
86 |
$.ajax({ |
|
87 |
url: $('.navigation-entries').data('order-url'), |
|
88 |
data: {'new-order': new_order} |
|
89 |
}); |
|
90 |
} |
|
91 |
}); |
|
51 | 92 |
}); |
52 | 93 |
</script> |
53 | 94 |
combo/apps/pwa/templates/combo/pwa/navigation.html | ||
---|---|---|
1 |
{% load combo %} |
|
2 |
<div class="pwa-navigation" id="pwa-navigation"> |
|
3 |
<div> |
|
4 |
<ul> |
|
5 |
{% for entry in entries %} |
|
6 |
<li class="{{ entry.css_class_names }}" data-entry-pk="{{ entry.pk }}" |
|
7 |
{% if entry.notification_count %}data-notification-count-url="{{site_base}}/api/notification/count/"{% endif %} |
|
8 |
{% 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 %}> |
|
9 |
<a href="{{ entry.get_url }}" |
|
10 |
{% if entry.icon %}style="background-image: url({{site_base}}{{entry.icon.url}});"{% endif %} |
|
11 |
><span>{{ entry.get_label }}</span></a></li> |
|
12 |
{% endfor %} |
|
13 |
</ul> |
|
14 |
</div> |
|
15 |
</div> |
|
16 |
<script> |
|
17 |
$('li[data-pwa-user-name]').each(function(idx, elem) { |
|
18 |
var user_name = $(this).data('pwa-user-name'); |
|
19 |
if (user_name) { |
|
20 |
$(this).find('span').text(user_name); |
|
21 |
} |
|
22 |
}); |
|
23 |
$('body.authenticated-user li[data-notification-count-url]').each(function(idx, elem) { |
|
24 |
var $entry = $(this); |
|
25 |
$.ajax({ |
|
26 |
url: $entry.data('notification-count-url'), |
|
27 |
xhrFields: { withCredentials: true }, |
|
28 |
async: true, |
|
29 |
dataType: 'json', |
|
30 |
crossDomain: true, |
|
31 |
success: function(data) { |
|
32 |
if (data.new) { |
|
33 |
$entry.find('span').append(' <span class="badge">' + data.new + '</span>'); |
|
34 |
} |
|
35 |
}}); |
|
36 |
}); |
|
37 |
</script> |
combo/apps/pwa/templatetags/pwa.py | ||
---|---|---|
1 |
# combo - content management system |
|
2 |
# Copyright (C) 2015-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.conf import settings |
|
19 | ||
20 |
from combo.apps.pwa.models import PwaNavigationEntry |
|
21 | ||
22 | ||
23 |
register = template.Library() |
|
24 | ||
25 |
@register.simple_tag(takes_context=True) |
|
26 |
def pwa_navigation(context): |
|
27 |
if settings.TEMPLATE_VARS.get('pwa_display') not in ('standalone', 'fullscreen'): |
|
28 |
return '' |
|
29 |
pwa_navigation_template = template.loader.get_template('combo/pwa/navigation.html') |
|
30 |
context = { |
|
31 |
'entries': PwaNavigationEntry.objects.all(), |
|
32 |
'user': context.get('user'), |
|
33 |
'render_skeleton': context.get('render_skeleton'), |
|
34 |
'site_base': context['request'].build_absolute_uri('/')[:-1], |
|
35 |
} |
|
36 |
return pwa_navigation_template.render(context) |
combo/apps/pwa/urls.py | ||
---|---|---|
20 | 20 | |
21 | 21 |
from .manager_views import ( |
22 | 22 |
ManagerHomeView, |
23 |
ManagerAddNavigationEntry, |
|
24 |
ManagerEditNavigationEntry, |
|
25 |
ManagerDeleteNavigationEntry, |
|
26 |
manager_navigation_order, |
|
23 | 27 |
) |
24 | 28 |
from .views import ( |
25 | 29 |
manifest_json, |
... | ... | |
32 | 36 | |
33 | 37 |
pwa_manager_urls = [ |
34 | 38 |
url('^$', ManagerHomeView.as_view(), name='pwa-manager-homepage'), |
39 |
url('^navigation/add/$', |
|
40 |
ManagerAddNavigationEntry.as_view(), |
|
41 |
name='pwa-manager-navigation-add'), |
|
42 |
url('^navigation/edit/(?P<pk>\w+)/$', |
|
43 |
ManagerEditNavigationEntry.as_view(), |
|
44 |
name='pwa-manager-navigation-edit'), |
|
45 |
url('^navigation/delete/(?P<pk>\w+)/$', |
|
46 |
ManagerDeleteNavigationEntry.as_view(), |
|
47 |
name='pwa-manager-navigation-delete'), |
|
48 |
url('^navigation/order/$', |
|
49 |
manager_navigation_order, |
|
50 |
name='pwa-manager-navigation-order'), |
|
35 | 51 |
] |
36 | 52 | |
37 | 53 |
urlpatterns = [ |
combo/data/utils.py | ||
---|---|---|
22 | 22 | |
23 | 23 |
from combo.apps.assets.models import Asset |
24 | 24 |
from combo.apps.maps.models import MapLayer |
25 |
from combo.apps.pwa.models import PwaSettings |
|
25 |
from combo.apps.pwa.models import PwaSettings, PwaNavigationEntry
|
|
26 | 26 |
from .models import Page |
27 | 27 | |
28 | 28 | |
... | ... | |
42 | 42 |
'assets': Asset.export_all_for_json(), |
43 | 43 |
'pwa': { |
44 | 44 |
'settings': PwaSettings.export_for_json(), |
45 |
'navigation': PwaNavigationEntry.export_all_for_json(), |
|
45 | 46 |
} |
46 | 47 |
} |
47 | 48 | |
... | ... | |
74 | 75 |
Asset.objects.all().delete() |
75 | 76 |
Page.objects.all().delete() |
76 | 77 |
PwaSettings.objects.all().delete() |
78 |
PwaNavigationEntry.objects.all().delete() |
|
77 | 79 | |
78 | 80 |
with transaction.atomic(): |
79 | 81 |
MapLayer.load_serialized_objects(data.get('map-layers') or []) |
... | ... | |
84 | 86 |
with transaction.atomic(): |
85 | 87 |
Page.load_serialized_pages(data.get('pages') or []) |
86 | 88 | |
87 |
with transaction.atomic(): |
|
88 |
PwaSettings.load_serialized_settings((data.get('pwa') or {}).get('settings')) |
|
89 |
if data.get('pwa'): |
|
90 |
with transaction.atomic(): |
|
91 |
PwaSettings.load_serialized_settings(data['pwa'].get('settings')) |
|
92 |
with transaction.atomic(): |
|
93 |
PwaNavigationEntry.load_serialized_objects(data['pwa'].get('navigation')) |
tests/test_import_export.py | ||
---|---|---|
1 |
import base64 |
|
1 | 2 |
import datetime |
2 | 3 |
import json |
3 | 4 |
import os |
... | ... | |
9 | 10 |
from django.contrib.auth.models import Group |
10 | 11 |
from django.core.files import File |
11 | 12 |
from django.core.management import call_command |
12 |
from django.utils.encoding import force_bytes |
|
13 |
from django.utils.encoding import force_bytes, force_text
|
|
13 | 14 |
from django.utils.six import BytesIO, StringIO |
14 | 15 | |
15 | 16 |
from combo.apps.assets.models import Asset |
16 | 17 |
from combo.apps.maps.models import MapLayer, Map |
17 |
from combo.apps.pwa.models import PwaSettings |
|
18 |
from combo.apps.pwa.models import PwaSettings, PwaNavigationEntry
|
|
18 | 19 |
from combo.data.models import Page, TextCell |
19 | 20 |
from combo.data.utils import export_site, import_site, MissingGroups |
20 | 21 | |
... | ... | |
213 | 214 |
import_site(data=json.loads(output)) |
214 | 215 |
assert PwaSettings.singleton().offline_retry_button is False |
215 | 216 |
assert PwaSettings.singleton().offline_text == 'Hello world' |
217 | ||
218 |
def test_import_export_pwa_navigation(app, some_data): |
|
219 |
page = Page.objects.get(slug='one') |
|
220 |
entry1 = PwaNavigationEntry(label='a', url='/', order=0) |
|
221 |
entry2 = PwaNavigationEntry(link_page=page, order=1, icon=File(BytesIO(b'te\30st'), 'test.png')) |
|
222 |
entry1.save() |
|
223 |
entry2.save() |
|
224 |
output = get_output_of_command('export_site') |
|
225 |
import_site(data={}, clean=True) |
|
226 |
assert PwaNavigationEntry.objects.all().count() == 0 |
|
227 | ||
228 |
import_site(data=json.loads(output)) |
|
229 |
assert PwaNavigationEntry.objects.all().count() == 2 |
|
230 |
# check identical file was not touched |
|
231 |
assert os.path.basename(PwaNavigationEntry.objects.get(order=1).icon.file.name) == 'test.png' |
|
232 |
assert PwaNavigationEntry.objects.get(order=1).icon.read() == b'te\30st' |
|
233 | ||
234 |
# check a second import doesn't create additional entries |
|
235 |
import_site(data=json.loads(output)) |
|
236 |
assert PwaNavigationEntry.objects.all().count() == 2 |
|
237 | ||
238 |
# check with a change in icon file content |
|
239 |
data = json.loads(output) |
|
240 |
data['pwa']['navigation'][1]['icon:base64'] = force_text(base64.encodestring(b'TEST')) |
|
241 |
import_site(data=data) |
|
242 |
assert PwaNavigationEntry.objects.all().count() == 2 |
|
243 |
assert PwaNavigationEntry.objects.get(order=1).icon.read() == b'TEST' |
|
244 | ||
245 |
# check with a change in icon file name |
|
246 |
data = json.loads(output) |
|
247 |
data['pwa']['navigation'][1]['fields']['icon'] = 'pwa/test2.png' |
|
248 |
data['pwa']['navigation'][1]['icon:base64'] = force_text(base64.encodestring(b'TEST2')) |
|
249 |
import_site(data=data) |
|
250 |
assert PwaNavigationEntry.objects.all().count() == 2 |
|
251 |
assert os.path.basename(PwaNavigationEntry.objects.get(order=1).icon.file.name) == 'test2.png' |
|
252 |
assert PwaNavigationEntry.objects.get(order=1).icon.read() == b'TEST2' |
tests/test_pwa.py | ||
---|---|---|
9 | 9 |
pywebpush = None |
10 | 10 | |
11 | 11 |
from django.conf import settings |
12 |
from django.core.files import File |
|
12 | 13 |
from django.core.urlresolvers import reverse |
14 |
from django.template import Context, Template |
|
13 | 15 |
from django.test import override_settings |
16 |
from django.test.client import RequestFactory |
|
17 |
from django.utils.six import BytesIO |
|
14 | 18 | |
15 | 19 |
from combo.apps.notifications.models import Notification |
16 |
from combo.apps.pwa.models import PushSubscription, PwaSettings |
|
20 |
from combo.apps.pwa.models import PushSubscription, PwaSettings, PwaNavigationEntry |
|
21 |
from combo.data.models import Page |
|
17 | 22 | |
18 | 23 |
from .test_manager import login |
19 | 24 | |
... | ... | |
78 | 83 |
assert resp.form['offline_text'].value == 'You are offline.' |
79 | 84 |
assert resp.form['offline_retry_button'].checked is False |
80 | 85 | |
86 |
resp = app.get('/manage/pwa/') |
|
87 |
resp = resp.click('Add a navigation entry') |
|
88 |
resp.form['label'] = 'Hello' |
|
89 |
resp.form['url'] = 'https://www.example.net' |
|
90 |
resp = resp.form.submit().follow() |
|
91 |
assert PwaNavigationEntry.objects.all().count() == 1 |
|
92 | ||
93 |
page = Page(title='test', slug='test') |
|
94 |
page.save() |
|
95 | ||
96 |
resp = resp.click('Add a navigation entry') |
|
97 |
resp.form['link_page'] = page.id |
|
98 |
resp = resp.form.submit().follow() |
|
99 |
assert PwaNavigationEntry.objects.all().count() == 2 |
|
100 | ||
101 |
for i in range(3): |
|
102 |
resp = resp.click('Add a navigation entry') |
|
103 |
resp.form['label'] = 'Hello %s' % i |
|
104 |
resp.form['url'] = 'https://www.example.net' |
|
105 |
resp = resp.form.submit().follow() |
|
106 | ||
107 |
# max 5 items |
|
108 |
assert 'Add a navigation entry' not in resp.text |
|
109 | ||
110 |
# reorder items, reverse them all |
|
111 |
entries = PwaNavigationEntry.objects.all() |
|
112 |
app.get('/manage/pwa/navigation/order/?new-order=%s' % |
|
113 |
','.join(reversed([str(x.id) for x in entries]))) |
|
114 |
entries = PwaNavigationEntry.objects.all() |
|
115 |
assert entries[0].label == 'Hello 2' |
|
116 | ||
117 |
# remove first item |
|
118 |
resp = app.get('/manage/pwa/') |
|
119 |
resp = resp.click(href='delete', index=0) |
|
120 |
resp = resp.form.submit().follow() |
|
121 |
assert 'Hello 2' not in resp.text |
|
122 |
assert 'Add a navigation entry' in resp.text |
|
123 | ||
124 |
# rename item |
|
125 |
resp = resp.click('Hello 1') |
|
126 |
resp.form['label'] = 'Hello 12' |
|
127 |
resp = resp.form.submit().follow() |
|
128 |
assert PwaNavigationEntry.objects.all()[0].label == 'Hello 12' |
|
129 | ||
130 |
# check error handling |
|
131 |
resp = resp.click('Hello 12') |
|
132 |
resp.form['label'] = '' |
|
133 |
resp.form['url'] = '' |
|
134 |
resp = resp.form.submit() |
|
135 |
assert 'A label is required' in resp.text |
|
136 |
assert 'An URL is required' in resp.text |
|
137 | ||
138 |
resp.form['url'] = 'foobar' |
|
139 |
resp.form['link_page'] = page.id |
|
140 |
resp = resp.form.submit() |
|
141 |
assert 'An URL cannot be specified' in resp.text |
|
142 | ||
143 | ||
81 | 144 |
def test_pwa_offline_page(app): |
82 | 145 |
PwaSettings.objects.all().delete() |
83 | 146 |
resp = app.get('/__pwa__/offline/') |
... | ... | |
91 | 154 |
resp = app.get('/__pwa__/offline/') |
92 | 155 |
assert 'You are offline.' in resp.text |
93 | 156 |
assert 'Retry' not in resp.text |
157 | ||
158 | ||
159 |
def test_pwa_navigation_templatetag(app): |
|
160 |
page = Page(title='One', slug='one') |
|
161 |
page.save() |
|
162 |
entry1 = PwaNavigationEntry(label='a', url='/', notification_count=True, |
|
163 |
use_user_name_as_label=True, order=0) |
|
164 |
entry2 = PwaNavigationEntry(link_page=page, order=1, icon=File(BytesIO(b'te\30st'), 'test.png')) |
|
165 |
entry1.save() |
|
166 |
entry2.save() |
|
167 |
t = Template('{% load pwa %}{% pwa_navigation %}') |
|
168 |
assert t.render(Context({})) == '' |
|
169 | ||
170 |
with override_settings(TEMPLATE_VARS={'pwa_display': 'standalone'}): |
|
171 |
request = RequestFactory().get('/') |
|
172 |
nav = t.render(Context({'request': request})) |
|
173 |
assert '<span>a</span>' in nav |
|
174 |
assert '<span>One</span>' in nav |
|
175 |
assert nav.count('background-image') == 1 |
|
176 |
assert nav.count('data-notification-count-url=') == 1 |
|
177 |
assert nav.count('data-pwa-user-name=""') == 1 |
|
178 | ||
179 |
nav = t.render(Context({'request': request, 'render_skeleton': True})) |
|
180 |
assert 'data-pwa-user-name="{% block placeholder-user-name %}' in nav |
|
94 |
- |