1460: Этап №5: Дополнительно - доработка А-Б тестов

1457: Этап №5: Со скайпа - Импорт подписчиков
remotes/origin/stage5
Alexander Burdeiny 10 years ago
parent 8cbe868bcf
commit 5919f1e6bf
  1. 1
      Makefile
  2. 2
      city/search_indexes.py
  3. 55
      emencia/django/newsletter/admin_forms.py
  4. 31
      emencia/django/newsletter/forms.py
  5. 90
      emencia/django/newsletter/mailer.py
  6. 4
      emencia/django/newsletter/management/commands/send_newsletter.py
  7. 493
      emencia/django/newsletter/migrations/0006_auto__add_field_newsletter_ab_final_stage__add_field_newsletter_ab_fin.py
  8. 21
      emencia/django/newsletter/models.py
  9. 40
      emencia/django/newsletter/views/admin_views.py
  10. 7
      fabfile.py
  11. 42
      templates/admin/newsletters/newsletter_history.html
  12. 6
      templates/admin/newsletters/newsletter_object.html
  13. 78
      templates/admin/newsletters/newsletter_stat.html

@ -20,6 +20,7 @@ syncdb:
@PYTHONPATH=$(PYTHONPATH):. DJANGO_SETTINGS_MODULE=$(project_name).settings ipython manage.py migrate
run:
-/opt/solr/bin/solr start -p 8983
@PYTHONPATH=$(PYTHONPATH):. DJANGO_SETTINGS_MODULE=$(project_name).settings ipython manage.py runserver
run-public:

@ -5,7 +5,7 @@ from models import City
from functions.search_mixin import ExpoSearchMixin
class CountryIndex(indexes.SearchIndex, indexes.Indexable, ExpoSearchMixin):
class CityIndex(indexes.SearchIndex, indexes.Indexable, ExpoSearchMixin):
text = indexes.CharField(document=True, use_template=True)
url = indexes.CharField()
content_auto = indexes.EdgeNgramField()

