diff options
| author | Sébastien Theys <seb@odoo.com> | 2019-02-06 09:50:38 +0000 |
|---|---|---|
| committer | Sébastien Theys <seb@odoo.com> | 2019-02-06 09:50:38 +0000 |
| commit | c1be1993f9bcd11f3fdff78e81ea71e518c15779 (patch) | |
| tree | 72045b536b781e963e4e6354b9ff84ceb20a5fbf /addons | |
| parent | 3b256ba487df8ed3ef28eddec25af75ce3be4677 (diff) | |
[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
Diffstat (limited to 'addons')
| -rw-r--r-- | addons/auth_signup/models/res_partner.py | 2 | ||||
| -rw-r--r-- | addons/mail/data/mail_data.xml | 4 | ||||
| -rw-r--r-- | addons/portal/models/portal_mixin.py | 10 | ||||
| -rw-r--r-- | addons/portal/wizard/portal_share.py | 10 | ||||
| -rw-r--r-- | addons/sale/models/sale.py | 7 | ||||
| -rw-r--r-- | addons/website/models/res_partner.py | 7 | ||||
| -rw-r--r-- | addons/website/models/website.py | 13 | ||||
| -rw-r--r-- | addons/website/static/src/js/tours/website_tour_reset_password.js | 181 | ||||
| -rw-r--r-- | addons/website/tests/__init__.py | 1 | ||||
| -rw-r--r-- | addons/website/tests/test_website_reset_password.py | 34 | ||||
| -rw-r--r-- | addons/website/views/website_templates.xml | 6 | ||||
| -rw-r--r-- | addons/website_sale/data/mail_template_data.xml | 2 | ||||
| -rw-r--r-- | addons/website_sale/models/account_invoice.py | 9 | ||||
| -rw-r--r-- | addons/website_sale/models/sale_order.py | 7 | ||||
| -rw-r--r-- | addons/website_sale/static/src/js/website_sale_tour_shop_mail.js | 94 | ||||
| -rw-r--r-- | addons/website_sale/tests/__init__.py | 1 | ||||
| -rw-r--r-- | addons/website_sale/tests/test_website_sale_mail.py | 24 | ||||
| -rw-r--r-- | addons/website_sale/views/templates.xml | 1 |
18 files changed, 404 insertions, 9 deletions
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 @@ <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> 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 @@ </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"/> 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 @@ <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 diff --git a/addons/website_sale/models/account_invoice.py b/addons/website_sale/models/account_invoice.py index 5696f4ace36..db5e9822889 100644 --- a/addons/website_sale/models/account_invoice.py +++ b/addons/website_sale/models/account_invoice.py @@ -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 diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index 0902fe5567f..85cf0a801d3 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -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" diff --git a/addons/website_sale/static/src/js/website_sale_tour_shop_mail.js b/addons/website_sale/static/src/js/website_sale_tour_shop_mail.js new file mode 100644 index 00000000000..c8343ef33e5 --- /dev/null +++ b/addons/website_sale/static/src/js/website_sale_tour_shop_mail.js @@ -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"]', + }, +]); +}); diff --git a/addons/website_sale/tests/__init__.py b/addons/website_sale/tests/__init__.py index 89fc4f46d6f..5fa8c188d49 100644 --- a/addons/website_sale/tests/__init__.py +++ b/addons/website_sale/tests/__init__.py @@ -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 diff --git a/addons/website_sale/tests/test_website_sale_mail.py b/addons/website_sale/tests/test_website_sale_mail.py new file mode 100644 index 00000000000..8cb495b5db9 --- /dev/null +++ b/addons/website_sale/tests/test_website_sale_mail.py @@ -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") diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index 1ea9dd78582..f2df9926d6a 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -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> |
