[ADD] Multi Website Core Support 🎉

This commit is contained in:
Siddharth Bhalgami 2017-12-22 17:38:41 +05:30
parent 7bdf18df35
commit d00801d466
17 changed files with 769 additions and 144 deletions

View File

@ -10,7 +10,8 @@
'website': 'https://flectrahq.com/page/website-builder', 'website': 'https://flectrahq.com/page/website-builder',
'version': '1.0', 'version': '1.0',
'description': "", 'description': "",
'depends': ['web', 'web_editor', 'web_planner', 'http_routing', 'portal'], 'depends': ['web', 'web_editor', 'web_planner',
'http_routing', 'portal', 'base_automation'],
'installable': True, 'installable': True,
'data': [ 'data': [
'data/website_data.xml', 'data/website_data.xml',
@ -23,6 +24,7 @@
'views/res_config_settings_views.xml', 'views/res_config_settings_views.xml',
'views/ir_actions_views.xml', 'views/ir_actions_views.xml',
'views/res_company_views.xml', 'views/res_company_views.xml',
'views/module_view.xml',
'wizard/base_language_install_views.xml', 'wizard/base_language_install_views.xml',
'data/web_planner_data.xml', 'data/web_planner_data.xml',
], ],

View File

@ -6,9 +6,13 @@ from flectra.http import request
class WebsiteBackend(http.Controller): class WebsiteBackend(http.Controller):
@http.route('/website/fetch_dashboard_data', type="json", auth='user') @http.route('/website/fetch_dashboard_data', type="json", auth='user',
def fetch_dashboard_data(self, date_from, date_to): website=True)
def fetch_dashboard_data(self, date_from, date_to, website_id=None):
if not website_id:
website_id = request.website.id
has_group_system = request.env.user.has_group('base.group_system') has_group_system = request.env.user.has_group('base.group_system')
has_group_designer = request.env.user.has_group('website.group_website_designer') has_group_designer = request.env.user.has_group('website.group_website_designer')
if has_group_system: if has_group_system:
@ -36,6 +40,10 @@ class WebsiteBackend(http.Controller):
ga_client_id=config.google_management_client_id or '', # void string instead of stringified False ga_client_id=config.google_management_client_id or '', # void string instead of stringified False
ga_analytics_key=config.google_analytics_key or '', # void string instead of stringified False ga_analytics_key=config.google_analytics_key or '', # void string instead of stringified False
) )
dashboard_data['website_ids'] = request.env['website'].search_read()
dashboard_data['website'] = request.env['website'].browse(
website_id).domain
dashboard_data['current_website'] = request.website.domain
return dashboard_data return dashboard_data
@http.route('/website/dashboard/set_ga_data', type='json', auth='user') @http.route('/website/dashboard/set_ga_data', type='json', auth='user')

View File

@ -272,7 +272,10 @@ class Website(Home):
@http.route("/website/get_switchable_related_views", type="json", auth="user", website=True) @http.route("/website/get_switchable_related_views", type="json", auth="user", website=True)
def get_switchable_related_views(self, key): def get_switchable_related_views(self, key):
views = request.env["ir.ui.view"].get_related_views(key, bundles=False).filtered(lambda v: v.customize_show) views = request.env["ir.ui.view"].get_related_views(
key, bundles=False).filtered(
lambda v: v.customize_show and (
v.website_id if v.website_id == request.website else None))
return views.read(['name', 'id', 'key', 'xml_id', 'arch', 'active', 'inherit_id']) return views.read(['name', 'id', 'key', 'xml_id', 'arch', 'active', 'inherit_id'])
@http.route('/website/reset_templates', type='http', auth='user', methods=['POST'], website=True) @http.route('/website/reset_templates', type='http', auth='user', methods=['POST'], website=True)

View File

@ -6,6 +6,7 @@
<field name="domain">localhost</field> <field name="domain">localhost</field>
<field name="company_id" ref="base.main_company"/> <field name="company_id" ref="base.main_company"/>
<field name="user_id" ref="base.public_user"/> <field name="user_id" ref="base.public_user"/>
<field name="is_default_website">True</field>
<field name="favicon" type="base64" file="web/static/src/img/favicon.ico"/> <field name="favicon" type="base64" file="web/static/src/img/favicon.ico"/>
</record> </record>
@ -35,6 +36,7 @@
<field name="name">Home</field> <field name="name">Home</field>
<field name="type">qweb</field> <field name="type">qweb</field>
<field name="key">website.homepage</field> <field name="key">website.homepage</field>
<field name="website_id" ref="default_website"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<t name="Home" priority="29" t-name="website.homepage"> <t name="Home" priority="29" t-name="website.homepage">
<t t-call="website.layout"> <t t-call="website.layout">
@ -48,6 +50,7 @@
<field name="website_published">True</field> <field name="website_published">True</field>
<field name="url">/</field> <field name="url">/</field>
<field name="view_id" ref="homepage" /> <field name="view_id" ref="homepage" />
<field name="website_ids" eval="[(4, ref('default_website'))]" />
</record> </record>
<record id="default_website" model="website"> <record id="default_website" model="website">
<field name="homepage_id" ref="homepage_page" /> <field name="homepage_id" ref="homepage_page" />
@ -91,6 +94,7 @@
<field name="url">/contactus</field> <field name="url">/contactus</field>
<field name="website_published">True</field> <field name="website_published">True</field>
<field name="view_id" ref="contactus" /> <field name="view_id" ref="contactus" />
<field name="website_ids" eval="[(4, ref('default_website'))]" />
</record> </record>
<record id="aboutus" model="ir.ui.view"> <record id="aboutus" model="ir.ui.view">
@ -147,6 +151,7 @@
<field name="website_published">True</field> <field name="website_published">True</field>
<field name="url">/aboutus</field> <field name="url">/aboutus</field>
<field name="view_id" ref="aboutus" /> <field name="view_id" ref="aboutus" />
<field name="website_ids" eval="[(4, ref('default_website'))]" />
</record> </record>
<record id="menu_homepage" model="website.menu"> <record id="menu_homepage" model="website.menu">
@ -555,5 +560,15 @@
<field name="url">/website/static/src/img/snippets_demo/s_team_member_4.png</field> <field name="url">/website/static/src/img/snippets_demo/s_team_member_4.png</field>
</record> </record>
<!-- Multi Website Automated Action Rule -->
<record id="multi_website_views_on_create" model="base.automation">
<field name="name">Multi Website: multi website rule on create</field>
<field name="model_id" ref="base.model_ir_ui_view"/>
<field name="state">code</field>
<field name="code">model.multi_website_view_rule()</field>
<field name="trigger">on_create</field>
<field name="active" eval="True"/>
</record>
</data> </data>
</flectra> </flectra>

