Skip to content
Snippets Groups Projects
models.py 5.13 KiB
from io import BytesIO
import hashlib
import json
import sys

from PIL import Image
from django.conf import settings
from django.core.cache import cache
from django.core.files.storage import default_storage as storage
from django.core.files.uploadedfile import InMemoryUploadedFile
from django.db import models
from django.db.models import Q, signals

use_sites = hasattr(settings, "SITE_ID")

if use_sites:
    from django.contrib.sites.models import Site
    from django.contrib.sites.managers import CurrentSiteManager

config = {
    'shortcut icon': [16, 32, 48, 128, 192],
    'touch-icon': [192],
    'icon': [192],
    'apple-touch-icon': [57, 72, 114, 144, 180],
    'apple-touch-icon-precomposed': [57, 72, 76, 114, 120, 144, 152, 180],
}

config = getattr(settings, 'FAVICON_CONFIG', config)
if "shortcut icon" not in config or 32 not in config["shortcut icon"]:
    config.setdefault("shortcut_icon", []).append(32)

config_cache_key = "favicons_" + hashlib.sha1(json.dumps(config, sort_keys=True).encode()).hexdigest()

image_path = getattr(settings, "FAVICON_PATH", "favicon")


def pre_delete_image(sender, instance, **kwargs):
    instance.del_image()


class Favicon(models.Model):
    title = models.CharField(max_length=100)
    faviconImage = models.ImageField(upload_to=image_path)

    isFavicon = models.BooleanField(default=True)

    objects = models.Manager()
    on_site = objects

    if use_sites:
        site = models.ForeignKey(Site, related_name="favicon", on_delete=models.CASCADE, blank=True, null=True, default=settings.SITE_ID)

        on_site = CurrentSiteManager()

        def save(self, *args, **kwargs):
            self.site = Site.objects.get_current()
            return super(Favicon, self).save(*args, **kwargs)

    class Meta:
        verbose_name = 'Favicon'
        verbose_name_plural = 'Favicons'

    def get_favicons(self, update=False, config_override=None):
        """
        Get all combinations of favicons as configured, creating mising ones.

        Pass update=True to force re-generation of existing icons.
        """
        # Use default config by default
        if config_override is None:
            config_override = config
        # Get all combinations of favicon rels and sizes from config
        query = Q()
        for rel in config_override:
            for size in config_override[rel]:
                query |= Q(rel=rel, size=size)

        # Get all existing favicons
        favicons = FaviconImg.objects.filter(faviconFK=self).filter(query)

        # Delete all favicon images to update all
        if update:
            favicons.delete()
            favicons = FaviconImg.objects.none()
            found = []
        else:
            favicons = favicons.all()
            found = [(f.rel, f.size) for f in favicons]

        # Check whether favicons are missing
        new_favicons = []
        for rel in config:
            for size in config[rel]:
                if not (rel, size) in found:
                    fav = FaviconImg(faviconFK=self, size=size, rel=rel)
                    fav.generate_image()
                    new_favicons.append(fav)
        if new_favicons:
            FaviconImg.objects.bulk_create(new_favicons, ignore_conflicts=True)
                
        return list(favicons) + new_favicons

    def __str__(self):
        return self.faviconImage.name

    def get_absolute_url(self):
        return self.faviconImage.name

    def del_image(self):
        self.faviconImage.delete()

    def as_html(self, update=False):
        """Return <link> html tags for this favicon set."""
        html = ''
        for favicon in self.get_favicons(update=update):
            html += favicon.as_html()
        return html

    def save(self, *args, **kwargs):
        if self.isFavicon:
            Favicon.on_site.exclude(pk=self.pk).update(isFavicon=False)

        super(Favicon, self).save(*args, **kwargs)

        cache.delete(config_cache_key)

        if self.faviconImage:
            html = self.as_html(update=True)
            cache.set(config_cache_key, html, timeout=None)


class FaviconImg(models.Model):
    faviconFK = models.ForeignKey(Favicon, on_delete=models.CASCADE)
    size = models.IntegerField()
    rel = models.CharField(max_length=250, null=True)
    faviconImage = models.ImageField(upload_to=image_path)

    def as_html(self):
        """Return a <link> tag forthis favicon image."""
        return f'<link rel="{self.rel}" sizes="{self.size}x{self.size}" href="{self.faviconImage.url}"/>'

    def generate_image(self):
        tmp = Image.open(storage.open(self.faviconFK.faviconImage.name))
        tmp.thumbnail((self.size, self.size), Image.ANTIALIAS)

        tmp_io = BytesIO()
        tmp.save(tmp_io, format='PNG')
        tmp_file = InMemoryUploadedFile(tmp_io, None, f"fav-{self.size}s.png", 'image/png', sys.getsizeof(tmp_io), None)

        self.faviconImage = tmp_file

    def del_image(self):
        self.faviconImage.delete()

    class Meta:
        constraints = [models.UniqueConstraint(fields=["faviconFK", "size", "rel"], name="favicon_size_rel_unique")]


signals.pre_delete.connect(pre_delete_image, sender=Favicon)
signals.pre_delete.connect(pre_delete_image, sender=FaviconImg)