diff options
Diffstat (limited to 'addons/mail/wizard/mail_compose_message.py')
| -rw-r--r-- | addons/mail/wizard/mail_compose_message.py | 490 |
1 files changed, 179 insertions, 311 deletions
diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index fe87b7584e0..96ce253c2a8 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -19,7 +19,6 @@ # ############################################################################## -import ast import re import tools @@ -28,158 +27,120 @@ from osv import fields from tools.safe_eval import safe_eval as eval from tools.translate import _ -from ..mail_message import to_email - # main mako-like expression pattern EXPRESSION_PATTERN = re.compile('(\$\{.+?\})') class mail_compose_message(osv.TransientModel): - """Generic Email composition wizard. This wizard is meant to be inherited - at model and view level to provide specific wizard features. - - The behavior of the wizard can be modified through the use of context - parameters, among which are: - - * mail.compose.message.mode: if set to 'reply', the wizard is in - reply to a previous message mode and pre-populated with the original - quote. If set to 'comment', it means you are writing a new message to - be attached to a document. If set to 'mass_mail', the wizard is in - mass mailing where the mail details can contain template placeholders - that will be merged with actual data before being sent to each - recipient. - * active_model: model name of the document to which the mail being - composed is related - * active_id: id of the document to which the mail being composed is - related, or id of the message to which user is replying, - in case ``mail.compose.message.mode == 'reply'`` - * active_ids: ids of the documents to which the mail being composed is - related, in case ``mail.compose.message.mode == 'mass_mail'``. + """ Generic message composition wizard. You may inherit from this wizard + at model and view levels to provide specific features. + + The behavior of the wizard can be modified through the context key + mail.compose.message.mode: + - 'reply': reply to a previous message. The wizard is pre-populated + via ``get_message_data``. + - 'comment': new post on a record. The wizard is pre-populated via + ``get_record_data`` + - 'mass_mail': wizard in mass mailing mode where the mail details can + contain template placeholders that will be merged with actual data + before being sent to each recipient. """ _name = 'mail.compose.message' - _inherit = 'mail.message.common' + _inherit = 'mail.message' _description = 'Email composition wizard' + _log_access = True def default_get(self, cr, uid, fields, context=None): - """ Overridden to provide specific defaults depending on the context - parameters. - - Composition mode - - comment: default mode; active_model, active_id = model and ID of a - document we are commenting, - - reply: active_id = ID of a mail.message to which we are replying. - From this message we can find the related model and res_id, - - mass_mailing mode: active_model, active_id = model and ID of a - document we are commenting, - - :param dict context: several context values will modify the behavior - of the wizard, cfr. the class description. + """ Handle composition mode. Some details about context keys: + - comment: default mode, model and ID of a record the user comments + - default_model or active_model + - default_res_id or active_id + - reply: active_id of a message the user replies to + - active_id: ID of a mail.message to which we are replying + - message.res_model or default_model + - message.res_id or default_res_id + - mass_mailing mode: model and IDs of records the user mass-mails + - active_ids: record IDs + - default_model or active_model """ + # get some important values from context if context is None: context = {} - compose_mode = context.get('mail.compose.message.mode', 'comment') - active_model = context.get('active_model') - active_id = context.get('active_id') result = super(mail_compose_message, self).default_get(cr, uid, fields, context=context) + # get some important values from context + composition_mode = context.get('mail.compose.message.mode') + model = context.get('default_model', context.get('active_model')) + res_id = context.get('default_res_id', context.get('active_id')) + active_id = context.get('active_id') + active_ids = context.get('active_ids') + # get default values according to the composition mode - vals = {} - if compose_mode in ['reply']: - vals = self.get_message_data(cr, uid, int(context['active_id']), context=context) - elif compose_mode in ['comment', 'mass_mail'] and active_model and active_id: - vals = self.get_value(cr, uid, active_model, active_id, context) + if composition_mode in ['reply']: + vals = self.get_message_data(cr, uid, active_id, context=context) + elif composition_mode in ['comment', 'mass_mail'] and model and res_id: + vals = self.get_record_data(cr, uid, model, res_id, context=context) + else: + vals = {'model': model, 'res_id': res_id} + if composition_mode: + vals['composition_mode'] = composition_mode + for field in vals: if field in fields: result[field] = vals[field] - - # link to model and record if not done yet - if not result.get('model') and active_model: - result['model'] = active_model - if not result.get('res_id') and active_id: - result['res_id'] = active_id - - # Try to provide default email_from if not specified yet - if not result.get('email_from'): - current_user = self.pool.get('res.users').browse(cr, uid, uid, context=context) - result['email_from'] = current_user.email or False return result + def _get_composition_mode_selection(self, cr, uid, context=None): + return [('comment', 'Comment a document'), ('reply', 'Reply to a message'), ('mass_mail', 'Mass mailing')] + _columns = { - 'dest_partner_ids': fields.many2many('res.partner', - 'email_message_send_partner_rel', - 'wizard_id', 'partner_id', 'Destination partners', - help="When sending emails through the social network composition wizard"\ - "you may choose to send a copy of the mail to partners."), - 'attachment_ids': fields.many2many('ir.attachment','email_message_send_attachment_rel', 'wizard_id', 'attachment_id', 'Attachments'), - 'auto_delete': fields.boolean('Auto Delete', help="Permanently delete emails after sending"), + 'composition_mode': fields.selection( + lambda s, *a, **k: s._get_composition_mode_selection(*a, **k), + string='Composition mode'), + 'partner_ids': fields.many2many('res.partner', + 'mail_compose_message_res_partner_rel', + 'wizard_id', 'partner_id', 'Additional contacts'), + 'attachment_ids': fields.many2many('ir.attachment', + 'mail_compose_message_ir_attachments_rel', + 'wizard_id', 'attachment_id', 'Attachments'), 'filter_id': fields.many2one('ir.filters', 'Filters'), + 'body_text': fields.text('Plain-text editor body'), + 'content_subtype': fields.char('Message content subtype', size=32, readonly=1, + help="Type of message, usually 'html' or 'plain', used to select "\ + "plain-text or rich-text contents accordingly"), + } + + _defaults = { + 'composition_mode': 'comment', + 'content_subtype': lambda self,cr, uid, context={}: 'plain', + 'body_text': lambda self,cr, uid, context={}: False, + 'body': lambda self,cr, uid, context={}: '', + 'subject': lambda self,cr, uid, context={}: False, + 'partner_ids': [], } - def get_value(self, cr, uid, model, res_id, context=None): + def notify(self, cr, uid, newid, context=None): + """ Override specific notify method of mail.message, because we do + not want that feature in the wizard. """ + return + + def get_record_data(self, cr, uid, model, res_id, context=None): """ Returns a defaults-like dict with initial values for the composition wizard when sending an email related to the document record identified by ``model`` and ``res_id``. - The default implementation returns an empty dictionary, and is meant - to be overridden by subclasses. - :param str model: model name of the document record this mail is related to. - :param int res_id: id of the document record this mail is related to. - :param dict context: several context values will modify the behavior - of the wizard, cfr. the class description. + :param int res_id: id of the document record this mail is related to """ - result = {} - user = self.pool.get('res.users').browse(cr, uid, uid, context=context) - result.update({ - 'model': model, - 'res_id': res_id, - 'email_from': user.email or tools.config.get('email_from', False), - 'body_html': False, - 'body_text': False, - 'subject': False, - 'dest_partner_ids': [], - }) - return result - - def onchange_email_mode(self, cr, uid, ids, value, model, res_id, context=None): - """ email_mode (values: True or False). This onchange on the email mode - allows to have some specific behavior when going in email mode, or - when going out of email mode. - Basically, dest_partner_ids is reset when going out of email - mode. - This method can be overridden for models that want to have their - specific behavior. - Note that currently, this onchange is used in mail.js and called - manually on the form instantiated in the Chatter. - """ - if not value: - return {'value': {'dest_partner_ids': []}} - return {'value': {}} - - def onchange_formatting(self, cr, uid, ids, value, model, res_id, context=None): - """ onchange_formatting (values: True or False). This onchange on the - formatting allows to have some specific behavior when going in - formatting mode, or when going out of formatting. - Basically, subject is reset when going out of formatting mode. - This method can be overridden for models that want to have their - specific behavior. - Note that currently, this onchange is used in mail.js and called - manually on the form instantiated in the Chatter. - """ - if not value: - return {'value': {'subject': False}} - return {'value': {}} + return {'model': model, 'res_id': res_id} def get_message_data(self, cr, uid, message_id, context=None): """ Returns a defaults-like dict with initial values for the composition wizard when replying to the given message (e.g. including the quote - of the initial message, and the correct recipient). It should not be - called unless ``context['mail.compose.message.mode'] == 'reply'``. + of the initial message, and the correct recipients). :param int message_id: id of the mail.message to which the user is replying. - :param dict context: several context values will modify the behavior - of the wizard, cfr. the class description. """ if context is None: context = {} @@ -187,143 +148,116 @@ class mail_compose_message(osv.TransientModel): if not message_id: return result - current_user = self.pool.get('res.users').browse(cr, uid, uid, context) - message_data = self.pool.get('mail.message').browse(cr, uid, message_id, context) - # Form the subject + current_user = self.pool.get('res.users').browse(cr, uid, uid, context=context) + message_data = self.pool.get('mail.message').browse(cr, uid, message_id, context=context) + + # create subject re_prefix = _("Re:") reply_subject = tools.ustr(message_data.subject or '') if not (reply_subject.startswith('Re:') or reply_subject.startswith(re_prefix)): reply_subject = "%s %s" % (re_prefix, reply_subject) - # Form the bodies (text and html). We use the plain text version of the - # original mail, by default, as it is easier to quote than the HTML - # version. TODO: make it possible to switch to HTML on the fly - sent_date = _('On %(date)s, ') % {'date': message_data.date} if message_data.date else '' - sender = _('%(sender_name)s wrote:') % {'sender_name': tools.ustr(message_data.email_from or _('You'))} - body_text = message_data.body_text or '' - body_html = message_data.body_html or '' - quoted_body_text = '> %s' % tools.ustr(body_text.replace('\n', "\n> ") or '') - quoted_body_html = '<blockquote>%s</blockquote>' % (tools.ustr(body_html)), - reply_body_text = '\n%s%s\n%s\n%s' % (sent_date, sender, quoted_body_text, current_user.signature) - reply_body_html = '<br /><br />%s%s<br />%s<br />%s' % (sent_date, sender, quoted_body_html, current_user.signature) - # form dest_partner_ids - dest_partner_ids = [partner.id for partner in message_data.partner_ids] - # Update header and references - reply_headers = {} - reply_references = message_data.references and tools.ustr(message_data.references) or False - reply_message_id = message_data.message_id or False - if reply_message_id: - reply_references = (reply_references or '') + " " + message_data.message_id - reply_headers['In-Reply-To'] = message_data.message_id + # create the reply in the body + reply_header = _('On %(date)s, %(sender_name)s wrote:') % { + 'date': message_data.date if message_data.date else '', + 'sender_name': message_data.author_id.name } + reply_body = '<div>%s<blockquote>%s</blockquote></div>%s' % (reply_header, message_data.body, current_user.signature) + # get partner_ids from original message + partner_ids = [partner.id for partner in message_data.partner_ids] if message_data.partner_ids else [] + # update the result result.update({ - 'body_text': reply_body_text, - 'body_html': reply_body_html, + 'model': message_data.model, + 'res_id': message_data.res_id, + 'parent_id': message_data.id, + 'body': reply_body, 'subject': reply_subject, - 'attachment_ids': [], - 'dest_partner_ids': dest_partner_ids, - 'model': message_data.model or False, - 'res_id': message_data.res_id or False, - 'email_from': current_user.email or message_data.email_to or False, - 'email_to': message_data.reply_to or message_data.email_from or False, - 'email_cc': message_data.email_cc or False, - 'user_id': uid, - # pass msg-id and references of mail we're replying to, to construct the - # new ones later when sending - 'message_id': reply_message_id, - 'references': reply_references, - 'headers': reply_headers, + 'partner_ids': partner_ids, + 'content_subtype': 'html', }) return result + def toggle_content_subtype(self, cr, uid, ids, context=None): + """ hit toggle formatting mode button: calls onchange_formatting to + emulate an on_change, then writes the value to update the form. """ + for record in self.browse(cr, uid, ids, context=context): + content_st_new_value = 'plain' if record.content_subtype == 'html' else 'html' + onchange_res = self.onchange_content_subtype(cr, uid, ids, content_st_new_value, record.model, record.res_id, context=context) + self.write(cr, uid, [record.id], onchange_res['value'], context=context) + return True + + def onchange_content_subtype(self, cr, uid, ids, value, model, res_id, context=None): + """ onchange_content_subtype (values: 'plain' or 'html'). This onchange + on the subtype allows to have some specific behavior when switching + between text or html mode. + Basically, subject is reset when going out of html mode. + This method can be overridden for models that want to have their + specific behavior. + """ + return {'value': {'content_subtype': value}} + + def onchange_partner_ids(self, cr, uid, ids, value, context=None): + """ onchange_partner_ids (value format: [[6, False, [3, 4]]]). The + basic purpose of this method is to check that destination partners + effectively have email addresses. Otherwise a warning is thrown. + """ + partner_ids = value[0][2] + partner_wo_email_lst = [] + for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids, context=context): + if not partner.email: + partner_wo_email_lst.append(partner) + if not partner_wo_email_lst: + return {'value': {}} + warning_msg = _('The following partners chosen as recipients for the email have no email address linked :') + for partner in partner_wo_email_lst: + warning_msg += '\n- %s' % (partner.name) + warning = { + 'title': _('Partners email addresses not found'), + 'message': warning_msg, + } + return {'warning': warning, 'value': {}} + def send_mail(self, cr, uid, ids, context=None): - '''Process the wizard contents and proceed with sending the corresponding - email(s), rendering any template patterns on the fly if needed. - If the wizard is in mass-mail mode (context['mail.compose.message.mode'] is - set to ``'mass_mail'``), the resulting email(s) are scheduled for being - sent the next time the mail.message scheduler runs, or the next time - ``mail.message.process_email_queue`` is called. - Otherwise the new message is sent immediately. - - :param dict context: several context values will modify the behavior - of the wizard, cfr. the class description. - ''' + """ Process the wizard content and proceed with sending the related + email(s), rendering any template patterns on the fly if needed. """ if context is None: context = {} - # composition wizard options - email_mode = context.get('email_mode') - formatting = context.get('formatting') - mass_mail_mode = context.get('mail.compose.message.mode') == 'mass_mail' - - mail_message_obj = self.pool.get('mail.message') - for mail_wiz in self.browse(cr, uid, ids, context=context): - # attachments + + for wizard in self.browse(cr, uid, ids, context=context): + mass_mail_mode = wizard.composition_mode == 'mass_mail' + attachment = {} - for attach in mail_wiz.attachment_ids: - attachment[attach.datas_fname] = attach.datas and attach.datas.decode('base64') + for attach in wizard.attachment_ids: + attachment[attach.datas_fname] = attach.datas and attach.datas or False # default values, according to the wizard options - subject = mail_wiz.subject if formatting else False - content_subtype = 'html' if formatting else 'plain' - type = 'email' if email_mode else 'comment' - state = 'outgoing' if email_mode else False - partner_ids = [partner.id for partner in mail_wiz.dest_partner_ids] - references = None - headers = {} - body = mail_wiz.body_html if content_subtype == 'html' else mail_wiz.body_text - - # get model, active_ids, and check if model is openchatter-enabled - if mass_mail_mode and context.get('active_ids') and context.get('active_model'): - active_ids = context['active_ids'] - active_model = context['active_model'] - elif mass_mail_mode: - active_model = mail_wiz.model + subject = wizard.subject if wizard.content_subtype == 'html' else False + partner_ids = [(4, partner.id) for partner in wizard.partner_ids] + body = wizard.body if wizard.content_subtype == 'html' else wizard.body_text + + active_model_pool = self.pool.get(wizard.model if wizard.model else 'mail.thread') + + #TODO: TDE: WIP: have to check for mass mail and templates - no time anymore today + if context.get('mail.compose.message.mode') == 'mass_mail' and context.get('default_model', False) and context.get('default_res_id', False): + active_model = context.get('default_model', False) active_model_pool = self.pool.get(active_model) - active_ids = active_model_pool.search(cr, uid, ast.literal_eval(mail_wiz.filter_id.domain), context=ast.literal_eval(mail_wiz.filter_id.context)) + subject = self.render_template(cr, uid, subject, active_model, active_id) + body = self.render_template(cr, uid, wizard.body, active_model, active_id) + + # TODO TDE: find partner_ids + # if email_to: find or create a partner + if values['email_to']: + partner_id = self.pool.get('mail.thread').message_partner_by_email(cr, uid, values['email_to'], context=context)['partner_id'] + if not partner_id: + partner_id = self.pool.get('res.partner').name_create(cr, uid, values['email_to'], context=context) + values['partner_ids'] = [partner_id] + + # determine the ids we are commenting + if mass_mail_mode: + res_ids = context.get('active_ids', []) else: - active_model = mail_wiz.model - active_ids = [mail_wiz.res_id] - active_model_pool = self.pool.get(active_model) - mail_thread_enabled = hasattr(active_model_pool, 'message_append') - - if context.get('mail.compose.message.mode') == 'mass_mail': - # Mass mailing: must render the template patterns - for active_id in active_ids: - rendered_subject = self.render_template(cr, uid, subject, active_model, active_id) - rendered_body_html = self.render_template(cr, uid, mail_wiz.body_html, active_model, active_id) - rendered_body_text = self.render_template(cr, uid, mail_wiz.body_text, active_model, active_id) - email_from = self.render_template(cr, uid, mail_wiz.email_from, active_model, active_id) - email_to = self.render_template(cr, uid, mail_wiz.email_to, active_model, active_id) - email_cc = self.render_template(cr, uid, mail_wiz.email_cc, active_model, active_id) - email_bcc = self.render_template(cr, uid, mail_wiz.email_bcc, active_model, active_id) - reply_to = self.render_template(cr, uid, mail_wiz.reply_to, active_model, active_id) - - # in mass-mailing mode we only schedule the mail for sending, it will be - # processed as soon as the mail scheduler runs. - if mail_thread_enabled: - active_model_pool.message_append(cr, uid, [active_id], rendered_subject, rendered_body_text, rendered_body_html, - type=type, content_subtype=content_subtype, state=state, partner_ids=partner_ids, - email_from=email_from, email_to=email_to, email_cc=email_cc, email_bcc=email_bcc, - reply_to=reply_to, references=references, attachments=attachment, headers=headers, context=context) - else: - mail_message_obj.schedule_with_attach(cr, uid, email_from, to_email(email_to), subject, rendered_body_text, - model=mail_wiz.model, email_cc=to_email(email_cc), email_bcc=to_email(email_bcc), reply_to=reply_to, - attachments=attachment, references=references, res_id=active_id, partner_ids=partner_ids, - content_subtype=mail_wiz.content_subtype, headers=headers, context=context) - else: - # normal mode - no mass-mailing - if mail_thread_enabled: - msg_ids = active_model_pool.message_append(cr, uid, active_ids, subject, mail_wiz.body_text, mail_wiz.body_html, - type=type, content_subtype=content_subtype, state=state, partner_ids=partner_ids, - email_from=mail_wiz.email_from, email_to=mail_wiz.email_to, email_cc=mail_wiz.email_cc, email_bcc=mail_wiz.email_bcc, - reply_to=mail_wiz.reply_to, references=references, attachments=attachment, headers=headers, context=context) - else: - msg_ids = [mail_message_obj.schedule_with_attach(cr, uid, mail_wiz.email_from, to_email(mail_wiz.email_to), subject, mail_wiz.body_text, - type=type, model=mail_wiz.model, email_cc=to_email(mail_wiz.email_cc), email_bcc=to_email(mail_wiz.email_bcc), reply_to=mail_wiz.reply_to, - attachments=attachment, references=references, res_id=int(mail_wiz.res_id), partner_ids=partner_ids, - content_subtype=mail_wiz.content_subtype, headers=headers, context=context)] - # in normal mode, we send the email immediately, as the user expects us to (delay should be sufficiently small) - if type == 'email': - mail_message_obj.send(cr, uid, msg_ids, context=context) + res_ids = [wizard.res_id] + active_model_pool.message_post(cr, uid, res_ids, body=body, subject=subject, msg_type='comment', + attachments=attachment, context=context, partner_ids=partner_ids) return {'type': 'ir.actions.act_window_close'} @@ -356,80 +290,14 @@ class mail_compose_message(osv.TransientModel): return tools.ustr(result) return template and EXPRESSION_PATTERN.sub(merge, template) - -class mail_compose_message_extended(osv.TransientModel): - """ Extension of 'mail.compose.message' to support default field values related - to CRM-like models that follow the following conventions: - - 1. The model object must have an attribute '_mail_compose_message' equal to True. - - 2. The model should define the following fields: - - 'name' as subject of the message (required); - - 'email_from' as destination email address (required); - - 'email_cc' as cc email addresses (required); - - 'section_id.reply_to' as reply-to address (optional). - """ - _inherit = 'mail.compose.message' - - def get_value(self, cr, uid, model, res_id, context=None): - """ Overrides the default implementation to provide more default field values - related to the corresponding CRM case. - """ - result = super(mail_compose_message_extended, self).get_value(cr, uid, model, res_id, context=context) - model_obj = self.pool.get(model) - if getattr(model_obj, '_mail_compose_message', False) and res_id: - data = model_obj.browse(cr, uid , res_id, context) - result.update({ - 'email_to': data.email_from or False, - 'email_cc': tools.ustr(data.email_cc or ''), - 'subject': data.name or False, - }) - if hasattr(data, 'section_id'): - result['reply_to'] = data.section_id and data.section_id.reply_to or False - return result - - def onchange_email_mode(self, cr, uid, ids, value, model, res_id, context=None): - """ Overrides the default implementation to provide default values for - dest_partner_ids. This method checks that a partner maching the - ``email_from`` of the record exists. It it does not exist, it - creates a new partner. The found or created partner is then added - in dest_partner_ids. - Partner check/creation valid inly if the value is True, and if - the model has the ``_mail_compose_message`` attribute. - """ - result = super(mail_compose_message_extended, self).onchange_email_mode(cr, uid, ids, value, model, res_id, context=context) - model_obj = self.pool.get(model) - if not value or not (getattr(model_obj, '_mail_compose_message', False) and res_id): - return result - data = model_obj.browse(cr, uid , res_id, context=context) - partner_obj = self.pool.get('res.partner') - partner_ids = partner_obj.search(cr, uid, [('email', '=', data.email_from)], context=context) - if partner_ids: - partner_id = partner_ids[0] - else: - partner_id = partner_obj.name_create(cr, uid, data.email_from, context=context)[0] - result['value'].update({ - 'dest_partner_ids': [partner_id], - 'email_cc': tools.ustr(data.email_cc or ''), - }) - if hasattr(data, 'section_id'): - result['value']['reply_to'] = data.section_id and data.section_id.reply_to or False - return result - - def onchange_formatting(self, cr, uid, ids, value, model, res_id, context=None): - """ Overrides the default implementation to provide default values for - the subject. - Subject re-creation valid only if the value is True, and if the - model has the ``_mail_compose_message`` attribute. - """ - result = super(mail_compose_message_extended, self).onchange_formatting(cr, uid, ids, value, model, res_id, context=context) - model_obj = self.pool.get(model) - if not value or not (getattr(model_obj, '_mail_compose_message', False) and res_id): - return result - data = model_obj.browse(cr, uid , res_id, context=context) - result['value'].update({ - 'subject': data.name or False, - }) - return result - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + def unlink(self, cr, uid, ids, context=None): + # Cascade delete all attachments, as they are owned by the composition wizard + for wizard in self.read(cr, uid, ids, ['attachment_ids'], context=context): + self.pool.get('ir.attachment').unlink(cr, uid, wizard['attachment_ids'], context=context) + return super(mail_compose_message,self).unlink(cr, uid, ids, context=context) + + def dummy(self, cr, uid, ids, context=None): + """ TDE: defined to have buttons that do basically nothing. It is + currently impossible to have buttons that do nothing special + in views (if type not specified, considered as 'object'). """ + return True |
