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(),
)