Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

[fix bug 690559] Add a personal comment to invites.

Refactored message to use jinja template.
Added invite message to test_invites.
Added tests to make sure that we can send invites without
the optional personal message.
  • Loading branch information...
commit 97d5f22ffd453068ad4af578385128f4add2f9a9 1 parent 58a413b
@almosteverywhere almosteverywhere authored
View
107 apps/phonebook/migrations/0006_auto_add_field_invite_message.py
@@ -0,0 +1,107 @@
+# encoding: 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 field 'Invite.message'
+ db.add_column('invite', 'message', self.gf('django.db.models.fields.TextField')(default=''), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'Invite.message'
+ db.delete_column('invite', 'message')
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'groups.group': {
+ 'Meta': {'object_name': 'Group', 'db_table': "'group'"},
+ 'always_auto_complete': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'auto_complete': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'irc_channel': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '63', 'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50', 'db_index': 'True'}),
+ 'steward': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['users.UserProfile']", 'null': 'True', 'blank': 'True'}),
+ 'system': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'url': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}),
+ 'website': ('django.db.models.fields.URLField', [], {'default': "''", 'max_length': '200', 'blank': 'True'}),
+ 'wiki': ('django.db.models.fields.URLField', [], {'default': "''", 'max_length': '200', 'blank': 'True'})
+ },
+ 'groups.skill': {
+ 'Meta': {'object_name': 'Skill'},
+ 'always_auto_complete': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'auto_complete': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50', 'db_index': 'True'})
+ },
+ 'phonebook.invite': {
+ 'Meta': {'object_name': 'Invite', 'db_table': "'invite'"},
+ 'code': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invites'", 'null': 'True', 'to': "orm['users.UserProfile']"}),
+ 'message': ('django.db.models.fields.TextField', [], {'default': "''"}),
+ 'recipient': ('django.db.models.fields.EmailField', [], {'max_length': '75'}),
+ 'redeemed': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'redeemer': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['users.UserProfile']", 'unique': 'True', 'null': 'True'})
+ },
+ 'users.userprofile': {
+ 'Meta': {'object_name': 'UserProfile', 'db_table': "'profile'"},
+ 'bio': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ 'display_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['groups.Group']", 'symmetrical': 'False'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ircname': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '63', 'blank': 'True'}),
+ 'is_vouched': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}),
+ 'photo': ('sorl.thumbnail.fields.ImageField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}),
+ 'skills': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['groups.Skill']", 'symmetrical': 'False'}),
+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}),
+ 'vouched_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['users.UserProfile']", 'null': 'True'}),
+ 'website': ('django.db.models.fields.URLField', [], {'default': "''", 'max_length': '200', 'null': 'True', 'blank': 'True'})
+ }
+ }
+
+ complete_apps = ['phonebook']
View
27 apps/phonebook/models.py
@@ -2,6 +2,7 @@
from django.core.mail import send_mail
from django.db import models
from django.dispatch import receiver
+from django.template.loader import get_template
from funfactory.urlresolvers import reverse
from funfactory.utils import absolutify
@@ -16,6 +17,9 @@ class Invite(models.Model):
#: This is the email address of where the invitation is sent.
recipient = models.EmailField()
+ # This is the message sent alongside the invite. "Hey you're cool."
+ message = models.TextField(blank=True)
+
#: The person who redeemed this invite.
redeemer = models.OneToOneField('users.UserProfile', null=True)
@@ -44,16 +48,19 @@ def send(self, sender=None):
sender.user.email)
subject = _('Become a Mozillian')
- message = _('Hi there. %s has invited you to join mozillians.org, '
- 'the community directory for Mozilla contributors. You '
- 'can create a community profile for yourself and search '
- 'for other contributors to learn more about them or get '
- 'in touch.' % (sender or _('A fellow Mozillian')))
- # l10n: %s is the registration link.
- link = _("Join Mozillians: %s") % self.get_url()
- message = "%s\n\n%s" % (message, link)
-
- send_mail(subject, message, 'no-reply@mozillians.org',
+
+ template = get_template('phonebook/invite_email.txt')
+
+ message = template.render({
+ 'personal_message': self.message,
+ 'sender': sender or _('A fellow Mozillian'),
+ 'link': self.get_url()})
+
+ # Manually replace quotes and double-quotes as these get
+ # escaped by the template and this makes the message look bad.
+ filtered_message = message.replace('"', '"').replace(''', "'")
+
+ send_mail(subject, filtered_message, 'no-reply@mozillians.org',
[self.recipient])
class Meta:
View
4 apps/phonebook/templates/phonebook/invite.html
@@ -18,7 +18,9 @@
{{ csrf() }}
{{ form.errors.recipient }}
- {{ form.recipient }}
+ {{ bootstrap(form.recipient) }}
+
+ {{ bootstrap(form.message) }}
<button type="submit" class="btn bump-button">
{{ _('Send invite') }}
View
16 apps/phonebook/templates/phonebook/invite_email.txt
@@ -0,0 +1,16 @@
+{% trans sender=sender %}
+Hi there. {{ sender }} has invited you to join mozillians.org,
+the community directory for Mozilla contributors. You
+can create a community profile for yourself and search
+for other contributors to learn more about them or get
+in touch.
+{% endtrans %}
+
+{% if personal_message %}
+
+{{ _('Personal message:') }}
+
+{{ personal_message }}
+{% endif %}
+
+{{ _('Join Mozillians:') }} {{ link }}
View
32 apps/phonebook/tests/test_invites.py
@@ -12,10 +12,13 @@
class InviteFlowTest(common.tests.TestCase):
fake_email = 'mr.fusion@gmail.com'
+ fake_email2 = 'mrs.fusion@gmail.com'
+
# Assertion doesn't matter since we monkey patched it for testing
fake_assertion = 'mrfusionsomereallylongstring'
+ fake_invite_message = 'Join Mozilla'
- def invite_someone(self, email):
+ def invite_someone(self, email, invite_message):
"""
This method will invite a user.
@@ -23,7 +26,7 @@ def invite_someone(self, email):
"""
# Send an invite.
url = reverse('invite')
- d = dict(recipient=email)
+ d = dict(recipient=email, message=invite_message)
r = self.mozillian_client.post(url, d, follow=True)
eq_(r.status_code, 200)
assert ('%s has been invited to Mozillians.' % email in
@@ -72,6 +75,27 @@ def redeem_invite(self, invite, email):
invited_user_profile = User.objects.get(email=email).get_profile()
return invited_user_profile
+ def invite_without_message(self, email):
+ """
+ Make sure we can send an invite without the optional personal
+ message and that the template doesn't use Personal message:
+ when there's no personal message.
+ """
+ # Send an invite without a personal message.
+ url = reverse('invite')
+ d = dict(recipient=email)
+ r = self.mozillian_client.post(url, d, follow=True)
+ eq_(r.status_code, 200)
+ assert ('%s has been invited to Mozillians.' % email in
+ pq(r.content)('div#main p').text())
+
+ # See that the email was sent.
+ eq_(len(mail.outbox), 2)
+
+ # Note it's mail.outbox[1] here because we're sending another
+ # message in a previous test.
+ assert not ("Personal message" in mail.outbox[1].body)
+
def test_send_invite_flow(self):
"""
Test the invitation flow.
@@ -81,7 +105,8 @@ def test_send_invite_flow(self):
Verify that we can't reuse the invite_url
Verify we can't reinvite a vouched user
"""
- invite = self.invite_someone(self.fake_email)
+ invite = self.invite_someone(self.fake_email, self.fake_invite_message)
+ self.invite_without_message(self.fake_email)
self.get_register(invite)
invited_user_profile = self.redeem_invite(invite, self.fake_email)
assert(invited_user_profile.is_vouched)
@@ -94,6 +119,7 @@ def test_send_invite_flow(self):
class InviteEdgeTest(common.tests.TestCase):
+
def test_no_reinvite(self):
"""Don't reinvite a vouched user."""
vouched_email = 'mr.fusion@gmail.com'
Please sign in to comment.
Something went wrong with that request. Please try again.