View File

@ -126,122 +126,6 @@ response = request.render("website.template_partner_comment", {
<record id="default_website" model="website"> <record id="default_website" model="website">
<field name="name">Website localhost</field> <field name="name">Website localhost</field>
</record> </record>
<record id="website2" model="website">
<field name="name">Website 0.0.0.0</field>
<field name="domain">0.0.0.0</field>
</record>
<record id="website2_homepage" model="ir.ui.view">
<field name="name">Home</field>
<field name="type">qweb</field>
<field name="key">website2.homepage</field>
<field name="arch" type="xml">
<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;">
<ol class="carousel-indicators hidden">
<li class="active" data-slide-to="0" data-target="#myCarousel0"/>
</ol>
<div class="carousel-inner">
<div class="item image_text oe_img_bg active" style="background-image: url(http://0.0.0.0:8069/web/image/website.s_background_image_11);">
<div class="container">
<div class="row content">
<div class="carousel-content col-md-6 col-sm-12">
<h2>Homepage 0.0.0.0</h2>
<h3>Click to customize this text</h3>
<p>
<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>
</div>
</div>
</div>
</div>
<div class="carousel-control left hidden" data-slide="prev" data-target="#myCarousel0" href="#myCarousel0" style="width: 10%">
<i class="fa fa-chevron-left"/>
</div>
<div class="carousel-control right hidden" data-slide="next" data-target="#myCarousel0" href="#myCarousel0" style="width: 10%">
<i class="fa fa-chevron-right"/>
</div>
</div>
</div>
</t>
</t>
</field>
</record>
<record id="website2_homepage_page" model="website.page">
<field name="website_published">True</field>
<field name="url">/</field>
<field name="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="website_published">True</field>
<field name="url">/contactus</field>
<field name="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">/</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">/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> </data>
</flectra> </flectra>

View File

@ -6,10 +6,12 @@ from . import ir_attachment
from . import ir_http from . import ir_http
from . import ir_qweb from . import ir_qweb
from . import website from . import website
from . import ir_model
from . import ir_ui_view from . import ir_ui_view
from . import res_company from . import res_company
from . import res_partner from . import res_partner
from . import web_planner from . import web_planner
from . import module
from . import res_config_settings from . import res_config_settings
from . import ir_model_fields from . import ir_model_fields
from . import ir_model from . import ir_model

View File

@ -1,7 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details # Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details
from flectra import api, models import logging
from flectra import api, fields, models, _
_logger = logging.getLogger(__name__)
# This is a nasty hack, targeted for V11 only # This is a nasty hack, targeted for V11 only
@ -13,3 +16,171 @@ class IrModel(models.Model):
self.env.cr.execute( self.env.cr.execute(
"DELETE FROM ir_model_fields WHERE name='website_id'") "DELETE FROM ir_model_fields WHERE name='website_id'")
return super(IrModel, self).unlink() return super(IrModel, self).unlink()
class IrModelData(models.Model):
_inherit = 'ir.model.data'
# Overriding Method
@api.model
def _update(self, model, module, values, xml_id=False, store=True,
noupdate=False, mode='init', res_id=False):
# records created during module install should not display the messages of OpenChatter
self = self.with_context(install_mode=True)
current_module = module
if xml_id and ('.' in xml_id):
assert len(xml_id.split('.')) == 2, _(
"'%s' contains too many dots. XML ids should not contain dots ! These are used to refer to other modules data, as in module.reference_id") % xml_id
module, xml_id = xml_id.split('.')
action = self.browse()
record = self.env[model].browse(res_id)
if xml_id:
self._cr.execute("""SELECT imd.id, imd.res_id, md.id, imd.model, imd.noupdate
FROM ir_model_data imd LEFT JOIN %s md ON (imd.res_id = md.id)
WHERE imd.module=%%s AND imd.name=%%s""" % record._table,
(module, xml_id))
results = self._cr.fetchall()
for imd_id, imd_res_id, real_id, imd_model, imd_noupdate in results:
# In update mode, do not update a record if it's ir.model.data is flagged as noupdate
if mode == 'update' and imd_noupdate:
return imd_res_id
if not real_id:
self.clear_caches()
self._cr.execute('DELETE FROM ir_model_data WHERE id=%s',
(imd_id,))
record = record.browse()
else:
assert model == imd_model, "External ID conflict, %s already refers to a `%s` record," \
" you can't define a `%s` record with this ID." % (
xml_id, imd_model, model)
action = self.browse(imd_id)
record = record.browse(imd_res_id)
if action and record:
# Set ``is_cloned`` to ``False``
if values.get('type') == 'qweb' and not values.get('is_cloned'):
values.update({'is_cloned': False})
record.write(values)
action.sudo().write({'date_update': fields.Datetime.now()})
elif record:
record.write(values)
if xml_id:
for parent_model, parent_field in record._inherits.items():
self.sudo().create({
'name': xml_id + '_' + parent_model.replace('.', '_'),
'model': parent_model,
'module': module,
'res_id': record[parent_field].id,
'noupdate': noupdate,
})
self.sudo().create({
'name': xml_id,
'model': model,
'module': module,
'res_id': record.id,
'noupdate': noupdate,
})
elif mode == 'init' or (mode == 'update' and xml_id):
existing_parents = set() # {parent_model, ...}
if xml_id:
for parent_model, parent_field in record._inherits.items():
xid = self.sudo().search([
('module', '=', module),
('name', '=',
xml_id + '_' + parent_model.replace('.', '_')),
])
# XML ID found in the database, try to recover an existing record
if xid:
parent = self.env[xid.model].browse(xid.res_id)
if parent.exists():
existing_parents.add(xid.model)
values[parent_field] = parent.id
else:
xid.unlink()
record = record.create(values)
if xml_id:
# To add an external identifiers to all inherits model
inherit_models = [record]
while inherit_models:
current_model = inherit_models.pop()
for parent_model_name, parent_field in current_model._inherits.items():
inherit_models.append(self.env[parent_model_name])
if parent_model_name in existing_parents:
continue
self.sudo().create({
'name': xml_id + '_' + parent_model_name.replace(
'.', '_'),
'model': parent_model_name,
'module': module,
'res_id': record[parent_field].id,
'noupdate': noupdate,
})
existing_parents.add(parent_model_name)
self.sudo().create({
'name': xml_id,
'model': model,
'module': module,
'res_id': record.id,
'noupdate': noupdate
})
if current_module and module != current_module:
_logger.warning(
"Creating the ir.model.data %s in module %s instead of %s.",
xml_id, module, current_module)
if xml_id and record:
self.loads[(module, xml_id)] = (model, record.id)
for parent_model, parent_field in record._inherits.items():
parent_xml_id = xml_id + '_' + parent_model.replace('.', '_')
self.loads[(module, parent_xml_id)] = (
parent_model, record[parent_field].id)
return record.id
@api.model
def _process_end(self, modules):
super(IrModelData, self)._process_end(modules)
ir_ui_view = self.env['ir.ui.view']
ir_model_data = self.env['ir.model.data']
default_website = self.env['website'].search([
('is_default_website', '=', True)])
for cus_view in ir_ui_view.search([('customize_show', '=', True),
('website_id', '=', False),
'|', ('active', '=', False),
('active', '=', True)]):
if default_website:
cus_view.write({'website_id': default_website.id})
for website in self.env['website'].search(
[('is_default_website', '=', False)]):
for view in ir_ui_view.search(
[('website_id', '=', default_website.id),
('customize_show', '=', True), ('is_cloned', '=', False),
'|', ('active', '=', False), ('active', '=', True)]):
if not ir_ui_view.search(
[('key', '=', view.key +
'_' + website.website_code),
'|', ('active', '=', False), ('active', '=', True)]):
new_cus_view = view.copy({
'is_cloned': True,
'key': view.key + '_' + website.website_code,
'website_id': website.id
})
model_data_id = ir_model_data.create({
'model': view.model_data_id.model,
'name': view.model_data_id.name +
'_' + website.website_code,
'res_id': new_cus_view.id,
'module': view.model_data_id.module,
})
new_cus_view.write({
'model_data_id': model_data_id
})

View File

@ -4,7 +4,7 @@
import logging import logging
from itertools import groupby from itertools import groupby
from flectra import api, fields, models from flectra import api, fields, models, _
from flectra import tools from flectra import tools
from flectra.addons.http_routing.models.ir_http import url_for from flectra.addons.http_routing.models.ir_http import url_for
from flectra.http import request from flectra.http import request
@ -21,6 +21,11 @@ class View(models.Model):
customize_show = fields.Boolean("Show As Optional Inherit", default=False) customize_show = fields.Boolean("Show As Optional Inherit", default=False)
website_id = fields.Many2one('website', ondelete='cascade', string="Website") website_id = fields.Many2one('website', ondelete='cascade', string="Website")
page_ids = fields.One2many('website.page', compute='_compute_page_ids', store=False) page_ids = fields.One2many('website.page', compute='_compute_page_ids', store=False)
is_cloned = fields.Boolean(string='Cloned', copy=False, default=False,
help="This view is cloned"
"(not present physically in file system) "
"from default website's view for "
"supporting multi-website feature.")
@api.one @api.one
def _compute_page_ids(self): def _compute_page_ids(self):
@ -157,3 +162,102 @@ class View(models.Model):
'url': '/website/pages', 'url': '/website/pages',
'target': 'self', 'target': 'self',
} }
# Multi Website: Automated Action On Create Rule
##################################################
# If views are manually created for default website,
# then it'll automatically cloned for other websites.
#
# As this method is also called when new website is created.
# Because at the time of website creation ``Home`` page will be cloned,
# So, this method will automatically triggered to
# cloned all customize view(s).
@api.model
def multi_website_view_rule(self):
default_website = self.env['website'].search([
('is_default_website', '=', True)])
ir_model_data = self.env['ir.model.data']
for website in self.env['website'].search(
[('is_default_website', '=', False)]):
for cus_view in self.search(
[('website_id', '=', default_website.id),
('customize_show', '=', True),
('is_cloned', '=', False),
'|', ('active', '=', False), ('active', '=', True)]):
if not self.search(
[('key', '=', cus_view.key +
'_' + website.website_code),
'|', ('active', '=', False), ('active', '=', True)]):
new_cus_view = cus_view.copy(
{'is_cloned': True,
'key': cus_view.key + '_' + website.website_code,
'website_id': website.id
})
model_data_id = ir_model_data.create({
'model': cus_view.model_data_id.model,
'name': cus_view.model_data_id.name +
'_' + website.website_code,
'res_id': new_cus_view.id,
'module': cus_view.model_data_id.module,
})
new_cus_view.write({
'model_data_id': model_data_id
})
# Add the website_id to each customize QWeb view(s) at the time
# of creation of new customize QWeb view(s).
@api.model
def create(self, values):
# For Theme's View(s)
if values.get('key') and values.get('type') == 'qweb' and \
self.env.context.get('install_mode_data'):
module_name = self.env.context['install_mode_data']['module']
module_obj = self.env['ir.module.module'].sudo().search(
[('name', '=', module_name)])
if module_obj and \
(module_obj.category_id.name == 'Theme'
or (module_obj.category_id.parent_id
and module_obj.category_id.parent_id.name
== 'Theme')):
values.update({
'website_id': module_obj.website_ids.id,
})
return super(View, self).create(self._compute_defaults(values))
# Keep other website's view as it is when run server using -i/-u
# As other website's views are not present anywhere in FS(file system).
# So, once those are created/cloned from default website,
# they can be changed/updated via debug mode only(ir.ui.view)
# Menu: Settings/Technical/User Interface/Views
#
# Scenario 1:
# -----------
# For Delete those views, Manually set ``is_cloned`` field to ``False``
#
# Scenario 2:
# -----------
# If you write the code for already cloned views in FS(file system)/Module
# to upgrade/update those views, then at the time of module update
# process that cloned views id are found in FS(file system)/Module,
# So in those particular views ``is_cloned`` will automatically
# set to ``False`` (Definitely it'll be done from another method!!),
# because now those views are not anymore cloned,
# now they are physically present!!
@api.multi
def unlink(self):
for view in self:
if view.is_cloned:
# Do not delete cloned view(s)
# ----------------
# 'View(s) that you want delete are '
# 'cloned view(s).\n'
# 'Cloned view(s) are automatically created '
# 'for supporting multi website feature.\n'
# 'If you still want to delete this view(s) '
# 'then first Uncheck(set to False) the '
# 'cloned field.\n'
# 'By deleting cloned view(s) multi website '
# 'will not work properly.\n'
# 'So, Be sure before deleting view(s).'
return True
return super(View, self).unlink()

