diff --git a/addons/bus/controllers/main.py b/addons/bus/controllers/main.py index ee9902ec983..3da386e80b2 100644 --- a/addons/bus/controllers/main.py +++ b/addons/bus/controllers/main.py @@ -20,6 +20,9 @@ class BusController(openerp.http.Controller): # override to add channels def _poll(self, dbname, channels, last, options): + # update the user presence + if request.session.uid and 'im_presence' in options: + request.env['bus.presence'].update(options.get('im_presence')) request.cr.close() request._cr = None return dispatch.poll(dbname, channels, last) diff --git a/addons/bus/models/__init__.py b/addons/bus/models/__init__.py index c1c365d98c0..208eb0c6c81 100644 --- a/addons/bus/models/__init__.py +++ b/addons/bus/models/__init__.py @@ -1,2 +1,4 @@ # -*- coding: utf-8 -*- import bus +import bus_presence +import res_users diff --git a/addons/bus/models/bus_presence.py b/addons/bus/models/bus_presence.py new file mode 100644 index 00000000000..d89cfc9a104 --- /dev/null +++ b/addons/bus/models/bus_presence.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +import datetime +import random +import time + +from openerp import api, fields, models +from openerp import tools +from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT + +from openerp.addons.bus.models.bus import TIMEOUT + +DISCONNECTION_TIMER = TIMEOUT + 5 +AWAY_TIMER = 600 # 10 minutes + + +class ImChatPresence(models.Model): + """ User Chat Presence + Its status is 'online', 'away' or 'offline'. This model should be a one2one, but is not + attached to res_users to avoid database concurrence errors. Since the 'update' method is executed + at each poll, if the user have multiple opened tabs, concurrence errors can happend, but are 'muted-logged'. + """ + + _name = 'im_chat.presence' + _description = 'User Chat Presence' + + _sql_constraints = [('im_chat_user_status_unique', 'unique(user_id)', 'A user can only have one IM status.')] + + user_id = fields.Many2one('res.users', 'Users', required=True, index=True, ondelete='cascade') + last_poll = fields.Datetime('Last Poll', default=lambda self: fields.Datetime.now()) + last_presence = fields.Datetime('Last Presence', default=lambda self: fields.Datetime.now()) + status = fields.Selection([('online', 'Online'), ('away', 'Away'), ('offline', 'Offline')], 'IM Status', default='offline') + + + @api.model + def update(self, user_presence=True): + """ Register the given presence of the current user, and trigger a im_status change if necessary. + The status will not be written or sent if not necessary. + :param user_presence : True, if the user (self._uid) is still detected using its browser. + :type user_presence : boolean + """ + presence = self.search([('user_id', '=', self._uid)], limit=1) + # set the default values + send_notification = True + values = { + 'last_poll': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT), + 'status' : presence and presence.status or 'offline' + } + # update the user or a create a new one + if not presence: # create a new presence for the user + values['status'] = 'online' + values['user_id'] = self._uid + self.create(values) + else: # write the user presence if necessary + if user_presence: + values['last_presence'] = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) + values['status'] = 'online' + else: + threshold = datetime.datetime.now() - datetime.timedelta(seconds=AWAY_TIMER) + if datetime.datetime.strptime(presence.last_presence, DEFAULT_SERVER_DATETIME_FORMAT) < threshold: + values['status'] = 'away' + send_notification = presence.status != values['status'] + # write only if the last_poll is passed TIMEOUT, or if the status has changed + delta = datetime.datetime.utcnow() - datetime.datetime.strptime(presence.last_poll, DEFAULT_SERVER_DATETIME_FORMAT) + if delta > datetime.timedelta(seconds=TIMEOUT) or send_notification: + # Hide transaction serialization errors, which can be ignored, the presence update is not essential + with tools.mute_logger('openerp.sql_db'): + presence.write(values) + # avoid TransactionRollbackError + self.env.cr.commit() # TODO : check if still necessary + # notify if the status has changed + if send_notification: # TODO : add user_id to the channel tuple to allow using user_watch in controller presence + self.env['bus.bus'].sendone((self._cr.dbname, 'im_chat.presence'), {'id': self._uid, 'im_status': values['status']}) + # gc : disconnect the users having a too old last_poll. 1 on 100 chance to do it. + if random.random() < 0.01: + self.check_users_disconnection() + return True + + @api.model + def check_users_disconnection(self): + """ Disconnect the users having a too old last_poll """ + limit_date = (datetime.datetime.utcnow() - datetime.timedelta(0, DISCONNECTION_TIMER)).strftime(DEFAULT_SERVER_DATETIME_FORMAT) + presences = self.search([('last_poll', '<', limit_date), ('status', '!=', 'offline')]) + presences.write({'status': 'offline'}) + notifications = [] + for presence in presences: + notifications.append([(self._cr.dbname, 'im_chat.presence'), {'id': presence.user_id.id, 'im_status': presence.status}]) + self.env['bus.bus'].sendmany(notifications) diff --git a/addons/im_chat/models/res_users.py b/addons/bus/models/res_users.py similarity index 100% rename from addons/im_chat/models/res_users.py rename to addons/bus/models/res_users.py diff --git a/addons/bus/security/ir.model.access.csv b/addons/bus/security/ir.model.access.csv index 8a679b74f81..4a626a0eb5a 100644 --- a/addons/bus/security/ir.model.access.csv +++ b/addons/bus/security/ir.model.access.csv @@ -1,2 +1,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_bus_bus,bus.bus public,model_bus_bus,,0,0,0,0 \ No newline at end of file +access_bus_bus,bus.bus public,model_bus_bus,,0,0,0,0 +access_bus_presence,bus.presence,model_bus_presence,base.group_user,1,1,1,1 +access_bus_presence_portal,bus.presence,model_bus_presence,base.group_portal,1,1,1,1 diff --git a/addons/bus/static/src/js/bus.js b/addons/bus/static/src/js/bus.js index b0756132012..3c828668308 100644 --- a/addons/bus/static/src/js/bus.js +++ b/addons/bus/static/src/js/bus.js @@ -9,12 +9,21 @@ bus.ERROR_DELAY = 10000; bus.Bus = Widget.extend({ init: function(){ + var self = this; this._super(); this.options = {}; this.activated = false; this.channels = []; this.last = 0; this.stop = false; + + // bus presence + this.set("window_focus", true); + this.on("change:window_focus", self, function(e) { + self.options.im_presence = self.get("window_focus"); + }); + $(window).on("focus", _.bind(this.window_focus, this)); + $(window).on("blur", _.bind(this.window_blur, this)); }, start_polling: function(){ if(!this.activated){ @@ -57,6 +66,13 @@ bus.Bus = Widget.extend({ delete_channel: function(channel){ this.channels = _.without(this.channels, channel); }, + // bus presence : window focus/unfocus + window_focus: function() { + this.set("window_focus", true); + }, + window_blur: function() { + this.set("window_focus", false); + }, }); // singleton diff --git a/addons/mail/__openerp__.py b/addons/mail/__openerp__.py index e1f124c77c5..1e9149dc4fb 100644 --- a/addons/mail/__openerp__.py +++ b/addons/mail/__openerp__.py @@ -36,9 +36,11 @@ Main Features 'views/mail_followers_views.xml', 'views/mail_thread_views.xml', 'views/mail_channel_views.xml', + 'views/mail_shortcode_views.xml', 'views/res_config_views.xml', 'data/mail_data.xml', 'data/mail_channel_data.xml', + 'data/mail_shortcode.xml', 'security/mail_security.xml', 'security/ir.model.access.csv', 'views/mail_alias_views.xml', @@ -58,6 +60,7 @@ Main Features 'application': True, 'qweb': [ 'static/src/xml/mail.xml', + 'static/src/xml/mail_chat_common.xml', 'static/src/xml/mail_followers.xml', 'static/src/xml/announcement.xml', ], diff --git a/addons/mail/controllers/__init__.py b/addons/mail/controllers/__init__.py index 8ee9bae18d9..c59e37b30c8 100644 --- a/addons/mail/controllers/__init__.py +++ b/addons/mail/controllers/__init__.py @@ -1 +1,3 @@ +# -*- coding: utf-8 -* import main +import bus diff --git a/addons/im_chat/controllers/main.py b/addons/mail/controllers/bus.py similarity index 100% rename from addons/im_chat/controllers/main.py rename to addons/mail/controllers/bus.py diff --git a/addons/im_chat/data/im_chat_session_data.xml b/addons/mail/data/mail_shortcode_data.xml similarity index 100% rename from addons/im_chat/data/im_chat_session_data.xml rename to addons/mail/data/mail_shortcode_data.xml diff --git a/addons/mail/models/__init__.py b/addons/mail/models/__init__.py index ca72296a6fb..d24e5295cf0 100644 --- a/addons/mail/models/__init__.py +++ b/addons/mail/models/__init__.py @@ -9,9 +9,10 @@ import mail_mail import mail_thread import mail_channel import mail_template +import mail_shortcode import res_partner import res_users import res_config import update import ir_actions -import html2text \ No newline at end of file +import html2text diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index f118133ddf9..635a4999cce 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -14,6 +14,8 @@ class ChannelPartner(models.Model): partner_id = fields.Many2one('res.partner', string='Recipient') channel_id = fields.Many2one('mail.channel', string='Channel') seen_message_id = fields.Many2one('mail.message', string='Last Seen') + state = fields.Selection([('open', 'Open'), ('folded', 'Folded'), ('closed', 'Closed')], string='Status', default='open') + seen_datetime = fields.Datetime('Last Seen Datetime') class Channel(models.Model): @@ -161,3 +163,139 @@ class Channel(models.Model): headers['X-Forge-To'] = list_to res['headers'] = repr(headers) return res + + + # -------------------------- + # Session Methods + # -------------------------- + @api.multi + def session_info(self): + """ Get the header of the current session + :returns the values of the session + :rtype : dict + """ + self.ensure_one() + info = { + 'uuid': self.uuid, + 'users': self.session_user_info(), + 'state': 'open', + } + # add user session state, if available and if user is logged + if self._uid: + domain = [('user_id', '=', self._uid), ('session_id', 'in', self.ids)] + session_state = self.env['im_chat.conversation_state'].search(domain, limit=1) + if session_state: + info['state'] = session_state.state + return info + + @api.multi + def session_user_info(self): + """ Get the user infos for all the identified user in the session + :returns a list of user infos + :rtype : list(dict) + """ + self.ensure_one() + return self.env['res.users'].browse(self.user_ids.ids).read(['id', 'name', 'im_status']) + + @api.model + def session_user_image(self, uuid, user_id): + """ Get the avatar of a user in the given session + :param uuid : the uuid of the session + :param user_id : the user identifier + """ + # get the session + image_b64 = False + if user_id: + session = self.env["im_chat.session"].search([('uuid', '=', uuid)], limit=1) + if session and session.sudo(user_id).is_in_session(): + image_b64 = self.env["res.users"].sudo().browse(user_id).image_small + # set default image if not found + if not image_b64: + image_b64 = 'R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' + return image_b64 + + @api.model + def session_get(self, user_to): + """ Get the canonical session between 2 users, create it if needed. + To reuse an old session, this one must be private, and contains only 2 users. + :param user_to : the identifier of the user + :returns a session header, or False if the user_to was False + :rtype : dict + """ + if user_to: + session = self.search([('user_ids', 'in', user_to), ('user_ids', 'in', [self._uid])], limit=1) + if not (session and len(session.user_ids) == 2 and session.is_private()): + session = self.create({'user_ids': [(6, 0, (user_to, self._uid))]}) + return session.session_info() + + # -------------------------- + # Utils Methods + # -------------------------- + @api.multi + def is_private(self): + """ Return true if the session is private between identified users (no external messages). + The only way to do that is to check if there is a message without author (from_id is False). + """ + self.ensure_one() + message_ids = self.env["im_chat.message"].search([('to_id', 'in', self.ids), ('from_id', '=', None)]) + return len(message_ids) == 0 + + @api.multi + def is_in_session(self): + """ Return True if the current user is in the user_ids of the session. False otherwise. + Note : the user need to be logged + """ + self.ensure_one() + user_id = self._uid + return user_id and user_id in [u.id for u in self.user_ids] + + @api.model + def update_state(self, uuid, state=None): + """ Update the fold_state of the given session. In order to syncronize web browser + tabs, the change will be broadcast to himself (the current user channel). + Note : the user need to be logged + :param status : the new status of the session for the current user. + """ + domain = [('user_id', '=', self._uid), ('session_id.uuid', '=', uuid)] + for session_state in self.env['im_chat.conversation_state'].search(domain): + if not state: + state = session_state.state + if session_state.state == 'open': + state = 'folded' + else: + state = 'open' + session_state.write({'state': state}) + self.env['bus.bus'].sendone((self._cr.dbname, 'im_chat.session', self._uid), session_state.session_id.session_info()) + + @api.model + def get_init_notifications(self): + """ Get unread messages and old messages received less than AWAY_TIMER + ago and the session_info for open or folded window + Note : the user need to be logged + """ + # get the message since the AWAY_TIMER + threshold = datetime.datetime.now() - datetime.timedelta(seconds=AWAY_TIMER) + threshold = threshold.strftime(DEFAULT_SERVER_DATETIME_FORMAT) + domain = [('to_id.user_ids', 'in', [self._uid]), ('create_date', '>', threshold)] + + # get the message since the last poll of the user + presence = self.env['im_chat.presence'].search([('user_id', '=', self._uid)], limit=1) + if presence: + domain.append(('create_date', '>', presence.last_poll)) + messages = self.search_read(domain, ['from_id', 'to_id', 'create_date', 'type', 'message'], order='id asc') + + # get the session of the messages and the not-closed ones + session_ids = [m['to_id'][0] for m in messages] + domain = [('user_id', '=', self._uid), '|', ('state', '!=', 'closed'), ('session_id', 'in', session_ids)] + session_states = self.env['im_chat.conversation_state'].search(domain) + # re-open the session where a message have been recieved recently + session_states.filtered(lambda r: r.state == 'closed').write({'state': 'folded'}) + + # create the notifications (session infos first, then messages) + notifications = [] + for state in session_states: + session_infos = state.session_id.session_info() + notifications.append([(self._cr.dbname, 'im_chat.session', self._uid), session_infos]) + for message in messages: + notifications.append([(self._cr.dbname, 'im_chat.session', self._uid), message]) + return notifications diff --git a/addons/mail/models/mail_shortcode.py b/addons/mail/models/mail_shortcode.py new file mode 100644 index 00000000000..42627e5abe5 --- /dev/null +++ b/addons/mail/models/mail_shortcode.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +import cgi +import re + +from openerp import api, fields, models + + +class ImChatShortcode(models.Model): + """ Shortcode + Canned Responses, allowing the user to defined shortcuts in its chat message. + These shortcode are globals and are available for every user. Smiley use this mecanism. + """ + + _name = 'im_chat.shortcode' + _description = 'Canned Response / Shortcode' + + source = fields.Char('Shortcut', required=True, index=True, help="The shortcut which must be replace in the Chat Messages") + substitution = fields.Char('Substitution', required=True, index=True, help="The excaped html code replacing the shortcut") + description = fields.Char('Description') + + @api.model + def create(self, values): + if values.get('substitution'): + values['substitution'] = self._sanitize_shorcode(values['substitution']) + return super(ImChatShortcode, self).create(values) + + @api.multi + def write(self, values): + if values.get('substitution'): + values['substitution'] = self._sanitize_shorcode(values['substitution']) + return super(ImChatShortcode, self).write(values) + + def _sanitize_shorcode(self, substitution): + """ Sanitize the shortcode substitution : + - HTML substitution : only allow the img tag (smiley) + - escape other substitutions to avoid XSS + """ + is_img_tag = re.match(r'''^$''', substitution, re.M|re.I) + if is_img_tag: + return substitution + return cgi.escape(substitution) + + @api.model + def replace_shortcode(self, message): + for shortcode in self.search([]): + regex = '(?:^|\s)(%s)(?:\s|$)' % re.escape(shortcode.source) + message = re.sub(regex, " " + shortcode.substitution + " ", message) + return message diff --git a/addons/im_chat/static/src/audio/purr.mp3 b/addons/mail/static/src/audio/purr.mp3 similarity index 100% rename from addons/im_chat/static/src/audio/purr.mp3 rename to addons/mail/static/src/audio/purr.mp3 diff --git a/addons/im_chat/static/src/audio/purr.ogg b/addons/mail/static/src/audio/purr.ogg similarity index 100% rename from addons/im_chat/static/src/audio/purr.ogg rename to addons/mail/static/src/audio/purr.ogg diff --git a/addons/im_chat/static/src/audio/ting.mp3 b/addons/mail/static/src/audio/ting.mp3 similarity index 100% rename from addons/im_chat/static/src/audio/ting.mp3 rename to addons/mail/static/src/audio/ting.mp3 diff --git a/addons/im_chat/static/src/audio/ting.ogg b/addons/mail/static/src/audio/ting.ogg similarity index 100% rename from addons/im_chat/static/src/audio/ting.ogg rename to addons/mail/static/src/audio/ting.ogg diff --git a/addons/im_chat/static/src/css/im_chat_backend.css b/addons/mail/static/src/css/mail_chat_backend.css similarity index 100% rename from addons/im_chat/static/src/css/im_chat_backend.css rename to addons/mail/static/src/css/mail_chat_backend.css diff --git a/addons/im_chat/static/src/css/im_chat_common.css b/addons/mail/static/src/css/mail_chat_common.css similarity index 100% rename from addons/im_chat/static/src/css/im_chat_common.css rename to addons/mail/static/src/css/mail_chat_common.css diff --git a/addons/im_chat/static/src/img/avatar.jpg b/addons/mail/static/src/img/smiley/avatar.jpg similarity index 100% rename from addons/im_chat/static/src/img/avatar.jpg rename to addons/mail/static/src/img/smiley/avatar.jpg diff --git a/addons/im_chat/static/src/img/beer.png b/addons/mail/static/src/img/smiley/beer.png similarity index 100% rename from addons/im_chat/static/src/img/beer.png rename to addons/mail/static/src/img/smiley/beer.png diff --git a/addons/im_chat/static/src/img/boar.png b/addons/mail/static/src/img/smiley/boar.png similarity index 100% rename from addons/im_chat/static/src/img/boar.png rename to addons/mail/static/src/img/smiley/boar.png diff --git a/addons/im_chat/static/src/img/cat.png b/addons/mail/static/src/img/smiley/cat.png similarity index 100% rename from addons/im_chat/static/src/img/cat.png rename to addons/mail/static/src/img/smiley/cat.png diff --git a/addons/im_chat/static/src/img/confused.png b/addons/mail/static/src/img/smiley/confused.png similarity index 100% rename from addons/im_chat/static/src/img/confused.png rename to addons/mail/static/src/img/smiley/confused.png diff --git a/addons/im_chat/static/src/img/cookie.png b/addons/mail/static/src/img/smiley/cookie.png similarity index 100% rename from addons/im_chat/static/src/img/cookie.png rename to addons/mail/static/src/img/smiley/cookie.png diff --git a/addons/im_chat/static/src/img/ghost.png b/addons/mail/static/src/img/smiley/ghost.png similarity index 100% rename from addons/im_chat/static/src/img/ghost.png rename to addons/mail/static/src/img/smiley/ghost.png diff --git a/addons/im_chat/static/src/img/green.png b/addons/mail/static/src/img/smiley/green.png similarity index 100% rename from addons/im_chat/static/src/img/green.png rename to addons/mail/static/src/img/smiley/green.png diff --git a/addons/im_chat/static/src/img/heart.png b/addons/mail/static/src/img/smiley/heart.png similarity index 100% rename from addons/im_chat/static/src/img/heart.png rename to addons/mail/static/src/img/smiley/heart.png diff --git a/addons/im_chat/static/src/img/laugh.png b/addons/mail/static/src/img/smiley/laugh.png similarity index 100% rename from addons/im_chat/static/src/img/laugh.png rename to addons/mail/static/src/img/smiley/laugh.png diff --git a/addons/im_chat/static/src/img/man.png b/addons/mail/static/src/img/smiley/man.png similarity index 100% rename from addons/im_chat/static/src/img/man.png rename to addons/mail/static/src/img/smiley/man.png diff --git a/addons/im_chat/static/src/img/man_with_turban.png b/addons/mail/static/src/img/smiley/man_with_turban.png similarity index 100% rename from addons/im_chat/static/src/img/man_with_turban.png rename to addons/mail/static/src/img/smiley/man_with_turban.png diff --git a/addons/im_chat/static/src/img/musti.png b/addons/mail/static/src/img/smiley/musti.png similarity index 100% rename from addons/im_chat/static/src/img/musti.png rename to addons/mail/static/src/img/smiley/musti.png diff --git a/addons/im_chat/static/src/img/neutral.png b/addons/mail/static/src/img/smiley/neutral.png similarity index 100% rename from addons/im_chat/static/src/img/neutral.png rename to addons/mail/static/src/img/smiley/neutral.png diff --git a/addons/im_chat/static/src/img/open_mouth.png b/addons/mail/static/src/img/smiley/open_mouth.png similarity index 100% rename from addons/im_chat/static/src/img/open_mouth.png rename to addons/mail/static/src/img/smiley/open_mouth.png diff --git a/addons/im_chat/static/src/img/pinky.png b/addons/mail/static/src/img/smiley/pinky.png similarity index 100% rename from addons/im_chat/static/src/img/pinky.png rename to addons/mail/static/src/img/smiley/pinky.png diff --git a/addons/im_chat/static/src/img/poop.png b/addons/mail/static/src/img/smiley/poop.png similarity index 100% rename from addons/im_chat/static/src/img/poop.png rename to addons/mail/static/src/img/smiley/poop.png diff --git a/addons/im_chat/static/src/img/postal_horn.png b/addons/mail/static/src/img/smiley/postal_horn.png similarity index 100% rename from addons/im_chat/static/src/img/postal_horn.png rename to addons/mail/static/src/img/smiley/postal_horn.png diff --git a/addons/im_chat/static/src/img/sad.png b/addons/mail/static/src/img/smiley/sad.png similarity index 100% rename from addons/im_chat/static/src/img/sad.png rename to addons/mail/static/src/img/smiley/sad.png diff --git a/addons/im_chat/static/src/img/scream.png b/addons/mail/static/src/img/smiley/scream.png similarity index 100% rename from addons/im_chat/static/src/img/scream.png rename to addons/mail/static/src/img/smiley/scream.png diff --git a/addons/im_chat/static/src/img/smile.png b/addons/mail/static/src/img/smiley/smile.png similarity index 100% rename from addons/im_chat/static/src/img/smile.png rename to addons/mail/static/src/img/smiley/smile.png diff --git a/addons/im_chat/static/src/img/stuck_out_tongue.png b/addons/mail/static/src/img/smiley/stuck_out_tongue.png similarity index 100% rename from addons/im_chat/static/src/img/stuck_out_tongue.png rename to addons/mail/static/src/img/smiley/stuck_out_tongue.png diff --git a/addons/im_chat/static/src/img/thumbsdown.png b/addons/mail/static/src/img/smiley/thumbsdown.png similarity index 100% rename from addons/im_chat/static/src/img/thumbsdown.png rename to addons/mail/static/src/img/smiley/thumbsdown.png diff --git a/addons/im_chat/static/src/img/thumbsup.png b/addons/mail/static/src/img/smiley/thumbsup.png similarity index 100% rename from addons/im_chat/static/src/img/thumbsup.png rename to addons/mail/static/src/img/smiley/thumbsup.png diff --git a/addons/im_chat/static/src/img/trollface.png b/addons/mail/static/src/img/smiley/trollface.png similarity index 100% rename from addons/im_chat/static/src/img/trollface.png rename to addons/mail/static/src/img/smiley/trollface.png diff --git a/addons/im_chat/static/src/img/wink.png b/addons/mail/static/src/img/smiley/wink.png similarity index 100% rename from addons/im_chat/static/src/img/wink.png rename to addons/mail/static/src/img/smiley/wink.png diff --git a/addons/im_chat/static/src/img/yellow.png b/addons/mail/static/src/img/smiley/yellow.png similarity index 100% rename from addons/im_chat/static/src/img/yellow.png rename to addons/mail/static/src/img/smiley/yellow.png diff --git a/addons/im_chat/static/src/img/yum.png b/addons/mail/static/src/img/smiley/yum.png similarity index 100% rename from addons/im_chat/static/src/img/yum.png rename to addons/mail/static/src/img/smiley/yum.png diff --git a/addons/im_chat/static/src/js/im_chat_backend.js b/addons/mail/static/src/js/mail_chat_backend.js similarity index 100% rename from addons/im_chat/static/src/js/im_chat_backend.js rename to addons/mail/static/src/js/mail_chat_backend.js diff --git a/addons/im_chat/static/src/js/im_chat_common.js b/addons/mail/static/src/js/mail_chat_common.js similarity index 100% rename from addons/im_chat/static/src/js/im_chat_common.js rename to addons/mail/static/src/js/mail_chat_common.js diff --git a/addons/im_chat/static/src/xml/im_chat.xml b/addons/mail/static/src/xml/mail_chat_common.xml similarity index 100% rename from addons/im_chat/static/src/xml/im_chat.xml rename to addons/mail/static/src/xml/mail_chat_common.xml diff --git a/addons/im_chat/views/im_chat_session_views.xml b/addons/mail/views/mail_shortcode_views.xml similarity index 100% rename from addons/im_chat/views/im_chat_session_views.xml rename to addons/mail/views/mail_shortcode_views.xml diff --git a/addons/mail/views/mail_templates.xml b/addons/mail/views/mail_templates.xml index 7472cdd39ff..8fa17f867d7 100644 --- a/addons/mail/views/mail_templates.xml +++ b/addons/mail/views/mail_templates.xml @@ -11,6 +11,11 @@ + + + + + diff --git a/openerp/tools/mail.py b/openerp/tools/mail.py index 3c22262e14c..f358044bd08 100644 --- a/openerp/tools/mail.py +++ b/openerp/tools/mail.py @@ -514,6 +514,24 @@ def html2plaintext(html, body_id=None, encoding='utf-8'): return html +def html_escape_keep_url(self, message): + """ Escape the message and transform the url into clickable link + :param string message: the message to escape url and transform them into clickable link + :returns the escaped message + :rtype : string + """ + safe_message = "" + first = 0 + last = 0 + for m in re.finditer('(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?', message): + last = m.start() + safe_message += cgi.escape(message[first:last]) + safe_message += '%s' % (cgi.escape(m.group(0)), m.group(0)) + first = m.end() + last = m.end() + safe_message += cgi.escape(message[last:]) + return safe_message + def plaintext2html(text, container_tag=False): """ Convert plaintext into html. Content of the text is escaped to manage html entities, using cgi.escape(). @@ -587,7 +605,7 @@ def append_content_to_html(html, content, plaintext=True, preserve=False, contai #---------------------------------------------------------- # matches any email in a body of text -email_re = re.compile(r"""([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6})""", re.VERBOSE) +email_re = re.compile(r"""([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6})""", re.VERBOSE) # matches a string containing only one email single_email_re = re.compile(r"""^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$""", re.VERBOSE)