Commits

Guilherme Gondim  committed 79d2a0b

first commit

  • Participants
  • Tags 0.1

Comments (0)

Files changed (19)

+syntax: glob
+*.pyc
+*.bak
+*.swp
+*~
+._*
+.\#*
+*\#
+dist/
+build/
+*.egg-info/
+[main]
+host = https://www.transifex.net
+
+[django-informativo.master]
+type = PO
+file_filter = informativo/locale/<lang>/LC_MESSAGES/django.po
+source_file = informativo/locale/en/LC_MESSAGES/django.po
+source_lang = en
+
+Django Informativo was created in 2012 by Guilherme (semente). Following is a
+(probably incomplete) list of its contributors:
+
+  * Guilherme Gondim <semente+django-informativo@taurinus.org>. 
+  * Your Name Here  ;-)
+Copyright (c) 2012 Guilherme Gondim and individual contributors.
+Copyright (c) 2005-2012 Django Software Foundation and individual contributors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    1. Redistributions of source code must retain the above copyright notice, 
+       this list of conditions and the following disclaimer.
+    
+    2. Redistributions in binary form must reproduce the above copyright 
+       notice, this list of conditions and the following disclaimer in the
+       documentation and/or other materials provided with the distribution.
+
+    3. Neither the name of Django nor the names of its contributors may be used
+       to endorse or promote products derived from this software without
+       specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+include AUTHORS
+include LICENSE
+include README.rst
+#recursive-include docs *
+recursive-include informativo/templates *
+===================
+Django Informativo
+===================
+
+**Django Informativo** is a pluggable application for `Django Web Framework`_.
+
+Project page
+    http://bitbucket.org/semente/django-informativo
+Translations
+    https://www.transifex.net/projects/p/django-informativo/
+
+.. _`Django Web Framework`: http://www.djangoproject.com
+
+
+Installing & Setup
+==================
+
+TODO
+
+
+License
+=======
+
+You should have received a copy of the BSD License along with this program; see
+the file LICENSE.

File informativo/__init__.py

+VERSION = (0, 1)
+
+
+def get_version():
+    """Returns the version as a human-format string.
+    """
+    return '.'.join([str(i) for i in VERSION])
+
+
+__author__ = 'See the file AUTHORS'
+__license__ = 'BSD License'
+__url__ = 'https://bitbucket.org/semente/django-informativo'
+__version__ = get_version()

File informativo/admin.py

+from django.conf import settings
+from django.contrib import admin
+
+from informativo.models import Contact, MailingList, Newsletter
+
+
+class ContactAdmin(admin.ModelAdmin):
+    pass
+
+admin.site.register(Contact, ContactAdmin)
+
+
+class MailingListAdmin(admin.ModelAdmin):
+    pass
+
+admin.site.register(MailingList, MailingListAdmin)
+
+
+class NewsletterAdmin(admin.ModelAdmin):
+    pass
+
+admin.site.register(Newsletter, NewsletterAdmin)
+
+
+if settings.DEBUG:
+    from informativo.models import ContactMailingStatus
+    admin.site.register(ContactMailingStatus)

File informativo/locale/en/LC_MESSAGES/django.po

+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2012-05-03 15:32-0300\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: models.py:13 models.py:20
+msgid "canceled"
+msgstr ""
+
+#: models.py:14
+msgid "waiting sending"
+msgstr ""
+
+#: models.py:15 models.py:21
+msgid "sending"
+msgstr ""
+
+#: models.py:16 models.py:22
+msgid "sent"
+msgstr ""
+
+#: models.py:23
+msgid "failed"
+msgstr ""
+
+#: models.py:28
+msgid "e-mail"
+msgstr ""
+
+#: models.py:29
+msgid "first name"
+msgstr ""
+
+#: models.py:30
+msgid "last name"
+msgstr ""
+
+#: models.py:33 models.py:52
+msgid "mailing lists"
+msgstr ""
+
+#: models.py:38 models.py:123
+msgid "contact"
+msgstr ""
+
+#: models.py:39
+msgid "contacts"
+msgstr ""
+
+#: models.py:47
+msgid "name"
+msgstr ""
+
+#: models.py:48
+msgid "description"
+msgstr ""
+
+#: models.py:51 models.py:67
+msgid "mailing list"
+msgstr ""
+
+#: models.py:61
+msgid "title"
+msgstr ""
+
+#: models.py:63
+msgid "Used on the e-mail's subject"
+msgstr ""
+
+#: models.py:65
+msgid "HTML content"
+msgstr ""
+
+#: models.py:66
+msgid "text content"
+msgstr ""
+
+#: models.py:69
+msgid "sending date"
+msgstr ""
+
+#: models.py:73 models.py:130
+msgid "status"
+msgstr ""
+
+#: models.py:80 models.py:127
+msgid "newsletter"
+msgstr ""
+
+#: models.py:81
+msgid "newsletters"
+msgstr ""
+
+#: models.py:136
+msgid "contact mailing status"
+msgstr ""
+
+#: models.py:137
+msgid "contact mailing statuses"
+msgstr ""