View File

@ -0,0 +1,24 @@
from flectra import api, fields, models, _
from flectra.exceptions import Warning
class IrModuleModule(models.Model):
_inherit = 'ir.module.module'
website_ids = fields.One2many('website', 'website_theme_id',
string='Website', readonly=True)
@api.multi
def button_immediate_install(self):
for app in self:
if app.category_id and (
app.category_id.name == 'Theme'
or app.category_id.parent_id.name == 'Theme') and \
not app.website_ids:
raise Warning(_('You are trying to install Theme module!\n'
'As Flectra will support multi-website so, '
'please install theme in specific website.\n'
'Go to...\n'
'- Menu: Website/Configuration/Settings\n'
'- Select website & its theme & Save it.'))
return super(IrModuleModule, self).button_immediate_install()

View File

@ -1,10 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. # Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
import logging
from ast import literal_eval from ast import literal_eval
from flectra import api, fields, models from flectra import api, fields, models, _
from flectra.exceptions import AccessDenied from flectra.exceptions import AccessDenied, AccessError, Warning
_logger = logging.getLogger(__name__)
class ResConfigSettings(models.TransientModel): class ResConfigSettings(models.TransientModel):
@ -40,6 +43,25 @@ class ResConfigSettings(models.TransientModel):
('b2b', 'On invitation (B2B)'), ('b2b', 'On invitation (B2B)'),
('b2c', 'Free sign up (B2C)'), ('b2c', 'Free sign up (B2C)'),
], string='Customer Account') ], string='Customer Account')
website_theme_id = fields.Many2one(
'ir.module.module', string='Theme',
related='website_id.website_theme_id',
help='Choose theme for current website.')
# Unique theme per Website for now ;)
# @todo Flectra:
# Do enable support for same theme in multiple website
@api.onchange('website_theme_id')
def onchange_theme_id(self):
if (self.website_id.id not in self.website_theme_id.website_ids.ids) \
and (self.website_theme_id and
self.website_theme_id.website_ids):
warning = {
'title': 'Warning',
'message': _('Selected theme is already active in '
'different website.')}
self.website_theme_id = False
return {'warning': warning}
@api.onchange('has_google_analytics') @api.onchange('has_google_analytics')
def onchange_has_google_analytics(self): def onchange_has_google_analytics(self):
@ -96,3 +118,121 @@ class ResConfigSettings(models.TransientModel):
action['res_id'] = literal_eval(self.env['ir.config_parameter'].sudo().get_param('auth_signup.template_user_id', 'False')) action['res_id'] = literal_eval(self.env['ir.config_parameter'].sudo().get_param('auth_signup.template_user_id', 'False'))
action['views'] = [[self.env.ref('base.view_users_form').id, 'form']] action['views'] = [[self.env.ref('base.view_users_form').id, 'form']]
return action return action
@api.model
def _get_classified_fields(self):
res = super(ResConfigSettings, self)._get_classified_fields()
if 'website_theme_id' in dir(self):
ir_module = self.env['ir.module.module']
install_theme_lst = []
uninstall_theme_lst = []
install_theme_lst.append(self.website_theme_id)
theme_un = ir_module.sudo().search(
['|', ('category_id.name', '=', 'Theme'),
('category_id.parent_id.name', '=', 'Theme')]
)
for theme in theme_un:
if not theme.website_ids and len(theme.website_ids.ids) < 1:
uninstall_theme_lst.append(theme)
res.update({
'install_theme': install_theme_lst,
'uninstall_theme': uninstall_theme_lst
})
return res
# Overriding Method
@api.multi
def execute(self):
self.ensure_one()
# Multi Website: Do not allow more than 1 website as default website
if self.env['website'].search_count(
[('is_default_website', '=', True)]) > 1:
raise Warning(
_('You can define only one website as default one.\n'
'More than one websites are not allowed '
'as default website.'))
if not self.env.user._is_superuser() and not \
self.env.user.has_group('base.group_system'):
raise AccessError(_("Only administrators can change the settings"))
self = self.with_context(active_test=False)
classified = self._get_classified_fields()
# default values fields
IrDefault = self.env['ir.default'].sudo()
for name, model, field in classified['default']:
if isinstance(self[name], models.BaseModel):
if self._fields[name].type == 'many2one':
value = self[name].id
else:
value = self[name].ids
else:
value = self[name]
IrDefault.set(model, field, value)
# group fields: modify group / implied groups
for name, groups, implied_group in classified['group']:
if self[name]:
groups.write({'implied_ids': [(4, implied_group.id)]})
else:
groups.write({'implied_ids': [(3, implied_group.id)]})
implied_group.write({'users': [(3, user.id) for user in
groups.mapped('users')]})
# other fields: execute method 'set_values'
# Methods that start with `set_` are now deprecated
for method in dir(self):
if method.startswith('set_') and method is not 'set_values':
_logger.warning(_('Methods that start with `set_` '
'are deprecated. Override `set_values` '
'instead (Method %s)') % method)
self.set_values()
# module fields: install/uninstall the selected modules
to_install = []
to_upgrade = self.env['ir.module.module']
to_uninstall_modules = self.env['ir.module.module']
lm = len('module_')
for name, module in classified['module']:
if self[name]:
to_install.append((name[lm:], module))
else:
if module and module.state in ('installed', 'to upgrade'):
to_uninstall_modules += module
if 'install_theme' in classified and 'uninstall_theme' in classified:
for theme in classified['install_theme']:
if theme:
to_install.append((theme.name, theme))
if theme.state == 'installed':
to_upgrade += theme
for theme in classified['uninstall_theme']:
if theme and theme.state in ('installed', 'to upgrade'):
to_uninstall_modules += theme
if to_uninstall_modules:
to_uninstall_modules.button_immediate_uninstall()
if to_upgrade:
to_upgrade.button_immediate_upgrade()
self._install_modules(to_install)
if to_install or to_uninstall_modules:
# After the uninstall/install calls, the registry and environments
# are no longer valid. So we reset the environment.
self.env.reset()
self = self.env()[self._name]
# pylint: disable=next-method-called
config = self.env['res.config'].next() or {}
if config.get('type') not in ('ir.actions.act_window_close',):
return config
# force client-side reload (update user menu and current view)
return {
'type': 'ir.actions.client',
'tag': 'reload',
}

