From 5919f1e6bf8f023302082d16b51b9937c5944288 Mon Sep 17 00:00:00 2001 From: Alexander Burdeiny Date: Thu, 14 Jul 2016 15:54:52 +0300 Subject: [PATCH] =?UTF-8?q?1460:=20=D0=AD=D1=82=D0=B0=D0=BF=20=E2=84=965:?= =?UTF-8?q?=20=D0=94=D0=BE=D0=BF=D0=BE=D0=BB=D0=BD=D0=B8=D1=82=D0=B5=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D0=BE=20-=20=D0=B4=D0=BE=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=BA=D0=B0=20=D0=90-=D0=91=20=D1=82=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=201457:=20=D0=AD=D1=82=D0=B0=D0=BF=20=E2=84=965:?= =?UTF-8?q?=20=D0=A1=D0=BE=20=D1=81=D0=BA=D0=B0=D0=B9=D0=BF=D0=B0=20-=20?= =?UTF-8?q?=D0=98=D0=BC=D0=BF=D0=BE=D1=80=D1=82=20=D0=BF=D0=BE=D0=B4=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D1=87=D0=B8=D0=BA=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 1 + city/search_indexes.py | 2 +- emencia/django/newsletter/admin_forms.py | 55 +- emencia/django/newsletter/forms.py | 31 +- emencia/django/newsletter/mailer.py | 90 +++- .../management/commands/send_newsletter.py | 4 +- ...inal_stage__add_field_newsletter_ab_fin.py | 493 ++++++++++++++++++ emencia/django/newsletter/models.py | 21 +- .../django/newsletter/views/admin_views.py | 40 +- fabfile.py | 7 + .../admin/newsletters/newsletter_history.html | 42 +- .../admin/newsletters/newsletter_object.html | 6 +- .../admin/newsletters/newsletter_stat.html | 78 ++- 13 files changed, 799 insertions(+), 71 deletions(-) create mode 100644 emencia/django/newsletter/migrations/0006_auto__add_field_newsletter_ab_final_stage__add_field_newsletter_ab_fin.py diff --git a/Makefile b/Makefile index 772dc2a0..013c057a 100644 --- a/Makefile +++ b/Makefile @@ -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: diff --git a/city/search_indexes.py b/city/search_indexes.py index e931270a..7fd826d4 100644 --- a/city/search_indexes.py +++ b/city/search_indexes.py @@ -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() diff --git a/emencia/django/newsletter/admin_forms.py b/emencia/django/newsletter/admin_forms.py index 283f53ff..ca7fa9f5 100644 --- a/emencia/django/newsletter/admin_forms.py +++ b/emencia/django/newsletter/admin_forms.py @@ -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): diff --git a/emencia/django/newsletter/forms.py b/emencia/django/newsletter/forms.py index 1271f416..3cfc8069 100644 --- a/emencia/django/newsletter/forms.py +++ b/emencia/django/newsletter/forms.py @@ -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 diff --git a/emencia/django/newsletter/mailer.py b/emencia/django/newsletter/mailer.py index fec4c82f..5afa8ea4 100644 --- a/emencia/django/newsletter/mailer.py +++ b/emencia/django/newsletter/mailer.py @@ -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): diff --git a/emencia/django/newsletter/management/commands/send_newsletter.py b/emencia/django/newsletter/management/commands/send_newsletter.py index d608a358..5754901a 100644 --- a/emencia/django/newsletter/management/commands/send_newsletter.py +++ b/emencia/django/newsletter/management/commands/send_newsletter.py @@ -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: diff --git a/emencia/django/newsletter/migrations/0006_auto__add_field_newsletter_ab_final_stage__add_field_newsletter_ab_fin.py b/emencia/django/newsletter/migrations/0006_auto__add_field_newsletter_ab_final_stage__add_field_newsletter_ab_fin.py new file mode 100644 index 00000000..e0502844 --- /dev/null +++ b/emencia/django/newsletter/migrations/0006_auto__add_field_newsletter_ab_final_stage__add_field_newsletter_ab_fin.py @@ -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'", 'max_length': '255'}), + 'header_sender': ('django.db.models.fields.CharField', [], {'default': "'Expomap'", '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'] \ No newline at end of file diff --git a/emencia/django/newsletter/models.py b/emencia/django/newsletter/models.py index ed4dfa17..11e77219 100644 --- a/emencia/django/newsletter/models.py +++ b/emencia/django/newsletter/models.py @@ -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) diff --git a/emencia/django/newsletter/views/admin_views.py b/emencia/django/newsletter/views/admin_views.py index a7d8bede..8b86452d 100644 --- a/emencia/django/newsletter/views/admin_views.py +++ b/emencia/django/newsletter/views/admin_views.py @@ -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): diff --git a/fabfile.py b/fabfile.py index 3c91f3ed..d697bdde 100644 --- a/fabfile.py +++ b/fabfile.py @@ -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') diff --git a/templates/admin/newsletters/newsletter_history.html b/templates/admin/newsletters/newsletter_history.html index ff5c7f40..06be6999 100644 --- a/templates/admin/newsletters/newsletter_history.html +++ b/templates/admin/newsletters/newsletter_history.html @@ -12,20 +12,28 @@
{{ filter_form.status }} {{ filter_form.email }} + {% if newsletter.ab_testing %} + {{ filter_form.ab }} + {% endif %} +
- - - - - + + {% if newsletter.ab_testing %} + + + + + + + @@ -34,8 +42,32 @@ + {% endfor %} + {% else %} + + + + + + + + + + + + + + {% for item in object_list %} + + + + + + {% endfor %} + {% endif %} +
Дата Контакт СтатусВариант письма
{{ item.creation_date|date:"Y-m-d H:i:s" }} {{ item.contact.email }} {{ item.get_verbose_status|safe }}{{ item.get_ab_display }}
ДатаКонтактСтатус
{{ item.creation_date|date:"Y-m-d H:i:s" }}{{ item.contact.email }}{{ item.get_verbose_status|safe }}
diff --git a/templates/admin/newsletters/newsletter_object.html b/templates/admin/newsletters/newsletter_object.html index 1b2fcebf..d00169e0 100644 --- a/templates/admin/newsletters/newsletter_object.html +++ b/templates/admin/newsletters/newsletter_object.html @@ -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(); diff --git a/templates/admin/newsletters/newsletter_stat.html b/templates/admin/newsletters/newsletter_stat.html index c25de804..73d392a3 100644 --- a/templates/admin/newsletters/newsletter_stat.html +++ b/templates/admin/newsletters/newsletter_stat.html @@ -1,5 +1,7 @@ {% extends 'admin/base.html' %} {% load staticfiles %} +{% load i18n %} + {% block body %}
@@ -7,6 +9,10 @@

