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 URLItem.objects.filter(shorturl=self, 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 URLItem.objects.filter(shorturl=self, published__gt=now).order_by('published')[0]
except IndexError :
return None
def last_item (self) :
"""The last URLItem available."""
try :
return URLItem.objects.filter(shorturl=self).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() # 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(),
)