def mail_update_message(self, res_id, message, message_id, **post): res_model = 'slide.channel' # keep this mecanism intern to slide message_body = plaintext2html(message) res_id = int(res_id) message_id = int(message_id) pid = int(post['pid']) if post.get('pid') else False if not _check_special_access(res_model, res_id, token=post.get('token'), _hash=post.get('hash'), pid=pid): raise Forbidden() # update mail.message domain = [ ('model', '=', res_model), ('res_id', '=', res_id), ('website_published', '=', True), ('author_id', '=', request.env.user.partner_id.id), ('id', '=', message_id) ] # restrict to the given message_id message = request.env['mail.message'].search(domain, limit=1) if not message: raise NotFound() message.write({ 'body': message_body }) # update rating if post.get('rating_value'): domain = [('res_model', '=', res_model), ('res_id', '=', res_id), ('website_published', '=', True), ('message_id', '=', message.id)] rating = request.env['rating.rating'].search(domain, order='write_date DESC', limit=1) rating.write({ 'rating': float(post['rating_value']) }) return werkzeug.utils.redirect(request.httprequest.referrer, 302)
def rating_apply(self, rate, token=None, feedback=None, subtype=None): """ Apply a rating given a token. If the current model inherits from mail.thread mixing, a message is posted on its chatter. :param rate : the rating value to apply :type rate : float :param token : access token :param feedback : additional feedback :type feedback : string :param subtype : subtype for mail :type subtype : string :returns rating.rating record """ Rating, rating = self.env['rating.rating'], None if token: rating = self.env['rating.rating'].search([('access_token', '=', token)], limit=1) else: rating = Rating.search([('res_model', '=', self._name), ('res_id', '=', self.ids[0])], limit=1) if rating: rating.write({'rating': rate, 'feedback': feedback, 'consumed': True}) if hasattr(self, 'message_post'): feedback = tools.plaintext2html(feedback or '') self.message_post( body="<img src='/rating/static/src/img/rating_%s.png' alt=':%s/10' style='width:18px;height:18px;float:left;margin-right: 5px;'/>%s" % (rate, rate, feedback), subtype=subtype or "mail.mt_comment", author_id=rating.partner_id and rating.partner_id.id or None # None will set the default author in mail_thread.py ) if hasattr(self, 'stage_id') and self.stage_id and hasattr(self.stage_id, 'auto_validation_kanban_state') and self.stage_id.auto_validation_kanban_state: if rating.rating > 5: self.write({'kanban_state': 'done'}) if rating.rating < 5: self.write({'kanban_state': 'blocked'}) return rating
def portal_chatter_post(self, res_model, res_id, message, **kw): url = request.httprequest.referrer if message: # message is received in plaintext and saved in html message = plaintext2html(message) _message_post_helper(res_model, int(res_id), message, **kw) url = url + "#discussion" return request.redirect(url)
def test_plaintext2html(self): cases = [ ("First \nSecond \nThird\n \nParagraph\n\r--\nSignature paragraph", 'div', "<div><p>First <br/>Second <br/>Third</p><p>Paragraph</p><p>--<br/>Signature paragraph</p></div>"), ("First<p>It should be escaped</p>\nSignature", False, "<p>First<p>It should be escaped</p><br/>Signature</p>") ] for content, container_tag, expected in cases: html = plaintext2html(content, container_tag) self.assertEqual(html, expected, 'plaintext2html is broken')
def mail_chat_post(self, uuid, message_content, **kwargs): # find the author from the user session, which can be None author_id = False # message_post accept 'False' author_id, but not 'None' if request.session.uid: author_id = request.env['res.users'].sudo().browse(request.session.uid).partner_id.id # post a message without adding followers to the channel. email_from=False avoid to get author from email data mail_channel = request.env["mail.channel"].sudo().search([('uuid', '=', uuid)], limit=1) body = tools.plaintext2html(message_content) message = mail_channel.sudo().with_context(mail_create_nosubscribe=True).message_post(author_id=author_id, email_from=False, body=body, message_type='comment', subtype='mail.mt_comment') return message and message.id or False
def mail_update_message(self, res_model, res_id, message, message_id, redirect=None, attachment_ids='', attachment_tokens='', **post): # keep this mechanism intern to slide currently (saas 12.5) as it is # considered experimental if res_model != 'slide.channel': raise Forbidden() res_id = int(res_id) attachment_ids = [ int(attachment_id) for attachment_id in attachment_ids.split(',') if attachment_id ] attachment_tokens = [ attachment_token for attachment_token in attachment_tokens.split(',') if attachment_token ] self._portal_post_check_attachments(attachment_ids, attachment_tokens) pid = int(post['pid']) if post.get('pid') else False if not _check_special_access(res_model, res_id, token=post.get('token'), _hash=post.get('hash'), pid=pid): raise Forbidden() # fetch and update mail.message message_id = int(message_id) message_body = plaintext2html(message) domain = [('model', '=', res_model), ('res_id', '=', res_id), ('is_internal', '=', False), ('author_id', '=', request.env.user.partner_id.id), ('message_type', '=', 'comment'), ('id', '=', message_id)] # restrict to the given message_id message = request.env['mail.message'].search(domain, limit=1) if not message: raise NotFound() message.sudo().write({ 'body': message_body, 'attachment_ids': [(4, aid) for aid in attachment_ids], }) # update rating if post.get('rating_value'): domain = [('res_model', '=', res_model), ('res_id', '=', res_id), ('is_internal', '=', False), ('message_id', '=', message.id)] rating = request.env['rating.rating'].sudo().search( domain, order='write_date DESC', limit=1) rating.write({ 'rating': float(post['rating_value']), 'feedback': html2plaintext(message.body), }) # redirect to specified or referrer or simply channel page as fallback redirect_url = redirect or (request.httprequest.referrer and request.httprequest.referrer + '#review') or '/slides/%s' % res_id return werkzeug.utils.redirect(redirect_url, 302)
def message_post(self, body='', subject=None, message_type='notification', subtype=None, parent_id=False, attachments=None, content_subtype='html', **kwargs): if attachments is None: attachments = {} if self.ids and not self.ensure_one(): raise exceptions.UserError( _('Invalid record set: should be called as model (without records) or on single-record recordset' )) # inception context = self._context.copy() if not context.get('from_composer', False): context.update({ 'mail_post_autofollow': False, 'mail_create_nosubscribe': True, }) # /inception # if we're processing a message directly coming from the gateway, the destination model was # set in the context. model = False if self.ids: self.ensure_one() model = self._context.get( 'thread_model', False) if self._name == 'mail.thread' else self._name if model and model != self._name and hasattr( self.env[model], 'message_post'): RecordModel = self.env[model].with_context( thread_model=None) # TDE: was removing the key ? return RecordModel.browse(self.ids).message_post( body=body, subject=subject, message_type=message_type, subtype=subtype, parent_id=parent_id, attachments=attachments, content_subtype=content_subtype, **kwargs) # 0: Find the message's author, because we need it for private discussion author_id = kwargs.get('author_id') if author_id is None: # keep False values author_id = self.env['mail.message']._get_default_author().id # 1: Handle content subtype: if plaintext, converto into HTML if content_subtype == 'plaintext': body = tools.plaintext2html(body) # 2: Private message: add recipients (recipients and author of parent message) - current author # + legacy-code management (! we manage only 4 and 6 commands) partner_ids = set() kwargs_partner_ids = kwargs.pop('partner_ids', []) for partner_id in kwargs_partner_ids: if isinstance( partner_id, (list, tuple)) and partner_id[0] == 4 and len(partner_id) == 2: partner_ids.add(partner_id[1]) if isinstance( partner_id, (list, tuple)) and partner_id[0] == 6 and len(partner_id) == 3: partner_ids |= set(partner_id[2]) elif isinstance(partner_id, pycompat.integer_types): partner_ids.add(partner_id) else: pass # we do not manage anything else if parent_id and not model: parent_message = self.env['mail.message'].browse(parent_id) private_followers = set( [partner.id for partner in parent_message.partner_ids]) if parent_message.author_id: private_followers.add(parent_message.author_id.id) private_followers -= set([author_id]) partner_ids |= private_followers # 4: mail.message.subtype subtype_id = kwargs.get('subtype_id', False) if not subtype_id: subtype = subtype or 'mt_note' if '.' not in subtype: subtype = 'mail.%s' % subtype subtype_id = self.env['ir.model.data'].xmlid_to_res_id(subtype) # inception if context.get('put_this_subtype_instead', False): subtype = context.get('put_this_subtype_instead') subtype_id = self.env['ir.model.data'].xmlid_to_res_id(subtype) # / inception # automatically subscribe recipients if asked to if self._context.get( 'mail_post_autofollow') and self.ids and partner_ids: partner_to_subscribe = partner_ids if self._context.get('mail_post_autofollow_partner_ids'): partner_to_subscribe = [ p for p in partner_ids if p in self._context.get( 'mail_post_autofollow_partner_ids') ] self.message_subscribe(list(partner_to_subscribe), force=False) # _mail_flat_thread: automatically set free messages to the first posted message MailMessage = self.env['mail.message'] if self._mail_flat_thread and model and not parent_id and self.ids: messages = MailMessage.search([ '&', ('res_id', '=', self.ids[0]), ('model', '=', model), ('message_type', '=', 'email') ], order="id ASC", limit=1) if not messages: messages = MailMessage.search( ['&', ('res_id', '=', self.ids[0]), ('model', '=', model)], order="id ASC", limit=1) parent_id = messages and messages[0].id or False # we want to set a parent: force to set the parent_id to the oldest ancestor, to avoid having more than 1 # level of thread elif parent_id: messages = MailMessage.sudo().search([('id', '=', parent_id), ('parent_id', '!=', False)], limit=1) # avoid loops when finding ancestors processed_list = [] if messages: message = messages[0] while (message.parent_id and message.parent_id.id not in processed_list): processed_list.append(message.parent_id.id) message = message.parent_id parent_id = message.id values = kwargs values.update({ 'author_id': author_id, 'model': model, 'res_id': model and self.ids[0] or False, 'body': body, 'subject': subject or False, 'message_type': message_type, 'parent_id': parent_id, 'subtype_id': subtype_id, 'partner_ids': [(4, pid) for pid in partner_ids], }) # 3. Attachments # - HACK TDE FIXME: Chatter: attachments linked to the document (not done JS-side), load the message attachment_ids = self._message_post_process_attachments( attachments, kwargs.pop('attachment_ids', []), values) values['attachment_ids'] = attachment_ids # Avoid warnings about non-existing fields for x in ('from', 'to', 'cc'): values.pop(x, None) # Post the message new_message = MailMessage.create(values) # Post-process: subscribe author, update message_last_post # Note: the message_last_post mechanism is no longer used. This # will be removed in a later version. if (self._context.get('mail_save_message_last_post') and model and model != 'mail.thread' and self.ids and subtype_id): subtype_rec = self.env['mail.message.subtype'].sudo().browse( subtype_id) if not subtype_rec.internal: # done with SUPERUSER_ID, because on some models users can post only with read access, # not necessarily write access self.sudo().write({'message_last_post': fields.Datetime.now()}) if author_id and model and self.ids and message_type != 'notification' and not \ self._context.get('mail_create_nosubscribe'): self.message_subscribe([author_id], force=False) self._message_post_after_hook(new_message) return new_message
def html(self, field_label, field_input): return plaintext2html(field_input)
def message_post(self, body='', subject=None, message_type='notification', subtype=None, parent_id=False, attachments=None, content_subtype='html', **kwargs): """ Post a new message in an existing thread, returning the new mail.message ID. :param int thread_id: thread ID to post into, or list with one ID; if False/0, mail.message model will also be set as False :param str body: body of the message, usually raw HTML that will be sanitized :param str type: see mail_message.message_type field :param str content_subtype:: if plaintext: convert body into html :param int parent_id: handle reply to a previous message by adding the parent partners to the message in case of private discussion :param tuple(str,str) attachments or list id: list of attachment tuples in the form ``(name,content)``, where content is NOT base64 encoded Extra keyword arguments will be used as default column values for the new mail.message record. Special cases: - attachment_ids: supposed not attached to any document; attach them to the related document. Should only be set by Chatter. :return int: ID of newly created mail.message """ if attachments is None: attachments = {} if self.ids and not self.ensure_one(): raise exceptions.Warning(_('Invalid record set: should be called ' 'as model (without records) or on ' 'single-record recordset')) # if we're processing a message directly coming from the gateway, # the destination model was set in the context. model = False if self.ids: self.ensure_one() _ = self._context.get('thread_model', False) model = _ if self._name == 'mail.thread' else self._name if model and model != self._name and hasattr(self.env[model], 'message_post'): # TDE: was removing the key ? RecordModel = self.env[model].with_context(thread_model=None) return RecordModel.browse(self.ids).message_post( body=body, subject=subject, message_type=message_type, subtype=subtype, parent_id=parent_id, attachments=attachments, content_subtype=content_subtype, **kwargs) # 0: Find the message's author, because we need it for private # discussion author_id = kwargs.get('author_id') if author_id is None: # keep False values author_id = self.env['mail.message']._get_default_author().id # 1: Handle content subtype: if plaintext, converto into HTML if content_subtype == 'plaintext': body = tools.plaintext2html(body) # 2: Private message: add recipients (recipients and author of parent # message) - current author + legacy-code management (! we manage only # 4 and 6 commands) partner_ids = set() kwargs_partner_ids = kwargs.pop('partner_ids', []) for partner_id in kwargs_partner_ids: if isinstance(partner_id, (list, tuple)) and \ partner_id[0] == 4 and len(partner_id) == 2: partner_ids.add(partner_id[1]) if isinstance(partner_id, (list, tuple)) and \ partner_id[0] == 6 and len(partner_id) == 3: partner_ids |= set(partner_id[2]) elif isinstance(partner_id, pycompat.integer_types): partner_ids.add(partner_id) else: pass # we do not manage anything else if parent_id and not model: parent_message = self.env['mail.message'].browse(parent_id) private_followers = set( [partner.id for partner in parent_message.partner_ids]) if parent_message.author_id: private_followers.add(parent_message.author_id.id) private_followers -= set([author_id]) partner_ids |= private_followers # 4: mail.message.subtype subtype_id = kwargs.get('subtype_id', False) if not subtype_id: subtype = subtype or 'mt_note' if '.' not in subtype: subtype = 'mail.%s' % subtype subtype_id = self.env['ir.model.data'].xmlid_to_res_id(subtype) # automatically subscribe recipients if asked to # if self._context.get('mail_post_autofollow') and \ # self.ids and partner_ids: # partner_to_subscribe = partner_ids # if self._context.get('mail_post_autofollow_partner_ids'): # partner_to_subscribe = [ # p for p in partner_ids if p in # self._context.get('mail_post_autofollow_partner_ids') # ] # self.message_subscribe(list(partner_to_subscribe), force=False) # _mail_flat_thread: automatically set free messages to the first # posted message MailMessage = self.env['mail.message'] if self._mail_flat_thread and model and not parent_id and self.ids: messages = MailMessage.search(['&', ('res_id', '=', self.ids[0]), ('model', '=', model), ('message_type', '=', 'email')], order="id ASC", limit=1) if not messages: messages = MailMessage.search(['&', ('res_id', '=', self.ids[0]), ('model', '=', model)], order="id ASC", limit=1) parent_id = messages and messages[0].id or False # we want to set a parent: force to set the parent_id to the oldest # ancestor, to avoid having more than 1 level of thread elif parent_id: messages = MailMessage.sudo().search([('id', '=', parent_id), ('parent_id', '!=', False)], limit=1) # avoid loops when finding ancestors processed_list = [] if messages: message = messages[0] while message.parent_id and \ message.parent_id.id not in processed_list: processed_list.append(message.parent_id.id) message = message.parent_id parent_id = message.id values = kwargs values.update({ 'author_id': author_id, 'model': model, 'res_id': model and self.ids[0] or False, 'body': body, 'subject': subject or False, 'message_type': message_type, 'parent_id': parent_id, 'subtype_id': subtype_id, 'partner_ids': [(4, pid) for pid in partner_ids], }) # 3. Attachments # - HACK TDE FIXME: Chatter: attachments linked to the document # (not done JS-side), load the message attachment_ids = self._message_post_process_attachments( attachments, kwargs.pop('attachment_ids', []), values) values['attachment_ids'] = attachment_ids # Avoid warnings about non-existing fields for x in ('from', 'to', 'cc'): values.pop(x, None) # Post the message new_message = MailMessage.create(values) # Post-process: subscribe author, update message_last_post # Note: the message_last_post mechanism is no longer used. This # will be removed in a later version. if (self._context.get('mail_save_message_last_post') and model and model != 'mail.thread' and self.ids and subtype_id): subtype_obj = self.env['mail.message.subtype'] subtype_rec = subtype_obj.sudo().browse(subtype_id) if not subtype_rec.internal: # done with SUPERUSER_ID, because on some models users can # post only with read access, not necessarily write access self.sudo().write({'message_last_post': fields.Datetime.now()}) if author_id and model and self.ids and \ message_type != 'notification' and \ not self._context.get('mail_create_nosubscribe'): self.message_subscribe([author_id], force=False) self._message_post_after_hook(new_message) return new_message
def event_track_proposal_post(self, event, **post): if not event.can_access_from_current_website(): raise NotFound() # Only accept existing tag indices. Use search instead of browse + exists: # this prevents users to register colorless tags if not allowed to (ACL). input_tag_indices = [ int(tag_id) for tag_id in post['tags'].split(',') if tag_id ] valid_tag_indices = request.env['event.track.tag'].search([ ('id', 'in', input_tag_indices) ]).ids contact = request.env['res.partner'] visitor_partner = request.env[ 'website.visitor']._get_visitor_from_request().partner_id # Contact name is required. Therefore, empty contacts are not considered here. At least one of contact_phone # and contact_email must be filled. Email is verified. If the post tries to create contact with no valid entry, # raise exception. If normalized email is the same as logged partner, use its partner_id on track instead. # This prevents contact duplication. Otherwise, create new contact with contact additional info of post. if post.get('add_contact_information'): valid_contact_email = tools.email_normalize( post.get('contact_email')) # Here, the phone is not formatted. To format it, one needs a country. Based on a country, from geoip for instance. # The problem is that one could propose a track in country A with phone number of country B. Validity is therefore # quite tricky. We accept any format of contact_phone. Could be improved with select country phone widget. if valid_contact_email or post.get('contact_phone'): if visitor_partner and valid_contact_email == visitor_partner.email_normalized: contact = visitor_partner else: contact = request.env['res.partner'].sudo().create({ 'email': valid_contact_email, 'name': post.get('contact_name'), 'phone': post.get('contact_phone'), }) else: raise exceptions.ValidationError( _("Format Error : please enter a valid contact phone or contact email." )) # If the speaker email is the same as logged user's, then also uses its partner on track, same as above. else: valid_speaker_email = tools.email_normalize(post['partner_email']) if visitor_partner and valid_speaker_email == visitor_partner.email_normalized: contact = visitor_partner track = request.env['event.track'].with_context({ 'mail_create_nosubscribe': True }).sudo().create({ 'name': post['track_name'], 'partner_id': contact.id, 'partner_name': post['partner_name'], 'partner_email': post['partner_email'], 'partner_phone': post['partner_phone'], 'partner_function': post['partner_function'], 'contact_phone': contact.phone, 'contact_email': contact.email, 'event_id': event.id, 'tag_ids': [(6, 0, valid_tag_indices)], 'description': plaintext2html(post['description']), 'partner_biography': plaintext2html(post['partner_biography']), 'user_id': False, 'image': base64.b64encode(post['image'].read()) if post.get('image') else False, }) if request.env.user != request.website.user_id: track.sudo().message_subscribe( partner_ids=request.env.user.partner_id.ids) return request.redirect('/event/%s/track_proposal/success/%s' % (event.id, track.id))
def mail_update_message(self, res_model, res_id, message, message_id, attachment_ids=None, attachment_tokens=None, **post): # keep this mechanism intern to slide currently (saas 12.5) as it is # considered experimental if res_model != 'slide.channel': raise Forbidden() res_id = int(res_id) self._portal_post_check_attachments(attachment_ids, attachment_tokens) pid = int(post['pid']) if post.get('pid') else False if not _check_special_access(res_model, res_id, token=post.get('token'), _hash=post.get('hash'), pid=pid): raise Forbidden() # fetch and update mail.message message_id = int(message_id) message_body = plaintext2html(message) domain = [('model', '=', res_model), ('res_id', '=', res_id), ('is_internal', '=', False), ('author_id', '=', request.env.user.partner_id.id), ('message_type', '=', 'comment'), ('id', '=', message_id)] # restrict to the given message_id message = request.env['mail.message'].search(domain, limit=1) if not message: raise NotFound() message.sudo().write({ 'body': message_body, 'attachment_ids': [(4, aid) for aid in attachment_ids], }) # update rating if post.get('rating_value'): domain = [('res_model', '=', res_model), ('res_id', '=', res_id), ('is_internal', '=', False), ('message_id', '=', message.id)] rating = request.env['rating.rating'].sudo().search( domain, order='write_date DESC', limit=1) rating.write({ 'rating': float(post['rating_value']), 'feedback': html2plaintext(message.body), }) channel = request.env[res_model].browse(res_id) return { 'default_message_id': message.id, 'default_message': html2plaintext(message.body), 'default_rating_value': message.rating_value, 'rating_avg': channel.rating_avg, 'rating_count': channel.rating_count, 'default_attachment_ids': message.attachment_ids.sudo().read( ['id', 'name', 'mimetype', 'file_size', 'access_token']), 'force_submit_url': '/slides/mail/update_comment', }
def _microsoft_to_odoo_values(self, microsoft_event, default_reminders=(), default_values={}): if microsoft_event.is_cancelled(): return {'active': False} sensitivity_o2m = { 'normal': 'public', 'private': 'private', 'confidential': 'confidential', } commands_attendee, commands_partner = self._odoo_attendee_commands_m( microsoft_event) timeZone_start = pytz.timezone(microsoft_event.start.get('timeZone')) timeZone_stop = pytz.timezone(microsoft_event.end.get('timeZone')) start = parse(microsoft_event.start.get('dateTime')).astimezone( timeZone_start).replace(tzinfo=None) if microsoft_event.isAllDay: stop = parse(microsoft_event.end.get('dateTime')).astimezone( timeZone_stop).replace(tzinfo=None) - relativedelta(days=1) else: stop = parse(microsoft_event.end.get('dateTime')).astimezone( timeZone_stop).replace(tzinfo=None) values = { **default_values, 'name': microsoft_event.subject or _("(No title)"), 'description': plaintext2html(microsoft_event.bodyPreview), 'location': microsoft_event.location and microsoft_event.location.get('displayName') or False, 'user_id': microsoft_event.owner(self.env).id, 'privacy': sensitivity_o2m.get(microsoft_event.sensitivity, self.default_get(['privacy'])['privacy']), 'attendee_ids': commands_attendee, 'partner_ids': commands_partner, 'allday': microsoft_event.isAllDay, 'start': start, 'stop': stop, 'show_as': 'free' if microsoft_event.showAs == 'free' else 'busy', 'recurrency': microsoft_event.is_recurrent() } values['microsoft_id'] = microsoft_event.id if microsoft_event.is_recurrent(): values[ 'microsoft_recurrence_master_id'] = microsoft_event.seriesMasterId alarm_commands = self._odoo_reminders_commands_m(microsoft_event) if alarm_commands: values['alarm_ids'] = alarm_commands return values
# for ext in test_mail_examples.MSOFFICE_1_OUT: # self.assertNotIn(ext, new_html) class TestHtmlTools(BaseCase): """ Test some of our generic utility functions about html """ def test_plaintext2html(self): cases = [ ("First \nSecond \nThird\n \nParagraph\n\r--\nSignature paragraph", 'div', "<div><p>First <br/>Second <br/>Third</p><p>Paragraph</p><p>--<br/>Signature paragraph</p></div>"), ("First<p>It should be escaped</p>\nSignature", False, "<p>First<p>It should be escaped</p><br/>Signature</p>") ] for content, container_tag, expected in cases: html = plaintext2html(content, container_tag) self.assertEqual(html, expected, 'plaintext2html is broken') def test_append_to_html(self): test_samples = [ ('<!DOCTYPE...><HTML encoding="blah">some <b>content</b></HtMl>', '--\nYours truly', True, True, False, '<!DOCTYPE...><html encoding="blah">some <b>content</b>\n<pre>--\nYours truly</pre>\n</html>'), ('<!DOCTYPE...><HTML encoding="blah">some <b>content</b></HtMl>', '--\nYours truly', True, False, False, '<!DOCTYPE...><html encoding="blah">some <b>content</b>\n<p>--<br/>Yours truly</p>\n</html>'), ('<html><body>some <b>content</b></body></html>', '<!DOCTYPE...>\n<html><body>\n<p>--</p>\n<p>Yours truly</p>\n</body>\n</html>', False, False, False, '<html><body>some <b>content</b>\n\n\n<p>--</p>\n<p>Yours truly</p>\n\n\n</body></html>'), ] for html, content, plaintext_flag, preserve_flag, container_tag, expected in test_samples: self.assertEqual(append_content_to_html(html, content, plaintext_flag, preserve_flag, container_tag), expected, 'append_content_to_html is broken') def test_is_html_empty(self):
def _microsoft_to_odoo_values(self, microsoft_event, default_reminders=(), default_values={}): if microsoft_event.is_cancelled(): return {'active': False} sensitivity_o2m = { 'normal': 'public', 'private': 'private', 'confidential': 'confidential', } commands_attendee, commands_partner = self._odoo_attendee_commands_m( microsoft_event) timeZone_start = pytz.timezone(microsoft_event.start.get('timeZone')) timeZone_stop = pytz.timezone(microsoft_event.end.get('timeZone')) start = parse(microsoft_event.start.get('dateTime')).astimezone( timeZone_start).replace(tzinfo=None) if microsoft_event.isAllDay: stop = parse(microsoft_event.end.get('dateTime')).astimezone( timeZone_stop).replace(tzinfo=None) - relativedelta(days=1) else: stop = parse(microsoft_event.end.get('dateTime')).astimezone( timeZone_stop).replace(tzinfo=None) values = { **default_values, 'name': microsoft_event.subject or _("(No title)"), 'description': plaintext2html(microsoft_event.bodyPreview), 'location': microsoft_event.location and microsoft_event.location.get('displayName') or False, 'user_id': microsoft_event.owner(self.env).id, 'privacy': sensitivity_o2m.get(microsoft_event.sensitivity, self.default_get(['privacy'])['privacy']), 'attendee_ids': commands_attendee, 'allday': microsoft_event.isAllDay, 'start': start, 'stop': stop, 'show_as': 'free' if microsoft_event.showAs == 'free' else 'busy', 'recurrency': microsoft_event.is_recurrent() } if commands_partner: # Add partner_commands only if set from Microsoft. The write method on calendar_events will # override attendee commands if the partner_ids command is set but empty. values['partner_ids'] = commands_partner if microsoft_event.is_recurrent( ) and not microsoft_event.is_recurrence(): # Propagate the follow_recurrence according to the google result values[ 'follow_recurrence'] = not microsoft_event.is_recurrence_outlier( ) values['microsoft_id'] = microsoft_event.id if microsoft_event.is_recurrent(): values[ 'microsoft_recurrence_master_id'] = microsoft_event.seriesMasterId alarm_commands = self._odoo_reminders_commands_m(microsoft_event) if alarm_commands: values['alarm_ids'] = alarm_commands return values
def _process_step_forward_operator(self, mail_channel): """ Special type of step that will add a human operator to the conversation when reached, which stops the script and allow the visitor to discuss with a real person. In case we don't find any operator (e.g: no-one is available) we don't post any messages. The script will continue normally, which allows to add extra steps when it's the case (e.g: ask for the visitor's email and create a lead). """ human_operator = False posted_message = False if mail_channel.livechat_channel_id: human_operator = mail_channel.livechat_channel_id._get_random_operator() if human_operator: mail_channel.sudo().add_members( human_operator.partner_id.ids, open_chat_window=True, post_joined_message=False) if self.message: # first post the message of the step (if we have one) posted_message = mail_channel._chatbot_post_message(self.chatbot_script_id, plaintext2html(self.message)) # then post a small custom 'Operator has joined' notification mail_channel._chatbot_post_message( self.chatbot_script_id, Markup('<div class="o_mail_notification">%s</div>') % _('%s has joined', human_operator.partner_id.name)) mail_channel._broadcast(human_operator.partner_id.ids) mail_channel.channel_pin(mail_channel.uuid, pinned=True) return posted_message