File informativo/locale/pt_BR/LC_MESSAGES/django.po

+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# 
+# Translators:
+# Guilherme Gondim <semente+transifex@taurinus.org>, 2012.
+msgid ""
+msgstr ""
+"Project-Id-Version: Django Informativo\n"
+"Report-Msgid-Bugs-To: https://bitbucket.org/semente/django-informativo/issues\n"
+"POT-Creation-Date: 2012-05-03 15:32-0300\n"
+"PO-Revision-Date: 2012-05-03 19:10+0000\n"
+"Last-Translator: Guilherme Gondim <semente+transifex@taurinus.org>\n"
+"Language-Team: Portuguese (Brazil) (http://www.transifex.net/projects/p/django-informativo/language/pt_BR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: pt_BR\n"
+"Plural-Forms: nplurals=2; plural=(n > 1)\n"
+
+#: models.py:13 models.py:20
+msgid "canceled"
+msgstr "cancelado"
+
+#: models.py:14
+msgid "waiting sending"
+msgstr "esperando envio"
+
+#: models.py:15 models.py:21
+msgid "sending"
+msgstr "enviando"
+
+#: models.py:16 models.py:22
+msgid "sent"
+msgstr "enviado"
+
+#: models.py:23
+msgid "failed"
+msgstr "falhou"
+
+#: models.py:28
+msgid "e-mail"
+msgstr "e-mail"
+
+#: models.py:29
+msgid "first name"
+msgstr "primeiro nome"
+
+#: models.py:30
+msgid "last name"
+msgstr "último nome"
+
+#: models.py:33 models.py:52
+msgid "mailing lists"
+msgstr "listas de endereços"
+
+#: models.py:38 models.py:123
+msgid "contact"
+msgstr "contato"
+
+#: models.py:39
+msgid "contacts"
+msgstr "contatos"
+
+#: models.py:47
+msgid "name"
+msgstr "nome"
+
+#: models.py:48
+msgid "description"
+msgstr "descrição"
+
+#: models.py:51 models.py:67
+msgid "mailing list"
+msgstr "lista de endereços"
+
+#: models.py:61
+msgid "title"
+msgstr "título"
+
+#: models.py:63
+msgid "Used on the e-mail's subject"
+msgstr "Utilizado no assunto do e-mail"
+
+#: models.py:65
+msgid "HTML content"
+msgstr "conteúdo HTML"
+
+#: models.py:66
+msgid "text content"
+msgstr "conteúdo texto"
+
+#: models.py:69
+msgid "sending date"
+msgstr "data de envio"
+
+#: models.py:73 models.py:130
+msgid "status"
+msgstr "status"
+
+#: models.py:80 models.py:127
+msgid "newsletter"
+msgstr "informativo"
+
+#: models.py:81
+msgid "newsletters"
+msgstr "informativos"
+
+#: models.py:136
+msgid "contact mailing status"
+msgstr ""
+
+#: models.py:137
+msgid "contact mailing statuses"
+msgstr ""

File informativo/management/__init__.py

Empty file added.

File informativo/management/commands/sendnewsletters.py

+from django.core.management.base import BaseCommand
+
+from informativo.models import Newsletter
+
+
+class Command(BaseCommand):
+    help = 'Send newsletters in "waiting sending" status'
+
+    def handle(self, *args, **options):
+        newsletters = Newsletter.objects.filter(status='waiting')
+        for newsletter in newsletters:
+            newsletter.send()

File informativo/migrations/0001_initial.py

