qrurls/models.py
author Tero Marttila <terom@fixme.fi>
Sat, 07 Sep 2013 15:38:24 +0300
changeset 81 3a2fdc820c41
parent 80 3aaac91a6654
child 82 6442e5c97b48
permissions -rw-r--r--
drop separate shorturl_id= lookup key, match shorturl.isdigit()
import datetime
import hashlib
import logging
import os.path

from django.db import models
import django.core.files.storage
from django.core.urlresolvers import reverse
import django.utils.http
from django.contrib.sites.models import get_current_site
from django.utils import timezone

log = logging.getLogger('qrurls.models')

QRCODE_API = 'https://chart.googleapis.com/chart?cht=qr&chs={width}x{height}&chl={url}&chld=H'
IMAGES_MEDIA = 'qrurls-images'

class SecretFileSystemStorage (django.core.files.storage.FileSystemStorage) :
    """
        Store files named by a sha1 hash of their contents.
    """

    HASH = hashlib.sha1
    
    def _hash (self, content) :
        """Compute hash for given UploadedFile (as a hex string)."""
        hash = self.HASH()

        for chunk in content.chunks() :
            hash.update(chunk)

        return hash.hexdigest()
    
    def save (self, name, content) :
        """Convert uploaded filename to a hash of the contents for storage."""
        if name is None:
            name = content.name
        
        dirname, filename = os.path.split(name)
        basename, fileext = os.path.splitext(filename)
        
        # hash
        name = "%s/%s%s" % (dirname, self._hash(content), fileext)

        return super(SecretFileSystemStorage, self).save(name, content)

class URL(models.Model):
    shorturl = models.SlugField(unique=True,
            help_text="Changing this will break existing QR-codes!")
    publishing_time = models.TimeField(default=datetime.time(),
            help_text="Default time to publish new URLItems (in timezone)")
    publishing_days = models.IntegerField(default=1,
            help_text="Default interval for publishing new URLItems")
    title = models.CharField(max_length=1024, blank=True, null=True,
            help_text="Text to display together with images.")

    class Meta:
        verbose_name = u"URL Feed"
        verbose_name_plural = u"URL Feeds"

    def qrcode_img (self, size=512) :
        return QRCODE_API.format(
                width=size, height=size,
                url=django.utils.http.urlquote(self.qrcode_url()),
        )

    def qrcode_url (self) :
        return 'HTTP://{domain}{url}'.format(
                domain = get_current_site(None).domain.upper(),
                url = self.get_absolute_url(),
        )

    def get_absolute_url (self) :
        return reverse('shorturl', args=[self.shorturl])

    def now (self, now=None) :
        """
            Return database-compatible concept of "now".

            All datetimes are strictly stored and compared as UTC. Any
            timezone-aware logic should happen in the admin.
        """
        if now :
            return now
        else :
            return timezone.now()
    
    def active_item (self, now=None) :
        """Currently published URLItem."""
        now = self.now(now)

        try :
            return self.urlitem_set.filter(published__lt=now).order_by('-published')[0]
        except IndexError :
            return None

    def upcoming_item (self, now=None) :
        """Next-up to-be-published URLItem."""
        now = self.now(now)

        try :
            return self.urlitem_set.filter(published__gt=now).order_by('published')[0]
        except IndexError :
            return None

    def last_item (self) :
        """The last URLItem available."""

        try :
            return self.urlitem_set.order_by('-published')[0]
        except IndexError :
            return None

    @property
    def publishing_timetz (self) :
        """publishing_time, with tzinfo on the correct timezone."""
        return self.publishing_time.replace(tzinfo=timezone.get_current_timezone())
    
    @property
    def publishing_offset (self) :
        return datetime.timedelta(days=self.publishing_days)

    def publishing_schedule (self) :
        """Calculate initial URLItem.published values for feed."""

        # following the last item in the queue
        item = self.last_item()

        if item and item.published > self.now():
            # starting from the following day
            date = item.published.date() + self.publishing_offset
        else :
            # defaults to today
            date = self.now().date()
        
        return date, self.publishing_timetz, self.publishing_offset
    
    @classmethod
    def apply_publishing_schedule (self, date, time, offset, count) :
        """Yield publishing times off given date/time to offset * count."""
        for index in xrange(0, count) :
            yield datetime.datetime.combine(date + offset * index, time)

    def __unicode__ (self) :
        return self.shorturl

