2019-01-04 20:56:08 +01:00
|
|
|
|
#
|
2018-11-26 17:14:05 +01:00
|
|
|
|
# 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/>.
|
|
|
|
|
|
2018-12-27 15:26:21 +01:00
|
|
|
|
import base64
|
2018-12-27 09:44:21 +01:00
|
|
|
|
import json
|
2021-05-04 14:15:30 +02:00
|
|
|
|
import os
|
2018-12-27 09:44:21 +01:00
|
|
|
|
|
2018-11-26 17:14:05 +01:00
|
|
|
|
from django.conf import settings
|
2018-12-27 09:44:21 +01:00
|
|
|
|
from django.core import serializers
|
2019-01-03 12:35:14 +01:00
|
|
|
|
from django.core.files.base import ContentFile
|
2018-12-27 15:26:21 +01:00
|
|
|
|
from django.core.files.storage import default_storage
|
2018-11-26 17:14:05 +01:00
|
|
|
|
from django.db import models
|
2023-03-15 17:37:26 +01:00
|
|
|
|
from django.db.models import JSONField
|
2022-08-16 14:23:28 +02:00
|
|
|
|
from django.utils.encoding import force_bytes, force_str
|
2022-08-16 14:14:49 +02:00
|
|
|
|
from django.utils.translation import gettext_lazy as _
|
2019-06-30 09:43:16 +02:00
|
|
|
|
from py_vapid import Vapid
|
|
|
|
|
|
2018-12-27 15:26:21 +01:00
|
|
|
|
from combo import utils
|
2018-12-27 09:44:21 +01:00
|
|
|
|
from combo.data.fields import RichTextField
|
2021-09-15 16:40:25 +02:00
|
|
|
|
from combo.middleware import get_request
|
2018-12-27 09:44:21 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PwaSettings(models.Model):
|
2019-01-04 20:56:08 +01:00
|
|
|
|
APPLICATION_ICON_SIZES = ['%sx%s' % (x, x) for x in (48, 96, 192, 256, 512)]
|
2019-04-16 12:05:12 +02:00
|
|
|
|
application_name = models.CharField(verbose_name=_('Application Name'), max_length=64, blank=True)
|
2019-01-04 20:56:08 +01:00
|
|
|
|
application_icon = models.FileField(
|
|
|
|
|
verbose_name=_('Application Icon'),
|
2020-04-01 10:35:52 +02:00
|
|
|
|
help_text=_(
|
|
|
|
|
'Icon file must be in JPEG or PNG format, and should be a square of at least 512×512 pixels.'
|
|
|
|
|
),
|
2019-01-04 20:56:08 +01:00
|
|
|
|
upload_to='pwa',
|
|
|
|
|
blank=True,
|
|
|
|
|
null=True,
|
|
|
|
|
)
|
2022-04-18 11:06:54 +02:00
|
|
|
|
maskable_icon = models.BooleanField(
|
|
|
|
|
verbose_name=_('Maskable Icon'),
|
|
|
|
|
default=False,
|
|
|
|
|
help_text=_(
|
|
|
|
|
'Maskable icons have their important content in a safe zone; they can be '
|
|
|
|
|
'cropped by devices to be shown in a variety of shapes. A circle marker '
|
|
|
|
|
'will be displayed on top of the preview so you can check is fits.'
|
|
|
|
|
),
|
|
|
|
|
)
|
2018-12-27 09:44:21 +01:00
|
|
|
|
offline_text = RichTextField(
|
|
|
|
|
verbose_name=_('Offline Information Text'),
|
|
|
|
|
default=_('You are currently offline.'),
|
|
|
|
|
config_name='small',
|
|
|
|
|
)
|
|
|
|
|
offline_retry_button = models.BooleanField(_('Include Retry Button'), default=True)
|
2019-06-30 09:43:16 +02:00
|
|
|
|
push_notifications = models.BooleanField(
|
|
|
|
|
verbose_name=_('Enable subscription to push notifications'), default=False
|
|
|
|
|
)
|
2021-04-09 15:17:38 +02:00
|
|
|
|
push_notifications_infos = JSONField(blank=True, default=dict)
|
2018-12-27 09:44:21 +01:00
|
|
|
|
last_update_timestamp = models.DateTimeField(auto_now=True)
|
|
|
|
|
|
2019-06-30 09:43:16 +02:00
|
|
|
|
def save(self, **kwargs):
|
|
|
|
|
if self.push_notifications and not self.push_notifications_infos:
|
|
|
|
|
# generate VAPID keys
|
|
|
|
|
vapid = Vapid()
|
|
|
|
|
vapid.generate_keys()
|
|
|
|
|
self.push_notifications_infos = {
|
2022-08-16 14:23:28 +02:00
|
|
|
|
'private_key': force_str(vapid.private_pem()),
|
2019-06-30 09:43:16 +02:00
|
|
|
|
}
|
|
|
|
|
elif not self.push_notifications:
|
|
|
|
|
self.push_notifications_infos = {}
|
|
|
|
|
return super().save(**kwargs)
|
|
|
|
|
|
2018-12-27 09:44:21 +01:00
|
|
|
|
@classmethod
|
|
|
|
|
def singleton(cls):
|
|
|
|
|
return cls.objects.first() or cls()
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def export_for_json(cls):
|
|
|
|
|
obj = cls.singleton()
|
|
|
|
|
if not obj.id:
|
|
|
|
|
return {}
|
|
|
|
|
serialized_settings = json.loads(serializers.serialize('json', [obj]))
|
2020-07-30 19:06:58 +02:00
|
|
|
|
result = serialized_settings[0].get('fields')
|
|
|
|
|
if obj.application_icon:
|
2022-08-16 14:23:28 +02:00
|
|
|
|
result['icon:base64'] = force_str(base64.encodebytes(obj.application_icon.read()))
|
2020-07-30 19:06:58 +02:00
|
|
|
|
return result
|
2018-12-27 09:44:21 +01:00
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def load_serialized_settings(cls, json_settings):
|
|
|
|
|
if not json_settings:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
obj = cls.singleton()
|
2020-07-30 19:06:58 +02:00
|
|
|
|
decoded_icon = None
|
|
|
|
|
if json_settings.get('icon:base64'):
|
|
|
|
|
decoded_icon = base64.decodebytes(force_bytes(json_settings['icon:base64']))
|
|
|
|
|
del json_settings['icon:base64']
|
2018-12-27 09:44:21 +01:00
|
|
|
|
for attr in json_settings:
|
|
|
|
|
setattr(obj, attr, json_settings[attr])
|
|
|
|
|
obj.save()
|
2020-07-30 19:06:58 +02:00
|
|
|
|
if decoded_icon:
|
|
|
|
|
if (
|
|
|
|
|
not default_storage.exists(obj.application_icon.name)
|
|
|
|
|
or obj.application_icon.read() != decoded_icon
|
|
|
|
|
):
|
|
|
|
|
# save new file
|
|
|
|
|
path = obj.application_icon.name
|
|
|
|
|
if path.startswith('pwa/'):
|
|
|
|
|
path = path[len('pwa/') :]
|
|
|
|
|
obj.application_icon.save(path, ContentFile(decoded_icon))
|
2018-11-26 17:14:05 +01:00
|
|
|
|
|
2019-04-16 12:05:12 +02:00
|
|
|
|
@classmethod
|
|
|
|
|
def get_default_application_name(cls):
|
|
|
|
|
return settings.TEMPLATE_VARS.get('global_title') or 'Compte Citoyen'
|
|
|
|
|
|
|
|
|
|
def get_application_name(self):
|
|
|
|
|
return self.application_name or self.get_default_application_name()
|
|
|
|
|
|
2018-11-26 17:14:05 +01:00
|
|
|
|
|
2018-12-27 15:26:21 +01:00
|
|
|
|
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)
|
2019-10-14 13:33:00 +02:00
|
|
|
|
link_page = models.ForeignKey(
|
|
|
|
|
'data.Page', on_delete=models.CASCADE, blank=True, null=True, verbose_name=_('Internal link')
|
|
|
|
|
)
|
2018-12-27 15:26:21 +01:00
|
|
|
|
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:
|
2021-09-15 16:40:25 +02:00
|
|
|
|
url = self.link_page.get_online_url()
|
2018-12-27 15:26:21 +01:00
|
|
|
|
else:
|
2021-09-15 16:40:25 +02:00
|
|
|
|
url = utils.get_templated_url(self.url)
|
|
|
|
|
if get_request():
|
|
|
|
|
url = get_request().build_absolute_uri(url)
|
|
|
|
|
return url
|
2018-12-27 15:26:21 +01:00
|
|
|
|
|
|
|
|
|
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
|
2021-02-15 18:01:46 +01:00
|
|
|
|
)
|
2018-12-27 15:26:21 +01:00
|
|
|
|
)[0]
|
|
|
|
|
if self.icon:
|
2022-08-16 14:23:28 +02:00
|
|
|
|
serialized_entry['icon:base64'] = force_str(base64.encodebytes(self.icon.read()))
|
2018-12-27 15:26:21 +01:00
|
|
|
|
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
|
2020-02-13 15:25:24 +01:00
|
|
|
|
fake_entry = next(serializers.deserialize('json', json.dumps([json_entry]), ignorenonexistent=True))
|
2021-08-20 16:34:18 +02:00
|
|
|
|
entry, dummy = cls.objects.get_or_create(
|
2018-12-27 15:26:21 +01:00
|
|
|
|
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
|
2020-02-13 15:25:24 +01:00
|
|
|
|
entry = next(serializers.deserialize('json', json.dumps([json_entry]), ignorenonexistent=True))
|
2018-12-27 15:26:21 +01:00
|
|
|
|
entry.save()
|
|
|
|
|
if json_entry.get('icon:base64'):
|
2020-04-22 20:12:42 +02:00
|
|
|
|
decoded_icon = base64.decodebytes(force_bytes(json_entry['icon:base64']))
|
2018-12-27 15:26:21 +01:00
|
|
|
|
if not default_storage.exists(entry.object.icon.name) or entry.object.icon.read() != decoded_icon:
|
|
|
|
|
# save new file
|
2021-05-04 14:15:30 +02:00
|
|
|
|
entry.object.icon.save(os.path.basename(entry.object.icon.name), ContentFile(decoded_icon))
|
2018-12-27 15:26:21 +01:00
|
|
|
|
|
|
|
|
|
|
2018-11-26 17:14:05 +01:00
|
|
|
|
class PushSubscription(models.Model):
|
|
|
|
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
2021-04-09 15:17:38 +02:00
|
|
|
|
subscription_info = JSONField(default=dict)
|
2018-11-26 17:14:05 +01:00
|
|
|
|
creation_timestamp = models.DateTimeField(auto_now_add=True)
|