# -*- coding: utf-8 -*- """Models for emencia.django.newsletter""" from smtplib import SMTP from datetime import datetime from datetime import timedelta from email.MIMEImage import MIMEImage from django.core.mail import EmailMessage from django.template import Context from django.db import models from django.contrib.sites.models import Site from django.conf import settings from django.utils.encoding import smart_str from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType from django.contrib.auth.models import Group from django.utils.encoding import force_unicode from django.template.loader import render_to_string, get_template from emencia.django.newsletter.managers import ContactManager from emencia.django.newsletter.settings import BASE_PATH from emencia.django.newsletter.settings import MAILER_HARD_LIMIT from emencia.django.newsletter.settings import DEFAULT_HEADER_REPLY from emencia.django.newsletter.settings import DEFAULT_HEADER_SENDER from emencia.django.newsletter.settings import ACTIVATION_SUBJECT, ACTIVATION_TEMPLATE, DEFAULT_SMPTSERVER_ID from emencia.django.newsletter.utils.vcard import vcard_contact_export from emencia.django.newsletter.utils import make_activation_code from functions.form_check import translit_with_separator class SMTPServer(models.Model): """Configuration of a SMTP server""" name = models.CharField(_('name'), max_length=255) host = models.CharField(_('server host'), max_length=255) user = models.CharField(_('server user'), max_length=128, blank=True, help_text=_('Leave it empty if the host is public.')) password = models.CharField(_('server password'), max_length=128, blank=True, help_text=_('Leave it empty if the host is public.')) port = models.IntegerField(_('server port'), default=25) tls = models.BooleanField(_('server use TLS')) headers = models.TextField(_('custom headers'), blank=True, help_text=_('key1: value1 key2: value2, splitted by return line.\n'\ 'Useful for passing some tracking headers if your provider allows it.')) mails_hour = models.IntegerField(_('mails per hour'), default=0) def connect(self): """Connect the SMTP Server""" smtp = SMTP(smart_str(self.host), int(self.port)) smtp.ehlo_or_helo_if_needed() if self.tls: smtp.starttls() smtp.ehlo_or_helo_if_needed() if self.user or self.password: smtp.login(smart_str(self.user), smart_str(self.password)) return smtp def delay(self): """compute the delay (in seconds) between mails to ensure mails per hour limit is not reached :rtype: float """ if not self.mails_hour: return 0.0 else: return 3600.0 / self.mails_hour def credits(self): """Return how many mails the server can send""" if not self.mails_hour: return MAILER_HARD_LIMIT last_hour = datetime.now() - timedelta(hours=1) sent_last_hour = ContactMailingStatus.objects.filter( models.Q(status=ContactMailingStatus.SENT) | models.Q(status=ContactMailingStatus.SENT_TEST), newsletter__server=self, creation_date__gte=last_hour).count() return self.mails_hour - sent_last_hour @property def custom_headers(self): if self.headers: headers = {} for header in self.headers.splitlines(): if header: key, value = header.split(':') headers[key.strip()] = value.strip() return headers return {} def __unicode__(self): return '%s (%s)' % (self.name, self.host) class Meta: verbose_name = _('SMTP server') verbose_name_plural = _('SMTP servers') class Contact(models.Model): """Contact for emailing""" activation_subject = ACTIVATION_SUBJECT activation_template = ACTIVATION_TEMPLATE user = models.ForeignKey('accounts.User', null=True) email = models.EmailField(_('email'), unique=True) first_name = models.CharField(_('first name'), max_length=50, blank=True) last_name = models.CharField(_('last name'), max_length=50, blank=True) subscriber = models.BooleanField(_('subscriber'), default=True) valid = models.BooleanField(_('valid email'), default=True) tester = models.BooleanField(_('contact tester'), default=False) activation_code = models.CharField( verbose_name=_('activation code'), max_length=40, default=make_activation_code ) activated = models.BooleanField(default=False) content_type = models.ForeignKey(ContentType, blank=True, null=True) object_id = models.PositiveIntegerField(blank=True, null=True) content_object = generic.GenericForeignKey('content_type', 'object_id') creation_date = models.DateTimeField(_('creation date'), auto_now_add=True) modification_date = models.DateTimeField(_('modification date'), auto_now=True) objects = ContactManager() def subscriptions(self): """Return the user subscriptions""" return MailingList.objects.filter(subscribers=self) def unsubscriptions(self): """Return the user unsubscriptions""" return MailingList.objects.filter(unsubscribers=self) def vcard_format(self): return vcard_contact_export(self) def mail_format(self): if self.first_name and self.last_name: return '%s %s <%s>' % (self.last_name, self.first_name, self.email) return self.email mail_format.short_description = _('mail format') def get_absolute_url(self): if self.content_type and self.object_id: return self.content_object.get_absolute_url() return reverse('admin:newsletter_contact_change', args=(self.pk,)) def get_announce_context(self): from exposition.models import Exposition from article.models import Article from django.utils.translation import activate activate('ru') events = Exposition.enable.upcoming() themes = list(self.contactsettings.theme.all()) try: country = self.contactsettings.country.all()[0] except IndexError: country = None if themes: theme_ids = [item.id for item in themes] events = events.filter(theme__in=theme_ids) else: return None if country: events = events.filter(country=country) events = list(events[:5]) if events: context = {'themes': themes, 'place': country, 'events': events} else: # no events for announce return None news = list(Article.objects.news() .filter(publish_date__gt=datetime.now()-timedelta(days=30), theme__in=theme_ids)[:3]) if news: context['news'] = news blogs = list(Article.objects.blogs()[:1]) if blogs: context['blogs'] = blogs return context def __unicode__(self): if self.first_name and self.last_name: contact_name = '%s %s' % (self.last_name, self.first_name) else: contact_name = self.email return contact_name def send_activation(self): user = self site = Site.objects.get_current() ctx_dict = {'activation_key': self.activation_code, 'site': site, 'user': user} subject = render_to_string(self.activation_subject, ctx_dict) # Email subject *must not* contain newlines subject = ''.join(subject.splitlines()) message = get_template(self.activation_template).render(Context(ctx_dict)) msg = EmailMessage(subject, message, settings.DEFAULT_FROM_EMAIL, [user.email]) msg.content_subtype = "html" images =(('/img/logo_reg.png', 'logo'), ('/img/soc-medias/sm-icon-rss.png', 'rss'), ('/img/soc-medias/sm-icon-fb.png', 'fb'), ('/img/soc-medias/sm-icon-lin.png', 'linkedin'), ('/img/soc-medias/sm-icon-vk.png', 'vk'), ('/img/soc-medias/sm-icon-twit.png', 'twit')) for img in images: fp = open(settings.STATIC_ROOT+img[0], 'rb') msg_img = MIMEImage(fp.read()) fp.close() msg_img.add_header('Content-ID', '<'+img[1]+'>') msg.attach(msg_img) msg.send() class Meta: ordering = ('-modification_date',) verbose_name = _('contact') verbose_name_plural = _('contacts') class ContactSettings(models.Model): WEEK = 1 # every week WEEK_2 = 2 # every 2 weeks MONTH = 3 # every 4 weeks PERIODIC_CHOICES = [(WEEK, _(u'Раз в неделю')), (WEEK_2, _(u'Раз в 2 недели')), (MONTH, _(u'Раз в месяц')) ] contact = models.OneToOneField(Contact) periodic = models.PositiveIntegerField(verbose_name=_(u'Периодичность отправки'), choices=PERIODIC_CHOICES, default=WEEK) exponent_practicum = models.BooleanField(default=True, verbose_name=_(u'Практикум экспонента')) organiser_practicum = models.BooleanField(default=True, verbose_name=_(u'Практикум организатора событий')) theme = models.ManyToManyField('theme.Theme', blank=True, null=True, verbose_name=_(u'Тематики')) area = models.ManyToManyField('country.Area', blank=True, null=True, verbose_name=_(u'Географические зоны')) country = models.ManyToManyField('country.Country', blank=True, null=True, verbose_name=_(u'Страны')) city = models.ManyToManyField('city.City', blank=True, null=True, verbose_name=_(u'Города')) class MailingList(models.Model): """Mailing list""" name = models.CharField(_('name'), max_length=255) description = models.TextField(_('description'), blank=True) subscribers = models.ManyToManyField(Contact, verbose_name=_('subscribers'), related_name='mailinglist_subscriber') unsubscribers = models.ManyToManyField(Contact, verbose_name=_('unsubscribers'), related_name='mailinglist_unsubscriber', null=True, blank=True) announce = models.BooleanField(default=False) announce_public_day = models.IntegerField(default=2) creation_date = models.DateTimeField(_('creation date'), auto_now_add=True) modification_date = models.DateTimeField(_('modification date'), auto_now=True) def subscribers_count(self): return self.subscribers.all().count() subscribers_count.short_description = _('subscribers') def unsubscribers_count(self): return self.unsubscribers.all().count() unsubscribers_count.short_description = _('unsubscribers') def expedition_set(self): unsubscribers_id = self.unsubscribers.values_list('id', flat=True) if not self.announce: return self.subscribers.valid_subscribers().exclude( id__in=unsubscribers_id) else: return self.subscribers.valid_subscribers().exclude( id__in=unsubscribers_id).filter(contactsettings__theme__isnull=False) def __unicode__(self): return self.name def check_day(self, day): weekday = day.weekday() if self.announce_public_day > 0: return (self.announce_public_day - 1) == weekday else: return weekday == 6 def generate_announce(self, day): # simple data sending_date = day + timedelta(days=1) start_sending_time = datetime.combine(sending_date, datetime.min.time()) title = u'Анонс событий на %s' % sending_date.strftime('%Y-%m-%y') newsletter = Newsletter(status=Newsletter.WAITING, sending_date=start_sending_time, title=title, mailing_list=self, slug=translit_with_separator(title)) newsletter.save() # add default attacments copied from default announce newsletter default_newsletter = Newsletter.objects.get(id=1) attacments = list(default_newsletter.attachment_set.all()) for a in attacments: a.id = None a.newsletter = newsletter a.save() return newsletter def generate_announce_newsletter(self, day): if self.check_day(day): return self.generate_announce(day) return None class Meta: ordering = ('-creation_date',) verbose_name = _('mailing list') verbose_name_plural = _('mailing lists') class Newsletter(models.Model): """Newsletter to be sended to contacts""" DRAFT = 0 WAITING = 1 SENDING = 2 SENT = 4 CANCELED = 5 STATUS_CHOICES = ((DRAFT, _('draft')), (WAITING, _('waiting sending')), (SENDING, _('sending')), (SENT, _('sent')), (CANCELED, _('canceled')), ) title = models.CharField(_('title'), max_length=255, help_text=_('You can use the "{{ UNIQUE_KEY }}" variable ' \ 'for unique identifier within the newsletter\'s title.')) content = models.TextField(_('content'), help_text=_('Or paste an URL.'), default=_('
\n\n')) mailing_list = models.ForeignKey(MailingList, verbose_name=_('mailing list')) test_contacts = models.ManyToManyField(Contact, verbose_name=_('test contacts'), blank=True, null=True) server = models.ForeignKey(SMTPServer, verbose_name=_('smtp server'), default=lambda: SMTPServer.objects.get(id=DEFAULT_SMPTSERVER_ID)) header_sender = models.CharField(_('sender'), max_length=255, default=DEFAULT_HEADER_SENDER) header_reply = models.CharField(_('reply to'), max_length=255, default=DEFAULT_HEADER_REPLY) status = models.IntegerField(_('status'), choices=STATUS_CHOICES, default=DRAFT) sending_date = models.DateTimeField(_('sending date'), default=datetime.now) slug = models.SlugField(help_text=_('Used for displaying the newsletter on the site.'), unique=True) creation_date = models.DateTimeField(_('creation date'), auto_now_add=True) modification_date = models.DateTimeField(_('modification date'), auto_now=True) def mails_sent(self): return self.contactmailingstatus_set.filter(status=ContactMailingStatus.SENT).count() @models.permalink def get_absolute_url(self): return ('newsletter_newsletter_preview', (self.slug,)) @models.permalink def get_historic_url(self): return ('newsletter_newsletter_historic', (self.slug,)) @models.permalink def get_statistics_url(self): return ('newsletter_newsletter_statistics', (self.slug,)) def get_status_text(self): for item in self.STATUS_CHOICES: if self.status == item[0]: return unicode(item[1]) return 'bug' def __unicode__(self): return self.title class Meta: ordering = ('-creation_date',) verbose_name = _('newsletter') verbose_name_plural = _('newsletters') permissions = (('can_change_status', 'Can change status'),) class Link(models.Model): """Link sended in a newsletter""" title = models.CharField(_('title'), max_length=255) url = models.CharField(_('url'), max_length=255) creation_date = models.DateTimeField(_('creation date'), auto_now_add=True) def get_absolute_url(self): return self.url def __unicode__(self): return self.title class Meta: ordering = ('-creation_date',) verbose_name = _('link') verbose_name_plural = _('links') class Attachment(models.Model): """Attachment file in a newsletter""" def get_newsletter_storage_path(self, filename): filename = force_unicode(filename) return '/'.join([BASE_PATH, self.newsletter.slug, filename]) newsletter = models.ForeignKey(Newsletter, verbose_name=_('newsletter')) title = models.CharField(_('title'), max_length=255) file_attachment = models.FileField(_('file to attach'), max_length=255, upload_to=get_newsletter_storage_path) class Meta: verbose_name = _('attachment') verbose_name_plural = _('attachments') def __unicode__(self): return self.title def get_absolute_url(self): return self.file_attachment.url class ContactMailingStatus(models.Model): """Status of the reception""" SENT_TEST = -1 SENT = 0 ERROR = 1 INVALID = 2 OPENED = 4 OPENED_ON_SITE = 5 LINK_OPENED = 6 UNSUBSCRIPTION = 7 ANNOUNCE_NO_DATA = 8 STATUS_CHOICES = ((SENT_TEST, _('sent in test')), (SENT, _('sent')), (ERROR, _('error')), (INVALID, _('invalid email')), (OPENED, _('opened')), (OPENED_ON_SITE, _('opened on site')), (LINK_OPENED, _('link opened')), (UNSUBSCRIPTION, _('unsubscription')), (ANNOUNCE_NO_DATA, _('announce no data')), ) newsletter = models.ForeignKey(Newsletter, verbose_name=_('newsletter')) contact = models.ForeignKey(Contact, verbose_name=_('contact')) status = models.IntegerField(_('status'), choices=STATUS_CHOICES) link = models.ForeignKey(Link, verbose_name=_('link'), blank=True, null=True) creation_date = models.DateTimeField(_('creation date'), auto_now_add=True) def get_verbose_status(self): verbose = '' for s in self.STATUS_CHOICES: if s[0] == self.status: verbose = unicode(s[1]) break if self.status == ContactMailingStatus.LINK_OPENED: verbose +=": %s"% (self.link.url, self.link.url) return verbose def __unicode__(self): return '%s : %s : %s' % (self.newsletter.__unicode__(), self.contact.__unicode__(), self.get_status_display()) class Meta: ordering = ('-creation_date',) verbose_name = _('contact mailing status') verbose_name_plural = _('contact mailing statuses') class WorkGroup(models.Model): """Work Group for privatization of the ressources""" name = models.CharField(_('name'), max_length=255) group = models.ForeignKey(Group, verbose_name=_('permissions group')) contacts = models.ManyToManyField(Contact, verbose_name=_('contacts'), blank=True, null=True) mailinglists = models.ManyToManyField(MailingList, verbose_name=_('mailing lists'), blank=True, null=True) newsletters = models.ManyToManyField(Newsletter, verbose_name=_('newsletters'), blank=True, null=True) def __unicode__(self): return self.name class Meta: verbose_name = _('workgroup') verbose_name_plural = _('workgroups')