diff --git a/addons/http_routing/models/ir_http.py b/addons/http_routing/models/ir_http.py index c16eb70fdd5..0c477483ad1 100644 --- a/addons/http_routing/models/ir_http.py +++ b/addons/http_routing/models/ir_http.py @@ -29,7 +29,19 @@ odoo._geoip_resolver = None # Slug API # ------------------------------------------------------------ -def slugify(s, max_length=None): +def _guess_mimetype(ext=False, default='text/html'): + exts = { + '.css': 'text/css', + '.less': 'text/less', + '.js': 'text/javascript', + '.xml': 'text/xml', + '.csv': 'text/csv', + '.html': 'text/html', + } + return ext is not False and exts.get(ext, default) or exts + + +def slugify_one(s, max_length=None): """ Transform a string to a slug that can be used in a url path. This method will first try to do the job with python-slugify if present. Otherwise it will process string by stripping leading and ending spaces, @@ -53,6 +65,21 @@ def slugify(s, max_length=None): return slug_str[:max_length] +def slugify(s, max_length=None, path=False): + if not path: + return slugify_one(s, max_length=max_length) + else: + res = [] + for u in s.split('/'): + if slugify_one(u, max_length=max_length) != '': + res.append(slugify_one(u, max_length=max_length)) + # check if supported extension + path_no_ext, ext = os.path.splitext(s) + if ext and ext in _guess_mimetype(): + res[-1] = slugify_one(path_no_ext) + ext + return '/'.join(res) + + def slug(value): if isinstance(value, models.BaseModel): if isinstance(value.id, models.NewId): diff --git a/addons/portal/static/src/less/website.ui.less b/addons/portal/static/src/less/website.ui.less index 74ba4b59a63..c4b57e018bf 100644 --- a/addons/portal/static/src/less/website.ui.less +++ b/addons/portal/static/src/less/website.ui.less @@ -23,11 +23,27 @@ body.o_connected_user { border-radius: 0; } + .o_menu_sections { + a { + > .fa-plus, > .fa-pencil { + margin-right: @grid-gutter-width/4; + } + } + + .o_mobile_preview a { + text-align: center; + font-size: 20px; + } + } .o_menu_systray { a { padding: 0 @grid-gutter-width/2; - &[data-action="edit"], &[data-action="translate"] { + &.css_edit_dynamic{ + padding: 0 @grid-gutter-width/4; + } + + &[data-action="edit"], &[data-action="translate"], &.css_edit_dynamic { .button-variant(white, @odoo-brand-optional, @odoo-brand-optional); } diff --git a/addons/web_editor/static/src/js/widgets/widgets.js b/addons/web_editor/static/src/js/widgets/widgets.js index 8f3d44ec75e..d25f1c631e2 100644 --- a/addons/web_editor/static/src/js/widgets/widgets.js +++ b/addons/web_editor/static/src/js/widgets/widgets.js @@ -1184,7 +1184,7 @@ var LinkDialog = Dialog.extend({ } } - if (!test && (!val || !$e[0].checkValidity())) { + if (!test && $e.prop('required') && (!val || !$e[0].checkValidity())) { // FIXME: error message $e.closest('.form-group').addClass('has-error'); $e.focus(); diff --git a/addons/web_editor/static/src/less/web_editor.common.less b/addons/web_editor/static/src/less/web_editor.common.less index c48fdf7df46..57c06c6f513 100644 --- a/addons/web_editor/static/src/less/web_editor.common.less +++ b/addons/web_editor/static/src/less/web_editor.common.less @@ -145,12 +145,6 @@ span[data-oe-type="monetary"] { // Menus // TODO should not be here but used by web_studio so must stay here for now ul.oe_menu_editor { - .fa-home { - display: none; - } - > li:first-child > div > span > .fa-home { - display: block; - } .oe_menu_placeholder { outline: 1px dashed #4183C4; } diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index 29cedb60a3a..e7929af071e 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -2,25 +2,23 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import base64 import datetime -from itertools import islice import json -from xml.etree import ElementTree as ET import logging -import re - import requests import werkzeug.utils import werkzeug.wrappers -import odoo -from odoo import http, models -from odoo import fields -from odoo.http import request +from itertools import islice +from xml.etree import ElementTree as ET +import odoo + +from odoo import http, models, fields, _ +from odoo.http import request +from odoo.tools import pycompat, OrderedSet from odoo.addons.http_routing.models.ir_http import slug from odoo.addons.web.controllers.main import WebClient, Binary, Home - -from odoo.tools import pycompat, OrderedSet +from odoo.addons.portal.controllers.portal import pager as portal_pager logger = logging.getLogger(__name__) @@ -66,16 +64,20 @@ class Website(Home): @http.route('/', type='http', auth="public", website=True) def index(self, **kw): - page = 'homepage' - main_menu = request.env.ref('website.main_menu', raise_if_not_found=False) - if main_menu: - first_menu = main_menu.child_id and main_menu.child_id[0] - if first_menu: - if first_menu.url and (not (first_menu.url.startswith(('/page/', '/?', '/#')) or (first_menu.url == '/'))): - return request.redirect(first_menu.url) - if first_menu.url and first_menu.url.startswith('/page/'): - return request.env['ir.http'].reroute(first_menu.url) - return self.page(page) + homepage = request.website.homepage_id + if homepage and homepage.url != '/': + return request.env['ir.http'].reroute(homepage.url) + + website_page = request.env['ir.http']._serve_page() + if website_page: + return website_page + else: + top_menu = request.website.sudo().menu_id + first_menu = top_menu and top_menu.child_id and top_menu.child_id[0] + if first_menu and first_menu.url != '/' and (not (first_menu.url.startswith(('/?', '/#')))): + return request.redirect(first_menu.url) + + raise request.not_found() #------------------------------------------------------ # Login - overwrite of the web login so that regular users are redirected to the backend @@ -106,30 +108,6 @@ class Website(Home): redirect.set_cookie('frontend_lang', lang) return redirect - @http.route('/page/', type='http', auth="public", website=True, cache=300) - def page(self, page, **opt): - values = { - 'path': page, - 'deletable': True, # used to add 'delete this page' in content menu - } - # /page/website.XXX --> /page/XXX - if page.startswith('website.'): - return request.redirect(b'/page/%s?%s' % (page[8:].encode('utf-8'), request.httprequest.query_string), code=301) - elif '.' not in page: - page = 'website.%s' % page - - try: - request.website.get_template(page) - except ValueError as e: - # page not found - if request.website.is_publisher(): - values.pop('deletable') - page = 'website.page_404' - else: - return request.env['ir.http']._handle_exception(e, 404) - - return request.render(page, values) - @http.route(['/website/country_infos/'], type='json', auth="public", methods=['POST'], website=True) def country_infos(self, country, **kw): fields = country.get_address_fields() @@ -140,7 +118,7 @@ class Website(Home): return request.render('website.robots', {'url_root': request.httprequest.url_root}, mimetype='text/plain') @http.route('/sitemap.xml', type='http', auth="public", website=True) - def sitemap_xml_index(self): + def sitemap_xml_index(self, **kwargs): current_website = request.website Attachment = request.env['ir.attachment'].sudo() View = request.env['ir.ui.view'].sudo() @@ -166,8 +144,8 @@ class Website(Home): if not content: # Remove all sitemaps in ir.attachments as we're going to regenerated them - dom = [('type', '=', 'binary'), '|', ('url', '=like' , '/sitemap-%d-%%.xml' % current_website.id), - ('url', '=' , '/sitemap-%d.xml' % current_website.id)] + dom = [('type', '=', 'binary'), '|', ('url', '=like', '/sitemap-%d-%%.xml' % current_website.id), + ('url', '=', '/sitemap-%d.xml' % current_website.id)] sitemaps = Attachment.search(dom) sitemaps.unlink() @@ -227,22 +205,44 @@ class Website(Home): # Edit #------------------------------------------------------ - @http.route('/website/add/', type='http', auth="user", website=True) - def pagenew(self, path, noredirect=False, add_menu=None, template=False): - if template: - xml_id = request.env['website'].new_page(path, template=template) - else: - xml_id = request.env['website'].new_page(path) - if add_menu: - request.env['website.menu'].create({ - 'name': path, - 'url': "/page/" + xml_id[8:], - 'parent_id': request.website.menu_id.id, - 'website_id': request.website.id, - }) - # Reverse action in order to allow shortcut for /page/ - url = "/page/" + re.sub(r"^website\.", '', xml_id) + @http.route(['/website/pages', '/website/pages/page/'], type='http', auth="user", website=True) + def pages_management(self, page=1, sortby='name', search='', **kw): + Page = request.env['website.page'] + searchbar_sortings = { + 'url': {'label': _('Sort by Url'), 'order': 'url'}, + 'name': {'label': _('Sort by Name'), 'order': 'name'}, + } + # default sortby order + sort_order = searchbar_sortings.get(sortby, 'name')['order'] + domain = ['|', ('website_ids', 'in', request.website.id), ('website_ids', '=', False)] + if search: + domain += ['|', ('name', 'ilike', search), ('url', 'ilike', search)] + + pages_count = Page.search_count(domain) + + pager = portal_pager( + url="/website/pages", + url_args={'sortby': sortby}, + total=pages_count, + page=page, + step=50 + ) + pages = Page.search(domain, order=sort_order, limit=50, offset=pager['offset']) + + values = { + 'pager': pager, + 'pages': pages, + 'search': search, + 'sortby': sortby, + 'searchbar_sortings': searchbar_sortings, + } + return request.render("website.edit_website_pages", values) + + @http.route(['/website/add/', '/website/add/'], type='http', auth="user", website=True) + def pagenew(self, path="", noredirect=False, add_menu=False, template=False): + template = template and dict(template=template) or {} + url = request.env['website'].new_page(path, add_menu=add_menu, **template) if noredirect: return werkzeug.wrappers.Response(url, mimetype='text/plain') return werkzeug.utils.redirect(url + "?enable_editor=1") @@ -380,7 +380,7 @@ class Website(Home): @http.route([ '/website/action/', '/website/action//', - ], type='http', auth="public", website=True) + ], type='http', auth="public", website=True) def actions_server(self, path_or_xml_id_or_id, **post): ServerActions = request.env['ir.actions.server'] action = action_id = None diff --git a/addons/website/data/website_data.xml b/addons/website/data/website_data.xml index f9f0c5a9935..61020e3d224 100644 --- a/addons/website/data/website_data.xml +++ b/addons/website/data/website_data.xml @@ -16,7 +16,7 @@ Home - /page/homepage + / 10 @@ -24,12 +24,151 @@ Contact us - /page/contactus + /contactus 60 + + + Home + qweb + website.homepage + + + + +
+ + + + + + Home + True + / + + + + + + + + Contact Us + qweb + website.contactus + + + +
+
+
+

Contact us

+
+
+
+
+

Contact us about anything related to our company or services.

+

We'll do our best to get back to you as soon as possible.

+
+
+ +
+
+ +
+
+
+
+
+ + + + + + Contact Us + /contactus + True + + + + + About us + qweb + website.aboutus + + + +
+
+ +
+
+
+
+

About us

+

Great products for great people

+
+
+
+
+ +
+
+
+
+

+ We are a team of passionate people whose goal is to improve everyone's + life through disruptive products. We build great products to solve your + business problems. +

+

+ Our products are designed for small to medium size companies willing to optimize + their performance. +

+
+
+ Our Team +
+
+
+
+ +
+
+
+
+
+
+
+ + + About us + True + /aboutus + + + + + + + + + + + Website Apps ir.module.module @@ -61,7 +200,7 @@ Website Homepage - /page/homepage + / self diff --git a/addons/website/data/website_demo.xml b/addons/website/data/website_demo.xml index a58ff42a725..c1ecd0f054e 100644 --- a/addons/website/data/website_demo.xml +++ b/addons/website/data/website_demo.xml @@ -132,12 +132,11 @@ response = request.render("website.template_partner_comment", { - Homepage + Home qweb - website.homepage - + website2.homepage - +
@@ -172,27 +171,79 @@ response = request.render("website.template_partner_comment", { + + Home + True + / + + + + + Contact Us + qweb + website2.contactus + + + +
+
+
+

Contact us

+
+
+
+
+

Contact us about anything related to our company or services.

+

We'll do our best to get back to you as soon as possible.

+
+
+ +
+
+ +
+
+
+
+
+ + + + + + Home + True + /contactus + + + + + + + + Top Menu - Home - /page/homepage + / 10 + - Contact us - /page/website.contactus + /contactus 60 + - diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py index 0f6369758cc..6853ff29422 100644 --- a/addons/website/models/ir_http.py +++ b/addons/website/models/ir_http.py @@ -2,8 +2,9 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import logging -import os import traceback +import datetime +import os import werkzeug import werkzeug.routing @@ -17,8 +18,7 @@ from odoo.tools import config from odoo.exceptions import QWebException from odoo.tools.safe_eval import safe_eval -from odoo.addons.http_routing.models.ir_http import ModelConverter - +from odoo.addons.http_routing.models.ir_http import ModelConverter, _guess_mimetype logger = logging.getLogger(__name__) @@ -34,7 +34,6 @@ class Http(models.AbstractModel): return dict( super(Http, cls)._get_converters(), model=ModelConverter, - page=PageConverter, ) @classmethod @@ -89,6 +88,53 @@ class Http(models.AbstractModel): return request.website.default_lang_id return super(Http, cls)._get_default_lang() + @classmethod + def _serve_page(cls): + req_page = request.httprequest.path + + domain = [('url', '=', req_page), '|', ('website_ids', 'in', request.website.id), ('website_ids', '=', False)] + + if not request.website.is_publisher: + domain += [('is_visible', '=', True)] + + mypage = request.env['website.page'].search(domain, limit=1) + _, ext = os.path.splitext(req_page) + if mypage: + return request.render(mypage.ir_ui_view_id.id, { + # 'path': req_page[1:], + 'deletable': True, + 'main_object': mypage, + }, mimetype=_guess_mimetype(ext)) + + return request.website.is_publisher() and request.render('website.page_404', {'path': req_page[1:]}) or False + + @classmethod + def _serve_redirect(cls): + req_page = request.httprequest.path + domain = [ + '|', ('website_id', '=', request.website.id), ('website_id', '=', False), + ('url_from', '=', req_page) + ] + return request.env['website.redirect'].search(domain, limit=1) + + @classmethod + def _serve_fallback(cls, exception): + # serve attachment before + parent = super(Http, cls)._serve_fallback(exception) + if parent: # attachment + return parent + + website_page = cls._serve_page() + if website_page: + return website_page + + redirect = cls._serve_redirect() + if redirect: + return request.redirect(redirect.url_to, code=redirect.type) + + return False + + @classmethod def _handle_exception(cls, exception): code = 500 # default code @@ -99,6 +145,7 @@ class Http(models.AbstractModel): else: try: response = super(Http, cls)._handle_exception(exception) + if isinstance(response, Exception): exception = response else: @@ -135,7 +182,7 @@ class Http(models.AbstractModel): if 'qweb_exception' in values: view = request.env["ir.ui.view"] views = view._views_get(exception.qweb['template']) - to_reset = views.filtered(lambda view: view.model_data_id.noupdate is True and not view.page) + to_reset = views.filtered(lambda view: view.model_data_id.noupdate is True and view.arch_fs) values['views'] = to_reset elif code == 403: logger.warn("403 Forbidden:\n\n%s", values['traceback']) @@ -178,30 +225,3 @@ class ModelConverter(ModelConverter): for record in Model.search_read(domain=domain, fields=['write_date', Model._rec_name]): if record.get(Model._rec_name, False): yield {'loc': (record['id'], record[Model._rec_name])} - - -class PageConverter(werkzeug.routing.PathConverter): - """ Only point of this converter is to bundle pages enumeration logic """ - - def generate(self, uid, query=None, args={}): - View = request.env['ir.ui.view'].sudo(uid) - domain = [('page', '=', True)] - query = query and query.startswith('website.') and query[8:] or query - if query: - domain += [('key', 'like', query)] - website = request.env['website'].get_current_website() - domain += ['|', ('website_id', '=', website.id), ('website_id', '=', False)] - - views = View.search_read(domain, fields=['key', 'priority', 'write_date'], order='name') - for view in views: - xid = view['key'].startswith('website.') and view['key'][8:] or view['key'] - # the 'page/homepage' url is indexed as '/', avoid aving the same page referenced twice - # when we will have an url mapping mechanism, replace this by a rule: page/homepage --> / - if xid == 'homepage': - continue - record = {'loc': xid} - if view['priority'] != 16: - record['__priority'] = min(round(view['priority'] / 32.0, 1), 1) - if view['write_date']: - record['__lastmod'] = view['write_date'][:10] - yield record diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py index eec00e2e897..55c2d2f4fd4 100644 --- a/addons/website/models/ir_ui_view.py +++ b/addons/website/models/ir_ui_view.py @@ -18,7 +18,6 @@ class View(models.Model): _name = "ir.ui.view" _inherit = ["ir.ui.view", "website.seo.metadata"] - page = fields.Boolean("Whether this view is a web page template (complete)", default=False) customize_show = fields.Boolean("Show As Optional Inherit", default=False) website_id = fields.Many2one('website', ondelete='cascade', string="Website") @@ -142,4 +141,4 @@ class View(models.Model): lang_code = self.env['website'].browse(website_id).default_lang_code return lang_code else: - return super(View, self).get_default_lang_code() \ No newline at end of file + return super(View, self).get_default_lang_code() diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 8208140b015..099bd6e1699 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -5,6 +5,7 @@ import inspect import logging import hashlib import re +import unicodedata from werkzeug import urls from werkzeug.exceptions import NotFound @@ -66,6 +67,7 @@ class Website(models.Model): cdn_filters = fields.Text('CDN Filters', default=lambda s: '\n'.join(DEFAULT_CDN_FILTERS), help="URL matching those filters will be rewritten using the CDN Base URL") partner_id = fields.Many2one(related='user_id.partner_id', relation='res.partner', string='Public Partner') menu_id = fields.Many2one('website.menu', compute='_compute_menu', string='Main Menu') + homepage_id = fields.Many2one('website.page', string='Homepage') favicon = fields.Binary(string="Website Favicon", help="This field holds the image used to display a favicon on the website.") @api.multi @@ -86,84 +88,74 @@ class Website(models.Model): #---------------------------------------------------------- # Page Management #---------------------------------------------------------- - @api.model - def new_page(self, name, template='website.default_page', ispage=True, namespace=None): + def new_page(self, name=False, add_menu=False, template='website.default_page', ispage=True, namespace=None): """ Create a new website page, and assign it a xmlid based on the given one :param name : the name of the page :param template : potential xml_id of the page to create :param namespace : module part of the xml_id if none, the template module name is used """ + if namespace: template_module = namespace else: - template_module, dummy = template.split('.') - website_id = self._context.get('website_id') - + template_module, _ = template.split('.') # completely arbitrary max_length - page_name = slugify(name, max_length=50) - page_xmlid = "%s.%s" % (template_module, page_name) + page_url = '/' + slugify(name, max_length=200, path=True) + page_key = self.get_unique_path(slugify(name, 50)) - # find a free xmlid - inc = 0 - domain_static = [('website_id', '=', False), ('website_id', '=', website_id)] - while self.env['ir.ui.view'].with_context(active_test=False).sudo().search([('key', '=', page_xmlid), '|'] + domain_static): - inc += 1 - page_xmlid = "%s.%s" % (template_module, page_name + ("-%s" % inc if inc else "")) - page_name += (inc and "-%s" % inc or "") + if not name: + name = 'Home' + page_key = 'home' - # new page template_record = self.env.ref(template) - key = '%s.%s' % (template_module, page_name) - page = template_record.copy({'website_id': website_id, 'key': key}) - page.with_context(lang=None).write({ - 'arch': page.arch.replace(template, page_xmlid), - 'name': page_name, - 'page': ispage, + website_id = self._context.get('website_id') + key = '%s.%s' % (template_module, page_key) + view = template_record.copy({'website_id': website_id, 'key': key}) + + view.with_context(lang=None).write({ + 'arch': template_record.arch.replace(template, key), + 'name': name, }) - return page_xmlid + if ispage: + page = self.env['website.page'].create({ + 'name': name, + 'url': page_url, + 'website_ids': [(6, None, [self.get_current_website().id])], + 'ir_ui_view_id': view.id + }) + if add_menu: + self.env['website.menu'].create({ + 'name': name, + 'url': page_url, + 'parent_id': self.get_current_website().menu_id.id, + 'page_id': page.id, + 'website_id': self.get_current_website().id, + }) + return page_url + + def get_unique_path(self, page_url): + """ Given an url, return that url suffixed by counter if it already exists + :param page_url : the url to be checked for uniqueness + """ + website_id = self.get_current_website().id + inc = 0 + domain_static = ['|', ('website_ids', '=', False), ('website_ids', 'in', website_id)] + page_temp = page_url + while self.env['website.page'].with_context(active_test=False).sudo().search([('url', '=', page_temp)] + domain_static): + inc += 1 + page_temp = page_url + (inc and "-%s" % inc or "") + return page_temp def key_to_view_id(self, view_id): return self.env['ir.ui.view'].search([ ('id', '=', view_id), '|', ('website_id', '=', self._context.get('website_id')), ('website_id', '=', False), - ('page', '=', True), ('type', '=', 'qweb') ]) @api.model - def delete_page(self, view_id): - """ Delete a page, given its identifier - :param view_id : ir.ui.view identifier - """ - view = self.key_to_view_id(view_id) - if view: - view.unlink() - - @api.model - def rename_page(self, view_id, new_name): - """ Change the name of the given page - :param view_id : id of the view to rename - :param new_name : name to use - """ - view = self.key_to_view_id(view_id) - if view: - # slugify the new name and prefix by module if - # not already done by end user - new_name = slugify(new_name, max_length=50) - prefix = view.key.split('.')[0] - if not new_name.startswith(prefix): - new_name = "%s.%s" % (prefix, new_name) - - view.write({ - 'key': new_name, - 'arch_db': view.arch_db.replace(view.key, new_name, 1) - }) - return new_name - return False - - @api.model - def page_search_dependencies(self, view_id=False): + def page_search_dependencies(self, page_id=False): """ Search dependencies just for information. It will not catch 100% of dependencies and False positive is more than possible Each module could add dependences in this dict @@ -171,60 +163,65 @@ class Website(models.Model): view, and the value is the list of text and link to the resource using given page """ dependencies = {} - if not view_id: + if not page_id: return dependencies - view = self.env['ir.ui.view'].browse(view_id) + page = self.env['website.page'].browse(int(page_id)) website_id = self._context.get('website_id') - name = view.key.replace("website.", "") - fullname = "website.%s" % name + url = page.url - if view.page: - # search for page with link - page_search_dom = [ - '|', ('website_id', '=', website_id), ('website_id', '=', False), - '|', ('arch_db', 'ilike', '/page/%s' % name), ('arch_db', 'ilike', '/page/%s' % fullname) - ] - page_key = _('Page') - pages = self.env['ir.ui.view'].search(page_search_dom) - for page in pages: - dependencies.setdefault(page_key, []) - if page.page: - dependencies[page_key].append({ - 'text': _('Page %s contains a link to this page') % page.key, - 'link': '/page/%s' % page.key - }) - else: - dependencies[page_key].append({ - 'text': _('Template %s (id:%s) contains a link to this page') % (page.key, page.id), - 'link': '#' - }) + page_key = _('Page') - # search for menu with link - menu_search_dom = [ - '|', ('website_id', '=', website_id), ('website_id', '=', False), - '|', ('url', 'ilike', '/page/%s' % name), ('url', 'ilike', '/page/%s' % fullname) - ] + # search for website_page with link + website_page_search_dom = [ + '|', ('website_ids', 'in', website_id), ('website_ids', '=', False), ('ir_ui_view_id.arch_db', 'ilike', url) + ] + pages = self.env['website.page'].search(website_page_search_dom) + page_view_ids = [] + for page in pages: + dependencies.setdefault(page_key, []) + dependencies[page_key].append({ + 'text': _('Page %s contains a link to this page') % page.url, + 'link': page.url + }) + page_view_ids.append(page.ir_ui_view_id.id) - menu_key = _('Menu') - menus = self.env['website.menu'].search(menu_search_dom) - for menu in menus: - dependencies.setdefault(menu_key, []).append({ - 'text': _('This page is in the menu %s') % menu.name, - 'link': False - }) + # search for ir_ui_view (not from a website_page) with link + page_search_dom = [ + '|', ('website_id', '=', website_id), ('website_id', '=', False), + ('arch_db', 'ilike', url), ('id', 'not in', page_view_ids) + ] + views = self.env['ir.ui.view'].search(page_search_dom) + for view in views: + dependencies.setdefault(page_key, []) + dependencies[page_key].append({ + 'text': _('Template %s (id:%s) contains a link to this page') % (view.key or view.name, view.id), + 'link': '#' + }) + # search for menu with link + menu_search_dom = [ + '|', ('website_id', '=', website_id), ('website_id', '=', False), ('url', 'ilike', '%s' % url) + ] + + menu_key = _('Menu') + menus = self.env['website.menu'].search(menu_search_dom) + for menu in menus: + dependencies.setdefault(menu_key, []).append({ + 'text': _('This page is in the menu %s') % menu.name, + 'link': False + }) return dependencies @api.model def page_exists(self, name, module='website'): try: - name = (name or "").replace("/page/website.", "").replace("/page/", "") + name = (name or "").replace("/website.", "").replace("/", "") if not name: return False return self.env.ref('%s.%s' % module, name) - except: + except Exception: return False #---------------------------------------------------------- @@ -348,7 +345,7 @@ class Website(models.Model): return all((arg in rule._converters) for arg in args) @api.multi - def enumerate_pages(self, query_string=None): + def enumerate_pages(self, query_string=None, force=False): """ Available pages in the website/CMS. This is mostly used for links generation and can be overridden by modules setting up new HTML controllers for dynamic pages (e.g. blog). @@ -360,13 +357,13 @@ class Website(models.Model): of the same. :rtype: list({name: str, url: str}) """ + router = request.httprequest.app.get_db_router(request.db) # Force enumeration to be performed as public user url_set = set() for rule in router.iter_rules(): if not self.rule_is_enumerable(rule): continue - converters = rule._converters or {} if query_string and not converters and (query_string not in rule.build([{}], append_unknown=False)[1]): continue @@ -400,12 +397,37 @@ class Website(models.Model): yield page + # '/' already has a http.route & is in the routing_map so it will already have an entry in the xml + domain = [('url', '!=', '/')] + if not force: + domain += [('website_indexed', '=', True)] + #is_visible + domain += [('website_published', '=', True), '|', ('date_publish', '!=', False), ('date_publish', '>', fields.Datetime.now())] + + if query_string: + domain += [('url', 'like', query_string)] + + pages = self.get_website_pages(domain) + + for page in pages: + record = {'loc': page['url'], 'id': page['id'], 'name': page['name']} + if page.ir_ui_view_id and page.ir_ui_view_id.priority != 16: + record['__priority'] = min(round(page.ir_ui_view_id.priority / 32.0, 1), 1) + if page['write_date']: + record['__lastmod'] = page['write_date'][:10] + yield record + + @api.multi + def get_website_pages(self, domain=[], order='name', limit=None): + domain += ['|', ('website_ids', 'in', self.get_current_website().id), ('website_ids', '=', False)] + pages = request.env['website.page'].search(domain, order='name', limit=limit) + return pages + @api.multi def search_pages(self, needle=None, limit=None): - name = re.sub(r"^/p(a(g(e(/(w(e(b(s(i(t(e(\.)?)?)?)?)?)?)?)?)?)?)?)?", "", needle or "") - name = slugify(name, max_length=50) + name = slugify(needle, max_length=50, path=True) res = [] - for page in self.enumerate_pages(query_string=name): + for page in self.enumerate_pages(query_string=name, force=True): res.append(page) if len(res) == limit: break @@ -437,72 +459,6 @@ class Website(models.Model): return self.env.ref('website.action_website').read()[0] -class Menu(models.Model): - - _name = "website.menu" - _description = "Website Menu" - - _parent_store = True - _parent_order = 'sequence' - _order = "sequence" - - def _default_sequence(self): - menu = self.search([], limit=1, order="sequence DESC") - return menu.sequence or 0 - - name = fields.Char('Menu', required=True, translate=True) - url = fields.Char('Url', default='') - new_window = fields.Boolean('New Window') - sequence = fields.Integer(default=_default_sequence) - website_id = fields.Many2one('website', 'Website') # TODO: support multiwebsite once done for ir.ui.views - parent_id = fields.Many2one('website.menu', 'Parent Menu', index=True, ondelete="cascade") - child_id = fields.One2many('website.menu', 'parent_id', string='Child Menus') - parent_left = fields.Integer('Parent Left', index=True) - parent_right = fields.Integer('Parent Rigth', index=True) - - # would be better to take a menu_id as argument - @api.model - def get_tree(self, website_id, menu_id=None): - def make_tree(node): - menu_node = dict( - id=node.id, - name=node.name, - url=node.url, - new_window=node.new_window, - sequence=node.sequence, - parent_id=node.parent_id.id, - children=[], - ) - for child in node.child_id: - menu_node['children'].append(make_tree(child)) - return menu_node - if menu_id: - menu = self.browse(menu_id) - else: - menu = self.env['website'].browse(website_id).menu_id - return make_tree(menu) - - @api.model - def save(self, website_id, data): - def replace_id(old_id, new_id): - for menu in data['data']: - if menu['id'] == old_id: - menu['id'] = new_id - if menu['parent_id'] == old_id: - menu['parent_id'] = new_id - to_delete = data['to_delete'] - if to_delete: - self.browse(to_delete).unlink() - for menu in data['data']: - mid = menu['id'] - if isinstance(mid, pycompat.string_types): - new_menu = self.create({'name': menu['name']}) - replace_id(mid, new_menu.id) - for menu in data['data']: - self.browse(menu['id']).write(menu) - return True - - class SeoMetadata(models.AbstractModel): _name = 'website.seo.metadata' @@ -538,3 +494,260 @@ class WebsitePublishedMixin(models.AbstractModel): 'url': self.website_url, 'target': 'self', } + + +class Page(models.Model): + _name = 'website.page' + _inherits = {'ir.ui.view': 'ir_ui_view_id'} + _inherit = 'website.published.mixin' + _description = 'Page' + + name = fields.Char('Page Name') + url = fields.Char('Page URL') + website_ids = fields.Many2many('website', string='Websites') + ir_ui_view_id = fields.Many2one('ir.ui.view', string='View', required=True, ondelete="cascade") + website_indexed = fields.Boolean('Page Indexed', default=True) + date_publish = fields.Datetime('Publishing Date') + # This is needed to be able to display if page is a menu in /website/page_management + menu_ids = fields.One2many('website.menu', 'page_id', 'Related Menus') + is_homepage = fields.Boolean(compute='_compute_homepage', string='Homepage') + is_visible = fields.Boolean(compute='_compute_visible', string='Is Visible') + + @api.one + def _compute_homepage(self): + self.is_homepage = self == self.env['website'].get_current_website().homepage_id + + @api.one + def _compute_visible(self): + self.is_visible = self.website_published and (not self.date_publish or self.date_publish < fields.Datetime.now()) + + @api.model + def get_page_info(self, id, website_id): + domain = ['|', ('website_ids', 'in', website_id), ('website_ids', '=', False), ('id', '=', id)] + item = self.search_read(domain, fields=['id', 'name', 'url', 'website_published', 'website_indexed', 'date_publish', 'menu_ids', 'is_homepage'], limit=1) + return item + + @api.model + def save_page_info(self, website_id, data): + website = self.env['website'].browse(website_id) + + if data['is_homepage'] and website.homepage_id.id != int(data['id']): + # If page is set as the new homepage, set it on website (only page can be set as homepage) + website.write({'homepage_id': data['id']}) + else: + if not data['is_homepage'] and website.homepage_id.id == int(data['id']): + # If the page is not a homepage, check if it was the homepage + website.write({'homepage_id': None}) + + #If URL has been edited, slug it + page = self.browse(int(data['id'])) + original_url = page.url + url = data['url'] + if page.url != url: + url = slugify(url, max_length=200, path=True) + + menu = self.env['website.menu'].search([('page_id', '=', int(data['id']))]) + if not data['is_menu']: + #If the page is no longer in menu, we should remove its website_menu + if menu: + menu.unlink() + else: + #The page is now a menu, check if has already one + if menu: + menu.write({'url': url}) + else: + self.env['website.menu'].create({ + 'name': data['name'], + 'url': url, + 'page_id': data['id'], + 'parent_id': website.menu_id.id, + 'website_id': website.id, + }) + + page.write({ + 'name': data['name'], 'url': url, + 'website_published': data['website_published'], + 'website_indexed': data['website_indexed'], + 'date_publish': data['date_publish'] or None + }) + + # Create redirect if needed + if data['create_redirect']: + self.env['website.redirect'].create({ + 'type': data['redirect_type'], + 'url_from': original_url, + 'url_to': url, + 'website_id': website.id, + }) + + return True + + @api.multi + def copy(self, default=None): + view = self.env['ir.ui.view'].browse(self.ir_ui_view_id.id) + new_view = view.copy() + default = { + 'name': self.name + ' (copy)', + 'url': self.env['website'].get_unique_path(self.url), + 'ir_ui_view_id': new_view.id, + } + return super(Page, self).copy(default=default) + + @api.model + def clone_page(self, page_id, clone_menu=True): + """ Clone a page, given its identifier + :param page_id : website.page identifier + """ + page = self.browse(int(page_id)) + new_page = page.copy() + if clone_menu: + menu = self.env['website.menu'].search([('page_id', '=', page_id)], limit=1) + if menu: + # If the page being cloned has a menu, clone it too + new_menu = menu.copy() + new_menu.write({'url': new_page.url, 'name': menu.name + ' (copy)', 'page_id': new_page.id}) + + return new_page.url + '?enable_editor=1' + + @api.multi + def unlink(self): + """ When a website_page is deleted, the ORM does not delete its ir_ui_view. + So we got to delete it ourself, but only if the ir_ui_view is not used by another website_page. + """ + # Handle it's ir_ui_view + for page in self: + # Other pages linked to the ir_ui_view of the page being deleted (will it even be possible?) + pages_linked_to_iruiview = self.env['website.page'].search( + [('ir_ui_view_id', '=', self.ir_ui_view_id.id), ('id', '!=', self.id)] + ) + if len(pages_linked_to_iruiview) == 0: + # If there is no other pages linked to that ir_ui_view, we can delete the ir_ui_view + self.env['ir.ui.view'].search([('id', '=', self.ir_ui_view_id.id)]).unlink() + # And then delete the website_page itself + return super(Page, self).unlink() + + @api.model + def delete_page(self, page_id): + """ Delete a page or a link, given its identifier + :param object_id : object identifier eg: menu-5 + """ + # If we are deleting a page (that could possibly be a menu with a page) + page = self.env['website.page'].browse(int(page_id)) + if page: + # Check if it is a menu with a page and also delete menu if so + menu = self.env['website.menu'].search([('page_id', '=', page.id)], limit=1) + if menu: + menu.unlink() + page.unlink() + + @api.multi + def write(self, vals): + self.ensure_one() + if 'url' in vals and not vals['url'].startswith('/'): + vals['url'] = '/' + vals['url'] + result = super(Page, self).write(vals) + return result + + +class Menu(models.Model): + + _name = "website.menu" + _description = "Website Menu" + + _parent_store = True + _parent_order = 'sequence' + _order = "sequence" + + def _default_sequence(self): + menu = self.search([], limit=1, order="sequence DESC") + return menu.sequence or 0 + + name = fields.Char('Menu', required=True, translate=True) + url = fields.Char('Url', default='') + page_id = fields.Many2one('website.page', 'Related Page') + new_window = fields.Boolean('New Window') + sequence = fields.Integer(default=_default_sequence) + website_id = fields.Many2one('website', 'Website') # TODO: support multiwebsite once done for ir.ui.views + parent_id = fields.Many2one('website.menu', 'Parent Menu', index=True, ondelete="cascade") + child_id = fields.One2many('website.menu', 'parent_id', string='Child Menus') + parent_left = fields.Integer('Parent Left', index=True) + parent_right = fields.Integer('Parent Rigth', index=True) + + @api.model + def clean_url(self): + # clean the url with heuristic + if self.page_id: + url = self.page_id.url + else: + url = self.url + if not self.url.startswith('/'): + if '@' in self.url and not self.url.startswith('mailto'): + url = 'mailto:%s' % self.url + elif not self.url.startswith('http'): + url = '/%s' % self.url + return url + + # would be better to take a menu_id as argument + @api.model + def get_tree(self, website_id, menu_id=None): + def make_tree(node): + page_id = node.page_id.id if node.page_id else None + is_homepage = page_id and self.env['website'].browse(website_id).homepage_id.id == page_id + menu_node = dict( + id=node.id, + name=node.name, + url=node.page_id.url if page_id else node.url, + new_window=node.new_window, + sequence=node.sequence, + parent_id=node.parent_id.id, + children=[], + is_homepage=is_homepage, + ) + for child in node.child_id: + menu_node['children'].append(make_tree(child)) + return menu_node + if menu_id: + menu = self.browse(menu_id) + else: + menu = self.env['website'].browse(website_id).menu_id + return make_tree(menu) + + @api.model + def save(self, website_id, data): + def replace_id(old_id, new_id): + for menu in data['data']: + if menu['id'] == old_id: + menu['id'] = new_id + if menu['parent_id'] == old_id: + menu['parent_id'] = new_id + to_delete = data['to_delete'] + if to_delete: + self.browse(to_delete).unlink() + for menu in data['data']: + mid = menu['id'] + # new menu are prefixed by new- + if isinstance(mid, pycompat.string_types): + new_menu = self.create({'name': menu['name']}) + replace_id(mid, new_menu.id) + for menu in data['data']: + # if the url match a website.page, set the m2o relation + page = self.env['website.page'].search([('url', '=', menu['url'])], limit=1) + if page: + menu['page_id'] = page.id + self.browse(menu['id']).write(menu) + + return True + + +class WebsiteRedirect(models.Model): + _name = "website.redirect" + _description = "Website Redirect" + _order = "sequence, id" + _rec_name = 'url_from' + + type = fields.Selection([('301', 'Moved permanently'), ('302', 'Moved temporarily')], string='Redirection Type') + url_from = fields.Char('Redirect From') + url_to = fields.Char('Redirect To') + website_id = fields.Many2one('website', 'Website') + active = fields.Boolean(default=True) + sequence = fields.Integer(default=0) diff --git a/addons/website/security/ir.model.access.csv b/addons/website/security/ir.model.access.csv index 849724a4094..cfc93d39510 100644 --- a/addons/website/security/ir.model.access.csv +++ b/addons/website/security/ir.model.access.csv @@ -3,6 +3,10 @@ access_website_public,website,website.model_website,,1,0,0,0 access_website,website,website.model_website,group_website_designer,1,1,1,1 access_website_menu,access_website_menu,model_website_menu,,1,0,0,0 access_website_menu_designer,Web Menu Manager,model_website_menu,group_website_designer,1,1,1,1 +access_website_redirect,access_website_redirect,model_website_redirect,,1,0,0,0 +access_website_redirect_designer,Web Redirect Manager,model_website_redirect,group_website_designer,1,1,1,1 +access_website_page,access_website_page,model_website_page,,1,0,0,0 +access_website_page_designer,Web Page Manager,model_website_page,group_website_designer,1,1,1,1 access_website,web menu manager,website.model_website,group_website_designer,1,1,1,1 access_website_ir_ui_view,access_website_ir_ui_view,model_ir_ui_view,group_website_designer,1,1,1,1 access_seo_public,access_seo_public,model_website_seo_metadata,,1,0,0,0 diff --git a/addons/website/security/website_security.xml b/addons/website/security/website_security.xml index 2e7ad4cbe6f..99264d4e460 100644 --- a/addons/website/security/website_security.xml +++ b/addons/website/security/website_security.xml @@ -45,4 +45,10 @@ + + website.page: portal/public: read published pages + + [('website_published', '=', True)] + + diff --git a/addons/website/static/src/js/content/website_root.js b/addons/website/static/src/js/content/website_root.js index 490151d38eb..74ab190f36d 100644 --- a/addons/website/static/src/js/content/website_root.js +++ b/addons/website/static/src/js/content/website_root.js @@ -195,6 +195,7 @@ var WebsiteRoot = BodyManager.extend({ }) .done(function (result) { $data.toggleClass("css_unpublished css_published"); + $data.find('input').prop("checked", result); $data.parents("[data-publish]").attr("data-publish", +result ? 'on' : 'off'); }) .fail(function (err, data) { diff --git a/addons/website/static/src/js/menu/content.js b/addons/website/static/src/js/menu/content.js index 1ed6e2305e0..9a7e38244cf 100644 --- a/addons/website/static/src/js/menu/content.js +++ b/addons/website/static/src/js/menu/content.js @@ -3,13 +3,241 @@ odoo.define('website.contentMenu', function (require) { var core = require('web.core'); var Dialog = require('web.Dialog'); +var time = require('web.time'); var weContext = require('web_editor.context'); var widget = require('web_editor.widget'); var websiteNavbarData = require('website.navbar'); +var websiteRootData = require('website.WebsiteRoot'); +var Widget = require('web.Widget'); var _t = core._t; var qweb = core.qweb; +var PagePropertiesDialog = widget.Dialog.extend({ + template: 'website.pagesMenu.page_info', + xmlDependencies: widget.Dialog.prototype.xmlDependencies.concat( + ['/website/static/src/xml/website.pageProperties.xml'] + ), + events: _.extend({}, widget.Dialog.prototype.events, { + 'keyup input#page_url': '_onUrlChanged', + 'change input#create_redirect': '_onCreateRedirectChanged', + }), + + /** + * @constructor + * @override + */ + init: function (parent, page_id, options) { + var self = this; + var serverUrl = window.location.origin + "/"; + var length_url = serverUrl.length; + var serverUrlTrunc = serverUrl; + if(length_url > 30) + serverUrlTrunc = serverUrl.slice(0,14) + '..' + serverUrl.slice(-14); + this.serverUrl = serverUrl; + this.serverUrlTrunc = serverUrlTrunc; + this.current_page_url = window.location.pathname; + this.page_id = page_id; + + var buttons = [ + {text: _t("Save"), classes: "btn-primary o_save_button", click: self.save}, + {text: _t("Discard"), close: true}, + ]; + if (options.fromPageManagement) { + buttons.push({text: _t("Go To Page"), icon: "fa-globe", classes: "btn-link pull-right", click: function(e){window.location.href = self.page.url;}}); + } + this._super(parent, _.extend({}, { + title: _t("Page Properties"), + size: 'medium', + buttons: buttons, + }, options || {})); + }, + /** + * @override + */ + willStart: function () { + var defs = [this._super.apply(this, arguments)]; + var self = this; + var context = weContext.get(); + + defs.push(this._rpc({ + model: 'website.page', + method: 'get_page_info', + args: [self.page_id,context.website_id], + kwargs: { + context: context + }, + }).then(function (page) { + page[0].url = page[0].url.startsWith('/') ? page[0].url.substring(1) : page[0].url; + self.page = page[0]; + })); + + defs.push(this._rpc({ + model: 'website.redirect', + method: 'fields_get', + }).then(function (fields) { + self.fields = fields; + })); + + return $.when.apply($, defs); + }, + /** + * @override + */ + start: function() { + var self = this; + var context = weContext.get(); + + this.$(".ask_for_redirect").hide(); + this.$(".redirect_type").hide(); + + //TODO: remove.html() & set self.var & display in view instead of controller (here) + this._getPageDependencies(self.page_id, context) + .then(function (dependencies) { + var dep = []; + _.each(dependencies, function( value, index ) { + if (dependencies[index].length > 0) { + dep.push(dependencies[index].length + ' ' + index.toLowerCase() + (dependencies[index].length > 1 ? 's': '')); + } + }); + if (dep.length > 0) { + $("#dependencies_redirect").html("(" + _("used in") + " " + dep.join(", ") + ")"); + } + }); + + var l10n = _t.database.parameters; + var datepickersOptions = { + minDate: moment({ y: 1900 }), + maxDate: moment().add(200, "y"), + calendarWeeks: true, + icons : { + time: 'fa fa-clock-o', + date: 'fa fa-calendar', + next: 'fa fa-chevron-right', + previous: 'fa fa-chevron-left', + up: 'fa fa-chevron-up', + down: 'fa fa-chevron-down', + }, + locale : moment.locale(), + format : time.strftime_to_moment_format(l10n.date_format +' '+ l10n.time_format), + widgetPositioning : { + horizontal: 'auto', + vertical: 'top', + }, + }; + if (self.page.date_publish) { + datepickersOptions.defaultDate = self.page.date_publish; + } + + this.$("#date_publish_container").datetimepicker(datepickersOptions); + + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + save: function(data){ + var self = this; + var context = weContext.get(); + var url = $(".o_page_management_info #page_url").val(); + + var date_publish = $(".o_page_management_info #date_publish").val(); + if (date_publish != "") { + date_publish = time.datetime_to_str(new Date(date_publish)); + } + var params = { + id: self.page.id, + name: $(".o_page_management_info #page_name").val(), + //replace duplicate following "/" by only one "/" + url: url.replace(/\/{2,}/g,"/"), + is_menu: $(".o_page_management_info #is_menu").prop('checked'), + is_homepage: $(".o_page_management_info #is_homepage").prop('checked'), + website_published: $(".o_page_management_info #is_published").prop('checked'), + create_redirect: $(".o_page_management_info #create_redirect").prop('checked'), + redirect_type: $(".o_page_management_info #redirect_type").val(), + website_indexed: $(".o_page_management_info #is_indexed").prop('checked'), + date_publish: date_publish, + }; + self._rpc({ + model: 'website.page', + method: 'save_page_info', + args: [[context.website_id], params], + kwargs: { + context: context + }, + }).then(function () { + // If from page manager: reload url, if from page itself: go to (possibly) new url + if (self._getMainObject().model == 'website.page') + window.location.href = url.toLowerCase(); + else + window.location.reload(true); + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Retrieves the page dependencies for the given object id. + * + * @private + * @param {integer} moID + * @param {Object} context + * @returns {Deferred} + */ + _getPageDependencies: function (moID, context) { + return this._rpc({ + model: 'website', + method: 'page_search_dependencies', + args: [moID], + context: context, + }); + }, + /** + * Returns information about the page main object. + * + * @private + * @returns {Object} model and id + */ + _getMainObject: function () { + var repr = $('html').data('main-object'); + var m = repr.match(/(.+)\((\d+),(.*)\)/); + return { + model: m[1], + id: m[2] | 0, + }; + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + _onUrlChanged: function () { + var url = this.$('input#page_url').val(); + if (url != this.page.url) { + this.$(".ask_for_redirect").show(); + } + else { + this.$(".ask_for_redirect").hide(); + } + }, + _onCreateRedirectChanged: function () { + var createRedirect = this.$('input#create_redirect').prop('checked'); + if (createRedirect) { + this.$(".redirect_type").show(); + } + else { + this.$(".redirect_type").hide(); + } + }, +}); + var MenuEntryDialog = widget.LinkDialog.extend({ xmlDependencies: widget.LinkDialog.prototype.xmlDependencies.concat( ['/website/static/src/xml/website.contentMenu.xml'] @@ -24,13 +252,16 @@ var MenuEntryDialog = widget.LinkDialog.extend({ data.isNewWindow = data.new_window; this.data = data; this.menu_link_options = options.menu_link_options; - return this._super.apply(this, arguments); + this._super(parent, _.extend({}, { + title: _t("Create Menu"), + }, options || {}), editor, data); }, /** * @override */ start: function () { var self = this; + self.$('#o_link_dialog_url_input').closest('.form-group').hide(); this.$('.o_link_dialog_preview').remove(); this.$('.window-new, .link-style').closest('.form-group').remove(); this.$('label[for="o_link_dialog_label_input"]').text(_t("Menu Label")); @@ -288,9 +519,8 @@ var EditMenuDialog = widget.Dialog.extend({ var ContentMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({ xmlDependencies: ['/website/static/src/xml/website.xml'], actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, { - delete_page: '_deletePage', edit_menu: '_editMenu', - rename_page: '_renamePage', + page_properties: '_pageProperties', }), //-------------------------------------------------------------------------- @@ -311,68 +541,11 @@ var ContentMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({ id: m[2] | 0, }; }, - /** - * Retrieves the page dependencies for the given object id. - * - * @private - * @param {integer} moID - * @param {Object} context - * @returns {Deferred} - */ - _getPageDependencies: function (moID, context) { - return this._rpc({ - model: 'website', - method: 'page_search_dependencies', - args: [moID], - context: context, - }); - }, //-------------------------------------------------------------------------- // Actions //-------------------------------------------------------------------------- - /** - * Searches for the page dependencies, inform the user about them then - * delete the page if the user agrees. - * - * @private - * @returns {Deferred} - */ - _deletePage: function () { - var self = this; - var moID = self._getMainObject().id; - var context = weContext.get(); - - var def = $.Deferred(); - - // Search the page dependencies - this._getPageDependencies(moID, context) - .then(function (dependencies) { - // Inform the user about those dependencies and ask him confirmation - var confirmDef = $.Deferred(); - Dialog.safeConfirm(self, "", { - title: _t("Delete Page"), - $content: $(qweb.render('website.delete_page', {dependencies: dependencies})), - confirm_callback: confirmDef.resolve.bind(confirmDef), - cancel_callback: def.resolve.bind(self), - }); - return confirmDef; - }).then(function () { - // Delete the page if the user confirmed - return self._rpc({ - model: 'website', - method: 'delete_page', - args: [moID], - context: context, - }); - }).then(function () { - // Redirect to homepage as the page is now deleted - window.location.href = '/'; - }, def.reject.bind(def)); - - return def; - }, /** * Asks the user which menu to edit if multiple menus exist on the page. * Then opens the menu edition dialog. @@ -419,58 +592,104 @@ var ContentMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({ return def; }, - /** - * Asks the user for the new name of the page, then reloads to the new page - * with the correct name if saved. - * - * @private - * @returns {Deferred} - * Unresolved if the page's name is changed and saved as the page - * will be reloaded. - * Resolved otherwise. - */ - _renamePage: function () { - var def = $.Deferred(); - + _pageProperties: function () { var self = this; var moID = self._getMainObject().id; - var context = weContext.get(); - - // Search the page dependencies - this._getPageDependencies(moID, context).then(function (dependencies) { - // Ask the user for the new page name and inform him about the dependencies - var renameDef = $.Deferred(); - var $content = $(qweb.render('website.rename_page', {dependencies: dependencies})); - Dialog.confirm(self, "", { - title: _t("Rename This Page"), - $content: $content, - confirm_callback: function () { - var $newNameInput = $content.find('input#new_name'); - renameDef.resolve($newNameInput.val()); - }, - cancel_callback: def.resolve.bind(def), - }); - return renameDef; - }).then(function (newName) { - // Rename the page with the user's choice - return self._rpc({ - model: 'website', - method: 'rename_page', - args: [moID, newName], - context: context, - }); - }).then(function (newTechnicalName) { - // Redirects to the new renamed page - window.location.href = ('/page/' + encodeURIComponent(newTechnicalName)); - }, def.reject.bind(def)); - - return def; + var dialog = new PagePropertiesDialog(self,moID,{}).open(); + return dialog; }, }); + +var PageManagement = Widget.extend({ + events: { + 'click a.js_page_properties': '_onPagePropertiesButtonClick', + 'click a.js_clone_page': '_onClonePageButtonClick', + 'click a.js_delete_page': '_onDeletePageButtonClick', + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Retrieves the page dependencies for the given object id. + * + * @private + * @param {integer} moID + * @param {Object} context + * @returns {Deferred} + */ + _getPageDependencies: function (moID, context) { + return this._rpc({ + model: 'website', + method: 'page_search_dependencies', + args: [moID], + context: context, + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + _onPagePropertiesButtonClick: function (ev) { + var moID = $(ev.currentTarget).data('id'); + var dialog = new PagePropertiesDialog(this,moID, {'fromPageManagement': true}).open(); + return dialog; + }, + _onClonePageButtonClick: function (ev) { + var pageId = $(ev.currentTarget).data('id'); + var context = weContext.get(); + this._rpc({ + model: 'website.page', + method: 'clone_page', + args: [pageId], + kwargs: { + context: context, + }, + }).then(function (path) { + window.location.href = path; + }); + }, + _onDeletePageButtonClick: function (ev) { + var pageId = $(ev.currentTarget).data('id'); + var self = this; + var context = weContext.get(); + + var def = $.Deferred(); + // Search the page dependencies + this._getPageDependencies(pageId, context) + .then(function (dependencies) { + // Inform the user about those dependencies and ask him confirmation + var confirmDef = $.Deferred(); + Dialog.safeConfirm(self, "", { + title: _t("Delete Page"), + $content: $(qweb.render('website.delete_page', {dependencies: dependencies})), + confirm_callback: confirmDef.resolve.bind(confirmDef), + cancel_callback: def.resolve.bind(self), + }); + return confirmDef; + }).then(function () { + // Delete the page if the user confirmed + return self._rpc({ + model: 'website.page', + method: 'delete_page', + args: [pageId], + context: context, + }); + }).then(function () { + window.location.reload(true); + }, def.reject.bind(def)); + }, +}); + + websiteNavbarData.websiteNavbarRegistry.add(ContentMenu, '#content-menu'); +websiteRootData.websiteRootRegistry.add(PageManagement, '#edit_website_pages'); return { + PagePropertiesDialog: PagePropertiesDialog, ContentMenu: ContentMenu, EditMenuDialog: EditMenuDialog, MenuEntryDialog: MenuEntryDialog, diff --git a/addons/website/static/src/js/menu/seo.js b/addons/website/static/src/js/menu/seo.js index 77b4d0a76e1..2a85b49bd59 100644 --- a/addons/website/static/src/js/menu/seo.js +++ b/addons/website/static/src/js/menu/seo.js @@ -375,8 +375,10 @@ var SeoConfigurator = Dialog.extend({ self.updateTable(removed); }); this.keywordList.insertAfter(this.$('.table thead')); - this.disableUnsavableFields(); - this.renderPreview(); + this.disableUnsavableFields().then(function(){ + self.renderPreview(); + }); + this.getLanguages(); this.updateTable(); }, @@ -396,7 +398,9 @@ var SeoConfigurator = Dialog.extend({ }, disableUnsavableFields: function () { var self = this; - this.loadMetaData().then(function (data) { + return this.loadMetaData().then(function (data) { + //If website.page, hide the google preview & tell user his page is currently unindexed + self.isIndexed = (data && ('website_indexed' in data)) ? data.website_indexed : true; self.canEditTitle = data && ('website_meta_title' in data); self.canEditDescription = data && ('website_meta_description' in data); self.canEditKeywords = data && ('website_meta_keywords' in data); @@ -474,6 +478,9 @@ var SeoConfigurator = Dialog.extend({ def.resolve(null); } else { var fields = ['website_meta_title', 'website_meta_description', 'website_meta_keywords']; + if (obj.model == 'website.page'){ + fields.push('website_indexed'); + } rpc.query({ model: obj.model, method: 'read', @@ -521,11 +528,20 @@ var SeoConfigurator = Dialog.extend({ }); }, renderPreview: function () { - var preview = new Preview(this, { - title: this.htmlPage.title(), - description: this.htmlPage.description(), - url: this.htmlPage.url(), - }); + var indexed = this.isIndexed; + var preview = ""; + if(indexed){ + preview = new Preview(this, { + title: this.htmlPage.title(), + description: this.htmlPage.description(), + url: this.htmlPage.url(), + }); + } + else{ + preview = new Preview(this, { + description: _("You have hidden this page from search results. It won't be indexed by search engine"), + }); + } var $preview = this.$('.js_seo_preview'); $preview.empty(); preview.appendTo($preview); diff --git a/addons/website/static/src/js/tours/rte.js b/addons/website/static/src/js/tours/rte.js index 35e7ad55bc4..c0119aec2a4 100644 --- a/addons/website/static/src/js/tours/rte.js +++ b/addons/website/static/src/js/tours/rte.js @@ -65,7 +65,7 @@ tour.register('rte_translator', { trigger: 'html:not(:has(#wrap p span)) .o_menu_systray a[data-action="translate"]', }, { content: "close modal", - trigger: '.modal-footer .btn-primary', + trigger: '.modal-footer .btn-default', }, { content: "check if translation is activate", trigger: '[data-oe-translation-id]', @@ -85,7 +85,7 @@ tour.register('rte_translator', { }, }, { content: "click on input", - trigger: 'input:first', + trigger: '#wrap input:first', extra_trigger: '#wrap .o_dirty font:first:contains(translated french text)', run: 'click', }, { diff --git a/addons/website/static/src/js/utils.js b/addons/website/static/src/js/utils.js index 40a4033540d..e7f76376480 100644 --- a/addons/website/static/src/js/utils.js +++ b/addons/website/static/src/js/utils.js @@ -7,7 +7,7 @@ var weContext = require('web_editor.context'); var qweb = core.qweb; /** - * Allows the given input to propose existing website pages. + * Allows the given input to propose existing website URLs. * * @param {ServicesMixin|Widget} self - an element capable to trigger an RPC * @param {jQuery} $input @@ -25,7 +25,12 @@ function autocompleteWithPages(self, $input) { }, }).then(function (exists) { var rs = _.map(exists, function (r) { - return r.loc; + if (show_only_website_page_model) { + return r.loc + ' (' + r.name + ')'; + } + else { + return r.loc; + } }); response(rs); }); diff --git a/addons/website/static/src/less/website.ui.components.less b/addons/website/static/src/less/website.ui.components.less index 409d72ba366..c1381f09e99 100644 --- a/addons/website/static/src/less/website.ui.components.less +++ b/addons/website/static/src/less/website.ui.components.less @@ -451,3 +451,31 @@ body .modal { content: initial; } } + +// Pages Management +.o_page_management_info .o_switch{ + padding-top:7px; +} +#edit_website_pages{ + .fa-check{ + color: #00a09d; + } + .fa-eye-slash{ + color: #00a09d; + } + th{ + background-color: #875A7B; + color: white; + opacity:0.8; + } + td{ + a.fa{ + color:#875A7B; + opacity:0.8; + margin-left:5px; + } + .text-muted{ + opacity:0.5; + } + } +} \ No newline at end of file diff --git a/addons/website/static/src/xml/website.contentMenu.xml b/addons/website/static/src/xml/website.contentMenu.xml index 4d3ba998ef0..a5b7590ba53 100644 --- a/addons/website/static/src/xml/website.contentMenu.xml +++ b/addons/website/static/src/xml/website.contentMenu.xml @@ -8,7 +8,7 @@ - +
@@ -356,7 +356,7 @@
@@ -449,7 +449,7 @@

- + Contact Us Now diff --git a/addons/website/views/website_navbar_templates.xml b/addons/website/views/website_navbar_templates.xml index 7bd6dceec09..95acebf9f06 100644 --- a/addons/website/views/website_navbar_templates.xml +++ b/addons/website/views/website_navbar_templates.xml @@ -33,12 +33,10 @@ @@ -61,6 +59,16 @@