You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

206 lines
7.9 KiB

# -*- 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'Содержимое этого поля будет дописано к адресу страницы в ссылке тизера. Символ <b>#</b> набирать не нужно.')
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
)