terom@27: import datetime terom@41: import hashlib terom@41: import os.path terom@27: terom@0: from django.db import models terom@41: import django.core.files.storage terom@2: from django.core.urlresolvers import reverse terom@4: import django.utils.http terom@4: from django.contrib.sites.models import get_current_site terom@6: from django.utils import timezone terom@4: terom@18: QRCODE_API = 'https://chart.googleapis.com/chart?cht=qr&chs={width}x{height}&chl={url}&chld=H' terom@41: IMAGES_MEDIA = 'qrurls-images' terom@41: terom@41: class SecretFileSystemStorage (django.core.files.storage.FileSystemStorage) : terom@41: """ terom@41: Store files named by a sha1 hash of their contents. terom@41: """ terom@41: terom@41: HASH = hashlib.sha1 terom@41: terom@41: def _hash (self, content) : terom@41: """Compute hash for given UploadedFile (as a hex string).""" terom@41: hash = self.HASH() terom@41: terom@41: for chunk in content.chunks() : terom@41: hash.update(chunk) terom@41: terom@41: return hash.hexdigest() terom@41: terom@41: def save (self, name, content) : terom@66: """Convert uploaded filename to a hash of the contents for storage.""" terom@41: if name is None: terom@41: name = content.name terom@41: terom@41: dirname, filename = os.path.split(name) terom@41: basename, fileext = os.path.splitext(filename) terom@41: terom@41: # hash terom@41: name = "%s/%s%s" % (dirname, self._hash(content), fileext) terom@41: terom@41: return super(SecretFileSystemStorage, self).save(name, content) terom@0: terom@2: class URL(models.Model): terom@48: shorturl = models.SlugField(unique=True, terom@48: help_text="Changing this will break existing QR-codes!") terom@47: publishing_time = models.TimeField(default=datetime.time(), terom@47: help_text="Default time to publish new URLItems (in timezone)") terom@63: publishing_days = models.IntegerField(default=1, terom@63: help_text="Default interval for publishing new URLItems") terom@51: title = models.CharField(max_length=1024, blank=True, null=True, terom@51: help_text="Text to display together with images.") terom@2: terom@3: class Meta: terom@12: verbose_name = u"URL Feed" terom@12: verbose_name_plural = u"URL Feeds" terom@3: terom@4: def qrcode_img (self, size=512) : terom@4: return QRCODE_API.format( terom@4: width=size, height=size, terom@4: url=django.utils.http.urlquote(self.qrcode_url()), terom@4: ) terom@4: terom@4: def qrcode_url (self) : terom@22: return 'HTTP://{domain}{url}'.format( terom@22: domain = get_current_site(None).domain.upper(), terom@23: url = self.get_absolute_url(), terom@22: ) terom@4: terom@2: def get_absolute_url (self) : terom@2: return reverse('shorturl', args=[self.shorturl]) terom@2: terom@47: def now (self, now=None) : terom@47: """ terom@47: Return database-compatible concept of "now". terom@47: terom@47: All datetimes are strictly stored and compared as UTC. Any terom@47: timezone-aware logic should happen in the admin. terom@47: """ terom@47: if now : terom@47: return now terom@47: else : terom@47: return timezone.now() terom@47: terom@47: def active_item (self, now=None) : terom@7: """Currently published URLItem.""" terom@47: now = self.now(now) terom@6: terom@16: try : terom@69: return self.urlitem_set.filter(published__lt=now).order_by('-published')[0] terom@16: except IndexError : terom@16: return None terom@6: terom@27: def upcoming_item (self, now=None) : terom@47: """Next-up to-be-published URLItem.""" terom@47: now = self.now(now) terom@7: terom@16: try : terom@69: return self.urlitem_set.filter(published__gt=now).order_by('published')[0] terom@16: except IndexError : terom@16: return None terom@7: terom@27: def last_item (self) : terom@27: """The last URLItem available.""" terom@27: terom@27: try : terom@69: return self.urlitem_set.order_by('-published')[0] terom@27: except IndexError : terom@27: return None terom@27: terom@56: @property terom@56: def publishing_timetz (self) : terom@56: """publishing_time, with tzinfo on the correct timezone.""" terom@56: return self.publishing_time.replace(tzinfo=timezone.get_current_timezone()) terom@56: terom@63: @property terom@63: def publishing_offset (self) : terom@63: return datetime.timedelta(days=self.publishing_days) terom@56: terom@56: def publishing_schedule (self) : terom@56: """Calculate initial URLItem.published values for feed.""" terom@56: terom@56: # following the last item in the queue terom@56: item = self.last_item() terom@56: terom@56: if item and item.published > self.now(): terom@56: # starting from the following day terom@56: date = item.published.date() + self.publishing_offset terom@56: else : terom@56: # defaults to today terom@56: date = self.now().date() terom@56: terom@56: return date, self.publishing_timetz, self.publishing_offset terom@56: terom@56: @classmethod terom@56: def apply_publishing_schedule (self, date, time, offset, count) : terom@56: """Yield publishing times off given date/time to offset * count.""" terom@56: for index in xrange(0, count) : terom@56: yield datetime.datetime.combine(date + offset * index, time) terom@56: terom@2: def __unicode__ (self) : terom@2: return self.shorturl terom@2: terom@28: class URLImage(models.Model): terom@41: image = models.ImageField(upload_to=IMAGES_MEDIA, storage=SecretFileSystemStorage()) terom@66: name = models.CharField(max_length=512, blank=False) terom@51: title = models.CharField(max_length=1024, blank=True) terom@30: uploaded = models.DateTimeField(auto_now_add=True) terom@30: terom@30: class Meta: terom@30: verbose_name = u"URL Image" terom@30: verbose_name_plural = u"URL Images" terom@30: ordering = ['uploaded'] terom@30: terom@66: def save (self) : terom@66: # keep real filename before saving with hash terom@66: self.name = self.image.name terom@66: terom@66: super(URLImage, self).save() terom@66: terom@32: def get_absolute_url (self) : terom@32: return self.image.url terom@32: terom@30: def __unicode__ (self) : terom@66: return "[%s] %s" % (self.uploaded.strftime("%Y-%m-%d"), self.name) terom@28: terom@2: class URLItem(models.Model): terom@2: shorturl = models.ForeignKey(URL) terom@68: published = models.DateTimeField(db_index=True) # UTC terom@32: terom@32: # either-or terom@32: url = models.URLField(blank=True) # populated from image terom@32: image = models.ForeignKey(URLImage, null=True, blank=True) terom@2: terom@3: class Meta: terom@3: verbose_name = u"URL Item" terom@3: verbose_name_plural = u"URL Items" terom@3: ordering = ['published'] terom@3: terom@2: def get_absolute_url (self) : terom@43: if self.url : terom@43: return self.url terom@44: elif self.shorturl and self.id : terom@44: return reverse('shorturl_item', kwargs=dict(shorturl=self.shorturl, item_id=self.id)) terom@35: else : terom@44: return None terom@47: terom@7: def published_age (self) : terom@47: now = self.shorturl.now() # UTC terom@6: terom@6: if now > self.published: terom@6: td = now - self.published terom@6: else : terom@6: td = self.published - now terom@6: terom@6: days, seconds = td.days, td.seconds terom@6: m, s = divmod(seconds, 60) terom@6: h, m = divmod(m, 60) terom@7: terom@9: return (self.published < now), days, "{h}h {m}m {s}s".format(h=h, m=m, s=s) terom@7: terom@7: def published_state (self) : terom@9: date = self.published.strftime("%Y-%m-%d") terom@9: published, days, age = self.published_age() terom@6: terom@9: if published and days : terom@9: return "[{date}]".format(date=date) terom@9: elif published : terom@9: return "[{age}]".format(age=age) terom@6: elif days : terom@9: return "({when})".format(when=date) terom@6: else : terom@9: return "({age})".format(age=age) terom@6: terom@72: def last_modified (self) : terom@72: # XXX: this asumes that URLImage is never changed after publishing.. terom@72: return self.published terom@72: terom@2: def __unicode__ (self) : terom@9: return u"{published_state} {url}".format( terom@9: published_state=self.published_state(), terom@7: url=self.get_absolute_url(), terom@7: ) terom@32: