diff options
Diffstat (limited to 'addons/website')
| -rw-r--r-- | addons/website/i18n/website.pot | 51 | ||||
| -rw-r--r-- | addons/website/models/ir_http.py | 2 | ||||
| -rw-r--r-- | addons/website/models/ir_ui_view.py | 66 | ||||
| -rw-r--r-- | addons/website/models/website.py | 22 | ||||
| -rw-r--r-- | addons/website/models/website_menu.py | 7 | ||||
| -rw-r--r-- | addons/website/models/website_redirect.py | 2 | ||||
| -rw-r--r-- | addons/website/static/src/js/menu/new_content.js | 2 | ||||
| -rw-r--r-- | addons/website/static/src/scss/bootstrap_overridden.scss | 4 | ||||
| -rw-r--r-- | addons/website/static/src/scss/website.edit_mode.scss | 2 | ||||
| -rw-r--r-- | addons/website/static/src/scss/website.scss | 1 | ||||
| -rw-r--r-- | addons/website/static/src/xml/website.editor.xml | 2 | ||||
| -rw-r--r-- | addons/website/tests/test_views.py | 98 | ||||
| -rw-r--r-- | addons/website/views/website_templates.xml | 6 |
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"> |
