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 @@
- Homepage
+ Home
qweb
- website.homepage
-
+ website2.homepage
-
+
@@ -152,7 +151,7 @@ response = request.render("website.template_partner_comment", {
Homepage 0.0.0.0
Click to customize this text
- Contact us
+ Contact us
@@ -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
-
-
-
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 @@
-
+
@@ -54,8 +54,12 @@
diff --git a/addons/website/static/src/xml/website.pageProperties.xml b/addons/website/static/src/xml/website.pageProperties.xml
new file mode 100644
index 00000000000..f21b0213f4e
--- /dev/null
+++ b/addons/website/static/src/xml/website.pageProperties.xml
@@ -0,0 +1,112 @@
+
+
+
+
diff --git a/addons/website/static/src/xml/website.seo.xml b/addons/website/static/src/xml/website.seo.xml
index a3018720515..7afd4ad32e9 100644
--- a/addons/website/static/src/xml/website.seo.xml
+++ b/addons/website/static/src/xml/website.seo.xml
@@ -11,7 +11,7 @@
@@ -334,7 +334,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 @@