diff --git a/website_cdn_support/__openerp__.py b/website_cdn_support/__openerp__.py deleted file mode 100755 index 3ee5d26..0000000 --- a/website_cdn_support/__openerp__.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# Odoo, an open source suite of business apps -# This module copyright (C) 2015 bloopark systems (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## -{ - 'name': "CDN support", - 'summary': """CDN support for website""", - 'description': """CDN support for website""", - 'author': "bloopark systems GmbH & Co. KG", - 'website': "http://www.bloopark.de", - 'license': 'AGPL-3', - 'category': 'Speed', - 'version': '1.0', - - 'depends': [ - 'base', - 'website' - ], - - 'data': [ - 'views/res_config.xml' - ], -} diff --git a/website_cdn_support/models/__init__.py b/website_cdn_support/models/__init__.py deleted file mode 100755 index 85f6f0e..0000000 --- a/website_cdn_support/models/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# Odoo, an open source suite of business apps -# This module copyright (C) 2015 bloopark systems (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## -from . import res_config -from . import website -from . import ir_qweb diff --git a/website_cdn_support/models/ir_qweb.py b/website_cdn_support/models/ir_qweb.py deleted file mode 100755 index 170d07d..0000000 --- a/website_cdn_support/models/ir_qweb.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# Odoo, an open source suite of business apps -# This module copyright (C) 2015 bloopark systems (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## -import collections -from openerp.osv import orm -from openerp.addons.web.http import request - - -class QWeb(orm.AbstractModel): - - """ - QWeb object for rendering stuff in the website context. - """ - _inherit = 'website.qweb' - - CDN_TRIGGERS = { - 'link': 'href', - 'script': 'src', - 'img': 'src', - } - - def render_attribute(self, element, name, value, qwebcontext): - context = qwebcontext.context or {} - if not context.get('rendering_bundle'): - if name == self.URL_ATTRS.get(element.tag): - value = qwebcontext.get('url_for')(value) - if request and request.website and request.website.cdn_activated\ - and name == self.CDN_TRIGGERS.get(element.tag): - value = request.website.get_cdn_url(value) - - return super(QWeb, self).render_attribute( - element, name, value, qwebcontext) - - def render_tag_call_assets( - self, element, template_attributes, generated_attributes, - qwebcontext): - if request and request.website and request.website.cdn_activated: - if qwebcontext.context is None: - qwebcontext.context = {} - qwebcontext.context['url_for'] = request.website.get_cdn_url - return super(QWeb, self).render_tag_call_assets( - element, template_attributes, generated_attributes, qwebcontext) - - def render_att_att( - self, element, attribute_name, attribute_value, qwebcontext): - if attribute_name.startswith("t-attf-"): - return [(attribute_name[7:], self.eval_format( - attribute_value, qwebcontext))] - - if attribute_name.startswith("t-att-"): - return [(attribute_name[6:], self.eval( - attribute_value, qwebcontext))] - - result = self.eval_object(attribute_value, qwebcontext) - if isinstance(result, collections.Mapping): - return result.iteritems() - # assume tuple - return [result] diff --git a/website_cdn_support/models/res_config.py b/website_cdn_support/models/res_config.py deleted file mode 100755 index b97096f..0000000 --- a/website_cdn_support/models/res_config.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# Odoo, an open source suite of business apps -# This module copyright (C) 2015 bloopark systems (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## -from openerp.models import TransientModel -from openerp import fields - - -class WebsiteConfigSettings(TransientModel): - - """Adds the fields for CDN.""" - - _inherit = 'website.config.settings' - - cdn_activated = fields.Boolean( - related='website_id.cdn_activated', - string='Use CDN' - ) - cdn_url = fields.Char( - related='website_id.cdn_url', - string='CDN Base URL' - ) - cdn_filters = fields.Text( - related='website_id.cdn_filters', - string='CDN Filters' - ) diff --git a/website_cdn_support/models/website.py b/website_cdn_support/models/website.py deleted file mode 100755 index 2548324..0000000 --- a/website_cdn_support/models/website.py +++ /dev/null @@ -1,56 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# Odoo, an open source suite of business apps -# This module copyright (C) 2015 bloopark systems (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## -from openerp.models import Model -from openerp import fields -from openerp.addons.web.http import request -import urlparse -import re - - -class Website(Model): - - """Adds the fields for CDN.""" - - _inherit = 'website' - - DEFAULT_CDN_FILTERS = [ - "^/[^/]+/static/", - "^/web/(css|js)/", - "^/website/image/", - ] - - cdn_activated = fields.Boolean('Activate CDN for assets', default=False) - cdn_url = fields.Char('CDN Base URL', default="//localhost:8069/") - cdn_filters = fields.Text( - 'CDN Filters', - help="URL matching those filters will be rewritten using the CDN " - "Base URL", - default='\n'.join(DEFAULT_CDN_FILTERS)) - - def get_cdn_url(self, cr, uid, uri, context=None): - # Currently only usable in a website_enable request context - if request and request.website and not request.debug: - cdn_url = request.website.cdn_url - cdn_filters = (request.website.cdn_filters or '').splitlines() - for flt in cdn_filters: - if flt and re.match(flt, uri): - return urlparse.urljoin(cdn_url, uri) - return uri diff --git a/website_cdn_support/readme.rst b/website_cdn_support/readme.rst deleted file mode 100755 index 892d211..0000000 --- a/website_cdn_support/readme.rst +++ /dev/null @@ -1,7 +0,0 @@ -======================= -CDN support for website -======================= - -added the option for CDN support from the master for version 8 - -you can turn it on or of in the backend \ No newline at end of file diff --git a/website_cdn_support/static/description/bloopark.png b/website_cdn_support/static/description/bloopark.png deleted file mode 100755 index ebbf675..0000000 Binary files a/website_cdn_support/static/description/bloopark.png and /dev/null differ diff --git a/website_cdn_support/static/description/cdn.png b/website_cdn_support/static/description/cdn.png deleted file mode 100755 index 4dba281..0000000 Binary files a/website_cdn_support/static/description/cdn.png and /dev/null differ diff --git a/website_cdn_support/static/description/icon.png b/website_cdn_support/static/description/icon.png deleted file mode 100755 index 2a7dfd1..0000000 Binary files a/website_cdn_support/static/description/icon.png and /dev/null differ diff --git a/website_cdn_support/static/description/index.html b/website_cdn_support/static/description/index.html deleted file mode 100755 index 86f020b..0000000 --- a/website_cdn_support/static/description/index.html +++ /dev/null @@ -1,43 +0,0 @@ -
-
-
-

CDN support for website

-
-
-

added the option for CDN support from the master for version 8

- -

you can turn it on or of in the backend

-
-
- -
-
-
- -
-
-

- - bloopark systems GmbH & Co. KG - -

- -
-

- We are an internet agency and Odoo partner based in Magdeburg, Germany with over 10 years of experience in e-commerce, responsive design and development. - We consider ourselves as A-team, small and efficient. - With remote developers all across the world, we are pride of our wide perspective and approach in every challenge, which leads to the best customer-oriented solutions. -

-
-
-
-

 