+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding model 'Contact'
+        db.create_table('informativo_contact', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('email', self.gf('django.db.models.fields.EmailField')(max_length=75)),
+            ('first_name', self.gf('django.db.models.fields.CharField')(max_length=64)),
+            ('last_name', self.gf('django.db.models.fields.CharField')(max_length=64)),
+        ))
+        db.send_create_signal('informativo', ['Contact'])
+
+        # Adding M2M table for field mailing_lists on 'Contact'
+        db.create_table('informativo_contact_mailing_lists', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('contact', models.ForeignKey(orm['informativo.contact'], null=False)),
+            ('mailinglist', models.ForeignKey(orm['informativo.mailinglist'], null=False))
+        ))
+        db.create_unique('informativo_contact_mailing_lists', ['contact_id', 'mailinglist_id'])
+
+        # Adding model 'MailingList'
+        db.create_table('informativo_mailinglist', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=64)),
+            ('description', self.gf('django.db.models.fields.TextField')(blank=True)),
+        ))
+        db.send_create_signal('informativo', ['MailingList'])
+
+        # Adding model 'Newsletter'
+        db.create_table('informativo_newsletter', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('title', self.gf('django.db.models.fields.CharField')(max_length=128)),
+            ('html_content', self.gf('django.db.models.fields.TextField')(blank=True)),
+            ('text_content', self.gf('django.db.models.fields.TextField')(blank=True)),
+            ('mailing_list', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['informativo.MailingList'])),
+            ('sending_date', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)),
+            ('status', self.gf('django.db.models.fields.CharField')(default='waiting', max_length=16)),
+        ))
+        db.send_create_signal('informativo', ['Newsletter'])
+
+        # Adding model 'ContactMailingStatus'
+        db.create_table('informativo_contactmailingstatus', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('contact', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['informativo.Contact'])),
+            ('newsletter', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['informativo.Newsletter'])),
+            ('status', self.gf('django.db.models.fields.CharField')(max_length=16)),
+        ))
+        db.send_create_signal('informativo', ['ContactMailingStatus'])
+
+    def backwards(self, orm):
+        # Deleting model 'Contact'
+        db.delete_table('informativo_contact')
+
+        # Removing M2M table for field mailing_lists on 'Contact'
+        db.delete_table('informativo_contact_mailing_lists')
+
+        # Deleting model 'MailingList'
+        db.delete_table('informativo_mailinglist')
+
+        # Deleting model 'Newsletter'
+        db.delete_table('informativo_newsletter')
+
+        # Deleting model 'ContactMailingStatus'
+        db.delete_table('informativo_contactmailingstatus')
+
+    models = {
+        'informativo.contact': {
+            'Meta': {'ordering': "('-id',)", 'object_name': 'Contact'},
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'mailing_lists': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'subscribers'", 'symmetrical': 'False', 'to': "orm['informativo.MailingList']"})
+        },
+        'informativo.contactmailingstatus': {
+            'Meta': {'ordering': "('-id',)", 'object_name': 'ContactMailingStatus'},
+            'contact': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['informativo.Contact']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['informativo.Newsletter']"}),
+            'status': ('django.db.models.fields.CharField', [], {'max_length': '16'})
+        },
+        'informativo.mailinglist': {
+            'Meta': {'ordering': "('-id',)", 'object_name': 'MailingList'},
+            'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64'})
+        },
+        'informativo.newsletter': {
+            'Meta': {'ordering': "('-id',)", 'object_name': 'Newsletter'},
+            'html_content': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'mailing_list': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['informativo.MailingList']"}),
+            'sending_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'status': ('django.db.models.fields.CharField', [], {'default': "'waiting'", 'max_length': '16'}),
+            'text_content': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '128'})
+        }
+    }
+
+    complete_apps = ['informativo']

File informativo/migrations/__init__.py

Empty file added.

File informativo/models.py

