diff --git a/addons/website/controllers/backend.py b/addons/website/controllers/backend.py index 3d75facfc30..d9ca7981612 100644 --- a/addons/website/controllers/backend.py +++ b/addons/website/controllers/backend.py @@ -31,6 +31,7 @@ class WebsiteBackend(http.Controller): if has_group_designer: config = request.env['res.config.settings'].sudo().create({}) + config.website_id = current_website if config.has_google_analytics_dashboard: dashboard_data['dashboards']['visits'] = dict( ga_client_id=config.google_management_client_id or '', # void string instead of stringified False @@ -39,7 +40,7 @@ class WebsiteBackend(http.Controller): return dashboard_data @http.route('/website/dashboard/set_ga_data', type='json', auth='user') - def website_set_ga_data(self, ga_client_id, ga_analytics_key): + def website_set_ga_data(self, website_id, ga_client_id, ga_analytics_key): if not request.env.user.has_group('base.group_system'): return { 'error': { @@ -54,10 +55,14 @@ class WebsiteBackend(http.Controller): 'message': 'The Google Analytics Client ID or Key you entered seems incorrect.', } } + Website = request.env['website'] + current_website = website_id and Website.browse(website_id) or Website.get_current_website() + request.env['res.config.settings'].create({ 'has_google_analytics': True, 'has_google_analytics_dashboard': True, 'google_management_client_id': ga_client_id, 'google_analytics_key': ga_analytics_key, + 'website_id': current_website, }).execute() return True diff --git a/addons/website/models/__init__.py b/addons/website/models/__init__.py index 8fa63d63d09..0ed659a8f3a 100644 --- a/addons/website/models/__init__.py +++ b/addons/website/models/__init__.py @@ -7,6 +7,7 @@ from . import ir_http from . import ir_qweb from . import ir_qweb_fields from . import website +from . import ir_rule from . import ir_ui_view from . import res_company from . import res_partner diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py index 620e2cfd739..21c37801b96 100644 --- a/addons/website/models/ir_http.py +++ b/addons/website/models/ir_http.py @@ -267,6 +267,8 @@ class ModelConverter(ModelConverter): def generate(self, uid, dom=None, args=None): Model = request.env[self.model].sudo(uid) + # Allow to current_website_id directly in route domain + args.update(current_website_id=request.env['website'].get_current_website().id) domain = safe_eval(self.domain, (args or {}).copy()) if dom: domain += dom diff --git a/addons/website/models/ir_rule.py b/addons/website/models/ir_rule.py index 2a90dbfacfa..ba5e07128ee 100644 --- a/addons/website/models/ir_rule.py +++ b/addons/website/models/ir_rule.py @@ -8,5 +8,9 @@ class IrRule(models.Model): @api.model def _eval_context(self): res = super(IrRule, self)._eval_context() - res['website_id'] = self._context.get('website_id') + res['website_id'] = self.env['website'].get_current_website().id return res + + def _compute_domain_keys(self): + """ Return the list of context keys to use for caching ``_compute_domain``. """ + return super(IrRule, self)._compute_domain_keys() + ['website_id'] diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py index 3148bb984eb..c7a87da022e 100644 --- a/addons/website/models/ir_ui_view.py +++ b/addons/website/models/ir_ui_view.py @@ -297,3 +297,6 @@ class View(models.Model): 'url': '/website/pages', 'target': 'self', } + + def _read_template_keys(self): + return super(View, self)._read_template_keys() + ['website_id'] diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 3d2f379e322..1fd2ea4b5e5 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -694,18 +694,63 @@ class SeoMetadata(models.AbstractModel): website_meta_keywords = fields.Char("Website meta keywords", translate=True) +class WebsiteMultiMixin(models.AbstractModel): + + _name = 'website.multi.mixin' + + website_id = fields.Many2one('website', string='Website', help='Restrict publishing to this website.') + + @api.multi + def can_access_from_current_website(self, website_id=False): + can_access = True + for record in self: + if (website_id or record.website_id.id) not in (False, request.website.id): + can_access = False + continue + return can_access + + class WebsitePublishedMixin(models.AbstractModel): _name = "website.published.mixin" - 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 + website_published = fields.Boolean('Visible on current website', related='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 + def _compute_website_url(self): + for record in self: + record.website_url = '#' + + @api.multi + def website_publish_button(self): + self.ensure_one() + if self.env.user.has_group('website.group_website_publisher') and self.website_url != '#': + # Force website to land on record's website to publish/unpublish it + if 'website_id' in self and self.env.user.has_group('website.group_multi_website'): + self.website_id._force() + return self.open_website_url() + return self.write({'website_published': not self.website_published}) + + def open_website_url(self): + return { + 'type': 'ir.actions.act_url', + 'url': self.website_url, + 'target': 'self', + } + + +class WebsitePublishedMultiMixin(WebsitePublishedMixin): + + _name = 'website.published.multi.mixin' + _inherit = ['website.published.mixin', 'website.multi.mixin'] + + website_published = fields.Boolean(compute='_compute_website_published', + inverse='_inverse_website_published', + search='_search_website_published', + related=False) + @api.multi @api.depends('is_published', 'website_id') def _compute_website_published(self): @@ -718,11 +763,8 @@ class WebsitePublishedMixin(models.AbstractModel): @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 ('=', '!='): @@ -740,30 +782,11 @@ class WebsitePublishedMixin(models.AbstractModel): else: # should be in the backend, return things that are published anywhere return is_published - @api.multi - def _compute_website_url(self): - for record in self: - record.website_url = '#' - - @api.multi - def website_publish_button(self): - self.ensure_one() - if self.env.user.has_group('website.group_website_publisher') and self.website_url != '#': - return self.open_website_url() - return self.write({'website_published': not self.website_published}) - - def open_website_url(self): - return { - 'type': 'ir.actions.act_url', - 'url': self.website_url, - 'target': 'self', - } - class Page(models.Model): _name = 'website.page' _inherits = {'ir.ui.view': 'view_id'} - _inherit = 'website.published.mixin' + _inherit = 'website.published.multi.mixin' _description = 'Page' _order = 'website_id' diff --git a/addons/website/static/src/js/backend/dashboard.js b/addons/website/static/src/js/backend/dashboard.js index 393709b91db..b4fe22ba84e 100644 --- a/addons/website/static/src/js/backend/dashboard.js +++ b/addons/website/static/src/js/backend/dashboard.js @@ -112,6 +112,7 @@ var Dashboard = AbstractAction.extend(ControlPanelMixin, { return this._rpc({ route: '/website/dashboard/set_ga_data', params: { + 'website_id': self.website_id, 'ga_client_id': ga_client_id, 'ga_analytics_key': ga_analytics_key, }, diff --git a/addons/website/views/website_views.xml b/addons/website/views/website_views.xml index c0e1b4c842c..2c5d5dc4daa 100644 --- a/addons/website/views/website_views.xml +++ b/addons/website/views/website_views.xml @@ -75,7 +75,7 @@ current - + website.redirect.form diff --git a/addons/website_blog/controllers/main.py b/addons/website_blog/controllers/main.py index f16f6ed761b..902b74dee90 100644 --- a/addons/website_blog/controllers/main.py +++ b/addons/website_blog/controllers/main.py @@ -48,13 +48,14 @@ class WebsiteBlog(http.Controller): '/blog/page/', ], type='http', auth="public", website=True) def blogs(self, page=1, **post): + domain = request.website.website_domain() Blog = request.env['blog.blog'] - blogs = Blog.search([], limit=2) + blogs = Blog.search(domain, limit=2) if len(blogs) == 1: return werkzeug.utils.redirect('/blog/%s' % slug(blogs[0]), code=302) BlogPost = request.env['blog.post'] - total = BlogPost.search([], count=True) + total = BlogPost.search_count(domain) pager = request.website.pager( url='/blog', @@ -62,7 +63,7 @@ class WebsiteBlog(http.Controller): page=page, step=self._blog_post_per_page, ) - posts = BlogPost.search([], offset=(page - 1) * self._blog_post_per_page, limit=self._blog_post_per_page) + posts = BlogPost.search(domain, offset=(page - 1) * self._blog_post_per_page, limit=self._blog_post_per_page) blog_url = QueryURL('', ['blog', 'tag']) return request.render("website_blog.latest_blogs", { 'posts': posts, @@ -71,10 +72,10 @@ class WebsiteBlog(http.Controller): }) @http.route([ - '/blog/', - '/blog//page/', - '/blog//tag/', - '/blog//tag//page/', + '''/blog/''', + '''/blog//page/''', + '''/blog//tag/''', + '''/blog//tag//page/''', ], type='http', auth="public", website=True) def blog(self, blog=None, tag=None, page=1, **opt): """ Prepare all values to display the blog. @@ -92,16 +93,19 @@ class WebsiteBlog(http.Controller): - 'date': date_begin optional parameter, used in archives navigation - 'blog_url': help object to create URLs """ + if not blog.can_access_from_current_website(): + raise werkzeug.exceptions.NotFound() + date_begin, date_end, state = opt.get('date_begin'), opt.get('date_end'), opt.get('state') published_count, unpublished_count = 0, 0 + domain = request.website.website_domain() + BlogPost = request.env['blog.post'] Blog = request.env['blog.blog'] - blogs = Blog.search([], order="create_date asc") + blogs = Blog.search(domain, order="create_date asc") - # build the domain for blog post to display - domain = [] # retrocompatibility to accept tag as slug active_tag_ids = tag and [int(unslug(t)[1]) for t in tag.split(',')] or [] if active_tag_ids: @@ -172,7 +176,7 @@ class WebsiteBlog(http.Controller): response = request.render("website_blog.blog_post_short", values) return response - @http.route(['/blog//feed'], type='http', auth="public") + @http.route(['''/blog//feed'''], type='http', auth="public") def blog_feed(self, blog, limit='15'): v = {} v['blog'] = blog @@ -185,7 +189,7 @@ class WebsiteBlog(http.Controller): return r @http.route([ - '''/blog//post//post//post/new', type='http', auth="public", website=True) def blog_post_create(self, blog_id, **post): + # Use sudo so this line prevents both editor and admin to access blog from another website + # as browse() will return the record even if forbidden by security rules but editor won't + # be able to access it + if not request.env['blog.blog'].browse(blog_id).sudo().can_access_from_current_website(): + raise werkzeug.exceptions.NotFound() + new_blog_post = request.env['blog.post'].create({ 'blog_id': blog_id, 'website_published': False, diff --git a/addons/website_blog/models/website_blog.py b/addons/website_blog/models/website_blog.py index b1e0253b385..d9989c3d6a4 100644 --- a/addons/website_blog/models/website_blog.py +++ b/addons/website_blog/models/website_blog.py @@ -15,7 +15,7 @@ from odoo.tools import html2plaintext class Blog(models.Model): _name = 'blog.blog' _description = 'Blogs' - _inherit = ['mail.thread', 'website.seo.metadata'] + _inherit = ['mail.thread', 'website.seo.metadata', 'website.multi.mixin'] _order = 'name' name = fields.Char('Blog Name', required=True, translate=True) @@ -108,7 +108,7 @@ class BlogTag(models.Model): class BlogPost(models.Model): _name = "blog.post" _description = "Blog Post" - _inherit = ['mail.thread', 'website.seo.metadata', 'website.published.mixin'] + _inherit = ['mail.thread', 'website.seo.metadata', 'website.published.multi.mixin'] _order = 'id DESC' _mail_post_access = 'read' @@ -168,6 +168,8 @@ class BlogPost(models.Model): visits = fields.Integer('No of Views', copy=False) ranking = fields.Float(compute='_compute_ranking', string='Ranking') + website_id = fields.Many2one(related='blog_id.website_id', readonly=True) + @api.multi @api.depends('content', 'teaser_manual') def _compute_teaser(self): diff --git a/addons/website_blog/views/website_blog_templates.xml b/addons/website_blog/views/website_blog_templates.xml index 1853eaefaa8..069e5f93256 100644 --- a/addons/website_blog/views/website_blog_templates.xml +++ b/addons/website_blog/views/website_blog_templates.xml @@ -64,26 +64,26 @@
- - In - - - - + + + In + + +
-
+
+ No keywords defined!
diff --git a/addons/website_blog/views/website_blog_views.xml b/addons/website_blog/views/website_blog_views.xml index ddeee669d33..e9adbde891b 100644 --- a/addons/website_blog/views/website_blog_views.xml +++ b/addons/website_blog/views/website_blog_views.xml @@ -8,6 +8,7 @@ + @@ -26,6 +27,7 @@ +
@@ -45,6 +47,7 @@ + diff --git a/addons/website_event/controllers/main.py b/addons/website_event/controllers/main.py index e50623b53fc..33e51724613 100644 --- a/addons/website_event/controllers/main.py +++ b/addons/website_event/controllers/main.py @@ -28,7 +28,6 @@ class WebsiteEventController(http.Controller): searches.setdefault('type', 'all') searches.setdefault('country', 'all') - domain_search = {} def sdn(date): return fields.Datetime.to_string(date.replace(hour=23, minute=59, second=59)) @@ -64,6 +63,7 @@ class WebsiteEventController(http.Controller): ] # search domains + domain_search = {'website_specific': request.website.website_domain()} current_date = None current_type = None current_country = None @@ -141,8 +141,11 @@ class WebsiteEventController(http.Controller): return request.render("website_event.index", values) - @http.route(['/event//page/'], type='http', auth="public", website=True, sitemap=False) + @http.route(['''/event//page/'''], type='http', auth="public", website=True, sitemap=False) def event_page(self, event, page, **post): + if not event.can_access_from_current_website(): + raise werkzeug.exceptions.NotFound() + values = { 'event': event, 'main_object': event @@ -161,8 +164,11 @@ class WebsiteEventController(http.Controller): return request.render(page, values) - @http.route(['/event/'], type='http', auth="public", website=True) + @http.route(['''/event/'''], type='http', auth="public", website=True) def event(self, event, **post): + if not event.can_access_from_current_website(): + raise werkzeug.exceptions.NotFound() + if event.menu_id and event.menu_id.child_id: target_url = event.menu_id.child_id[0].url else: @@ -171,8 +177,11 @@ class WebsiteEventController(http.Controller): target_url += '?enable_editor=1' return request.redirect(target_url) - @http.route(['/event//register'], type='http', auth="public", website=True, sitemap=False) + @http.route(['''/event//register'''], type='http', auth="public", website=True, sitemap=False) def event_register(self, event, **post): + if not event.can_access_from_current_website(): + raise werkzeug.exceptions.NotFound() + values = { 'event': event, 'main_object': event, @@ -195,6 +204,7 @@ class WebsiteEventController(http.Controller): 'date_begin': fields.Date.to_string(date_begin), 'date_end': fields.Date.to_string((date_begin + timedelta(days=(1)))), 'seats_available': 1000, + 'website_id': request.website.id, } return request.env['event.event'].with_context(context or {}).create(vals) @@ -210,11 +220,12 @@ class WebsiteEventController(http.Controller): country_code = request.session['geoip'].get('country_code') result = {'events': [], 'country': False} events = None + domain = request.website.website_domain() if country_code: country = request.env['res.country'].search([('code', '=', country_code)], limit=1) - events = Event.search(['|', ('address_id', '=', None), ('country_id.code', '=', country_code), ('date_begin', '>=', '%s 00:00:00' % fields.Date.today()), ('state', '=', 'confirm')], order="date_begin") + events = Event.search(domain + ['|', ('address_id', '=', None), ('country_id.code', '=', country_code), ('date_begin', '>=', '%s 00:00:00' % fields.Date.today()), ('state', '=', 'confirm')], order="date_begin") if not events: - events = Event.search([('date_begin', '>=', '%s 00:00:00' % fields.Date.today()), ('state', '=', 'confirm')], order="date_begin") + events = Event.search(domain + [('date_begin', '>=', '%s 00:00:00' % fields.Date.today()), ('state', '=', 'confirm')], order="date_begin") for event in events: if country_code and event.country_id.code == country_code: result['country'] = country @@ -252,8 +263,11 @@ class WebsiteEventController(http.Controller): registration[key] = value return list(registrations.values()) - @http.route(['/event//registration/confirm'], type='http', auth="public", methods=['POST'], website=True) + @http.route(['''/event//registration/confirm'''], type='http', auth="public", methods=['POST'], website=True) def registration_confirm(self, event, **post): + if not event.can_access_from_current_website(): + raise werkzeug.exceptions.NotFound() + Attendees = request.env['event.registration'] registrations = self._process_registration_details(post) @@ -270,7 +284,7 @@ class WebsiteEventController(http.Controller): 'iCal_url': urls.get('iCal_url') }) - @http.route(['/event//ics'], type='http', auth="public", website=True) + @http.route(['''/event//ics'''], type='http', auth="public", website=True) def make_event_ics_file(self, event, **kwargs): if not event or not event.registration_ids: return request.not_found() diff --git a/addons/website_event/models/event.py b/addons/website_event/models/event.py index 5bb045a320f..ae07f798939 100644 --- a/addons/website_event/models/event.py +++ b/addons/website_event/models/event.py @@ -32,9 +32,9 @@ class EventType(models.Model): class Event(models.Model): _name = 'event.event' - _inherit = ['event.event', 'website.seo.metadata', 'website.published.mixin'] + _inherit = ['event.event', 'website.seo.metadata', 'website.published.multi.mixin'] - website_published = fields.Boolean(track_visibility='onchange') + is_published = fields.Boolean(track_visibility='onchange') is_participating = fields.Boolean("Is Participating", compute="_compute_is_participating") @@ -45,7 +45,7 @@ class Event(models.Model): def _compute_is_participating(self): # we don't allow public user to see participating label - if self.env.user != self.env.ref('base.public_user'): + if self.env.user != self.env['website'].get_current_website().user_id: email = self.env.user.partner_id.email for event in self: domain = ['&', '|', ('email', '=', email), ('partner_id', '=', self.env.user.partner_id.id), ('event_id', '=', event.id)] @@ -120,9 +120,9 @@ class Event(models.Model): @api.multi def _track_subtype(self, init_values): self.ensure_one() - if 'website_published' in init_values and self.website_published: + if 'is_published' in init_values and self.is_published: return 'website_event.mt_event_published' - elif 'website_published' in init_values and not self.website_published: + elif 'is_published' in init_values and not self.is_published: return 'website_event.mt_event_unpublished' return super(Event, self)._track_subtype(init_values) diff --git a/addons/website_event/views/event_views.xml b/addons/website_event/views/event_views.xml index 14339ac30d3..3c533e9e2ec 100644 --- a/addons/website_event/views/event_views.xml +++ b/addons/website_event/views/event_views.xml @@ -30,6 +30,9 @@ event.event + + +
@@ -197,6 +221,7 @@ + diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index 1f9a207d57b..cbc64b58754 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -271,7 +271,9 @@