combo/combo/apps/pwa/models.py

209 lines
8.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#
# 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/>.
import base64
import json
import os
from django.conf import settings
from django.core import serializers
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
from django.db import models
from django.db.models import JSONField
from django.utils.encoding import force_bytes, force_str
from django.utils.translation import gettext_lazy as _
from py_vapid import Vapid
from combo import utils
from combo.data.fields import RichTextField
from combo.middleware import get_request
class PwaSettings(models.Model):
APPLICATION_ICON_SIZES = ['%sx%s' % (x, x) for x in (48, 96, 192, 256, 512)]
application_name = models.CharField(verbose_name=_('Application Name'), max_length=64, blank=True)
application_icon = models.FileField(
verbose_name=_('Application Icon'),
help_text=_(
'Icon file must be in JPEG or PNG format, and should be a square of at least 512×512 pixels.'
),
upload_to='pwa',
blank=True,
null=True,
)
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.'
),
)
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)
push_notifications = models.BooleanField(
verbose_name=_('Enable subscription to push notifications'), default=False
)
push_notifications_infos = JSONField(blank=True, default=dict)
last_update_timestamp = models.DateTimeField(auto_now=True)
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 = {
'private_key': force_str(vapid.private_pem()),
}
elif not self.push_notifications:
self.push_notifications_infos = {}
return super().save(**kwargs)
@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]))
result = serialized_settings[0].get('fields')
if obj.application_icon:
result['icon:base64'] = force_str(base64.encodebytes(obj.application_icon.read()))
return result
@classmethod
def load_serialized_settings(cls, json_settings):
if not json_settings:
return
obj = cls.singleton()
decoded_icon = None
if json_settings.get('icon:base64'):
decoded_icon = base64.decodebytes(force_bytes(json_settings['icon:base64']))
del json_settings['icon:base64']
for attr in json_settings:
setattr(obj, attr, json_settings[attr])
obj.save()
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))
@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()
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', on_delete=models.CASCADE, 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:
url = self.link_page.get_online_url()
else:
url = utils.get_templated_url(self.url)
if get_request():
url = get_request().build_absolute_uri(url)
return 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:
serialized_entry['icon:base64'] = force_str(base64.encodebytes(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 = next(serializers.deserialize('json', json.dumps([json_entry]), ignorenonexistent=True))
entry, dummy = 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 = next(serializers.deserialize('json', json.dumps([json_entry]), ignorenonexistent=True))
entry.save()
if json_entry.get('icon:base64'):
decoded_icon = base64.decodebytes(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(os.path.basename(entry.object.icon.name), ContentFile(decoded_icon))
class PushSubscription(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
subscription_info = JSONField(default=dict)
creation_timestamp = models.DateTimeField(auto_now_add=True)