View File

@ -5,6 +5,7 @@ import inspect
import logging import logging
import hashlib import hashlib
import re import re
from uuid import uuid4
from werkzeug import urls from werkzeug import urls
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
@ -17,6 +18,7 @@ from flectra.tools import pycompat
from flectra.http import request from flectra.http import request
from flectra.osv.expression import FALSE_DOMAIN from flectra.osv.expression import FALSE_DOMAIN
from flectra.tools.translate import _ from flectra.tools.translate import _
from flectra.exceptions import Warning
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -71,6 +73,17 @@ class Website(models.Model):
menu_id = fields.Many2one('website.menu', compute='_compute_menu', string='Main Menu') menu_id = fields.Many2one('website.menu', compute='_compute_menu', string='Main Menu')
homepage_id = fields.Many2one('website.page', string='Homepage') 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.") favicon = fields.Binary(string="Website Favicon", help="This field holds the image used to display a favicon on the website.")
is_default_website = fields.Boolean(string='Default Website', readonly=1)
website_code = fields.Char(string='Website Code', readonly=1,
default=lambda self: uuid4().hex[:8],
help='Unique code per website.')
website_theme_id = fields.Many2one('ir.module.module', string='Theme',
help='Choose theme for current '
'website.')
_sql_constraints = [
('domain_uniq', 'unique(domain)', 'Domain name already exists !'),
]
@api.multi @api.multi
def _compute_menu(self): def _compute_menu(self):
@ -82,14 +95,95 @@ class Website(models.Model):
def noop(self, *args, **kwargs): def noop(self, *args, **kwargs):
pass pass
# ----------------------------------------------------------
# Multi Website
# ----------------------------------------------------------
@api.multi @api.multi
def write(self, values): def write(self, values):
self._get_languages.clear_cache(self) self._get_languages.clear_cache(self)
if values.get('website_code') or values.get('is_default_website'):
raise Warning(_('Unexpected bad things will happen!\n'
'Changing website code or default website '
'can have unintended side effects.\n'
'- We will not updated your old views.\n'
'- If above action is not properly done '
'then it will break your current '
'multi website feature.'))
return super(Website, self).write(values) return super(Website, self).write(values)
#---------------------------------------------------------- @api.model
def create(self, values):
res = super(Website, self).create(values)
default_website = self.env['website'].search([(
'is_default_website', '=', True)])
if not len(default_website) or len(default_website) > 1:
raise Warning(_('Either default website is not defined '
'or multiple default website is defined!!\n'
'You can define only one website as '
'default website.'))
website_menu = self.env['website.menu']
ir_model_data = self.env['ir.model.data']
# Menu Entries:
# Clone top menu & home menu of default website for new website
top_menu = self.env.ref('website.main_menu', False)
home_menu = self.env.ref('website.menu_homepage', False)
new_home_menu = False
if top_menu and home_menu:
top_menu = website_menu.search([
('id', '=', self.env.ref('website.main_menu').id),
('website_id', '=', default_website.id)])
home_menu = website_menu.search([
('id', '=', self.env.ref('website.menu_homepage').id),
('website_id', '=', default_website.id)])
new_top_menu = top_menu.copy()
new_top_menu.write({
'website_id': res.id,
})
new_home_menu = home_menu.copy()
new_home_menu.write({
'website_id': res.id,
'parent_id': new_top_menu.id,
})
# Home Page & View Entry:
# Clone home page & view of default website for new website
home_page = self.env.ref('website.homepage_page', False)
if home_page and new_home_menu:
new_home_page = home_page.copy()
new_home_page.view_id.write({
'name': home_page.view_id.name,
'website_id': res.id,
'key': home_page.view_id.key + '_' + res.website_code,
'is_cloned': True,
})
home_model_data_id = ir_model_data.create({
'model': home_page.view_id.model_data_id.model,
'name': home_page.view_id.model_data_id.name +
'_' + res.website_code,
'res_id': new_home_page.view_id.id,
'module': home_page.view_id.model_data_id.module,
})
new_home_page.view_id.write({
'model_data_id': home_model_data_id
})
new_home_page.write({
'url': home_page.url,
'view_id': new_home_page.view_id.id,
'website_published': True,
'website_ids': [(6, 0, [res.id])],
'menu_ids': [(6, 0, [new_home_menu.id])],
})
return res
# ----------------------------------------------------------
# Page Management # Page Management
#---------------------------------------------------------- # ----------------------------------------------------------
@api.model @api.model
def new_page(self, name=False, add_menu=False, 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 """ Create a new website page, and assign it a xmlid based on the given one
@ -315,9 +409,9 @@ class Website(models.Model):
except Exception: except Exception:
return False return False
#---------------------------------------------------------- # ----------------------------------------------------------
# Languages # Languages
#---------------------------------------------------------- # ----------------------------------------------------------
@api.multi @api.multi
def get_languages(self): def get_languages(self):
@ -362,9 +456,9 @@ class Website(models.Model):
lang['hreflang'] = lang['short'] lang['hreflang'] = lang['short']
return langs return langs
#---------------------------------------------------------- # ----------------------------------------------------------
# Utilities # Utilities
#---------------------------------------------------------- # ----------------------------------------------------------
@api.model @api.model
def get_current_website(self): def get_current_website(self):

View File

@ -30,6 +30,7 @@ var Dashboard = Widget.extend(ControlPanelMixin, {
'click .o_dashboard_action': 'on_dashboard_action', 'click .o_dashboard_action': 'on_dashboard_action',
'click .o_dashboard_action_form': 'on_dashboard_action_form', 'click .o_dashboard_action_form': 'on_dashboard_action_form',
'click .o_dashboard_hide_panel': 'on_dashboard_hide_panel', 'click .o_dashboard_hide_panel': 'on_dashboard_hide_panel',
'click li.js_website_deshboard': 'js_website_deshboard',
}, },
init: function(parent, context) { init: function(parent, context) {
@ -42,6 +43,8 @@ var Dashboard = Widget.extend(ControlPanelMixin, {
this.dashboards_templates = ['website.dashboard_visits']; this.dashboards_templates = ['website.dashboard_visits'];
this.graphs = []; this.graphs = [];
this.is_bound = $.Deferred();
this.dashboards_header = ['website.dashboard_header'];
}, },
willStart: function() { willStart: function() {
@ -58,21 +61,26 @@ var Dashboard = Widget.extend(ControlPanelMixin, {
self.render_dashboards(); self.render_dashboards();
self.render_graphs(); self.render_graphs();
self.$el.parent().addClass('oe_background_grey'); self.$el.parent().addClass('oe_background_grey');
self.bind_menu();
}); });
}, },
/** /**
* Fetches dashboard data * Fetches dashboard data
*/ */
fetch_data: function() { fetch_data: function(website_id=null) {
var self = this; var self = this;
return this._rpc({ return this._rpc({
route: '/website/fetch_dashboard_data', route: '/website/fetch_dashboard_data',
params: { params: {
date_from: this.date_from.year()+'-'+(this.date_from.month()+1)+'-'+this.date_from.date(), date_from: this.date_from.year()+'-'+(this.date_from.month()+1)+'-'+this.date_from.date(),
date_to: this.date_to.year()+'-'+(this.date_to.month()+1)+'-'+this.date_to.date(), date_to: this.date_to.year()+'-'+(this.date_to.month()+1)+'-'+this.date_to.date(),
'website_id': website_id,
}, },
}).done(function(result) { }).done(function(result) {
self.website_ids = result.website_ids;
self.website = result.website;
self.current_website = result.current_website;
self.data = result; self.data = result;
self.dashboards_data = result.dashboards; self.dashboards_data = result.dashboards;
self.currency_id = result.currency_id; self.currency_id = result.currency_id;
@ -80,6 +88,80 @@ var Dashboard = Widget.extend(ControlPanelMixin, {
}); });
}, },
js_website_deshboard: function(ev){
ev.preventDefault();
var self = this;
$.when(this.fetch_data($(ev.target).data('website_id'))).then(function() {
self.$('.o_website_dashboard_content').empty();
self.$('.o_dashboard_common').remove();
self.render_dashboards();
self.render_dashboards_header();
self.render_graphs();
});
},
render_dashboards_header: function() {
var self = this;
_.each(this.dashboards_header, function(template) {
self.$('.o_website_dashboard_content').prepend(QWeb.render(template, {widget: self}));
});
},
bind_menu: function() {
var self = this;
var lazyreflow = _.debounce(this.reflow.bind(this), 200);
core.bus.on('resize', this, function() {
if ($(window).width() < 768 ) {
lazyreflow('all_outside');
} else {
lazyreflow();
}
});
core.bus.trigger('resize');
this.is_bound.resolve();
},
reflow: function(behavior) {
var self = this;
var $more_container = this.$('#website_more_container').hide();
var $more = this.$('#website_more');
$more.children('li').insertBefore($more_container);
if (behavior === 'all_outside') {
// Show list of menu items
self.$el.show();
this.$el.find('li').show();
$more_container.hide();
return;
}
var $toplevel_items = this.$el.find('li').not($more_container).hide();
self.$el.show();
$toplevel_items.each(function() {
var remaining_space = self.$el.find('div.navbar-collapse.collapse').width() - $more_container.outerWidth();
self.$el.find('div.navbar-collapse.collapse ul.website_tab :visible').each
(function() {
if($(this).parent("ul").length){
remaining_space -= $(this).width();
}
});
if ($(this).width() >= remaining_space) {
return false;
}
$(this).show();
});
$more.append($toplevel_items.filter(':hidden').show());
$more_container.toggle(!!$more.children().length);
var $toplevel = self.$el.children("ul.website_tab li:visible");
if ($toplevel.length === 1) {
$toplevel.hide();
}
},
on_link_analytics_settings: function(ev) { on_link_analytics_settings: function(ev) {
ev.preventDefault(); ev.preventDefault();

View File

@ -7,9 +7,26 @@
<t t-name="website.WebsiteDashboardMain"> <t t-name="website.WebsiteDashboardMain">
<div class="o_dashboards"> <div class="o_dashboards">
<div class="container-fluid o_website_dashboard"> <div class="o_dashboards">
<t t-call="website.dashboard_header"/> <div class="navbar-collapse collapse" style="padding:0;">
<div class="o_website_dashboard_content"/> <ul class="nav nav-tabs website_tab">
<li t-foreach="widget.website_ids" t-as="website"
t-attf-class="js_website_deshboard #{widget.current_website == website.domain and 'active' or ''}">
<a data-toggle="tab" t-att-data-website_id="website.id">
<t t-esc="website.name"/>
</a>
</li>
<li id="website_more_container" class="dropdown" style="display: none;">
<a href="#" class="dropdown-toggle"
data-toggle="dropdown">More <b class="caret"/></a>
<ul id="website_more" class="dropdown-menu"/>
</li>
</ul>
</div>
<div class="container-fluid o_website_dashboard">
<t t-call="website.dashboard_header"/>
<div class="o_website_dashboard_content"/>
</div>
</div> </div>
</div> </div>
</t> </t>
@ -17,7 +34,9 @@
<t t-name="website.dashboard_header"> <t t-name="website.dashboard_header">
<div class="row o_dashboard_common"> <div class="row o_dashboard_common">
<div class="o_box"> <div class="o_box">
<a href="#" class="o_box_item o_dashboard_action" name="website.action_website" title="Go to Website"> <a t-attf-href="#{widget.website}"
class="o_box_item o_dashboard_action"
name="website.action_website" title="Go to Website">
<div class="o_inner_box o_primary"> <div class="o_inner_box o_primary">
<i class="fa fa-globe fa-3x"></i><br/> <i class="fa fa-globe fa-3x"></i><br/>
Go to Website Go to Website

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<flectra>
<record id="base_module_form_inherit" model="ir.ui.view">
<field name="name">ir.module.module.form.inherit</field>
<field name="model">ir.module.module</field>
<field name="inherit_id" ref="base.module_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='state']" position="after">
<field name="website_ids" widget="many2many_tags"
invisible="0"/>
</xpath>
</field>
</record>
</flectra>

View File

@ -10,24 +10,30 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//div[hasclass('settings')]" position="inside"> <xpath expr="//div[hasclass('settings')]" position="inside">
<div class="app_settings_block" data-string="Website" string="Website" data-key="website" groups="website.group_website_designer"> <div class="app_settings_block" data-string="Website" string="Website" data-key="website" groups="website.group_website_designer">
<field name="website_id" invisible="1"/>
<h2>Website</h2> <h2>Website</h2>
<div class="row mt16 o_settings_container" id="webmaster_settings"> <div class="row mt16 o_settings_container" id="webmaster_settings">
<div class="col-xs-12 col-md-6 o_setting_box" id="domain_setting"> <div class="col-xs-12 col-md-6 o_setting_box" id="domain_setting">
<div class="o_setting_right_pane"> <div class="o_setting_right_pane">
<label string="Website Title"/> <label string="Website Title"/>
<div class="text-muted"> <div class="text-muted">
Name and favicon of your website Name, favicon &amp; theme of your website
</div> </div>
<div class="content-group"> <div class="content-group">
<div class="row mt16"> <div class="row mt16">
<label class="col-md-3 o_light_label" string="Name"/> <label class="col-md-3 o_light_label" string="Name"/>
<field name="website_name"/> <field name="website_name"/>
</div> </div>
<div class="row"> <div class="row mt8">
<label class="col-md-3 o_light_label" for="favicon" /> <label class="col-md-3 o_light_label" for="favicon" />
<field name="favicon" widget="image" class="pull-left oe_avatar"/> <field name="favicon" widget="image" class="pull-left oe_avatar"/>
</div> </div>
<div class="row">
<label class="col-md-3 o_light_label" for="website_theme_id"/>
<field name="website_theme_id"
options="{'no_create': True, 'no_open': True}"
domain="['|', ('category_id.name', '=', 'Theme'),
('category_id.parent_id.name', '=', 'Theme')]"/>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -69,7 +75,7 @@
</div> </div>
</div> </div>
<div attrs="{'invisible': [('has_google_analytics', '=', False)]}"> <div attrs="{'invisible': [('has_google_analytics', '=', False)]}">
<a href="https://www.flectra.com/documentation/user/11.0/website/optimize/google_analytics.html" <a href="https://www.flectrahq.com/documentation/user/11.0/website/optimize/google_analytics.html"
class="oe_link fa fa-arrow-right" target="_blank"> class="oe_link fa fa-arrow-right" target="_blank">
How to get my Tracking ID How to get my Tracking ID
</a> </a>
@ -96,7 +102,7 @@
</div> </div>
</div> </div>
<div attrs="{'invisible': [('has_google_analytics_dashboard', '=', False)]}"> <div attrs="{'invisible': [('has_google_analytics_dashboard', '=', False)]}">
<a href="https://www.flectra.com/documentation/user/online/website/optimize/google_analytics_dashboard.html" <a href="https://www.flectrahq.com/documentation/user/online/website/optimize/google_analytics_dashboard.html"
class="oe_link fa fa-arrow-right" target="_blank"> class="oe_link fa fa-arrow-right" target="_blank">
How to get my Client ID How to get my Client ID
</a> </a>
@ -195,6 +201,24 @@
<field name="context">{'module' : 'website'}</field> <field name="context">{'module' : 'website'}</field>
</record> </record>
<record id="action_ui_qweb_view" model="ir.actions.act_window">
<field name="name">QWeb Views</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">ir.ui.view</field>
<field name="view_id" ref="base.view_view_tree"/>
<field name="context">{'search_default_type': 'qweb',
'search_default_group_website_id': True}
</field>
</record>
<record id="action_website_website_list" model="ir.actions.act_window">
<field name="name">Websites</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">website</field>
<field name="view_id" ref="website.view_website_tree"/>
<field name="context">{}</field>
</record>
<menuitem id="menu_website_global_configuration" parent="menu_website_configuration" <menuitem id="menu_website_global_configuration" parent="menu_website_configuration"
sequence="100" name="Configuration" groups="base.group_system"/> sequence="100" name="Configuration" groups="base.group_system"/>
<menuitem name="Settings" <menuitem name="Settings"
@ -217,4 +241,25 @@
sequence="30" sequence="30"
groups="base.group_no_one"/> groups="base.group_no_one"/>
<menuitem name="Websites"
id="menu_website_website_list"
action="action_website_website_list"
parent="menu_website_global_configuration"
sequence="10"
groups="base.group_no_one"/>
<menuitem name="Menus"
id="menu_website_menus_list"
action="action_website_menu"
parent="menu_website_global_configuration"
sequence="40"
groups="base.group_no_one"/>
<menuitem name="QWeb Views"
id="menu_website_qweb_views_list"
action="action_ui_qweb_view"
parent="menu_website_global_configuration"
sequence="50"
groups="base.group_no_one"/>
</flectra> </flectra>

View File

@ -21,8 +21,10 @@
<div name="domain"> <div name="domain">
<separator name="domain" string="Domain"/> <separator name="domain" string="Domain"/>
<group name="domain"> <group name="domain">
<field name="name"/> <field name="name" required="True"/>
<field name="domain"/> <field name="domain" required="True"/>
<field name="website_code" invisible="True"/>
<field name="is_default_website" invisible="True"/>
<field name="google_analytics_key" placeholder="UA-XXXXXXXX-Y"/> <field name="google_analytics_key" placeholder="UA-XXXXXXXX-Y"/>
</group> </group>
</div> </div>
@ -57,6 +59,8 @@
<field name="name"/> <field name="name"/>
<field name="company_id" groups="base.group_multi_company"/> <field name="company_id" groups="base.group_multi_company"/>
<field name="default_lang_id"/> <field name="default_lang_id"/>
<field name="website_code"/>
<field name="is_default_website"/>
</tree> </tree>
</field> </field>
</record> </record>
@ -212,6 +216,7 @@
<field name="name" position="after"> <field name="name" position="after">
<field name="website_id" readonly="1"/> <field name="website_id" readonly="1"/>
<field name="key" readonly="1"/> <field name="key" readonly="1"/>
<field name="is_cloned"/>
<field name="page_ids" invisible="1" /> <field name="page_ids" invisible="1" />
</field> </field>
<sheet position="before"> <sheet position="before">
@ -223,6 +228,20 @@
</field> </field>
</record> </record>
<!-- ir.ui.view search -->
<record model="ir.ui.view" id="view_view_search_extend">
<field name="model">ir.ui.view</field>
<field name="inherit_id" ref="base.view_view_search"/>
<field name="arch" type="xml">
<field name="type" position="after">
<field name="website_id"/>
</field>
<xpath expr="//group" position="inside">
<filter name="group_website_id" string="Website" domain="[]" context="{'group_by':'website_id'}"/>
</xpath>
</field>
</record>
<!-- Dashboard --> <!-- Dashboard -->
<record id="backend_dashboard" model="ir.actions.client"> <record id="backend_dashboard" model="ir.actions.client">
<field name="name">Dashboard</field> <field name="name">Dashboard</field>

View File

@ -157,7 +157,6 @@
<record model="ir.module.category" id="module_category_theme"> <record model="ir.module.category" id="module_category_theme">
<field name="name">Theme</field> <field name="name">Theme</field>
<field name="exclusive" eval="1"/>
<field name="sequence">50</field> <field name="sequence">50</field>
</record> </record>