summaryrefslogtreecommitdiff
path: root/addons/website
diff options
context:
space:
mode:
Diffstat (limited to 'addons/website')
-rw-r--r--addons/website/i18n/website.pot51
-rw-r--r--addons/website/models/ir_http.py2
-rw-r--r--addons/website/models/ir_ui_view.py66
-rw-r--r--addons/website/models/website.py22
-rw-r--r--addons/website/models/website_menu.py7
-rw-r--r--addons/website/models/website_redirect.py2
-rw-r--r--addons/website/static/src/js/menu/new_content.js2
-rw-r--r--addons/website/static/src/scss/bootstrap_overridden.scss4
-rw-r--r--addons/website/static/src/scss/website.edit_mode.scss2
-rw-r--r--addons/website/static/src/scss/website.scss1
-rw-r--r--addons/website/static/src/xml/website.editor.xml2
-rw-r--r--addons/website/tests/test_views.py98
-rw-r--r--addons/website/views/website_templates.xml6
13 files changed, 243 insertions, 22 deletions
diff --git a/addons/website/i18n/website.pot b/addons/website/i18n/website.pot
index eabf5704b9a..c5e74f6a5e1 100644
--- a/addons/website/i18n/website.pot
+++ b/addons/website/i18n/website.pot
@@ -6,8 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server saas~12.1\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-12-24 10:57+0000\n"
-"PO-Revision-Date: 2018-12-24 10:57+0000\n"
+"POT-Creation-Date: 2019-01-16 07:58+0000\n"
+"PO-Revision-Date: 2019-01-16 07:58+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@@ -533,6 +533,26 @@ msgid "<i class=\"fa fa-trash-o\"/>Remove Slide"
msgstr ""
#. module: website
+#: model_terms:ir.ui.view,arch_db:website.list_website_pages
+msgid "<i title=\"Is the page SEO optimized?\" class=\"fa fa-search\"/>"
+msgstr ""
+
+#. module: website
+#: model_terms:ir.ui.view,arch_db:website.list_website_pages
+msgid "<i title=\"Is the page included in the main menu?\" class=\"fa fa-thumb-tack\"/>"
+msgstr ""
+
+#. module: website
+#: model_terms:ir.ui.view,arch_db:website.list_website_pages
+msgid "<i title=\"Is the page indexed by search engines?\" class=\"fa fa-globe\"/>"
+msgstr ""
+
+#. module: website
+#: model_terms:ir.ui.view,arch_db:website.list_website_pages
+msgid "<i title=\"Is the page published?\" class=\"fa fa-eye\"/>"
+msgstr ""
+
+#. module: website
#: model_terms:ir.ui.view,arch_db:website.s_comparisons
msgid "<i>Instant setup, satisfied or reimbursed.</i>"
msgstr ""
@@ -570,6 +590,16 @@ msgid "<span class=\"carousel-control-prev-icon\"/>\n"
msgstr ""
#. module: website
+#: model_terms:ir.ui.view,arch_db:website.res_config_settings_view_form
+msgid "<span class=\"fa fa-lg fa-globe\" title=\"Values set here are website-specific.\" groups=\"website.group_multi_website\"/>"
+msgstr ""
+
+#. module: website
+#: model_terms:ir.ui.view,arch_db:website.company_description
+msgid "<span class=\"fa fa-map-marker fa-fw mt16\" role=\"img\" aria-label=\"Address\" title=\"Address\"/>"
+msgstr ""
+
+#. module: website
#: model_terms:ir.ui.view,arch_db:website.user_navbar
msgid "<span class=\"fa fa-pencil\"/>Edit"
msgstr ""
@@ -580,6 +610,11 @@ msgid "<span class=\"fa fa-plus\"/>New"
msgstr ""
#. module: website
+#: model_terms:ir.ui.view,arch_db:website.list_website_pages
+msgid "<span class=\"fa fa-sort fa-lg\" role=\"img\" aria-label=\"Sort\" title=\"Sort\"/>"
+msgstr ""
+
+#. module: website
#: model_terms:ir.ui.view,arch_db:website.s_facebook_page
msgid "<span class=\"o_add_facebook_page\">\n"
" <i class=\"fa fa-plus-circle\"/> Add Facebook Page\n"
@@ -619,6 +654,11 @@ msgstr ""
#. module: website
#: model_terms:ir.ui.view,arch_db:website.user_navbar
+msgid "<span title=\"Mobile preview\" role=\"img\" aria-label=\"Mobile preview\" class=\"fa fa-mobile\"/>"
+msgstr ""
+
+#. module: website
+#: model_terms:ir.ui.view,arch_db:website.user_navbar
msgid "<span/>\n"
" <span class=\"css_publish\">Unpublished</span>\n"
" <span class=\"css_unpublish\">Published</span>"
@@ -3719,6 +3759,13 @@ msgid "The installation of an App is already in progress."
msgstr ""
#. module: website
+#. openerp-web
+#: code:addons/website/static/src/xml/website.seo.xml:93
+#, python-format
+msgid "The language of the keyword and related keywords."
+msgstr ""
+
+#. module: website
#: model_terms:ir.ui.view,arch_db:website.403
msgid "The page you were looking for could not be authorized."
msgstr ""
diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py
index a649a9acc81..be3085eb835 100644
--- a/addons/website/models/ir_http.py
+++ b/addons/website/models/ir_http.py
@@ -164,7 +164,7 @@ class Http(models.AbstractModel):
redirect = cls._serve_redirect()
if redirect:
- return request.redirect(_build_url_w_params(redirect.url_to, request.params), code=redirect.type)
+ return request.redirect(_build_url_w_params(redirect.url_to, request.params), code=redirect.redirect_type)
return False
diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py
index 1a98713326d..a94397e77ba 100644
--- a/addons/website/models/ir_ui_view.py
+++ b/addons/website/models/ir_ui_view.py
@@ -74,7 +74,7 @@ class View(models.Model):
view._create_website_specific_pages_for_view(website_specific_view,
view.env['website'].browse(current_website_id))
- for inherit_child in view.inherit_children_ids.filter_duplicate():
+ for inherit_child in view.inherit_children_ids.filter_duplicate().sorted(key=lambda v: (v.priority, v.id)):
if inherit_child.website_id.id == current_website_id:
# In the case the child was already specific to the current
# website, we cannot just reattach it to the new specific
@@ -93,6 +93,55 @@ class View(models.Model):
return True
@api.multi
+ def _get_specific_views(self):
+ """ Given a view, return a record set containing all the specific views
+ for that view's key.
+ If the given view is already specific, it will also return itself.
+ """
+ self.ensure_one()
+ domain = [('key', '=', self.key), ('website_id', '!=', False)]
+ return self.with_context(active_test=False).search(domain)
+
+ def _load_records_write(self, values):
+ """ During module update, when updating a generic view, we should also
+ update its specific views (COW'd).
+ Note that we will only update unmodified fields. That will mimic the
+ noupdate behavior on views having an ir.model.data.
+ """
+ if self.type == 'qweb' and not self.website_id:
+ # Update also specific views
+ for cow_view in self._get_specific_views():
+ authorized_vals = {}
+ for key in values:
+ if cow_view[key] == self[key]:
+ authorized_vals[key] = values[key]
+ cow_view.write(authorized_vals)
+ super(View, self)._load_records_write(values)
+
+ def _load_records_create(self, values):
+ """ During module install, when creating a generic child view, we should
+ also create that view under specific view trees (COW'd).
+ Top level view (no inherit_id) do not need that behavior as they
+ will be shared between websites since there is no specific yet.
+ """
+ records = super(View, self)._load_records_create(values)
+ for record in records:
+ if record.type == 'qweb' and record.inherit_id and not record.website_id and not record.inherit_id.website_id:
+ specific_parent_views = record.with_context(active_test=False).search([
+ ('key', '=', record.inherit_id.key),
+ ('website_id', '!=', None),
+ ])
+ for specific_parent_view in specific_parent_views:
+ record.copy({
+ # Set key to avoid copy() to generate an unique key as
+ # we want the specific view to have the same key
+ 'key': record.key,
+ 'inherit_id': specific_parent_view.id,
+ 'website_id': specific_parent_view.website_id.id,
+ })
+ return records
+
+ @api.multi
def unlink(self):
'''This implements COU (copy-on-unlink). When deleting a generic page
website-specific pages will be created so only the current
@@ -143,8 +192,12 @@ class View(models.Model):
return self.filtered(lambda view: not view.website_id)
for view in self:
+ # specific view: add it if it's for the current website and ignore
+ # it if it's for another website
if view.website_id and view.website_id.id == current_website_id:
most_specific_views |= view
+ # generic view: add it only if, for the current website, there is no
+ # specific view for this view (based on the same `key` attribute)
elif not view.website_id and not any(view.key == view2.key and view2.website_id and view2.website_id.id == current_website_id for view2 in self):
most_specific_views |= view
@@ -201,6 +254,17 @@ class View(models.Model):
@api.model
@tools.ormcache_context('self._uid', 'xml_id', keys=('website_id',))
def get_view_id(self, xml_id):
+ """If a website_id is in the context and the given xml_id is not an int
+ then try to get the id of the specific view for that website, but
+ fallback to the id of the generic view if there is no specific.
+
+ If no website_id is in the context, it might randomly return the generic
+ or the specific view, so it's probably not recommanded to use this
+ method. `viewref` is probably more suitable.
+
+ Archived views are ignored (unless the active_test context is set, but
+ then the ormcache_context will not work as expected).
+ """
if 'website_id' in self._context and not isinstance(xml_id, int):
current_website = self.env['website'].browse(self._context.get('website_id'))
domain = ['&', ('key', '=', xml_id)] + current_website.website_domain()
diff --git a/addons/website/models/website.py b/addons/website/models/website.py
index ee918cec237..4eeb73c5423 100644
--- a/addons/website/models/website.py
+++ b/addons/website/models/website.py
@@ -502,12 +502,18 @@ class Website(models.Model):
def viewref(self, view_id, raise_if_not_found=True):
''' Given an xml_id or a view_id, return the corresponding view record.
In case of website context, return the most specific one.
+
+ If no website_id is in the context, it will return the generic view,
+ instead of a random one like `get_view_id`.
+
+ Look also for archived views, no matter the context.
+
:param view_id: either a string xml_id or an integer view_id
:param raise_if_not_found: should the method raise an error if no view found
:return: The view record or empty recordset
'''
View = self.env['ir.ui.view']
- view = None
+ view = View
if isinstance(view_id, str):
if 'website_id' in self._context:
domain = [('key', '=', view_id)] + self.env['website'].website_domain(self._context.get('website_id'))
@@ -519,20 +525,20 @@ class Website(models.Model):
if views:
view = views.filter_duplicate()
else:
- view = self.env.ref(view_id)
+ # we handle the raise below
+ view = self.env.ref(view_id, raise_if_not_found=False)
# self.env.ref might return something else than an ir.ui.view (eg: a theme.ir.ui.view)
- if view._name != 'ir.ui.view':
- view = None
+ if not view or view._name != 'ir.ui.view':
+ # make sure we always return a recordset
+ view = View
elif isinstance(view_id, int):
view = View.browse(view_id)
else:
raise ValueError('Expecting a string or an integer, not a %s.' % (type(view_id)))
- if view:
- return view
- if raise_if_not_found:
+ if not view and raise_if_not_found:
raise ValueError('No record found for unique ID %s. It may have been deleted.' % (view_id))
- return None
+ return view
@api.model
def get_template(self, template):
diff --git a/addons/website/models/website_menu.py b/addons/website/models/website_menu.py
index 3a435b0a76b..202f6e19039 100644
--- a/addons/website/models/website_menu.py
+++ b/addons/website/models/website_menu.py
@@ -70,9 +70,12 @@ class Menu(models.Model):
@api.multi
def unlink(self):
default_menu = self.env.ref('website.main_menu', raise_if_not_found=False)
+ menus_to_remove = self
for menu in self.filtered(lambda m: default_menu and m.parent_id.id == default_menu.id):
- self.env['website.menu'].search([('url', '=', menu.url), ('id', '!=', menu.id)]).unlink()
- return super(Menu, self).unlink()
+ menus_to_remove |= self.env['website.menu'].search([('url', '=', menu.url),
+ ('website_id', '!=', False),
+ ('id', '!=', menu.id)])
+ return super(Menu, menus_to_remove).unlink()
@api.one
def _compute_visible(self):
diff --git a/addons/website/models/website_redirect.py b/addons/website/models/website_redirect.py
index f9daf8d4148..511a006b8a7 100644
--- a/addons/website/models/website_redirect.py
+++ b/addons/website/models/website_redirect.py
@@ -10,7 +10,7 @@ class WebsiteRedirect(models.Model):
_order = "sequence, id"
_rec_name = 'url_from'
- redirect_type = fields.Selection([('301', 'Moved permanently (301)'), ('302', 'Moved temporarily (302)')], string='Redirection Type', required=True, oldname='type')
+ redirect_type = fields.Selection([('301', 'Moved permanently (301)'), ('302', 'Moved temporarily (302)')], string='Redirection Type', required=True, default="301")
url_from = fields.Char('Redirect From', required=True)
url_to = fields.Char('Redirect To', required=True)
website_id = fields.Many2one('website', 'Website')
diff --git a/addons/website/static/src/js/menu/new_content.js b/addons/website/static/src/js/menu/new_content.js
index 18e7e5a6904..d414bc8a800 100644
--- a/addons/website/static/src/js/menu/new_content.js
+++ b/addons/website/static/src/js/menu/new_content.js
@@ -81,7 +81,7 @@ var NewContentMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
var $group = this.$dialog.find('div.form-group');
$group.removeClass('mb0');
- var $add = $('<div/>', {'class': 'form-group mb0'})
+ var $add = $('<div/>', {'class': 'form-group mb0 row'})
.append($('<span/>', {'class': 'offset-md-3 col-md-9 text-left'})
.append(qweb.render('website.components.switch', {id: 'switch_addTo_menu', label: _t("Add to menu")})));
$add.find('input').prop('checked', true);
diff --git a/addons/website/static/src/scss/bootstrap_overridden.scss b/addons/website/static/src/scss/bootstrap_overridden.scss
index fbf776ae283..1dd53f6a7bf 100644
--- a/addons/website/static/src/scss/bootstrap_overridden.scss
+++ b/addons/website/static/src/scss/bootstrap_overridden.scss
@@ -29,3 +29,7 @@ $font-family-sans-serif: nth($o-theme-fonts, $o-theme-font-number) !default;
$headings-font-family: nth($o-theme-fonts, $o-theme-headings-font-number) !default;
$headings-color: palette-color('h1') !default;
+
+// Jumbotron
+
+$jumbotron-bg: transparent !default;
diff --git a/addons/website/static/src/scss/website.edit_mode.scss b/addons/website/static/src/scss/website.edit_mode.scss
index de6f5d4c1a4..85ee8dc8fd4 100644
--- a/addons/website/static/src/scss/website.edit_mode.scss
+++ b/addons/website/static/src/scss/website.edit_mode.scss
@@ -176,8 +176,6 @@
font-family: Roboto, $font-family-sans-serif;
.o_tooltip_container {
- display: inline-flex;
- align-items: center;
transition: opacity 1s linear;
> .o_tooltip {
diff --git a/addons/website/static/src/scss/website.scss b/addons/website/static/src/scss/website.scss
index 3c506db48ac..71619d3aaa3 100644
--- a/addons/website/static/src/scss/website.scss
+++ b/addons/website/static/src/scss/website.scss
@@ -104,6 +104,7 @@ font[class*='bg-'] {
// Icons
.fa {
+ font-family: "FontAwesome" !important;
$size: 3rem;
&.rounded-circle,
diff --git a/addons/website/static/src/xml/website.editor.xml b/addons/website/static/src/xml/website.editor.xml
index f636ad3d2dd..5b060389618 100644
--- a/addons/website/static/src/xml/website.editor.xml
+++ b/addons/website/static/src/xml/website.editor.xml
@@ -3,7 +3,7 @@
<div t-name="website.homepage_editor_welcome_message" class="container text-center o_homepage_editor_welcome_message">
<h2 class="mt0">Welcome to your <b>Homepage</b>!</h2>
<p class="lead d-none d-md-block">Let's start designing.</p>
- <div class="o_tooltip_container d-none d-md-block fade">Follow all the <div class="o_tooltip bottom"/> signs to get your website ready in no time.</div>
+ <div class="o_tooltip_container d-none d-md-inline-flex fade">Follow all the <div class="o_tooltip bottom"/> signs to get your website ready in no time.</div>
</div>
<div t-name="website.leaving_current_page_edition">
<p>What do you want to do?</p>
diff --git a/addons/website/tests/test_views.py b/addons/website/tests/test_views.py
index 132472de2c0..9ece6cf4c3d 100644
--- a/addons/website/tests/test_views.py
+++ b/addons/website/tests/test_views.py
@@ -622,6 +622,104 @@ class TestCowViewSaving(common.TransactionCase):
views = View.with_context(website_id=1).get_related_views('B')
self.assertEqual(views.mapped('key'), ['B', 'I', 'II'], "Should only return the specific tree")
+ def test_cow_inherit_children_order(self):
+ """ COW method should loop on inherit_children_ids in correct order
+ when copying them on the new specific tree.
+ Correct order is the same as the one when applying view arch:
+ PRIORITY, ID
+ And not the default one from ir.ui.view (NAME, PRIORIRTY, ID).
+ """
+ self.inherit_view.copy({
+ 'name': 'alphabetically before "Extension"',
+ 'key': '_test.alphabetically_first',
+ 'arch': '<div position="replace"><p>COMPARE</p></div>',
+ })
+ # Next line should not crash, COW loop on inherit_children_ids should be sorted correctly
+ self.base_view.with_context(website_id=1).write({'name': 'Product (W1)'})
+
+ def test_module_new_inherit_view_on_parent_already_forked(self):
+ """ If a generic parent view is copied (COW) and that another module
+ creates a child view for that generic parent, all the COW views
+ should also get a copy of that new child view.
+
+ Typically, a parent view (website_sale.product) is copied (COW)
+ and then wishlist module is installed.
+ Wishlist views inhering from website_sale.product are added to the
+ generic `website_sale.product`. But it should also be added to the
+ COW `website_sale.product` to activate the module views for that
+ website.
+ """
+ Website = self.env['website']
+ View = self.env['ir.ui.view']
+
+ # Simulate website_sale product view
+ self.base_view.write({'name': 'Product', 'key': '_website_sale.product'})
+ # Trigger cow on website_sale hierarchy for website 1
+ self.base_view.with_context(website_id=1).write({'name': 'Product (W1)'})
+
+ # Simulate website_sale_comparison install
+ View._load_records([dict(xml_id='_website_sale_comparison.product_add_to_compare', values={
+ 'name': 'Add to comparison in product page',
+ 'mode': 'extension',
+ 'inherit_id': self.base_view.id,
+ 'arch': '<div position="replace"><p>COMPARE</p></div>',
+ 'key': '_website_sale_comparison.product_add_to_compare',
+ })])
+
+ specific_view = Website.with_context(load_all_views=True, website_id=1).viewref('_website_sale.product')
+ specific_view_arch = specific_view.read_combined(['arch'])['arch']
+ self.assertEqual(specific_view.website_id.id, 1, "Ensure we got specific view to perform the checks against")
+ self.assertEqual(specific_view_arch, '<p>COMPARE</p>', "When a module creates an inherited view (on a generic tree), it should also create that view in the specific COW'd tree.")
+
+ # Simulate website_sale_comparison update
+ View._load_records([dict(xml_id='_website_sale_comparison.product_add_to_compare', values={
+ 'arch': '<div position="replace"><p>COMPARE EDITED</p></div>',
+ })])
+
+ specific_view_arch = Website.with_context(load_all_views=True, website_id=1).viewref('_website_sale.product').read_combined(['arch'])['arch']
+ self.assertEqual(specific_view_arch, '<p>COMPARE EDITED</p>', "When a module updates an inherited view (on a generic tree), it should also update the copies of that view (COW).")
+
+ # Test fields that should not be COW'd
+ random_view_id = View.search([], limit=1).id
+ View._load_records([dict(xml_id='_website_sale_comparison.product_add_to_compare', values={
+ 'website_id': None,
+ 'inherit_id': random_view_id,
+ })])
+
+ w1_specific_child_view = Website.with_context(load_all_views=True, website_id=1).viewref('_website_sale_comparison.product_add_to_compare')
+ generic_child_view = Website.with_context(load_all_views=True).viewref('_website_sale_comparison.product_add_to_compare')
+ self.assertEqual(w1_specific_child_view.website_id.id, 1, "website_id is a prohibited field when COWing views during _load_records")
+ self.assertEqual(w1_specific_child_view.inherit_id.id != random_view_id, True, "inherit_id is a prohibited field when COWing views during _load_records")
+ self.assertEqual(generic_child_view.inherit_id.id, random_view_id, "prohibited fields only concerned write on COW'd view. Generic should still considere these fields")
+
+ # Set back the generic view as parent for the rest of the test
+ generic_child_view.inherit_id = self.base_view
+
+ # Don't update fields from COW'd view if these fields have been modified from original view
+ new_website = Website.create({'name': 'New Website'})
+ self.base_view.with_context(website_id=new_website.id).write({'name': 'Product (new_website)'})
+ new_website_specific_child_view = Website.with_context(load_all_views=True, website_id=new_website.id).viewref('_website_sale_comparison.product_add_to_compare')
+ new_website_specific_child_view.priority = 6
+ View._load_records([dict(xml_id='_website_sale_comparison.product_add_to_compare', values={
+ 'priority': 3,
+ })])
+ self.assertEqual(generic_child_view.priority, 3, "XML update should be written on the Generic View")
+ self.assertEqual(w1_specific_child_view.priority, 3, "XML update should be written on the specific view if the fields have not been modified on that specific view")
+ self.assertEqual(new_website_specific_child_view.priority, 6, "XML update should NOT be written on the specific view if the fields have been modified on that specific view")
+
+ # Simulate website_sale update on top level view
+ self.env['ir.model.data'].create({
+ 'module': '_website_sale',
+ 'name': 'product',
+ 'model': self.base_view._name,
+ 'res_id': self.base_view.id,
+ })
+ View._load_records([dict(xml_id='_website_sale.product', values={
+ 'website_meta_title': 'A bug got fixed by updating this field',
+ })])
+ all_title_updated = specific_view.website_meta_title == self.base_view.website_meta_title == "A bug got fixed by updating this field"
+ self.assertEqual(all_title_updated, True, "Update on top level generic views should also be applied on specific views")
+
class Crawler(HttpCase):
def setUp(self):
diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml
index 605ed8ec953..e7564b75400 100644
--- a/addons/website/views/website_templates.xml
+++ b/addons/website/views/website_templates.xml
@@ -847,7 +847,7 @@
<div class="container accordion mb32 mt32" id="debug_infos">
<div class="card" t-if="exception">
<h4 class="card-header">
- <a data-toggle="collapse" data-parent="#debug_infos" href="#error_main">Error</a>
+ <a data-toggle="collapse" href="#error_main">Error</a>
</h4>
<div id="error_main" class="collapse in">
<div class="card-body">
@@ -861,7 +861,7 @@
</div>
<div class="card" t-if="qweb_exception">
<h4 class="card-header">
- <a data-toggle="collapse" data-parent="#adebug_infos" href="#error_qweb">QWeb</a>
+ <a data-toggle="collapse" href="#error_qweb">QWeb</a>
</h4>
<div id="error_qweb" class="collapse">
<div class="card-body">
@@ -881,7 +881,7 @@
</div>
<div class="card" t-if="traceback">
<h4 class="card-header">
- <a data-toggle="collapse" data-parent="#adebug_infos" href="#error_traceback">Traceback</a>
+ <a data-toggle="collapse" href="#error_traceback">Traceback</a>
</h4>
<div id="error_traceback" class="collapse">
<div class="card-body">