@ -1,11 +1,19 @@
# -*- coding: utf-8 -*-
import xlrd
from city.models import City
from ckeditor.widgets import CKEditorWidget
from country.models import Area, Country
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.utils import translation
from ckeditor.widgets import CKEditorWidget
from emencia.django.newsletter.models import Contact, ContactSettings, MailingList, Newsletter, Attachment
from city.models import City
from country.models import Country, Area
from django.utils.translation import ugettext_lazy as _
from emencia.django.newsletter.models import (
Attachment,
Contact,
ContactSettings,
MailingList,
Newsletter
)
from theme.models import Theme
class ContactSettingsForm(forms.ModelForm):
@ -61,9 +69,6 @@ class ContactSettingsForm(forms.ModelForm):
return City.objects.none()
import xlrd
from theme.models import Theme
class MailingListForm(forms.ModelForm):
excel_file = forms.FileField(label=_(u'Импортировать подписчиков'), required=False)
theme_for_filter = forms.MultipleChoiceField(label=_(u'Тематики'), required=False,
@ -81,11 +86,15 @@ class MailingListForm(forms.ModelForm):
contact_list = list(Contact.objects.all())
book = xlrd.open_workbook(file_contents=f.read())
sheet = book.sheet_by_index(0)
excel_emails = [sheet.row_values(row_number)[0] for row_number in range(1, sheet.nrows)]
excel_emails = {sheet.row_values(row_number)[0]: sheet.row_values(row_number)[1] for row_number in range(1, sheet.nrows)}
valid_contacts = []
for contact in contact_list:
if contact.email in excel_emails:
valid_contacts.append(contact)
# for contact in contact_list:
# if contact.email in excel_emails:
# valid_contacts.append(contact)
for email, first_name in excel_emails.iteritems():
valid_contacts.append(Contact.objects.create(
email=email, first_name=first_name,
valid=True, activated=True))
ml.subscribers = valid_contacts
ml.save()
return ml
@ -99,12 +108,32 @@ class NewsletterForm(forms.ModelForm):
class Meta:
model = Newsletter
fields = ('preheader', 'title', 'content', 'ab_testing', 'title2', 'content2', 'mailing_list', 'theme_for_filter', 'test_contacts', 'header_sender',
fields = ('preheader', 'title', 'content', 'ab_testing', 'ab_first_stage', 'title2', 'content2', 'mailing_list', 'theme_for_filter', 'test_contacts', 'header_sender',
'header_reply', 'status', 'sending_date', 'slug')
widgets = {
'ab_first_stage': forms.widgets.TextInput(attrs={'type':'number', 'min': 5, 'max': 100}),
}
def __init__(self, *args, **kwargs):
super(NewsletterForm, self).__init__(*args, **kwargs)
# добавляем поле для возможности указания варианта финальной рассылки (А-В тестирование)
if self.instance and self.instance.ab_final_stage:
self._meta.fields = (
'preheader', 'title', 'content', 'ab_testing',
'ab_first_stage', 'title2', 'content2', 'mailing_list',
'theme_for_filter', 'test_contacts', 'header_sender',
'header_reply', 'ab_final_choice', 'status', 'sending_date',
'slug')
field = forms.ChoiceField(
label=_(u'Вариант в финальную рассылку'),
choices=Newsletter.AB_FINAL_CHOICE,
initial=self.instance.ab_final_choice or Newsletter.A)
self.fields.insert(self.fields.keyOrder.index('status'), 'ab_final_choice', field)
if self.instance and self.instance.mailing_list_id:
self.fields['theme_for_filter'].queryset = self.instance.mailing_list.theme_for_filter.all()
self.initial['ab_first_stage'] = self.initial.get('ab_first_stage') or 10
class AttachmentForm(forms.ModelForm):

@ -1,20 +1,28 @@
# -*- coding: utf-8 -*-
"""Forms for emencia.django.newsletter"""
from datetime import datetime, date, timedelta
from datetime import date, datetime, timedelta
import xlrd
from city.models import City
from country.models import Area, Country
from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Sum
from django.utils.translation import ugettext_lazy as _
from django.http import Http404
from django.core.exceptions import ValidationError
from django.utils import translation
from haystack.query import SearchQuerySet
from functions.search_forms import get_by_lang
from emencia.django.newsletter.models import Contact, ContactSettings, MailingList, PopupCount, ContactMailingStatus
from django.utils.translation import ugettext_lazy as _
from emencia.django.newsletter.models import (
Contact,
ContactMailingStatus,
ContactSettings,
MailingList,
Newsletter,
PopupCount
)
from functions.form_check import translit_with_separator as tr
from functions.search_forms import get_by_lang
from haystack.query import SearchQuerySet
from theme.models import Theme
from country.models import Country, Area
from city.models import City
class MailingListSubscriptionForm(forms.ModelForm):
@ -179,7 +187,6 @@ class ContactFilterForm(forms.Form):
return qs, title.replace('"', '\"').replace('\n', '')
import xlrd
COUNTRY_CHOICES = [(c.id, c.name) for c in list(set(Country.objects.language('ru').all()))]
COUNTRY_CHOICES.insert(0, ('', _(u'Страна')))
@ -378,13 +385,17 @@ class MailingStatusFilter(forms.Form):
status = forms.ChoiceField(choices=[('', _(u'Не выбрано'))] + [(item[0], item[1]) for item in ContactMailingStatus.STATUS_CHOICES],
required=False)
email = forms.CharField(required=False, widget=forms.TextInput(attrs={'placeholder': 'Email'}))
ab = forms.TypedChoiceField(required=False, choices=((0, _(u'Любое')), ) + Newsletter.AB_FINAL_CHOICE, coerce=int, empty_value=0)
def filter(self, newsletter):
status = self.cleaned_data.get('status')
email = self.cleaned_data.get('email')
ab = self.cleaned_data.get('ab')
qs = ContactMailingStatus.objects.select_related().filter(newsletter=newsletter)
if status:
qs = qs.filter(status=status)
if email:
qs = qs.filter(contact__email=email)
if ab:
qs = qs.filter(ab=ab)
return qs

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
"""Mailer for emencia.django.newsletter"""
import re
import sys
@ -99,10 +100,12 @@ class NewsLetterSender(object):
self.newsletter_template2 = None
self.title_template2 = None
self.ab_state = 'A'
self.ab_state = Newsletter.A
if newsletter.ab_testing == True:
self.newsletter_template2 = Template(self.newsletter.content2)
self.title_template2 = Template(self.newsletter.title2)
if self.newsletter.ab_final_stage:
self.ab_state = self.newsletter.ab_final_choice
self.title_template = Template(self.newsletter.title)
#
@ -258,9 +261,9 @@ class NewsLetterSender(object):
context = Context({'contact': contact,
'UNIQUE_KEY': ''.join(sample(UNIQUE_KEY_CHAR_SET,
UNIQUE_KEY_LENGTH))})
if self.newsletter.ab_testing == True and self.ab_state == 'A':
if self.newsletter.ab_testing and self.ab_state == Newsletter.A:
content = self.title_template.render(context)
elif self.newsletter.ab_testing == True and self.ab_state == 'B':
elif self.newsletter.ab_testing and self.ab_state == Newsletter.B:
content = self.title_template2.render(context)
else:
title = self.title_template.render(context)
@ -280,12 +283,10 @@ class NewsLetterSender(object):
context.update(announce_context)
content = template.render(context)
elif self.newsletter.ab_testing == True:
if self.ab_state == 'A':
if self.ab_state == Newsletter.A:
content = self.newsletter_template.render(context)
self.ab_state = 'B'
else:
content = self.newsletter_template2.render(context)
self.ab_state = 'A'
else:
content = self.newsletter_template.render(context)
@ -327,6 +328,8 @@ class NewsLetterSender(object):
self.newsletter.mails_sent() >= \
self.newsletter.mailing_list.expedition_set().count():
self.newsletter.status = Newsletter.SENT
if self.newsletter.ab_testing:
self.newsletter.ab_final_stage = True
if self.announce and not self.expedition_list:
self.newsletter.status = Newsletter.SENT
@ -361,13 +364,17 @@ class NewsLetterSender(object):
else:
qs = self.newsletter.mailing_list.expedition_set()
status_params = {'status': ContactMailingStatus.SENT}
if self.newsletter.ab_testing and not self.newsletter.ab_final_stage:
status_params = {'status__in': [ContactMailingStatus.SENT, ContactMailingStatus.AB_WAITING]}
if self.newsletter.theme_for_filter:
already_sent = \
ContactMailingStatus.objects.filter(
Q(newsletter=self.newsletter) | \
Q(newsletter__mailing_list=self.newsletter.mailing_list,
creation_date__gte=datetime.now() - timedelta(days=2)),
status=ContactMailingStatus.SENT,
**status_params
).values_list('contact__id', flat=True)
qs = qs.filter(contactsettings__theme=self.newsletter.theme_for_filter)
@ -375,8 +382,8 @@ class NewsLetterSender(object):
else:
already_sent = \
ContactMailingStatus.objects.filter(
status=ContactMailingStatus.SENT,
newsletter=self.newsletter
newsletter=self.newsletter,
**status_params
).values_list('contact__id', flat=True)
qs = qs.exclude(id__in=already_sent)
@ -399,8 +406,23 @@ class NewsLetterSender(object):
print >>sys.stderr, 'smtp connection raises %s' % exception
status = ContactMailingStatus.ERROR
ContactMailingStatus.objects.create(
newsletter=self.newsletter, contact=contact, status=status)
params = {
'newsletter': self.newsletter,
'contact': contact,
'status': status,
}
if self.newsletter.ab_testing and self.newsletter.ab_final_stage:
try:
contact = ContactMailingStatus.objects.get(
newsletter=self.newsletter, contact=contact,
status=ContactMailingStatus.AB_WAITING)
contact.status, contact.ab = status, self.ab_state
contact.save()
return
except ContactMailingStatus.DoesNotExist:
print('contactmailing status not found - creating...')
params.update({'ab': self.ab_state})
ContactMailingStatus.objects.create(**params)
class Mailer(NewsLetterSender):
@ -439,6 +461,7 @@ class Mailer(NewsLetterSender):
announce_context = None
try:
# pass
if send:
message = self.build_message(contact, announce_context)
self.smtp.sendmail(self.newsletter.header_sender,
@ -448,8 +471,16 @@ class Mailer(NewsLetterSender):
exception = e
else:
exception = None
self.update_ab_state()
# обновляем статус
self.update_contact_status(contact, exception, send)
# удаляем контакт из маркировки на второй этап
if self.newsletter.ab_testing and not self.newsletter.ab_final_stage:
try:
self.second_stage_expedition_list_ids.remove(contact.pk)
except ValueError:
pass
if SLEEP_BETWEEN_SENDING:
time.sleep(SLEEP_BETWEEN_SENDING)
@ -459,6 +490,9 @@ class Mailer(NewsLetterSender):
i += 1
# маркируем оставшиеся контакты на второй этап
self.mark_contacts_for_second_stage()
self.smtp.quit()
self.update_newsletter_status()
@ -469,10 +503,38 @@ class Mailer(NewsLetterSender):
@property
def expedition_list(self):
"""Build the expedition list"""
credits = self.newsletter.server.credits()
if credits <= 0:
self.credits = self.newsletter.server.credits()
if self.credits <= 0:
return []
return super(Mailer, self).expedition_list[:credits]
qs = super(Mailer, self).expedition_list
qs = qs[:self.credits]
self.second_stage_expedition_list_ids = []
if self.newsletter.ab_testing and not self.newsletter.ab_final_stage:
# сохраняем себе все айд контактов с текущей итерации,
# после отправки писем исключим все отправленные и поставим
# в ожидание второго этапа
self.second_stage_expedition_list_ids = list(qs.values_list('pk', flat=True))
print('всего контактов %s' % len(self.second_stage_expedition_list_ids))
# берем заданый процент от всех контактов в текущей итерации
_slice = int(float(len(self.second_stage_expedition_list_ids))/100 * (self.newsletter.ab_first_stage or 5))
qs = qs[:_slice]
return qs
def mark_contacts_for_second_stage(self):
if self.second_stage_expedition_list_ids:
func = lambda pk: ContactMailingStatus(
newsletter=self.newsletter, contact_id=pk,
status=ContactMailingStatus.AB_WAITING)
contact_ms = list(map(func, self.second_stage_expedition_list_ids))
ContactMailingStatus.objects.bulk_create(contact_ms)
print('Marked for second stage %s.' % len(self.second_stage_expedition_list_ids))
def update_ab_state(self):
if not self.newsletter.ab_testing or self.newsletter.ab_final_stage: return
self.ab_state = Newsletter.A if self.ab_state == Newsletter.B else Newsletter.B
@property
def can_send(self):

@ -18,7 +18,9 @@ class Command(NoArgsCommand):
activate(settings.LANGUAGE_CODE)
for newsletter in Newsletter.objects.exclude(
status=Newsletter.DRAFT).exclude(status=Newsletter.SENT):
status__in=[Newsletter.DRAFT,
Newsletter.SENT,
Newsletter.CANCELED,]):
mailer = Mailer(newsletter, verbose=verbose)
if mailer.can_send:

@ -0,0 +1,493 @@
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'Newsletter.ab_first_stage'
db.add_column(u'newsletter_newsletter', 'ab_first_stage',
self.gf('django.db.models.fields.PositiveSmallIntegerField')(null=True, blank=True),
keep_default=False)
# Adding field 'Newsletter.ab_final_stage'
db.add_column(u'newsletter_newsletter', 'ab_final_stage',
self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False)
# Adding field 'Newsletter.ab_final_choice'
db.add_column(u'newsletter_newsletter', 'ab_final_choice',
self.gf('django.db.models.fields.PositiveSmallIntegerField')(null=True, blank=True),
keep_default=False)
# Adding field 'ContactMailingStatus.ab'
db.add_column(u'newsletter_contactmailingstatus', 'ab',
self.gf('django.db.models.fields.PositiveSmallIntegerField')(null=True, blank=True),
keep_default=False)
def backwards(self, orm):
# Deleting field 'Newsletter.ab_first_stage'
db.delete_column(u'newsletter_newsletter', 'ab_first_stage')
# Deleting field 'Newsletter.ab_final_stage'
db.delete_column(u'newsletter_newsletter', 'ab_final_stage')
# Deleting field 'Newsletter.ab_final_choice'
db.delete_column(u'newsletter_newsletter', 'ab_final_choice')
# Deleting field 'ContactMailingStatus.ab'
db.delete_column(u'newsletter_contactmailingstatus', 'ab')
models = {
u'accounts.user': {
'Meta': {'ordering': "['-rating']", 'object_name': 'User'},
'blocked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'company': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'users'", 'null': 'True', 'to': u"orm['company.Company']"}),
'date_joined': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'date_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'date_registered': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '255', 'db_index': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'organiser': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['organiser.Organiser']", 'unique': 'True', 'null': 'True', 'on_delete': 'models.PROTECT', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'position': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'rating': ('django.db.models.fields.IntegerField', [], {'default': '100'}),
'translator': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': u"orm['translator.Translator']", 'blank': 'True', 'unique': 'True'}),
'url': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'blank': 'True'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
},
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'city.city': {
'Meta': {'unique_together': '()', 'object_name': 'City', 'index_together': '()'},
'code_IATA': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['directories.Iata']", 'null': 'True', 'blank': 'True'}),
'country': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'cities'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': u"orm['country.Country']"}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'inflect': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'logo': ('django.db.models.fields.files.ImageField', [], {'max_length': '255', 'blank': 'True'}),
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'old_url': ('django.db.models.fields.CharField', [], {'max_length': '55'}),
'phone_code': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'population': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'services': ('django.db.models.fields.BigIntegerField', [], {'default': 'None'}),
'url': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'})
},
u'company.company': {
'Meta': {'ordering': "['-rating', 'id']", 'unique_together': '()', 'object_name': 'Company', 'index_together': '()'},
'address': ('functions.custom_fields.LocationField', [], {'blank': 'True'}),
'blocked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'city': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'companies'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': u"orm['city.City']"}),
'country': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'companies'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': u"orm['country.Country']"}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'creator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'created_company'", 'null': 'True', 'to': u"orm['accounts.User']"}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'facebook': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
'fax': ('django.db.models.fields.BigIntegerField', [], {'null': 'True', 'blank': 'True'}),
'foundation': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'linkedin': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
'logo': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'blank': 'True'}),
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'phone': ('django.db.models.fields.BigIntegerField', [], {'null': 'True', 'blank': 'True'}),
'rating': ('django.db.models.fields.IntegerField', [], {'default': '100'}),
'staff_number': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
'tag': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'companies'", 'null': 'True', 'symmetrical': 'False', 'to': u"orm['theme.Tag']"}),
'theme': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'companies'", 'null': 'True', 'symmetrical': 'False', 'to': u"orm['theme.Theme']"}),
'twitter': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
'url': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
'vk': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
'web_page': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'country.area': {
'Meta': {'ordering': "['translations__name']", 'unique_together': '()', 'object_name': 'Area', 'index_together': '()'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
u'country.country': {
'Meta': {'ordering': "['translations__name']", 'unique_together': '()', 'object_name': 'Country', 'index_together': '()'},
'area': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['country.Area']"}),
'big_cities': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'cities'", 'null': 'True', 'symmetrical': 'False', 'to': u"orm['city.City']"}),
'capital': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'capital'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': u"orm['city.City']"}),
'country_code': ('django.db.models.fields.CharField', [], {'max_length': '2'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'currency': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['directories.Currency']", 'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'inflect': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'language': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['directories.Language']", 'null': 'True', 'blank': 'True'}),
'latitude': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
'logo': ('django.db.models.fields.files.ImageField', [], {'max_length': '255', 'blank': 'True'}),
'longitude': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'old_url': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '55'}),
'phone_code': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'population': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'services': ('django.db.models.fields.BigIntegerField', [], {'default': 'None'}),
'teritory': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'time_delivery': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
'timezone': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
'url': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'})
},
u'directories.currency': {
'Meta': {'unique_together': '()', 'object_name': 'Currency', 'index_together': '()'},
'code': ('django.db.models.fields.CharField', [], {'max_length': '3'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
u'directories.iata': {
'Meta': {'object_name': 'Iata'},
'airport': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'code': ('django.db.models.fields.CharField', [], {'max_length': '4'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
u'directories.language': {
'Meta': {'unique_together': '()', 'object_name': 'Language', 'index_together': '()'},
'code': ('django.db.models.fields.CharField', [], {'max_length': '2'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'language': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
u'file.filemodel': {
'Meta': {'unique_together': '()', 'object_name': 'FileModel', 'index_together': '()'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']", 'null': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'file_path': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
'file_type': ('functions.custom_fields.EnumField', [], {'default': "'PDF'", 'values': "('PDF', 'DOC', 'TXT', 'OTHER', 'JPG', 'BMP', 'PNG', 'GIF')", 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'img_height': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'img_width': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'purpose': ('functions.custom_fields.EnumField', [], {'default': "'photo'", 'values': "['photo', 'flat', 'logo', 'map', 'scheme teritory', 'diplom', 'preview', 'preview2']"})
},
u'newsletter.attachment': {
'Meta': {'object_name': 'Attachment'},
'file_attachment': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['newsletter.Newsletter']"}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
u'newsletter.contact': {
'Meta': {'ordering': "('-modification_date',)", 'object_name': 'Contact'},
'activated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'activation_code': ('django.db.models.fields.CharField', [], {'default': "'acff6c1ac35fe6d218e81d52ed46667212b90657'", 'max_length': '40'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
'modification_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'subscriber': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'tester': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['accounts.User']", 'null': 'True'}),
'valid': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'newsletter.contactmailingstatus': {
'Meta': {'ordering': "('-creation_date',)", 'object_name': 'ContactMailingStatus'},
'ab': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
'contact': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['newsletter.Contact']"}),
'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'link': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['newsletter.Link']", 'null': 'True', 'blank': 'True'}),
'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['newsletter.Newsletter']"}),
'status': ('django.db.models.fields.IntegerField', [], {})
},
u'newsletter.contactsettings': {
'Meta': {'object_name': 'ContactSettings'},
'area': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['country.Area']", 'null': 'True', 'blank': 'True'}),
'city': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['city.City']", 'null': 'True', 'blank': 'True'}),
'contact': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['newsletter.Contact']", 'unique': 'True'}),
'country': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['country.Country']", 'null': 'True', 'blank': 'True'}),
'exponent_practicum': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'organiser_practicum': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'periodic': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
'theme': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['theme.Theme']", 'null': 'True', 'blank': 'True'})
},
u'newsletter.link': {
'Meta': {'ordering': "('-creation_date',)", 'object_name': 'Link'},
'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'url': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
u'newsletter.mailinglist': {
'Meta': {'ordering': "('-creation_date',)", 'object_name': 'MailingList'},
'announce': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'announce_public_day': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modification_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'subscribers': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'mailinglist_subscriber'", 'symmetrical': 'False', 'to': u"orm['newsletter.Contact']"}),
'theme_for_filter': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['theme.Theme']", 'null': 'True', 'blank': 'True'}),
'unsubscribers': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'mailinglist_unsubscriber'", 'null': 'True', 'symmetrical': 'False', 'to': u"orm['newsletter.Contact']"})
},
u'newsletter.newsletter': {
'Meta': {'ordering': "('-creation_date',)", 'object_name': 'Newsletter'},
'ab_final_choice': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
'ab_final_stage': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'ab_first_stage': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
'ab_testing': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'content': ('django.db.models.fields.TextField', [], {'default': "''"}),
'content2': ('django.db.models.fields.TextField', [], {'default': "''"}),
'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'header_reply': ('django.db.models.fields.CharField', [], {'default': "'Expomap<mail@expomap.ru>'", 'max_length': '255'}),
'header_sender': ('django.db.models.fields.CharField', [], {'default': "'Expomap<mail@expomap.ru>'", 'max_length': '255'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'mailing_list': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['newsletter.MailingList']"}),
'modification_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'preheader': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'sending_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'server': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['newsletter.SMTPServer']"}),
'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'}),
'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'test_contacts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['newsletter.Contact']", 'null': 'True', 'blank': 'True'}),
'theme_for_filter': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['theme.Theme']", 'null': 'True', 'blank': 'True'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'title2': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
},
u'newsletter.popupcount': {
'Meta': {'ordering': "['-date']", 'unique_together': "(('theme', 'date'),)", 'object_name': 'PopupCount'},
'cnt': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'date': ('django.db.models.fields.DateField', [], {}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'theme': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['theme.Theme']", 'null': 'True'})
},
u'newsletter.smtpserver': {
'Meta': {'object_name': 'SMTPServer'},
'headers': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'mails_hour': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
'port': ('django.db.models.fields.IntegerField', [], {'default': '25'}),
'tls': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'user': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'})
},
u'newsletter.workgroup': {
'Meta': {'object_name': 'WorkGroup'},
'contacts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['newsletter.Contact']", 'null': 'True', 'blank': 'True'}),
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'mailinglists': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['newsletter.MailingList']", 'null': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'newsletters': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['newsletter.Newsletter']", 'null': 'True', 'blank': 'True'})
},
u'organiser.organiser': {
'Meta': {'unique_together': '()', 'object_name': 'Organiser', 'index_together': '()'},
'active': ('django.db.models.fields.NullBooleanField', [], {'default': '0', 'null': 'True', 'blank': 'True'}),
'address': ('functions.custom_fields.LocationField', [], {'null': 'True', 'blank': 'True'}),
'city': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['city.City']", 'null': 'True', 'on_delete': 'models.PROTECT', 'blank': 'True'}),
'country': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['country.Country']", 'null': 'True', 'on_delete': 'models.PROTECT', 'blank': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'events_number': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'facebook': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
'fax': ('django.db.models.fields.BigIntegerField', [], {'null': 'True', 'blank': 'True'}),
'foundation': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'linkedin': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
'logo': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'blank': 'True'}),
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'phone': ('django.db.models.fields.BigIntegerField', [], {'null': 'True', 'blank': 'True'}),
'place_conference': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'organiser_place_conference'", 'null': 'True', 'symmetrical': 'False', 'to': u"orm['place_conference.PlaceConference']"}),
'place_exposition': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'organiser_place_exposition'", 'null': 'True', 'symmetrical': 'False', 'to': u"orm['place_exposition.PlaceExposition']"}),
'rating': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'staff_number': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'tag': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['theme.Tag']", 'null': 'True', 'blank': 'True'}),
'theme': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['theme.Theme']", 'null': 'True', 'blank': 'True'}),
'twitter': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
'url': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'blank': 'True'}),
'vk': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
'web_page': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'})
},
u'photologue.gallery': {
'Meta': {'ordering': "['-date_added']", 'unique_together': '()', 'object_name': 'Gallery', 'index_together': '()'},
'date_added': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'photos': ('sortedm2m.fields.SortedManyToManyField', [], {'blank': 'True', 'related_name': "'galleries'", 'null': 'True', 'symmetrical': 'False', 'to': u"orm['photologue.Photo']"}),
'sites': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['sites.Site']", 'null': 'True', 'blank': 'True'}),
'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '200'}),
'tags': ('photologue.models.TagField', [], {'max_length': '255', 'blank': 'True'})
},
u'photologue.photo': {
'Meta': {'ordering': "['sort']", 'unique_together': '()', 'object_name': 'Photo', 'index_together': '()'},
'crop_from': ('django.db.models.fields.CharField', [], {'default': "'center'", 'max_length': '10', 'blank': 'True'}),
'date_added': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'date_taken': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'effect': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'photo_related'", 'null': 'True', 'to': u"orm['photologue.PhotoEffect']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}),
'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'sites': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['sites.Site']", 'null': 'True', 'blank': 'True'}),
'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '200'}),
'sort': ('django.db.models.fields.PositiveIntegerField', [], {'default': '10', 'null': 'True', 'db_index': 'True'}),
'tags': ('photologue.models.TagField', [], {'max_length': '255', 'blank': 'True'}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['photologue.UserMark']", 'null': 'True', 'symmetrical': 'False'}),
'view_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
},
u'photologue.photoeffect': {
'Meta': {'object_name': 'PhotoEffect'},
'background_color': ('django.db.models.fields.CharField', [], {'default': "'#FFFFFF'", 'max_length': '7'}),
'brightness': ('django.db.models.fields.FloatField', [], {'default': '1.0'}),
'color': ('django.db.models.fields.FloatField', [], {'default': '1.0'}),
'contrast': ('django.db.models.fields.FloatField', [], {'default': '1.0'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'filters': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
'reflection_size': ('django.db.models.fields.FloatField', [], {'default': '0'}),
'reflection_strength': ('django.db.models.fields.FloatField', [], {'default': '0.6'}),
'sharpness': ('django.db.models.fields.FloatField', [], {'default': '1.0'}),
'transpose_method': ('django.db.models.fields.CharField', [], {'max_length': '15', 'blank': 'True'})
},
u'photologue.usermark': {
'Meta': {'object_name': 'UserMark'},
'height': ('django.db.models.fields.PositiveSmallIntegerField', [], {}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'left': ('django.db.models.fields.PositiveSmallIntegerField', [], {}),
'top': ('django.db.models.fields.PositiveSmallIntegerField', [], {}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'marks'", 'to': u"orm['accounts.User']"}),
'width': ('django.db.models.fields.PositiveSmallIntegerField', [], {})
},
u'place_conference.placeconference': {
'Meta': {'unique_together': '()', 'object_name': 'PlaceConference', 'index_together': '()'},
'address': ('functions.custom_fields.LocationField', [], {}),
'amount_halls': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'banquet_hall': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'catering': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'city': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'place_conferences'", 'on_delete': 'models.PROTECT', 'to': u"orm['city.City']"}),
'conference_call': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'country': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['country.Country']", 'on_delete': 'models.PROTECT'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'exp_hall_area': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'exposition_hall': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'fax': ('django.db.models.fields.BigIntegerField', [], {'null': 'True', 'blank': 'True'}),
'foundation_year': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'hotel': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'logo': ('django.db.models.fields.files.ImageField', [], {'max_length': '255', 'blank': 'True'}),
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'multimedia_equipment': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'phone': ('django.db.models.fields.BigIntegerField', [], {'null': 'True', 'blank': 'True'}),
'total_capacity': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'translate_equipment': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'type': ('functions.custom_fields.EnumField', [], {'default': "'Convention centre'", 'values': "['Convention centre', 'Exposition centre']"}),
'url': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'}),
'video_link': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'views': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'virtual_tour': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
'web_page': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
'wifi': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'})
},
u'place_exposition.placeexposition': {
'Meta': {'ordering': "['-rating', 'id']", 'unique_together': '()', 'object_name': 'PlaceExposition', 'index_together': '()'},
'address': ('functions.custom_fields.LocationField', [], {}),
'bank': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'business_centre': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'cafe': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'children_room': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'city': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'place_expositions'", 'on_delete': 'models.PROTECT', 'to': u"orm['city.City']"}),
'closed_area': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'conference_centre': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'country': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['country.Country']", 'on_delete': 'models.PROTECT'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'disabled_service': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'event_in_year': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'fax': ('django.db.models.fields.BigIntegerField', [], {'null': 'True', 'blank': 'True'}),
'foundation_year': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_published': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'logo': ('django.db.models.fields.files.ImageField', [], {'max_length': '255', 'blank': 'True'}),
'mobile_application': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'online_registration': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'open_area': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'parking': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'partner': ('django.db.models.fields.NullBooleanField', [], {'default': '0', 'null': 'True', 'blank': 'True'}),
'phone': ('django.db.models.fields.BigIntegerField', [], {'null': 'True', 'blank': 'True'}),
'photogallery': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['photologue.Gallery']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
'press_centre': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'rating': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'terminals': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'total_area': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'total_halls': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'total_pavilions': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'type': ('functions.custom_fields.EnumField', [], {'default': "'Exposition complex'", 'values': "['Exposition complex', 'Convention centre', 'Exposition centre']"}),
'url': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}),
'views': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'virtual_tour': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
'web_page': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
'wifi': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'})
},
u'sites.site': {
'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"},
'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'theme.tag': {
'Meta': {'unique_together': '()', 'object_name': 'Tag', 'index_together': '()'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'inflect': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'old_url': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}),
'theme': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tags'", 'on_delete': 'models.PROTECT', 'to': u"orm['theme.Theme']"}),
'url': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'})
},
u'theme.theme': {
'Meta': {'unique_together': '()', 'object_name': 'Theme', 'index_together': '()'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'inflect': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'main_page_conf': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'db_index': 'True'}),
'main_page_expo': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'db_index': 'True'}),
'old_url': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}),
'types': ('django.db.models.fields.BigIntegerField', [], {'default': 'None'}),
'url': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'})
},
u'translator.translator': {
'Meta': {'unique_together': '()', 'object_name': 'Translator', 'index_together': '()'},
'birth': ('django.db.models.fields.DateField', [], {}),
'car': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'gender': ('functions.custom_fields.EnumField', [], {'default': "'male'", 'values': "('male', 'female')"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
}
}
complete_apps = ['newsletter']

@ -7,6 +7,7 @@ from email.MIMEImage import MIMEImage
import copy
from django.core.mail import EmailMessage
from django.core.validators import MaxValueValidator, MinValueValidator
from django.template import Context
from django.db import models
from django.contrib.sites.models import Site
@ -365,6 +366,14 @@ class Newsletter(models.Model):
(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)
title = models.CharField(_('title'), max_length=255,
help_text=_('You can use the "{{ UNIQUE_KEY }}" variable ' \
@ -375,6 +384,9 @@ class Newsletter(models.Model):
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'))
theme_for_filter = models.ForeignKey('theme.Theme', verbose_name=_(u'Ограничение по тематике'), null=True, blank=True)
@ -397,7 +409,9 @@ class Newsletter(models.Model):
modification_date = models.DateTimeField(_('modification date'), auto_now=True)
def mails_sent(self):
return self.contactmailingstatus_set.filter(status=ContactMailingStatus.SENT).count()
return self.contactmailingstatus_set.filter(
status__in=[ContactMailingStatus.SENT, ContactMailingStatus.AB_WAITING],
).count()
@models.permalink
def get_absolute_url(self):
@ -423,6 +437,8 @@ class Newsletter(models.Model):
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())]
@ -499,6 +515,7 @@ class ContactMailingStatus(models.Model):
LINK_OPENED = 6
UNSUBSCRIPTION = 7
ANNOUNCE_NO_DATA = 8
AB_WAITING = 9
STATUS_CHOICES = ((SENT_TEST, _('sent in test')),
(SENT, _('sent')),
@ -509,6 +526,7 @@ class ContactMailingStatus(models.Model):
(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'))
@ -516,6 +534,7 @@ class ContactMailingStatus(models.Model):
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)

@ -198,11 +198,11 @@ class CreateMailingList(CreateView):
def form_valid(self, form):
obj = form.save()
filter_form = ContactFilterForm(self.request.GET)
if filter_form.is_valid():
contacts, _ = filter_form.filter()
obj.subscribers = contacts
obj.save()
# filter_form = ContactFilterForm(self.request.GET)
# if filter_form.is_valid():
# contacts, _ = filter_form.filter()
# obj.subscribers = contacts
# obj.save()
return HttpResponseRedirect(self.success_url)
@ -307,19 +307,28 @@ class NewsletterStatistics(DetailView):
def get_context_data(self, **kwargs):
context = super(NewsletterStatistics, self).get_context_data(**kwargs)
sent = self.object.contactmailingstatus_set.filter(status=ContactMailingStatus.SENT).count()
errors = self.object.contactmailingstatus_set.filter(status=ContactMailingStatus.ERROR).count()
opened = self.object.contactmailingstatus_set.filter(status=ContactMailingStatus.OPENED).\
if self.object.ab_testing:
context.update({'stat': self.get_stat_data(params={'ab': Newsletter.A})})
context.update({'statB': self.get_stat_data(params={'ab': Newsletter.B})})
else:
context.update({'stat': self.get_stat_data()})
return context
def get_stat_data(self, params={}):
sent = self.object.contactmailingstatus_set.filter(status=ContactMailingStatus.SENT, **params).count()
errors = self.object.contactmailingstatus_set.filter(status=ContactMailingStatus.ERROR, **params).count()
opened = self.object.contactmailingstatus_set.filter(status=ContactMailingStatus.OPENED, **params).\
values_list('contact', flat=True).distinct().count()
unsubscribed = self.object.contactmailingstatus_set.filter(status=ContactMailingStatus.UNSUBSCRIPTION).\
unsubscribed = self.object.contactmailingstatus_set.filter(status=ContactMailingStatus.UNSUBSCRIPTION, **params).\
values_list('contact', flat=True).distinct().count()
no_data = self.object.contactmailingstatus_set.filter(status=ContactMailingStatus.ANNOUNCE_NO_DATA).count()
links = self.object.contactmailingstatus_set.filter(status=ContactMailingStatus.LINK_OPENED).count()
links_unique = self.object.contactmailingstatus_set.filter(status=ContactMailingStatus.LINK_OPENED).\
no_data = self.object.contactmailingstatus_set.filter(status=ContactMailingStatus.ANNOUNCE_NO_DATA, **params).count()
links = self.object.contactmailingstatus_set.filter(status=ContactMailingStatus.LINK_OPENED, **params).count()
links_unique = self.object.contactmailingstatus_set.filter(status=ContactMailingStatus.LINK_OPENED, **params).\
values_list('contact', flat=True).distinct().count()
CMS = ContactMailingStatus
opened_percent = 0 if opened or sent == 0 else (float(opened)/sent)*100
unsub_percent = 0 if unsubscribed or sent == 0 else (float(unsubscribed)/sent)*100
ab = params.get('ab', '')
stat = {
'sent': {'data': sent, 'filter': ""},
'errors': {'data': errors, 'filter': CMS.ERROR},
@ -327,9 +336,10 @@ class NewsletterStatistics(DetailView):
'unsub': {'data': unsubscribed, 'filter': CMS.UNSUBSCRIPTION, 'percent': unsub_percent},
'no_data': {'data': no_data, 'filter': CMS.ANNOUNCE_NO_DATA},
'links': {'data': links, 'filter': CMS.LINK_OPENED},
'links_unique': {'data': links_unique, 'filter': CMS.LINK_OPENED}}
context.update({'stat': stat})
return context
'links_unique': {'data': links_unique, 'filter': CMS.LINK_OPENED},
'ab': ab,
}
return stat
class NewsletterCloneView(RedirectView):

7
fabfile.py vendored

@ -153,6 +153,13 @@ def t1463():
run('python manage.py migrate newsletter')
@stage(5)
def t1460():
with cd(REMOTE_HOME_DIR):
run('python manage.py migrate newsletter')
# def stage3_pre_final():
# with cd(REMOTE_HOME_DIR):
# run('pip install pandas')

@ -12,20 +12,28 @@
<div class="form-group">
{{ filter_form.status }}
{{ filter_form.email }}
{% if newsletter.ab_testing %}
{{ filter_form.ab }}
{% endif %}
<button type="submit" class="btn">Применить</button>
</div>
</form>
<table class="table table-hover table table-bordered table-striped">
<colgroup>
<col width="33%">
<col width="33%">
<col width="33%">
</colgroup>
{% if newsletter.ab_testing %}
<colgroup>
<col width="25%">
<col width="25%">
<col width="25%">
<col width="25%">
</colgroup>
<thead>
<tr>
<th>Дата</th>
<th>Контакт</th>
<th>Статус</th>
<th>Вариант письма</th>
</tr>
</thead>
<tbody>
@ -34,8 +42,32 @@
<td>{{ item.creation_date|date:"Y-m-d H:i:s" }}</td>
<td>{{ item.contact.email }}</td>
<td>{{ item.get_verbose_status|safe }}</td>
<td>{{ item.get_ab_display }}</td>
</tr>
{% endfor %}
{% else %}
<colgroup>
<col width="33%">
<col width="33%">
<col width="33%">
</colgroup>
<thead>
<tr>
<th>Дата</th>
<th>Контакт</th>
<th>Статус</th>
</tr>
</thead>
<tbody>
{% for item in object_list %}
<tr>
<td>{{ item.creation_date|date:"Y-m-d H:i:s" }}</td>
<td>{{ item.contact.email }}</td>
<td>{{ item.get_verbose_status|safe }}</td>
</tr>
{% endfor %}
{% endif %}
</tbody>
</table>

@ -36,11 +36,9 @@
function makeContent2() {
if ($('#id_ab_testing').is(':checked')) {
$('div#cke_id_content2 div.cke_contents').height($('div#cke_id_content div.cke_contents').height());
$('#id_content2').parents('div.control-group').show();
$('#id_title2').parents('div.control-group').show();
$('#id_content2, #id_title2, #id_ab_first_stage').parents('div.control-group').show();
} else {
$('#id_content2').parents('div.control-group').hide();
$('#id_title2').parents('div.control-group').hide();
$('#id_content2, #id_title2, #id_ab_first_stage').parents('div.control-group').hide();
}
}
makeContent2();

@ -1,5 +1,7 @@
{% extends 'admin/base.html' %}
{% load staticfiles %}
{% load i18n %}
{% block body %}
<div class="box span10">
@ -7,6 +9,10 @@
<h2><i class="icon-arrow-down"></i>Статистика для {{ newsletter.title }}</h2>
</div>
<div class="box-content">
{% if newsletter.ab_testing %}
<i class="icon-info-sign"></i>{% trans "Статистика для писем типа А" %}
{% endif %}
<table class="table table-hover table table-bordered table-striped">
<colgroup>
<col width="45%">
@ -24,42 +30,100 @@
<tr>
<td>Отправлено</td>
<td>{{ stat.sent.data }}</td>
<td><a href="{% url 'newsletters_newsletters_history' object.id %}?status={{ stat.sent.filter }}">Подробно</a></td>
<td><a href="{% url 'newsletters_newsletters_history' object.id %}?status={{ stat.sent.filter }}&ab={{ stat.ab }}">Подробно</a></td>
</tr>
<tr>
<td>Ошибок</td>
<td>{{ stat.errors.data }}</td>
<td><a href="{% url 'newsletters_newsletters_history' object.id %}?status={{ stat.errors.filter }}">Подробно</a></td>
<td><a href="{% url 'newsletters_newsletters_history' object.id %}?status={{ stat.errors.filter }}&ab={{ stat.ab }}">Подробно</a></td>
</tr>
<tr>
<td>Открыто(уникальных)</td>
<td>{{ stat.opened.data }} {% if stat.opened.percent %} ({{ stat.opened.percent|floatformat:"0" }}%) {% endif %}</td>
<td><a href="{% url 'newsletters_newsletters_history' object.id %}?status={{ stat.opened.filter }}">Подробно</a></td>
<td><a href="{% url 'newsletters_newsletters_history' object.id %}?status={{ stat.opened.filter }}&ab={{ stat.ab }}">Подробно</a></td>
</tr>
<tr>
<td>Отписалось(уникальных)</td>
<td>{{ stat.unsub.data }} {% if stat.unsub.percent %} ({{ stat.unsub.percent|floatformat:"0" }}%) {% endif %}</td>
<td><a href="{% url 'newsletters_newsletters_history' object.id %}?status={{ stat.unsub.filter }}">Подробно</a></td>
<td><a href="{% url 'newsletters_newsletters_history' object.id %}?status={{ stat.unsub.filter }}&ab={{ stat.ab }}">Подробно</a></td>
</tr>
<tr>
<td>Нет данных</td>
<td>{{ stat.no_data.data }}</td>
<td><a href="{% url 'newsletters_newsletters_history' object.id %}?status={{ stat.no_data.filter }}">Подробно</a></td>
<td><a href="{% url 'newsletters_newsletters_history' object.id %}?status={{ stat.no_data.filter }}&ab={{ stat.ab }}">Подробно</a></td>
</tr>
<tr>
<td>Переходов по ссылкам</td>
<td>{{ stat.links.data }}</td>
<td><a href="{% url 'newsletters_newsletters_history' object.id %}?status={{ stat.links.filter }}">Подробно</a></td>
<td><a href="{% url 'newsletters_newsletters_history' object.id %}?status={{ stat.links.filter }}&ab={{ stat.ab }}">Подробно</a></td>
</tr>
<tr>
<td>Переходов уникальными пользователями</td>
<td>{{ stat.links_unique.data }}</td>
<td><a href="{% url 'newsletters_newsletters_history' object.id %}?status={{ stat.links_unique.filter }}">Подробно</a></td>
<td><a href="{% url 'newsletters_newsletters_history' object.id %}?status={{ stat.links_unique.filter }}&ab={{ stat.ab }}">Подробно</a></td>
</tr>
</tbody>
</table>
{% if newsletter.ab_testing %}
<br>
<i class="icon-info-sign"></i>{% trans "Статистика для писем типа В" %}
<table class="table table-hover table table-bordered table-striped">
<colgroup>
<col width="45%">
<col width="45%">
<col width="10%">
</colgroup>
<thead>
<tr>
<th>Название</th>
<th>Количество писем</th>
<th>&nbsp</th>
</tr>
</thead>
<tbody>
<tr>
<td>Отправлено</td>
<td>{{ statB.sent.data }}</td>
<td><a href="{% url 'newsletters_newsletters_history' object.id %}?status={{ statB.sent.filter }}&ab={{ statB.ab }}">Подробно</a></td>
</tr>
<tr>
<td>Ошибок</td>
<td>{{ statB.errors.data }}</td>
<td><a href="{% url 'newsletters_newsletters_history' object.id %}?status={{ statB.errors.filter }}&ab={{ statB.ab }}">Подробно</a></td>
</tr>
<tr>
<td>Открыто(уникальных)</td>
<td>{{ statB.opened.data }} {% if statB.opened.percent %} ({{ statB.opened.percent|floatformat:"0" }}%) {% endif %}</td>
<td><a href="{% url 'newsletters_newsletters_history' object.id %}?status={{ statB.opened.filter }}&ab={{ statB.ab }}">Подробно</a></td>
</tr>
<tr>
<td>Отписалось(уникальных)</td>
<td>{{ statB.unsub.data }} {% if statB.unsub.percent %} ({{ statB.unsub.percent|floatformat:"0" }}%) {% endif %}</td>
<td><a href="{% url 'newsletters_newsletters_history' object.id %}?status={{ statB.unsub.filter }}&ab={{ statB.ab }}">Подробно</a></td>
</tr>
<tr>
<td>Нет данных</td>
<td>{{ statB.no_data.data }}</td>
<td><a href="{% url 'newsletters_newsletters_history' object.id %}?status={{ statB.no_data.filter }}&ab={{ statB.ab }}">Подробно</a></td>
</tr>
<tr>
<td>Переходов по ссылкам</td>
<td>{{ statB.links.data }}</td>
<td><a href="{% url 'newsletters_newsletters_history' object.id %}?status={{ statB.links.filter }}&ab={{ statB.ab }}">Подробно</a></td>
</tr>
<tr>
<td>Переходов уникальными пользователями</td>
<td>{{ statB.links_unique.data }}</td>
<td><a href="{% url 'newsletters_newsletters_history' object.id %}?status={{ statB.links_unique.filter }}&ab={{ statB.ab }}">Подробно</a></td>
</tr>
</tbody>
</table>
{% endif %}
</div>
</div>

Loading…
Cancel
Save