terom@27: import datetime terom@41: import hashlib terom@80: import logging terom@41: import os.path terom@27: terom@41: import django.core.files.storage terom@4: import django.utils.http terom@4: from django.contrib.sites.models import get_current_site terom@82: from django.core.cache import cache terom@82: from django.core.urlresolvers import reverse terom@82: from django.db import models terom@6: from django.utils import timezone terom@4: terom@80: log = logging.getLogger('qrurls.models') terom@80: 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@77: # but not when updating! terom@77: if not self.name: terom@77: 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@80: @classmethod terom@82: def get_item (cls, shorturl, item_id=None, related=()) : terom@80: """ terom@80: Return the URLItem for a given shorturl, either the given specific one, terom@80: or the latest, from the database. terom@80: terom@80: Raises URLItem.NotFound terom@80: """ terom@80: # JOIN against shorturl, urlimage terom@80: url_item = cls.objects.select_related(*related) terom@80: terom@81: if not shorturl: terom@81: raise cls.DoesNotExist() terom@81: elif shorturl.isdigit(): terom@81: shorturl_id = int(shorturl) terom@80: url_item = url_item.filter(shorturl__id=shorturl_id) terom@80: else: terom@81: url_item = url_item.filter(shorturl__shorturl=shorturl) terom@80: terom@80: # match for published items terom@80: now = timezone.now() terom@80: url_item = url_item.filter(published__lt=now).order_by('-published') terom@80: terom@80: if item_id : terom@80: # specific, but still the published on terom@82: item_id = int(item_id) terom@82: log.debug("search @ %d", item_id) terom@82: terom@82: return url_item.get(id=item_id) # raises DoesNotExist terom@80: else : terom@80: # most recent terom@82: log.debug("search @ %s", now) terom@80: try: terom@80: return url_item[0] terom@80: except IndexError: terom@80: raise DoesNotExist() terom@80: terom@82: @classmethod terom@82: def get_url (cls, shorturl) : terom@82: """ terom@82: Return the current URL for a given shorturl, from cache or DB. terom@82: terom@82: Returns url:str, modified:datetime terom@82: Raises URLItem.NotFound terom@82: """ terom@82: terom@82: key = 'qrurls/url/{shorturl}'.format(shorturl=shorturl) terom@82: get = cache.get(key) terom@82: terom@82: if get : terom@82: url, modified = get terom@82: log.debug("get cache: %s: %s", key, url) terom@82: else: terom@82: # from db terom@82: url_item = cls.get_item(shorturl=shorturl) terom@82: terom@82: url = url_item.get_absolute_url() terom@82: modified = url_item.last_modified() terom@82: terom@82: log.debug("set cache: %s: %s", key, url) terom@82: cache.set(key, (url, modified)) # XXX: expiry terom@82: terom@82: return url, modified terom@82: 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@78: def title (self) : terom@78: if self.image and self.image.title.strip() : terom@78: return self.image.title terom@78: else : terom@78: return self.shorturl.title terom@78: 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: