From ed68878eb585e786215f1a803be100d73d7c5ac9 Mon Sep 17 00:00:00 2001 From: Cesar Lage Date: Wed, 7 Oct 2015 10:21:45 -0400 Subject: [PATCH 01/55] [IMP] website_seo module migrated to version 9 --- website_seo/README.rst | 64 ++++ website_seo/__init__.py | 22 ++ website_seo/__openerp__.py | 38 +++ website_seo/controllers/__init__.py | 21 ++ website_seo/controllers/main.py | 55 ++++ website_seo/i18n/de.po | 73 +++++ website_seo/models/__init__.py | 24 ++ website_seo/models/ir_http.py | 58 ++++ website_seo/models/ir_translation.py | 51 +++ website_seo/models/ir_ui_view.py | 128 ++++++++ website_seo/models/website.py | 229 ++++++++++++++ website_seo/static/description/icon.png | Bin 0 -> 9455 bytes website_seo/static/description/index.html | 50 +++ website_seo/static/src/js/seo_robots.js | 299 ++++++++++++++++++ .../static/src/xml/website_seo_robots.xml | 26 ++ website_seo/tests/__init__.py | 21 ++ website_seo/tests/test_website_seo.py | 40 +++ website_seo/views/header.xml | 19 ++ website_seo/views/ir_ui_view.xml | 40 +++ website_seo/views/res_config.xml | 19 ++ website_seo/views/website_templates.xml | 12 + 21 files changed, 1289 insertions(+) create mode 100644 website_seo/README.rst create mode 100644 website_seo/__init__.py create mode 100644 website_seo/__openerp__.py create mode 100644 website_seo/controllers/__init__.py create mode 100644 website_seo/controllers/main.py create mode 100644 website_seo/i18n/de.po create mode 100644 website_seo/models/__init__.py create mode 100644 website_seo/models/ir_http.py create mode 100644 website_seo/models/ir_translation.py create mode 100644 website_seo/models/ir_ui_view.py create mode 100644 website_seo/models/website.py create mode 100644 website_seo/static/description/icon.png create mode 100644 website_seo/static/description/index.html create mode 100644 website_seo/static/src/js/seo_robots.js create mode 100644 website_seo/static/src/xml/website_seo_robots.xml create mode 100644 website_seo/tests/__init__.py create mode 100644 website_seo/tests/test_website_seo.py create mode 100644 website_seo/views/header.xml create mode 100644 website_seo/views/ir_ui_view.xml create mode 100644 website_seo/views/res_config.xml create mode 100644 website_seo/views/website_templates.xml diff --git a/website_seo/README.rst b/website_seo/README.rst new file mode 100644 index 0000000..1ee77d5 --- /dev/null +++ b/website_seo/README.rst @@ -0,0 +1,64 @@ +Website SEO +=========== + +Provide the base for an improved SEO handling +--------------------------------------------- + +This module adds a new seo_url field to the website.seo.metadata model. It means all models which inherit website.seo.metadata will +have the new seo_url field. In general it affects website modules like website_blog, website_forum, website_hr_recruitment etc. The module itself adds no +SEO handling. It is done in additional modules like website_seo_blog. + +Provide the base for Robots Meta Information +-------------------------------------------- + +This module adds a new 'Robots Content' field to the promote panel where the user have full control on the robots meta information. +The selected value is stored in the website_meta_robots field in the website.seo.metadata model. It means all models which inherit website.seo.metadata +will have access to this functionality except of the ir.ui.view model. We add the website_meta_robots field to this model separately because ir.ui.view +doesn't inherit website.seo.metadata. + +*The description of the meta robots information is as follow*: + +- **index**: pages may be indexed +- **noindex**: pages may not be indexed +- **follow**: pages must be followed +- **nofollow**: pages should not be followed + +Important +--------- + +If you install this module you have to update all modules with models which inherits website.seo.metadata after the installation. In +general it is enough to update the website module. It is needed to populate the seo_url and website_meta_robots fields in all related models of the +installed website modules. + +If you uninstall this module or a related SEO module like website_seo_blog you have to clean up the related seo_url and website_meta_robots +entries in the database table ir_model_fields manually. You also have to delete the seo_url and website_meta_robots column in the related database tables +manually. + +Regards Bloopark + +Known issues / Roadmap +====================== + + * make seo_url field editable in the frontend via the promote panel + * add translation handling for SEO urls + +Credits +======= + +Contributors +------------ + +* Robert Rübner (rruebner@bloopark.de) + +Maintainer +---------- + +.. image:: http://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: http://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use. + +To contribute to this module, please visit http://odoo-community.org. \ No newline at end of file diff --git a/website_seo/__init__.py b/website_seo/__init__.py new file mode 100644 index 0000000..c8f6e41 --- /dev/null +++ b/website_seo/__init__.py @@ -0,0 +1,22 @@ +# -*- 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 controllers +from . import models diff --git a/website_seo/__openerp__.py b/website_seo/__openerp__.py new file mode 100644 index 0000000..9115487 --- /dev/null +++ b/website_seo/__openerp__.py @@ -0,0 +1,38 @@ +# -*- 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': 'Website SEO', + 'category': 'Website', + 'summary': 'Provide the base for an improved SEO handling', + 'version': '1.0', + 'author': "bloopark systems GmbH & Co. KG ," + "Odoo Community Association (OCA)", + 'website': "http://www.bloopark.de", + 'depends': ['website'], + 'data': [ + 'views/header.xml', + 'views/ir_ui_view.xml', + 'views/res_config.xml', + 'views/website_templates.xml' + ], + 'installable': True, + 'auto_install': False, +} diff --git a/website_seo/controllers/__init__.py b/website_seo/controllers/__init__.py new file mode 100644 index 0000000..0b4a657 --- /dev/null +++ b/website_seo/controllers/__init__.py @@ -0,0 +1,21 @@ +# -*- 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 main diff --git a/website_seo/controllers/main.py b/website_seo/controllers/main.py new file mode 100644 index 0000000..b734216 --- /dev/null +++ b/website_seo/controllers/main.py @@ -0,0 +1,55 @@ +# -*- 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.addons.web import http +from openerp.addons.web.http import request +from openerp.addons.website.controllers.main import Website + + +class Website(Website): + + @http.route(['/'], type='http', auth="public", website=True) + def path_page(self, seo_url, **kwargs): + """Handle SEO urls for ir.ui.views. + + ToDo: Add additional check for field seo_url_parent. Otherwise it is + possible to use invalid url structures. For example: if you have two + pages 'study-1' and 'study-2' with the same seo_url_level and different + seo_url_parent you can use '/ecommerce/study-1/how-to-do-it-right' and + '/ecommerce/study-2/how-to-do-it-right' to call the page + 'how-to-do-it-right'. + """ + env = request.env(context=request.context) + seo_url_parts = [s.encode('utf8') for s in seo_url.split('/') + if s != ''] + views = env['ir.ui.view'].search([('seo_url', 'in', seo_url_parts)], + order='seo_url_level ASC') + page = 'website.404' + if len(seo_url_parts) == len(views): + seo_url_check = [v.seo_url.encode('utf8') for v in views] + current_view = views[-1] + if (seo_url_parts == seo_url_check + and (current_view.seo_url_level + 1) == len(views)): + page = current_view.key + else: + if request.website.is_publisher(): + page = 'website.page_404' + + return request.render(page, {}) diff --git a/website_seo/i18n/de.po b/website_seo/i18n/de.po new file mode 100644 index 0000000..2b325d9 --- /dev/null +++ b/website_seo/i18n/de.po @@ -0,0 +1,73 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_seo +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-06-26 09:02+0000\n" +"PO-Revision-Date: 2015-06-26 09:02+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: website_seo +#: help:website.seo.metadata,seo_url:0 +msgid "If you fill out this field manually the allowed characters are a-z, A-Z, 0-9, - and _." +msgstr "Wenn Sie dieses Feld manuell ausfüllen, sind nur die Zeichen a-z, A-Z, 0-9, - and _ erlaubt." + +#. module: website_seo +#: field:website.seo.metadata,seo_url:0 +msgid "SEO Url" +msgstr "SEO Url" + +#. module: website_seo +#: model:ir.model,name:website_seo.model_website_seo_metadata +msgid "SEO metadata" +msgstr "SEO Metadaten" + +#. module: website_seo +#: model:ir.model,name:website_seo.model_website_menu +msgid "Website Menu" +msgstr "Webseite Menü" + +#. module: website_seo +#: code:addons/website_seo/models/website.py:90 +#, python-format +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 "" +"The SEO Parent field is used to describe hierarchical urls like " +"\"/ecommerce/study/how-to-do-it-right\". Taking this as example you " +"have to create 3 pages (ir.ui.view records) for \"ecommerce\", \"study\" " +"and \"how-to-do-it-right\". The \"ecommerce\" page is the first level " +"part and it doesn't need a SEO parent. The \"study\" page is the " +"second level part and it needs the parent page \"ecommerce\". The " +"\"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\"." +msgstr "" +"Die SEO übergeordnete Seite wird für hierarchische Urls wie \"/ecommerce/" +"study/how-to-do-it-right\" benutzt. Taking this as example you " +"have to create 3 pages (ir.ui.view records) for \"ecommerce\", \"study\" " +"and \"how-to-do-it-right\". The \"ecommerce\" page is the first level " +"part and it doesn't need a SEO parent. The \"study\" page is the " +"second level part and it needs the parent page \"ecommerce\". The " +"\"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 diff --git a/website_seo/models/__init__.py b/website_seo/models/__init__.py new file mode 100644 index 0000000..04a3db7 --- /dev/null +++ b/website_seo/models/__init__.py @@ -0,0 +1,24 @@ +# -*- 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_http +from . import ir_ui_view +from . import ir_translation +from . import website \ No newline at end of file diff --git a/website_seo/models/ir_http.py b/website_seo/models/ir_http.py new file mode 100644 index 0000000..dd8c030 --- /dev/null +++ b/website_seo/models/ir_http.py @@ -0,0 +1,58 @@ +# -*- 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 import models +from openerp.http import request + +import werkzeug + + +class IrHttp(models.TransientModel): + _inherit = 'ir.http' + + 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('/') + + request.website = request.registry['website'].get_current_website( + request.cr, request.uid, context=request.context) + + langs = [lg[0] for lg in request.website.get_languages()] + cook_lang = request.httprequest.cookies.get('website_lang') + nearest_lang = self.get_nearest_lang(path[1]) + preferred_lang = ((cook_lang if cook_lang in langs else False) + or self.get_nearest_lang(request.lang) + or request.website.default_lang_code) + + request.lang = nearest_lang or preferred_lang + + # added handling from addons/website/models/ir_http.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): + raise werkzeug.exceptions.NotFound() + + return handler diff --git a/website_seo/models/ir_translation.py b/website_seo/models/ir_translation.py new file mode 100644 index 0000000..1bfb8a4 --- /dev/null +++ b/website_seo/models/ir_translation.py @@ -0,0 +1,51 @@ +# -*- 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 import api, models + +UPDATE_TRANSLATION_DATA = { + 'ir.ui.view,seo_url': {'model': 'ir.ui.view', 'method': 'update_menu_url'} +} + + +class IrTranslation(models.Model): + _inherit = 'ir.translation' + + @api.model + def create(self, vals): + obj = super(IrTranslation, self).create(vals) + obj.update_translation_data() + return obj + + @api.multi + def write(self, vals): + res = super(IrTranslation, self).write(vals) + self.update_translation_data() + return res + + @api.multi + def update_translation_data(self): + for obj in self: + data = UPDATE_TRANSLATION_DATA.get(obj.name, False) + if data: + model = self.env[data['model']].browse([obj.res_id]) + model_context = getattr(model, 'with_context')(lang=obj.lang) + getattr(model_context, data['method'])() + diff --git a/website_seo/models/ir_ui_view.py b/website_seo/models/ir_ui_view.py new file mode 100644 index 0000000..b7749d3 --- /dev/null +++ b/website_seo/models/ir_ui_view.py @@ -0,0 +1,128 @@ +# -*- 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 import api, fields, models +from openerp.addons.website_seo.models.website import slug +from openerp.http import request + + +class View(models.Model): + + """Update view model with additional SEO variables.""" + + _name = 'ir.ui.view' + _inherit = ['ir.ui.view', 'website.seo.metadata'] + + # ToDo: if the automatically setup of the field seo_url_level is done when + # the seo_url_parent is changed set readonly to True + # ToDo: check and move if needed the two fields to website.seo.metadata, + # I think it will be necessary when the website_seo_blog module is updated + # with the same seo url handling like in the website_seo module + seo_url_level = fields.Integer( + string='SEO Url Level', readonly=False, + help='Indicates the SEO url level. It starts with the root level 0.') + seo_url_parent = fields.Many2one( + 'ir.ui.view', string='SEO Parent', + domain=[('type', '=', 'qweb'), ('inherit_id', '=', False)], + help='The SEO Parent field is used to describe hierarchical urls like ' + '"/ecommerce/study/how-to-do-it-right". Taking this as example you ' + 'have to create 3 pages (ir.ui.view records) for "ecommerce", "study" ' + 'and "how-to-do-it-right". The "ecommerce" page is the first level ' + 'part and it doesn\'t need a SEO parent. The "study" page is the ' + 'second level part and it needs the parent page "ecommerce". The ' + '"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".' + ) + seo_url_children = fields.One2many('ir.ui.view', 'seo_url_parent', 'SEO Children') + + @api.onchange('seo_url_parent') + def onchange_seo_url_parent(self): + url_level = 0 + if self.seo_url_parent: + url_level = self.seo_url_parent.seo_url_level + 1 + self.seo_url_level = url_level + + @api.multi + def write(self, vals): + res = super(View, self).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() + return res + + @api.multi + def update_related_views(self): + for obj in self: + if obj.seo_url_children: + obj.seo_url_children.write({'seo_url_level': obj.seo_url_level + 1}) + + @api.multi + def update_website_menus(self): + self.env['website.menu'].search([]).update_website_menus() + + @api.one + def get_seo_url_parts(self): + seo_url_parts = [] + if self.seo_url: + seo_url_parts.append(self.seo_url) + if self.seo_url_parent: + seo_url_parts += self.seo_url_parent.get_seo_url_parts()[0] + return seo_url_parts + + @api.one + def get_seo_path(self): + seo_url_parts = self.get_seo_url_parts()[0] + if len(seo_url_parts) == self.seo_url_level + 1: + seo_url_parts.reverse() + return '/' + '/'.join(seo_url_parts) + return False + + @api.model + def find_by_seo_path(self, path): + url_parts = path.split('/') + views = self.search([('seo_url', 'in', url_parts)], + order='seo_url_level ASC') + if len(url_parts) == len(views): + view = views[-1] + if len(views) == view.seo_url_level + 1: + return view + return False + + @api.cr_uid_ids_context + def render(self, cr, uid, id_or_xml_id, values=None, engine='ir.qweb', + context=None): + """Add additional helper variables. + + Add slug function with additional seo url handling and the query string + of the http request environment to the values object. + """ + if values is None: + values = {} + + values.update({ + 'slug': slug, + 'request_query_string': request.httprequest.environ['QUERY_STRING'] + }) + + return super(View, self).render(cr, uid, id_or_xml_id, values=values, + engine=engine, context=context) diff --git a/website_seo/models/website.py b/website_seo/models/website.py new file mode 100644 index 0000000..2d12806 --- /dev/null +++ b/website_seo/models/website.py @@ -0,0 +1,229 @@ +# -*- 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 +import urlparse + +from openerp import api, fields, models +from openerp.addons.web.http import request +from openerp.addons.website.models import website +from openerp.addons.website.models.website import slugify, is_multilang_url +from openerp.exceptions import ValidationError +from openerp.osv import orm +from openerp.tools.translate import _ + + +def url_for(path_or_uri, lang=None): + if isinstance(path_or_uri, unicode): + path_or_uri = path_or_uri.encode('utf-8') + current_path = request.httprequest.path + 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) + + if request and not url.netloc and not url.scheme and (url.path or force_lang): + location = urlparse.urljoin(current_path, location) + + lang = lang or request.context.get('lang') + langs = [lg[0] for lg in request.website.get_languages()] + + if (len(langs) > 1 or force_lang) and is_multilang_url(location, langs): + if lang != request.context.get('lang'): + translated_location = url_for_lang(location, lang) + if translated_location != location: + location = translated_location + ps = location.split('/') + if ps[1] in langs: + # Replace the language only if we explicitly provide a language to url_for + if force_lang: + ps[1] = lang + # Remove the default language unless it's explicitly provided + elif ps[1] == request.website.default_lang_code: + ps.pop(1) + # Insert the context language or the provided language + elif lang != request.website.default_lang_code or force_lang: + ps.insert(1, lang) + location = '/'.join(ps) + + return location.decode('utf-8') + + +def url_for_lang(location, lang): + # TODO: maybe search location in seo_url views instead of url menus + menu = request.registry['website.menu'] + 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 + return location + + +# change method url_for to use the one redefined here +setattr(website, 'url_for', url_for) + + +def slug(value): + """Add seo url check in slug handling.""" + if isinstance(value, orm.browse_record): + # if a seo url field exists in a record and it is not empty return it + if 'seo_url' in value._fields and value.seo_url: + return value.seo_url + + # [(id, name)] = value.name_get() + id, name = value.id, value.display_name + else: + # assume name_search result tuple + id, name = value + slugname = slugify(name or '').strip().strip('-') + if not slugname: + return str(id) + return "%s-%d" % (slugname, id) + + +class WebsiteMenu(models.Model): + + """Add translation possibility to website menu entries.""" + + _inherit = 'website.menu' + + url = fields.Char(translate=True) + + @api.one + def get_seo_url_level(self): + url_level = 0 + if self.parent_id and self.parent_id != self.env.ref('website.main_menu'): + url_level = self.parent_id.get_seo_url_level()[0] + 1 + return url_level + + @api.one + def get_website_view(self): + view = False + if self.url: + view = self.env['ir.ui.view'].find_by_seo_path(self.url) + if not view: + url_parts = self.url.split('/') + xml_id = url_parts[-1] + if '.' not in xml_id: + xml_id = 'website.%s' % 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.ui.view'].search([('key', '=', xml_id)]) + return view + + @api.model + def create(self, vals): + obj = super(WebsiteMenu, self).create(vals) + obj.update_related_views() + obj.update_website_menus() + return obj + + @api.multi + def write(self, vals): + res = super(WebsiteMenu, self).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() + return res + + @api.multi + def update_related_views(self): + for obj in self: + view = obj.get_website_view()[0] + if view: + view_parent_id = False + if obj.parent_id: + view_parent = obj.parent_id.get_website_view()[0] + view_parent_id = view_parent and view_parent.id + seo_url_level = obj.get_seo_url_level()[0] + view.write({ + 'seo_url_parent': view_parent_id, + 'seo_url_level': seo_url_level + }) + + @api.multi + def update_website_menus(self): + for obj in self: + vals = {} + view = obj.get_website_view()[0] + if view: + seo_path = view.get_seo_path()[0] + if seo_path: + vals.update({'url': seo_path}) + else: + vals.update({'url': '/page/%s' % view.key.replace('website.', '')}) + + if 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: + vals.update({'parent_id': menu.id}) + break + if vals: + obj.with_context(view_updated=True).write(vals) + + +class WebsiteSeoMetadata(models.Model): + + """Add additional SEO fields which can be used by other models.""" + + _inherit = 'website.seo.metadata' + + 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 _.') + website_meta_robots = fields.Selection([ + ('INDEX,FOLLOW', 'INDEX,FOLLOW'), + ('NOINDEX,FOLLOW', 'NOINDEX,FOLLOW'), + ('INDEX,NOFOLLOW', 'INDEX,NOFOLLOW'), + ('NOINDEX,NOFOLLOW', 'NOINDEX,NOFOLLOW') + ], string='Website meta robots') + + @api.model + def create(self, vals): + """Add check for correct SEO urls. + + Exceptional cases will be handled in the additional website SEO + modules. For example have a look at the website_seo_blog module in the + create() function in website_seo_blog/models/website_blog.py. + """ + if vals.get('seo_url', False): + self.validate_seo_url(vals['seo_url']) + + return super(WebsiteSeoMetadata, self).create(vals) + + @api.multi + def write(self, vals): + """Add check for correct SEO urls.""" + if vals.get('seo_url', False): + self.validate_seo_url(vals['seo_url']) + + return super(WebsiteSeoMetadata, self).write(vals) + + 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)): + raise ValidationError(_('Only a-z, A-Z, 0-9, - and _ are allowed ' + 'characters for the SEO url.')) + return True diff --git a/website_seo/static/description/icon.png b/website_seo/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/website_seo/static/description/index.html b/website_seo/static/description/index.html new file mode 100644 index 0000000..35a4f54 --- /dev/null +++ b/website_seo/static/description/index.html @@ -0,0 +1,50 @@ +
+
+
+

