qrurls/models.py
author Tero Marttila <terom@fixme.fi>
Mon, 02 Sep 2013 02:21:42 +0300
changeset 69 f7db1b752763
parent 68 182ac4b328ec
child 72 ea7a5a5ce7d4
permissions -rw-r--r--
URL model: use self.urlitem_set to let orm caching work for item.shorturl (optimize one unneeded query on view)
import datetime
import hashlib
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

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
        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']

    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 __unicode__ (self) :
        return u"{published_state} {url}".format(
                published_state=self.published_state(),
                url=self.get_absolute_url(),
        )