From 647feb73a2c3cbcc30244a022da04f5be3aa3346 Mon Sep 17 00:00:00 2001 From: bnewbold Date: Sat, 7 May 2011 15:11:11 -0400 Subject: support contact form [WIP] --- piccast/contact_form/LICENSE.txt | 28 +++ piccast/contact_form/__init__.py | 0 piccast/contact_form/forms.py | 259 +++++++++++++++++++++ piccast/contact_form/urls.py | 28 +++ piccast/contact_form/views.py | 92 ++++++++ piccast/settings.py.example | 7 + piccast/templates/contact_form/contact_form.html | 13 ++ piccast/templates/contact_form/contact_form.txt | 7 + .../templates/contact_form/contact_form_sent.html | 11 + .../contact_form/contact_form_subject.txt | 1 + piccast/urls.py | 1 + 11 files changed, 447 insertions(+) create mode 100644 piccast/contact_form/LICENSE.txt create mode 100644 piccast/contact_form/__init__.py create mode 100644 piccast/contact_form/forms.py create mode 100644 piccast/contact_form/urls.py create mode 100644 piccast/contact_form/views.py create mode 100644 piccast/templates/contact_form/contact_form.html create mode 100644 piccast/templates/contact_form/contact_form.txt create mode 100644 piccast/templates/contact_form/contact_form_sent.html create mode 100644 piccast/templates/contact_form/contact_form_subject.txt (limited to 'piccast') 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 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 @@ + + +PicCast + + +

Support Form

+
+{{ form.as_p }} + +
+ + + 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 @@ + + +PicCast + + +

Support Form

+

Thanks for your message, we'll try to get back to you as soon as possible! +

You can return to the front page here. + + + 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')), -- cgit v1.2.3