Provide the base for an improved SEO handling

+
+
+

This module adds a new seo_url field to the website.seo.metadata model. It means all models which inherit website.seo.metadata will + have the new seo_url field. In general it affects website modules like website_blog, website_forum, website_hr_recruitment etc. The module itself adds no + SEO handling. It is done in additional modules like website_seo_blog.

+
+
+

Provide the base for Robots Meta Information

+
+
+

This module adds a new 'Robots Content' field to the promote panel where the user have full control on the robots meta information. + The selected value is stored in the website_meta_robots field in the website.seo.metadata model. It means all models which inherit website.seo.metadata + will have access to this functionality except of the ir.ui.view model. We add the website_meta_robots field to this model separately because ir.ui.view + doesn't inherit website.seo.metadata.

+

The description of the meta robots information is as follow:

+
    +
  • index: pages may be indexed
  • +
  • noindex: pages may not be indexed
  • +
  • follow: pages must be followed
  • +
  • nofollow: pages should not be followed
  • +
+
+
+

Important

+

If you install this module you have to update all modules with models which inherits website.seo.metadata after the installation. In + general it is enough to update the website module. It is needed to populate the seo_url and website_meta_robots fields in all related models of the + installed website modules.

+

If you uninstall this module or a related SEO module like website_seo_blog you have to clean up the related seo_url and website_meta_robots + entries in the database table ir_model_fields manually. You also have to delete the seo_url and website_meta_robots column in the related database tables + manually.