Статистика для {{ newsletter.title }}

+ {% if newsletter.ab_testing %} + {% trans "Статистика для писем типа А" %} + {% endif %} + @@ -24,42 +30,100 @@ - + - + - + - + - + - + - + + + +
Отправлено {{ stat.sent.data }}ПодробноПодробно
Ошибок {{ stat.errors.data }}ПодробноПодробно
Открыто(уникальных) {{ stat.opened.data }} {% if stat.opened.percent %} ({{ stat.opened.percent|floatformat:"0" }}%) {% endif %}ПодробноПодробно
Отписалось(уникальных) {{ stat.unsub.data }} {% if stat.unsub.percent %} ({{ stat.unsub.percent|floatformat:"0" }}%) {% endif %}ПодробноПодробно
Нет данных {{ stat.no_data.data }}ПодробноПодробно
Переходов по ссылкам {{ stat.links.data }}ПодробноПодробно
Переходов уникальными пользователями {{ stat.links_unique.data }}ПодробноПодробно
+ + {% if newsletter.ab_testing %} +
+ {% trans "Статистика для писем типа В" %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
НазваниеКоличество писем 
Отправлено{{ statB.sent.data }}Подробно
Ошибок{{ statB.errors.data }}Подробно
Открыто(уникальных){{ statB.opened.data }} {% if statB.opened.percent %} ({{ statB.opened.percent|floatformat:"0" }}%) {% endif %}Подробно
Отписалось(уникальных){{ statB.unsub.data }} {% if statB.unsub.percent %} ({{ statB.unsub.percent|floatformat:"0" }}%) {% endif %}Подробно
Нет данных{{ statB.no_data.data }}Подробно
Переходов по ссылкам{{ statB.links.data }}Подробно
Переходов уникальными пользователями{{ statB.links_unique.data }}Подробно
+ {% endif %}