[FIX] *: share URL on the correct website if possible

* = base, auth_signup, mail, portal, sale, website, website_sale

Before this commit, sending an object by email would always link to web.base.url
even if the object was created from a specific website.

Now if the object has a website, we use the URL of that website if it is set.
The fallback will always be on the web.base.url.

opw-1921030
PR: #30000
This commit is contained in:
Sébastien Theys
2019-02-06 09:50:38 +00:00
parent 3b256ba487
commit c1be1993f9
19 changed files with 410 additions and 9 deletions
+1 -1
View File
@@ -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()
+3 -1
View File
@@ -231,7 +231,9 @@
<td align="center" style="min-width: 590px;">
<t t-set="access_action" t-value="record.with_context(force_website=True).get_access_action()"/>
<t t-set="is_online" t-value="access_action and access_action['type'] == 'ir.actions.act_url'"/>
<t t-set="access_url" t-value="is_online and record._get_share_url(redirect=True, signup_partner=True) or ''"/>
<t t-set="base_url" t-value="record.get_base_url()"/>
<t t-set="share_url" t-value="record._get_share_url(redirect=True, signup_partner=True)"/>
<t t-set="access_url" t-value="is_online and share_url and base_url + share_url or ''"/>
<t t-set="access_name">
View <t t-esc="model_description or 'document'"/>
</t>
+10
View File
@@ -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
+5 -5
View File
@@ -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',
+7
View File
@@ -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])
+7
View File
@@ -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
+13
View File
@@ -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):
@@ -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,
}],
});
});
},
},
]);
});
+1
View File
@@ -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
@@ -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")
@@ -14,6 +14,12 @@
</xpath>
</template>
<template id="assets_common" name="Website Assets Common" inherit_id="web.assets_common">
<xpath expr="." position="inside">
<script type="text/javascript" src="/website/static/src/js/tours/website_tour_reset_password.js"></script>
</xpath>
</template>
<template id="assets_backend" inherit_id="web.assets_backend" name="Website Backend Assets (used in backend interface)">
<xpath expr="//link[last()]" position="after">
<link rel="stylesheet" type="text/scss" href="/website/static/src/scss/website.backend.scss"/>
@@ -57,7 +57,7 @@
<hr/>
% endif
<div style="text-align: center; margin: 16px 0px 16px 0px; font-size: 14px;">
<a href="/shop/cart?access_token=${object.access_token}"
<a href="${object.get_base_url()}/shop/cart?access_token=${object.access_token}"
target="_blank"
style="background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">
Resume order
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo import fields, models, api
class AccountInvoice(models.Model):
@@ -10,3 +10,10 @@ class AccountInvoice(models.Model):
website_id = fields.Many2one('website', related='partner_id.website_id', string='Website',
help='Website through which this invoice was created.',
store=True, readonly=True)
@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(AccountInvoice, self).get_base_url()
return self.website_id and self.website_id._get_http_domain() or res
+7
View File
@@ -338,6 +338,13 @@ class SaleOrder(models.Model):
},
}
@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(SaleOrder, self).get_base_url()
return self.website_id and self.website_id._get_http_domain() or res
class SaleOrderLine(models.Model):
_inherit = "sale.order.line"
@@ -0,0 +1,94 @@
odoo.define('website_sale.tour_shop_mail', function (require) {
'use strict';
var base = require('web_editor.base');
var rpc = require('web.rpc');
var tour = require('web_tour.tour');
tour.register('shop_mail', {
test: true,
url: '/',
wait_for: base.ready(),
},
[
{
content: "Change the domain of the websites and go to shop",
trigger: 'body',
run: function () {
// 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",
}],
});
$.when(def1, def2).then(function (data) {
window.location.href = '/shop?search=Acoustic Bloc Screens';
});
},
},
{
content: "select Acoustic Bloc Screens",
trigger: '.oe_product_cart a:containsExact("Acoustic Bloc Screens")',
},
{
content: "click add to cart",
trigger: '#product_details #add_to_cart',
},
{
content: "check product is in cart, get cart id, go to backend",
trigger: 'td.td-product_name:contains("Acoustic Bloc Screens")',
run: function () {
rpc.query({
'model': 'website',
'method': 'sale_get_order',
'args': [[$('html').data('website-id')]],
}).then(function (data) {
var orderId = parseInt(data.match(/sale\.order\((\d+),\)/)[1]);
window.location.href = "/web#action=sale.action_orders&view_type=form&id=" + orderId;
});
},
},
{
content: "click confirm",
trigger: '.btn[name="action_confirm"]',
},
{
content: "click send by email",
trigger: '.btn[name="action_quotation_send"]',
extra_trigger: '.o_statusbar_status .btn-primary:contains("Sales Order")',
},
{
content: "click Send email",
trigger: '.btn[name="action_send_mail"]',
},
{
content: "wait mail to be sent, and go see it",
trigger: '.o_thread_message_content:contains("Here is"):contains("the order")',
run: function () {
window.location.href = "/web#action=mail.action_view_mail_mail&view_type=list";
},
},
{
content: "click on the first email",
trigger: '.o_data_cell:contains("YourCompany Order")',
},
{
content: "check it's the correct email, and the URL is correct too",
trigger: 'iframe p:contains("Here is"):contains("the order")',
extra_trigger: 'iframe a[href^="http://my-test-domain.com"]',
},
]);
});
+1
View File
@@ -1,5 +1,6 @@
from . import test_customize
from . import test_sale_process
from . import test_website_sale_mail
from . import test_website_sale_pricelist
from . import test_website_sale_product_attribute_value_config
from . import test_website_sale_zoom
@@ -0,0 +1,24 @@
# -*- 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 TestWebsiteSaleMail(HttpCase):
def test_01_shop_mail_tour(self):
"""The goal of this test is to make sure sending SO by email works."""
# we override unlink because we don't want the email to be auto deleted
MailMail = odoo.addons.mail.models.mail_mail.MailMail
with patch.object(MailMail, 'unlink', lambda self: None):
self.browser_js("/", "odoo.__DEBUG__.services['web_tour.tour'].run('shop_mail')", "odoo.__DEBUG__.services['web_tour.tour'].tours.shop_mail.ready", login="admin")
+1
View File
@@ -46,6 +46,7 @@
<template id="assets_common" name="tour" inherit_id="web.assets_common">
<xpath expr="." position="inside">
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_tour_shop.js"></script>
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_tour_shop_mail.js"></script>
</xpath>
</template>
+6
View File
@@ -868,6 +868,12 @@ class Partner(models.Model):
def _get_country_name(self):
return self.country_id.name or ''
@api.multi
def get_base_url(self):
"""Get the base URL for the current partner."""
self.ensure_one()
return self.env['ir.config_parameter'].sudo().get_param('web.base.url')
class ResPartnerIndustry(models.Model):
_description = 'Industry'