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