+

Regards Bloopark

+
+
+
+
+
+

+ 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.

+
+
+
\ No newline at end of file diff --git a/website_seo/static/src/js/seo_robots.js b/website_seo/static/src/js/seo_robots.js new file mode 100644 index 0000000..ba0b328 --- /dev/null +++ b/website_seo/static/src/js/seo_robots.js @@ -0,0 +1,299 @@ +odoo.define('website_seo.seo_robots', function (require) { + 'use strict'; + + 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 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]+|^|$)'; + + 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()); + }, + changeRobots: function(robots) { + $('meta[name=robots]').attr('content', robots); + this.trigger('robots-changed', robots); + }, + seo_url: function () { + var $seo_url = $('meta[name=seo_url]'); + return ($seo_url.length > 0) && ($seo_url.attr('content') && $seo_url.attr('content').trim()); + }, + changeSeoUrl: function (seo_url) { + $('meta[name=seo_url]').attr('content', seo_url); + this.trigger('seo_url-changed', seo_url); + }, + }); + + 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'); + } + }, + }); + + seo.Configurator.include({ + events: { + 'keyup input[name=seo_page_keywords]': 'confirmKeyword', + 'keyup input[name=seo_page_title]': 'titleChanged', + 'keyup textarea[name=seo_page_description]': 'descriptionChanged', + 'change select[name=seo_page_robots]': 'robotsChanged', + 'keyup input[name=seo_url]': 'seoUrlChanged', + 'click button[data-action=add]': 'addKeyword', + 'click button[data-action=update]': 'update', + 'hidden.bs.modal': 'destroy' + }, + canEditRobots: false, + canEditSeoUrl: false, + start: function() { + var self = this; + var $modal = self.$el; + 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.suggestImprovements(); + // self.imageList = new ImageList(self, { page: htmlPage }); + // if (htmlPage.images().length === 0) { + // $modal.find('.js_image_section').remove(); + // } else { + // self.imageList.appendTo($modal.find('.js_seo_image_list')); + // } + 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 () { + $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, language) { + self.keywordList.add(word, language); + }); + self.keywordList.appendTo($modal.find('.js_seo_keywords_list')); + self.disableUnsavableFields(); + self.renderPreview(); + $modal.modal(); + self.getLanguages(); + }, + disableUnsavableFields: function () { + var self = this; + var $modal = self.$el; + self.loadMetaData().then(function(data) { + self.canEditTitle = data && ('website_meta_title' in data); + self.canEditDescription = data && ('website_meta_description' in data); + self.canEditKeywords = data && ('website_meta_keywords' in data); + // Allow editing the meta robots only for pages that have + self.canEditRobots = data && ('website_meta_robots' in data); + self.canEditSeoUrl = data && ('seo_url' in data); + if (!self.canEditTitle) { + $modal.find('input[name=seo_page_title]').attr('disabled', true); + } + if (!self.canEditDescription) { + $modal.find('textarea[name=seo_page_description]').attr('disabled', true); + } + if (!self.canEditTitle && !self.canEditDescription && !self.canEditKeywords) { + $modal.find('button[data-action=update]').attr('disabled', true); + } + if (!self.canEditRobots) { + $modal.find('select[name=seo_page_robots]').attr('disabled', true); + } + if (!self.canEditSeoUrl) { + $modal.find('input[name=seo_url]').attr('disabled', true); + } + }); + }, + update: function () { + var self = this; + var data = {}; + if (self.canEditTitle) { + data.website_meta_title = self.htmlPage.title(); + } + if (self.canEditDescription) { + data.website_meta_description = self.htmlPage.description(); + } + if (self.canEditKeywords) { + data.website_meta_keywords = self.keywordList.keywords().join(", "); + } + if (self.canEditRobots) { + data.website_meta_robots = self.htmlPage.robots(); + } + if (self.canEditSeoUrl) { + data.seo_url = self.htmlPage.seo_url(); + } + self.saveMetaData(data).then(function() { + self.$el.modal('hide'); + }); + }, + 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 fields = ['website_meta_title', 'website_meta_description', 'website_meta_keywords', 'website_meta_robots', 'seo_url']; + var model = new Model(obj.model).call('read', [[obj.id], fields, base.get_context()]).then(function (data) { + if (data.length) { + var meta = data[0]; + meta.model = obj.model; + def.resolve(meta); + } else { + def.resolve(null); + } + }).fail(function () { + def.reject(); + }); + } + return def; + }, + robotsChanged: function () { + var self = this; + setTimeout(function () { + var robots = self.$('select[name=seo_page_robots]').val(); + self.htmlPage.changeRobots(robots); + self.renderPreview(); + }, 0); + }, + seoUrlChanged: function () { + var self = this; + setTimeout(function () { + var seo_url = self.$('input[name=seo_url]').val(); + self.htmlPage.changeSeoUrl(seo_url); + self.renderPreview(); + }, 0); + }, + }); + +}); \ 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 new file mode 100644 index 0000000..d8767c0 --- /dev/null +++ b/website_seo/static/src/xml/website_seo_robots.xml @@ -0,0 +1,26 @@ + + + + + +
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
\ No newline at end of file diff --git a/website_seo/tests/__init__.py b/website_seo/tests/__init__.py new file mode 100644 index 0000000..9671cf4 --- /dev/null +++ b/website_seo/tests/__init__.py @@ -0,0 +1,21 @@ +# -*- 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 test_website_seo diff --git a/website_seo/tests/test_website_seo.py b/website_seo/tests/test_website_seo.py new file mode 100644 index 0000000..cac7220 --- /dev/null +++ b/website_seo/tests/test_website_seo.py @@ -0,0 +1,40 @@ +# -*- 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.exceptions import ValidationError +from openerp.tests import common + + +class TestWebsiteSeo(common.TransactionCase): + + """Unit tests about website SEO url validation.""" + + at_install = False + post_install = False + + def test_00_website_seo(self): + """----- Test valid SEO url.""" + self.assertTrue(self.env['website.seo.metadata']. + validate_seo_url('my-blog-post')) + + def test_01_website_seo(self): + """----- Test invalid SEO url.""" + with self.assertRaises(ValidationError): + self.env['website.seo.metadata'].validate_seo_url('my-blog-post!') diff --git a/website_seo/views/header.xml b/website_seo/views/header.xml new file mode 100644 index 0000000..a01a5e5 --- /dev/null +++ b/website_seo/views/header.xml @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/website_seo/views/ir_ui_view.xml b/website_seo/views/ir_ui_view.xml new file mode 100644 index 0000000..327aef7 --- /dev/null +++ b/website_seo/views/ir_ui_view.xml @@ -0,0 +1,40 @@ + + + + + + ir.ui.view + + + + + + + + + + + + website.seo.pages.tree + ir.ui.view + + + + + + + + + + + + Website SEO Pages + ir.ui.view + form + form,tree + + [('type', '=', 'qweb'), ('inherit_id', '=', False)] + + + + \ No newline at end of file diff --git a/website_seo/views/res_config.xml b/website_seo/views/res_config.xml new file mode 100644 index 0000000..8efe884 --- /dev/null +++ b/website_seo/views/res_config.xml @@ -0,0 +1,19 @@ + + + + + + Website settings + website.config.settings + + + + +