# # 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 . 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)