summaryrefslogtreecommitdiff
path: root/addons/website/models/website.py
diff options
context:
space:
mode:
Diffstat (limited to 'addons/website/models/website.py')
-rw-r--r--addons/website/models/website.py305
1 files changed, 238 insertions, 67 deletions
diff --git a/addons/website/models/website.py b/addons/website/models/website.py
index 94d73420ccf..c0310d53d3d 100644
--- a/addons/website/models/website.py
+++ b/addons/website/models/website.py
@@ -15,6 +15,7 @@ from odoo.addons.website.models.ir_http import sitemap_qs2dom
from odoo.addons.portal.controllers.portal import pager
from odoo.tools import pycompat
from odoo.http import request
+from odoo.osv import expression
from odoo.osv.expression import FALSE_DOMAIN
from odoo.tools.translate import _
@@ -44,26 +45,46 @@ class Website(models.Model):
def_lang = self.env['res.lang'].search([('code', '=', lang_code)], limit=1)
return def_lang.id if def_lang else self._active_languages()[0]
+ def _default_social_twitter(self):
+ return self.env.ref('base.main_company').social_twitter
+
+ def _default_social_facebook(self):
+ return self.env.ref('base.main_company').social_facebook
+
+ def _default_social_github(self):
+ return self.env.ref('base.main_company').social_github
+
+ def _default_social_linkedin(self):
+ return self.env.ref('base.main_company').social_linkedin
+
+ def _default_social_youtube(self):
+ return self.env.ref('base.main_company').social_youtube
+
+ def _default_social_googleplus(self):
+ return self.env.ref('base.main_company').social_googleplus
+
name = fields.Char('Website Name')
domain = fields.Char('Website Domain')
+ country_group_ids = fields.Many2many('res.country.group', 'website_country_group_rel', 'website_id', 'country_group_id',
+ string='Country Groups', help='Used when multiple websites have the same domain.')
company_id = fields.Many2one('res.company', string="Company", default=lambda self: self.env.ref('base.main_company').id)
language_ids = fields.Many2many('res.lang', 'website_lang_rel', 'website_id', 'lang_id', 'Languages', default=_active_languages)
default_lang_id = fields.Many2one('res.lang', string="Default Language", default=_default_language, required=True)
default_lang_code = fields.Char(related='default_lang_id.code', string="Default language code", store=True)
auto_redirect_lang = fields.Boolean('Autoredirect Language', default=True, help="Should users be redirected to their browser's language")
- social_twitter = fields.Char(related="company_id.social_twitter")
- social_facebook = fields.Char(related="company_id.social_facebook")
- social_github = fields.Char(related="company_id.social_github")
- social_linkedin = fields.Char(related="company_id.social_linkedin")
- social_youtube = fields.Char(related="company_id.social_youtube")
- social_googleplus = fields.Char(related="company_id.social_googleplus")
+ social_twitter = fields.Char('Twitter Account', default=_default_social_twitter)
+ social_facebook = fields.Char('Facebook Account', default=_default_social_facebook)
+ social_github = fields.Char('GitHub Account', default=_default_social_github)
+ social_linkedin = fields.Char('LinkedIn Account', default=_default_social_linkedin)
+ social_youtube = fields.Char('Youtube Account', default=_default_social_youtube)
+ social_googleplus = fields.Char('Google+ Account', default=_default_social_googleplus)
google_analytics_key = fields.Char('Google Analytics Key')
google_management_client_id = fields.Char('Google Client ID')
google_management_client_secret = fields.Char('Google Client Secret')
- user_id = fields.Many2one('res.users', string='Public User', required=True, default=lambda self: self.env.ref('base.public_user').id)
+ user_id = fields.Many2one('res.users', string='Public User', required=True)
cdn_activated = fields.Boolean('Activate CDN for assets')
cdn_url = fields.Char('CDN Base URL', default='')
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")
@@ -71,29 +92,73 @@ class Website(models.Model):
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.")
+ installed_theme_id = fields.Many2one('ir.module.module')
+ theme_ids = fields.Many2many('ir.module.module', 'website_theme', 'website_id', 'ir_module_module_id')
+ auth_signup_uninvited = fields.Selection([
+ ('b2b', 'On invitation (B2B)'),
+ ('b2c', 'Free sign up (B2C)'),
+ ], string='Customer Account', default='b2b')
@api.multi
def _compute_menu(self):
Menu = self.env['website.menu']
for website in self:
- website.menu_id = Menu.search([('parent_id', '=', False), ('website_id', '=', website.id)], order='id', limit=1).id
+ website.menu_id = Menu.search([('parent_id', '=', False), ('website_id', 'in', (website.id, False))], order='id', limit=1).id
# cf. Wizard hack in website_views.xml
def noop(self, *args, **kwargs):
pass
+ @api.model
+ def create(self, vals):
+ if 'user_id' not in vals:
+ company = self.env['res.company'].browse(vals.get('company_id'))
+ vals['user_id'] = company._get_public_user().id if company else self.env.ref('base.public_user').id
+
+ res = super(Website, self).create(vals)
+ res._bootstrap_homepage()
+ return res
+
@api.multi
def write(self, values):
self._get_languages.clear_cache(self)
+ if 'company_id' in values and 'user_id' not in values:
+ company = self.env['res.company'].browse(values['company_id'])
+ values['user_id'] = company._get_public_user().id
+
result = super(Website, self).write(values)
if 'cdn_activated' in values or 'cdn_url' in values or 'cdn_filters' in values:
# invalidate the caches from static node at compile time
self.env['ir.qweb'].clear_caches()
return result
+ @api.onchange('company_id')
+ def _onchange_company_id(self):
+ self.social_twitter = self.company_id.social_twitter
+ self.social_facebook = self.company_id.social_facebook
+ self.social_github = self.company_id.social_github
+ self.social_linkedin = self.company_id.social_linkedin
+ self.social_youtube = self.company_id.social_youtube
+ self.social_googleplus = self.company_id.social_googleplus
+
#----------------------------------------------------------
# Page Management
#----------------------------------------------------------
+ def _bootstrap_homepage(self):
+ standard_homepage = self.env.ref('website.homepage', raise_if_not_found=False)
+ if not standard_homepage:
+ return
+
+ new_homepage_view = '''<t name="Homepage" t-name="website.homepage%s">
+ <t t-call="website.layout">
+%s
+ </t>
+</t>''' % (self.id, self.env['ir.ui.view'].render_template('website.default_homepage', values={'website': self}).decode())
+ standard_homepage.with_context(website_id=self.id).arch_db = new_homepage_view
+
+ self.homepage_id = self.env['website.page'].search([('website_id', '=', self.id),
+ ('key', '=', standard_homepage.key)])
+
@api.model
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
@@ -130,8 +195,8 @@ class Website(models.Model):
if ispage:
page = self.env['website.page'].create({
'url': page_url,
- 'website_ids': [(6, None, [self.get_current_website().id])],
- 'view_id': view.id
+ 'website_id': self.get_current_website().id,
+ 'view_id': view.id,
})
result['view_id'] = view.id
if add_menu:
@@ -154,7 +219,7 @@ class Website(models.Model):
"""
website_id = self.get_current_website().id
inc = 0
- domain_static = ['|', ('website_ids', '=', False), ('website_ids', 'in', website_id)]
+ domain_static = ['|', ('website_id', '=', False), ('website_id', '=', 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
@@ -177,7 +242,7 @@ class Website(models.Model):
#Look for unique key
key_copy = string
inc = 0
- domain_static = ['|', ('website_ids', '=', False), ('website_ids', 'in', website_id)]
+ domain_static = ['|', ('website_id', '=', False), ('website_id', '=', website_id)]
while self.env['website.page'].with_context(active_test=False).sudo().search([('key', '=', key_copy)] + domain_static):
inc += 1
key_copy = string + (inc and "-%s" % inc or "")
@@ -208,7 +273,7 @@ class Website(models.Model):
# search for website_page with link
website_page_search_dom = [
- '|', ('website_ids', 'in', website_id), ('website_ids', '=', False), ('view_id.arch_db', 'ilike', url)
+ '|', ('website_id', '=', False), ('website_id', '=', website_id), ('view_id.arch_db', 'ilike', url)
]
pages = self.env['website.page'].search(website_page_search_dom)
page_key = _('Page')
@@ -276,7 +341,7 @@ class Website(models.Model):
# search for website_page with link
website_page_search_dom = [
- '|', ('website_ids', 'in', website_id), ('website_ids', '=', False), ('view_id.arch_db', 'ilike', key),
+ '|', ('website_id', '=', False), ('website_id', '=', website_id), ('view_id.arch_db', 'ilike', key),
('id', '!=', page.id),
]
pages = self.env['website.page'].search(website_page_search_dom)
@@ -376,19 +441,38 @@ class Website(models.Model):
@api.model
def get_current_website(self):
+ if request and request.session.get('force_website_id'):
+ return self.browse(request.session['force_website_id'])
+
domain_name = request and request.httprequest.environ.get('HTTP_HOST', '').split(':')[0] or None
- website_id = self._get_current_website_id(domain_name)
+
+ country = request.session.geoip.get('country_code') if request and request.session.geoip else False
+ country_id = False
+ if country:
+ country_id = request.env['res.country'].search([('code', '=', country)], limit=1).id
+
+ website_id = self._get_current_website_id(domain_name, country_id)
if request:
request.context = dict(request.context, website_id=website_id)
return self.browse(website_id)
- @tools.cache('domain_name')
- def _get_current_website_id(self, domain_name):
- """ Reminder : cached method should be return record, since they will use a closed cursor. """
- website = self.search([('domain', '=', domain_name)], limit=1)
- if not website:
- website = self.search([], limit=1)
- return website.id
+ @tools.cache('domain_name', 'country_id')
+ def _get_current_website_id(self, domain_name, country_id):
+ # sort on country_group_ids so that we fall back on a generic website (empty country_group_ids)
+ websites = self.search([('domain', '=', domain_name)]).sorted('country_group_ids')
+
+ if not websites:
+ return self.search([], limit=1).id
+ elif len(websites) == 1:
+ return websites.id
+ else: # > 1 website with the same domain
+ country_specific_websites = websites.filtered(lambda website: country_id in website.country_group_ids.mapped('country_ids').ids)
+ return country_specific_websites[0].id if country_specific_websites else websites[0].id
+
+ def _fix_to_session(self):
+ # call on empty recordset to clear
+ if request:
+ request.session['force_website_id'] = self.id
@api.model
def is_publisher(self):
@@ -545,7 +629,7 @@ class Website(models.Model):
@api.multi
def get_website_pages(self, domain=[], order='name', limit=None):
- domain += ['|', ('website_ids', 'in', self.get_current_website().id), ('website_ids', '=', False)]
+ domain += ['|', ('website_id', '=', False), ('website_id', '=', self.get_current_website().id)]
pages = request.env['website.page'].search(domain, order='name', limit=limit)
return pages
@@ -599,10 +683,49 @@ class WebsitePublishedMixin(models.AbstractModel):
_name = "website.published.mixin"
- website_published = fields.Boolean('Visible in Website', copy=False)
+ website_published = fields.Boolean('Visible on current website',
+ compute='_compute_website_published',
+ inverse='_inverse_website_published',
+ search='_search_website_published') # todo jov evaluate places where this is used, we can probably replace some with is_published
+ is_published = fields.Boolean('Is published')
+ website_id = fields.Many2one('website', string='Website', help='Restrict publishing to this website.')
website_url = fields.Char('Website URL', compute='_compute_website_url', help='The full URL to access the document through the website.')
@api.multi
+ @api.depends('is_published', 'website_id')
+ def _compute_website_published(self):
+ current_website_id = self._context.get('website_id')
+ for record in self:
+ if current_website_id:
+ record.website_published = record.is_published and (not record.website_id or record.website_id.id == current_website_id)
+ else:
+ record.website_published = record.is_published
+
+ @api.multi
+ def _inverse_website_published(self):
+ current_website_id = self._context.get('website_id')
+ for record in self:
+ record.is_published = record.website_published
+ if record.website_published and current_website_id:
+ record.write({'website_id': current_website_id})
+
+ def _search_website_published(self, operator, value):
+ if not isinstance(value, bool) or operator not in ('=', '!='):
+ logger.warning('unsupported search on website_published: %s, %s', operator, value)
+ return [()]
+
+ if operator in expression.NEGATIVE_TERM_OPERATORS:
+ value = not value
+
+ current_website_id = self._context.get('website_id')
+ if current_website_id:
+ is_published = [('is_published', '=', True)]
+ on_current_website = expression.OR([[('website_id', '=', False)], [('website_id', '=', current_website_id)]])
+ return (['!'] if value is False else []) + expression.AND([is_published, on_current_website])
+ else: # should be in the backend, return things that are published anywhere
+ return [('is_published', '=', value)]
+
+ @api.multi
def _compute_website_url(self):
for record in self:
record.website_url = '#'
@@ -627,9 +750,9 @@ class Page(models.Model):
_inherits = {'ir.ui.view': 'view_id'}
_inherit = 'website.published.mixin'
_description = 'Page'
+ _order = 'website_id'
url = fields.Char('Page URL')
- website_ids = fields.Many2many('website', string='Websites')
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')
@@ -642,6 +765,9 @@ class Page(models.Model):
header_overlay = fields.Boolean()
header_color = fields.Char()
+ # don't use mixin website_id but use website_id on ir.ui.view instead
+ website_id = fields.Many2one(related='view_id.website_id', store=True)
+
@api.one
def _compute_homepage(self):
self.is_homepage = self == self.env['website'].get_current_website().homepage_id
@@ -660,10 +786,20 @@ class Page(models.Model):
def _compute_visible(self):
self.is_visible = self.website_published and (not self.date_publish or self.date_publish < fields.Datetime.now())
+ @api.multi
+ def _is_most_specific_page(self, page_to_test):
+ '''This will test if page_to_test is the most specific page in self.'''
+ pages_for_url = self.filtered(lambda page: page.url == page_to_test.url)
+
+ # this works because pages are _order'ed by website_id
+ most_specific_page = pages_for_url[0]
+
+ return most_specific_page == page_to_test
+
@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)
+ domain = ['|', ('website_id', '=', False), ('website_id', '=', website_id), ('id', '=', id)]
+ item = self.search_read(domain, fields=['id', 'name', 'url', 'website_published', 'website_indexed', 'date_publish', 'menu_ids', 'is_homepage', 'website_id'], limit=1)
return item
@api.multi
@@ -709,11 +845,15 @@ class Page(models.Model):
'website_id': website.id,
})
- page.write({
+ # Edits via the page manager shouldn't trigger the COW
+ # mechanism and generate new pages. The user manages page
+ # visibility manually with is_published here.
+ page.with_context(no_cow=True).write({
'key': page_key,
'name': data['name'],
'url': url,
- 'website_published': data['website_published'],
+ 'is_published': data['website_published'],
+ 'website_id': website.id if data['restrict_to_current_website'] else False,
'website_indexed': data['website_indexed'],
'date_publish': data['date_publish'] or None,
'is_homepage': data['is_homepage'],
@@ -733,17 +873,18 @@ class Page(models.Model):
@api.multi
@api.returns('self', lambda value: value.id)
def copy(self, default=None):
- view = self.env['ir.ui.view'].browse(self.view_id.id)
- # website.page's ir.ui.view should have a different key than the one it
- # is copied from.
- # (eg: website_version: an ir.ui.view record with the same key is
- # expected to be the same ir.ui.view but from another version)
- new_view = view.copy({'key': view.key + '.copy', 'name': '%s %s' % (view.name, _('(copy)'))})
- default = {
- 'name': '%s %s' % (self.name, _('(copy)')),
- 'url': self.env['website'].get_unique_path(self.url),
- 'view_id': new_view.id,
- }
+ if default and not default.get('view_id'):
+ view = self.env['ir.ui.view'].browse(self.view_id.id)
+ # website.page's ir.ui.view should have a different key than the one it
+ # is copied from.
+ # (eg: website_version: an ir.ui.view record with the same key is
+ # expected to be the same ir.ui.view but from another version)
+ new_view = view.copy({'key': view.key + '.copy', 'name': '%s %s' % (view.name, _('(copy)'))})
+ default = {
+ 'name': '%s %s' % (self.name, _('(copy)')),
+ 'url': self.env['website'].get_unique_path(self.url),
+ 'view_id': new_view.id,
+ }
return super(Page, self).copy(default=default)
@api.model
@@ -758,47 +899,30 @@ class Page(models.Model):
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': '%s %s' % (menu.name, _('(copy)')), 'page_id': new_page.id})
+ new_menu.write({'url': new_page.url, 'name': '%s %s' % (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
+ # 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.
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.search(
[('view_id', '=', self.view_id.id), ('id', '!=', self.id)]
)
- if len(pages_linked_to_iruiview) == 0:
+ if len(pages_linked_to_iruiview) == 0 and not page.view_id.inherit_children_ids:
# 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.view_id.id)]).unlink()
- # And then delete the website_page itself
+ page.view_id.unlink()
return super(Page, self).unlink()
- @api.model
- def delete_page(self, page_id):
- """ Delete a page, given its identifier
- :param page_id : website.page identifier
- """
- # 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):
if 'url' in vals and not vals['url'].startswith('/'):
vals['url'] = '/' + vals['url']
- result = super(Page, self).write(vals)
- return result
+ return super(Page, self).write(vals)
class Menu(models.Model):
@@ -815,15 +939,47 @@ class Menu(models.Model):
name = fields.Char('Menu', required=True, translate=True)
url = fields.Char('Url', default='')
- page_id = fields.Many2one('website.page', 'Related Page')
+ page_id = fields.Many2one('website.page', 'Related Page', ondelete='cascade')
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
+ website_id = fields.Many2one('website', 'Website')
parent_id = fields.Many2one('website.menu', 'Parent Menu', index=True, ondelete="cascade")
child_id = fields.One2many('website.menu', 'parent_id', string='Child Menus')
parent_path = fields.Char(index=True)
is_visible = fields.Boolean(compute='_compute_visible', string='Is Visible')
+ @api.multi
+ def unlink(self):
+ '''This implements COU (copy-on-unlink). When deleting a generic menu
+ item website-specific menu items will be created.'''
+ current_website_id = self._context.get('website_id')
+
+ if current_website_id and not self._context.get('no_cow'):
+ for menu in self.filtered(lambda menu: not menu.website_id):
+ for website in self.env['website'].search([('id', '!=', current_website_id)]):
+ # reuse the COW mechanism to create website-specific copies
+ menu.with_context(website_id=website.id).write({})
+
+ return super(Menu, self).unlink()
+
+ @api.multi
+ def write(self, vals):
+ '''This implements COW (copy-on-write). This way editing websites does
+ not impact other websites and newly created websites will only
+ contain the default menus.
+ '''
+ current_website_id = self._context.get('website_id')
+
+ if current_website_id and not self.website_id and not self._context.get('no_cow'):
+ new_website_specific_menu = self.copy({'website_id': current_website_id})
+
+ for child in self.child_id:
+ child.write({'parent_id': new_website_specific_menu.id})
+
+ return new_website_specific_menu.write(vals)
+
+ return super(Menu, self).write(vals)
+
@api.one
def _compute_visible(self):
visible = True
@@ -846,6 +1002,21 @@ class Menu(models.Model):
url = '/%s' % self.url
return url
+ def get_children_for_current_website(self):
+ most_specific_child_menus = self.env['website.menu']
+ website_id = self._context.get('website_id')
+
+ if not website_id:
+ return self.child_id
+
+ for child in self.child_id:
+ if child.website_id and child.website_id.id == website_id:
+ most_specific_child_menus |= child
+ elif not child.website_id and not any(child.clean_url() == child2.clean_url() and child2.website_id.id == website_id for child2 in self.child_id):
+ most_specific_child_menus |= child
+
+ return most_specific_child_menus
+
# would be better to take a menu_id as argument
@api.model
def get_tree(self, website_id, menu_id=None):
@@ -862,7 +1033,7 @@ class Menu(models.Model):
children=[],
is_homepage=is_homepage,
)
- for child in node.child_id:
+ for child in node.get_children_for_current_website():
menu_node['children'].append(make_tree(child))
return menu_node
if menu_id: