aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorbnewbold <bnewbold@robocracy.org>2011-05-07 15:11:11 -0400
committerbnewbold <bnewbold@robocracy.org>2011-05-07 15:11:11 -0400
commit647feb73a2c3cbcc30244a022da04f5be3aa3346 (patch)
tree9baad9d881cefcca4ca21f7713bceb06140b40a3
parent8d0cfc4a8107adf6c5f64ad8d03a0a30c92cc4c2 (diff)
downloadpiccast-647feb73a2c3cbcc30244a022da04f5be3aa3346.zip
piccast-647feb73a2c3cbcc30244a022da04f5be3aa3346.tar.gz
support contact form [WIP]
-rw-r--r--piccast/contact_form/LICENSE.txt28
-rw-r--r--piccast/contact_form/__init__.py0
-rw-r--r--piccast/contact_form/forms.py259
-rw-r--r--piccast/contact_form/urls.py28
-rw-r--r--piccast/contact_form/views.py92
-rw-r--r--piccast/settings.py.example7
-rw-r--r--piccast/templates/contact_form/contact_form.html13
-rw-r--r--piccast/templates/contact_form/contact_form.txt7
-rw-r--r--piccast/templates/contact_form/contact_form_sent.html11
-rw-r--r--piccast/templates/contact_form/contact_form_subject.txt1
-rw-r--r--piccast/urls.py1
11 files changed, 447 insertions, 0 deletions
diff --git a/piccast/contact_form/LICENSE.txt b/piccast/contact_form/LICENSE.txt
new file mode 100644
index 0000000..5fb0014
--- /dev/null
+++ b/piccast/contact_form/LICENSE.txt
@@ -0,0 +1,28 @@
+Copyright (c) 2007, James Bennett
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * 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.
+ * Neither the name of the author nor the names of other
+ 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.
diff --git a/piccast/contact_form/__init__.py b/piccast/contact_form/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/piccast/contact_form/__init__.py
diff --git a/piccast/contact_form/forms.py b/piccast/contact_form/forms.py
new file mode 100644
index 0000000..20ae360
--- /dev/null
+++ b/piccast/contact_form/forms.py
@@ -0,0 +1,259 @@
+"""
+A base contact form for allowing users to send email messages through
+a web interface, and a subclass demonstrating useful functionality.
+
+"""
+
+
+from django import forms
+from django.conf import settings
+from django.core.mail import send_mail
+from django.template import loader
+from django.template import RequestContext
+from django.contrib.sites.models import Site
+
+
+# I put this on all required fields, because it's easier to pick up
+# on them with CSS or JavaScript if they have a class of "required"
+# in the HTML. Your mileage may vary.
+attrs_dict = { 'class': 'required' }
+
+
+class ContactForm(forms.Form):
+ """
+ Base contact form class from which all contact form classes should
+ inherit.
+
+ If you don't need any custom functionality, you can simply use
+ this form to provide basic contact functionality; it will collect
+ name, email address and message.
+
+ The ``contact_form`` view included in this application knows how
+ to work with this form and can handle many types of subclasses as
+ well (see below for a discussion of the important points), so in
+ many cases it will be all that you need. If you'd like to use this
+ form or a subclass of it from one of your own views, just do the
+ following:
+
+ 1. When you instantiate the form, pass the current ``HttpRequest``
+ object to the constructor as the keyword argument ``request``;
+ this is used internally by the base implementation, and also
+ made available so that subclasses can add functionality which
+ relies on inspecting the request.
+
+ 2. To send the message, call the form's ``save`` method, which
+ accepts the keyword argument ``fail_silently`` and defaults it
+ to ``False``. This argument is passed directly to
+ ``send_mail``, and allows you to suppress or raise exceptions
+ as needed for debugging. The ``save`` method has no return
+ value.
+
+ Other than that, treat it like any other form; validity checks and
+ validated data are handled normally, through the ``is_valid``
+ method and the ``cleaned_data`` dictionary.
+
+
+ Base implementation
+ -------------------
+
+ Under the hood, this form uses a somewhat abstracted interface in
+ order to make it easier to subclass and add functionality. There
+ are several important attributes subclasses may want to look at
+ overriding, all of which will work (in the base implementation) as
+ either plain attributes or as callable methods:
+
+ * ``from_email`` -- used to get the address to use in the
+ ``From:`` header of the message. The base implementation returns
+ the value of the ``DEFAULT_FROM_EMAIL`` setting.
+
+ * ``message`` -- used to get the message body as a string. The
+ base implementation renders a template using the form's
+ ``cleaned_data`` dictionary as context.
+
+ * ``recipient_list`` -- used to generate the list of recipients
+ for the message. The base implementation returns the email
+ addresses specified in the ``MANAGERS`` setting.
+
+ * ``subject`` -- used to generate the subject line for the
+ message. The base implementation returns the string 'Message
+ sent through the web site', with the name of the current
+ ``Site`` prepended.
+
+ * ``template_name`` -- used by the base ``message`` method to
+ determine which template to use for rendering the
+ message. Default is ``contact_form/contact_form.txt``.
+
+ Internally, the base implementation ``_get_message_dict`` method
+ collects ``from_email``, ``message``, ``recipient_list`` and
+ ``subject`` into a dictionary, which the ``save`` method then
+ passes directly to ``send_mail`` as keyword arguments.
+
+ Particularly important is the ``message`` attribute, with its base
+ implementation as a method which renders a template; because it
+ passes ``cleaned_data`` as the template context, any additional
+ fields added by a subclass will automatically be available in the
+ template. This means that many useful subclasses can get by with
+ just adding a few fields and possibly overriding
+ ``template_name``.
+
+ Much useful functionality can be achieved in subclasses without
+ having to override much of the above; adding additional validation
+ methods works the same as any other form, and typically only a few
+ items -- ``recipient_list`` and ``subject_line``, for example,
+ need to be overridden to achieve customized behavior.
+
+
+ Other notes for subclassing
+ ---------------------------
+
+ Subclasses which want to inspect the current ``HttpRequest`` to
+ add functionality can access it via the attribute ``request``; the
+ base ``message`` takes advantage of this to use ``RequestContext``
+ when rendering its template. See the ``AkismetContactForm``
+ subclass in this file for an example of using the request to
+ perform additional validation.
+
+ Subclasses which override ``__init__`` need to accept ``*args``
+ and ``**kwargs``, and pass them via ``super`` in order to ensure
+ proper behavior.
+
+ Subclasses should be careful if overriding ``_get_message_dict``,
+ since that method **must** return a dictionary suitable for
+ passing directly to ``send_mail`` (unless ``save`` is overridden
+ as well).
+
+ Overriding ``save`` is relatively safe, though remember that code
+ which uses your form will expect ``save`` to accept the
+ ``fail_silently`` keyword argument. In the base implementation,
+ that argument defaults to ``False``, on the assumption that it's
+ far better to notice errors than to silently not send mail from
+ the contact form (see also the Zen of Python: "Errors should never
+ pass silently, unless explicitly silenced").
+
+ """
+ def __init__(self, data=None, files=None, request=None, *args, **kwargs):
+ if request is None:
+ raise TypeError("Keyword argument 'request' must be supplied")
+ super(ContactForm, self).__init__(data=data, files=files, *args, **kwargs)
+ self.request = request
+
+ name = forms.CharField(max_length=100,
+ widget=forms.TextInput(attrs=attrs_dict),
+ label=u'Your name')
+ email = forms.EmailField(widget=forms.TextInput(attrs=dict(attrs_dict,
+ maxlength=200)),
+ label=u'Your email address')
+ body = forms.CharField(widget=forms.Textarea(attrs=attrs_dict),
+ label=u'Your message')
+
+ from_email = settings.DEFAULT_FROM_EMAIL
+
+ recipient_list = [mail_tuple[1] for mail_tuple in settings.MANAGERS]
+
+ subject_template_name = "contact_form/contact_form_subject.txt"
+
+ template_name = 'contact_form/contact_form.txt'
+
+ def message(self):
+ """
+ Render the body of the message to a string.
+
+ """
+ if callable(self.template_name):
+ template_name = self.template_name()
+ else:
+ template_name = self.template_name
+ return loader.render_to_string(template_name,
+ self.get_context())
+
+ def subject(self):
+ """
+ Render the subject of the message to a string.
+
+ """
+ subject = loader.render_to_string(self.subject_template_name,
+ self.get_context())
+ return ''.join(subject.splitlines())
+
+ def get_context(self):
+ """
+ Return the context used to render the templates for the email
+ subject and body.
+
+ By default, this context includes:
+
+ * All of the validated values in the form, as variables of the
+ same names as their fields.
+
+ * The current ``Site`` object, as the variable ``site``.
+
+ * Any additional variables added by context processors (this
+ will be a ``RequestContext``).
+
+ """
+ if not self.is_valid():
+ raise ValueError("Cannot generate Context from invalid contact form")
+ return RequestContext(self.request,
+ dict(self.cleaned_data,
+ site=Site.objects.get_current()))
+
+ def get_message_dict(self):
+ """
+ Generate the various parts of the message and return them in a
+ dictionary, suitable for passing directly as keyword arguments
+ to ``django.core.mail.send_mail()``.
+
+ By default, the following values are returned:
+
+ * ``from_email``
+
+ * ``message``
+
+ * ``recipient_list``
+
+ * ``subject``
+
+ """
+ if not self.is_valid():
+ raise ValueError("Message cannot be sent from invalid contact form")
+ message_dict = {}
+ for message_part in ('from_email', 'message', 'recipient_list', 'subject'):
+ attr = getattr(self, message_part)
+ message_dict[message_part] = callable(attr) and attr() or attr
+ return message_dict
+
+ def save(self, fail_silently=False):
+ """
+ Build and send the email message.
+
+ """
+ send_mail(fail_silently=fail_silently, **self.get_message_dict())
+
+
+class AkismetContactForm(ContactForm):
+ """
+ Contact form which doesn't add any extra fields, but does add an
+ Akismet spam check to the validation routine.
+
+ Requires the setting ``AKISMET_API_KEY``, which should be a valid
+ Akismet API key.
+
+ """
+ def clean_body(self):
+ """
+ Perform Akismet validation of the message.
+
+ """
+ if 'body' in self.cleaned_data and getattr(settings, 'AKISMET_API_KEY', ''):
+ from akismet import Akismet
+ from django.utils.encoding import smart_str
+ akismet_api = Akismet(key=settings.AKISMET_API_KEY,
+ blog_url='http://%s/' % Site.objects.get_current().domain)
+ if akismet_api.verify_key():
+ akismet_data = { 'comment_type': 'comment',
+ 'referer': self.request.META.get('HTTP_REFERER', ''),
+ 'user_ip': self.request.META.get('REMOTE_ADDR', ''),
+ 'user_agent': self.request.META.get('HTTP_USER_AGENT', '') }
+ if akismet_api.comment_check(smart_str(self.cleaned_data['body']), data=akismet_data, build_data=True):
+ raise forms.ValidationError(u"Akismet thinks this message is spam")
+ return self.cleaned_data['body']
diff --git a/piccast/contact_form/urls.py b/piccast/contact_form/urls.py
new file mode 100644
index 0000000..f80c27f
--- /dev/null
+++ b/piccast/contact_form/urls.py
@@ -0,0 +1,28 @@
+"""
+Example URLConf for a contact form.
+
+Because the ``contact_form`` view takes configurable arguments, it's
+recommended that you manually place it somewhere in your URL
+configuration with the arguments you want. If you just prefer the
+default, however, you can hang this URLConf somewhere in your URL
+hierarchy (for best results with the defaults, include it under
+``/contact/``).
+
+"""
+
+
+from django.conf.urls.defaults import *
+from django.views.generic.simple import direct_to_template
+
+from contact_form.views import contact_form
+
+
+urlpatterns = patterns('',
+ url(r'^$',
+ contact_form,
+ name='contact_form'),
+ url(r'^sent/$',
+ direct_to_template,
+ { 'template': 'contact_form/contact_form_sent.html' },
+ name='contact_form_sent'),
+ )
diff --git a/piccast/contact_form/views.py b/piccast/contact_form/views.py
new file mode 100644
index 0000000..ad603f5
--- /dev/null
+++ b/piccast/contact_form/views.py
@@ -0,0 +1,92 @@
+"""
+View which can render and send email from a contact form.
+
+"""
+
+from django.core.urlresolvers import reverse
+from django.http import HttpResponseRedirect
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+
+from contact_form.forms import ContactForm
+
+
+def contact_form(request, form_class=ContactForm,
+ template_name='contact_form/contact_form.html',
+ success_url=None, extra_context=None,
+ fail_silently=False):
+ """
+ Render a contact form, validate its input and send an email
+ from it.
+
+ **Optional arguments:**
+
+ ``extra_context``
+ A dictionary of variables to add to the template context. Any
+ callable object in this dictionary will be called to produce
+ the end result which appears in the context.
+
+ ``fail_silently``
+ If ``True``, errors when sending the email will be silently
+ supressed (i.e., with no logging or reporting of any such
+ errors. Default value is ``False``.
+
+ ``form_class``
+ The form to use. If not supplied, this will default to
+ ``contact_form.forms.ContactForm``. If supplied, the form
+ class must implement a method named ``save()`` which sends the
+ email from the form; the form class must accept an
+ ``HttpRequest`` as the keyword argument ``request`` to its
+ constructor, and it must implement a method named ``save()``
+ which sends the email and which accepts the keyword argument
+ ``fail_silently``.
+
+ ``success_url``
+ The URL to redirect to after a successful submission. If not
+ supplied, this will default to the URL pointed to by the named
+ URL pattern ``contact_form_sent``.
+
+ ``template_name``
+ The template to use for rendering the contact form. If not
+ supplied, defaults to
+ :template:`contact_form/contact_form.html`.
+
+ **Context:**
+
+ ``form``
+ The form instance.
+
+ **Template:**
+
+ The value of the ``template_name`` keyword argument, or
+ :template:`contact_form/contact_form.html`.
+
+ """
+ #
+ # We set up success_url here, rather than as the default value for
+ # the argument. Trying to do it as the argument's default would
+ # mean evaluating the call to reverse() at the time this module is
+ # first imported, which introduces a circular dependency: to
+ # perform the reverse lookup we need access to contact_form/urls.py,
+ # but contact_form/urls.py in turn imports from this module.
+ #
+
+ if success_url is None:
+ success_url = reverse('contact_form_sent')
+ if request.method == 'POST':
+ form = form_class(data=request.POST, files=request.FILES, request=request)
+ if form.is_valid():
+ form.save(fail_silently=fail_silently)
+ return HttpResponseRedirect(success_url)
+ else:
+ form = form_class(request=request)
+
+ if extra_context is None:
+ extra_context = {}
+ context = RequestContext(request)
+ for key, value in extra_context.items():
+ context[key] = callable(value) and value() or value
+
+ return render_to_response(template_name,
+ { 'form': form },
+ context_instance=context)
diff --git a/piccast/settings.py.example b/piccast/settings.py.example
index 71ef664..dd23ae7 100644
--- a/piccast/settings.py.example
+++ b/piccast/settings.py.example
@@ -16,6 +16,12 @@ DATABASE_PASSWORD = '' # Not used with sqlite3.
DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3.
DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3.
+EMAIL_HOST = 'localhost'
+EMAIL_PORT = 587
+EMAIL_HOST_USER = ''
+EMAIL_HOST_PASSWORD = ''
+EMAIL_USE_TLS = False
+
# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
@@ -82,4 +88,5 @@ INSTALLED_APPS = (
'django.contrib.admindocs',
'django.contrib.databrowse',
'piccast.feeds',
+ 'piccast.contact_form',
)
diff --git a/piccast/templates/contact_form/contact_form.html b/piccast/templates/contact_form/contact_form.html
new file mode 100644
index 0000000..2d886f0
--- /dev/null
+++ b/piccast/templates/contact_form/contact_form.html
@@ -0,0 +1,13 @@
+<html>
+<head>
+<title>PicCast</title>
+</head>
+<body>
+<h1>Support Form</h1>
+<form action="/support/" method="post">
+{{ form.as_p }}
+<input type="submit" value="Submit" />
+</form>
+</body>
+</html>
+
diff --git a/piccast/templates/contact_form/contact_form.txt b/piccast/templates/contact_form/contact_form.txt
new file mode 100644
index 0000000..d84a5be
--- /dev/null
+++ b/piccast/templates/contact_form/contact_form.txt
@@ -0,0 +1,7 @@
+
+From: {{name}} <{{ email }}>
+
+===============================================================================
+{{ body }}
+===============================================================================
+(via http://piccastapp.com/contact/)
diff --git a/piccast/templates/contact_form/contact_form_sent.html b/piccast/templates/contact_form/contact_form_sent.html
new file mode 100644
index 0000000..908487a
--- /dev/null
+++ b/piccast/templates/contact_form/contact_form_sent.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+<title>PicCast</title>
+</head>
+<body>
+<h1>Support Form</h1>
+<p>Thanks for your message, we'll try to get back to you as soon as possible!
+<p>You can return to the front page <a href="/">here</a>.
+</body>
+</html>
+
diff --git a/piccast/templates/contact_form/contact_form_subject.txt b/piccast/templates/contact_form/contact_form_subject.txt
new file mode 100644
index 0000000..10c5937
--- /dev/null
+++ b/piccast/templates/contact_form/contact_form_subject.txt
@@ -0,0 +1 @@
+[PicCast] Contact message from {{ email }}
diff --git a/piccast/urls.py b/piccast/urls.py
index aa2361f..dc46b4b 100644
--- a/piccast/urls.py
+++ b/piccast/urls.py
@@ -16,6 +16,7 @@ urlpatterns = patterns('',
(r'^admin/doc/', include('django.contrib.admindocs.urls')),
(r'^admin/', include(admin.site.urls)),
(r'^browse/(.*)', databrowse.site.root),
+ (r'^support/', include('contact_form.urls')),
# Not using the subdirectory scheme; I like to have it all at the toplevel
# (r'^piccast/', include('piccast.foo.urls')),