- -
- -
-
-
-
-
-
diff --git a/website_cdn_support/views/res_config.xml b/website_cdn_support/views/res_config.xml deleted file mode 100755 index eb0fea5..0000000 --- a/website_cdn_support/views/res_config.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - CDN support - website.config.settings - - - - - - - - - - - diff --git a/website_compress_html/__init__.py b/website_compress_html/__init__.py deleted file mode 100755 index 6370c23..0000000 --- a/website_compress_html/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# Odoo, an open source suite of business apps -# This module copyright (C) 2015 bloopark systems (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## -from . import models -from . import ir diff --git a/website_compress_html/__openerp__.py b/website_compress_html/__openerp__.py deleted file mode 100755 index 3f688d0..0000000 --- a/website_compress_html/__openerp__.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# Odoo, an open source suite of business apps -# This module copyright (C) 2015 bloopark systems (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## -{ - 'name': "Compress HTML", - 'summary': """Compress HTML Code Output""", - 'description': """Compress HTML Code Output""", - 'author': "bloopark systems GmbH & Co. KG", - 'website': "http://www.bloopark.de", - 'license': 'AGPL-3', - 'category': 'Speed', - 'version': '1.0', - - 'depends': [ - 'base', - 'website' - ], - - 'data': [ - 'views/res_config.xml' - ], -} diff --git a/website_compress_html/ir/__init__.py b/website_compress_html/ir/__init__.py deleted file mode 100755 index e8ad1ff..0000000 --- a/website_compress_html/ir/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# Odoo, an open source suite of business apps -# This module copyright (C) 2015 bloopark systems (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## -from . import ir_qweb diff --git a/website_compress_html/ir/ir_qweb.py b/website_compress_html/ir/ir_qweb.py deleted file mode 100755 index 6eb9586..0000000 --- a/website_compress_html/ir/ir_qweb.py +++ /dev/null @@ -1,137 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# Odoo, an open source suite of business apps -# This module copyright (C) 2015 bloopark systems (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## -from lxml import etree -from openerp.addons.base.ir import ir_qweb -from openerp.addons.base.ir.ir_qweb import QWebException -from openerp.addons.base.ir.ir_qweb import raise_qweb_exception - - -class QWeb(ir_qweb.QWeb): - - """QWeb object for rendering stuff in the website context.""" - - def render_node(self, element, qwebcontext): - generated_attributes = "" - t_render = None - template_attributes = {} - for (attribute_name, attribute_value) in element.attrib.iteritems(): - attribute_name = str(attribute_name) - if attribute_name == "groups": - cr = qwebcontext.get('request') and qwebcontext[ - 'request'].cr or None - uid = qwebcontext.get('request') and qwebcontext[ - 'request'].uid or None - can_see = self.user_has_groups( - cr, uid, groups=attribute_value) if cr and uid else False - if not can_see: - return '' - - attribute_value = attribute_value.encode("utf8") - - if attribute_name.startswith("t-"): - for attribute in self._render_att: - if attribute_name[2:].startswith(attribute): - attrs = self._render_att[attribute]( - self, element, attribute_name, attribute_value, - qwebcontext) - for att, val in attrs: - if not val: - continue - if not isinstance(val, str): - val = unicode(val).encode('utf-8') - generated_attributes += self.render_attribute( - element, att, val, qwebcontext) - break - else: - if attribute_name[2:] in self._render_tag: - t_render = attribute_name[2:] - template_attributes[attribute_name[2:]] = attribute_value - else: - generated_attributes += self.render_attribute( - element, attribute_name, attribute_value, qwebcontext) - - if 'debug' in template_attributes: - debugger = template_attributes.get('debug', 'pdb') - __import__(debugger).set_trace() # pdb, ipdb, pudb, ... - if t_render: - result = self._render_tag[t_render]( - self, element, template_attributes, generated_attributes, - qwebcontext) - else: - result = self.render_element( - element, template_attributes, generated_attributes, - qwebcontext) - - if element.tail: - result += self.render_tail(element.tail, element, qwebcontext) - - if isinstance(result, unicode): - return result.encode('utf-8') - return result - - def render_element( - self, element, template_attributes, generated_attributes, - qwebcontext, inner=None): - # element: element - # template_attributes: t-* attributes - # generated_attributes: generated attributes - # qwebcontext: values - # inner: optional innerXml - if inner: - g_inner = inner.encode('utf-8') if isinstance(inner, unicode) \ - else inner - else: - g_inner = [] if element.text is None else [self.render_text( - element.text, element, qwebcontext)] - for current_node in element.iterchildren(tag=etree.Element): - try: - g_inner.append(self.render_node(current_node, qwebcontext)) - except QWebException: - raise - except Exception: - template = qwebcontext.get('__template__') - raise_qweb_exception( - message="Could not render element %r" % element.tag, - node=element, template=template) - name = str(element.tag) - inner = "".join(g_inner) - trim = template_attributes.get("trim", 0) - if trim == 0: - pass - elif trim == 'left': - inner = inner.lstrip() - elif trim == 'right': - inner = inner.rstrip() - elif trim == 'both': - inner = inner.strip() - if name == "t": - return inner - elif len(inner) or name not in self._void_elements: - return "<%s%s>%s" % tuple( - qwebcontext if isinstance(qwebcontext, str) else - qwebcontext.encode('utf-8') - for qwebcontext in (name, generated_attributes, inner, name) - ) - else: - return "<%s%s/>" % (name, generated_attributes) - - def render_text(self, text, element, qwebcontext): - return text.encode('utf-8') diff --git a/website_compress_html/models/__init__.py b/website_compress_html/models/__init__.py deleted file mode 100755 index 85f6f0e..0000000 --- a/website_compress_html/models/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# Odoo, an open source suite of business apps -# This module copyright (C) 2015 bloopark systems (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## -from . import res_config -from . import website -from . import ir_qweb diff --git a/website_compress_html/models/ir_qweb.py b/website_compress_html/models/ir_qweb.py deleted file mode 100755 index c07ee51..0000000 --- a/website_compress_html/models/ir_qweb.py +++ /dev/null @@ -1,59 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# Odoo, an open source suite of business apps -# This module copyright (C) 2015 bloopark systems (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## -import re -from openerp.osv import orm -from openerp.addons.web.http import request - - -class QWeb(orm.AbstractModel): - - """QWeb object for rendering stuff in the website context.""" - - _inherit = 'website.qweb' - - URL_ATTRS = { - 'form': 'action', - 'a': 'href', - } - re_remove_spaces = re.compile(r'\s+') - PRESERVE_WHITESPACE = [ - 'pre', - 'textarea', - 'script', - 'style', - ] - - def render_text(self, text, element, qwebcontext): - compress = request and not request.debug and request.website and \ - request.website.compress_html - if compress and element.tag not in self.PRESERVE_WHITESPACE: - text = self.re_remove_spaces.sub(' ', text.lstrip()) - return super(QWeb, self).render_text(text, element, qwebcontext) - - def render_tail(self, tail, element, qwebcontext): - compress = request and not request.debug and request.website and \ - request.website.compress_html - if compress and element.getparent().tag not in \ - self.PRESERVE_WHITESPACE: - # No need to recurse because those tags children are not html5 - # parser friendly - tail = self.re_remove_spaces.sub(' ', tail.rstrip()) - return super(QWeb, self).render_tail(tail, element, qwebcontext) diff --git a/website_compress_html/models/res_config.py b/website_compress_html/models/res_config.py deleted file mode 100755 index cf917c3..0000000 --- a/website_compress_html/models/res_config.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# Odoo, an open source suite of business apps -# This module copyright (C) 2015 bloopark systems (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## -from openerp.models import TransientModel -from openerp import fields - - -class WebsiteConfigSettings(TransientModel): - _inherit = 'website.config.settings' - - compress_html = fields.Boolean( - string='Compress HTML', - related='website_id.compress_html', - help=('Compress HTML Code Output') - ) diff --git a/website_compress_html/models/website.py b/website_compress_html/models/website.py deleted file mode 100755 index 530f32b..0000000 --- a/website_compress_html/models/website.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# Odoo, an open source suite of business apps -# This module copyright (C) 2015 bloopark systems (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## -from openerp.models import Model -from openerp import fields - - -class Website(Model): - _inherit = 'website' - - compress_html = fields.Boolean('Compress HTML', default=False) diff --git a/website_compress_html/readme.rst b/website_compress_html/readme.rst deleted file mode 100755 index 657baf6..0000000 --- a/website_compress_html/readme.rst +++ /dev/null @@ -1,7 +0,0 @@ -========================= -Compress HTML Code Output -========================= - -added the option to activate the HTML compession from the master for version 8 - -you can turn it on or off in the backend \ No newline at end of file diff --git a/website_compress_html/static/description/bloopark.png b/website_compress_html/static/description/bloopark.png deleted file mode 100755 index ebbf675..0000000 Binary files a/website_compress_html/static/description/bloopark.png and /dev/null differ diff --git a/website_compress_html/static/description/html_compress.png b/website_compress_html/static/description/html_compress.png deleted file mode 100755 index 8ea14fa..0000000 Binary files a/website_compress_html/static/description/html_compress.png and /dev/null differ diff --git a/website_compress_html/static/description/icon.png b/website_compress_html/static/description/icon.png deleted file mode 100755 index 2a7dfd1..0000000 Binary files a/website_compress_html/static/description/icon.png and /dev/null differ diff --git a/website_compress_html/static/description/index.html b/website_compress_html/static/description/index.html deleted file mode 100755 index d1a6b66..0000000 --- a/website_compress_html/static/description/index.html +++ /dev/null @@ -1,45 +0,0 @@ -
-
-
-

- Compress HTML Code Output -

-
- -
-

added the option to activate the HTML compession from the master for version 8

-

you can turn it on or of in the backend

-
-
- -
-
-
- -
-
-

- - bloopark systems GmbH & Co. KG - -

- -
-

- We are an internet agency and Odoo partner based in Magdeburg, Germany with over 10 years of experience in e-commerce, responsive design and development. - We consider ourselves as A-team, small and efficient. - With remote developers all across the world, we are pride of our wide perspective and approach in every challenge, which leads to the best customer-oriented solutions. -

-
-
-
-

 

