def test_autowatch_reply(self, get_current): """ Tests the autowatch setting of users. If a user has the setting turned on, they should get notifications after posting in a thread for that thread. If they have that setting turned off, they should not. """ get_current.return_value.domain = 'testserver' u = user(save=True) t1 = question(save=True) t2 = question(save=True) assert not QuestionReplyEvent.is_notifying(u, t1) assert not QuestionReplyEvent.is_notifying(u, t2) self.client.login(username=u.username, password='******') s = Setting.objects.create(user=u, name='questions_watch_after_reply', value='True') data = {'content': 'some content'} post(self.client, 'questions.reply', data, args=[t1.id]) assert QuestionReplyEvent.is_notifying(u, t1) s.value = 'False' s.save() post(self.client, 'questions.reply', data, args=[t2.id]) assert not QuestionReplyEvent.is_notifying(u, t2)
def save(self, user, locale, product, product_config, *args, **kwargs): self.instance.creator = user self.instance.locale = locale self.instance.product = product category_config = product_config["categories"][self.cleaned_data["category"]] if category_config: t = category_config.get("topic") if t: self.instance.topic = Topic.objects.get(slug=t, product=product) question = super(NewQuestionForm, self).save(*args, **kwargs) if self.cleaned_data.get("notifications", False): QuestionReplyEvent.notify(question.creator, question) user_ct = ContentType.objects.get_for_model(user) qst_ct = ContentType.objects.get_for_model(question) # Move over to the question all of the images I added to the reply form up_images = ImageAttachment.objects.filter(creator=user, content_type=user_ct) up_images.update(content_type=qst_ct, object_id=question.id) # User successfully submitted a new question question.add_metadata(**self.cleaned_metadata) if product_config: # TODO: This add_metadata call should be removed once we are # fully IA-driven (sync isn't special case anymore). question.add_metadata(product=product_config["key"]) # The first time a question is saved, automatically apply some tags: question.auto_tag() return question
def test_autowatch_reply(self, get_current): """ Tests the autowatch setting of users. If a user has the setting turned on, they should get notifications after posting in a thread for that thread. If they have that setting turned off, they should not. """ get_current.return_value.domain = 'testserver' u = UserFactory() t1 = QuestionFactory() t2 = QuestionFactory() assert not QuestionReplyEvent.is_notifying(u, t1) assert not QuestionReplyEvent.is_notifying(u, t2) self.client.login(username=u.username, password='******') s = Setting.objects.create(user=u, name='questions_watch_after_reply', value='True') data = {'content': 'some content'} post(self.client, 'questions.reply', data, args=[t1.id]) assert QuestionReplyEvent.is_notifying(u, t1) s.value = 'False' s.save() post(self.client, 'questions.reply', data, args=[t2.id]) assert not QuestionReplyEvent.is_notifying(u, t2)
def test_autowatch_reply(self, get_current): """ Tests the autowatch setting of users. If a user has the setting turned on, they should get notifications after posting in a thread for that thread. If they have that setting turned off, they should not. """ get_current.return_value.domain = "testserver" u = user(save=True) t1 = question(save=True) t2 = question(save=True) assert not QuestionReplyEvent.is_notifying(u, t1) assert not QuestionReplyEvent.is_notifying(u, t2) self.client.login(username=u.username, password="******") s = Setting.objects.create(user=u, name="questions_watch_after_reply", value="True") data = {"content": "some content"} post(self.client, "questions.reply", data, args=[t1.id]) assert QuestionReplyEvent.is_notifying(u, t1) s.value = "False" s.save() post(self.client, "questions.reply", data, args=[t2.id]) assert not QuestionReplyEvent.is_notifying(u, t2)
def setUp(self): p = profile() p.save() self.user = p.user self.client.login(username=self.user.username, password='******') self.question = question(creator=self.user, save=True) QuestionReplyEvent.notify(self.user, self.question)
def test_no_notification_on_update(self): """Saving an existing question does not watch it.""" q = Question.objects.get(pk=1) assert not QuestionReplyEvent.is_notifying(q.creator, q) q.save() assert not QuestionReplyEvent.is_notifying(q.creator, q)
def reply(request, question_id): """Post a new answer to a question.""" question = get_object_or_404(Question, pk=question_id, is_spam=False) answer_preview = None if not question.allows_new_answer(request.user): raise PermissionDenied form = AnswerForm(request.POST, **{"user": request.user, "question": question}) # NOJS: delete images if "delete_images" in request.POST: for image_id in request.POST.getlist("delete_image"): ImageAttachment.objects.get(pk=image_id).delete() return question_details(request, question_id=question_id, form=form) # NOJS: upload image if "upload_image" in request.POST: upload_imageattachment(request, request.user) return question_details(request, question_id=question_id, form=form) if form.is_valid() and not request.limited: answer = Answer( question=question, creator=request.user, content=form.cleaned_data["content"], ) if "preview" in request.POST: answer_preview = answer else: if form.cleaned_data.get("is_spam"): _add_to_moderation_queue(request, answer) else: answer.save() ans_ct = ContentType.objects.get_for_model(answer) # Move over to the answer all of the images I added to the # reply form user_ct = ContentType.objects.get_for_model(request.user) up_images = ImageAttachment.objects.filter(creator=request.user, content_type=user_ct) up_images.update(content_type=ans_ct, object_id=answer.id) # Handle needsinfo tag if "needsinfo" in request.POST: question.set_needs_info() elif "clear_needsinfo" in request.POST: question.unset_needs_info() if Setting.get_for_user(request.user, "questions_watch_after_reply"): QuestionReplyEvent.notify(request.user, question) return HttpResponseRedirect(answer.get_absolute_url()) return question_details( request, question_id=question_id, form=form, answer_preview=answer_preview )
def test_no_notification_on_update(self): """Saving an existing question does not watch it.""" q = QuestionFactory() QuestionReplyEvent.stop_notifying(q.creator, q) assert not QuestionReplyEvent.is_notifying(q.creator, q) q.save() assert not QuestionReplyEvent.is_notifying(q.creator, q)
def test_no_notification_on_update(self): """Saving an existing question does not watch it.""" q = question(save=True) QuestionReplyEvent.stop_notifying(q.creator, q) assert not QuestionReplyEvent.is_notifying(q.creator, q) q.save() assert not QuestionReplyEvent.is_notifying(q.creator, q)
def test_notify_arbitrary_reply_to(self): """ Test that notifications to the asker have a correct reply to field. """ watcher = user(save=True) QuestionReplyEvent.notify(watcher, self.question) self.makeAnswer() notification = [m for m in mail.outbox if m.to == [watcher.email]][0] # Headers should be compared case-insensitively. headers = dict((k.lower(), v) for k, v in notification.extra_headers.items()) eq_("*****@*****.**", headers["reply-to"])
def test_notify_anonymous_reply_to(self): """ Test that notifications to the asker have a correct reply to field. """ ANON_EMAIL = "*****@*****.**" QuestionReplyEvent.notify(ANON_EMAIL, self.question) self.makeAnswer() notification = [m for m in mail.outbox if m.to == [ANON_EMAIL]][0] # Headers should be compared case-insensitively. headers = dict((k.lower(), v) for k, v in notification.extra_headers.items()) eq_("*****@*****.**", headers["reply-to"])
def test_notify_anonymous_reply_to(self): """ Test that notifications to the asker have a correct reply to field. """ ANON_EMAIL = '*****@*****.**' QuestionReplyEvent.notify(ANON_EMAIL, self.question) self.makeAnswer() notification = [m for m in mail.outbox if m.to == [ANON_EMAIL]][0] # Headers should be compared case-insensitively. headers = dict((k.lower(), v) for k, v in notification.extra_headers.items()) eq_('*****@*****.**', headers['reply-to'])
def test_notify_arbitrary_reply_to(self): """ Test that notifications to the asker have a correct reply to field. """ watcher = user(save=True) QuestionReplyEvent.notify(watcher, self.question) self.makeAnswer() notification = [m for m in mail.outbox if m.to == [watcher.email]][0] # Headers should be compared case-insensitively. headers = dict((k.lower(), v) for k, v in notification.extra_headers.items()) eq_('*****@*****.**', headers['reply-to'])
def test_notify_anonymous(self): """Test that anonymous users are notified of new answers.""" ANON_EMAIL = "*****@*****.**" QuestionReplyEvent.notify(ANON_EMAIL, self.question) self.makeAnswer() # One for the asker's email, and one for the anonymous email. eq_(2, len(mail.outbox)) notification = [m for m in mail.outbox if m.to == [ANON_EMAIL]][0] eq_([ANON_EMAIL], notification.to) eq_("Re: {0}".format(self.question.title), notification.subject) body = re.sub(r"auth=[a-zA-Z0-9%_-]+", "auth=AUTH", notification.body) starts_with(body, ANSWER_EMAIL_TO_ANONYMOUS.format(**self.format_args()))
def test_notify_arbitrary(self): """Test that arbitrary users are notified of new answers.""" watcher = user(save=True) QuestionReplyEvent.notify(watcher, self.question) self.makeAnswer() # One for the asker's email, and one for the watcher's email. eq_(2, len(mail.outbox)) notification = [m for m in mail.outbox if m.to == [watcher.email]][0] eq_([watcher.email], notification.to) eq_("Re: {0}".format(self.question.title), notification.subject) body = re.sub(r'auth=[a-zA-Z0-9%_-]+', 'auth=AUTH', notification.body) starts_with(body, ANSWER_EMAIL.format(to_user=display_name(watcher), **self.format_args()))
def test_notify_arbitrary(self): """Test that arbitrary users are notified of new answers.""" watcher = user(save=True) QuestionReplyEvent.notify(watcher, self.question) self.makeAnswer() # One for the asker's email, and one for the watcher's email. eq_(2, len(mail.outbox)) notification = [m for m in mail.outbox if m.to == [watcher.email]][0] eq_([watcher.email], notification.to) eq_("Re: {0}".format(self.question.title), notification.subject) body = re.sub(r"auth=[a-zA-Z0-9%_-]+", "auth=AUTH", notification.body) starts_with(body, ANSWER_EMAIL.format(to_user=watcher.username, **self.format_args()))
def test_notify_arbitrary(self): """Test that arbitrary users are notified of new answers.""" watcher = UserFactory() QuestionReplyEvent.notify(watcher, self.question) self.makeAnswer() # One for the asker's email, and one for the watcher's email. eq_(2, len(mail.outbox)) notification = [m for m in mail.outbox if m.to == [watcher.email]][0] eq_([watcher.email], notification.to) eq_(u'Re: {0}'.format(self.question.title), notification.subject) body = re.sub(r'auth=[a-zA-Z0-9%_-]+', 'auth=AUTH', notification.body) starts_with(body, ANSWER_EMAIL.format(to_user=display_name(watcher), **self.format_args()))
def test_notify_anonymous(self): """Test that anonymous users are notified of new answers.""" ANON_EMAIL = '*****@*****.**' QuestionReplyEvent.notify(ANON_EMAIL, self.question) self.makeAnswer() # One for the asker's email, and one for the anonymous email. eq_(2, len(mail.outbox)) notification = [m for m in mail.outbox if m.to == [ANON_EMAIL]][0] eq_([ANON_EMAIL], notification.to) eq_("Re: {0}".format(self.question.title), notification.subject) body = re.sub(r'auth=[a-zA-Z0-9%_-]+', 'auth=AUTH', notification.body) starts_with(body, ANSWER_EMAIL_TO_ANONYMOUS .format(**self.format_args()))
def save(self, update=True, no_notify=False, *args, **kwargs): """ Override save method to update question info and take care of updated. """ new = self.id is None if new: page = self.question.num_answers / config.ANSWERS_PER_PAGE + 1 self.page = page else: self.updated = datetime.now() self.clear_cached_html() super(Answer, self).save(*args, **kwargs) if new: # Occasionally, num_answers seems to get out of sync with the # actual number of answers. This changes it to pull from # uncached on the off chance that fixes it. Plus if we enable # caching of counts, this will continue to work. self.question.num_answers = Answer.uncached.filter( question=self.question).count() self.question.last_answer = self self.question.save(update) self.question.clear_cached_contributors() if not no_notify: # Avoid circular import: events.py imports Question. from kitsune.questions.events import QuestionReplyEvent QuestionReplyEvent(self).fire(exclude=self.creator) else: # Clear the attached images cache. self.clear_cached_images()
def test_notification_created(self): """Creating a new question auto-watches it for answers.""" u = user(save=True) q = question(creator=u, title='foo', content='bar', save=True) assert QuestionReplyEvent.is_notifying(u, q)
def unsubscribe_watch(request, watch_id, secret): """Stop watching a question, for anonymous users.""" watch = get_object_or_404(Watch, pk=watch_id) question = watch.content_object success = False if watch.secret == secret and isinstance(question, Question): user_or_email = watch.user or watch.email QuestionReplyEvent.stop_notifying(user_or_email, question) QuestionSolvedEvent.stop_notifying(user_or_email, question) success = True return render( request, "questions/unsubscribe_watch.html", {"question": question, "success": success}, )
def save(self, update=False, *args, **kwargs): """Override save method to take care of updated if requested.""" new = not self.id if not new: self.clear_cached_html() if update: self.updated = datetime.now() super(Question, self).save(*args, **kwargs) if new: # Avoid circular import, events.py imports Question from kitsune.questions.events import QuestionReplyEvent # Authors should automatically watch their own questions. QuestionReplyEvent.notify(self.creator, self)
def test_notification_created(self): """Creating a new question auto-watches it for answers.""" u = UserFactory() q = QuestionFactory(creator=u, title='foo', content='bar') assert QuestionReplyEvent.is_notifying(u, q)
def test_notification_created(self): """Creating a new question auto-watches it for answers.""" u = User.objects.get(pk=118533) q = Question(creator=u, title='foo', content='bar') q.save() assert QuestionReplyEvent.is_notifying(u, q)
def watch_question(request, question_id): """Start watching a question for replies or solution.""" question = get_object_or_404(Question, pk=question_id, is_spam=False) form = WatchQuestionForm(request.user, request.POST) # Process the form msg = None if form.is_valid(): user_or_email = (request.user if request.user.is_authenticated else form.cleaned_data["email"]) try: if form.cleaned_data["event_type"] == "reply": QuestionReplyEvent.notify(user_or_email, question) else: QuestionSolvedEvent.notify(user_or_email, question) except ActivationRequestFailed: msg = _("Could not send a message to that email address.") # Respond to ajax request if request.is_ajax(): if form.is_valid(): msg = msg or (_("You will be notified of updates by email.") if request.user.is_authenticated else _( "You should receive an email shortly " "to confirm your subscription.")) return HttpResponse(json.dumps({"message": msg})) if request.POST.get("from_vote"): tmpl = "questions/includes/question_vote_thanks.html" else: tmpl = "questions/includes/email_subscribe.html" html = render_to_string(tmpl, context={ "question": question, "watch_form": form }, request=request) return HttpResponse(json.dumps({"html": html})) if msg: messages.add_message(request, messages.ERROR, msg) return HttpResponseRedirect(question.get_absolute_url())
def test_notify_anonymous(self): """Test that anonymous users are notified of new answers.""" ANON_EMAIL = '*****@*****.**' QuestionReplyEvent.notify(ANON_EMAIL, self.question) self.makeAnswer() # One for the asker's email, and one for the anonymous email. eq_(2, len(mail.outbox)) notification = [m for m in mail.outbox if m.to == [ANON_EMAIL]][0] eq_([ANON_EMAIL], notification.to) eq_("{0} commented on a Firefox question you're watching" .format(self.answer.creator.username), notification.subject) body = re.sub(r'auth=[a-zA-Z0-9%_-]+', 'auth=AUTH', notification.body) starts_with(body, ANSWER_EMAIL_TO_ANONYMOUS .format(**self.format_args()))
def test_autowatch_reply(self, get_current): get_current.return_value.domain = 'testserver' u = user(save=True) t1 = question(save=True) t2 = question(save=True) assert not QuestionReplyEvent.is_notifying(u, t1) assert not QuestionReplyEvent.is_notifying(u, t2) self.client.login(username=u.username, password='******') s = Setting.objects.create(user=u, name='questions_watch_after_reply', value='True') data = {'content': 'some content'} post(self.client, 'questions.reply', data, args=[t1.id]) assert QuestionReplyEvent.is_notifying(u, t1) s.value = 'False' s.save() post(self.client, 'questions.reply', data, args=[t2.id]) assert not QuestionReplyEvent.is_notifying(u, t2)
def test_notify_unique_auth_tokens(self, email_mock): """Test that arbitrary users get unique auth tokens.""" auth_backend = TokenLoginBackend() auth_re = re.compile(r'auth=([a-zA-Z0-9%_-]+)') watcher = UserFactory() QuestionReplyEvent.notify(watcher, self.question) asker_id = self.question.creator.id self.makeAnswer() def get_auth_token(ctx): return auth_re.search(ctx['answer_url']).group(1).replace('%3D', '=') eq_(email_mock.call_count, 2) all_calls = email_mock.call_args_list for call in all_calls: ctx = call[1]['context_vars'] user = ctx['to_user'] if user.id == asker_id: auth = get_auth_token(ctx) eq_(user, auth_backend.authenticate(auth)) else: assert auth_re.search(ctx['answer_url']) is None
def save(self, update=True, no_notify=False, *args, **kwargs): """ Override save method to update question info and take care of updated. """ new = self.id is None if new: page = self.question.num_answers // config.ANSWERS_PER_PAGE + 1 self.page = page else: self.updated = datetime.now() self.clear_cached_html() super(Answer, self).save(*args, **kwargs) self.question.num_answers = Answer.objects.filter( question=self.question, is_spam=False).count() latest = Answer.objects.filter(question=self.question, is_spam=False).order_by("-created")[:1] self.question.last_answer = self if new else latest[0] if len( latest) else None self.question.save(update) if new: # Occasionally, num_answers seems to get out of sync with the # actual number of answers. This changes it to pull from # uncached on the off chance that fixes it. Plus if we enable # caching of counts, this will continue to work. self.question.clear_cached_contributors() if not no_notify: # tidings # Avoid circular import: events.py imports Question. from kitsune.questions.events import QuestionReplyEvent if not self.is_spam: QuestionReplyEvent(self).fire(exclude=self.creator) # actstream actstream.actions.follow(self.creator, self, send_action=False, actor_only=False) actstream.actions.follow(self.creator, self.question, send_action=False, actor_only=False)
def update(request, flagged_object_id): """Update the status of a flagged object.""" flagged = get_object_or_404(FlaggedObject, pk=flagged_object_id) new_status = request.POST.get("status") if new_status: ct = flagged.content_type # If the object is an Answer let's fire a notification # if the flag is invalid if str(new_status) == str( FlaggedObject.FLAG_REJECTED) and ct.model_class() == Answer: answer = flagged.content_object QuestionReplyEvent(answer).fire(exclude=answer.creator) flagged.status = new_status flagged.save() return HttpResponseRedirect(reverse("flagit.queue"))
def _answers_data(request, question_id, form=None, watch_form=None, answer_preview=None): """Return a map of the minimal info necessary to draw an answers page.""" question = get_object_or_404(Question, pk=question_id) answers_ = question.answers.all() # Remove spam flag if an answer passed the moderation queue if not settings.READ_ONLY: answers_.filter(flags__status=2).update(is_spam=False) if not request.user.has_perm("flagit.can_moderate"): answers_ = answers_.filter(is_spam=False) answers_ = paginate(request, answers_, per_page=config.ANSWERS_PER_PAGE) feed_urls = (( reverse("questions.answers.feed", kwargs={"question_id": question_id}), AnswersFeed().title(question), ), ) frequencies = dict(FREQUENCY_CHOICES) is_watching_question = request.user.is_authenticated and ( QuestionReplyEvent.is_notifying(request.user, question) or QuestionSolvedEvent.is_notifying(request.user, question)) return { "question": question, "answers": answers_, "form": form or AnswerForm(), "answer_preview": answer_preview, "watch_form": watch_form or _init_watch_form(request, "reply"), "feeds": feed_urls, "frequencies": frequencies, "is_watching_question": is_watching_question, "can_tag": request.user.has_perm("questions.tag_question"), "can_create_tags": request.user.has_perm("taggit.add_tag"), }
def test_answer_notification(self, get_current): """Assert that hitting the watch toggle toggles and that proper mails are sent to anonymous users, registered users, and the question asker.""" # TODO: This test is way too monolithic, and the fixtures encode # assumptions that aren't obvious here. Split this test into about 5, # each of which tests just 1 thing. Consider using instantiation # helpers. get_current.return_value.domain = 'testserver' # An arbitrary registered user watches: watcher = user(save=True) q = self._toggle_watch_question('reply', watcher, turn_on=True) # An anonymous user watches: QuestionReplyEvent.notify('*****@*****.**', q) # The question asker watches: QuestionReplyEvent.notify(q.creator, q) # Post a reply replier = user(save=True) self.client.login(username=replier.username, password='******') post(self.client, 'questions.reply', {'content': 'an answer'}, args=[q.id]) a = Answer.uncached.filter().order_by('-id')[0] # Order of emails is not important. eq_(3, len(mail.outbox)) emails_to = [m.to[0] for m in mail.outbox] i = emails_to.index(watcher.email) attrs_eq(mail.outbox[i], to=[watcher.email], subject='%s commented on a Firefox question ' "you're watching" % a.creator.username) body = mail.outbox[i].body body = re.sub(r'auth=[a-zA-Z0-9%_-]+', r'auth=AUTH', body) starts_with(body, ANSWER_EMAIL.format( to_user=watcher.username, title=q.title, content=a.content, replier=replier.username, question_id=q.id, answer_id=a.id)) i = emails_to.index(q.creator.email) attrs_eq(mail.outbox[i], to=[q.creator.email], subject='%s posted an answer to your question "%s"' % (a.creator.username, q.title)) body = mail.outbox[i].body body = re.sub(r'auth=[a-zA-Z0-9%_-]+', r'auth=AUTH', body) starts_with(body, ANSWER_EMAIL_TO_ASKER.format( asker=q.creator.username, title=q.title, content=a.content, replier=replier.username, question_id=q.id, answer_id=a.id)) i = emails_to.index('*****@*****.**') attrs_eq(mail.outbox[i], to=['*****@*****.**'], subject="%s commented on a Firefox question you're watching" % a.creator.username) body = mail.outbox[i].body body = re.sub(r'auth=[a-zA-Z0-9%_-]+', r'auth=AUTH', body) starts_with(body, ANSWER_EMAIL_TO_ANONYMOUS.format( title=q.title, content=a.content, replier=replier.username, question_id=q.id, answer_id=a.id))
def unwatch_question(request, question_id): """Stop watching a question.""" question = get_object_or_404(Question, pk=question_id) QuestionReplyEvent.stop_notifying(request.user, question) QuestionSolvedEvent.stop_notifying(request.user, question) return HttpResponseRedirect(question.get_absolute_url())
def setUp(self): self.user = UserFactory() self.client.login(username=self.user.username, password='******') self.question = QuestionFactory(creator=self.user) QuestionReplyEvent.notify(self.user, self.question)
def setUp(self): super(TestAnswerNotifications, self).setUp() self._get_current_mock = mock.patch.object(Site.objects, 'get_current') self._get_current_mock.start().return_value.domain = 'testserver' self.question = QuestionFactory() QuestionReplyEvent.notify(self.question.creator, self.question)
def test_answer_notification(self, get_current): """Assert that hitting the watch toggle toggles and that proper mails are sent to anonymous users, registered users, and the question asker.""" # TODO: This test is way too monolithic, and the fixtures encode # assumptions that aren't obvious here. Split this test into about 5, # each of which tests just 1 thing. Consider using instantiation # helpers. get_current.return_value.domain = 'testserver' # An arbitrary registered user watches: watcher = user(save=True) q = self._toggle_watch_question('reply', watcher, turn_on=True) # An anonymous user watches: QuestionReplyEvent.notify('*****@*****.**', q) # The question asker watches: QuestionReplyEvent.notify(q.creator, q) # Post a reply replier = user(save=True) self.client.login(username=replier.username, password='******') post(self.client, 'questions.reply', {'content': 'an answer'}, args=[q.id]) a = Answer.uncached.filter().order_by('-id')[0] # Order of emails is not important. eq_(3, len(mail.outbox)) emails_to = [m.to[0] for m in mail.outbox] i = emails_to.index(watcher.email) attrs_eq(mail.outbox[i], to=[watcher.email], subject='%s commented on a Firefox question ' "you're watching" % a.creator.username) starts_with( mail.outbox[i].body, ANSWER_EMAIL.format(to_user=watcher.username, title=q.title, content=a.content, replier=replier.username, question_id=q.id, answer_id=a.id)) i = emails_to.index(q.creator.email) attrs_eq(mail.outbox[i], to=[q.creator.email], subject='%s posted an answer to your question "%s"' % (a.creator.username, q.title)) starts_with( mail.outbox[i].body, ANSWER_EMAIL_TO_ASKER.format(asker=q.creator.username, title=q.title, content=a.content, replier=replier.username, question_id=q.id, answer_id=a.id)) i = emails_to.index('*****@*****.**') attrs_eq(mail.outbox[i], to=['*****@*****.**'], subject="%s commented on a Firefox question you're watching" % a.creator.username) starts_with( mail.outbox[i].body, ANSWER_EMAIL_TO_ANONYMOUS.format(title=q.title, content=a.content, replier=replier.username, question_id=q.id, answer_id=a.id))