# -*- coding: utf-8 -*- import hashlib import datetime from time import sleep import traceback from django.db import models, DatabaseError from django.db.models import Q, F from django.utils import timezone from django.core.cache import cache from django.core.urlresolvers import reverse from django.core.mail import mail_admins from django.conf import settings from cms.models.pluginmodel import CMSPlugin from cms.models.pagemodel import Page class Teaser(models.Model): """Тизер.""" page = models.ForeignKey(Page, verbose_name=u'страница', limit_choices_to={'publisher_is_draft': True}, help_text=u'Куда перейти по клику.') title = models.CharField(u'заголовок', max_length=255) anchor = models.CharField(u'якорь', max_length=50, blank=True, default='', help_text=u'Содержимое этого поля будет дописано к адресу страницы в ссылке тизера. Символ # набирать не нужно.') image = models.ImageField(u'картинка', upload_to='teasers/', blank=True, default='') body = models.TextField(u'текст', max_length=500, blank=True, default='') views_count = models.PositiveIntegerField(u'показы', default=0) clicks_count = models.PositiveIntegerField(u'клики', default=0, editable=False) # NOT USED last_viewed_at = models.DateTimeField(u'последний показ', null=True, default=None, editable=False) created_at = models.DateTimeField(u'создан', auto_now_add=True) updated_at = models.DateTimeField(u'изменен', auto_now=True) def __unicode__(self): return self.title class Meta: verbose_name = u'тизер' verbose_name_plural = u'тизеры' def get_url(self): page_url = self.page.get_absolute_url() anchor = self.anchor.replace('#','').strip() if anchor: return u'%s#%s' % (page_url, anchor) else: return page_url def ctr(self): """Показатель кликабельности. NOT USED """ if self.views_count > 0: ctr = float(self.clicks_count) / self.views_count * 100 return u'%.1f%%' % ctr else: return u'-' ctr.short_description = u'CTR' class PageTeasers(CMSPlugin): """Тизеры на cms странице.""" teasers = models.ManyToManyField(Teaser, verbose_name=u'тизеры') def copy_relations(self, oldinstance): self.teasers = oldinstance.teasers.all() def __unicode__(self): return u'%s' % self.pk class PathTeaser(models.Model): """Тизер на странице с определенным адресом.""" teaser = models.ForeignKey(Teaser, related_name='path_teasers') path = models.CharField(max_length=500) expire_date = models.DateTimeField() class Meta: index_together = [ ['path', 'teaser'], ['path', 'expire_date'], ] ### common_cache_prefix = getattr(settings, 'COMMON_CACHE_PREFIX') # let it fail if no prefix lock_key_prefix = '%s%s' % (common_cache_prefix, 'path_teasers_lock_') lock_expire = 2*60 # 2 minutes make_lock_key = lambda path: '%s%s' % (lock_key_prefix, hashlib.sha1(path).hexdigest()) def get_teasers_for_path(path): """Получить тизеры в порядке, заданном для указанного пути path. Возвращает бесконечный generator. Когда тизеры в списке заканчиваются, возвращает None, вместо исключения StopIteration. """ def _gen(teasers): _ids = set() _page_ids = set() for _t in teasers: if _t.pk not in _ids: # только уникальные тизеры _ids.add(_t.pk) if _t.page_id not in _page_ids: # только уникальные страницы _page_ids.add(_t.page_id) yield _t del _ids del _page_ids while True: yield None # --- if not PathTeaser.objects.filter(path__iexact=path, expire_date__gt=timezone.now()).count(): lock_key = make_lock_key(path) # lock на обновление списка тизеров для пути path, чтобы другой процесс не запустил параллельное обновление if cache.add(lock_key, 'true', lock_expire): try: # завтра, между 1 и 2 часами ночи expire_date = datetime.datetime.now().replace(hour=1) + datetime.timedelta(days=1) if timezone.is_naive(expire_date): expire_date = timezone.make_aware(expire_date, timezone.get_current_timezone()) objects_list = [] for teaser in Teaser.objects.all().order_by('?'): objects_list.append( PathTeaser( teaser = teaser, path = path, expire_date = expire_date ) ) if objects_list: try: PathTeaser.objects.bulk_create(objects_list) except DatabaseError as e: sleep(1) try: # one last try PathTeaser.objects.bulk_create(objects_list) except DatabaseError as e: if settings.DEBUG: raise e else: # только письмо об ошибке. # дальше не падаем, иначе пользователь не увидит страницу mail_admins( subject=u'ERROR reordering teasers for the path %s' % path, message=traceback.format_exc() ) finally: cache.delete(lock_key) # если в PathTeaser вдруг нет актуальных по времени записей для пути path, # то вернёт тизеры с сортировкой по умолчанию teasers = Teaser.objects.filter( Q(path_teasers__path__iexact=path) | Q(path_teasers__path__isnull=True), Q(path_teasers__expire_date__gt=timezone.now()) | Q(path_teasers__expire_date__isnull=True), ).order_by('path_teasers__pk') return _gen(teasers) ### def update_views_count_many(teaser_ids): """Обновляет статистику просмотров сразу для нескольких тизеров.""" try: return Teaser.objects.filter(pk__in=teaser_ids).update( views_count = F('views_count') + 1, last_viewed_at = timezone.now() ) except DatabaseError as e: # здесь можно упасть после второй попытки, так как это служебный ajax-запрос sleep(1) # one last try return Teaser.objects.filter(pk__in=teaser_ids).update( views_count = F('views_count') + 1, last_viewed_at = timezone.now() ) def update_views_count(teaser_id): """Обновляет статистику просмотров.""" return Teaser.objects.filter(pk=teaser_id).update( views_count = F('views_count') + 1, last_viewed_at = timezone.now() ) def update_clicks_count(teaser_id): """Обновляет статистику переходов.""" return Teaser.objects.filter(pk=teaser_id).update( clicks_count = F('clicks_count') + 1 )