def action_submit_rating(self, token, **kwargs): rate = int(kwargs.get('rate')) assert rate in (1, 3, 5), "Incorrect rating" rating = request.env['helpdesk.ticket'].sudo().search([('access_token', '=', token)]) if not rating: return request.not_found() feedback = (kwargs.get('feedback')) rating.rating_last_value = float(rate) if feedback: message = plaintext2html(feedback) post_values = { 'res_model': 'helpdesk.ticket', 'res_id': rating.id, 'message': feedback, 'send_after_commit': False, 'attachment_ids': False, # will be added afterward } message = _message_post_helper(**post_values) lang = rating.partner_id.lang or get_lang(request.env).code return request.env['ir.ui.view'].with_context( lang=lang)._render_template( 'helpdesk_basic.rating_external_page_view', { 'web_base_url': request.env['ir.config_parameter'].sudo().get_param( 'web.base.url'), 'rating_value': rating, })
def _sync_activities(self, fields): # update activities for event in self: if event.activity_ids: activity_values = {} if 'name' in fields: activity_values['summary'] = event.name if 'description' in fields: activity_values[ 'note'] = event.description and tools.plaintext2html( event.description) if 'start' in fields: # self.start is a datetime UTC *only when the event is not allday* # activty.date_deadline is a date (No TZ, but should represent the day in which the user's TZ is) # See 72254129dbaeae58d0a2055cba4e4a82cde495b7 for the same issue, but elsewhere deadline = event.start user_tz = self.env.context.get('tz') if user_tz and not event.allday: deadline = pytz.UTC.localize(deadline) deadline = deadline.astimezone(pytz.timezone(user_tz)) activity_values['date_deadline'] = deadline.date() if 'user_id' in fields: activity_values['user_id'] = event.user_id.id if activity_values.keys(): event.activity_ids.write(activity_values)
def _timesheet_create_task_prepare_values(self): self.ensure_one() project = self._timesheet_find_project() planned_hours = self._convert_qty_company_hours() return { 'name': '%s:%s' % (self.order_id.name or '', self.name.split('\n')[0] or self.product_id.name), 'planned_hours': planned_hours, 'remaining_hours': planned_hours, 'partner_id': self.order_id.partner_id.id, 'description': plaintext2html(self.name) if self.name else False, 'project_id': project.id, 'sale_line_id': self.id, 'company_id': self.company_id.id, 'email_from': self.order_id.partner_id.email, 'user_id': False, # force non assigned task, as created as sudo() }
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=':rating_%s' style='width:20px;height:20px;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 event_track_proposal_post(self, event, **post): if not event.can_access_from_current_website(): raise NotFound() tags = [] for tag in event.allowed_track_tag_ids: if post.get('tag_' + str(tag.id)): tags.append(tag.id) track = request.env['event.track'].sudo().create({ 'name': post['track_name'], 'partner_name': post['partner_name'], 'partner_email': post['email_from'], 'partner_phone': post['phone'], 'partner_biography': plaintext2html(post['biography']), 'event_id': event.id, 'tag_ids': [(6, 0, tags)], 'user_id': False, 'description': plaintext2html(post['description']), '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) else: partner = request.env['res.partner'].sudo().search([ ('email', '=', post['email_from']) ]) if partner: track.sudo().message_subscribe(partner_ids=partner.ids) return request.render("website_event_track.event_track_proposal", { 'track': track, 'event': event })
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 _message_sms(self, body, subtype_id=False, partner_ids=False, number_field=False, sms_numbers=None, sms_pid_to_number=None, **kwargs): """ Main method to post a message on a record using SMS-based notification method. :param body: content of SMS; :param subtype_id: mail.message.subtype used in mail.message associated to the sms notification process; :param partner_ids: if set is a record set of partners to notify; :param number_field: if set is a name of field to use on current record to compute a number to notify; :param sms_numbers: see ``_notify_record_by_sms``; :param sms_pid_to_number: see ``_notify_record_by_sms``; """ self.ensure_one() sms_pid_to_number = sms_pid_to_number if sms_pid_to_number is not None else {} if number_field or (partner_ids is False and sms_numbers is None): info = self._sms_get_recipients_info( force_field=number_field)[self.id] info_partner_ids = info['partner'].ids if info['partner'] else False info_number = info['sanitized'] if info['sanitized'] else info[ 'number'] if info_partner_ids and info_number: sms_pid_to_number[info_partner_ids[0]] = info_number if info_partner_ids: partner_ids = info_partner_ids + (partner_ids or []) if not info_partner_ids: if info_number: sms_numbers = [info_number] + (sms_numbers or []) # will send a falsy notification allowing to fix it through SMS wizards elif not sms_numbers: sms_numbers = [False] if subtype_id is False: subtype_id = self.env['ir.model.data'].xmlid_to_res_id( 'mail.mt_note') return self.message_post( body=plaintext2html(html2plaintext(body)), partner_ids=partner_ids or [], # TDE FIXME: temp fix otherwise crash mail_thread.py message_type='sms', subtype_id=subtype_id, sms_numbers=sms_numbers, sms_pid_to_number=sms_pid_to_number, **kwargs)
def rating_apply(self, rate, token=None, feedback=None, subtype_xmlid=None): """ Apply a rating given a token. If the current model inherits from mail.thread mixin, a message is posted on its chatter. User going through this method should have at least employee rights because of rating manipulation (either employee, either sudo-ed in public controllers after security check granting access). :param float rate : the rating value to apply :param string token : access token :param string feedback : additional feedback :param string subtype_xmlid : xml id of a valid mail.message.subtype :returns rating.rating record """ rating = None if token: rating = self.env['rating.rating'].search( [('access_token', '=', token)], limit=1) else: rating = self.env['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/5' style='width:18px;height:18px;float:left;margin-right: 5px;'/>%s" % (rate, rate, feedback), subtype_xmlid=subtype_xmlid 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 > 2: self.write({'kanban_state': 'done'}) else: self.write({'kanban_state': 'blocked'}) return rating
def mail_chat_post(self, uuid, message_content, **kwargs): mail_channel = request.env["mail.channel"].sudo().search([('uuid', '=', uuid)], limit=1) if not mail_channel: return False # find the author from the user session if request.session.uid: author = request.env['res.users'].sudo().browse(request.session.uid).partner_id author_id = author.id email_from = author.email_formatted else: # If Public User, use catchall email from company author_id = False email_from = mail_channel.anonymous_name or mail_channel.create_uid.company_id.catchall_formatted # post a message without adding followers to the channel. email_from=False avoid to get author from email data body = tools.plaintext2html(message_content) message = mail_channel.with_context(mail_create_nosubscribe=True).message_post(author_id=author_id, email_from=email_from, body=body, message_type='comment', subtype_xmlid='mail.mt_comment') return message and message.id or False
def portal_chatter_post(self, res_model, res_id, message, redirect=None, attachment_ids='', attachment_tokens='', **kw): """Create a new `mail.message` with the given `message` and/or `attachment_ids` and redirect the user to the newly created message. The message will be associated to the record `res_id` of the model `res_model`. The user must have access rights on this target document or must provide valid identifiers through `kw`. See `_message_post_helper`. """ url = redirect or (request.httprequest.referrer and request.httprequest.referrer + "#discussion") or '/my' 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) if message or attachment_ids: # message is received in plaintext and saved in html if message: message = plaintext2html(message) post_values = { 'res_model': res_model, 'res_id': res_id, 'message': message, 'send_after_commit': False, 'attachment_ids': False, # will be added afterward } post_values.update((fname, kw.get(fname)) for fname in self._portal_post_filter_params()) message = _message_post_helper(**post_values) if attachment_ids: # sudo write the attachment to bypass the read access # verification in mail message record = request.env[res_model].browse(res_id) message_values = {'res_id': res_id, 'model': res_model} attachments = record._message_post_process_attachments([], attachment_ids, message_values) if attachments.get('attachment_ids'): message.sudo().write(attachments) return request.redirect(url)
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