summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOdoo's Mergebot <robodoo@odoo.com>2021-03-19 18:27:51 +0100
committerGitHub <noreply@github.com>2021-03-19 18:27:51 +0100
commit4a8cc2ef2b0c8508c94b00bcde6d6ab6dba8edf7 (patch)
treede28f9344bf2ae2dda0111c8ce16ba293a36a1a7
parente446233a1b454ab1aa883609fd0b8a588fec9f00 (diff)
parent353bc5de9fafbc1527c382614e02438ee799c8a4 (diff)
[MERGE] event: provide tests and fixes for communication schedulers
Purpose of this merge is to provide some fixes and additional tests for event mail schedulers. Notably * do not autocommit when running event mail schedulers in test mode * allow event user to confirm registrations even with mail schedulers * respect scheduled date when calling mail schedulers execute Improve tests. Related to Task ID-2414658 COM PR #68158 Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
-rw-r--r--addons/event/models/event_mail.py27
-rw-r--r--addons/event/models/event_registration.py2
-rw-r--r--addons/event/tests/test_event_mail_schedule.py284
-rw-r--r--addons/event_sms/models/event_mail.py22
-rw-r--r--addons/mail/tests/common.py4
5 files changed, 270 insertions, 69 deletions
diff --git a/addons/event/models/event_mail.py b/addons/event/models/event_mail.py
index 58fc24af9ed..9d9df8faf06 100644
--- a/addons/event/models/event_mail.py
+++ b/addons/event/models/event_mail.py
@@ -1,6 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import logging
+import random
+import threading
+
from datetime import datetime
from dateutil.relativedelta import relativedelta
@@ -8,8 +12,6 @@ from odoo import api, fields, models, tools
from odoo.tools import exception_to_unicode
from odoo.tools.translate import _
-import random
-import logging
_logger = logging.getLogger(__name__)
_INTERVALS = {
@@ -117,10 +119,11 @@ class EventMailScheduler(models.Model):
if lines:
mail.write({'mail_registration_ids': lines})
# execute scheduler on registrations
- mail.mail_registration_ids.filtered(lambda reg: reg.scheduled_date and reg.scheduled_date <= now).execute()
+ mail.mail_registration_ids.execute()
else:
# Do not send emails if the mailing was scheduled before the event but the event is over
- if not mail.mail_sent and (mail.interval_type != 'before_event' or mail.event_id.date_end > now) and mail.notification_type == 'mail':
+ if not mail.mail_sent and mail.scheduled_date <= now and mail.notification_type == 'mail' and \
+ (mail.interval_type != 'before_event' or mail.event_id.date_end > now):
mail.event_id.mail_attendees(mail.template_id.id)
mail.write({'mail_sent': True})
return True
@@ -176,7 +179,7 @@ You receive this email because you are:
self.invalidate_cache()
self._warn_template_error(scheduler, e)
else:
- if autocommit:
+ if autocommit and not getattr(threading.currentThread(), 'testing', False):
self.env.cr.commit()
return True
@@ -193,10 +196,16 @@ class EventMailRegistration(models.Model):
mail_sent = fields.Boolean('Mail Sent')
def execute(self):
- for mail in self:
- if mail.registration_id.state in ['open', 'done'] and not mail.mail_sent and mail.scheduler_id.notification_type == 'mail':
- mail.scheduler_id.template_id.send_mail(mail.registration_id.id)
- mail.write({'mail_sent': True})
+ now = fields.Datetime.now()
+ todo = self.filtered(lambda reg_mail:
+ not reg_mail.mail_sent and \
+ reg_mail.registration_id.state in ['open', 'done'] and \
+ (reg_mail.scheduled_date and reg_mail.scheduled_date <= now) and \
+ reg_mail.scheduler_id.notification_type == 'mail'
+ )
+ for reg_mail in todo:
+ reg_mail.scheduler_id.template_id.send_mail(reg_mail.registration_id.id)
+ todo.write({'mail_sent': True})
@api.depends('registration_id', 'scheduler_id.interval_unit', 'scheduler_id.interval_type')
def _compute_scheduled_date(self):
diff --git a/addons/event/models/event_registration.py b/addons/event/models/event_registration.py
index 58960460622..5b14153be03 100644
--- a/addons/event/models/event_registration.py
+++ b/addons/event/models/event_registration.py
@@ -160,7 +160,7 @@ class EventRegistration(models.Model):
if vals.get('state') == 'open':
# auto-trigger after_sub (on subscribe) mail schedulers, if needed
onsubscribe_schedulers = self.mapped('event_id.event_mail_ids').filtered(lambda s: s.interval_type == 'after_sub')
- onsubscribe_schedulers.execute()
+ onsubscribe_schedulers.sudo().execute()
return ret
diff --git a/addons/event/tests/test_event_mail_schedule.py b/addons/event/tests/test_event_mail_schedule.py
index 42e1d18017d..896a5873205 100644
--- a/addons/event/tests/test_event_mail_schedule.py
+++ b/addons/event/tests/test_event_mail_schedule.py
@@ -1,76 +1,254 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from datetime import datetime
from dateutil.relativedelta import relativedelta
+from freezegun import freeze_time
-from odoo import fields
from odoo.addons.event.tests.common import TestEventCommon
-from odoo.tools import mute_logger
+from odoo.addons.mail.tests.common import MockEmail
+from odoo.tools import formataddr, mute_logger
-class TestMailSchedule(TestEventCommon):
+class TestMailSchedule(TestEventCommon, MockEmail):
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.models')
def test_event_mail_schedule(self):
""" Test mail scheduling for events """
- now = fields.Datetime.now()
- event_date_begin = now + relativedelta(days=1)
- event_date_end = now + relativedelta(days=3)
-
- test_event = self.env['event.event'].with_user(self.user_eventmanager).create({
- 'name': 'TestEventMail',
- 'auto_confirm': True,
- 'date_begin': event_date_begin,
- 'date_end': event_date_end,
- 'event_mail_ids': [
- (0, 0, { # right at subscription
- 'interval_unit': 'now',
- 'interval_type': 'after_sub',
- 'template_id': self.env['ir.model.data'].xmlid_to_res_id('event.event_subscription')}),
- (0, 0, { # 1 days before event
- 'interval_nbr': 1,
- 'interval_unit': 'days',
- 'interval_type': 'before_event',
- 'template_id': self.env['ir.model.data'].xmlid_to_res_id('event.event_reminder')}),
- ]
- })
+ event_cron_id = self.env.ref('event.event_mail_scheduler')
- # create some registrations
- self.env['event.registration'].with_user(self.user_eventuser).create({
- 'event_id': test_event.id,
- 'name': 'Reg0',
- 'email': 'reg0@example.com',
- })
- self.env['event.registration'].with_user(self.user_eventuser).create({
- 'event_id': test_event.id,
- 'name': 'Reg1',
- 'email': 'reg1@example.com',
- })
+ # deactivate other schedulers to avoid messing with crons
+ self.env['event.mail'].search([]).unlink()
+
+ # freeze some datetimes, and ensure more than 1D+1H before event starts
+ # to ease time-based scheduler check
+ now = datetime(2021, 3, 20, 14, 30, 15)
+ event_date_begin = datetime(2021, 3, 22, 8, 0, 0)
+ event_date_end = datetime(2021, 3, 24, 18, 0, 0)
+
+ with freeze_time(now):
+ test_event = self.env['event.event'].with_user(self.user_eventmanager).create({
+ 'name': 'TestEventMail',
+ 'auto_confirm': True,
+ 'date_begin': event_date_begin,
+ 'date_end': event_date_end,
+ 'event_mail_ids': [
+ (0, 0, { # right at subscription
+ 'interval_unit': 'now',
+ 'interval_type': 'after_sub',
+ 'template_id': self.env['ir.model.data'].xmlid_to_res_id('event.event_subscription')}),
+ (0, 0, { # one day after subscription
+ 'interval_nbr': 1,
+ 'interval_unit': 'hours',
+ 'interval_type': 'after_sub',
+ 'template_id': self.env['ir.model.data'].xmlid_to_res_id('event.event_subscription')}),
+ (0, 0, { # 1 days before event
+ 'interval_nbr': 1,
+ 'interval_unit': 'days',
+ 'interval_type': 'before_event',
+ 'template_id': self.env['ir.model.data'].xmlid_to_res_id('event.event_reminder')}),
+ (0, 0, { # immediately after event
+ 'interval_nbr': 1,
+ 'interval_unit': 'hours',
+ 'interval_type': 'after_event',
+ 'template_id': self.env['ir.model.data'].xmlid_to_res_id('event.event_reminder')}),
+ ]
+ })
# check subscription scheduler
- schedulers = self.env['event.mail'].search([('event_id', '=', test_event.id), ('interval_type', '=', 'after_sub')])
- self.assertEqual(len(schedulers), 1, 'event: wrong scheduler creation')
- self.assertEqual(schedulers[0].scheduled_date, test_event.create_date, 'event: incorrect scheduled date for checking controller')
+ after_sub_scheduler = self.env['event.mail'].search([('event_id', '=', test_event.id), ('interval_type', '=', 'after_sub'), ('interval_unit', '=', 'now')])
+ self.assertEqual(len(after_sub_scheduler), 1, 'event: wrong scheduler creation')
+ self.assertEqual(after_sub_scheduler.scheduled_date, test_event.create_date)
+ self.assertTrue(after_sub_scheduler.done)
+ after_sub_scheduler_2 = self.env['event.mail'].search([('event_id', '=', test_event.id), ('interval_type', '=', 'after_sub'), ('interval_unit', '=', 'hours')])
+ self.assertEqual(len(after_sub_scheduler_2), 1, 'event: wrong scheduler creation')
+ self.assertEqual(after_sub_scheduler_2.scheduled_date, test_event.create_date + relativedelta(hours=1))
+ self.assertTrue(after_sub_scheduler_2.done)
+ # check before event scheduler
+ event_prev_scheduler = self.env['event.mail'].search([('event_id', '=', test_event.id), ('interval_type', '=', 'before_event')])
+ self.assertEqual(len(event_prev_scheduler), 1, 'event: wrong scheduler creation')
+ self.assertEqual(event_prev_scheduler.scheduled_date, event_date_begin + relativedelta(days=-1))
+ self.assertFalse(event_prev_scheduler.done)
+ # check after event scheduler
+ event_next_scheduler = self.env['event.mail'].search([('event_id', '=', test_event.id), ('interval_type', '=', 'after_event')])
+ self.assertEqual(len(event_next_scheduler), 1, 'event: wrong scheduler creation')
+ self.assertEqual(event_next_scheduler.scheduled_date, event_date_end + relativedelta(hours=1))
+ self.assertFalse(event_next_scheduler.done)
+
+ # create some registrations
+ with freeze_time(now), self.mock_mail_gateway():
+ reg1 = self.env['event.registration'].with_user(self.user_eventuser).create({
+ 'event_id': test_event.id,
+ 'name': 'Reg1',
+ 'email': 'reg1@example.com',
+ })
+ reg2 = self.env['event.registration'].with_user(self.user_eventuser).create({
+ 'event_id': test_event.id,
+ 'name': 'Reg2',
+ 'email': 'reg2@example.com',
+ })
+
+ # REGISTRATIONS / PRE SCHEDULERS
+ # --------------------------------------------------
+
+ # check registration state
+ self.assertTrue(all(reg.state == 'open' for reg in reg1 + reg2), 'Registrations: should be auto-confirmed')
+ self.assertTrue(all(reg.date_open == now for reg in reg1 + reg2), 'Registrations: should have open date set to confirm date')
# verify that subscription scheduler was auto-executed after each registration
- self.assertEqual(len(schedulers[0].mail_registration_ids), 2, 'event: incorrect number of mail scheduled date')
+ self.assertEqual(len(after_sub_scheduler.mail_registration_ids), 2, 'event: should have 2 scheduled communication (1 / registration)')
+ for mail_registration in after_sub_scheduler.mail_registration_ids:
+ self.assertEqual(mail_registration.scheduled_date, now)
+ self.assertTrue(mail_registration.mail_sent, 'event: registration mail should be sent at registration creation')
+ self.assertTrue(after_sub_scheduler.done, 'event: all subscription mails should have been sent')
- mails = self.env['mail.mail'].sudo().search([('subject', 'ilike', 'registration'), ('date', '>=', now)], order='date DESC', limit=3)
- self.assertEqual(len(mails), 2, 'event: wrong number of registration mail sent')
+ # check emails effectively sent
+ self.assertEqual(len(self._new_mails), 2, 'event: should have 2 scheduled emails (1 / registration)')
+ self.assertMailMailWEmails(
+ [formataddr((reg1.name, reg1.email)), formataddr((reg2.name, reg2.email))],
+ 'outgoing', '',
+ fields_values={'subject': 'Your registration at %s' % test_event.name,
+ 'email_from': self.user_eventmanager.company_id.email_formatted,
+ })
- for registration in schedulers[0].mail_registration_ids:
- self.assertTrue(registration.mail_sent, 'event: wrongly confirmed mailing on registration')
+ # same for second scheduler: scheduled but not sent
+ self.assertEqual(len(after_sub_scheduler_2.mail_registration_ids), 2, 'event: should have 2 scheduled communication (1 / registration)')
+ for mail_registration in after_sub_scheduler_2.mail_registration_ids:
+ self.assertEqual(mail_registration.scheduled_date, now + relativedelta(hours=1))
+ self.assertFalse(mail_registration.mail_sent, 'event: registration mail should be scheduled, not sent')
+ self.assertFalse(after_sub_scheduler_2.done, 'event: all subscription mails should be scheduled, not sent')
- # check before event scheduler
- schedulers = self.env['event.mail'].search([('event_id', '=', test_event.id), ('interval_type', '=', 'before_event')])
- self.assertEqual(len(schedulers), 1, 'event: wrong scheduler creation')
- self.assertEqual(schedulers[0].scheduled_date, event_date_begin + relativedelta(days=-1), 'event: incorrect scheduled date')
+ # execute event reminder scheduler explicitly, before scheduled date -> should not do anything
+ with freeze_time(now), self.mock_mail_gateway():
+ after_sub_scheduler_2.execute()
+ self.assertFalse(any(mail_reg.mail_sent for mail_reg in after_sub_scheduler_2.mail_registration_ids))
+ self.assertFalse(after_sub_scheduler_2.done)
+ self.assertEqual(len(self._new_mails), 0, 'event: should not send mails before scheduled date')
+
+ # execute event reminder scheduler explicitly, right at scheduled date -> should sent mails
+ now_registration = now + relativedelta(hours=1)
+ with freeze_time(now_registration), self.mock_mail_gateway():
+ after_sub_scheduler_2.execute()
+
+ # verify that subscription scheduler was auto-executed after each registration
+ self.assertEqual(len(after_sub_scheduler_2.mail_registration_ids), 2, 'event: should have 2 scheduled communication (1 / registration)')
+ self.assertTrue(all(mail_reg.mail_sent for mail_reg in after_sub_scheduler_2.mail_registration_ids))
+ # FIXME: field not updated
+ # self.assertTrue(after_sub_scheduler_2.done, 'event: all subscription mails should have been sent')
+
+ # check emails effectively sent
+ self.assertEqual(len(self._new_mails), 2, 'event: should have 2 scheduled emails (1 / registration)')
+ self.assertMailMailWEmails(
+ [formataddr((reg1.name, reg1.email)), formataddr((reg2.name, reg2.email))],
+ 'outgoing', '',
+ fields_values={'subject': 'Your registration at %s' % test_event.name,
+ 'email_from': self.user_eventmanager.company_id.email_formatted,
+ })
+
+ # PRE SCHEDULERS (MOVE FORWARD IN TIME)
+ # --------------------------------------------------
+
+ self.assertFalse(event_prev_scheduler.mail_sent)
+ self.assertFalse(event_prev_scheduler.done)
+
+ # execute event reminder scheduler explicitly, before scheduled date -> should not do anything
+ now_start = event_date_begin + relativedelta(hours=-25)
+ with freeze_time(now_start), self.mock_mail_gateway():
+ event_prev_scheduler.execute()
+
+ self.assertFalse(event_prev_scheduler.mail_sent)
+ self.assertFalse(event_prev_scheduler.done)
+ self.assertEqual(len(self._new_mails), 0)
+
+ # execute cron to run schedulers
+ now_start = event_date_begin + relativedelta(hours=-23)
+ with freeze_time(now_start), self.mock_mail_gateway():
+ event_cron_id.method_direct_trigger()
+
+ # check that scheduler is finished
+ self.assertTrue(event_prev_scheduler.mail_sent, 'event: reminder scheduler should have run')
+ self.assertTrue(event_prev_scheduler.done, 'event: reminder scheduler should have run')
+
+ # check emails effectively sent
+ self.assertEqual(len(self._new_mails), 2, 'event: should have scheduled 2 mails (1 / registration)')
+ self.assertMailMailWEmails(
+ [formataddr((reg1.name, reg1.email)), formataddr((reg2.name, reg2.email))],
+ 'outgoing', '',
+ fields_values={'subject': '%s: tomorrow' % test_event.name,
+ 'email_from': self.user_eventmanager.company_id.email_formatted,
+ })
+
+ # NEW REGISTRATION EFFECT ON SCHEDULERS
+ # --------------------------------------------------
+
+ test_event.write({'auto_confirm': False})
+ with freeze_time(now_start), self.mock_mail_gateway():
+ reg3 = self.env['event.registration'].with_user(self.user_eventuser).create({
+ 'event_id': test_event.id,
+ 'name': 'Reg3',
+ 'email': 'reg3@example.com',
+ })
+
+ # no more seats
+ self.assertEqual(reg3.state, 'draft')
+
+ # schedulers state untouched
+ self.assertTrue(event_prev_scheduler.mail_sent)
+ self.assertTrue(event_prev_scheduler.mail_sent)
+ self.assertFalse(event_next_scheduler.mail_sent)
+ self.assertFalse(event_next_scheduler.done)
+ self.assertFalse(after_sub_scheduler.done, 'event: scheduler registrations should be lower than effective registrations')
+ self.assertFalse(after_sub_scheduler_2.done, 'event: scheduler registrations should be lower than effective registrations')
+
+ # confirm registration -> should trigger registration schedulers
+ # NOTE: currently all schedulers are based on date_open which equals create_date
+ # meaning several communications may be sent in the time time
+ with freeze_time(now_start + relativedelta(hours=1)), self.mock_mail_gateway():
+ reg3.action_confirm()
+
+ # verify that subscription scheduler was auto-executed after new registration confirmed
+ self.assertEqual(len(after_sub_scheduler.mail_registration_ids), 3, 'event: should have 3 scheduled communication (1 / registration)')
+ new_mail_reg = after_sub_scheduler.mail_registration_ids.filtered(lambda mail_reg: mail_reg.registration_id == reg3)
+ self.assertEqual(new_mail_reg.scheduled_date, now_start)
+ self.assertTrue(new_mail_reg.mail_sent, 'event: registration mail should be sent at registration creation')
+ self.assertTrue(after_sub_scheduler.done, 'event: all subscription mails should have been sent')
+
+ # verify that subscription scheduler was auto-executed after new registration confirmed
+ self.assertEqual(len(after_sub_scheduler_2.mail_registration_ids), 3, 'event: should have 3 scheduled communication (1 / registration)')
+ new_mail_reg = after_sub_scheduler_2.mail_registration_ids.filtered(lambda mail_reg: mail_reg.registration_id == reg3)
+ self.assertEqual(new_mail_reg.scheduled_date, now_start + relativedelta(hours=1))
+ self.assertTrue(new_mail_reg.mail_sent, 'event: registration mail should be sent at registration creation')
+ self.assertTrue(after_sub_scheduler_2.done, 'event: all subscription mails should have been sent')
+
+ # check emails effectively sent
+ self.assertEqual(len(self._new_mails), 2, 'event: should have 1 scheduled emails (new registration only)')
+ # manual check because 2 identical mails are sent and mail tools do not support it easily
+ for mail in self._new_mails:
+ self.assertEqual(mail.email_from, self.user_eventmanager.company_id.email_formatted)
+ self.assertEqual(mail.subject, 'Your registration at %s' % test_event.name)
+ self.assertEqual(mail.state, 'outgoing')
+ self.assertEqual(mail.email_to, formataddr((reg3.name, reg3.email)))
+
+ # POST SCHEDULERS (MOVE FORWARD IN TIME)
+ # --------------------------------------------------
+
+ self.assertFalse(event_next_scheduler.mail_sent)
+ self.assertFalse(event_next_scheduler.done)
- # execute event reminder scheduler explicitly
- schedulers[0].execute()
+ # execute event reminder scheduler explicitly after its schedule date
+ new_end = event_date_end + relativedelta(hours=2)
+ with freeze_time(new_end), self.mock_mail_gateway():
+ event_cron_id.method_direct_trigger()
- self.assertTrue(schedulers[0].mail_sent, 'event: reminder scheduler should have sent an email')
- self.assertTrue(schedulers[0].done, 'event: reminder scheduler should be done')
+ # check that scheduler is finished
+ self.assertTrue(event_next_scheduler.mail_sent, 'event: reminder scheduler should should have run')
+ self.assertTrue(event_next_scheduler.done, 'event: reminder scheduler should have run')
- mails = self.env['mail.mail'].sudo().search([('subject', 'ilike', 'TestEventMail'), ('date', '>=', now)], order='date DESC', limit=3)
- self.assertEqual(len(mails), 3, 'event: wrong number of reminders in outgoing mail queue')
+ # check emails effectively sent
+ self.assertEqual(len(self._new_mails), 3, 'event: should have scheduled 3 mails, one for each registration')
+ self.assertMailMailWEmails(
+ [formataddr((reg1.name, reg1.email)), formataddr((reg2.name, reg2.email)), formataddr((reg3.name, reg3.email))],
+ 'outgoing', '',
+ fields_values={'subject': '%s: today' % test_event.name,
+ 'email_from': self.user_eventmanager.company_id.email_formatted,
+ })
diff --git a/addons/event_sms/models/event_mail.py b/addons/event_sms/models/event_mail.py
index 53562875f10..3e56a0552a7 100644
--- a/addons/event_sms/models/event_mail.py
+++ b/addons/event_sms/models/event_mail.py
@@ -36,7 +36,9 @@ class EventMailScheduler(models.Model):
now = fields.Datetime.now()
if mail.interval_type != 'after_sub':
# Do not send SMS if the communication was scheduled before the event but the event is over
- if not mail.mail_sent and (mail.interval_type != 'before_event' or mail.event_id.date_end > now) and mail.notification_type == 'sms' and mail.sms_template_id:
+ if not mail.mail_sent and mail.scheduled_date <= now and mail.notification_type == 'sms' and \
+ (mail.interval_type != 'before_event' or mail.event_id.date_end > now) and \
+ mail.sms_template_id:
self.env['event.registration']._message_sms_schedule_mass(
template=mail.sms_template_id,
active_domain=[('event_id', '=', mail.event_id.id), ('state', '!=', 'cancel')],
@@ -50,8 +52,18 @@ class EventMailRegistration(models.Model):
_inherit = 'event.mail.registration'
def execute(self):
- for record in self:
- if record.registration_id.state in ['open', 'done'] and not record.mail_sent and record.scheduler_id.notification_type == 'sms':
- record.registration_id._message_sms_schedule_mass(template=record.scheduler_id.sms_template_id, mass_keep_log=True)
- record.write({'mail_sent': True})
+ now = fields.Datetime.now()
+ todo = self.filtered(lambda reg_mail:
+ not reg_mail.mail_sent and \
+ reg_mail.registration_id.state in ['open', 'done'] and \
+ (reg_mail.scheduled_date and reg_mail.scheduled_date <= now) and \
+ reg_mail.scheduler_id.notification_type == 'sms'
+ )
+ for reg_mail in todo:
+ reg_mail.registration_id._message_sms_schedule_mass(
+ template=reg_mail.scheduler_id.sms_template_id,
+ mass_keep_log=True
+ )
+ todo.write({'mail_sent': True})
+
return super(EventMailRegistration, self).execute()
diff --git a/addons/mail/tests/common.py b/addons/mail/tests/common.py
index d4dbfb2f949..3e1dd62994d 100644
--- a/addons/mail/tests/common.py
+++ b/addons/mail/tests/common.py
@@ -240,7 +240,9 @@ class MockEmail(common.BaseCase):
if content:
self.assertIn(content, sent_mail.body_html)
for fname, fvalue in (fields_values or {}).items():
- self.assertEqual(sent_mail[fname], fvalue)
+ self.assertEqual(
+ sent_mail[fname], fvalue,
+ 'Mail: expected %s for %s, got %s' % (fvalue, fname, sent_mail[fname]))
def assertNoMail(self, author, recipients, mail_message):
try: