Commits

Branko Vukelic  committed bfabebb

Initial commit

  • Participants

Comments (0)

Files changed (19)

+Copyright (c) 2013, Branko Vukelic
+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.
+
+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.
+========================
+django-pluggable-contact
+========================
+
+Pluggable Django app for contact forms with database-based inbox and support for
+topic-based sorting and sending to multiple recipients based on topic. Fully
+i18n-enabled and supports South migrations. It supports HTML emails and
+automatic conversion of HTML email to plain-text.
+
+Overview
+========
+
+django-pluggable-contact should be fairly flexible. It is designed to be able
+to handle anything from a very simple contact form to scenarios where you may
+want to send different messages to different recipients.
+
+Although it uses the Message models to store messages in the database, it is
+optional. You can send out a message even if the object is not saved. Topics are
+also optional. If you don't want the complexity of topics, you can disable them
+completely and they won't show up in the admin or the contact form.
+
+While more features will be added to this app as time goes, the overhead of
+additional features will be kept to minimum or eliminated by using appropriate
+settings for enabling them and having them disabled by default. On the other
+hand, this app will always be just a contacts app.
+
+TODO
+====
+
+1. Better docs
+
+2. Unit tests
+
+3. Add support for configuring multiple email accounts
+
+Installation
+============
+
+TODO
+
+Add the ``contact`` app to ``INSTALLED_APPS`` and call the syncdb management
+command or migrate using South.
+
+Map the URLs like so::
+
+    url(r'^contact/', include('contact.urls'))
+
+You can also include the URL in i18n_patterns as the URLs are fully
+translatable.
+
+Basic usage
+===========
+
+At very least, you want to override the provided basic templates. Please look
+at the ``contact/templates`` directory.
+
+Settings
+========
+
+TODO

File contact/__init__.py

Empty file added.

File contact/admin.py

+from __future__ import unicode_literals
+
+from django.contrib import admin
+
+from .models import Topic, TopicInbox, Message
+from .settings import SIMPLE
+
+
+class TopicInboxesInline(admin.StackedInline):
+    model = TopicInbox
+    extra = 0
+
+
+class TopicAdmin(admin.ModelAdmin):
+    pass
+
+
+if SIMPLE:
+    message_admin_excludes = ('topic',)
+else:
+    message_admin_excludes = []
+
+
+class MessageAdmin(admin.ModelAdmin):
+    exclude = message_admin_excludes
+
+
+if not SIMPLE:
+    admin.site.register(Topic, TopicAdmin)
+
+admin.site.register(Message, MessageAdmin)

File contact/email.py

+from django.core.mail import EmailMultiAlternatives
+from django.template.loader import render_to_string
+from django.utils.html import strip_tags
+from django.utils.text import wrap
+
+
+def send_html_email(subject, from_email, to, template, data, text_only=False,
+                    reply_to=None, send_separately=False):
+    """ Send HTML email based on a template """
+
+    # Helper func to actually perform the sending
+    def send_message(msg, html):
+        if not text_only:
+            # Attach the HTML part as needed
+            msg.attach_alternative(html, "text/html")
+        msg.send()
+
+    # ``to`` must be a list
+    if not type(to) is list:
+        to = [to]
+
+    if text_only:
+        # Consider the template a plain-text template and set HTML to None
+        html = None
+        text = render_to_string(template, data)
+    else:
+        # Render the HTML template and create the text version by stripping
+        # HTML tags from the resulting HTML
+        html = render_to_string(template, data)
+        text = wrap(strip_tags(html), 75)
+
+    # Extra headers (none by default)
+    headers = {}
+
+    # Set up Reply-To header if necessary
+    if reply_to:
+        headers['Reply-To'] = reply_to
+
+    if to.__iter__ and len(to) and send_separately:
+        # We need to send to each recipient a separate copy of the message.
+        for r in to:
+            send_message(
+                EmailMultiAlternatives(subject, text, from_email, [r],
+                                       headers=headers),
+                html
+            )
+    else:
+        # Send normally (note that when sending to multiple recipients, each
+        # recipient will be able to see the other recipients' emails. Use
+        # ``send_separately`` argument to send multiple messages.
+        send_message(
+            EmailMultiAlternatives(subject, text, from_email, to,
+                                   headers=headers),
+            html
+        )
+

File contact/forms.py