class URLImage(models.Model):
    image = models.ImageField(upload_to=IMAGES_MEDIA, storage=SecretFileSystemStorage())
    name = models.CharField(max_length=512, blank=False)
    title = models.CharField(max_length=1024, blank=True)
    uploaded = models.DateTimeField(auto_now_add=True)

    class Meta:
        verbose_name = u"URL Image"
        verbose_name_plural = u"URL Images"
        ordering = ['uploaded']

    def save (self) :
        # keep real filename before saving with hash
        # but not when updating!
        if not self.name:
            self.name = self.image.name

        super(URLImage, self).save()

    def get_absolute_url (self) :
        return self.image.url

    def __unicode__ (self) :
        return "[%s] %s" % (self.uploaded.strftime("%Y-%m-%d"), self.name)

class URLItem(models.Model):
    shorturl = models.ForeignKey(URL)
    published = models.DateTimeField(db_index=True) # UTC

    # either-or
    url = models.URLField(blank=True) # populated from image
    image = models.ForeignKey(URLImage, null=True, blank=True)
    
    class Meta:
        verbose_name = u"URL Item"
        verbose_name_plural = u"URL Items"
        ordering = ['published']

    @classmethod
    def search (cls, shorturl=None, item_id=None, related=()) :
        """
            Return the URLItem for a given shorturl, either the given specific one,
            or the latest, from the database.

            Raises URLItem.NotFound
        """
        # JOIN against shorturl, urlimage
        url_item = cls.objects.select_related(*related)

        if not shorturl:
            raise cls.DoesNotExist()
        elif shorturl.isdigit():
            shorturl_id = int(shorturl)
            url_item = url_item.filter(shorturl__id=shorturl_id)
        else:
            url_item = url_item.filter(shorturl__shorturl=shorturl)
        
        # match for published items
        now = timezone.now()
        url_item = url_item.filter(published__lt=now).order_by('-published')
        
        log.info("Search URLItem @ %s", now)
       
        if item_id :
            # specific, but still the published on
            return url_item.get(id=int(item_id)) # raises DoesNotExist
        else :
            # most recent
            try:
                return url_item[0]
            except IndexError:
                raise DoesNotExist()

    def get_absolute_url (self) :
        if self.url :
            return self.url
        elif self.shorturl and self.id :
            return reverse('shorturl_item', kwargs=dict(shorturl=self.shorturl, item_id=self.id))
        else :
            return None
   
    def published_age (self) :
        now = self.shorturl.now() # UTC

        if now > self.published:
            td = now - self.published
        else :
            td = self.published - now

        days, seconds = td.days, td.seconds
        m, s = divmod(seconds, 60)
        h, m = divmod(m, 60)

        return (self.published < now), days, "{h}h {m}m {s}s".format(h=h, m=m, s=s)

    def published_state (self) :
        date = self.published.strftime("%Y-%m-%d")
        published, days, age = self.published_age()

        if published and days :
            return "[{date}]".format(date=date)
        elif published :
            return "[{age}]".format(age=age)
        elif days :
            return "({when})".format(when=date)
        else :
            return "({age})".format(age=age)

    def last_modified (self) :
        # XXX: this asumes that URLImage is never changed after publishing..
        return self.published

    def title (self) :
        if self.image and self.image.title.strip() :
            return self.image.title
        else :
            return self.shorturl.title
    
    def __unicode__ (self) :
        return u"{published_state} {url}".format(
                published_state=self.published_state(),
                url=self.get_absolute_url(),
        )