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.
965 lines
39 KiB
965 lines
39 KiB
# -*- coding: utf-8 -*-
|
|
"""Models for emencia.django.newsletter"""
|
|
from smtplib import SMTP
|
|
from datetime import datetime
|
|
from datetime import date
|
|
from datetime import timedelta
|
|
from dateutil import relativedelta
|
|
from email.MIMEImage import MIMEImage
|
|
from random import choice
|
|
from itertools import chain, groupby
|
|
import copy
|
|
import operator
|
|
from pymorphy2 import MorphAnalyzer
|
|
import redis
|
|
from redis_collections import List
|
|
|
|
from django.db import models
|
|
from django.db.models import Q
|
|
from django.utils.encoding import smart_str
|
|
from django.utils.encoding import force_unicode
|
|
from django.utils.translation import ugettext_lazy as _
|
|
from django.utils.translation import activate
|
|
from django.utils.safestring import mark_safe
|
|
from django.conf import settings
|
|
from django.core.mail import EmailMessage
|
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
|
from django.core.urlresolvers import reverse
|
|
from django.contrib.sites.models import Site
|
|
from django.contrib.contenttypes import generic
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.contrib.auth.models import Group
|
|
from django.template.loader import render_to_string, get_template
|
|
from django.template import Context
|
|
from django.http import QueryDict
|
|
|
|
from emencia.django.newsletter.managers import ContactManager
|
|
from emencia.django.newsletter.settings import BASE_PATH
|
|
from emencia.django.newsletter.settings import MAILER_HARD_LIMIT
|
|
from emencia.django.newsletter.settings import DEFAULT_HEADER_REPLY
|
|
from emencia.django.newsletter.settings import DEFAULT_HEADER_SENDER
|
|
from emencia.django.newsletter.settings import ACTIVATION_SUBJECT, ACTIVATION_TEMPLATE, DEFAULT_SMPTSERVER_ID
|
|
from emencia.django.newsletter.utils.vcard import vcard_contact_export
|
|
from emencia.django.newsletter.utils import make_activation_code
|
|
from functions.form_check import translit_with_separator
|
|
from functions.model_utils import EnumChoices
|
|
from functions.files import gen_path
|
|
|
|
from exposition.models import Exposition
|
|
from conference.models import Conference
|
|
from article.models import Article
|
|
from haystack.query import SearchQuerySet, RelatedSearchQuerySet, SQ
|
|
from haystack.inputs import Exact
|
|
|
|
morph = MorphAnalyzer()
|
|
|
|
|
|
class SMTPServer(models.Model):
|
|
"""Configuration of a SMTP server"""
|
|
name = models.CharField(_('name'), max_length=255)
|
|
host = models.CharField(_('server host'), max_length=255)
|
|
user = models.CharField(_('server user'), max_length=128, blank=True,
|
|
help_text=_('Leave it empty if the host is public.'))
|
|
password = models.CharField(_('server password'), max_length=128, blank=True,
|
|
help_text=_('Leave it empty if the host is public.'))
|
|
port = models.IntegerField(_('server port'), default=25)
|
|
tls = models.BooleanField(_('server use TLS'))
|
|
|
|
headers = models.TextField(_('custom headers'), blank=True,
|
|
help_text=_('key1: value1 key2: value2, splitted by return line.\n'\
|
|
'Useful for passing some tracking headers if your provider allows it.'))
|
|
mails_hour = models.IntegerField(_('mails per hour'), default=0)
|
|
|
|
def connect(self):
|
|
"""Connect the SMTP Server"""
|
|
smtp = SMTP(smart_str(self.host), int(self.port))
|
|
smtp.ehlo_or_helo_if_needed()
|
|
if self.tls:
|
|
smtp.starttls()
|
|
smtp.ehlo_or_helo_if_needed()
|
|
|
|
if self.user or self.password:
|
|
smtp.login(smart_str(self.user), smart_str(self.password))
|
|
return smtp
|
|
|
|
def delay(self):
|
|
"""compute the delay (in seconds) between mails to ensure mails
|
|
per hour limit is not reached
|
|
|
|
:rtype: float
|
|
"""
|
|
if not self.mails_hour:
|
|
return 0.0
|
|
else:
|
|
return 3600.0 / self.mails_hour
|
|
|
|
def credits(self):
|
|
"""Return how many mails the server can send"""
|
|
if not self.mails_hour:
|
|
return MAILER_HARD_LIMIT
|
|
|
|
last_hour = datetime.now() - timedelta(hours=1)
|
|
sent_last_hour = ContactMailingStatus.objects.filter(
|
|
models.Q(status=ContactMailingStatus.SENT) |
|
|
models.Q(status=ContactMailingStatus.SENT_TEST),
|
|
newsletter__server=self,
|
|
creation_date__gte=last_hour).count()
|
|
return self.mails_hour - sent_last_hour
|
|
|
|
@property
|
|
def custom_headers(self):
|
|
if self.headers:
|
|
headers = {}
|
|
for header in self.headers.splitlines():
|
|
if header:
|
|
key, value = header.split(':')
|
|
headers[key.strip()] = value.strip()
|
|
return headers
|
|
return {}
|
|
|
|
def __unicode__(self):
|
|
return '%s (%s)' % (self.name, self.host)
|
|
|
|
class Meta:
|
|
verbose_name = _('SMTP server')
|
|
verbose_name_plural = _('SMTP servers')
|
|
|
|
|
|
class Contact(models.Model):
|
|
"""Contact for emailing"""
|
|
PERIODIC_CHOICES = EnumChoices(
|
|
WEEK = (1, _(u'Раз в неделю')),
|
|
WEEK_2 = (2, _(u'Раз в 2 недели')),
|
|
MONTH = (3, _(u'Раз в месяц')),
|
|
)
|
|
PERIODIC_DAY_CHOICES = EnumChoices(
|
|
MON=(1, _(u'Пн')),
|
|
TUE=(2, _(u'Вт')),
|
|
WED=(3, _(u'Ср')),
|
|
THU=(4, _(u'Чт')),
|
|
FRI=(5, _(u'Пт')),
|
|
# Sat=(6, _(u'Суббота')),
|
|
# Sun=(7, _(u'Воскресенье')),
|
|
)
|
|
activation_subject = ACTIVATION_SUBJECT
|
|
activation_template = ACTIVATION_TEMPLATE
|
|
user = models.ForeignKey('accounts.User', null=True)
|
|
email = models.EmailField(_('email'), unique=True, max_length=255)
|
|
first_name = models.CharField(_('first name'), max_length=50, blank=True)
|
|
last_name = models.CharField(_('last name'), max_length=50, blank=True)
|
|
|
|
subscriber = models.BooleanField(_('subscriber'), default=True)
|
|
valid = models.BooleanField(_('valid email'), default=True)
|
|
tester = models.BooleanField(_('contact tester'), default=False)
|
|
activation_code = models.CharField(
|
|
verbose_name=_('activation code'), max_length=40,
|
|
default=make_activation_code
|
|
)
|
|
activated = models.BooleanField(default=False)
|
|
from_users = models.BooleanField(default=False)
|
|
dailymailing = models.BooleanField(default=False)
|
|
|
|
moscow = models.BooleanField(_(u'Москва'), blank=True, default=True)
|
|
russia = models.BooleanField(_(u'Россия'), blank=True, default=True)
|
|
r_cities = models.ManyToManyField('city.City', blank=True, null=True, verbose_name=_(u'Города России'))
|
|
foreign = models.BooleanField(_(u'Зарубеж'), blank=True, default=True)
|
|
f_countries = models.ManyToManyField('country.Country', blank=True, null=True, verbose_name=_(u'Зарубежные страны'))
|
|
area = models.ManyToManyField('country.Area', blank=True, null=True, verbose_name=_(u'Географическая зона'))
|
|
periodic = models.PositiveSmallIntegerField(_(u'Периодичность отправки'),
|
|
choices=PERIODIC_CHOICES, default=PERIODIC_CHOICES.WEEK)
|
|
periodic_day = models.PositiveSmallIntegerField(_(u'День отправки'),
|
|
choices=PERIODIC_DAY_CHOICES, default=PERIODIC_DAY_CHOICES.WED)
|
|
content_news = models.BooleanField(_(u'Новости событий'), blank=True, default=True)
|
|
content_overview = models.BooleanField(_(u'Статьи для экспонентов'), blank=True, default=True)
|
|
content_articles = models.BooleanField(_(u'Статьи для организаторов событий'), blank=True, default=True)
|
|
tags = models.ManyToManyField('theme.Tag', verbose_name=_(u'Теги'), blank=True, null=True)
|
|
themes = models.ManyToManyField('theme.Theme', blank=True, null=True, verbose_name=_(u'Тематики'))
|
|
|
|
conferences = models.ManyToManyField('conference.Conference', verbose_name=_(u'Конференции'), null=True)
|
|
expositions = models.ManyToManyField('exposition.Exposition', verbose_name=_(u'Выставки'), null=True)
|
|
|
|
content_type = models.ForeignKey(ContentType, blank=True, null=True)
|
|
object_id = models.PositiveIntegerField(blank=True, null=True)
|
|
content_object = generic.GenericForeignKey('content_type', 'object_id')
|
|
|
|
creation_date = models.DateTimeField(_('creation date'), auto_now_add=True)
|
|
modification_date = models.DateTimeField(_('modification date'), auto_now=True)
|
|
last_mailing_date = models.DateField(_(u'Дата посленего автоматического письма'), blank=True, null=True)
|
|
|
|
objects = ContactManager()
|
|
|
|
def unsubscribe(self):
|
|
self.subscriber = False
|
|
self.moscow = False
|
|
self.russia = False
|
|
self.foreign = False
|
|
self.content_news = False
|
|
self.content_overview = False
|
|
self.content_articles = False
|
|
self.tags.clear()
|
|
self.themes.clear()
|
|
self.f_countries.clear()
|
|
self.r_cities.clear()
|
|
self.save()
|
|
|
|
def subscriptions(self):
|
|
"""Return the user subscriptions"""
|
|
return MailingList.objects.filter(subscribers=self)
|
|
|
|
def unsubscriptions(self):
|
|
"""Return the user unsubscriptions"""
|
|
return MailingList.objects.filter(unsubscribers=self)
|
|
|
|
def vcard_format(self):
|
|
return vcard_contact_export(self)
|
|
|
|
def mail_format(self):
|
|
if self.first_name and self.last_name:
|
|
return '%s %s <%s>' % (self.last_name, self.first_name, self.email)
|
|
return self.email
|
|
mail_format.short_description = _('mail format')
|
|
|
|
def get_absolute_url(self):
|
|
if self.content_type and self.object_id:
|
|
return self.content_object.get_absolute_url()
|
|
return reverse('admin:newsletter_contact_change', args=(self.pk,))
|
|
|
|
def get_or_fillattr(self, attr, func):
|
|
value = getattr(self, 'attr', None)
|
|
if value is None:
|
|
value = func()
|
|
setattr(self, attr, value)
|
|
return value
|
|
|
|
def get_r_cities(self):
|
|
return self.get_or_fillattr('_r_cities', lambda: list(self.r_cities.language().exclude(pk=settings.MOSCOW_PK)))
|
|
|
|
def get_r_cities_pks(self):
|
|
return [x.pk for x in self.get_r_cities()]
|
|
|
|
def get_f_countries(self):
|
|
return self.get_or_fillattr('_f_countries', lambda: list(self.f_countries.language().all()))
|
|
|
|
def get_areas(self):
|
|
return self.get_or_fillattr('_areas', lambda: list(self.area.language().all()))
|
|
|
|
def get_areas_pks(self):
|
|
return [x.pk for x in self.get_areas()]
|
|
|
|
def build_dailymail_preheader(self, preheader_ctx=None):
|
|
preheader_ctx = preheader_ctx or {}
|
|
themes = preheader_ctx.get('themes')
|
|
themes_string = ''
|
|
if themes:
|
|
themes_string = u'по теме ' + themes
|
|
geo = []
|
|
geo_string = u''
|
|
if self.moscow:
|
|
geo.append(u'Москве')
|
|
if self.russia:
|
|
if not self.get_r_cities():
|
|
geo.append(u'России')
|
|
else:
|
|
for city in self.get_r_cities()[:3]:
|
|
try:
|
|
geo.append(morph.parse(city.name)[0].inflect({'loct'}).word.capitalize())
|
|
except:
|
|
pass
|
|
if self.foreign:
|
|
if not self.get_f_countries() and not self.get_areas():
|
|
geo.append(u'зарубежом')
|
|
else:
|
|
for x in list(chain(self.get_f_countries(), self.get_areas()))[:3]:
|
|
geo.append(morph.parse(x.name)[0].inflect({'loct'}).word.capitalize())
|
|
if geo:
|
|
geo_string = u'в ' + u', '.join(geo[:3])
|
|
if len(geo) > 3:
|
|
geo_string += u' и др. местам'
|
|
return u' '.join(filter(lambda x: x, [u'Все главные события', themes_string, geo_string]))
|
|
|
|
def get_announce_context_v2(self, date=None):
|
|
# import error
|
|
from emencia.django.newsletter.utils.tokens import tokenize
|
|
|
|
date = date or datetime.today().date()
|
|
activate('ru')
|
|
ctx = {
|
|
'recommended': [],
|
|
'moscow': [],
|
|
'russia': [],
|
|
'foreign': [],
|
|
'news': [],
|
|
'blog': [],
|
|
'mailing_settings_url': reverse('newsletter-authmailingsettings', args=tokenize(self)),
|
|
}
|
|
th_tg_filter_params = []
|
|
th_tg_structure = {}
|
|
th_tg_filter = None
|
|
for tag, theme in self.tags.values_list('pk', 'theme_id'):
|
|
th_tg_structure.setdefault(theme, []).append(tag)
|
|
for theme in self.themes.values_list('pk', flat=True):
|
|
th_tg_structure.setdefault(theme, [])
|
|
if th_tg_structure:
|
|
for theme, tags in th_tg_structure.iteritems():
|
|
p = {'theme': theme}
|
|
if tags:
|
|
p.update({'tag__in': tags})
|
|
th_tg_filter_params.append(SQ(**p))
|
|
th_tg_filter = reduce(operator.or_, th_tg_filter_params)
|
|
|
|
# recommended
|
|
paid_recommend = NewsletterRecommend.objects.filter(
|
|
fr__lte=date,
|
|
to__gte=date)
|
|
recommended = SearchQuerySet().models(Exposition, Conference)\
|
|
.filter(expohit=True, data_begin__gte=date)
|
|
if th_tg_filter:
|
|
paid_recommend = list(paid_recommend.filter(Q(theme__in=th_tg_structure.keys()) | Q(theme__isnull=True)))
|
|
recommended = recommended.filter(th_tg_filter)
|
|
try:
|
|
ctx['recommended'] = choice(paid_recommend).event()
|
|
except:
|
|
try:
|
|
ctx['recommended'] = recommended[0].object
|
|
except:
|
|
pass
|
|
|
|
# moscow
|
|
if self.moscow:
|
|
moscow_sqs = SearchQuerySet().models(Exposition, Conference)\
|
|
.filter(city_id=Exact(settings.MOSCOW_PK), data_begin__gte=date)
|
|
if th_tg_filter is not None:
|
|
moscow_sqs = moscow_sqs.filter(th_tg_filter)
|
|
ctx['moscow'] = moscow_sqs.order_by('data_begin')[:4]
|
|
moscow_filter_url = QueryDict('', mutable=True)
|
|
moscow_filter_url.setlist('theme', th_tg_structure.keys())
|
|
moscow_filter_url.setlist('tag', list(chain(*th_tg_structure.values())))
|
|
moscow_filter_url['city'] = settings.MOSCOW_PK
|
|
moscow_filter_url['date_from'] = date.strftime('%d.%m.%Y')
|
|
ctx['moscow_filter_url'] = mark_safe(moscow_filter_url.urlencode())
|
|
|
|
# russia
|
|
if self.russia:
|
|
r_cities = set(self.get_r_cities_pks())
|
|
r_date = date + relativedelta.relativedelta(months=1)
|
|
russia_sqs = SearchQuerySet().models(Exposition, Conference) \
|
|
.filter(country_id=Exact(settings.RUSSIA_PK), data_begin__gte=r_date) \
|
|
# .exclude(city_id=Exact(settings.MOSCOW_PK))
|
|
# if r_cities or self.moscow:
|
|
# russia_sqs = russia_sqs.exclude(city_id=Exact(settings.MOSCOW_PK))
|
|
if r_cities:
|
|
russia_sqs = russia_sqs.filter(city_id__in=r_cities)
|
|
if th_tg_filter is not None:
|
|
russia_sqs = russia_sqs.filter(th_tg_filter)
|
|
ctx['russia'] = russia_sqs.order_by('data_begin')[:3]
|
|
russia_filter_url = QueryDict('', mutable=True)
|
|
russia_filter_url.setlist('theme', th_tg_structure.keys())
|
|
russia_filter_url.setlist('tag', list(chain(*th_tg_structure.values())))
|
|
russia_filter_url['country'] = settings.RUSSIA_PK
|
|
russia_filter_url['date_from'] = r_date.strftime('%d.%m.%Y')
|
|
ctx['russia_filter_url'] = mark_safe(russia_filter_url.urlencode())
|
|
|
|
# foreign
|
|
if self.foreign:
|
|
f_countries_cleaned = list(self.f_countries.order_by('pk').values_list('pk', 'area_id'))
|
|
areas = set(self.get_areas_pks())
|
|
areas_with_country = set()
|
|
f_countries = set()
|
|
for area, group in groupby(f_countries_cleaned, lambda x: x[1]):
|
|
areas_with_country.add(area)
|
|
f_countries.update(map(lambda x: x[0], group))
|
|
full_areas = areas.difference(areas_with_country)
|
|
params_list = []
|
|
if f_countries:
|
|
params_list.append(SQ(country_id__in=f_countries))
|
|
if full_areas:
|
|
params_list.append(SQ(area_id__in=full_areas))
|
|
foreign_sqs = SearchQuerySet().models(Exposition, Conference)\
|
|
.filter(data_begin__gte=date)
|
|
if params_list:
|
|
foreign_sqs = foreign_sqs.filter(reduce(operator.or_, params_list))
|
|
if th_tg_filter is not None:
|
|
foreign_sqs = foreign_sqs.filter(th_tg_filter)
|
|
ctx['foreign'] = foreign_sqs.order_by('data_begin')[:4]
|
|
foreign_filter_url = QueryDict('', mutable=True)
|
|
foreign_filter_url.setlist('theme', th_tg_structure.keys())
|
|
foreign_filter_url.setlist('tag', list(chain(*th_tg_structure.values())))
|
|
foreign_filter_url.setlist('country', f_countries)
|
|
foreign_filter_url['date_from'] = date.strftime('%d.%m.%Y')
|
|
ctx['foreign_filter_url'] = mark_safe(foreign_filter_url.urlencode())
|
|
|
|
# news
|
|
if self.content_news:
|
|
news = Article.objects.news().filter(draft=False)
|
|
if self.last_mailing_date:
|
|
news = news.filter(publish_date__gte=self.last_mailing_date)
|
|
try:
|
|
ctx['news'] = news.order_by('-publish_date')[0]
|
|
except:
|
|
pass
|
|
|
|
# blog
|
|
if self.content_articles:
|
|
blog = Article.objects.blogs()
|
|
if self.last_mailing_date:
|
|
blog = blog.filter(publish_date__gte=self.last_mailing_date)
|
|
try:
|
|
ctx['blog'] = blog.order_by('-publish_date')[0]
|
|
except:
|
|
pass
|
|
|
|
# import pdb; pdb.set_trace()
|
|
return ctx
|
|
|
|
def get_announce_context(self):
|
|
activate('ru')
|
|
expo = Exposition.enable.upcoming()
|
|
conf = Conference.enable.upcoming()
|
|
themes = list(self.contactsettings.theme.all())
|
|
|
|
# making query
|
|
params = Q()
|
|
|
|
_countries = set(self.contactsettings.country.all().values_list('pk', flat=True))
|
|
countries = set()
|
|
# countries_overall = set()
|
|
_cities = self.contactsettings.city.all().values_list('pk', 'country_id')
|
|
cities = set()
|
|
cities_overall = set()
|
|
|
|
for city, country_id in _cities:
|
|
if country_id in _countries:
|
|
cities.add(city)
|
|
countries.add(country_id)
|
|
else:
|
|
cities_overall.add(city)
|
|
countries_overall = _countries.difference(countries)
|
|
|
|
if countries_overall:
|
|
params |= Q(country_id__in=countries_overall)
|
|
|
|
if cities_overall:
|
|
params |= Q(city_id__in=cities_overall)
|
|
|
|
if countries and cities:
|
|
params |= Q(city_id__in=cities, country_id__in=countries)
|
|
|
|
if themes:
|
|
theme_ids = set([item.id for item in themes])
|
|
params &= Q(theme__in=theme_ids)
|
|
|
|
expo = list(expo.filter(params)[:5])
|
|
conf = list(conf.filter(params)[:5])
|
|
|
|
if expo or conf:
|
|
context = {
|
|
'themes': themes,
|
|
'expo': expo,
|
|
'conf': conf,
|
|
}
|
|
if expo:
|
|
context['expo_places'] = u', '.join(set([x.country.inflect for x in expo]))
|
|
if conf:
|
|
context['conf_places'] = u', '.join(set([x.country.inflect for x in conf]))
|
|
else:
|
|
# no events for announce
|
|
return None
|
|
|
|
news = list(Article.objects.news()
|
|
.filter(publish_date__gt=datetime.now()-timedelta(days=7), theme__in=theme_ids)
|
|
.distinct()[:3])
|
|
if news:
|
|
context['news'] = news
|
|
|
|
blogs = list(Article.objects.blogs()
|
|
.filter(publish_date__gt=datetime.now()-timedelta(days=7))
|
|
.distinct()[:3])
|
|
if blogs:
|
|
context['blogs'] = blogs
|
|
return context
|
|
|
|
def __unicode__(self):
|
|
if self.first_name and self.last_name:
|
|
contact_name = '%s %s' % (self.last_name, self.first_name)
|
|
else:
|
|
contact_name = self.email
|
|
return contact_name
|
|
|
|
def send_activation(self):
|
|
user = self
|
|
site = Site.objects.get_current()
|
|
ctx_dict = {'activation_key': self.activation_code,
|
|
'site': site,
|
|
'user': user}
|
|
subject = render_to_string(self.activation_subject,
|
|
ctx_dict)
|
|
# Email subject *must not* contain newlines
|
|
subject = ''.join(subject.splitlines())
|
|
|
|
message = get_template(self.activation_template).render(Context(ctx_dict))
|
|
msg = EmailMessage(subject, message, settings.DEFAULT_FROM_EMAIL, [user.email])
|
|
msg.content_subtype = "html"
|
|
images =(('/img/logo_reg.png', 'logo'),
|
|
# ('/img/soc-medias/sm-icon-rss.png', 'rss'),
|
|
('/img/soc-medias/sm-icon-fb.png', 'fb'),
|
|
('/img/soc-medias/sm-icon-lin.png', 'linkedin'),
|
|
('/img/soc-medias/sm-icon-vk.png', 'vk'),
|
|
('/img/soc-medias/sm-icon-twit.png', 'twit'))
|
|
for img in images:
|
|
fp = open(settings.STATIC_ROOT+img[0], 'rb')
|
|
msg_img = MIMEImage(fp.read())
|
|
fp.close()
|
|
msg_img.add_header('Content-ID', '<'+img[1]+'>')
|
|
msg.attach(msg_img)
|
|
|
|
msg.send()
|
|
|
|
class Meta:
|
|
ordering = ('-modification_date',)
|
|
verbose_name = _('contact')
|
|
verbose_name_plural = _('contacts')
|
|
|
|
|
|
class ContactSettings(models.Model):
|
|
WEEK = 1 # every week
|
|
WEEK_2 = 2 # every 2 weeks
|
|
MONTH = 3 # every 4 weeks
|
|
|
|
PERIODIC_CHOICES = [(WEEK, _(u'Раз в неделю')),
|
|
(WEEK_2, _(u'Раз в 2 недели')),
|
|
(MONTH, _(u'Раз в месяц'))
|
|
]
|
|
|
|
contact = models.OneToOneField(Contact)
|
|
periodic = models.PositiveIntegerField(verbose_name=_(u'Периодичность отправки'),
|
|
choices=PERIODIC_CHOICES, default=WEEK)
|
|
exponent_practicum = models.BooleanField(default=True, verbose_name=_(u'Практикум экспонента'))
|
|
organiser_practicum = models.BooleanField(default=False, verbose_name=_(u'Практикум организатора событий'))
|
|
theme = models.ManyToManyField('theme.Theme', blank=True, null=True, verbose_name=_(u'Тематики'))
|
|
area = models.ManyToManyField('country.Area', blank=True, null=True, verbose_name=_(u'Географические зоны'))
|
|
country = models.ManyToManyField('country.Country', blank=True, null=True, verbose_name=_(u'Страны'))
|
|
city = models.ManyToManyField('city.City', blank=True, null=True, verbose_name=_(u'Города'))
|
|
|
|
def save_mailing_lists(self):
|
|
practicum_exponent = MailingList.objects.get(id=2)
|
|
practicum_organiser = MailingList.objects.get(id=3)
|
|
contact = self.contact
|
|
if self.exponent_practicum:
|
|
practicum_exponent.subscribers.add(contact)
|
|
else:
|
|
practicum_exponent.subscribers.remove(contact)
|
|
if self.organiser_practicum:
|
|
practicum_organiser.subscribers.add(contact)
|
|
else:
|
|
practicum_organiser.subscribers.remove(contact)
|
|
|
|
def save(self, *args, **kwargs):
|
|
super(ContactSettings, self).save(*args, **kwargs)
|
|
try:
|
|
self.save_mailing_lists()
|
|
except:
|
|
pass
|
|
|
|
|
|
class MailingList(models.Model):
|
|
"""Mailing list"""
|
|
name = models.CharField(_('name'), max_length=255)
|
|
description = models.TextField(_('description'), blank=True)
|
|
|
|
subscribers = models.ManyToManyField(Contact, verbose_name=_('subscribers'),
|
|
related_name='mailinglist_subscriber')
|
|
unsubscribers = models.ManyToManyField(Contact, verbose_name=_('unsubscribers'),
|
|
related_name='mailinglist_unsubscriber',
|
|
null=True, blank=True)
|
|
announce = models.BooleanField(default=False)
|
|
announce_public_day = models.IntegerField(default=2)
|
|
creation_date = models.DateTimeField(_('creation date'), auto_now_add=True)
|
|
modification_date = models.DateTimeField(_('modification date'), auto_now=True)
|
|
theme_for_filter = models.ManyToManyField('theme.Theme', verbose_name=_(u'Тематики'), null=True, blank=True)
|
|
|
|
def subscribers_count(self):
|
|
return self.subscribers.all().count()
|
|
subscribers_count.short_description = _('subscribers')
|
|
|
|
def unsubscribers_count(self):
|
|
return self.unsubscribers.all().count()
|
|
unsubscribers_count.short_description = _('unsubscribers')
|
|
|
|
def expedition_set(self):
|
|
unsubscribers_id = self.unsubscribers.values_list('id', flat=True)
|
|
if not self.announce:
|
|
return self.subscribers.valid_subscribers().exclude(
|
|
id__in=unsubscribers_id)
|
|
else:
|
|
return self.subscribers.valid_subscribers().exclude(
|
|
id__in=unsubscribers_id).filter(contactsettings__theme__isnull=False).distinct()
|
|
|
|
def __unicode__(self):
|
|
return self.name
|
|
|
|
def check_day(self, day):
|
|
weekday = day.weekday()
|
|
if self.announce_public_day > 0:
|
|
return (self.announce_public_day - 1) == weekday
|
|
else:
|
|
return weekday == 6
|
|
|
|
def generate_announce(self, day):
|
|
# simple data
|
|
sending_date = day + timedelta(days=1)
|
|
start_sending_time = datetime.combine(sending_date, datetime.min.time())
|
|
title = u'Анонс событий на %s' % sending_date.strftime('%Y-%m-%d')
|
|
|
|
newsletter = Newsletter(status=Newsletter.WAITING, sending_date=start_sending_time,
|
|
title=title, mailing_list=self, slug=translit_with_separator(title))
|
|
|
|
newsletter.save()
|
|
# add default attacments copied from default announce newsletter
|
|
default_newsletter = Newsletter.objects.get(id=1)
|
|
attacments = list(default_newsletter.attachment_set.all())
|
|
for a in attacments:
|
|
a.id = None
|
|
a.newsletter = newsletter
|
|
a.save()
|
|
|
|
return newsletter
|
|
|
|
def generate_announce_newsletter(self, day):
|
|
if self.check_day(day):
|
|
return self.generate_announce(day)
|
|
|
|
return None
|
|
|
|
class Meta:
|
|
ordering = ('-creation_date',)
|
|
verbose_name = _('mailing list')
|
|
verbose_name_plural = _('mailing lists')
|
|
|
|
|
|
class Newsletter(models.Model):
|
|
"""Newsletter to be sended to contacts"""
|
|
DRAFT = 0
|
|
WAITING = 1
|
|
SENDING = 2
|
|
SENT = 4
|
|
CANCELED = 5
|
|
|
|
STATUS_CHOICES = ((DRAFT, _('draft')),
|
|
(WAITING, _('waiting sending')),
|
|
(SENDING, _('sending')),
|
|
(SENT, _('sent')),
|
|
(CANCELED, _('canceled')),
|
|
)
|
|
|
|
# выбираем вариант письма для финальной рассылки (А-В тестирование)
|
|
A = 1
|
|
B = 2
|
|
AB_FINAL_CHOICE = (
|
|
(A, _(u'Письмо типа А')),
|
|
(B, _(u'Письмо типа B')),
|
|
)
|
|
|
|
preheader = models.CharField(_(u'Прехедер'), max_length=255, null=True, blank=True)
|
|
preheader2 = models.CharField(_(u'Прехедер B'), max_length=255, null=True, blank=True)
|
|
title = models.CharField(_('title'), max_length=255,
|
|
help_text=_('You can use the "{{ UNIQUE_KEY }}" variable ' \
|
|
'for unique identifier within the newsletter\'s title.'))
|
|
title2 = models.CharField(_('title B'), max_length=255,
|
|
help_text=_('You can use the "{{ UNIQUE_KEY }}" variable ' \
|
|
'for unique identifier within the newsletter\'s title.'), null=True, blank=True)
|
|
content = models.TextField(_('content'), help_text=_('Or paste an URL.'), default='')
|
|
content2 = models.TextField(_('content B'), help_text=_('Or paste an URL.'), default='')
|
|
ab_testing = models.BooleanField(_('A-B testing'), default=False)
|
|
ab_first_stage = models.PositiveSmallIntegerField(_(u'% в первый этап'), null=True, blank=True, validators=[MinValueValidator(5), MaxValueValidator(100)])
|
|
ab_final_stage = models.BooleanField(_(u'Финальная рассылка'), default=False)
|
|
ab_final_choice = models.PositiveSmallIntegerField(_(u'Вариант в финальную рассылку'), choices=AB_FINAL_CHOICE, null=True, blank=True)
|
|
|
|
mailing_list = models.ForeignKey(MailingList, verbose_name=_('mailing list'), null=True, blank=True)
|
|
theme_for_filter = models.ForeignKey('theme.Theme', verbose_name=_(u'Ограничение по тематике'), null=True, blank=True)
|
|
test_contacts = models.ManyToManyField(Contact, verbose_name=_('test contacts'),
|
|
blank=True, null=True)
|
|
|
|
server = models.ForeignKey(SMTPServer, verbose_name=_('smtp server'),
|
|
default=lambda: SMTPServer.objects.get(id=DEFAULT_SMPTSERVER_ID))
|
|
header_sender = models.CharField(_('sender'), max_length=255,
|
|
default=DEFAULT_HEADER_SENDER)
|
|
header_reply = models.CharField(_('reply to'), max_length=255,
|
|
default=DEFAULT_HEADER_REPLY)
|
|
|
|
status = models.IntegerField(_('status'), choices=STATUS_CHOICES, default=DRAFT)
|
|
sending_date = models.DateTimeField(_('sending date'), default=datetime.now)
|
|
|
|
slug = models.SlugField(help_text=_('Used for displaying the newsletter on the site.'),
|
|
unique=True)
|
|
dailymail = models.BooleanField(_(u'Ежедневная рассылка'), blank=True, default=False)
|
|
banner = models.ImageField(_(u'Баннер'), upload_to=gen_path, null=True, blank=True)
|
|
banner_url = models.URLField(_(u'ссылка для баннера'), null=True, blank=True)
|
|
|
|
creation_date = models.DateTimeField(_('creation date'), auto_now_add=True)
|
|
modification_date = models.DateTimeField(_('modification date'), auto_now=True)
|
|
|
|
def mails_sent(self):
|
|
return self.contactmailingstatus_set.filter(
|
|
status__in=[ContactMailingStatus.SENT, ContactMailingStatus.AB_WAITING],
|
|
).count()
|
|
|
|
def can_cancel(self):
|
|
return self.status in [self.WAITING, self.SENDING]
|
|
|
|
@models.permalink
|
|
def get_absolute_url(self):
|
|
return ('newsletter_newsletter_preview', (self.slug,))
|
|
|
|
@models.permalink
|
|
def get_historic_url(self):
|
|
return ('newsletter_newsletter_historic', (self.slug,))
|
|
|
|
@models.permalink
|
|
def get_statistics_url(self):
|
|
return ('newsletter_newsletter_statistics', (self.slug,))
|
|
|
|
def get_status_text(self):
|
|
for item in self.STATUS_CHOICES:
|
|
if self.status == item[0]:
|
|
return unicode(item[1])
|
|
return 'bug'
|
|
#
|
|
# def get_expedition_count(self):
|
|
# add_sent = False
|
|
# if self.dailymail:
|
|
# qs = self.get_dailymail_subscribers()
|
|
# add_sent = True
|
|
# else:
|
|
# qs = self.mailing_list.expedition_set()
|
|
# if self.theme_for_filter:
|
|
# # qs = qs
|
|
# status_params = {'status': ContactMailingStatus.SENT}
|
|
# if self.ab_testing and not self.ab_final_stage:
|
|
# status_params = {'status__in': [ContactMailingStatus.SENT, ContactMailingStatus.AB_WAITING]}
|
|
# another_mailing_sent = ContactMailingStatus.objects.filter(
|
|
# newsletter__mailing_list=self.mailing_list,
|
|
# creation_date__gte=datetime.now() - timedelta(days=5),
|
|
# **status_params,
|
|
# ).exclude(newsletter_id=self.pk).values_list('contact__id', flat=True)
|
|
# qs = qs\
|
|
# .filter(contactsettings__theme=self.theme_for_filter)\
|
|
# .exclude(id__in=another_mailing_sent)
|
|
# count = qs.count()
|
|
# if add_sent:
|
|
# count += self.mails_sent()
|
|
# return count
|
|
|
|
def clone(self):
|
|
clone = copy.copy(self)
|
|
clone.id = None
|
|
clone.title+=u"_копия"
|
|
clone.slug += "c"
|
|
clone.status = Newsletter.DRAFT
|
|
clone.ab_final_stage = False
|
|
clone.ab_final_choice = None
|
|
clone.save()
|
|
|
|
cloned_attachments = [copy.copy(a) for a in list(self.attachment_set.all())]
|
|
for a in cloned_attachments:
|
|
a.id = None
|
|
a.save()
|
|
a.newsletter = clone
|
|
a.save()
|
|
|
|
clone.test_contacts = self.test_contacts.all()
|
|
clone.save()
|
|
return clone
|
|
|
|
def __unicode__(self):
|
|
return self.title
|
|
|
|
class Meta:
|
|
ordering = ('-creation_date',)
|
|
verbose_name = _('newsletter')
|
|
verbose_name_plural = _('newsletters')
|
|
permissions = (('can_change_status', 'Can change status'),)
|
|
|
|
def get_dailymail_subscribers(self):
|
|
periodic_filter = []
|
|
periodic_filter.append(Q(
|
|
periodic=Contact.PERIODIC_CHOICES.WEEK,
|
|
last_mailing_date__lte=self.sending_date - timedelta(days=7),
|
|
))
|
|
periodic_filter.append(Q(
|
|
periodic=Contact.PERIODIC_CHOICES.WEEK_2,
|
|
last_mailing_date__lte=self.sending_date - timedelta(days=14),
|
|
))
|
|
month = relativedelta.relativedelta(month=1)
|
|
periodic_filter.append(Q(
|
|
periodic=Contact.PERIODIC_CHOICES.MONTH,
|
|
last_mailing_date__lte=self.sending_date - month,
|
|
))
|
|
periodic_filter.append(Q(last_mailing_date__isnull=True))
|
|
qs = Contact.objects.valid_subscribers().filter(
|
|
reduce(operator.or_, periodic_filter),
|
|
periodic_day=self.sending_date.isoweekday(),
|
|
dailymailing=True,
|
|
)
|
|
|
|
r = redis.StrictRedis()
|
|
l = List(key='newsletter_sending_contacts', redis=r)
|
|
qs = qs.exclude(pk__in=l)
|
|
|
|
return qs
|
|
|
|
|
|
class NewsletterRecommend(models.Model):
|
|
theme = models.ManyToManyField('theme.Theme', verbose_name=_(u'Тема'), null=True, blank=True)
|
|
exposition = models.ForeignKey('exposition.Exposition', verbose_name=_(u'Выставка'), null=True)
|
|
conference = models.ForeignKey('conference.Conference', verbose_name=_(u'Конференция'), null=True)
|
|
fr = models.DateField(_(u'Первый день показов'), default=lambda: date.today())
|
|
to = models.DateField(_(u'Последний день показов'), blank=True, null=True)
|
|
|
|
creation_date = models.DateTimeField(_('creation date'), auto_now_add=True)
|
|
|
|
def __unicode__(self):
|
|
return u'{0} - {1}'.format(self.fr, self.to)
|
|
|
|
class Meta:
|
|
ordering = ('-creation_date',)
|
|
verbose_name = _(u'Рекомендация')
|
|
verbose_name_plural = _(u'Рекомендации')
|
|
permissions = (('can_change_status', 'Can change status'),)
|
|
|
|
def event(self):
|
|
return self.exposition or self.conference
|
|
|
|
|
|
class Link(models.Model):
|
|
"""Link sended in a newsletter"""
|
|
title = models.CharField(_('title'), max_length=255)
|
|
url = models.CharField(_('url'), max_length=255)
|
|
|
|
creation_date = models.DateTimeField(_('creation date'), auto_now_add=True)
|
|
|
|
def get_absolute_url(self):
|
|
return self.url
|
|
|
|
def __unicode__(self):
|
|
return self.title
|
|
|
|
class Meta:
|
|
ordering = ('-creation_date',)
|
|
verbose_name = _('link')
|
|
verbose_name_plural = _('links')
|
|
|
|
|
|
class Attachment(models.Model):
|
|
"""Attachment file in a newsletter"""
|
|
|
|
def get_newsletter_storage_path(self, filename):
|
|
filename = force_unicode(filename)
|
|
return '/'.join([BASE_PATH, self.newsletter.slug, filename])
|
|
|
|
newsletter = models.ForeignKey(Newsletter, verbose_name=_('newsletter'))
|
|
title = models.CharField(_('title'), max_length=255)
|
|
file_attachment = models.FileField(_('file to attach'), max_length=255,
|
|
upload_to=get_newsletter_storage_path)
|
|
|
|
class Meta:
|
|
verbose_name = _('attachment')
|
|
verbose_name_plural = _('attachments')
|
|
|
|
def __unicode__(self):
|
|
return self.title
|
|
|
|
def get_absolute_url(self):
|
|
return self.file_attachment.url
|
|
|
|
|
|
class ContactMailingStatus(models.Model):
|
|
"""Status of the reception"""
|
|
SENT_TEST = -1
|
|
SENT = 0
|
|
ERROR = 1
|
|
INVALID = 2
|
|
OPENED = 4
|
|
OPENED_ON_SITE = 5
|
|
LINK_OPENED = 6
|
|
UNSUBSCRIPTION = 7
|
|
ANNOUNCE_NO_DATA = 8
|
|
AB_WAITING = 9
|
|
|
|
STATUS_CHOICES = ((SENT_TEST, _('sent in test')),
|
|
(SENT, _('sent')),
|
|
(ERROR, _('error')),
|
|
(INVALID, _('invalid email')),
|
|
(OPENED, _('opened')),
|
|
(OPENED_ON_SITE, _('opened on site')),
|
|
(LINK_OPENED, _('link opened')),
|
|
(UNSUBSCRIPTION, _('unsubscription')),
|
|
(ANNOUNCE_NO_DATA, _('announce no data')),
|
|
(AB_WAITING, _('waiting for A-B final mailing')),
|
|
)
|
|
|
|
newsletter = models.ForeignKey(Newsletter, verbose_name=_('newsletter'))
|
|
contact = models.ForeignKey(Contact, verbose_name=_('contact'))
|
|
status = models.IntegerField(_('status'), choices=STATUS_CHOICES)
|
|
link = models.ForeignKey(Link, verbose_name=_('link'),
|
|
blank=True, null=True)
|
|
ab = models.PositiveSmallIntegerField(_(u'Вариант А-В'), choices=Newsletter.AB_FINAL_CHOICE, null=True, blank=True)
|
|
|
|
creation_date = models.DateTimeField(_('creation date'), auto_now_add=True)
|
|
|
|
def get_verbose_status(self):
|
|
verbose = ''
|
|
for s in self.STATUS_CHOICES:
|
|
if s[0] == self.status:
|
|
verbose = unicode(s[1])
|
|
break
|
|
if self.status == ContactMailingStatus.LINK_OPENED:
|
|
verbose +=": <a href='%s'>%s</a>"% (self.link.url, self.link.url)
|
|
return verbose
|
|
|
|
def __unicode__(self):
|
|
return '%s : %s : %s' % (self.newsletter.__unicode__(),
|
|
self.contact.__unicode__(),
|
|
self.get_status_display())
|
|
|
|
class Meta:
|
|
ordering = ('-creation_date',)
|
|
verbose_name = _('contact mailing status')
|
|
verbose_name_plural = _('contact mailing statuses')
|
|
|
|
|
|
class WorkGroup(models.Model):
|
|
"""Work Group for privatization of the ressources"""
|
|
name = models.CharField(_('name'), max_length=255)
|
|
group = models.ForeignKey(Group, verbose_name=_('permissions group'))
|
|
|
|
contacts = models.ManyToManyField(Contact, verbose_name=_('contacts'),
|
|
blank=True, null=True)
|
|
mailinglists = models.ManyToManyField(MailingList, verbose_name=_('mailing lists'),
|
|
blank=True, null=True)
|
|
newsletters = models.ManyToManyField(Newsletter, verbose_name=_('newsletters'),
|
|
blank=True, null=True)
|
|
|
|
def __unicode__(self):
|
|
return self.name
|
|
|
|
class Meta:
|
|
verbose_name = _('workgroup')
|
|
verbose_name_plural = _('workgroups')
|
|
|
|
|
|
class PopupCount(models.Model):
|
|
theme = models.ForeignKey('theme.Theme', null=True)
|
|
date = models.DateField(auto_created=True)
|
|
cnt = models.PositiveIntegerField(default=0)
|
|
|
|
class Meta:
|
|
unique_together = ('theme', 'date')
|
|
ordering = ['-date']
|
|
|
|
def get_subscr(self):
|
|
qs = Contact.objects.filter(creation_date__startswith=self.date)
|
|
return qs.count()
|
|
|
|
def get_active(self):
|
|
qs = Contact.objects.filter(creation_date__startswith=self.date, activated=True)
|
|
return qs.count()
|
|
|