+from django import forms
+
+from .models import Message
+
+
+class SimpleContactForm(forms.ModelForm):
+    class Meta:
+        model = Message
+        fields = ('sender', 'subject', 'body')
+
+
+class TopicContactForm(forms.ModelForm):
+    class Meta:
+        model = Message

File contact/locale/sr_Latn/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: 2013-06-13 13:48+0200\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"
+
+#: .\contact\models.py:14
+msgid "name"
+msgstr "naziv"
+
+#: .\contact\models.py:17
+msgid "Identifier, must be unique and up to 10 characters."
+msgstr "Identifikator, mora biti jedinstven i sadržati najviše 10 karaktera."
+
+#: .\contact\models.py:20
+msgid "display name"
+msgstr "prikazani naziv"
+
+#: .\contact\models.py:26
+msgid "default topic"
+msgstr "podrazumevana svrha"
+
+#: .\contact\models.py:39 .\contact\models.py:46 .\contact\models.py:70
+msgid "topic"
+msgstr "svrha"
+
+#: .\contact\models.py:40
+msgid "topics"
+msgstr "svrha"
+
+#: .\contact\models.py:50
+msgid "email address"
+msgstr "email adresa"
+
+#: .\contact\models.py:55
+msgid "topic inbox"
+msgstr "inboks svrhe"
+
+#: .\contact\models.py:56
+msgid "topic inboxes"
+msgstr "inboksi srvhe"
+
+#: .\contact\models.py:64
+#, python-format
+msgid "The message is too short. It must have at least %s characters."
+msgstr "Poruka je prekratka. Poruka mora biti duža od %s karaktera."
+
+#: .\contact\models.py:76
+msgid "your email"
+msgstr "vaš email"
+
+#: .\contact\models.py:79
+msgid "subject"
+msgstr "tema"
+
+#: .\contact\models.py:83 .\contact\models.py:135
+msgid "message"
+msgstr "poruka"
+
+#: .\contact\models.py:87
+msgid "created_at"
+msgstr "vreme nastanka"
+
+#: .\contact\models.py:136
+msgid "messages"
+msgstr ""
+
+#: .\contact\urls.py:11
+msgid "^$"
+msgstr ""
+
+#: .\contact\urls.py:12
+msgid "^thank-you/$"
+msgstr ""
+
+#: .\contact\templates\contact\contact.html.py:6
+#, fuzzy
+msgid "Contact us"
+msgstr "kontakt"
+
+#: .\contact\templates\contact\thank_you.html.py:6
+msgid "Thank you for contacting us"
+msgstr ""
+
+#: .\contact\templates\contact\email\default.html.py:5
+msgid "sent you a message at"
+msgstr ""
+
+#: .\contact\templates\contact\email\default.html.py:7
+msgid "regarding"
+msgstr ""

