diff --git a/addons/auth_signup/models/res_partner.py b/addons/auth_signup/models/res_partner.py
index 87a47ffa455..5703e018548 100644
--- a/addons/auth_signup/models/res_partner.py
+++ b/addons/auth_signup/models/res_partner.py
@@ -53,8 +53,8 @@ class ResPartner(models.Model):
the url state components (menu_id, id, view_type) """
res = dict.fromkeys(self.ids, False)
- base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
for partner in self:
+ base_url = partner.get_base_url()
# when required, make sure the partner has a valid signup token
if self.env.context.get('signup_valid') and not partner.user_ids:
partner.sudo().signup_prepare()
diff --git a/addons/mail/data/mail_data.xml b/addons/mail/data/mail_data.xml
index b65ddd02efa..e313f7538f4 100644
--- a/addons/mail/data/mail_data.xml
+++ b/addons/mail/data/mail_data.xml
@@ -231,7 +231,9 @@
-
+
+
+
View
diff --git a/addons/portal/models/portal_mixin.py b/addons/portal/models/portal_mixin.py
index 99d8f18a266..09dc6ec9c01 100644
--- a/addons/portal/models/portal_mixin.py
+++ b/addons/portal/models/portal_mixin.py
@@ -35,6 +35,16 @@ class PortalMixin(models.AbstractModel):
self.sudo().write({'access_token': str(uuid.uuid4())})
return self.access_token
+ @api.multi
+ def get_base_url(self):
+ """Get the base URL for the current model.
+
+ Defined here to be overriden by website specific models.
+ The method has to be public because it is called from mail templates.
+ """
+ self.ensure_one()
+ return self.env['ir.config_parameter'].sudo().get_param('web.base.url')
+
def _get_share_url(self, redirect=False, signup_partner=False, pid=None):
"""
Build the url of the record that will be sent by mail and adds additional parameters such as
diff --git a/addons/portal/wizard/portal_share.py b/addons/portal/wizard/portal_share.py
index 5905df4eed4..8474f2c810c 100644
--- a/addons/portal/wizard/portal_share.py
+++ b/addons/portal/wizard/portal_share.py
@@ -10,9 +10,10 @@ class PortalShare(models.TransientModel):
@api.model
def default_get(self, fields):
result = super(PortalShare, self).default_get(fields)
+ record = self.env[result['res_model']].browse(result['res_id'])
result['res_model'] = self._context.get('active_model')
result['res_id'] = self._context.get('active_id')
- result['share_link'] = self.env[result['res_model']].browse(result['res_id'])._get_share_url(redirect=True)
+ result['share_link'] = record.get_base_url() + record._get_share_url(redirect=True)
return result
res_model = fields.Char('Related Document Model', required=True)
@@ -24,12 +25,11 @@ class PortalShare(models.TransientModel):
@api.depends('res_model', 'res_id')
def _compute_share_link(self):
- base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
for rec in self:
res_model = self.env[rec.res_model]
if isinstance(res_model, self.pool['portal.mixin']):
record = res_model.browse(rec.res_id)
- rec.share_link = base_url + record._get_share_url(redirect=True)
+ rec.share_link = record.get_base_url() + record._get_share_url(redirect=True)
@api.depends('res_model', 'res_id')
def _compute_access_warning(self):
@@ -52,7 +52,7 @@ class PortalShare(models.TransientModel):
partner_ids = self.partner_ids.filtered(lambda x: x.user_ids)
# if partner already user or record has access token send common link in batch to all user
for partner in self.partner_ids:
- share_link = active_record._get_share_url(redirect=True, pid=partner.id)
+ share_link = active_record.get_base_url() + active_record._get_share_url(redirect=True, pid=partner.id)
active_record.with_context(mail_post_autofollow=True).message_post_with_view(template,
values={'partner': partner, 'note': self.note, 'record': active_record,
'share_link': share_link},
@@ -67,7 +67,7 @@ class PortalShare(models.TransientModel):
share_link = partner._get_signup_url_for_action(action='/mail/view', res_id=self.res_id, model=self.model)[partner.id]
active_record.with_context(mail_post_autofollow=True).message_post_with_view(template,
values={'partner': partner, 'note': self.note, 'record': active_record,
- 'share_link': share_link },
+ 'share_link': share_link},
subject=_("You are invited to access %s" % active_record.display_name),
subtype_id=note.id,
notif_layout='mail.mail_notification_light',
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index 2e88fcc3233..ad8f061e192 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -884,7 +884,14 @@ class SaleOrder(models.Model):
self.ensure_one()
return '%s %s' % (self.type_name, self.name)
+ @api.multi
def _get_share_url(self, redirect=False, signup_partner=False, pid=None):
+ """Override for sales order.
+
+ If the SO is in a state where an action is required from the partner,
+ return the URL with a login token. Otherwise, return the URL with a
+ generic access token (no login).
+ """
self.ensure_one()
if self.state not in ['sale', 'done']:
auth_param = url_encode(self.partner_id.signup_get_auth_param()[self.partner_id.id])
diff --git a/addons/website/models/res_partner.py b/addons/website/models/res_partner.py
index 247804c387b..dcb0a463760 100644
--- a/addons/website/models/res_partner.py
+++ b/addons/website/models/res_partner.py
@@ -53,3 +53,10 @@ class Partner(models.Model):
# onchange uses the cache to retrieve value, we need to copy computed_value into the initial env
for record, record2 in izip(self, self2):
record.display_name = record2.display_name
+
+ @api.multi
+ def get_base_url(self):
+ """When using multi-website, we want the user to be redirected to the
+ most appropriate website if possible."""
+ res = super(Partner, self).get_base_url()
+ return self.website_id and self.website_id._get_http_domain() or res
diff --git a/addons/website/models/website.py b/addons/website/models/website.py
index 8dc06a4f972..759f52390bd 100644
--- a/addons/website/models/website.py
+++ b/addons/website/models/website.py
@@ -731,6 +731,19 @@ class Website(models.Model):
'target': 'self',
}
+ @api.multi
+ def _get_http_domain(self):
+ """Get the domain of the current website, prefixed by http if no
+ scheme is specified.
+
+ Empty string if no domain is specified on the website.
+ """
+ self.ensure_one()
+ if not self.domain:
+ return ''
+ res = urls.url_parse(self.domain)
+ return 'http://' + self.domain if not res.scheme else self.domain
+
class SeoMetadata(models.AbstractModel):
diff --git a/addons/website/static/src/js/tours/website_tour_reset_password.js b/addons/website/static/src/js/tours/website_tour_reset_password.js
new file mode 100644
index 00000000000..2b304bb0103
--- /dev/null
+++ b/addons/website/static/src/js/tours/website_tour_reset_password.js
@@ -0,0 +1,181 @@
+odoo.define('website.tour_reset_password', function (require) {
+'use strict';
+
+var base = require('web_editor.base');
+var localStorage = require('web.local_storage');
+var rpc = require('web.rpc');
+var tour = require('web_tour.tour');
+
+var currentDomain = window.location.protocol + '//' + window.location.hostname;
+var emailsUrl = '/web#action=mail.action_view_mail_mail&view_type=list';
+var usersUrl = '/web#action=base.action_res_users&view_type=list';
+var resetLinkKey = 'website.tour_reset_password.resetLink';
+
+tour.register('website_reset_password', {
+ test: true,
+ url: '/web',
+ wait_for: base.ready(),
+},
+[
+ {
+ content: "Change the domain of the websites and go to users page",
+ trigger: '.oe_topbar_name:contains("Admin")',
+ run: function () {
+ // We could do it with the UI but this is not the goal of this test,
+ // so we just make an RPC to be faster.
+
+ // We change the domain of the website to test that the email that
+ // will be sent uses the correct domain for its links.
+ var def1 = rpc.query({
+ 'model': 'website',
+ 'method': 'write',
+ 'args': [[1], {
+ 'domain': "my-test-domain.com",
+ }],
+ });
+ // We need to change the domain of all the websites otherwise the
+ // website selector will return the website 2 since the domain we
+ // set on website 1 doesn't actually match our test server.
+ var def2 = rpc.query({
+ 'model': 'website',
+ 'method': 'write',
+ 'args': [[2], {
+ 'domain': "https://domain-not-used.fr",
+ }],
+ });
+ // Set a website on "Admin" partner to test the URL of the website.
+ var def3 = rpc.query({
+ model: 'res.partner',
+ method: 'name_search',
+ kwargs: {'name': 'Mitchell Admin'},
+ }).then(function (res) {
+ return rpc.query({
+ 'model': 'res.partner',
+ 'method': 'write',
+ 'args': [[res[0][0]], {
+ 'website_id': 1,
+ }],
+ });
+ });
+
+ return $.when(def1, def2, def3).then(function () {
+ window.location.href = usersUrl;
+ });
+ },
+ },
+ {
+ content: "click on Admin",
+ trigger: '.o_data_cell:contains("Admin")',
+ },
+ {
+ content: "click on reset password",
+ trigger: '.btn[name="action_reset_password"]',
+ },
+ {
+ content: "wait mail to be sent, and go see it",
+ trigger: 'a[name="signup_url"][href^="http://my-test-domain.com"]',
+ run: function () {
+ window.location.href = emailsUrl;
+ },
+ },
+ {
+ content: "click on the first email",
+ trigger: '.o_data_cell:contains("Password reset"):eq(0)',
+ },
+ {
+ content: "check page has an iframe",
+ trigger: 'iframe body',
+ run: function () {
+ var content = $('iframe').contents()[0];
+ console.log(content && new XMLSerializer().serializeToString(content));
+ },
+ },
+ {
+ content: "check iframe has the button",
+ trigger: 'iframe a:contains("Change password")',
+ run: function () {},
+ },
+ {
+ content: "check the URL is correct too",
+ trigger: 'iframe a:contains("Change password")[href^="http://my-test-domain.com"]',
+ run: function () {
+ // reset the domain of the websites, go to users page
+ return rpc.query({
+ 'model': 'website',
+ 'method': 'write',
+ 'args': [[1, 2], {
+ 'domain': "",
+ }],
+ }).then(function () {
+ window.location.href = usersUrl;
+ });
+ },
+ },
+ {
+ content: "click on Admin",
+ trigger: '.o_data_cell:contains("Admin")',
+ },
+ {
+ content: "click on reset password",
+ trigger: '.btn[name="action_reset_password"]',
+ },
+ {
+ content: "wait mail to be sent, and go see it",
+ trigger: 'a[name="signup_url"][href^="' + currentDomain + '"]',
+ run: function () {
+ window.location.href = emailsUrl;
+ },
+ },
+ {
+ content: "click on the first email",
+ trigger: '.o_data_cell:contains("Password reset"):eq(0)',
+ },
+ {
+ content: "check the link has the current host, save the link, logout",
+ trigger: 'iframe a:contains("Change password")[href^="' + currentDomain + '"]',
+ run: function () {
+ var link = $('iframe').contents().find('a:contains("Change password")').attr('href');
+ localStorage.setItem(resetLinkKey, link);
+ window.location.href = "/web/session/logout?redirect=/";
+ },
+ },
+ {
+ content: "go to the reset link",
+ trigger: 'a[href="/web/login"]',
+ run: function () {
+ window.location.href = localStorage.getItem(resetLinkKey);
+ },
+ },
+ {
+ content: "fill new password and submit",
+ trigger: '.oe_reset_password_form',
+ run: function () {
+ var $form = $('.oe_reset_password_form');
+ $form.find('input[name="password"]').val('adminadmin');
+ // password must be at least 8 if the module `auth_password_policy`
+ // is installed which is the case on runbot
+ $form.find('input[name="confirm_password"]').val('adminadmin');
+ $form.find('button[type="submit"]').click();
+ },
+ },
+ {
+ content: "check logged in, and reset admin website",
+ trigger: '.oe_topbar_name:contains("Admin")',
+ run: function () {
+ return rpc.query({
+ model: 'res.partner',
+ method: 'name_search',
+ kwargs: {'name': 'Admin'},
+ }).then(function (res) {
+ return rpc.query({
+ 'model': 'res.partner',
+ 'method': 'write',
+ 'args': [[res[0][0]], {
+ 'website_id': false,
+ }],
+ });
+ });
+ },
+ },
+]);
+});
diff --git a/addons/website/tests/__init__.py b/addons/website/tests/__init__.py
index 1c57eeab793..d2dd7916ecc 100644
--- a/addons/website/tests/__init__.py
+++ b/addons/website/tests/__init__.py
@@ -8,3 +8,4 @@ from . import test_ui
from . import test_views
from . import test_menu
from . import test_page
+from . import test_website_reset_password
diff --git a/addons/website/tests/test_website_reset_password.py b/addons/website/tests/test_website_reset_password.py
new file mode 100644
index 00000000000..b9fe135d35f
--- /dev/null
+++ b/addons/website/tests/test_website_reset_password.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+try:
+ from unittest.mock import patch
+except ImportError:
+ from mock import patch
+
+import odoo
+from odoo.tests import tagged
+from odoo.tests.common import HttpCase
+
+
+@tagged('post_install', '-at_install')
+class TestWebsiteResetPassword(HttpCase):
+
+ def test_01_website_reset_password_tour(self):
+ """The goal of this test is to make sure the reset password works."""
+
+ # We override unlink because we don't want the email to be auto deleted
+ # if the send works.
+ MailMail = odoo.addons.mail.models.mail_mail.MailMail
+
+ # We override send_mail because in HttpCase on runbot we don't have an
+ # SMTP server, so if force_send is set, the test is going to fail.
+ MailTemplate = odoo.addons.mail.models.mail_template.MailTemplate
+ original_send_mail = MailTemplate.send_mail
+
+ def my_send_mail(*args, **kwargs):
+ kwargs.update(force_send=False)
+ return original_send_mail(*args, **kwargs)
+
+ with patch.object(MailMail, 'unlink', lambda self: None), patch.object(MailTemplate, 'send_mail', my_send_mail):
+ self.browser_js("/", "odoo.__DEBUG__.services['web_tour.tour'].run('website_reset_password')", "odoo.__DEBUG__.services['web_tour.tour'].tours.website_reset_password.ready", login="admin")
diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml
index f2c9e93b0fc..9abc878412e 100644
--- a/addons/website/views/website_templates.xml
+++ b/addons/website/views/website_templates.xml
@@ -14,6 +14,12 @@
+
+
+
+
+
+
diff --git a/addons/website_sale/data/mail_template_data.xml b/addons/website_sale/data/mail_template_data.xml
index 5cecc496363..59d668f8758 100644
--- a/addons/website_sale/data/mail_template_data.xml
+++ b/addons/website_sale/data/mail_template_data.xml
@@ -57,7 +57,7 @@
% endif