From 1b52b00d2aa96b9360d50a0f0960f1febb5e607e Mon Sep 17 00:00:00 2001 From: Jeremy Kersten Date: Wed, 27 Sep 2017 19:23:05 +0200 Subject: [PATCH] [IMP] website*: clean sitemap + add method to declare sitemap function. Don't add useless routes or route that will return 404. Improve generate function from ModelConverter to have a better management of query_string. Now we have an helper sitemap_qs2dom that will analyse the current route and check if query string is plausible and if yes, generate a domain, when the query_string don't seems to match the route, we return a Falsy domain. Before this commit, if qs was /product/ipad, enumerate_page check for each modelconverter of the route a name ilike '/product/ipad'. Now we check all routes that contains product and one converter that match ipad or routes that contains ipad and one converter that match product. This commit a new way to declare the sitemap for a route. def sitemap_xx(env, rule, query_string): yield {'loc': '/my_url'} @http.route(..., sitemap=sitemap_xx) In this case, only the loc returned by this function will be in the sitemap for all rules. You can pass sitempa=False, if you don't want that route are into the sitemap --- addons/auth_signup/controllers/main.py | 4 +- addons/web/controllers/main.py | 2 +- addons/website/models/ir_http.py | 27 +++++++--- addons/website/models/website.py | 50 ++++++++++++++----- .../controllers/main.py | 23 ++++++++- addons/website_customer/controllers/main.py | 24 ++++++++- addons/website_event/controllers/main.py | 10 ++-- .../website_event_track/controllers/main.py | 10 ++-- addons/website_forum/controllers/main.py | 23 ++++++--- .../controllers/main.py | 6 ++- .../website_mail_channel/controllers/main.py | 9 ++-- addons/website_slides/controllers/main.py | 41 +++++++++------ 12 files changed, 168 insertions(+), 61 deletions(-) diff --git a/addons/auth_signup/controllers/main.py b/addons/auth_signup/controllers/main.py index 262dfdad86d..98e5df56bb1 100644 --- a/addons/auth_signup/controllers/main.py +++ b/addons/auth_signup/controllers/main.py @@ -24,7 +24,7 @@ class AuthSignupHome(Home): return http.redirect_with_hash(request.params.get('redirect')) return response - @http.route('/web/signup', type='http', auth='public', website=True) + @http.route('/web/signup', type='http', auth='public', website=True, sitemap=False) def web_auth_signup(self, *args, **kw): qcontext = self.get_auth_signup_qcontext() @@ -56,7 +56,7 @@ class AuthSignupHome(Home): return request.render('auth_signup.signup', qcontext) - @http.route('/web/reset_password', type='http', auth='public', website=True) + @http.route('/web/reset_password', type='http', auth='public', website=True, sitemap=False) def web_auth_reset_password(self, *args, **kw): qcontext = self.get_auth_signup_qcontext() diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 84e5b524343..2e3ad2357fe 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -460,7 +460,7 @@ class Home(http.Controller): def _login_redirect(self, uid, redirect=None): return redirect if redirect else '/web' - @http.route('/web/login', type='http', auth="none") + @http.route('/web/login', type='http', auth="none", sitemap=False) def web_login(self, redirect=None, **kw): ensure_db() request.params['login_success'] = False diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py index b972240f881..e61b97a6b71 100644 --- a/addons/website/models/ir_http.py +++ b/addons/website/models/ir_http.py @@ -3,8 +3,8 @@ import logging import traceback -import datetime import os +import unittest import werkzeug import werkzeug.routing @@ -17,12 +17,28 @@ from odoo.http import request from odoo.tools import config from odoo.exceptions import QWebException from odoo.tools.safe_eval import safe_eval +from odoo.osv.expression import FALSE_DOMAIN from odoo.addons.http_routing.models.ir_http import ModelConverter, _guess_mimetype logger = logging.getLogger(__name__) +def sitemap_qs2dom(qs, route, field='name'): + """ Convert a query_string (can contains a path) to a domain""" + dom = [] + if qs and qs.lower() not in route: + needles = qs.strip('/').split('/') + # needles will be altered and keep only element which one is not in route + # diff(from=['shop', 'product'], to=['shop', 'product', 'product']) => to=['product'] + unittest.util.unorderable_list_difference(route.strip('/').split('/'), needles) + if len(needles) == 1: + dom = [(field, 'ilike', needles[0])] + else: + dom = FALSE_DOMAIN + return dom + + class Http(models.AbstractModel): _inherit = 'ir.http' @@ -68,7 +84,7 @@ class Http(models.AbstractModel): super(Http, cls)._add_dispatch_parameters(func) if request.is_frontend and request.routing_iteration == 1: - request.website = request.website.with_context(request.context) + request.website = request.website.with_context(context) @classmethod def _get_languages(cls): @@ -134,7 +150,6 @@ class Http(models.AbstractModel): return False - @classmethod def _handle_exception(cls, exception): code = 500 # default code @@ -223,11 +238,11 @@ class Http(models.AbstractModel): class ModelConverter(ModelConverter): - def generate(self, uid, query=None, args=None): + def generate(self, uid, dom=None, args=None): Model = request.env[self.model].sudo(uid) domain = safe_eval(self.domain, (args or {}).copy()) - if query: - domain.append((Model._rec_name, 'ilike', '%' + query + '%')) + if dom: + domain += dom for record in Model.search_read(domain=domain, fields=['write_date', Model._rec_name]): if record.get(Model._rec_name, False): yield {'loc': (record['id'], record[Model._rec_name])} diff --git a/addons/website/models/website.py b/addons/website/models/website.py index b640d4e1717..6af04e20986 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -5,16 +5,17 @@ import inspect import logging import hashlib import re -import unicodedata from werkzeug import urls from werkzeug.exceptions import NotFound from odoo import api, fields, models, tools from odoo.addons.http_routing.models.ir_http import slugify +from odoo.addons.website.models.ir_http import sitemap_qs2dom from odoo.addons.portal.controllers.portal import pager from odoo.tools import pycompat from odoo.http import request +from odoo.osv.expression import FALSE_DOMAIN from odoo.tools.translate import _ logger = logging.getLogger(__name__) @@ -360,9 +361,25 @@ class Website(models.Model): router = request.httprequest.app.get_db_router(request.db) # Force enumeration to be performed as public user url_set = set() + + sitemap_endpoint_done = set() + for rule in router.iter_rules(): + if 'sitemap' in rule.endpoint.routing: + if rule.endpoint in sitemap_endpoint_done: + continue + sitemap_endpoint_done.add(rule.endpoint) + + func = rule.endpoint.routing['sitemap'] + if func is False: + continue + for loc in func(self.env, rule, query_string): + yield loc + continue + if not self.rule_is_enumerable(rule): continue + converters = rule._converters or {} if query_string and not converters and (query_string not in rule.build([{}], append_unknown=False)[1]): continue @@ -370,12 +387,18 @@ class Website(models.Model): # converters with a domain are processed after the other ones convitems = sorted( converters.items(), - key=lambda x: hasattr(x[1], 'domain') and (x[1].domain != '[]')) + key=lambda x: (hasattr(x[1], 'domain') and (x[1].domain != '[]'), rule._trace.index((True, x[0])))) + for (i, (name, converter)) in enumerate(convitems): newval = [] for val in values: query = i == len(convitems)-1 and query_string - for value_dict in converter.generate(uid=self.env.uid, query=query, args=val): + if query: + r = "".join([x[1] for x in rule._trace[1:] if not x[0]]) # remove model converter from route + query = sitemap_qs2dom(query, r) + if query == FALSE_DOMAIN: + continue + for value_dict in converter.generate(uid=self.env.uid, dom=query, args=val): newval.append(val.copy()) value_dict[name] = value_dict['loc'] del value_dict['loc'] @@ -384,17 +407,18 @@ class Website(models.Model): for value in values: domain_part, url = rule.build(value, append_unknown=False) - page = {'loc': url} - for key, val in value.items(): - if key.startswith('__'): - page[key[2:]] = val - if url in ('/sitemap.xml',): - continue - if url in url_set: - continue - url_set.add(url) + if not query_string or query_string.lower() in url.lower(): + page = {'loc': url} + for key, val in value.items(): + if key.startswith('__'): + page[key[2:]] = val + if url in ('/sitemap.xml',): + continue + if url in url_set: + continue + url_set.add(url) - yield page + yield page # '/' already has a http.route & is in the routing_map so it will already have an entry in the xml domain = [('url', '!=', '/')] diff --git a/addons/website_crm_partner_assign/controllers/main.py b/addons/website_crm_partner_assign/controllers/main.py index 63d4926d185..9166b37fa31 100644 --- a/addons/website_crm_partner_assign/controllers/main.py +++ b/addons/website_crm_partner_assign/controllers/main.py @@ -11,6 +11,7 @@ from odoo import fields from odoo import http from odoo.http import request from odoo.addons.http_routing.models.ir_http import slug, unslug +from odoo.addons.website.models.ir_http import sitemap_qs2dom from odoo.addons.portal.controllers.portal import CustomerPortal from odoo.addons.website_partner.controllers.main import WebsitePartnerPage @@ -179,6 +180,26 @@ class WebsiteAccount(CustomerPortal): class WebsiteCrmPartnerAssign(WebsitePartnerPage): _references_per_page = 40 + def sitemap_partners(env, rule, qs): + if not qs or qs.lower() in '/partners': + yield {'loc': '/partners'} + + Grade = env['res.partner.grade'] + dom = [('website_published', '=', True)] + dom += sitemap_qs2dom(qs=qs, route='/partners/grade/', field=Grade._rec_name) + for grade in env['res.partner.grade'].search(dom): + loc = '/partners/grade/%s' % slug(grade) + if not qs or qs.lower() in loc: + yield {'loc': loc} + + partners_dom = [('is_company', '=', True), ('grade_id', '!=', False), ('website_published', '=', True), ('grade_id.website_published', '=', True)] + dom += sitemap_qs2dom(qs=qs, route='/partners/country/') + countries = env['res.partner'].sudo().read_group(partners_dom, fields=['id', 'country_id'], groupby='country_id') + for country in countries: + loc = '/partners/country/%s' % slug(country['country_id']) + if not qs or qs.lower() in loc: + yield {'loc': loc} + @http.route([ '/partners', '/partners/page/', @@ -191,7 +212,7 @@ class WebsiteCrmPartnerAssign(WebsitePartnerPage): '/partners/grade//country/', '/partners/grade//country//page/', - ], type='http', auth="public", website=True) + ], type='http', auth="public", website=True, sitemap=sitemap_partners) def partners(self, country=None, grade=None, page=0, **post): country_all = post.pop('country_all', False) partner_obj = request.env['res.partner'] diff --git a/addons/website_customer/controllers/main.py b/addons/website_customer/controllers/main.py index 480bb62c4d4..421a809b36d 100644 --- a/addons/website_customer/controllers/main.py +++ b/addons/website_customer/controllers/main.py @@ -4,7 +4,8 @@ import werkzeug.urls from odoo import http -from odoo.addons.http_routing.models.ir_http import unslug +from odoo.addons.http_routing.models.ir_http import unslug, slug +from odoo.addons.website.models.ir_http import sitemap_qs2dom from odoo.tools.translate import _ from odoo.http import request @@ -12,6 +13,25 @@ from odoo.http import request class WebsiteCustomer(http.Controller): _references_per_page = 20 + def sitemap_industry(env, rule, qs): + if not qs or qs.lower() in '/customers': + yield {'loc': '/customers'} + + Industry = env['res.partner.industry'] + dom = sitemap_qs2dom(qs, '/customers/industry', Industry._rec_name) + for industry in Industry.search(dom): + loc = '/customers/industry/%s' % slug(industry) + if not qs or qs.lower() in loc: + yield {'loc': loc} + + dom = [('website_published', '=', True), ('assigned_partner_id', '!=', False)] + dom += sitemap_qs2dom(qs, '/customers/country') + countries = env['res.partner'].sudo().read_group(dom, ['id', 'country_id'], groupby='country_id') + for country in countries: + loc = '/customers/country/%s' % slug(country['country_id']) + if not qs or qs.lower() in loc: + yield {'loc': loc} + @http.route([ '/customers', '/customers/page/', @@ -21,7 +41,7 @@ class WebsiteCustomer(http.Controller): '/customers/industry//page/', '/customers/industry//country/', '/customers/industry//country//page/', - ], type='http', auth="public", website=True) + ], type='http', auth="public", website=True, sitemap=sitemap_industry) def customers(self, country=None, industry=None, page=0, **post): Tag = request.env['res.partner.tag'] Partner = request.env['res.partner'] diff --git a/addons/website_event/controllers/main.py b/addons/website_event/controllers/main.py index ff1f9c00ce1..28ac0c17a8a 100644 --- a/addons/website_event/controllers/main.py +++ b/addons/website_event/controllers/main.py @@ -13,7 +13,11 @@ from odoo.http import request class WebsiteEventController(http.Controller): - @http.route(['/event', '/event/page/', '/events', '/events/page/'], type='http', auth="public", website=True) + def sitemap_event(env, rule, qs): + if not qs or qs.lower() in '/events': + yield {'loc': '/events'} + + @http.route(['/event', '/event/page/', '/events', '/events/page/'], type='http', auth="public", website=True, sitemap=sitemap_event) def events(self, page=1, **searches): Event = request.env['event.event'] EventType = request.env['event.type'] @@ -133,7 +137,7 @@ class WebsiteEventController(http.Controller): return request.render("website_event.index", values) - @http.route(['/event//page/'], type='http', auth="public", website=True) + @http.route(['/event//page/'], type='http', auth="public", website=True, sitemap=False) def event_page(self, event, page, **post): values = { 'event': event, @@ -163,7 +167,7 @@ class WebsiteEventController(http.Controller): target_url += '?enable_editor=1' return request.redirect(target_url) - @http.route(['/event//register'], type='http', auth="public", website=True) + @http.route(['/event//register'], type='http', auth="public", website=True, sitemap=False) def event_register(self, event, **post): values = { 'event': event, diff --git a/addons/website_event_track/controllers/main.py b/addons/website_event_track/controllers/main.py index f11aa0bea13..f2ae337deda 100644 --- a/addons/website_event_track/controllers/main.py +++ b/addons/website_event_track/controllers/main.py @@ -65,7 +65,7 @@ class WebsiteEventTrackController(http.Controller): 'dates': dates } - @http.route(['''/event//agenda'''], type='http', auth="public", website=True, sitemap=False) def event_agenda(self, event, tag=None, **post): days_tracks = collections.defaultdict(lambda: []) for track in event.track_ids.sorted(lambda track: (track.date or '', bool(track.location_id))): @@ -87,9 +87,9 @@ class WebsiteEventTrackController(http.Controller): }) @http.route([ - '''/event//track''', + '''/event//track/tag/''' + ], type='http', auth="public", website=True, sitemap=False) def event_tracks(self, event, tag=None, **post): searches = {} if tag: @@ -108,7 +108,7 @@ class WebsiteEventTrackController(http.Controller): } return request.render("website_event_track.tracks", values) - @http.route(['''/event//track_proposal'''], type='http', auth="public", website=True, sitemap=False) def event_track_proposal(self, event, **post): return request.render("website_event_track.event_track_proposal", {'event': event}) diff --git a/addons/website_forum/controllers/main.py b/addons/website_forum/controllers/main.py index 911e3eb6d93..5f315491074 100644 --- a/addons/website_forum/controllers/main.py +++ b/addons/website_forum/controllers/main.py @@ -13,6 +13,7 @@ from datetime import datetime from odoo import http, modules, SUPERUSER_ID, tools, _ from odoo.addons.http_routing.models.ir_http import slug from odoo.addons.web.controllers.main import binary_content +from odoo.addons.website.models.ir_http import sitemap_qs2dom from odoo.http import request @@ -56,7 +57,7 @@ class WebsiteForum(http.Controller): request.session['validation_email_sent'] = True return True - @http.route('/forum/validate_email', type='http', auth='public', website=True) + @http.route('/forum/validate_email', type='http', auth='public', website=True, sitemap=False) def validate_email(self, token, id, email, forum_id=None, **kwargs): if forum_id: try: @@ -100,11 +101,19 @@ class WebsiteForum(http.Controller): request.env['mail.message'].browse([int(kwargs.get('notification_id'))]).set_message_done() return True + def sitemap_forum(env, rule, qs): + Forum = env['forum.forum'] + dom = sitemap_qs2dom(qs, '/forum', Forum._rec_name) + for f in Forum.search(dom): + loc = '/forum/%s' % slug(f) + if not qs or qs.lower() in loc: + yield {'loc': loc} + @http.route(['/forum/', '/forum//page/', - '''/forum//tag//tag//tag//questions''', + '''/forum//tag//questions/page/''', + ], type='http', auth="public", website=True, sitemap=sitemap_forum) def questions(self, forum, tag=None, page=1, filters='all', sorting=None, search='', post_type=None, **post): Post = request.env['forum.post'] @@ -170,7 +179,7 @@ class WebsiteForum(http.Controller): values = self._prepare_forum_values(forum=forum, searches=dict(), header={'is_guidelines': True}, **post) return request.render("website_forum.faq", values) - @http.route('/forum/get_tags', type='http', auth="public", methods=['GET'], website=True) + @http.route('/forum/get_tags', type='http', auth="public", methods=['GET'], website=True, sitemap=False) def tag_read(self, q='', l=25, **post): data = request.env['forum.tag'].search_read( domain=[('name', '=ilike', (q or '') + "%")], @@ -179,7 +188,7 @@ class WebsiteForum(http.Controller): ) return json.dumps(data) - @http.route(['/forum//tag', '/forum//tag/'], type='http', auth="public", website=True) + @http.route(['/forum//tag', '/forum//tag/'], type='http', auth="public", website=True, sitemap=False) def tags(self, forum, tag_char=None, **post): # build the list of tag first char, with their value as tag_char param Ex : [('All', 'all'), ('C', 'c'), ('G', 'g'), ('Z', z)] first_char_tag = forum.get_tags_first_char() @@ -569,7 +578,7 @@ class WebsiteForum(http.Controller): return werkzeug.utils.redirect("/forum/%s/user/%d" % (slug(forum), partner.user_ids[0].id)) return werkzeug.utils.redirect("/forum/%s" % slug(forum)) - @http.route(['/forum/user//avatar'], type='http', auth="public", website=True) + @http.route(['/forum/user//avatar'], type='http', auth="public", website=True, sitemap=False) def user_avatar(self, user_id=0, **post): status, headers, content = binary_content(model='res.users', id=user_id, field='image_medium', default_mimetype='image/png', env=request.env(user=SUPERUSER_ID)) diff --git a/addons/website_hr_recruitment/controllers/main.py b/addons/website_hr_recruitment/controllers/main.py index 4556976d9cf..3ff0b29b4b6 100644 --- a/addons/website_hr_recruitment/controllers/main.py +++ b/addons/website_hr_recruitment/controllers/main.py @@ -7,6 +7,10 @@ from odoo.http import request class WebsiteHrRecruitment(http.Controller): + def sitemap_jobs(env, rule, qs): + if not qs or qs.lower() in '/jobs': + yield {'loc': '/jobs'} + @http.route([ '/jobs', '/jobs/country/', @@ -16,7 +20,7 @@ class WebsiteHrRecruitment(http.Controller): '/jobs/country//office/', '/jobs/department//office/', '/jobs/country//department//office/', - ], type='http', auth="public", website=True) + ], type='http', auth="public", website=True, sitemap=sitemap_jobs) def jobs(self, country=None, department=None, office_id=None, **kwargs): env = request.env(context=dict(request.env.context, show_address=True, no_tag_br=True)) diff --git a/addons/website_mail_channel/controllers/main.py b/addons/website_mail_channel/controllers/main.py index e14f355f9f4..f3e5b51847e 100644 --- a/addons/website_mail_channel/controllers/main.py +++ b/addons/website_mail_channel/controllers/main.py @@ -116,10 +116,9 @@ class MailGroup(http.Controller): channel.sudo()._send_confirmation_email(partner_ids, unsubscribe) return "email" - @http.route([ - "/groups/", - "/groups//page/" + '''/groups/''', + '''/groups//page/''' ], type='http', auth="public", website=True) def thread_headers(self, group, page=1, mode='thread', date_begin=None, date_end=None, **post): if group.channel_type != 'channel': @@ -154,7 +153,7 @@ class MailGroup(http.Controller): return request.render('website_mail_channel.group_messages', values) @http.route([ - '''/groups/////''', + '''/slides//page/''', - '''/slides//''', + '''/slides///page/''', - '''/slides//tag/''', + '''/slides//tag//page/''', - '''/slides//category/''', + '''/slides//category//page/''', - '''/slides//category//''', + '''/slides//category///page/'''], + type='http', auth="public", website=True, sitemap=sitemap_slide) def channel(self, channel, category=None, tag=None, page=1, slide_type=None, sorting='creation', search=None, **kw): user = request.env.user Slide = request.env['slide.slide'] @@ -161,15 +171,16 @@ class WebsiteSlides(http.Controller): self._set_viewed_slide(slide, 'slide') return request.render('website_slides.slide_detail_view', values) - @http.route('''/slides/slide//pdf_content''', type='http', auth="public", website=True) + @http.route('''/slides/slide//pdf_content''', + type='http', auth="public", website=True, sitemap=False) def slide_get_pdf_content(self, slide): response = werkzeug.wrappers.Response() response.data = slide.datas and base64.b64decode(slide.datas) or b'' response.mimetype = 'application/pdf' return response - @http.route('''/slides/slide//download''', type='http', auth="public", website=True) - def slide_download(self, slide): + @http.route('''/slides/slide//download''', type='http', auth="public", website=True) + def slide_download(self, slide, sitemap=False): if slide.download_security == 'public' or (slide.download_security == 'user' and request.session.uid): filecontent = base64.b64decode(slide.datas) disposition = 'attachment; filename=%s.pdf' % werkzeug.urls.url_quote(slide.name) @@ -279,7 +290,7 @@ class WebsiteSlides(http.Controller): # -------------------------------------------------- # EMBED IN THIRD PARTY WEBSITES # -------------------------------------------------- - @http.route('/slides/embed/', type='http', auth='public', website=True) + @http.route('/slides/embed/', type='http', auth='public', website=True, sitemap=False) def slides_embed(self, slide_id, page="1", **kw): # Note : don't use the 'model' in the route (use 'slide_id'), otherwise if public cannot access the embedded # slide, the error will be the website.403 page instead of the one of the website_slides.embed_slide.