File contact/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 'Topic'
+        db.create_table(u'contact_topic', (
+            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=10)),
+            ('display_name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=40)),
+            ('default', self.gf('django.db.models.fields.BooleanField')(default=False)),
+        ))
+        db.send_create_signal(u'contact', ['Topic'])
+
+        # Adding model 'TopicInbox'
+        db.create_table(u'contact_topicinbox', (
+            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('topic', self.gf('django.db.models.fields.related.ForeignKey')(related_name=u'inboxes', to=orm['contact.Topic'])),
+            ('address', self.gf('django.db.models.fields.EmailField')(max_length=75)),
+        ))
+        db.send_create_signal(u'contact', ['TopicInbox'])
+
+        # Adding model 'Message'
+        db.create_table(u'contact_message', (
+            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('topic', self.gf('django.db.models.fields.related.ForeignKey')(related_name=u'messages', null=True, on_delete=models.SET_NULL, to=orm['contact.Topic'])),
+            ('sender', self.gf('django.db.models.fields.EmailField')(max_length=75)),
+            ('subject', self.gf('django.db.models.fields.CharField')(max_length=100)),
+            ('body', self.gf('django.db.models.fields.TextField')()),
+            ('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
+        ))
+        db.send_create_signal(u'contact', ['Message'])
+
+
+    def backwards(self, orm):
+        # Deleting model 'Topic'
+        db.delete_table(u'contact_topic')
+
+        # Deleting model 'TopicInbox'
+        db.delete_table(u'contact_topicinbox')
+
+        # Deleting model 'Message'
+        db.delete_table(u'contact_message')
+
+
+    models = {
+        u'contact.message': {
+            'Meta': {'object_name': 'Message'},
+            'body': ('django.db.models.fields.TextField', [], {}),
+            'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'sender': ('django.db.models.fields.EmailField', [], {'max_length': '75'}),
+            'subject': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'topic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'messages'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['contact.Topic']"})
+        },
+        u'contact.topic': {
+            'Meta': {'object_name': 'Topic'},
+            'default': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'display_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '10'})
+        },
+        u'contact.topicinbox': {
+            'Meta': {'object_name': 'TopicInbox'},
+            'address': ('django.db.models.fields.EmailField', [], {'max_length': '75'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'topic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'inboxes'", 'to': u"orm['contact.Topic']"})
+        }
+    }
+
+    complete_apps = ['contact']

File contact/migrations/__init__.py

Empty file added.

File contact/models.py

+from __future__ import unicode_literals
+
+from django.core.exceptions import ValidationError
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+from django.utils import timezone
+
+from . import settings
+from .email import send_html_email
+
+
+class Topic(models.Model):
+    name = models.CharField(
+        _('name'),
+        max_length=10,
+        unique=True,
+        help_text=_('Identifier, must be unique and up to 10 characters.')
+    )
+    display_name = models.CharField(
+        _('display name'),
+        max_length=40,
+        unique=True,
+        help_text='Unique label used in forms, up to 40 characters.'
+    )
+    default = models.BooleanField(
+        _('default topic'),
+        default=False,
+        help_text='Default topics will receive messages when a message has no '
+        'topic'
+    )
+
+    def message_count(self):
+        return self.messages.count()
+
+    def __unicode__(self):
+        return self.name
+
+    class Meta:
+        verbose_name = _('topic')
+        verbose_name_plural = _('topics')
+
+
+class TopicInbox(models.Model):
+    topic = models.ForeignKey(
+        Topic,
+        verbose_name=_('topic'),
+        related_name='inboxes'
+    )
+    address = models.EmailField(
+        _('email address'),
+
+    )
+
+    class Meta:
+        verbose_name = _('topic inbox')
+        verbose_name_plural = _('topic inboxes')
+
+
+class Message(models.Model):
+    @staticmethod
+    def message_length(s):
+        if len(s) < settings.MIN_LENGTH:
+            raise ValidationError(
+                _('The message is too short. '
+                  'It must have at least %s characters.')
+            )
+
+    topic = models.ForeignKey(
+        Topic,
+        verbose_name=_('topic'),
+        related_name='messages',
+        null=True,
+        on_delete=models.SET_NULL
+    )
+    sender = models.EmailField(
+        _('your email'),
+    )
+    subject = models.CharField(
+        _('subject'),
+        max_length=100
+    )
+    body = models.TextField(
+        _('message'),
+        validators=[message_length]
+    )
+    created_at = models.DateTimeField(
+        _('created at'),
+        auto_now_add=True,
+        editable=False
+    )
+
+    def send(self, template='contact/email/default.html', extra_context={},
+             text_only=False, recipients=None):
+
+        if settings.SIMPLE:
+            recipients = settings.DEFAULT_RECIPIENT
+        elif not recipients:
+            # If there are no explicitly specified recipients...
+
+            if self.topic:
+                # We have a topic, so send email to its inboxes
+                recipients = [i.address for i in self.topic.inboxes.all()]
+            else:
+                # We don't have a message topic, so send to default topics
+                # (if any)
+                default_topics = Topic.objects.filter(default=True).all()
+                recipients = [i.address for t in default_topics for i in t.inboxes]
+
+            # If there is no message topic and there are no default topics, use
+            # the address specified by ``contact.settings.DEFAULT_RECIPIENT``
+            recipients = recipients or [settings.DEFAULT_RECIPIENT]
+
+        # Add this message object to extra context and use it as template
+        # context
+        context = extra_context
+        context['message'] = self
+        context['timestamp'] = timezone.now()
+
+        # Send email
+        send_html_email(
+            subject=self.subject,
+            from_email=settings.SENDER,
+            to=recipients,
+            template=template,
+            data=context,
+            reply_to=self.sender,
+            text_only=text_only,
+            send_separately=True
+        )
+
+    def __unicode__(self):
+        return '%s from %s' % (
+            self.subject,
+            self.sender
+        )
+
+    class Meta:
+        verbose_name = _('message')
+        verbose_name_plural = _('messages')

File contact/settings.py

+from django.conf import settings
+
+# Minimum message length for contact forms and Message model
+MIN_LENGTH = getattr(settings, 'CONTACT_MIN_MESSAGE_LENGTH', 10)
+
+# Sender address for contact messages. Defaults to settings.DEFAULT_FROM_EMAIL.
+SENDER = getattr(settings, 'CONTACT_SENDER', settings.DEFAULT_FROM_EMAIL)
+
+# Default recipient for contact messages. Defaults to SENDER.
+DEFAULT_RECIPIENT = getattr(settings, 'CONTACT_DEFAULT_RECIPIENT', SENDER)
+
+# Simple mode. Disables access topics and selects SimpleContactForm for the
+# contact view if True. Default is True.
+SIMPLE = getattr(settings, 'CONTACT_SIMPLE', True)
+
+# Whether to save messages in the database. Note that disabling this will still
+# create the Message model and its database table.
+DB_INBOX = getattr(settings, 'DB_INBOX', True)
+

File contact/templates/contact/base.html

+<!DOCTYPE html>
+<html>
+<head>
+    <title>Contact</title>
+</head>
+<body>
+    {% block main %}
+    {% endblock %}
+</body>
+</html>

File contact/templates/contact/contact.html

+{% extends 'contact/base.html' %}
+
+{% load i18n %}
+
+{% block main %}
+    <h2>{% trans 'Contact us' %}</h2>
+
+    <form action="{% url 'contact' %}" method="POST">
+        {% csrf_token %}
+        {{ form }}
+        <button>Send</button>
+    </form>
+{% endblock %}

File contact/templates/contact/email/_signature.html

+<p>Sent from contact form</p>

File contact/templates/contact/email/default.html

+{% load i18n %}
+<html>
+<body>
+{% block content %}
+<p><b>{{ message.sender }} {% trans 'sent you a message at' %}
+{{ timestamp }}
+{% if message.topic %}{% trans 'regarding' %} {{ message.topic.display_name }}{% endif %}
+</b></p>
+
+{{ message.body|linebreaks }}
+
+{% endblock %}
+
+{% block signature %}{% include 'contact/email/_signature.html' %}{% endblock %}
+</body>
+</html>
+
+{% comment %} Keep indentation at 0 and linebreaks to minimum. {% endcomment %}

File contact/templates/contact/thank_you.html

+{% extends 'contact/base.html' %}
+
+{% load i18n %}
+
+{% block main %}
+    <h2>{% trans 'Thank you for contacting us' %}</h2>
+{% endblock %}

File contact/tests.py

+"""
+This file demonstrates writing tests using the unittest module. These will pass
+when you run "manage.py test".
+
+Replace this with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+
+class SimpleTest(TestCase):
+    def test_basic_addition(self):
+        """
+        Tests that 1 + 1 always equals 2.
+        """
+        self.assertEqual(1 + 1, 2)

File contact/urls.py

+from __future__ import unicode_literals
+
+from django.conf.urls import patterns, url
+from django.utils.translation import ugettext_lazy as _
+
+from .views import ContactView, ThankYouView
+
+
+urlpatterns = (
+    '',
+    url(_(r'^$'), ContactView.as_view(), name='contact'),
+    url(_(r'^thank-you/$'), ThankYouView.as_view(), name='thank_you'),
+)

File contact/views.py

+from __future__ import unicode_literals
+
+from django.views.generic import FormView, TemplateView
+from django.core.urlresolvers import reverse
+
+from .forms import SimpleContactForm, TopicContactForm
+from .settings import SIMPLE, DB_INBOX
+
+
+class ContactView(FormView):
+    template_name = 'contact/contact.html'
+
+    def get_form_class(self):
+        if SIMPLE:
+            return SimpleContactForm
+        else:
+            return TopicContactForm
+
+    def get_success_url(self):
+        return reverse('thank_you')
+
+    def send_message(self, message):
+        # Send the message with default options
+        message.send()
+
+    def form_valid(self, form):
+        # Create a message instance
+        if DB_INBOX:
+            # If DB_INBOX is set to True, simply commit the message to database
+            message = form.save()
+        else:
+            # DB_INBOX is set to False, so do not commit to database
+            message = form.save(commit=False)
+
+        self.send_message(message)
+
+        return super(ContactView, self).form_valid(form)
+
+
+class ThankYouView(TemplateView):
+    template_name = 'contact/thank_you.html'