- -
- -
-
-
-
-
-
diff --git a/website_compress_html/views/res_config.xml b/website_compress_html/views/res_config.xml deleted file mode 100755 index 9c617a2..0000000 --- a/website_compress_html/views/res_config.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - Compress HTML - website.config.settings - - - - - - - - - - - - - diff --git a/website_seo/__init__.py b/website_seo/__init__.py index c8f6e41..6238c62 100755 --- a/website_seo/__init__.py +++ b/website_seo/__init__.py @@ -18,5 +18,6 @@ # along with this program. If not, see . # ############################################################################## +from . import core from . import controllers from . import models diff --git a/website_seo/__openerp__.py b/website_seo/__openerp__.py index 048da2e..35cf80e 100755 --- a/website_seo/__openerp__.py +++ b/website_seo/__openerp__.py @@ -31,6 +31,7 @@ 'data/website_seo_data.xml', 'views/header.xml', 'views/ir_ui_view.xml', + 'views/website.xml', 'views/res_lang.xml', 'views/res_config.xml', 'views/website_templates.xml' diff --git a/website_seo/controllers/main.py b/website_seo/controllers/main.py index ac00872..818c1fb 100755 --- a/website_seo/controllers/main.py +++ b/website_seo/controllers/main.py @@ -47,9 +47,44 @@ def path_page(self, seo_url, **kwargs): current_view = views[-1] if (seo_url_parts == seo_url_check and (current_view.seo_url_level + 1) == len(views)): - page = current_view.xml_id - else: - if request.website.is_publisher(): - page = 'website.page_404' + page = current_view.key + + if page == 'website.404': + url = self.look_for_redirect_url(seo_url, **kwargs) + if url: + return request.redirect(url, code=301) + + if page == 'website.404' and request.website.is_publisher(): + page = 'website.page_404' return request.render(page, {}) + + def look_for_redirect_url(self, seo_url, **kwargs): + env = request.env(context=request.context) + if not seo_url.startswith('/'): + seo_url = '/%s' % seo_url + lang = env.context.get('lang', False) + if not lang: + lang = request.website.default_lang_code + lang = env['res.lang'].get_code_from_alias(lang) + domain = [('url', '=', seo_url), ('lang', '=', lang)] + data = env['website.seo.redirect'].search(domain) + if data: + model, rid = data[0].resource.split(',') + resource = env[model].browse(int(rid)) + return resource.get_seo_path()[0] + + @http.route() + def page(self, page, **opt): + try: + view = request.website.get_template(page) + if view.seo_url: + return request.redirect(view.get_seo_path()[0], code=301) + except: + pass + return super(Website, self).page(page, **opt) + + @http.route() + def seo_suggest(self, keywords=None, lang=None): + code = request.env['res.lang'].get_code_from_alias(lang) + return super(Website, self).seo_suggest(keywords=keywords, lang=code) diff --git a/website_cdn_support/__init__.py b/website_seo/core/__init__.py similarity index 97% rename from website_cdn_support/__init__.py rename to website_seo/core/__init__.py index e7747f1..89c2485 100755 --- a/website_cdn_support/__init__.py +++ b/website_seo/core/__init__.py @@ -18,4 +18,4 @@ # along with this program. If not, see . # ############################################################################## -from . import models +import core diff --git a/website_seo/core/core.py b/website_seo/core/core.py new file mode 100755 index 0000000..7e0bf58 --- /dev/null +++ b/website_seo/core/core.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Odoo, an open source suite of business apps +# This module copyright (C) 2015 bloopark systems (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +import types +from openerp import api, fields, models +from openerp.osv import expression + + +def modify_selection_to_be_translatable(): + + for k, v in fields._String.__dict__.items(): + if not k.startswith('__') and type(v) == types.FunctionType: + setattr(fields.Selection, k, v) + +modify_selection_to_be_translatable() + + +def exists_short_code(cr): + cr.execute("select column_name from information_schema.columns " + "where table_name='res_lang' AND column_name='short_code'") + return cr.fetchall() + + +def update_translated_field(): + + @api.model + def _extended_generate_translated_field(self, table_alias, field, query): + """ + Change short code lang (eg. en) for real code lang (eg. en_US) + """ + lang = self._context.get('lang') + lang_model = self.env['res.lang'] + if hasattr(lang_model, 'get_code_from_alias') and exists_short_code(self.env.cr): + lang = lang_model.get_code_from_alias(self._context.get('lang')) + if lang and lang != 'en_US': + alias, alias_statement = query.add_join( + (table_alias, 'ir_translation', 'id', 'res_id', field), + implicit=False, + outer=True, + extra='"{rhs}"."name" = %s AND "{rhs}"."lang" = %s AND "{rhs}"."value" != %s', + extra_params=["%s,%s" % (self._name, field), lang, ""], + ) + return 'COALESCE("%s"."%s", "%s"."%s")' % (alias, 'value', table_alias, field) + else: + return '"%s"."%s"' % (table_alias, field) + + setattr(models.Model, '_generate_translated_field', _extended_generate_translated_field) + +update_translated_field() + + +def update_lang_code_from_alias_in_expression(): + + def extended_init(self, cr, uid, exp, table, context): + + self._unaccent = expression.get_unaccent_wrapper(cr) + self.joins = [] + self.root_model = table + + # normalize and prepare the expression for parsing + self.expression = expression.distribute_not(expression.normalize_domain(exp)) + + # look for real lang from context before parse + parse_ctx = context.copy() + if parse_ctx.get('lang', False) and exists_short_code(cr): + cr.execute("select code from res_lang where short_code = '%s'" % parse_ctx['lang']) + res = cr.fetchall() + if res and res[0]: + parse_ctx.update({'lang': res[0][0]}) + + # parse the domain expression + self.parse(cr, uid, context=parse_ctx) + + setattr(expression.expression, '__init__', extended_init) + +update_lang_code_from_alias_in_expression() diff --git a/website_seo/data/website_seo_data.xml b/website_seo/data/website_seo_data.xml index f9968fa..916561f 100755 --- a/website_seo/data/website_seo_data.xml +++ b/website_seo/data/website_seo_data.xml @@ -4,17 +4,12 @@ website_meta_title - The title tag refers to the subject of the HTML page, it is very important to Google and should describe the content very well. -The title tag is displayed in the Google search results pages. -The title tag should a maximum of 55 characters (including spaces) long, if need be it can also be a little longer, it is then cut in the search results. -The keyword should, if possible, as far as possible be forward. + The title tag refers to the topic of the HTML page. It is very important for Google and should describe the content very well. The title tag is displayed in the Google search results pages. The title tag should have a maximum of 55 characters (including spaces). It can also have more characters, it will be cut in the search results. If possible the keyword should be at the beginning. website_meta_description - The meta description will be displayed on the Google search results pages. -The meta description is a detailed description of the HTML page, they should encourage users to the search result click (Call-to-Action). -The length should not exceed 156 characters (including spaces). + The meta description will be displayed on the Google search results pages. The meta description is a detailed characterization of the HTML page. It should encourage users to click on the search result (Call-to-Action). The length should be a maximum of 156 characters (including spaces). diff --git a/website_seo/i18n/de.po b/website_seo/i18n/de.po index 2b325d9..2f56fa9 100755 --- a/website_seo/i18n/de.po +++ b/website_seo/i18n/de.po @@ -41,12 +41,6 @@ msgstr "Webseite Menü" msgid "Only a-z, A-Z, 0-9, - and _ are allowed characters for the SEO url." msgstr "Nur a-z, A-Z, 0-9, - and _ sind erlaubte Zeichen für die SEO Url." -#. module: website_seo -#: code:addons/website_seo/static/src/xml/website_seo_robots.xml -#, python-format -msgid "Robots Content" -msgstr "Robots Inhalt" - #. module: website_seo #: help:ir.ui.view,seo_url_parent:0 msgid "" @@ -70,4 +64,139 @@ msgstr "" "\"how-to-do-it-right\" page is the third part and it needs the parent " "page \"study\". If all pages are configured correct the page " "\"how-to-do-it-right\" is rendered when calling " -"\"/ecommerce/study/how-to-do-it-right\"." \ No newline at end of file +"\"/ecommerce/study/how-to-do-it-right\"." + +#. module: website_seo +#: model:website.seo.information,information:website_seo.field_title_tip +msgid "The title tag refers to the topic of the HTML page. It is very important for Google and should describe the content very well. The title tag is displayed in the Google search results pages. The title tag should have a maximum of 55 characters (including spaces). It can also have more characters, it will be cut in the search results. If possible the keyword should be at the beginning." +msgstr "Der Title Tag bezieht sich auf das Thema der HTML Seite. Es ist für Google sehr wichtig und sollte den Seiteninhalt sehr gut beschreiben. Der Title Tag wird in den Google Suchergebnisseiten angezeigt. Der Title Tag sollte maximal 55 Zeichen (mit Leerzeichen) lang sein. Zur Not kann es auch ein wenig länger sein, es wird dann in den Suchergebnissen abgeschnitten. Das Schlüsselwort sollte wenn möglich vorn stehen." + +#. module: website_seo +#: model:website.seo.information,information:website_seo.field_description_tip +msgid "The meta description will be displayed on the Google search results pages. The meta description is a detailed characterization of the HTML page. It should encourage users to click on the search result (Call-to-Action). The length should be a maximum of 156 characters (including spaces)." +msgstr "Die Meta Beschreibung wird auf den Google Suchergebnisseiten angezeigt. Die Meta Beschreibung ist eine nähere Charakterisierung der HTML Seite. Sie soll Benutzer animieren auf das Suchergebnis zu klicken (Call-to-Action). Die Länge sollte maximal 156 Zeichen (mit Leerzeichen) betragen." + +#. module: website_seo +#. openerp-web +#: code:addons/website_seo/static/src/xml/website_seo_robots.xml:8 +#, python-format +msgid "" +"This form will save only the configuration for the selected language. If you change the language before save the SEO information recently changed will be lost." +msgstr "Dieses Formular wird nur die Konfiguration für die ausgewählte Sprache speichern. Wenn Sie die Sprache wechseln, bevor Sie die SEO-Informationen speichern, gehen diese Daten verloren." + +#. module: website_seo +#. openerp-web +#: code:addons/website_seo/static/src/xml/website_seo_robots.xml:13 +#, python-format +msgid "Language:" +msgstr "Sprache:" + +#. module: website_seo +#. openerp-web +#: code:addons/website_seo/static/src/xml/website_seo_robots.xml:21 +#, python-format +msgid "Page Title" +msgstr "Seitentitel" + +#. module: website_seo +#. openerp-web +#: code:addons/website_seo/static/src/xml/website_seo_robots.xml:32 +#, python-format +msgid "Description" +msgstr "Beschreibung" + +#. module: website_seo +#. openerp-web +#: code:addons/website_seo/static/src/xml/website_seo_robots.xml:42 +#, python-format +msgid "Robots Content" +msgstr "Robots Inhalt" + +#. module: website_seo +#. openerp-web +#: code:addons/website_seo/static/src/xml/website_seo_robots.xml:45 +#: selection:ir.ui.view,website_meta_robots:0 +#: selection:website.seo.metadata,website_meta_robots:0 +#, python-format +msgid "INDEX,FOLLOW" +msgstr "INDEX,FOLLOW" + +#. module: website_seo +#. openerp-web +#: code:addons/website_seo/static/src/xml/website_seo_robots.xml:47 +#: selection:ir.ui.view,website_meta_robots:0 +#: selection:website.seo.metadata,website_meta_robots:0 +#, python-format +msgid "INDEX,NOFOLLOW" +msgstr "INDEX,NOFOLLOW" + +#. module: website_seo +#. openerp-web +#: code:addons/website_seo/static/src/xml/website_seo_robots.xml:46 +#: selection:ir.ui.view,website_meta_robots:0 +#: selection:website.seo.metadata,website_meta_robots:0 +#, python-format +msgid "NOINDEX,FOLLOW" +msgstr "NOINDEX,FOLLOW" + +#. module: website_seo +#. openerp-web +#: code:addons/website_seo/static/src/xml/website_seo_robots.xml:48 +#: selection:ir.ui.view,website_meta_robots:0 +#: selection:website.seo.metadata,website_meta_robots:0 +#, python-format +msgid "NOINDEX,NOFOLLOW" +msgstr "NOINDEX,NOFOLLOW" + +#. module: website_seo +#. openerp-web +#: code:addons/website_seo/static/src/xml/website_seo_robots.xml:54 +#, python-format +msgid "SEO URL" +msgstr "SEO URL" + +#. module: website_seo +#: model:ir.ui.view,arch_db:website_seo.view_website_config_settings +msgid "SEO Pages" +msgstr "SEO Seiten" + +#. module: website_seo +#: model:ir.ui.view,arch_db:website_seo.view_website_config_settings +msgid "Configure SEO pages" +msgstr "Konfiguriere SEO Seiten" + +#. module: website_seo +#: model:ir.actions.act_window,name:website_seo.action_website_seo_pages +#: model:ir.ui.view,arch_db:website_seo.website_seo_pages_tree +msgid "Website SEO Pages" +msgstr "Webseite SEO Seiten" + +#. module: website_seo +#. openerp-web +#: code:addons/website_seo/static/src/js/seo_robots.js:509 +#, python-format +msgid "Invalid SEO URL. The allowed characters are a-z, A-Z, 0-9, - and _." +msgstr "Invalid SEO URL. The allowed characters are a-z, A-Z, 0-9, - and _." + +#. module: website_seo +#. openerp-web +#: code:addons/website_seo/static/src/js/seo_robots.js:512 +#, python-format +msgid "The SEO URL '%s' is already taken in the application." +msgstr "The SEO URL '%s' is already taken in the application." + +#. module: website_seo +#: model:ir.ui.view,arch_db:website_seo.view_website_config_settings +msgid "SEO Redirects" +msgstr "SEO Umleitungen" + +#. module: website_seo +#: model:ir.ui.view,arch_db:website_seo.view_website_config_settings +msgid "Configure SEO redirects" +msgstr "Konfiguriere SEO Umleitungen" + +#. module: website_seo +#: model:ir.actions.act_window,name:website_seo.action_website_seo_pages +#: model:ir.ui.view,arch_db:website_seo.website_seo_pages_tree +msgid "Website SEO Redirects" +msgstr "Webseite SEO Umleitungen" diff --git a/website_seo/i18n/website_seo.pot b/website_seo/i18n/website_seo.pot index 19a5fd5..ddf87da 100755 --- a/website_seo/i18n/website_seo.pot +++ b/website_seo/i18n/website_seo.pot @@ -41,12 +41,6 @@ msgstr "Website Menu" msgid "Only a-z, A-Z, 0-9, - and _ are allowed characters for the SEO url." msgstr "Only a-z, A-Z, 0-9, - and _ are allowed characters for the SEO url." -#. module: website_seo -#: code:addons/website_seo/static/src/xml/website_seo_robots.xml -#, python-format -msgid "Robots Content" -msgstr "Robots Content" - #. module: website_seo #: help:ir.ui.view,seo_url_parent:0 msgid "" @@ -70,4 +64,139 @@ msgstr "" "\"how-to-do-it-right\" page is the third part and it needs the parent " "page \"study\". If all pages are configured correct the page " "\"how-to-do-it-right\" is rendered when calling " -"\"/ecommerce/study/how-to-do-it-right\"." \ No newline at end of file +"\"/ecommerce/study/how-to-do-it-right\"." + +#. module: website_seo +#: model:website.seo.information,information:website_seo.field_title_tip +msgid "The title tag refers to the topic of the HTML page. It is very important for Google and should describe the content very well. The title tag is displayed in the Google search results pages. The title tag should have a maximum of 55 characters (including spaces). It can also have more characters, it will be cut in the search results. If possible the keyword should be at the beginning." +msgstr "The title tag refers to the topic of the HTML page. It is very important for Google and should describe the content very well. The title tag is displayed in the Google search results pages. The title tag should have a maximum of 55 characters (including spaces). It can also have more characters, it will be cut in the search results. If possible the keyword should be at the beginning." + +#. module: website_seo +#: model:website.seo.information,information:website_seo.field_description_tip +msgid "The meta description will be displayed on the Google search results pages. The meta description is a detailed characterization of the HTML page. It should encourage users to click on the search result (Call-to-Action). The length should be a maximum of 156 characters (including spaces)." +msgstr "The meta description will be displayed on the Google search results pages. The meta description is a detailed characterization of the HTML page. It should encourage users to click on the search result (Call-to-Action). The length should be a maximum of 156 characters (including spaces)." + +#. module: website_seo +#. openerp-web +#: code:addons/website_seo/static/src/xml/website_seo_robots.xml:8 +#, python-format +msgid "" +"This form will save only the configuration for the selected language. If you change the language before save the SEO information recently changed will be lost." +msgstr "This form will save only the configuration for the selected language. If you change the language before save the SEO information recently changed will be lost." + +#. module: website_seo +#. openerp-web +#: code:addons/website_seo/static/src/xml/website_seo_robots.xml:13 +#, python-format +msgid "Language:" +msgstr "Language:" + +#. module: website_seo +#. openerp-web +#: code:addons/website_seo/static/src/xml/website_seo_robots.xml:21 +#, python-format +msgid "Page Title" +msgstr "Page Title" + +#. module: website_seo +#. openerp-web +#: code:addons/website_seo/static/src/xml/website_seo_robots.xml:32 +#, python-format +msgid "Description" +msgstr "Description" + +#. module: website_seo +#. openerp-web +#: code:addons/website_seo/static/src/xml/website_seo_robots.xml:42 +#, python-format +msgid "Robots Content" +msgstr "Robots Content" + +#. module: website_seo +#. openerp-web +#: code:addons/website_seo/static/src/xml/website_seo_robots.xml:45 +#: selection:ir.ui.view,website_meta_robots:0 +#: selection:website.seo.metadata,website_meta_robots:0 +#, python-format +msgid "INDEX,FOLLOW" +msgstr "INDEX,FOLLOW" + +#. module: website_seo +#. openerp-web +#: code:addons/website_seo/static/src/xml/website_seo_robots.xml:47 +#: selection:ir.ui.view,website_meta_robots:0 +#: selection:website.seo.metadata,website_meta_robots:0 +#, python-format +msgid "INDEX,NOFOLLOW" +msgstr "INDEX,NOFOLLOW" + +#. module: website_seo +#. openerp-web +#: code:addons/website_seo/static/src/xml/website_seo_robots.xml:46 +#: selection:ir.ui.view,website_meta_robots:0 +#: selection:website.seo.metadata,website_meta_robots:0 +#, python-format +msgid "NOINDEX,FOLLOW" +msgstr "NOINDEX,FOLLOW" + +#. module: website_seo +#. openerp-web +#: code:addons/website_seo/static/src/xml/website_seo_robots.xml:48 +#: selection:ir.ui.view,website_meta_robots:0 +#: selection:website.seo.metadata,website_meta_robots:0 +#, python-format +msgid "NOINDEX,NOFOLLOW" +msgstr "NOINDEX,NOFOLLOW" + +#. module: website_seo +#. openerp-web +#: code:addons/website_seo/static/src/xml/website_seo_robots.xml:54 +#, python-format +msgid "SEO URL" +msgstr "SEO URL" + +#. module: website_seo +#: model:ir.ui.view,arch_db:website_seo.view_website_config_settings +msgid "SEO Pages" +msgstr "SEO Pages" + +#. module: website_seo +#: model:ir.ui.view,arch_db:website_seo.view_website_config_settings +msgid "Configure SEO pages" +msgstr "Configure SEO pages" + +#. module: website_seo +#: model:ir.actions.act_window,name:website_seo.action_website_seo_pages +#: model:ir.ui.view,arch_db:website_seo.website_seo_pages_tree +msgid "Website SEO Pages" +msgstr "Website SEO Pages" + +#. module: website_seo +#. openerp-web +#: code:addons/website_seo/static/src/js/seo_robots.js:509 +#, python-format +msgid "Invalid SEO URL. The allowed characters are a-z, A-Z, 0-9, - and _." +msgstr "Invalid SEO URL. The allowed characters are a-z, A-Z, 0-9, - and _." + +#. module: website_seo +#. openerp-web +#: code:addons/website_seo/static/src/js/seo_robots.js:512 +#, python-format +msgid "The SEO URL '%s' is already taken in the application." +msgstr "The SEO URL '%s' is already taken in the application." + +#. module: website_seo +#: model:ir.ui.view,arch_db:website_seo.view_website_config_settings +msgid "SEO Redirects" +msgstr "SEO Redirects" + +#. module: website_seo +#: model:ir.ui.view,arch_db:website_seo.view_website_config_settings +msgid "Configure SEO redirects" +msgstr "Configure SEO redirects" + +#. module: website_seo +#: model:ir.actions.act_window,name:website_seo.action_website_seo_pages +#: model:ir.ui.view,arch_db:website_seo.website_seo_pages_tree +msgid "Website SEO Redirects" +msgstr "Website SEO Redirects" diff --git a/website_seo/models/ir_http.py b/website_seo/models/ir_http.py index dd8c030..5a89399 100755 --- a/website_seo/models/ir_http.py +++ b/website_seo/models/ir_http.py @@ -29,13 +29,13 @@ class IrHttp(models.TransientModel): def _find_handler(self, return_rule=False): """Update handler finding to avoid endless recursion.""" - handler = super(IrHttp, self)._find_handler(return_rule=return_rule) - # ToDo: I reuse some parts of the _dispatch() function in - # addons/website/models/ir_http.py, maybe we can re-structure - # (complete overwrite) this function to have the needed values at this - # place path = request.httprequest.path.split('/') + # avoid handle static resource as seo urls + if 'static' in path: + raise werkzeug.exceptions.NotFound() + + handler = super(IrHttp, self)._find_handler(return_rule=return_rule) request.website = request.registry['website'].get_current_website( request.cr, request.uid, context=request.context) @@ -49,7 +49,7 @@ def _find_handler(self, return_rule=False): request.lang = nearest_lang or preferred_lang - # added handling from addons/website/models/ir_http.py in _dispatch() + # added handling from addons/website/models/fields.py in _dispatch() # function to avoid endless recursion when using different languages if (path[1] != request.website.default_lang_code and path[1] == request.lang): diff --git a/website_seo/models/ir_translation.py b/website_seo/models/ir_translation.py index 913909c..02a7ced 100755 --- a/website_seo/models/ir_translation.py +++ b/website_seo/models/ir_translation.py @@ -29,7 +29,7 @@ class IrTranslation(models.Model): _inherit = 'ir.translation' - @tools.ormcache_multi(skiparg=3, multi=6) + @api.model def _get_ids(self, cr, uid, name, tt, lang, ids): lang = self.pool.get('res.lang').get_code_from_alias(cr, uid, lang) return super(IrTranslation, self)._get_ids(cr, uid, name, tt, lang, ids) @@ -44,6 +44,15 @@ def _get_source(self, name, types, lang, source=None, res_id=None): lang = self.env['res.lang'].get_code_from_alias(lang) return super(IrTranslation, self)._get_source(name, types, lang, source, res_id) + @api.model + def _get_terms_query(self, field, records): + lang = self.env['res.lang'].get_code_from_alias(records.env.lang) + query = """ SELECT * FROM ir_translation + WHERE lang=%s AND type=%s AND name=%s AND res_id IN %s """ + name = "%s,%s" % (field.model_name, field.name) + params = (lang, 'model', name, tuple(records.ids)) + return query, params + def translate_fields(self, cr, uid, model, id, field=None, context=None): res = super(IrTranslation, self).translate_fields(cr, uid, model, id, field, context) # the translate_fields method does not set module field, it is needed because if you are going to diff --git a/website_seo/models/ir_ui_view.py b/website_seo/models/ir_ui_view.py old mode 100755 new mode 100644 index b92ae74..dd506d7 --- a/website_seo/models/ir_ui_view.py +++ b/website_seo/models/ir_ui_view.py @@ -32,6 +32,7 @@ def url_for(path_or_uri, lang=None): if isinstance(current_path, unicode): current_path = current_path.encode('utf-8') location = path_or_uri.strip() + force_lang = lang is not None url = urlparse.urlparse(location) @@ -41,6 +42,12 @@ def url_for(path_or_uri, lang=None): lang = lang or request.context.get('lang') langs = [lg[0] for lg in request.website.get_languages()] + #-------------------------------------------------- + translation = _get_translated_seo_url(location, lang) + if translation: + location = translation + #-------------------------------------------------- + if (len(langs) > 1 or force_lang) and is_multilang_url(location, langs): if lang != request.context.get('lang'): location = url_for_lang(location, lang) @@ -60,13 +67,49 @@ def url_for(path_or_uri, lang=None): return location.decode('utf-8') -def url_for_lang(location, lang): - menu = request.registry['website.menu'] +def _get_translation_value(location): ctx = request.context.copy() - menu_ids = menu.search(request.cr, request.uid, [('url', '=', location)], context=ctx) - if menu_ids: - ctx.update({'lang': lang}) - location = menu.browse(request.cr, request.uid, menu_ids[0], context=ctx).url + + trans = request.registry['ir.translation'] + view = request.registry['ir.ui.view'] + + view_ids = view.search(request.cr, request.uid, [('seo_url', '=', location[1:])], context=ctx) + if view_ids: + trans_ids = trans.search(request.cr, request.uid, [('name','=', "ir.ui.view,seo_url"), ('res_id', '=', view_ids[0])], context=ctx) + value = trans.browse(request.cr, request.uid, trans_ids, context=ctx)[0].value + return "/"+value + else: + return location + + +def _get_translated_seo_url(location, lang): + if len(location) > 1: + ctx = request.context.copy() + + trans = request.registry['ir.translation'] + view = request.registry['ir.ui.view'] + + trans_ids = trans.search(request.cr, request.uid, [('value', '=', location[1:])], context=ctx) + if trans_ids: + # print location, trans_ids + ctx.update({'lang': lang}) + res_id = trans.browse(request.cr, request.uid, trans_ids[0], context=ctx)[0].res_id + seo_url = view.browse(request.cr, request.uid, [res_id], context=ctx)[0].seo_url + # print lang, seo_url + return "/"+seo_url + else: + return None + else: + return location + + +def url_for_lang(location, lang): + seo_url = _get_translated_seo_url(location, lang) + if seo_url: + return seo_url + else: + return _get_translation_value(location) + return location @@ -110,11 +153,14 @@ def onchange_seo_url_parent(self): @api.multi def write(self, vals): - res = super(View, self).write(vals) + lang = self.env.context.get('lang', False) + if lang: + lang = self.env['res.lang'].get_code_from_alias(lang) + res = super(View, self.with_context(lang=lang)).write(vals) fields = ['seo_url', 'seo_url_parent', 'seo_url_level'] if set(fields).intersection(set(vals.keys())): - self.update_related_views() - self.update_website_menus() + self.with_context(lang=lang).update_related_views() + self.with_context(lang=lang).update_website_menus() return res @api.multi @@ -146,7 +192,7 @@ def get_seo_path(self): @api.model def find_by_seo_path(self, path): - url_parts = path.split('/') + url_parts = [u for u in path.split('/') if u != ''] views = self.search([('seo_url', 'in', url_parts)], order='seo_url_level ASC') if len(url_parts) == len(views): diff --git a/website_seo/models/res_lang.py b/website_seo/models/res_lang.py index 79ce551..3d3abd7 100755 --- a/website_seo/models/res_lang.py +++ b/website_seo/models/res_lang.py @@ -19,7 +19,6 @@ # ############################################################################## from openerp import api, fields, models -from openerp.osv import expression class ResLang(models.Model): @@ -32,34 +31,3 @@ class ResLang(models.Model): def get_code_from_alias(self, code): lang = self.search([('short_code', '=', code)]) return lang and lang[0].code or code - - -def update_lang_code_from_alias_in_expression(): - - def extended_init(self, cr, uid, exp, table, context): - - self._unaccent = expression.get_unaccent_wrapper(cr) - self.joins = [] - self.root_model = table - - # normalize and prepare the expression for parsing - self.expression = expression.distribute_not(expression.normalize_domain(exp)) - - # look for real lang from context before parse - parse_ctx = context.copy() - if parse_ctx.get('lang', False): - cr.execute("select column_name from information_schema.columns " - "where table_name='res_lang' AND column_name='short_code'") - res = cr.fetchall() - if res: - cr.execute("select code from res_lang where short_code = '%s'" % parse_ctx['lang']) - res = cr.fetchall() - if res and res[0]: - parse_ctx.update({'lang': res[0][0]}) - - # parse the domain expression - self.parse(cr, uid, context=parse_ctx) - - setattr(expression.expression, '__init__', extended_init) - -update_lang_code_from_alias_in_expression() diff --git a/website_seo/models/website.py b/website_seo/models/website.py index 33cba43..545b116 100755 --- a/website_seo/models/website.py +++ b/website_seo/models/website.py @@ -36,6 +36,8 @@ ('NOINDEX,NOFOLLOW', 'NOINDEX,NOFOLLOW') ] +KNOWN_URLS = [] + def slug(value): """Add seo url check in slug handling.""" @@ -58,14 +60,59 @@ def slug(value): class Website(models.Model): _inherit = 'website' - @openerp.tools.ormcache(skiparg=3) + @openerp.tools.ormcache('id') def _get_languages(self, cr, uid, id): website = self.browse(cr, uid, id) return [(lg.short_code or lg.code, lg.name) for lg in website.language_ids] + def get_canonical_url(self, cr, uid, req=None, context=None): + if req is None: + req = request.httprequest + default = self.get_current_website(cr, uid, context=context).default_lang_code + if request.lang != default: + url = req.url_root[0:-1] + '/' + request.lang + req.path + if req.query_string: + url += '?' + req.query_string + return url + return req.url + def get_alternate_languages(self, cr, uid, ids, req=None, context=None): - # TODO: do something here to show url translated in HEAD, current show wrong URL - return super(Website, self).get_alternate_languages(cr, uid, ids, req, context) + langs = [] + if req is None: + req = request.httprequest + default = self.get_current_website(cr, uid, context=context).default_lang_code + shorts = [] + for code, name in self.get_languages(cr, uid, ids, context=context): + lg_path = ('/' + code) if code != default else '' + lg = code.split('_') + shorts.append(lg[0]) + path = self.get_translated_path(cr, uid, req.path, code, context=context) + href = req.url_root[0:-1] + lg_path + path + if req.query_string: + href += '?' + req.query_string + lang = { + 'hreflang': ('-'.join(lg)).lower(), + 'short': lg[0], + 'href': href, + } + langs.append(lang) + for lang in langs: + if shorts.count(lang['short']) == 1: + lang['hreflang'] = lang['short'] + return langs + + def get_translated_path(self, cr, uid, path, lang, context=None): + if lang == request.lang: + return path + ctx = context.copy() + ctx.update({'lang': request.lang}) + view = self.pool.get('ir.ui.view') + view_ids = view.search(cr, uid, [('seo_url', '!=', False)], context=context) + for obj in view.browse(cr, uid, view_ids, context=ctx): + if obj.get_seo_path()[0] == path: + ctx.update({'lang': lang}) + return view.browse(cr, uid, obj.id, context=ctx).get_seo_path()[0] + return path class WebsiteMenu(models.Model): @@ -93,10 +140,10 @@ def get_website_view(self): xml_id = url_parts[-1] if '.' not in xml_id: xml_id = 'website.%s' % xml_id - view = self.env['ir.model.data'].xmlid_to_object(xml_id) + view = self.env['ir.ui.view'].search([('key', '=', xml_id)]) if not view: xml_id = 'website.%s' % slugify(self.name) - view = self.env['ir.model.data'].xmlid_to_object(xml_id) + view = self.env['ir.ui.view'].search([('key', '=', xml_id)]) return view @api.model @@ -108,11 +155,14 @@ def create(self, vals): @api.multi def write(self, vals): - res = super(WebsiteMenu, self).write(vals) + lang = self.env.context.get('lang', False) + if lang: + lang = self.env['res.lang'].get_code_from_alias(lang) + res = super(WebsiteMenu, self.with_context(lang=lang)).write(vals) if not self.env.context.get('view_updated', False) \ and (vals.get('parent_id', False) or vals.get('url', False)): - self.update_related_views() - self.update_website_menus() + self.with_context(lang=lang).update_related_views() + self.with_context(lang=lang).update_website_menus() return res @api.multi @@ -140,12 +190,8 @@ def update_website_menus(self): if seo_path: vals.update({'url': seo_path}) else: - view_name = view.get_xml_id() - if view_name: - view_name = view_name[view.id].replace('website.', '') - vals.update({'url': '/page/%s' % view_name}) - - if obj.parent_id.get_website_view()[0] != view.seo_url_parent: + vals.update({'url': '/page/%s' % view.key.replace('website.', '')}) + if view.seo_url_parent and obj.parent_id.get_website_view()[0] != view.seo_url_parent: # TODO: create a new method to get a menu from a view for menu in self: if menu.get_website_view()[0] == view.seo_url_parent: @@ -164,10 +210,18 @@ class WebsiteSeoMetadata(models.Model): seo_url = fields.Char( string='SEO Url', translate=True, help='If you fill out this field ' 'manually the allowed characters are a-z, A-Z, 0-9, - and _.') + seo_url_redirect = fields.One2many(compute='_get_seo_url_redirect', + comodel_name='website.seo.redirect', + string='SEO Url Redirect', translate=True) website_meta_robots = fields.Selection(META_ROBOTS, string='Website meta robots', translate=True) + @api.one + def _get_seo_url_redirect(self): + resource = '%s,%s' % (self._name, str(self.id)) + return self.env['website.seo.redirect'].search([('resource', '=', resource)]) + @api.model def create(self, vals): """Add check for correct SEO urls. @@ -183,12 +237,36 @@ def create(self, vals): @api.multi def write(self, vals): - """Add check for correct SEO urls.""" + """- Add check for correct SEO urls. + - Saves old seo_url in seo_url_redirect field + """ if vals.get('seo_url', False): self.validate_seo_url(vals['seo_url']) - + if vals.get('seo_url', False) or vals.get('seo_url_parent', False): + for obj in self: + obj.update_seo_redirect() + super(WebsiteSeoMetadata, obj).write(vals) + return True return super(WebsiteSeoMetadata, self).write(vals) + @api.one + def update_seo_redirect(self): + # TODO: includes the case when the seo_url is added for first time + # and the url '/page/...' must be saved to redirect + if self.seo_url: + seo_url = self.get_seo_path()[0] + if seo_url not in [x.url for x in self.seo_url_redirect]: + lang = self.env.context.get('lang', False) + if not lang: + lang = self.env['website'].get_current_website().default_lang_code + lang = self.env['res.lang'].get_code_from_alias(lang) + redirect = { + 'url': seo_url, + 'lang': lang, + 'resource': '%s,%s' % (self._name, self.id) + } + self.env['website.seo.redirect'].create(redirect) + def validate_seo_url(self, seo_url): """Validate a manual entered SEO url.""" if not seo_url or not bool(re.match('^([.a-zA-Z0-9-_]+)$', seo_url)): @@ -196,12 +274,50 @@ def validate_seo_url(self, seo_url): 'characters for the SEO url.')) return True + @api.one + def get_seo_path(self): + """This method must be override in child classes in order to provide + a different behavior of the model""" + if self.seo_url: + return "/%s" % self.seo_url + return False + @api.model def get_information_from(self, field): domain = [('field', '=', field)] obj = self.env['website.seo.information'].search(domain) return obj and obj[0].information or False + def _check_known_urls(self, cr, uid, ids, context=None): + for seo in self.browse(cr, uid, ids, context=context): + if seo.seo_url in KNOWN_URLS: + return False + return True + + _constraints = [ + (_check_known_urls, "The URL already exists in Odoo.", ['seo_url']), + ] + + @api.model + def get_known_seo_urls(self): + return KNOWN_URLS + + +class WebsiteSeoRedirect(models.Model): + _name = 'website.seo.redirect' + + """Class used to store old urls for each resource. With these urls the + website can do redirect 301 if some url has changed. + + The field 'resource' can't be a strong reference because + the model website.seo.metadata is used to inherit and the fields + actually are in the resources (eg. ir.ui.view, blog.blog). + """ + + url = fields.Char(string='URL') + lang = fields.Char(string='Lang') + resource = fields.Char(string='Resource', help='This field use the format model,id') + class WebsiteSeoInformation(models.Model): _name = 'website.seo.information' diff --git a/website_seo/static/src/js/seo_robots.js b/website_seo/static/src/js/seo_robots.js index 9c50b32..feed2e2 100755 --- a/website_seo/static/src/js/seo_robots.js +++ b/website_seo/static/src/js/seo_robots.js @@ -1,16 +1,129 @@ -// Override necessary parts of website/static/src/js/website.seo.js to enable -// META robots management via promote panel. -// -// We need to override all needed function. We don't can use the super function -// here. - -(function () { +odoo.define('website_seo.seo_robots', function (require) { 'use strict'; - var website = openerp.website; - website.add_template_file('/website_seo/static/src/xml/website_seo_robots.xml'); + var ajax = require('web.ajax'); + var core = require('web.core'); + var Class = require('web.Class'); + var mixins = require('web.mixins'); + var Model = require('web.Model'); + var Widget = require('web.Widget'); + var base = require('web_editor.base'); + var seo = require('website.seo'); + + var _t = core._t; + + var qweb = core.qweb; + + ajax.loadXML('/website_seo/static/src/xml/website_seo_robots.xml', qweb); + + // This replaces \b, because accents(e.g. à, é) are not seen as word boundaries. + // Javascript \b is not unicode aware, and words beginning or ending by accents won't match \b + var WORD_SEPARATORS_REGEX = '([\\u2000-\\u206F\\u2E00-\\u2E7F\'!"#\\$%&\\(\\)\\*\\+,\\-\\.\\/:;<=>\\?¿¡@\\[\\]\\^_`\\{\\|\\}~\\s]+|^|$)'; + + function analyzeKeyword(htmlPage, keyword) { + return htmlPage.isInTitle(keyword) ? { + title: 'label label-primary', + description: "This keyword is used in the page title", + } : htmlPage.isInDescription(keyword) ? { + title: 'label label-info', + description: "This keyword is used in the page description", + } : htmlPage.isInBody(keyword) ? { + title: 'label label-info', + description: "This keyword is used in the page content." + } : { + title: 'label label-default', + description: "This keyword is not used anywhere on the page." + }; + } + + var Tip = Widget.extend({ + template: 'website.seo_tip', + events: { + 'closed.bs.alert': 'destroy', + }, + init: function (parent, options) { + this.message = options.message; + // cf. http://getbootstrap.com/components/#alerts + // success, info, warning or danger + this.type = options.type || 'info'; + this._super(parent); + }, + }); + + var Preview = Widget.extend({ + template: 'website.seo_preview', + init: function (parent, options) { + this.title = options.title; + this.url = options.url; + this.description = options.description || "[ The description will be generated by google unless you specify one ]"; + this._super(parent); + }, + }); - website.seo.HtmlPage.include({ + var HtmlPage = Class.extend(mixins.PropertiesMixin, { + url: function () { + var url = window.location.href; + var hashIndex = url.indexOf('#'); + return hashIndex >= 0 ? url.substring(0, hashIndex) : url; + }, + title: function () { + var $title = $('title'); + return ($title.length > 0) && $title.text() && $title.text().trim(); + }, + changeTitle: function (title) { + // TODO create tag if missing + $('title').text(title); + this.trigger('title-changed', title); + }, + description: function () { + var $description = $('meta[name=description]'); + return ($description.length > 0) && ($description.attr('content') && $description.attr('content').trim()); + }, + changeDescription: function (description) { + // TODO create tag if missing + $('meta[name=description]').attr('content', description); + this.trigger('description-changed', description); + }, + keywords: function () { + var $keywords = $('meta[name=keywords]'); + var parsed = ($keywords.length > 0) && $keywords.attr('content') && $keywords.attr('content').split(","); + return (parsed && parsed[0]) ? parsed: []; + }, + changeKeywords: function (keywords) { + // TODO create tag if missing + $('meta[name=keywords]').attr('content', keywords.join(",")); + this.trigger('keywords-changed', keywords); + }, + headers: function (tag) { + return $('#wrap '+tag).map(function () { + return $(this).text(); + }); + }, + images: function () { + return $('#wrap img').map(function () { + var $img = $(this); + return { + src: $img.attr('src'), + alt: $img.attr('alt'), + }; + }); + }, + company: function () { + return $('html').attr('data-oe-company-name'); + }, + bodyText: function () { + return $('body').children().not('.js_seo_configuration').text(); + }, + isInBody: function (text) { + return new RegExp(WORD_SEPARATORS_REGEX+text+WORD_SEPARATORS_REGEX, "gi").test(this.bodyText()); + }, + isInTitle: function (text) { + return new RegExp(WORD_SEPARATORS_REGEX+text+WORD_SEPARATORS_REGEX, "gi").test(this.title()); + }, + isInDescription: function (text) { + return new RegExp(WORD_SEPARATORS_REGEX+text+WORD_SEPARATORS_REGEX, "gi").test(this.description()); + }, + // Add robots and seo_url robots: function() { var $robots = $('meta[name=robots]'); return ($robots.length > 0) && ($robots.attr('content') && $robots.attr('content').trim()); @@ -29,7 +142,220 @@ }, }); - website.seo.Configurator.include({ + var Suggestion = Widget.extend({ + template: 'website.seo_suggestion', + events: { + 'click .js_seo_suggestion': 'select', + }, + init: function (parent, options) { + this.root = options.root; + this.keyword = options.keyword; + this.language = options.language; + this.htmlPage = options.page; + this._super(parent); + }, + start: function () { + this.htmlPage.on('title-changed', this, this.renderElement); + this.htmlPage.on('description-changed', this, this.renderElement); + }, + analyze: function () { + return analyzeKeyword(this.htmlPage, this.keyword); + }, + highlight: function () { + return this.analyze().title; + }, + tooltip: function () { + return this.analyze().description; + }, + select: function () { + this.trigger('selected', this.keyword); + }, + }); + + var SuggestionList = Widget.extend({ + template: 'website.seo_suggestion_list', + init: function (parent, options) { + this.root = options.root; + this.language = options.language; + this.htmlPage = options.page; + this._super(parent); + }, + start: function () { + this.refresh(); + }, + refresh: function () { + var self = this; + self.$el.append(_t("Loading...")); + var language = self.language || base.get_context().lang.toLowerCase(); + ajax.jsonRpc('/website/seo_suggest', 'call', { + 'keywords': self.root, + 'lang': language, + }).then(function(keyword_list){ + self.addSuggestions(JSON.parse(keyword_list)); + }); + }, + addSuggestions: function(keywords) { + var self = this; + self.$el.empty(); + // TODO Improve algorithm + Ajust based on custom user keywords + var regex = new RegExp(self.root, "gi"); + var keywords = _.map(_.uniq(keywords), function (word) { + return word.replace(regex, "").trim(); + }); + // TODO Order properly ? + _.each(keywords, function (keyword) { + if (keyword) { + var suggestion = new Suggestion(self, { + root: self.root, + language: self.language, + keyword: keyword, + page: self.htmlPage, + }); + suggestion.on('selected', self, function (word, language) { + self.trigger('selected', word, language); + }); + suggestion.appendTo(self.$el); + } + }); + }, + }); + + var Keyword = Widget.extend({ + template: 'website.seo_keyword', + events: { + 'click a[data-action=remove-keyword]': 'destroy', + }, + maxWordsPerKeyword: 4, // TODO Check + init: function (parent, options) { + this.keyword = options.word; + this.language = options.language; + this.htmlPage = options.page; + this._super(parent); + }, + start: function () { + this.htmlPage.on('title-changed', this, this.updateLabel); + this.htmlPage.on('description-changed', this, this.updateLabel); + this.suggestionList = new SuggestionList(this, { + root: this.keyword, + language: this.language, + page: this.htmlPage, + }); + this.suggestionList.on('selected', this, function (word, language) { + this.trigger('selected', word, language); + }); + this.suggestionList.appendTo(this.$('.js_seo_keyword_suggestion')); + }, + analyze: function () { + return analyzeKeyword(this.htmlPage, this.keyword); + }, + highlight: function () { + return this.analyze().title; + }, + tooltip: function () { + return this.analyze().description; + }, + updateLabel: function () { + var cssClass = "oe_seo_keyword js_seo_keyword " + this.highlight(); + this.$(".js_seo_keyword").attr('class', cssClass); + this.$(".js_seo_keyword").attr('title', this.tooltip()); + }, + destroy: function () { + this.trigger('removed'); + this._super(); + }, + }); + + var KeywordList = Widget.extend({ + template: 'website.seo_list', + maxKeywords: 10, + init: function (parent, options) { + this.htmlPage = options.page; + this._super(parent); + }, + start: function () { + var self = this; + var existingKeywords = self.htmlPage.keywords(); + if (existingKeywords.length > 0) { + _.each(existingKeywords, function (word) { + self.add.call(self, word); + }); + } + }, + keywords: function () { + var result = []; + this.$('.js_seo_keyword').each(function () { + result.push($(this).data('keyword')); + }); + return result; + }, + isFull: function () { + return this.keywords().length >= this.maxKeywords; + }, + exists: function (word) { + return _.contains(this.keywords(), word); + }, + add: function (candidate, language) { + var self = this; + // TODO Refine + var word = candidate ? candidate.replace(/[,;.:<>]+/g, " ").replace(/ +/g, " ").trim().toLowerCase() : ""; + if (word && !self.isFull() && !self.exists(word)) { + var keyword = new Keyword(self, { + word: word, + language: language, + page: this.htmlPage, + }); + keyword.on('removed', self, function () { + self.trigger('list-not-full'); + self.trigger('removed', word); + }); + keyword.on('selected', self, function (word, language) { + self.trigger('selected', word, language); + }); + keyword.appendTo(self.$el); + } + if (self.isFull()) { + self.trigger('list-full'); + } + }, + }); + + var Image = Widget.extend({ + template: 'website.seo_image', + init: function (parent, options) { + this.src = options.src; + this.alt = options.alt; + this._super(parent); + }, + }); + + var ImageList = Widget.extend({ + init: function (parent, options) { + this.htmlPage = options.page; + this._super(parent); + }, + start: function () { + var self = this; + this.htmlPage.images().each(function (index, image) { + new Image(self, image).appendTo(self.$el); + }); + }, + images: function () { + var result = []; + this.$('input').each(function () { + var $input = $(this); + result.push({ + src: $input.attr('src'), + alt: $input.val(), + }); + }); + return result; + }, + add: function (image) { + new Image(this, image).appendTo(this.$el); + }, + }); + + seo.Configurator.include({ events: { 'keyup input[name=seo_page_keywords]': 'confirmKeyword', 'keyup input[name=seo_page_title]': 'titleChanged', @@ -38,6 +364,7 @@ 'keyup input[name=seo_url]': 'seoUrlChanged', 'click button[data-action=add]': 'addKeyword', 'click button[data-action=update]': 'update', + 'change select[name=seo_url_page_language]': 'changeLanguage', 'hidden.bs.modal': 'destroy' }, canEditRobots: false, @@ -45,34 +372,69 @@ start: function() { var self = this; var $modal = self.$el; - var htmlPage = this.htmlPage = new website.seo.HtmlPage(); + var htmlPage = this.htmlPage = new HtmlPage(); $modal.find('.js_seo_page_url').text(htmlPage.url()); $modal.find('input[name=seo_page_title]').val(htmlPage.title()); $modal.find('textarea[name=seo_page_description]').val(htmlPage.description()); $modal.find('select[name=seo_page_robots]').val(htmlPage.robots()); $modal.find('input[name=seo_url]').val(htmlPage.seo_url()); - self.keywordList = new website.seo.KeywordList(self, { page: htmlPage }); - self.keywordList.on('list-full', self, function() { + var url_parts = window.location.href.split('/'); + var path = url_parts[url_parts.length - 1]; + if (path) { + path = path.replace('#', '').replace('?', ''); + } + if (! path || path === 'homepage'){ + $('input[name=seo_url]').css('visibility','hidden'); + $('label[for=seo_url]').css('visibility','hidden'); + } + new Model('website.seo.metadata').call('get_known_seo_urls') + .then(function(data) { + self.known_urls = data; + }); + self.keywordList = new KeywordList(self, { page: htmlPage }); + self.keywordList.on('list-full', self, function () { $modal.find('input[name=seo_page_keywords]') .attr('readonly', "readonly") .attr('placeholder', "Remove a keyword first"); $modal.find('button[data-action=add]') .prop('disabled', true).addClass('disabled'); }); - self.keywordList.on('list-not-full', self, function() { + self.keywordList.on('list-not-full', self, function () { $modal.find('input[name=seo_page_keywords]') .removeAttr('readonly').attr('placeholder', ""); $modal.find('button[data-action=add]') .prop('disabled', false).removeClass('disabled'); }); - self.keywordList.on('selected', self, function(word) { - self.keywordList.add(word); + self.keywordList.on('selected', self, function (word, language) { + self.keywordList.add(word, language); }); self.keywordList.appendTo($modal.find('.js_seo_keywords_list')); self.disableUnsavableFields(); self.renderPreview(); $modal.modal(); + self.getLanguages(); + }, + getLanguages: function(){ + var self = this; + ajax.jsonRpc('/web/dataset/call_kw', 'call', { + model: 'website', + method: 'get_languages', + args: [], + kwargs: { + ids: [base.get_context().website_id], + context: base.get_context() + } + }).then( function(data) { + self.$('#language-box').html(core.qweb.render('Configurator.language_promote', { + 'language': data, + 'def_lang': base.get_context().lang + })); + self.$('#seo-language-box').html(core.qweb.render('Configurator.language_promote', { + 'language': data, + 'def_lang': base.get_context().lang + })); + }); }, disableUnsavableFields: function () { var self = this; @@ -104,13 +466,10 @@ suggestField: function (field) { var tip = self.$('.js_seo_' + field + '_tips'); if (tip.children().length === 0) { - var model = website.session.model('website.seo.metadata'); - model.call('get_information_from', [field, website.get_context()]).then(function(data) { + var model = new Model('website.seo.metadata'); + model.call('get_information_from', [field, base.get_context()]).then(function(data) { if (data.length){ - new website.seo.Tip(self, { - message: data, - type: 'info' - }).appendTo(tip); + new Tip(self, {message: data, type: 'info'}).appendTo(tip); } }); } @@ -118,39 +477,104 @@ tip.children()[0].remove(); } }, + addKeyword: function (word) { + var $input = this.$('input[name=seo_page_keywords]'); + var $language = this.$('select[name=seo_page_language]'); + var keyword = _.isString(word) ? word : $input.val(); + var language = $language.val().toLowerCase(); + this.keywordList.add(keyword, language); + $input.val(""); + }, update: function () { var self = this; var data = {}; + var error = null; if (self.canEditTitle) { - data.website_meta_title = self.htmlPage.title(); + data.website_meta_title = this.$('input[name=seo_page_title]').val(); //self.htmlPage.title(); } if (self.canEditDescription) { - data.website_meta_description = self.htmlPage.description(); + data.website_meta_description = this.$('textarea[name=seo_page_description]').val(); //self.htmlPage.description(); } if (self.canEditKeywords) { data.website_meta_keywords = self.keywordList.keywords().join(", "); } if (self.canEditRobots) { - data.website_meta_robots = self.htmlPage.robots(); + data.website_meta_robots = this.$('select[name=seo_page_robots]').val(); //self.htmlPage.robots(); } if (self.canEditSeoUrl) { - data.seo_url = self.htmlPage.seo_url(); - } + var seo_url_regex = /^([.a-zA-Z0-9-_]+)$/; + var seo_url = this.$('input[name=seo_url]').val(); //self.htmlPage.seo_url(); - self.saveMetaData(data).then(function() { - self.$el.modal('hide'); - }); + if (seo_url && !error && !seo_url.match(seo_url_regex)) { + error = _t("Invalid SEO URL. The allowed characters are a-z, A-Z, 0-9, - and _."); + } + if (!error && self.known_urls.indexOf(seo_url) >= 0) { + error = _.str.sprintf(_t("The SEO URL '%s' is already taken in the application."), seo_url); + } + if (error) { + var div_error = self.$('.js_seo_url_tips'); + if (div_error.children().length > 0) { + div_error.children()[0].remove(); + } + new Tip(self, {message: error, type: 'danger'}).appendTo(div_error); + } + else { + data.seo_url = seo_url; + } + } + if (!error) { + self.saveMetaData(data).then(function() { + self.$el.modal('hide'); + self.$('#seo-language-box').val(base.get_context().lang); + self.getSeoPath().then(function(seo_path) { + if (seo_path) { + location.replace(seo_path, 301); + } + }); + }); + } + }, + getSeoPath: function () { + var self = this; + var obj = this.getMainObject(); + var def = $.Deferred(); + if (!obj) { + def.resolve(null); + } else { + var ctx = base.get_context(); + var lang = self.getCurrentLanguage(); + if (lang) { + ctx.lang = lang; + } + new Model(obj.model) + .call('get_seo_path', [obj.id, ctx]) + .then(function (result) { + if (result && result[0] !== false) { + def.resolve(result[0]); + } else { + def.resolve(null); + } + }).fail(function () { + def.reject(); + }); + } + return def; }, loadMetaData: function () { var self = this; var obj = this.getMainObject(); var def = $.Deferred(); if (!obj) { + // return $.Deferred().reject(new Error("No main_object was found.")); def.resolve(null); } else { + var ctx = base.get_context(); + var lang = self.getCurrentLanguage(); + if (lang) { + ctx.lang = lang; + } var fields = ['website_meta_title', 'website_meta_description', 'website_meta_keywords', 'website_meta_robots', 'seo_url']; - var model = website.session.model(obj.model); - model.call('read', [[obj.id], fields, website.get_context()]).then(function(data) { + var model = new Model(obj.model).call('read', [[obj.id], fields, ctx]).then(function (data) { if (data.length) { var meta = data[0]; meta.model = obj.model; @@ -158,12 +582,25 @@ } else { def.resolve(null); } - }).fail(function() { - def.reject(); + }).fail(function () { + def.reject(); }); } return def; }, + saveMetaData: function (data) { + var obj = this.getMainObject(); + if (!obj) { + return $.Deferred().reject(); + } else { + var ctx = base.get_context(); + var lang = this.getCurrentLanguage(); + if (lang) { + ctx.lang = lang; + } + return new Model(obj.model).call('write', [[obj.id], data, ctx]); + } + }, robotsChanged: function () { var self = this; setTimeout(function () { @@ -180,15 +617,62 @@ self.renderPreview(); }, 0); }, + renderPreview: function () { + var self = this; + var url = this.htmlPage.url(); + var seo_url = this.$('input[name=seo_url]').val(); + self.getSeoPath().then(function(seo_path) { + if (seo_url) { + var url_parts = url.split('/'); + var lang = self.getCurrentLanguage(); + if (seo_path) { + if (lang && lang !== base.get_context().lang) { + url_parts.splice(3, 0, lang); + } + url_parts[url_parts.length - 1] = seo_url; + } + else { + url_parts = url_parts.slice(0, 3); + if (lang && lang !== base.get_context().lang) { + url_parts.push(lang); + } + url_parts.push(seo_url); + } + url = url_parts.join('/'); + } + var preview = new Preview(self, { + title: self.htmlPage.title(), + description: self.htmlPage.description(), + url: url, + }); + var $preview = self.$('.js_seo_preview'); + $preview.empty(); + preview.appendTo($preview); + }); + }, + getCurrentLanguage: function () { + return this.$('#seo-language-box').val(); + }, + changeLanguage: function() { + var self = this; + this.loadMetaData().then(function(data){ + var $modal = self.$el; + $modal.find('input[name=seo_page_title]').val(data.website_meta_title); + $modal.find('textarea[name=seo_page_description]').val(data.website_meta_description || ''); + $modal.find('select[name=seo_page_robots]').val(data.website_meta_robots); + $modal.find('input[name=seo_url]').val(data.seo_url || ''); + self.renderPreview(); + }); + } }); - website.ready().done(function() { + base.ready().then(function () { $(document.body).on('click', '#title_tip', function() { - new website.seo.Configurator(this).suggestField('website_meta_title'); + new seo.Configurator(this).suggestField('website_meta_title'); }); $(document.body).on('click', '#description_tip', function() { - new website.seo.Configurator(this).suggestField('website_meta_description'); + new seo.Configurator(this).suggestField('website_meta_description'); }); }); -})(); \ No newline at end of file +}); \ No newline at end of file diff --git a/website_seo/static/src/xml/website_seo_robots.xml b/website_seo/static/src/xml/website_seo_robots.xml index cbdcad3..4dab7c9 100755 --- a/website_seo/static/src/xml/website_seo_robots.xml +++ b/website_seo/static/src/xml/website_seo_robots.xml @@ -4,9 +4,21 @@
+ +
+ This form will save only the configuration for the selected language. If you change the language before save the SEO information recently changed will be lost. +
+ +
+ +
+
@@ -37,12 +49,15 @@
+ +
+
diff --git a/website_seo/views/res_config.xml b/website_seo/views/res_config.xml index 8efe884..36d6495 100755 --- a/website_seo/views/res_config.xml +++ b/website_seo/views/res_config.xml @@ -11,6 +11,9 @@