[ADD] website: add new page management

website.page = old ir.ui.view with page=True
website.redirect is a new mechanism to replace in the futur the ir.attachment
mechanism of redirect.

From now, we don't have a specific /page controller to serve 'page'.

We use a new model website.page which is rendered if none route matches the url
 and that the field 'url' on website.page matches the request.httprequest.path.

The order to serve a path is:
    - Routes defines in controllers (/shop, /blog, ...)
    - ir.attachment with name matching the path
    - website.page with url matching the path
    - website.redirect with url_from matching the path
    - 404

To improve:
    - allow regexp in website.redirect model
    - allow to edit the view_arch from the page.management via redirect backend
      (needed when traceback in the page, or when modifying a js/css/less/...)
This commit is contained in:
rde
2017-09-15 15:29:37 +02:00
committed by Jeremy Kersten
parent 13a44d33bd
commit 4ecbacaf59
52 changed files with 1654 additions and 678 deletions
+28 -1
View File
@@ -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):
+17 -1
View File
@@ -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);
}
@@ -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();
@@ -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;
}
+63 -63
View File
@@ -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/<page: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/<model("res.country"):country>'], 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/<path:path>', 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/<website_xml_id>
url = "/page/" + re.sub(r"^website\.", '', xml_id)
@http.route(['/website/pages', '/website/pages/page/<int: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/<path:path>'], 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/<path_or_xml_id_or_id>',
'/website/action/<path_or_xml_id_or_id>/<path:path>',
], 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
+142 -3
View File
@@ -16,7 +16,7 @@
<record id="menu_homepage" model="website.menu">
<field name="name">Home</field>
<field name="url">/page/homepage</field>
<field name="url">/</field>
<field name="parent_id" ref="website.main_menu"/>
<field name="sequence" type="int">10</field>
<field name="website_id" ref="default_website"/>
@@ -24,12 +24,151 @@
<record id="menu_contactus" model="website.menu">
<field name="name">Contact us</field>
<field name="url">/page/contactus</field>
<field name="url">/contactus</field>
<field name="parent_id" ref="website.main_menu"/>
<field name="sequence" type="int">60</field>
<field name="website_id" ref="default_website"/>
</record>
<!--
Keep homepage & contactus page split between ir_ui_view & website_page record
So external_id of the ir_ui_view record is still 'website.contactus' and not 'website.contactus_ir_ui_view' (same for homepage)
Indeed Odoo put a suffix on external_id on submodels records (inherits) with the model's name as suffix.
eg: record website_page is 'website.contactus' & ir_ui_view record is 'website.contactus_ir_ui_view'
but since website_crm_template.xml (and surely others) inherit the contactus ir_ui_view record (inherit_id="website.contactus"),
the external_id of the contactus ir_ui_view record still has to be 'website.contactus'.
(Put in another way, external_id 'website.contactus' can't be the external_id of the website_page record or the inherit will ofc fail)
(Or change inherit_id in related files but possibly alot. Safer this way)
-->
<record id="homepage" model="ir.ui.view">
<field name="name">Home</field>
<field name="type">qweb</field>
<field name="key">website.homepage</field>
<field name="arch" type="xml">
<t name="Home" priority="29" t-name="website.homepage">
<t t-call="website.layout">
<t t-set="pageName" t-value="'homepage'"/>
<div id="wrap" class="oe_structure oe_empty"/>
</t>
</t>
</field>
</record>
<record id="homepage_page" model="website.page">
<field name="name">Home</field>
<field name="website_published">True</field>
<field name="url">/</field>
<field name="ir_ui_view_id" ref="homepage" />
</record>
<record id="default_website" model="website">
<field name="homepage_id" ref="homepage_page" />
</record>
<record id="contactus" model="ir.ui.view">
<field name="name">Contact Us</field>
<field name="type">qweb</field>
<field name="key">website.contactus</field>
<field name="arch" type="xml">
<t name="Contact Us" t-name="website.contactus">
<t t-call="website.layout">
<div id="wrap">
<div class="oe_structure"/>
<div class="container">
<h1>Contact us</h1>
<div class="row">
<div class="col-md-8">
<div class="oe_structure">
<div>
<p>Contact us about anything related to our company or services.</p>
<p>We'll do our best to get back to you as soon as possible.</p>
</div>
</div>
<div class="text-center mt64" name="mail_button">
<a t-attf-href="mailto:{{ res_company.email }}" class="btn btn-primary" id="o_contact_mail">Send us an email</a>
</div>
</div>
<div class="col-md-4 mb32">
<t t-call="website.company_description"/>
</div>
</div>
</div>
<div class="oe_structure"/>
</div>
</t>
</t>
</field>
</record>
<record id="contactus_page" model="website.page">
<field name="name">Contact Us</field>
<field name="url">/contactus</field>
<field name="website_published">True</field>
<field name="ir_ui_view_id" ref="contactus" />
</record>
<record id="aboutus" model="ir.ui.view">
<field name="name">About us</field>
<field name="type">qweb</field>
<field name="key">website.aboutus</field>
<field name="arch" type="xml">
<t name="About us" t-name="website.aboutus">
<t t-call="website.layout">
<div id="wrap">
<div class="oe_structure">
<section>
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="text-center">About us</h1>
<h3 class="text-muted text-center">Great products for great people</h3>
</div>
</div>
</div>
</section>
<section>
<div class="container">
<div class="row">
<div class="col-md-6 mt32">
<p>
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.
</p>
<p>
Our products are designed for small to medium size companies willing to optimize
their performance.
</p>
</div>
<div class="col-md-4 col-md-offset-2 mt16 mb16">
<img src="/website/static/src/img/library/business_conference.jpg" class="img img-responsive shadow" alt="Our Team"/>
</div>
</div>
</div>
</section>
</div>
<div class="oe_structure"></div>
</div>
</t>
</t>
</field>
</record>
<record id="aboutus_page" model="website.page">
<field name="name">About us</field>
<field name="website_published">True</field>
<field name="url">/aboutus</field>
<field name="ir_ui_view_id" ref="aboutus" />
</record>
<record id="menu_homepage" model="website.menu">
<field name="page_id" ref="homepage_page" />
</record>
<record id="menu_contactus" model="website.menu">
<field name="page_id" ref="contactus_page" />
</record>
<record id="action_module_website" model="ir.actions.act_window">
<field name="name">Website Apps</field>
<field name="res_model">ir.module.module</field>
@@ -61,7 +200,7 @@
</record>
<record id="action_website_homepage" model="ir.actions.act_url">
<field name="name">Website Homepage</field>
<field name="url">/page/homepage</field>
<field name="url">/</field>
<field name="target">self</field>
</record>
+61 -10
View File
@@ -132,12 +132,11 @@ response = request.render("website.template_partner_comment", {
</record>
<record id="website2_homepage" model="ir.ui.view">
<field name="name">Homepage</field>
<field name="name">Home</field>
<field name="type">qweb</field>
<field name="key">website.homepage</field>
<field name="website_id" ref="website2"/>
<field name="key">website2.homepage</field>
<field name="arch" type="xml">
<t name="Homepage" priority="29" t-name="website.homepage">
<t name="Home" priority="29" t-name="website2.homepage">
<t t-call="website.layout">
<div id="wrap" class="oe_structure oe_empty">
<div class="carousel slide mb32" id="myCarousel0" style="height: 320px;">
@@ -152,7 +151,7 @@ response = request.render("website.template_partner_comment", {
<h2>Homepage 0.0.0.0</h2>
<h3>Click to customize this text</h3>
<p>
<a class="btn btn-success btn-large" href="/page/website.contactus">Contact us</a>
<a class="btn btn-success btn-large" href="/contactus">Contact us</a>
</p>
</div>
<span class="carousel-img col-md-6 hidden-sm hidden-xs"> </span>
@@ -172,27 +171,79 @@ response = request.render("website.template_partner_comment", {
</t>
</field>
</record>
<record id="website2_homepage_page" model="website.page">
<field name="name">Home</field>
<field name="website_published">True</field>
<field name="url">/</field>
<field name="ir_ui_view_id" ref="website2_homepage" />
<field name="website_ids" eval="[(4, ref('website2'))]" />
</record>
<record id="website2_contactus" model="ir.ui.view">
<field name="name">Contact Us</field>
<field name="type">qweb</field>
<field name="key">website2.contactus</field>
<field name="arch" type="xml">
<t name="Contact Us" t-name="website2.contactus">
<t t-call="website.layout">
<div id="wrap">
<div class="oe_structure"/>
<div class="container">
<h1>Contact us</h1>
<div class="row">
<div class="col-md-8">
<div class="oe_structure">
<div>
<p>Contact us about anything related to our company or services.</p>
<p>We'll do our best to get back to you as soon as possible.</p>
</div>
</div>
<div class="text-center mt64" name="mail_button">
<a t-attf-href="mailto:{{ res_company.email }}" class="btn btn-primary" id="o_contact_mail">Send us an email</a>
</div>
</div>
<div class="col-md-4 mb32">
<t t-call="website.company_description"/>
</div>
</div>
</div>
<div class="oe_structure"/>
</div>
</t>
</t>
</field>
</record>
<record id="website2_contactus_page" model="website.page">
<field name="name">Home</field>
<field name="website_published">True</field>
<field name="url">/contactus</field>
<field name="ir_ui_view_id" ref="website2_contactus" />
<field name="website_ids" eval="[(4, ref('website2'))]" />
</record>
<!-- Menu & Homepage -->
<record id="website2" model="website">
<field name="homepage_id" ref="website2_homepage_page" />
</record>
<record id="website2_main_menu" model="website.menu">
<field name="name">Top Menu</field>
<field name="website_id" ref="website2"/>
</record>
<record id="website2_menu_homepage" model="website.menu">
<field name="name">Home</field>
<field name="url">/page/homepage</field>
<field name="url">/</field>
<field name="parent_id" ref="website.website2_main_menu"/>
<field name="sequence" type="int">10</field>
<field name="website_id" ref="website2"/>
<field name="page_id" ref="website2_homepage_page" />
</record>
<record id="website2_menu_contactus" model="website.menu">
<field name="name">Contact us</field>
<field name="url">/page/website.contactus</field>
<field name="url">/contactus</field>
<field name="parent_id" ref="website.website2_main_menu"/>
<field name="sequence" type="int">60</field>
<field name="website_id" ref="website2"/>
<field name="page_id" ref="website2_contactus_page" />
</record>
</data>
</odoo>
+52 -32
View File
@@ -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
+1 -2
View File
@@ -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()
return super(View, self).get_default_lang_code()
+377 -164
View File
@@ -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 <b>%s</b> contains a link to this page') % page.key,
'link': '/page/%s' % page.key
})
else:
dependencies[page_key].append({
'text': _('Template <b>%s (id:%s)</b> 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 <b>%s</b> 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 <b>%s</b>') % 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 <b>%s (id:%s)</b> 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 <b>%s</b>') % 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)
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
3 access_website website website.model_website group_website_designer 1 1 1 1
4 access_website_menu access_website_menu model_website_menu 1 0 0 0
5 access_website_menu_designer Web Menu Manager model_website_menu group_website_designer 1 1 1 1
6 access_website_redirect access_website_redirect model_website_redirect 1 0 0 0
7 access_website_redirect_designer Web Redirect Manager model_website_redirect group_website_designer 1 1 1 1
8 access_website_page access_website_page model_website_page 1 0 0 0
9 access_website_page_designer Web Page Manager model_website_page group_website_designer 1 1 1 1
10 access_website web menu manager website.model_website group_website_designer 1 1 1 1
11 access_website_ir_ui_view access_website_ir_ui_view model_ir_ui_view group_website_designer 1 1 1 1
12 access_seo_public access_seo_public model_website_seo_metadata 1 0 0 0
@@ -45,4 +45,10 @@
<field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="True"/>
</record>
<record id="website_page_rule_public" model="ir.rule">
<field name="name">website.page: portal/public: read published pages</field>
<field name="model_id" ref="website.model_website_page"/>
<field name="domain_force">[('website_published', '=', True)]</field>
<field name="groups" eval="[(4, ref('base.group_portal')), (4, ref('base.group_public'))]"/>
</record>
</odoo>
@@ -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) {
+323 -104
View File
@@ -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<Array>}
*/
_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<Array>}
*/
_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<Array>}
*/
_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,
+24 -8
View File
@@ -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);
+2 -2
View File
@@ -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',
}, {
+7 -2
View File
@@ -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);
});
@@ -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;
}
}
}
@@ -8,7 +8,7 @@
<span class="js_menu_label">
<t t-esc="submenu.name"/>
</span>
<i class="pull-right fa fa-home" style="margin-top: 3px"/>
<i t-if="submenu.is_homepage" class="pull-right fa fa-home" style="margin-top: 3px"/>
</span>
<span class="input-group-btn">
<button type="button" class="btn btn-primary js_edit_menu fa fa-pencil-square-o"/>
@@ -54,8 +54,12 @@
<div class="form-group">
<label class="control-label col-sm-3">Link</label>
<div class="col-sm-9">
<input type="radio" name="link_menu_options" id="url" value="url" checked="checked"/> <label class="control-label mr16" for="url" >URL or Email</label>
<input type="radio" name="link_menu_options" id="new_page" value="new_page"/> <label class="control-label" for="new_page">New Page</label>
<div class="radio">
<label class="checkbox-inline" for="new_page"><input type="radio" name="link_menu_options" id="new_page" value="new_page" checked="checked"/>New Page</label>
</div>
<div class="radio">
<label class="checkbox-inline" for="url"><input type="radio" name="link_menu_options" id="url" value="url" />URL or Email</label>
</div>
</div>
</div>
</t>
@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<div t-name="website.pagesMenu.page_info" class="o_page_management_info">
<form class="form-horizontal">
<ul class="nav nav-tabs">
<li class="active"><a data-toggle="tab" href="#basic_page_info">Name</a></li>
<li><a data-toggle="tab" href="#advances_page_info">Publish</a></li>
</ul>
<div class="tab-content mt16">
<div id="basic_page_info" class="tab-pane fade in active">
<div class="form-group">
<label class="control-label col-sm-3" for="page_name">Page Name</label>
<div class="col-sm-9">
<input type="text" class="form-control input-sm" id="page_name" t-att-value="widget.page.name" />
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-3" for="page_url">Page Url</label>
<div class="col-sm-9">
<div class="input-group">
<span class="input-group-addon" t-att-title="widget.serverUrl"><small><t t-esc="widget.serverUrlTrunc" /></small></span>
<input type="text" class="form-control input-sm" id="page_url" t-att-value="widget.page.url" />
</div>
</div>
</div>
<div class="form-group ask_for_redirect">
<label class="control-label col-sm-3" for="is_homepage">Redirect old url</label>
<div class="col-sm-2">
<a>
<label class="o_switch" for="create_redirect" >
<input type="checkbox" id="create_redirect"/>
<span/>
</label>
</a>
</div>
<div class="col-sm-7 mt4">
<span class="text-muted" id="dependencies_redirect"></span>
</div>
</div>
<div class="form-group ask_for_redirect">
<label class="control-label col-sm-3 redirect_type" for="redirect_type">Type</label>
<div class="col-sm-6 redirect_type">
<select class="form-control" id="redirect_type">
<option t-foreach="widget.fields.type.selection" t-as="field" t-att-value="field[0]"><t t-esc="field[0] + ': ' + field[1]" /></option>
</select>
</div>
</div>
</div>
<div id="advances_page_info" class="tab-pane fade">
<div class="form-group">
<label class="control-label col-sm-3" for="is_menu">Show in top menu</label>
<div class="col-sm-2">
<a>
<label class="o_switch" for="is_menu" >
<input type="checkbox" t-att-checked="widget.page.menu_ids.length > 0 ? true : undefined" id="is_menu"/>
<span/>
</label>
</a>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-3" for="is_homepage">Use as homepage</label>
<div class="col-sm-2">
<a>
<label class="o_switch" for="is_homepage" >
<input type="checkbox" t-att-checked="widget.page.is_homepage ? true : undefined" id="is_homepage"/>
<span/>
</label>
</a>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-3" for="is_indexed">
Indexed
<i class="fa fa-question-circle-o" title="Hide this page from search results" aria-hidden="true"></i>
</label>
<div class="col-sm-2">
<a>
<label class="o_switch" for="is_indexed" >
<input type="checkbox" t-att-checked="widget.page.website_indexed ? true : undefined" id="is_indexed"/>
<span/>
</label>
</a>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-3" for="is_published">Publish</label>
<div class="col-sm-2">
<a>
<label class="o_switch js_publish_btn" for="is_published" >
<input type="checkbox" t-att-checked="widget.page.website_published ? true : undefined" id="is_published"/>
<span/>
</label>
</a>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-3" for="date_publish">Publishing Date</label>
<div class="col-sm-9">
<div class='input-group date' id='date_publish_container'>
<input type='text' class="form-control" name="date_publish" id="date_publish" />
<span class="input-group-addon">
<span class="fa fa-calendar"></span>
</span>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</templates>
@@ -11,7 +11,7 @@
<div class="col-md-6">
<div class="form-horizontal mt16" role="form">
<div class="form-group">
<label for="seo_page_title" class="col-xs-3 control-label">Page title</label>
<label for="seo_page_title" class="col-xs-3 control-label">Page Title</label>
<div class="col-xs-9">
<input type="text" name="seo_page_title" class="form-control" maxlength="70" size="70"/>
</div>
@@ -110,7 +110,7 @@
<div class="o_setting_right_pane">
<label for="has_google_maps"/>
<div class="text-muted">
Show your address on a map in the <a href="/page/contactus">Contact Us</a> page
Show your address on a map in the <a href="/contactus">Contact Us</a> page
</div>
<div class="content-group" attrs="{'invisible': [('has_google_maps', '=', False)]}">
<div class="row mt16">
+7 -7
View File
@@ -27,7 +27,7 @@
<div class="carousel-content col-md-4 col-md-offset-1 mt128 mb96">
<h2>Your Slide Title</h2>
<h4>Click to customize this text</h4>
<p><a href="/page/website.contactus" class="btn btn-warning btn-large">Contact us</a></p>
<p><a href="/contactus" class="btn btn-warning btn-large">Contact us</a></p>
</div>
<div class="carousel-img col-md-6 col-md-offset-1 hidden-sm hidden-xs mt48">
<img class="img-responsive" src="/web/image/website.s_banner_default_image" alt="Slider Odoo Image"/>
@@ -94,7 +94,7 @@
<div class="mb32 col-md-offset-1 col-md-8">
<h1>Sell Online. Easily.</h1>
<p>Write one sentence to convince visitor about your message.</p>
<a class="btn btn-warning btn-lg" href="/page/website.contactus">Contact us</a>
<a class="btn btn-warning btn-lg" href="/contactus">Contact us</a>
</div>
</div>
</div>
@@ -185,7 +185,7 @@
reinforce your image.
</p>
<p class="text-center">
<a href="/page/website.contactus">Contact us »</a>
<a href="/contactus">Contact us »</a>
</p>
</div>
</div>
@@ -312,7 +312,7 @@
<p class="text-muted">
<i>Instant setup, satisfied or reimbursed.</i>
</p>
<a href="/page/website.contactus" class="btn btn-primary btn-lg">Order now</a>
<a href="/contactus" class="btn btn-primary btn-lg">Order now</a>
</div>
</div>
</div>
@@ -334,7 +334,7 @@
</ul>
<div class="panel-footer text-center">
<p class="text-muted"><i>Instant setup, satisfied or reimbursed.</i></p>
<a href="/page/website.contactus" class="btn btn-primary btn-lg">Order now</a>
<a href="/contactus" class="btn btn-primary btn-lg">Order now</a>
</div>
</div>
</div>
@@ -356,7 +356,7 @@
</ul>
<div class="panel-footer text-center">
<p class="text-muted"><i>Instant setup, satisfied or reimbursed.</i></p>
<a href="/page/website.contactus" class="btn btn-primary btn-lg">Contact us</a>
<a href="/contactus" class="btn btn-primary btn-lg">Contact us</a>
</div>
</div>
</div>
@@ -449,7 +449,7 @@
</p>
</div>
<div class="col-md-3">
<a href="/page/website.contactus" class="btn btn-primary btn-lg pull-right mt8">
<a href="/contactus" class="btn btn-primary btn-lg pull-right mt8">
<span>Contact Us Now</span>
<i class="fa fa-chevron-right"></i>
</a>
@@ -33,12 +33,10 @@
<li t-if="editable" class="dropdown" id="content-menu">
<a id="content-menu-button" class="dropdown-toggle waves" data-toggle="dropdown" href="#">Content</a>
<ul class="dropdown-menu" role="menu">
<li>
<a data-action="edit_menu" href="#" title="Edit Top Menu">Edit Menu</a>
</li>
<li><a data-action="edit_menu" href="#" title="Edit Top Menu">Edit Menu</a></li>
<li><a href="/website/pages" title="Manage Your Website Pages">Manage Pages</a></li>
<li t-if="deletable" role="separator" class="divider"></li>
<li t-if="deletable"><a href="#" data-action="rename_page">Rename Page</a></li>
<li t-if="deletable"><a href="#" data-action="delete_page">Delete Page</a></li>
<li t-if="deletable"><a href="#" data-action="page_properties">Page Properties</a></li>
</ul>
</li>
<!-- Customize -->
@@ -61,6 +59,16 @@
</ul>
<ul class="o_menu_systray hidden-xs" groups="website.group_website_publisher">
<li t-if="'website_published' in main_object.fields_get()" t-attf-class="js_publish_management #{main_object.website_published and 'css_published' or 'css_unpublished'}" t-att-data-id="main_object.id" t-att-data-object="main_object._name" t-att-data-controller="publish_controller">
<a>
<label class="o_switch js_publish_btn" for="id" >
<input type="checkbox" t-att-checked="main_object.website_published" id="id"/>
<span/>
<span class="css_publish">Unpublished</span>
<span class="css_unpublish">Published</span>
</label>
</a>
</li>
<!-- Mobile preview -->
<li class="o_mobile_preview" id="mobile-menu">
<a data-action="show-mobile-preview" href="#"><span title="Mobile preview" class="fa fa-mobile"/></a>
@@ -77,6 +85,18 @@
<li t-if="not translatable" id="edit-page-menu">
<a data-action="edit" href="#"><span class="fa fa-pencil"/>Edit</a>
</li>
<li t-if="'website_published' in main_object.fields_get()">
<a class="btn btn-primary btn-xs dropdown-toggle css_edit_dynamic" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</a>
<ul class="dropdown-menu" role="menu">
<li id="edit-in-backend">
<a style="text-align: left;" t-attf-href="/web#return_label=Website&amp;view_type=form&amp;model=#{main_object._name}&amp;id=#{main_object.id}&amp;action=#{action}"
class="btn btn-link btn-xs" title='Edit in backend'>Edit in backend</a>
</li>
</ul>
</li>
<li t-if="translatable">
<a data-action="translate" href="#">TRANSLATE</a>
</li>
+85 -87
View File
@@ -68,15 +68,15 @@
<!-- Layout -->
<template id="submenu" name="Submenu">
<li t-if="not submenu.child_id" t-att-class="
'active' if submenu.url and submenu.url != '/' and unslug_url(request.httprequest.path) == unslug_url(submenu.url.replace('/page/website.', '/page/')) else None
'active' if submenu.clean_url() and unslug_url(request.httprequest.path) == unslug_url(submenu.clean_url()) else None
">
<a t-att-href="(website.menu_id.child_id[0] == submenu) and '/' or submenu.url" t-ignore="true" t-att-target="'_blank' if submenu.new_window else None">
<a t-att-href="submenu.clean_url()" t-ignore="true" t-att-target="'_blank' if submenu.new_window else None">
<span t-field="submenu.name"/>
</a>
</li>
<li t-if="submenu.child_id" t-attf-class="dropdown #{
(submenu.url and submenu.url != '/' and any([request.httprequest.path == child.url.replace('/page/website.', '/page/') for child in submenu.child_id if child.url]) or
(submenu.url and request.httprequest.path == submenu.url.replace('/page/website.', '/page/'))) and 'active'
(submenu.clean_url() and submenu.clean_url() != '/' and any([request.httprequest.path == child.url for child in submenu.child_id if child.url]) or
(submenu.clean_url() and request.httprequest.path == submenu.clean_url())) and 'active'
}">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<span t-field="submenu.name"/> <span class="caret" t-ignore="true"></span>
@@ -299,7 +299,7 @@
<div class="col-md-4 col-lg-3" id="info">
<h4>Connect with us</h4>
<ul class="list-unstyled">
<li><a href="/page/website.contactus">Contact us</a></li>
<li><a href="/contactus">Contact us</a></li>
</ul>
<ul class="list-unstyled">
<li t-ignore="true"><i class="fa fa-phone"></i> <span t-field="res_company.phone"></span></li>
@@ -317,7 +317,7 @@
<div class="col-md-4 col-lg-5 col-lg-offset-1">
<h4>
<span t-field="res_company.name" />
<small> - <a href="/page/website.aboutus">About us</a></small>
<small> - <a href="/aboutus">About us</a></small>
</h4>
<div>
<p>
@@ -628,7 +628,7 @@
<p>Maybe you were looking for one of these popular pages ?</p>
<ul>
<li><a href="/">Homepage</a></li>
<li><a href="/page/website.contactus/">Contact Us</a></li>
<li><a href="/contactus">Contact Us</a></li>
</ul>
</div>
<t t-if="editable or request.debug">
@@ -653,7 +653,7 @@
<p>Maybe you were looking for one of these popular pages ?</p>
<ul>
<li><a href="/">Homepage</a></li>
<li><a href="/page/website.contactus/">Contact Us</a></li>
<li><a href="/contactus">Contact Us</a></li>
</ul>
</div>
</div>
@@ -801,14 +801,6 @@ Sitemap: <t t-esc="url_root"/>sitemap.xml
</sitemapindex>
</template>
<!-- Actual pages -->
<template id="homepage" name="Homepage" page="True" priority="29">
<t t-call="website.layout">
<t t-set="pageName" t-value="'homepage'"/>
<div id="wrap" class="oe_structure oe_empty"/>
</t>
</template>
<template id="company_description" name="Company Description">
<address itemscope="itemscope" itemtype="http://schema.org/Organization">
<!-- TODO widget contact must add itemprop attributes -->
@@ -826,77 +818,6 @@ Sitemap: <t t-esc="url_root"/>sitemap.xml
</t>
</template>
<template id="contactus" name="Contact us" page="True">
<t t-call="website.layout">
<div id="wrap">
<div class="oe_structure"/>
<div class="container">
<h1>Contact us</h1>
<div class="row">
<div class="col-md-8">
<div class="oe_structure">
<div>
<p>Contact us about anything related to our company or services.</p>
<p>We'll do our best to get back to you as soon as possible.</p>
</div>
</div>
<div class="text-center mt64" name="mail_button">
<a t-attf-href="mailto:{{ res_company.email }}" class="btn btn-primary" id="o_contact_mail">Send us an email</a>
</div>
</div>
<div class="col-md-4 mb32">
<t t-call="website.company_description"/>
</div>
</div>
</div>
<div class="oe_structure"/>
</div>
</t>
</template>
<template id="aboutus" name="About us" page="True">
<t t-call="website.layout">
<div id="wrap">
<div class="oe_structure">
<section>
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="text-center">About us</h1>
<h3 class="text-muted text-center">Great products for great people</h3>
</div>
</div>
</div>
</section>
<section>
<div class="container">
<div class="row">
<div class="col-md-6 mt32">
<p>
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.
</p>
<p>
Our products are designed for small to medium size companies willing to optimize
their performance.
</p>
</div>
<div class="col-md-4 col-md-offset-2 mt16 mb16">
<img src="/website/static/src/img/library/business_conference.jpg" class="img img-responsive shadow" alt="Our Team"/>
</div>
</div>
</div>
</section>
</div>
<div class="oe_structure"></div>
</div>
</t>
</template>
<template id="website_search_box" name="Website Searchbox">
<div class="input-group">
<div class="oe_search">
@@ -907,4 +828,81 @@ Sitemap: <t t-esc="url_root"/>sitemap.xml
</span>
</div>
</template>
<template id="index_management">
<t groups="website.group_website_publisher" t-ignore="true">
<div t-attf-class="pull-right js_index_management #{object.website_indexed and 'css_published' or 'css_unpublished'}" t-att-data-id="object.id" t-att-data-object="object._name">
<button class="btn btn-danger js_index_btn">Unindexed</button>
<button class="btn btn-success js_index_btn">Indexed</button>
</div>
</t>
</template>
<template id="edit_website_pages" name="Website Pages Management">
<t t-call="website.layout">
<div id="wrap">
<div class="container" id="edit_website_pages">
<form class="mt8 pull-right" role="search" t-attf-action="/website/pages_management" method="get">
<t t-call="website.website_search_box"/>
</form>
<div t-if="searchbar_sortings" class="dropdown pull-right mt8 mr8">
<button class="btn btn-default" type="button" data-toggle="dropdown">
<span class="fa fa-sort fa-lg" />
<span class='hidden-xs hidden-sm hidden-md' t-esc="searchbar_sortings[sortby].get('label', 'Newest')"/>
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="portal_searchbar_sortby">
<li t-foreach="searchbar_sortings" t-as="option" t-attf-class="#{sortby == option and 'active'}">
<a t-att-href="request.httprequest.path + '?' + keep_query('*', sortby=option)">
<span t-esc="searchbar_sortings[option].get('label')"/>
</a>
</li>
</ul>
</div>
<h3 class="mt16">Pages Management</h3>
<t t-if="not pages">
<div t-if="search" class="alert alert-warning mt8" role="alert">
Your search '<t t-esc="search" />' did not match any pages.
</div>
<div t-else="" class="alert alert-warning mt8" role="alert">
There are currently no pages for your website.
</div>
</t>
<div t-if="pages" class="table-responsive">
<table class="table table-hover o_my_status_table">
<thead>
<tr class="active">
<th>Name</th>
<th>Url</th>
<th class="col-md-1 text-center"><i title="Is the page included in the main menu?" class="fa fa-thumb-tack" aria-hidden="true"></i></th>
<th class="col-md-1 text-center"><i title="Is the page published?" class="fa fa-eye" aria-hidden="true"></i></th>
<th class="col-md-1 text-center"><i title="Is the page indexed by search engines?" class="fa fa-globe" aria-hidden="true"></i></th>
<th class="col-md-1"></th>
</tr>
</thead>
<t t-foreach="pages" t-as="page">
<tr>
<td><i t-if="page.is_homepage" class="fa fa-home"></i> <span style="word-break: break-all;" t-esc="page.name"/></td>
<td><a style="word-break: break-all;" t-attf-href="{{page.url}}"><t t-esc="page.url"/></a></td>
<td style="text-align:center"><i t-att-class="'fa fa-check' if page.menu_ids else 'fa fa-times text-muted'" aria-hidden="true"></i></td>
<t t-set='date_formatted'><t t-options='{"widget": "date"}' t-esc="page.date_publish"/></t>
<td style="text-align:center"><i t-att-title="not page.is_visible and page.website_published and 'This page will be visible on ' + date_formatted" t-att-class="'fa fa-check' if page.is_visible and page.website_published else 'fa fa-eye-slash' if not page.is_visible and page.website_published else 'fa fa-times text-muted'" aria-hidden="true"></i></td>
<td style="text-align:center"><i t-att-class="'fa fa-check' if page.website_indexed else 'fa fa-times text-muted'" aria-hidden="true"></i></td>
<td>
<a class="fa fa-cog js_page_properties" t-att-data-id="page.id" href="#" title="Manage this page"></a>
<a class="fa fa-clone js_clone_page" t-att-data-id="page.id" href="#" title="Clone this page"></a>
<a class="fa fa-trash js_delete_page" t-att-data-id="page.id" href="#" title="Delete this page"></a>
</td>
</tr>
</t>
</table>
</div>
<div t-if="pager" class="o_portal_pager text-center">
<t t-call="website.pager"/>
</div>
</div>
</div>
</t>
</template>
</odoo>
+111
View File
@@ -61,6 +61,117 @@
</field>
</record>
<!-- website.redirect views -->
<record id="website_redirect_form_view" model="ir.ui.view">
<field name="name">website.redirect.form</field>
<field name="model">website.redirect</field>
<field name="arch" type="xml">
<form string="Website Redirect Settings">
<sheet>
<group>
<group>
<field name="type"/>
<field name="url_from"/>
<field name="url_to"/>
</group>
<group>
<field name="website_id"/>
<field name="active"/>
<field name="sequence"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="website_redirect_tree_view" model="ir.ui.view">
<field name="name">website.redirect.list</field>
<field name="model">website.redirect</field>
<field name="arch" type="xml">
<tree string="Website Redirects">
<field name="type"/>
<field name="url_from"/>
<field name="url_to"/>
<field name="website_id"/>
<field name="active"/>
<field name="sequence"/>
</tree>
</field>
</record>
<record id="action_website_redirect_list" model="ir.actions.act_window">
<field name="name">Website Redirects</field>
<field name="res_model">website.redirect</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="website_redirect_tree_view"/>
<field name="target">current</field>
</record>
<menuitem name="Redirects"
id="menu_website_redirect_list"
action="action_website_redirect_list"
parent="website.menu_website_configuration"
groups="base.group_system"/>
<!-- website.page views -->
<record id="website_pages_form_view" model="ir.ui.view">
<field name="name">website.page.form</field>
<field name="model">website.page</field>
<field name="arch" type="xml">
<form string="Website Page Settings">
<sheet>
<group>
<group>
<field name="name"/>
<field name="url"/>
<field name="ir_ui_view_id"/>
</group>
<group>
<field name="website_indexed"/>
<field name="website_published"/>
<field name="date_publish"/>
</group>
</group>
<field name="website_ids"/>
<field name="menu_ids"/>
</sheet>
</form>
</field>
</record>
<record id="website_pages_tree_view" model="ir.ui.view">
<field name="name">website.page.list</field>
<field name="model">website.page</field>
<field name="arch" type="xml">
<tree string="Website Pages">
<field name="name"/>
<field name="url"/>
<field name="website_indexed"/>
<field name="website_published"/>
<field name="create_uid" invisible="1"/>
<field name="write_uid"/>
<field name="write_date"/>
</tree>
</field>
</record>
<record id="action_website_pages_list" model="ir.actions.act_window">
<field name="name">Website Pages</field>
<field name="res_model">website.page</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="website_pages_tree_view"/>
<field name="target">current</field>
</record>
<menuitem name="Pages"
id="menu_website_pages_list"
action="action_website_pages_list"
parent="website.menu_website_configuration"
groups="base.group_system"/>
<!-- website.menu views -->
<record id="action_website_menu" model="ir.actions.act_window">
<field name="name">Website Menu</field>
+5 -6
View File
@@ -8,15 +8,14 @@ class Website(models.Model):
_inherit = "website"
@api.model
def page_search_dependencies(self, view_id):
dep = super(Website, self).page_search_dependencies(view_id)
def page_search_dependencies(self, page_id=False):
dep = super(Website, self).page_search_dependencies(page_id=page_id)
view = self.env['ir.ui.view'].browse(view_id)
name = view.key.replace("website.", "")
fullname = "website.%s" % name
page = self.env['website.page'].browse(int(page_id))
path = page.url
dom = [
'|', ('content', 'ilike', '/page/%s' % name), ('content', 'ilike', '/page/%s' % fullname)
('content', 'ilike', path)
]
posts = self.env['blog.post'].search(dom)
if posts:
@@ -63,13 +63,13 @@ odoo.define("website_blog.tour", function (require) {
content: _t("Once you have reviewed the content on mobile, close the preview."),
position: "right",
}, {
trigger: "button.btn-danger.js_publish_btn",
trigger: ".js_publish_management .js_publish_btn",
extra_trigger: "body:not(.editor_enable)",
position: "bottom",
content: _t("<b>Publish your blog post</b> to make it visible to your visitors."),
}, {
trigger: "#customize-menu > a",
extra_trigger: "body:not(.editor_enable) .js_publish_management button.js_publish_btn.btn-success:visible",
extra_trigger: ".js_publish_management .js_publish_btn .css_unpublish:visible",
content: _t("<b>That's it, your blog post is published!</b> Discover more features through the <i>Customize</i> menu."),
position: "bottom",
width: 500,
@@ -217,22 +217,20 @@
</template>
<!-- Blog Post Complete -->
<template id="blog_edit_options" inherit_id="website.user_navbar" name="Edit Blog Options">
<xpath expr="//li[@id='edit-in-backend']" position="after">
<li groups="website.group_website_designer" t-if="main_object._name == 'blog.post'">
<form class="duplicate hidden" action="/blog/post_duplicate" method="POST">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input name="blog_post_id" t-att-value="blog_post.id if blog_post else None"/>
</form>
<a href="#" class="duplicate" onclick="$(this).prev('form').submit()">Duplicate</a>
</li>
</xpath>
</template>
<template id="blog_post_complete" name="Blog Post">
<t t-call="website_blog.index">
<div id="title" class="blog_header" t-ignore="True">
<div class="container" style="position: relative;">
<t t-call="website.publish_management">
<t t-set="object" t-value="blog_post"/>
<t t-set="publish_edit" t-value="True"/>
<li>
<form class="duplicate hidden" action="/blog/post_duplicate" method="POST">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input name="blog_post_id" t-att-value="blog_post.id"/>
</form>
<a href="#" class="duplicate" onclick="$(this).prev('form').submit()">Duplicate</a>
</li>
</t>
</div>
<t t-call="website_blog.blog_cover">
<t t-set="cover_properties" t-value="blog_post_cover_properties"/>
+1 -1
View File
@@ -22,7 +22,7 @@
<record id="action_open_website" model="ir.actions.act_url">
<field name="name">Website Contact Form</field>
<field name="target">self</field>
<field name="url">/page/contactus</field>
<field name="url">/contactus</field>
</record>
<record id="base.open_menu" model="ir.actions.todo">
@@ -6,7 +6,7 @@ odoo.define('website_crm.tour', function(require) {
tour.register('website_crm_tour', {
test: true,
url: '/page/contactus',
url: '/contactus',
wait_for: base.ready(),
}, [{
content: "Complete name",
@@ -12,7 +12,7 @@
<div class="o_setting_right_pane">
<label for="id" string="Contact Form"/>
<div class="text-muted" id="msg_contact_form_install_setting">
Add a contact form in the <a href="/page/website.contactus">Contact Us</a> page
Add a contact form in the <a href="/contactus">Contact Us</a> page
</div>
<div class="row">
<label class="col-md-3 o_light_label" string="Sales Channel" for="crm_default_team_id"/>
@@ -3,7 +3,7 @@
<template id="contactus_form" name="Contact Form" inherit_id="website.contactus" customize_show="True">
<xpath expr="//div[@name='mail_button']" position="replace">
<div>
<form action="/website_form/" method="post" data-model_name="crm.lead" data-success_page="/page/website_crm.contactus_thanks" class="s_website_form form-horizontal container-fluid mt32" enctype="multipart/form-data" data-editable-form="false">
<form action="/website_form/" method="post" data-model_name="crm.lead" data-success_page="/contactus-thank-you" class="s_website_form form-horizontal container-fluid mt32" enctype="multipart/form-data" data-editable-form="false">
<div class="form-group form-field o_website_form_required_custom">
<label class="col-md-3 col-sm-4 control-label" for="contact_name">Your Name</label>
<div class="col-md-7 col-sm-8">
@@ -50,38 +50,50 @@
</div>
</xpath>
</template>
<template id="contactus_thanks" name="Contact us">
<t t-call="website.layout">
<div id="wrap">
<div class="oe_structure"/>
<div class="container">
<h1>Thanks!</h1>
<div class="row">
<div class="col-md-8">
<div class="alert alert-success">
Your message has been sent successfully.
<button type="button" class="close" data-dismiss="alert">&amp;times;</button>
<record id="contactus_thanks" model="ir.ui.view">
<field name="name">Thanks (Contact us)</field>
<field name="type">qweb</field>
<field name="key">website_crm.contactus_thanks</field>
<field name="arch" type="xml">
<t name="Thanks (Contact us)" t-name="website_crm.contactus_thanks">
<t t-call="website.layout">
<div id="wrap">
<div class="oe_structure"/>
<div class="container">
<h1>Thanks!</h1>
<div class="row">
<div class="col-md-8">
<div class="alert alert-success">
Your message has been sent successfully.
<button type="button" class="close" data-dismiss="alert">&amp;times;</button>
</div>
<p>
We will get back to you shortly.
</p><p class="mt64">
If you have an emergency, do not hesitate to contact us by phone:
</p>
<ul class="list-unstyled">
<li><i class="fa fa-phone"></i> : <span t-field="res_company.phone"/></li>
<li><i class="fa fa-envelope"></i> : <span t-field="res_company.email"/></li>
</ul>
</div>
<div class="col-md-4">
<t t-call="website.company_description"/>
</div>
<p>
We will get back to you shortly.
</p><p class="mt64">
If you have an emergency, do not hesitate to contact us by phone:
</p>
<ul class="list-unstyled">
<li><i class="fa fa-phone"></i> : <span t-field="res_company.phone"/></li>
<li><i class="fa fa-envelope"></i> : <span t-field="res_company.email"/></li>
</ul>
</div>
<div class="col-md-4">
<t t-call="website.company_description"/>
</div>
</div>
</div>
<div class="oe_structure"/>
</div>
<div class="oe_structure"/>
</div>
</t>
</t>
</template>
</field>
</record>
<record id="contactus_thanks_page" model="website.page">
<field name="name">Thanks (Contact us)</field>
<field name="url">/contactus-thank-you</field>
<field name="website_published">True</field>
<field name="ir_ui_view_id" ref="contactus_thanks" />
</record>
<template id="website_crm_tests" name="Website CRM Tests" inherit_id="web.assets_common">
<xpath expr="." position="inside">
+2 -2
View File
@@ -85,8 +85,8 @@ class Event(models.Model):
for sequence, (name, url, xml_id) in enumerate(self._get_menu_entries()):
if name not in existing_page_names:
if not url:
newpath = self.env['website'].new_page(name + ' ' + self.name, xml_id, ispage=False)
url = "/event/" + slug(self) + "/page/" + newpath
newpath = self.env['website'].new_page(name + ' ' + self.name, template=xml_id, ispage=False)
url = "/event/" + slug(self) + "/page/" + newpath[1:]
self.env['website.menu'].create({
'name': name,
'url': url,
@@ -33,14 +33,14 @@ odoo.define("website_event.tour", function (require) {
content: _t("Once you click on save, your event is updated."),
position: "bottom",
}, {
trigger: "button.btn-danger.js_publish_btn",
trigger: ".js_publish_management .js_publish_btn",
extra_trigger: "body:not(.editor_enable)",
content: _t("Click to publish your event."),
position: "top",
}, {
trigger: ".js_publish_management button[data-toggle=\"dropdown\"]",
extra_trigger: ".js_publish_management button.js_publish_btn.btn-success:visible",
trigger: ".css_edit_dynamic",
extra_trigger: ".js_publish_management .js_publish_btn .css_unpublish:visible",
content: _t("Click here to customize your event further."),
position: "left",
position: "bottom",
}]);
});
@@ -202,6 +202,11 @@
</xpath>
</template>
<template id="event_edit_options" inherit_id="website.user_navbar" name="Edit Event Options">
<xpath expr="//li[@id='edit-page-menu']" position="after">
<t t-if="main_object._name == 'event.event'" t-set="action" t-value="'event.action_event_view'" />
</xpath>
</template>
<template id="layout">
<t t-call="website.layout">
<div id="wrap" class="js_event o_website_event">
@@ -226,13 +231,6 @@
</t>
</ol>
</div>
<div class="col-sm-5" groups="event.group_event_manager">
<t t-call="website.publish_management">
<t t-set="object" t-value="event"/>
<t t-set="publish_edit" t-value="True"/>
<t t-set="action" t-value="'event.action_event_view'"/>
</t>
</div>
</div>
</div>
<t t-raw="0"/>
@@ -173,7 +173,11 @@
</template>
<template id="track_edit_options" inherit_id="website.user_navbar" name="Edit Track Options">
<xpath expr="//li[@id='edit-page-menu']" position="after">
<t t-if="main_object._name == 'event.track'" t-set="action" t-value="'website_event_track.action_event_track'" />
</xpath>
</template>
<template id="track_view">
<t t-call="website.layout">
<div id="wrap" class="js_event">
@@ -186,11 +190,6 @@
</t>
</ol>
</div>
<t t-call="website.publish_management">
<t t-set="object" t-value="track"/>
<t t-set="publish_edit" t-value="True"/>
<t t-set="action" t-value="'website_event_track.action_event_track'"/>
</t>
</div>
</div>
</div>
@@ -1382,10 +1382,6 @@
<h1 class="mt0 page-header">
<span t-field="user.name"/>
<small>profile</small>
<t t-call="website.publish_management">
<t t-set="object" t-value="user"/>
<t t-set="publish_edit" t-value="True"/>
</t>
</h1>
<div class="row">
<div class="col-sm-2">
-1
View File
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
from . import models
@@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main
-15
View File
@@ -1,15 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import http
from odoo.http import request
class WebsiteHr(http.Controller):
@http.route(['/page/website.aboutus', '/page/aboutus'], type='http', auth="public", website=True)
def blog(self, **post):
employees_domain = []
if not request.env['res.users'].has_group('website.group_website_publisher'):
employees_domain += [('website_published', '=', True)]
employees = request.env['hr.employee'].search(employees_domain)
return request.render("website.aboutus", {'employees': employees})
+1 -1
View File
@@ -5,7 +5,7 @@
<record id="action_open_website" model="ir.actions.act_url">
<field name="name">Website About</field>
<field name="target">self</field>
<field name="url">/page/aboutus</field>
<field name="url">/aboutus</field>
</record>
<record id="base.open_menu" model="ir.actions.todo">
<field name="action_id" ref="action_open_website"/>
+1 -1
View File
@@ -13,4 +13,4 @@ class HrEmployee(models.Model):
def _compute_website_url(self):
super(HrEmployee, self)._compute_website_url()
for employee in self:
employee.website_url = '/page/website.aboutus#team'
employee.website_url = '/aboutus#team'
@@ -10,6 +10,9 @@
<template id="aboutus" inherit_id="website.aboutus" customize_show="True" name="Our Team">
<xpath expr="//div[hasclass('oe_structure')]" position="after">
<section class="container" id="team">
<t t-set="is_publisher" t-value="request.env['res.users'].has_group('website.group_website_publisher')" />
<t t-set="employees_domain" t-value="is_publisher and [] or [('website_published', '=', True)]" />
<t t-set="employees" t-value="request.env['hr.employee'].search(employees_domain)" />
<div class="col-sm-12 text-center" t-if="len(employees)">
<h2>Our Team</h2>
</div>
@@ -38,7 +38,7 @@
<div class="col-md-12" id="jobs_grid">
<div t-if="not jobs">
<div class="text-center text-muted">
<h3 class="css_editable_hidden"><a t-attf-href="/page/website.contactus">Contact us</a> for job opportunities.</h3>
<h3 class="css_editable_hidden"><a t-attf-href="/contactus">Contact us</a> for job opportunities.</h3>
<h4 groups="hr_recruitment.group_hr_recruitment_manager">
Create new job pages from the <strong><i>+New</i></strong> top-right button.
</h4>
@@ -86,11 +86,6 @@
<div class="container">
<div class="pull-right">
<a t-attf-href="/jobs/apply/#{job.id}" class="btn btn-primary btn-lg pull-right mt32 mb4">Apply Now!</a>
<br/>
<t t-call="website.publish_management">
<t t-set="object" t-value="job"/>
<t t-set="publish_edit" t-value="True"/>
</t>
</div>
<label class="mb0 mt16"><a href="/jobs">Jobs</a></label> /
<h1 class="mb0 mt0" t-field="job.name"/>
@@ -129,7 +124,7 @@
<div class="row">
<section id="forms">
<form action="/website_form/" method="post" class="s_website_form form-horizontal container-fluid mt32" enctype="multipart/form-data" data-model_name="hr.applicant" data-success_page="/page/website_hr_recruitment.thankyou" t-att-data-form_field_department_id="job and job.department_id.id or False" t-att-data-form_field_job_id="job and job.id or False">
<form action="/website_form/" method="post" class="s_website_form form-horizontal container-fluid mt32" enctype="multipart/form-data" data-model_name="hr.applicant" data-success_page="/job-thank-you" t-att-data-form_field_department_id="job and job.department_id.id or False" t-att-data-form_field_job_id="job and job.id or False">
<div class="form-group form-field o_website_form_required_custom">
<div class="col-md-3 col-sm-4 text-right">
<label class="control-label" for="partner_name">Your Name</label>
@@ -390,59 +385,72 @@
</section>
</template>
<template id="thankyou">
<t t-call="website.layout">
<div id="wrap">
<div class="oe_structure">
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="text-center">Congratulations!</h1>
<p class="text-center">
Your application has been posted successfully.
</p>
</div>
<t t-if="request.session.get('form_builder_model_model', '') == 'hr.applicant'">
<t t-set="job" t-value="request.website._website_form_last_record().sudo().job_id"/>
<t t-set="responsible" t-value="job and job.user_id.website_published and job.user_id "/>
</t>
<t t-if="responsible">
<div class="col-md-12">
<h3 class="mb32 text-center">Your application has been sent to:</h3>
</div>
<div class="col-md-1 col-md-offset-4">
<p t-field="responsible.image_small" t-field-options="{'widget': 'image', 'class': 'img-circle center-block'}"/>
</div>
<div class="col-md-5 o_responsible_data">
<h4 class="mt0" t-field="responsible.name"/>
<p t-field="responsible.function"/>
<t t-if='responsible.email'>
<i class="fa fa-envelope"></i> <a t-attf-href="mailto:#{responsible.email}" t-esc="responsible.email"/>
<record id="thankyou" model="ir.ui.view">
<field name="name">Thank you (Recruitment)</field>
<field name="type">qweb</field>
<field name="key">website_hr_recruitment.thankyou</field>
<field name="arch" type="xml">
<t name="Thank you (Recruitment)" t-name="website_hr_recruitment.thankyou">
<t t-call="website.layout">
<div id="wrap">
<div class="oe_structure">
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="text-center">Congratulations!</h1>
<p class="text-center">
Your application has been posted successfully.
</p>
</div>
<t t-if="request.session.get('form_builder_model_model', '') == 'hr.applicant'">
<t t-set="job" t-value="request.website._website_form_last_record().sudo().job_id"/>
<t t-set="responsible" t-value="job and job.user_id.website_published and job.user_id "/>
</t>
<t t-if='responsible.phone'>
<br/><i class="fa fa-phone"></i> <span t-field="responsible.phone"/>
<t t-if="responsible">
<div class="col-md-12">
<h3 class="mb32 text-center">Your application has been sent to:</h3>
</div>
<div class="col-md-1 col-md-offset-4">
<p t-field="responsible.image_small" t-field-options="{'widget': 'image', 'class': 'img-circle center-block'}"/>
</div>
<div class="col-md-5 o_responsible_data">
<h4 class="mt0" t-field="responsible.name"/>
<p t-field="responsible.function"/>
<t t-if='responsible.email'>
<i class="fa fa-envelope"></i> <a t-attf-href="mailto:#{responsible.email}" t-esc="responsible.email"/>
</t>
<t t-if='responsible.phone'>
<br/><i class="fa fa-phone"></i> <span t-field="responsible.phone"/>
</t>
</div>
<div class="col-md-12 mt32 text-center">
<span>
We usually reply between one and three days.<br/>
Feel free to contact him/her if you have further questions.
</span>
</div>
</t>
</div>
<div class="col-md-12 mt32 text-center">
<span>
We usually reply between one and three days.<br/>
Feel free to contact him/her if you have further questions.
</span>
<div class="row" id="o_recruitment_thank_cta">
<div class="col-md-12 text-center mt32 mb32">
In the meantime,
<h3 class="mt8 mb32">Look around on our website:</h3>
<a href="/" class="btn btn-primary btn-lg">Continue To Our Website</a>
</div>
</div>
</t>
</div>
<div class="row" id="o_recruitment_thank_cta">
<div class="col-md-12 text-center mt32 mb32">
In the meantime,
<h3 class="mt8 mb32">Look around on our website:</h3>
<a href="/" class="btn btn-primary btn-lg">Continue To Our Website</a>
</div>
</div>
</div>
</div>
</div>
</t>
</template>
</t>
</t>
</field>
</record>
<record id="thankyou_page" model="website.page">
<field name="name">Thank you (Recruitment)</field>
<field name="url">/job-thank-you</field>
<field name="website_published">True</field>
<field name="ir_ui_view_id" ref="thankyou" />
</record>
<template id="job_countries" inherit_id="website_hr_recruitment.index" active="False" customize_show="True" name="Filter by Countries">
<xpath expr="//div[@id='jobs_grid_left']" position="inside">
@@ -112,7 +112,7 @@
<template id="s_mail_block_footer_social" inherit_id="mass_mailing.s_mail_block_footer_social">
<xpath expr="//td[hasclass('o_mail_footer_links')]" position="inside">
<t> | <a href="/page/contactus" class="btn btn-link">Contact</a></t>
<t> | <a href="/contactus" class="btn btn-link">Contact</a></t>
</xpath>
<xpath expr="//tr" position="before">
<tr>
@@ -125,7 +125,7 @@
<template id="s_mail_block_footer_social_left" inherit_id="mass_mailing.s_mail_block_footer_social_left">
<xpath expr="//div[hasclass('o_mail_footer_links')]" position="inside">
<t> | <a href="/page/contactus" class="btn btn-link">Contact</a></t>
<t> | <a href="/contactus" class="btn btn-link">Contact</a></t>
</xpath>
<xpath expr="//td" position="after">
<td class="o_mail_footer_social">
@@ -15,10 +15,6 @@
</template>
<template id="partner_detail" name="Partner Details">
<t t-call="website.publish_management">
<t t-set="object" t-value="partner"/>
<t t-set="publish_edit" t-value="True"/>
</t>
<h1 class="col-md-12 text-center" id="partner_name" t-field="partner.display_name"/>
<div class="col-md-4">
<div t-field="partner.image" t-options='{"widget": "image", "class": "center-block mb16"}'/>
@@ -55,7 +55,7 @@ odoo.define("website_sale.tour_shop", function (require) {
content: _t("Once you click on <b>Save</b>, your product is updated."),
position: "bottom",
}, {
trigger: ".js_publish_management button.js_publish_btn.btn-danger",
trigger: ".js_publish_management .js_publish_btn .css_publish",
extra_trigger: "body:not(.editor_enable)",
content: _t("Click on this button so your customers can see it."),
position: "top",
+5 -5
View File
@@ -378,6 +378,11 @@
</template>
<!-- /shop/product product page -->
<template id="product_edit_options" inherit_id="website.user_navbar" name="Edit Product Options">
<xpath expr="//li[@id='edit-page-menu']" position="after">
<t t-if="main_object._name == 'product.template'" t-set="action" t-value="'product.product_template_action'" />
</xpath>
</template>
<template id="product" name="Product">
<t t-call="website.layout">
<t t-set="additional_title" t-value="product.name" />
@@ -401,11 +406,6 @@
<t t-call="website_sale.search" />
</div>
<div id="website_published_button" class="col-sm-3">
<t t-call="website.publish_management" groups="sales_team.group_sale_manager">
<t t-set="object" t-value="product" />
<t t-set="publish_edit" t-value="True" />
<t t-set="action" t-value="'product.product_template_action'" />
</t>
</div>
<div class="col-sm-2 text-right">
<t t-call="website_sale.pricelist_list" />
@@ -368,6 +368,11 @@
</template>
<!-- Slide: main template: detailed view -->
<template id="slide_edit_options" inherit_id="website.user_navbar" name="Edit Slide Options">
<xpath expr="//li[@id='edit-page-menu']" position="after">
<t t-if="main_object._name == 'slide.slide'" t-set="action" t-value="'website_slides.action_slides_slides'" />
</xpath>
</template>
<template id="slide_detail_view" name="Slide Detailed View">
<t t-set="head">
<t t-if="not private">
@@ -411,13 +416,6 @@
<a t-attf-href="/slides/#{slug(slide.channel_id)}/tag/#{slug(slide_tag)}" t-attf-class="label label-default ml4 mb4 pull-right #{tag and tag.id == slide_tag.id and 'label-primary' ''}" t-field="slide_tag.name"/>
</t>
</div>
<div class="pull-right">
<t t-call="website.publish_management">
<t t-set="object" t-value="slide"/>
<t t-set="publish_edit" t-value="True"/>
<t t-set="action" t-value="'website_slides.action_slides_slides'"/>
</t>
</div>
</div>
</div>
</div>
+11 -4
View File
@@ -10,7 +10,6 @@ import mimetypes
import os
import re
import sys
import unicodedata
import werkzeug
import werkzeug.exceptions
@@ -157,6 +156,14 @@ class IrHttp(models.AbstractModel):
response.data = base64.b64decode(datas)
return response
@classmethod
def _serve_fallback(cls, exception):
# serve attachment
attach = cls._serve_attachment()
if attach:
return attach
return False
@classmethod
def _handle_exception(cls, exception):
# If handle_exception returns something different than None, it will be used as a response
@@ -164,9 +171,9 @@ class IrHttp(models.AbstractModel):
# This is done first as the attachment path may
# not match any HTTP controller
if isinstance(exception, werkzeug.exceptions.HTTPException) and exception.code == 404:
attach = cls._serve_attachment()
if attach:
return attach
serve = cls._serve_fallback(exception)
if serve:
return serve
# Don't handle exception but use werkeug debugger if server in --dev mode
if 'werkzeug' in tools.config['dev_mode']:
-2
View File
@@ -702,8 +702,6 @@ form: module.record_id""" % (xml_id,)
if groups:
grp_lst = [("ref('%s')" % x) for x in groups.split(',')]
record.append(Field(name="groups_id", eval="[(6, 0, ["+', '.join(grp_lst)+"])]"))
if el.attrib.pop('page', None) == 'True':
record.append(Field(name="page", eval="True"))
if el.get('primary') == 'True':
# Pseudo clone mode, we'll set the t-name to the full canonical xmlid
el.append(