Permalink
Browse files

[Bug 722388] Collect emails from users.

This adds a checkbox and a input box to the survey form. The checkbox is
to indicate the user wants to give an email address, and the input field
is for the email address, and only shows up after the box has been
checked.

This email address is stored in a seperate table with a foreign key, for
possible security benefits down the road.
  • Loading branch information...
1 parent 2c651b7 commit ae1221d31688facabd5e392fb6533f477cd5794f @mythmon committed Oct 8, 2012
View
@@ -1,4 +1,4 @@
-from datetime import datetime, timedelta
+from datetime import timedelta
from django.shortcuts import render
from django.template.defaultfilters import slugify
@@ -4,7 +4,7 @@
from nose.tools import eq_
from fjord.base.tests import TestCase
-from fjord.base.util import smart_truncate, smart_int, smart_datetime
+from fjord.base.util import smart_truncate, smart_int, smart_datetime, smart_bool
def test_smart_truncate():
@@ -52,3 +52,26 @@ def test_format(self):
def test_null_bytes(self):
# strptime likes to barf on null bytes in strings, so test it.
eq_(None, smart_datetime('/etc/passwd\x00'))
+
+
+class SmartBoolTest(TestCase):
+
+ msg_template = 'smart_bool(%r) - Expected %r, got %r'
+
+ def test_truthy(self):
+ truths = ['Yes', 'y', u'TRUE', '1', u'1']
+ for x in truths:
+ b = smart_bool(x, 'fallback')
+ assert b == True, self.msg_template % (x, True, b)
+
+ def test_falsey(self):
+ falses = ['No', 'n', u'FALSE', '0', u'0']
+ for x in falses:
+ b = smart_bool(x, 'fallback')
+ assert b == False, self.msg_template % (x, False, b)
+
+ def test_fallback(self):
+ garbages = [None, 'apple', u'']
+ for x in garbages:
+ b = smart_bool(x, 'fallback')
+ assert b == 'fallback', self.msg_template % (x, 'fallback', b)
View
@@ -48,3 +48,24 @@ def smart_datetime(s, format='%Y-%m-%d', fallback=None):
return datetime.strptime(s, format)
except (ValueError, TypeError):
return fallback
+
+
+def smart_bool(s, fallback=False):
+ """Convert a string that has a semantic boolean value to a real boolean.
+
+ Note that this is not the same as ``s`` being "truthy". The string
+ ``'False'`` will be returned as False, even though it is Truthy, and non-
+ boolean values like ``'apple'`` would return the fallback parameter, since
+ it doesn't represent a boolean value.
+
+ """
+ try:
+ s = s.lower()
+ if s in ['true', 't', 'yes', 'y', '1']:
+ return True
+ elif s in ['false', 'f', 'no', 'n', '0']:
+ return False
+ except AttributeError:
+ pass
+
+ return fallback
View
@@ -1,5 +1,7 @@
from django import forms
+from tower import ugettext as _
+
class URLInput(forms.TextInput):
"""Text field with HTML5 URL Input type."""
@@ -17,3 +19,23 @@ class SimpleForm(forms.Form):
description = forms.CharField(widget=forms.Textarea(), required=True)
# required=False means this allowed to be False, not that it can be blank.
happy = forms.BooleanField(required=False, widget=forms.HiddenInput())
+
+ email_ok = forms.BooleanField(required=False)
+ email = forms.EmailField(required=False)
+
+ def clean(self):
+ cleaned_data = super(SimpleForm, self).clean()
+
+ email_ok = cleaned_data.get('email_ok')
+ email = cleaned_data.get('email')
+
+ if email_ok and not email or 'email' in self._errors:
+ self._errors['email'] = self.error_class([_(
+ u'Please enter a valid email address, or uncheck the box '
+ 'allowing us to contact you.')])
+
+ # If email_ok is not checked, ignore errors on email.
+ if not email_ok and 'email' in self._errors:
+ del self._errors['email']
+
+ return cleaned_data
@@ -0,0 +1,48 @@
+# -*- 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 'SimpleEmail'
+ db.create_table('feedback_simpleemail', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('opinion', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['feedback.Simple'])),
+ ('email', self.gf('django.db.models.fields.EmailField')(max_length=75)),
+ ))
+ db.send_create_signal('feedback', ['SimpleEmail'])
+
+
+ def backwards(self, orm):
+ # Deleting model 'SimpleEmail'
+ db.delete_table('feedback_simpleemail')
+
+
+ models = {
+ 'feedback.simple': {
+ 'Meta': {'ordering': "['-created']", 'object_name': 'Simple'},
+ 'browser': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'browser_version': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'happy': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'locale': ('django.db.models.fields.CharField', [], {'max_length': '8', 'blank': 'True'}),
+ 'platform': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'prodchan': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
+ 'user_agent': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'})
+ },
+ 'feedback.simpleemail': {
+ 'Meta': {'object_name': 'SimpleEmail'},
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'opinion': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['feedback.Simple']"})
+ }
+ }
+
+ complete_apps = ['feedback']
View
@@ -97,3 +97,8 @@ def extract_document(cls, obj_id, obj=None):
mapping = cls.get_mapping()
return dict((field, getattr(obj, field))
for field in mapping.keys())
+
+
+class SimpleEmail(ModelBase):
+ opinion = models.ForeignKey(Simple)
+ email = models.EmailField()
@@ -145,6 +145,24 @@
$('#happy-with-url').clickEnable('#happy-url');
$('#sad-with-url').clickEnable('#sad-url');
+
+ function email_expansion(elem, time) {
+ var checked = $(elem).prop('checked');
+ var email = $(elem).parents('label').siblings('.email');
+
+ if (checked) {
+ email.show(time);
+ } else {
+ email.hide(time);
+ }
+ }
+
+ $('.email-ok input[type=checkbox]').on('change', function() {
+ email_expansion(this, 300);
+ }).each(function() {
+ email_expansion(this, 0);
+ });
+
});
$('html').addClass('js');
@@ -55,6 +55,35 @@
{% endtrans %}
</div>
{% endif %}
+
+ <label class="email-ok">
+ <p>
+ {{ form.email_ok }}
+ {{ _('Check here to let us contact you to follow up on your feedback.') }}
+ </p>
+ </label>
+
+ <div class="email">
+ <label for="id_email">
+ {{ _('Email address (optional):') }}
+ </label>
+ <p>{{ form.email }}</p>
+ {{ form.email.errors }}
+
+ <p>
+ {% trans %}
+ While your feedback will be publicly visible, email addresses
+ are kept private. We understand your privacy is important.
+ {% endtrans %}
+ </p>
+ </div>
+
+ <p>
+ {% trans url="http://support.mozilla.org" %}
+ For support requests, please go to <a href="{{ url }}">Mozilla Support</a> instead.
+ {% endtrans %}
+ </p>
+
<div class="privacy-wrapper">
<p class="privacy">
<span>
@@ -69,3 +69,68 @@ def test_feedback_router(self):
r = self.client.get(url, HTTP_USER_AGENT='Firefox')
eq_(200, r.status_code)
self.assertTemplateUsed(r, 'feedback/feedback.html')
+
+ def test_email_collection(self):
+ """If the user enters an email and checks the box, collect the email."""
+ url = reverse('feedback', args=('firefox.desktop.stable',))
+
+ r = self.client.post(url, {
+ 'happy': 0,
+ 'description': u"I like the colors.",
+ 'email': 'bob@example.com',
+ 'email_ok': 1,
+ })
+ eq_(models.SimpleEmail.objects.count(), 1)
+ eq_(r.status_code, 302)
+
+ def test_email_privacy(self):
+ """If an email is entered, but the box is not checked, don't collect."""
+ url = reverse('feedback', args=('firefox.desktop.stable',))
+
+ r = self.client.post(url, {
+ 'happy': 0,
+ 'description': u"I like the colors.",
+ 'email': 'bob@example.com',
+ 'email_ok': 0,
+ })
+ assert not models.SimpleEmail.objects.exists()
+ eq_(r.status_code, 302)
+
+ def test_email_missing(self):
+ """If an email is not entered and the box is checked, don't error out."""
+ url = reverse('feedback', args=('firefox.desktop.stable',))
+
+ r = self.client.post(url, {
+ 'happy': 0,
+ 'description': u'Can you fix it?',
+ 'email_ok': 1,
+ })
+ assert not models.SimpleEmail.objects.exists()
+ # No redirect to thank you page, since there is a form error.
+ eq_(r.status_code, 200)
+ self.assertContains(r, 'Please enter a valid email')
+
+ def test_email_invalid(self):
+ """If an email is not entered and the box is checked, don't error out."""
+ url = reverse('feedback', args=('firefox.desktop.stable',))
+
+ r = self.client.post(url, {
+ 'happy': 0,
+ 'description': u'There is something wrong here.\0',
+ 'email_ok': 1,
+ 'email': '/dev/sda1\0',
+ })
+ assert not models.SimpleEmail.objects.exists()
+ # No redirect to thank you page, since there is a form error.
+ eq_(r.status_code, 200)
+ self.assertContains(r, 'Please enter a valid email')
+
+ r = self.client.post(url, {
+ 'happy': 0,
+ 'description': u'There is something wrong here.\0',
+ 'email_ok': 0,
+ 'email': "huh what's this for?",
+ })
+ assert not models.SimpleEmail.objects.exists()
+ # Bad email if the box is not checked is not an error.
+ eq_(r.status_code, 302)
View
@@ -1,9 +1,10 @@
-from django.http import HttpResponseRedirect
+from django.http import HttpResponseRedirect, HttpResponse
from django.shortcuts import render
from funfactory.urlresolvers import reverse
from session_csrf import anonymous_csrf_exempt
+from fjord.base.util import smart_bool
from fjord.feedback.forms import SimpleForm
from fjord.feedback import models
@@ -23,6 +24,7 @@ def desktop_stable_feedback(request, template=None):
if request.method == 'POST':
form = SimpleForm(request.POST)
if form.is_valid():
+ data = form.cleaned_data
# Most platforms aren't different enough between versions to care.
# Windows is.
platform = request.BROWSER.platform
@@ -31,9 +33,9 @@ def desktop_stable_feedback(request, template=None):
opinion = models.Simple(
# Data coming from the user
- happy=form.cleaned_data['happy'],
- url=form.cleaned_data['url'],
- description=form.cleaned_data['description'],
+ happy=data['happy'],
+ url=data['url'],
+ description=data['description'],
# Inferred data
prodchan='firefox.desktop.stable',
user_agent=request.META.get('HTTP_USER_AGENT', ''),
@@ -43,11 +45,17 @@ def desktop_stable_feedback(request, template=None):
locale=request.locale,
)
opinion.save()
+
+ if data['email_ok'] and data['email']:
+ e = models.SimpleEmail(email=data['email'], opinion=opinion)
+ e.save()
+
return HttpResponseRedirect(reverse('thanks'))
else:
# The user did something wrong. Update the appropriate form, so
# the errors show correctly.
- if request.POST.get('happy'):
+ happy = smart_bool(request.POST.get('happy', None))
+ if happy:
forms['happy'] = form
else:
forms['sad'] = form

0 comments on commit ae1221d

Please sign in to comment.