+import smtplib
+
+from datetime import datetime
+
+from django.conf import settings
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+
+from informativo.utils import send_mass_mail
+
+
+NEWSLETTER_STATUS = (
+    ('canceled', _('canceled')),
+    ('waiting', _('waiting sending')),
+    ('sending', _('sending')),
+    ('sent', _('sent')),
+)
+
+CONTACT_MAILING_STATUS = (
+    ('canceled', _('canceled')),
+    ('sending', _('sending')),
+    ('sent', _('sent')),
+    ('failed', _('failed')),
+)
+
+
+class Contact(models.Model):
+    email = models.EmailField(_('e-mail'))
+    first_name = models.CharField(_('first name'), max_length=64)
+    last_name = models.CharField(_('last name'), max_length=64)
+    mailing_lists = models.ManyToManyField(
+        'informativo.MailingList',
+        verbose_name=_('mailing lists'),
+        related_name='subscribers'
+    )
+
+    class Meta:
+        verbose_name = _('contact')
+        verbose_name_plural = _('contacts')
+        ordering = ('-id',)
+
+    def __unicode__(self):
+        return u'%s %s <%s>' % (self.first_name, self.last_name, self.email)
+
+
+class MailingList(models.Model):
+    name = models.CharField(_('name'), max_length=64)
+    description = models.TextField(_('description'), blank=True)
+
+    class Meta:
+        verbose_name = _('mailing list')
+        verbose_name_plural = _('mailing lists')
+        ordering = ('-id',)
+
+    def __unicode__(self):
+        return unicode(self.name)
+
+
+class Newsletter(models.Model):
+    title = models.CharField(
+        _('title'),
+        max_length=128,
+        help_text=_("Used on the e-mail's subject")
+    )
+    html_content = models.TextField(_('HTML content'), blank=True)
+    text_content = models.TextField(_('text content'), blank=True)
+    mailing_list = models.ForeignKey(MailingList, verbose_name=_('mailing list'))
+    sending_date = models.DateTimeField(
+        _('sending date'),
+        default=datetime.now,
+    )
+    status = models.CharField(
+        _('status'),
+        max_length=16,
+        choices=NEWSLETTER_STATUS,
+        default='waiting'
+    )
+
+    class Meta:
+        verbose_name = _('newsletter')
+        verbose_name_plural = _('newsletters')
+        ordering = ('-id',)
+
+    def __unicode__(self):
+        return unicode(self.title)
+
+    def send(self):
+        subject = self.title
+        text_content, html_content = self.text_content, self.html_content
+        from_email = settings.DEFAULT_FROM_EMAIL
+
+        self.status = 'sending'
+        self.save()
+
+        for recipient in self.mailing_list.subscribers.all():
+            contact_status, created = ContactMailingStatus.objects.get_or_create(
+                contact=recipient,
+                newsletter=self,
+                defaults={'status': 'sending'}
+            )
+
+            # Do not send the same newsletter to the same recipient twice
+            if contact_status.status in ('canceled', 'sent'):
+                continue
+
+            # Send mail
+            datatuple = ((subject, text_content, html_content, from_email, [recipient.email]),)
+            try:
+                send_mass_mail(datatuple)
+                contact_status.status = 'sent'
+                status = 'sent'
+            except smtplib.SMTPException:
+                contact_status.status = 'failed'
+            contact_status.save()
+
+        self.status = 'sent'
+        self.save()
+
+
+class ContactMailingStatus(models.Model):
+    contact = models.ForeignKey(
+        'informativo.Contact',
+        verbose_name=_('contact')
+    )
+    newsletter = models.ForeignKey(
+        'informativo.Newsletter',
+        verbose_name=_('newsletter')
+    )
+    status = models.CharField(
+        _('status'),
+        max_length=16,
+        choices=CONTACT_MAILING_STATUS
+    )
+
+    class Meta:
+        verbose_name = _('contact mailing status')
+        verbose_name_plural = _('contact mailing statuses')
+        ordering = ('-id',)
+
+    def __unicode__(self):
+        status = self.get_status_display()
+        return u'"%s" to %s %s' % (self.newsletter, self.contact, status)

File informativo/tests.py

+# TODO

File informativo/utils.py

+from django.core.mail import get_connection, EmailMultiAlternatives
+
+
+def send_mass_mail(datatuple, fail_silently=False, user=None, password=None, connection=None):
+    """`django.core.mail.send_mass_mail` withHTML support
+
+    Given a datatuple of (subject, text_content, html_content, from_email,
+    recipient_list), sends each message to each recipient list. Returns the
+    number of emails sent.
+
+    If from_email is None, the DEFAULT_FROM_EMAIL setting is used.
+    If auth_user and auth_password are set, they're used to log in.
+    If auth_user is None, the EMAIL_HOST_USER setting is used.
+    If auth_password is None, the EMAIL_HOST_PASSWORD setting is used.
+
+    """
+    connection = connection or get_connection(
+        username=user, password=password, fail_silently=fail_silently
+    )
+
+    messages = []
+    for subject, text, html, from_email, recipient in datatuple:
+        message = EmailMultiAlternatives(subject, text, from_email, recipient)
+        if html: message.attach_alternative(html, 'text/html')
+        messages.append(message)
+
+    return connection.send_messages(messages)

File informativo/views.py

+# Create your views here.
+#/usr/bin/env python
+
+import codecs
+import os
+import sys
+
+from setuptools import setup, find_packages
+
+
+if 'publish' in sys.argv:
+    os.system('python setup.py sdist upload')
+    sys.exit()
+
+read = lambda filepath: codecs.open(filepath, 'r', 'utf-8').read()
+
+
+# Dynamically calculate the version based on informativo.VERSION.
+version = __import__('informativo').get_version()
+
+setup(
+    name='django-informativo',
+    version=version,
+    description='Newsletter application for Django projects',
+    long_description=read(os.path.join(os.path.dirname(__file__), 'README.rst')),
+    author='Guilherme Gondim',
+    author_email='semente+django-informativo@taurinus.org',
+    maintainer='Guilherme Gondim',
+    maintainer_email='semente+django-informativo@taurinus.org',
+    license='BSD License',
+    url='http://bitbucket.org/semente/django-informativo/',
+    packages=find_packages(),
+    zip_safe=False,
+    include_package_data=True,
+    classifiers=[
+        'Development Status :: 5 - Production/Stable',
+        'Environment :: Web Environment',
+        'Framework :: Django',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: BSD License',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+        'Topic :: Software Development :: Libraries :: Python Modules',
+    ],
+)