1 # -*- coding: utf-8 -*-
12 import werkzeug.exceptions
13 import werkzeug.wrappers
16 from openerp.osv import orm, osv, fields
17 from openerp.tools.safe_eval import safe_eval
19 from openerp.addons.web.http import request, LazyResponse
20 from ..utils import slugify
22 logger = logging.getLogger(__name__)
24 def url_for(path_or_uri, lang=None, keep_query=None):
25 location = path_or_uri.strip()
26 url = urlparse.urlparse(location)
27 if request and not url.netloc and not url.scheme:
28 location = urlparse.urljoin(request.httprequest.path, location)
29 lang = lang or request.context.get('lang')
30 langs = [lg[0] for lg in request.website.get_languages()]
31 if location[0] == '/' and len(langs) > 1 and lang != request.website.default_lang_code:
32 ps = location.split('/')
37 location = '/'.join(ps)
39 url = urlparse.urlparse(location)
41 params = werkzeug.url_decode(url.query)
42 query_params = frozenset(werkzeug.url_decode(request.httprequest.query_string).keys())
44 for param in fnmatch.filter(query_params, kq):
45 params[param] = request.params[param]
46 params = werkzeug.urls.url_encode(params)
48 location += '?%s' % params
53 if isinstance(value, orm.browse_record):
54 # [(id, name)] = value.name_get()
55 id, name = value.id, value[value._rec_name]
57 # assume name_search result tuple
59 return "%s-%d" % (slugify(name), id)
61 def urlplus(url, params):
65 # can't use urlencode because it encodes to (ascii, replace) in p2
66 return "%s?%s" % (url, '&'.join(
67 k + '=' + urllib.quote_plus(v.encode('utf-8') if isinstance(v, unicode) else str(v))
68 for k, v in params.iteritems()
71 def quote_plus(value):
72 return urllib.quote_plus(value.encode('utf-8') if isinstance(value, unicode) else str(value))
74 def preload_records(*args, **kwargs):
75 """ This helper allows to check the existence and prefetch one or many browse_records at once.
76 If the browse record(s) does not exists in the db it will raise a LazyResponse
78 field = kwargs.pop('field', 'name')
79 on_error = kwargs.pop('on_error', 'website.404')
80 error_code = kwargs.pop('error_code', 404)
83 if isinstance(arg, orm.browse_record):
85 elif isinstance(arg, orm.browse_record_list):
86 [record[field] for record in arg]
88 lazy_error = request.website.render(on_error, status_code=error_code)
89 raise werkzeug.exceptions.HTTPException(response=lazy_error)
91 class website(osv.osv):
92 def _get_menu_website(self, cr, uid, ids, context=None):
93 # IF a menu is changed, update all websites
94 return self.search(cr, uid, [], context=context)
96 def _get_menu(self, cr, uid, ids, name, arg, context=None):
97 root_domain = [('parent_id', '=', False)]
98 menus = self.pool.get('website.menu').search(cr, uid, root_domain, order='id', context=context)
99 menu = menus and menus[0] or False
100 return dict( map(lambda x: (x, menu), ids) )
102 def _get_public_user(self, cr, uid, ids, name='public_user', arg=(), context=None):
103 ref = self.get_public_user(cr, uid, context=context)
104 return dict( map(lambda x: (x, ref), ids) )
106 _name = "website" # Avoid website.website convention for conciseness (for new api). Got a special authorization from xmo and rco
107 _description = "Website"
109 'name': fields.char('Domain'),
110 'company_id': fields.many2one('res.company', string="Company"),
111 'language_ids': fields.many2many('res.lang', 'website_lang_rel', 'website_id', 'lang_id', 'Languages'),
112 'default_lang_id': fields.many2one('res.lang', string="Default language"),
113 'default_lang_code': fields.related('default_lang_id', 'code', type="char", string="Default language code", store=True),
114 'social_twitter': fields.char('Twitter Account'),
115 'social_facebook': fields.char('Facebook Account'),
116 'social_github': fields.char('GitHub Account'),
117 'social_linkedin': fields.char('LinkedIn Account'),
118 'social_youtube': fields.char('Youtube Account'),
119 'social_googleplus': fields.char('Google+ Account'),
120 'google_analytics_key': fields.char('Google Analytics Key'),
121 'public_user': fields.function(_get_public_user, relation='res.users', type='many2one', string='Public User'),
122 'menu_id': fields.function(_get_menu, relation='website.menu', type='many2one', string='Main Menu',
124 'website.menu': (_get_menu_website, ['sequence','parent_id','website_id'], 10)
128 def write(self, cr, uid, ids, vals, context=None):
129 self._get_languages.clear_cache(self)
130 return super(website, self).write(cr, uid, ids, vals, context)
132 def new_page(self, cr, uid, name, template='website.default_page', ispage=True, context=None):
133 context=context or {}
134 # completely arbitrary max_length
135 idname = slugify(name, max_length=50)
137 imd = self.pool.get('ir.model.data')
138 view = self.pool.get('ir.ui.view')
140 module, tmp_page = template.split('.')
141 view_model, view_id = imd.get_object_reference(cr, uid, module, tmp_page)
143 cr.execute('SAVEPOINT new_page')
145 newview_id = view.copy(cr, uid, view_id, context=context)
146 newview = view.browse(cr, uid, newview_id, context=context)
148 'arch': newview.arch.replace(template, "%s.%s" % (module, idname)),
152 imd.create(cr, uid, {
155 'model': 'ir.ui.view',
156 'res_id': newview_id,
159 cr.execute('RELEASE SAVEPOINT new_page')
160 return "%s.%s" % (module, idname)
162 cr.execute("ROLLBACK TO SAVEPOINT new_page")
165 def page_for_name(self, cr, uid, ids, name, module='website', context=None):
167 return '%s.%s' % (module, slugify(name, max_length=50))
169 def page_exists(self, cr, uid, ids, name, module='website', context=None):
170 page = self.page_for_name(cr, uid, ids, name, module=module, context=context)
173 self.pool["ir.model.data"].get_object_reference(cr, uid, module, name)
177 def get_public_user(self, cr, uid, context=None):
178 uid = openerp.SUPERUSER_ID
179 res = self.pool['ir.model.data'].get_object_reference(cr, uid, 'website', 'public_user')
180 return res and res[1] or False
182 @openerp.tools.ormcache(skiparg=3)
183 def _get_languages(self, cr, uid, id, context=None):
184 website = self.browse(cr, uid, id)
185 return [(lg.code, lg.name) for lg in website.language_ids]
187 def get_languages(self, cr, uid, ids, context=None):
188 return self._get_languages(cr, uid, ids[0])
190 def get_current_website(self, cr, uid, context=None):
191 # TODO: Select website, currently hard coded
192 return self.pool['website'].browse(cr, uid, 1, context=context)
194 def preprocess_request(self, cr, uid, ids, request, context=None):
195 # TODO FP: is_website_publisher and editable in context should be removed
196 # for performance reasons (1 query per image to load) but also to be cleaner
197 # I propose to replace this by a group 'base.group_website_publisher' on the
198 # view that requires it.
199 Access = request.registry['ir.model.access']
200 is_website_publisher = Access.check(cr, uid, 'ir.ui.view', 'write', False, context)
202 lang = request.context['lang']
203 is_master_lang = lang == request.website.default_lang_code
205 request.redirect = lambda url: werkzeug.utils.redirect(url_for(url))
206 request.context.update(
207 is_master_lang=is_master_lang,
208 editable=is_website_publisher,
209 translatable=not is_master_lang,
212 def _render(self, cr, uid, ids, template, values=None, context=None):
213 user = self.pool.get("res.users")
218 qweb_values = context.copy()
221 qweb_values.update(values)
226 website=request.website,
229 res_company=request.website.company_id,
230 user_id=user.browse(cr, uid, uid),
231 quote_plus=quote_plus,
233 qweb_values.setdefault('editable', False)
235 # in edit mode ir.ui.view will tag nodes
236 context['inherit_branding']=qweb_values['editable']
238 result = self.pool['ir.ui.view'].render(cr, uid, template, qweb_values, engine='website.qweb', context=context)
241 def render(self, cr, uid, ids, template, values=None, status_code=None, context=None):
242 def callback(template, values, context):
243 return self._render(cr, uid, ids, template, values, context)
246 return LazyResponse(callback, status_code=status_code, template=template, values=values, context=context)
248 def pager(self, cr, uid, ids, url, total, page=1, step=30, scope=5, url_args=None, context=None):
250 page_count = int(math.ceil(float(total) / step))
252 page = max(1, min(int(page), page_count))
255 pmin = max(page - int(math.floor(scope/2)), 1)
256 pmax = min(pmin + scope, page_count)
258 if pmax - pmin < scope:
259 pmin = pmax - scope if pmax - scope > 0 else 1
262 _url = "%spage/%s/" % (url, page)
264 _url = "%s?%s" % (_url, urllib.urlencode(url_args))
268 "page_count": page_count,
269 "offset": (page - 1) * step,
271 'url': get_url(page),
275 'url': get_url(pmin),
279 'url': get_url(max(pmin, page - 1)),
280 'num': max(pmin, page - 1)
283 'url': get_url(min(pmax, page + 1)),
284 'num': min(pmax, page + 1)
287 'url': get_url(pmax),
291 {'url': get_url(page), 'num': page}
292 for page in xrange(pmin, pmax+1)
296 def rule_is_enumerable(self, rule):
297 """ Checks that it is possible to generate sensible GET queries for
298 a given rule (if the endpoint matches its own requirements)
300 :type rule: werkzeug.routing.Rule
303 endpoint = rule.endpoint
304 methods = rule.methods or ['GET']
305 converters = rule._converters.values()
309 and endpoint.routing['type'] == 'http'
310 and endpoint.routing['auth'] in ('none', 'public')
311 and endpoint.routing.get('website', False)
312 # preclude combinatorial explosion by only allowing a single converter
313 and len(converters) <= 1
314 # ensure all converters on the rule are able to generate values for
316 and all(hasattr(converter, 'generate') for converter in converters)
317 ) and self.endpoint_is_enumerable(rule)
319 def endpoint_is_enumerable(self, rule):
320 """ Verifies that it's possible to generate a valid url for the rule's
323 :type rule: werkzeug.routing.Rule
326 spec = inspect.getargspec(rule.endpoint)
328 # if *args bail the fuck out, only dragons can live there
332 # remove all arguments with a default value from the list
333 defaults_count = len(spec.defaults or []) # spec.defaults can be None
334 # a[:-0] ~ a[:0] ~ [] -> replace defaults_count == 0 by None to get
336 args = spec.args[:(-defaults_count or None)]
338 # params with defaults were removed, leftover allowed are:
339 # * self (technically should be first-parameter-of-instance-method but whatever)
340 # * any parameter mapping to a converter
342 (arg == 'self' or arg in rule._converters)
345 def enumerate_pages(self, cr, uid, ids, query_string=None, context=None):
346 """ Available pages in the website/CMS. This is mostly used for links
347 generation and can be overridden by modules setting up new HTML
348 controllers for dynamic pages (e.g. blog).
350 By default, returns template views marked as pages.
352 :param str query_string: a (user-provided) string, fetches pages
354 :returns: a list of mappings with two keys: ``name`` is the displayable
355 name of the resource (page), ``url`` is the absolute URL
357 :rtype: list({name: str, url: str})
359 router = request.httprequest.app.get_db_router(request.db)
360 # Force enumeration to be performed as public user
361 uid = self.get_public_user(cr, uid, context=context)
362 for rule in router.iter_rules():
363 if not self.rule_is_enumerable(rule):
366 converters = rule._converters
367 filtered = bool(converters)
369 # allow single converter as decided by fp, checked by
371 [(name, converter)] = converters.items()
372 converter_values = converter.generate(
373 request.cr, uid, query=query_string, context=context)
374 generated = ({k: v} for k, v in itertools.izip(
375 itertools.repeat(name), converter_values))
377 # force single iteration for literal urls
380 for values in generated:
381 domain_part, url = rule.build(values, append_unknown=False)
382 page = {'name': url, 'url': url}
384 if not filtered and query_string and not self.page_matches(cr, uid, page, query_string, context=context):
388 def search_pages(self, cr, uid, ids, needle=None, limit=None, context=None):
389 return list(itertools.islice(
390 self.enumerate_pages(cr, uid, ids, query_string=needle, context=context),
393 def page_matches(self, cr, uid, page, needle, context=None):
394 """ Checks that a "page" matches a user-provide search string.
396 The default implementation attempts to perform a non-contiguous
397 substring match of the page's name.
399 :param page: {'name': str, 'url': str}
403 haystack = page['name'].lower()
405 needle = iter(needle.lower())
409 for char in haystack:
410 if char != n: continue
412 n = next(needle, end)
413 # found all characters of needle in haystack in order
419 def kanban(self, cr, uid, ids, model, domain, column, template, step=None, scope=None, orderby=None, context=None):
420 step = step and int(step) or 10
421 scope = scope and int(scope) or 5
422 orderby = orderby or "name"
424 get_args = dict(request.httprequest.args or {})
425 model_obj = self.pool[model]
426 relation = model_obj._columns.get(column)._obj
427 relation_obj = self.pool[relation]
429 get_args.setdefault('kanban', "")
430 kanban = get_args.pop('kanban')
431 kanban_url = "?%s&kanban=" % urllib.urlencode(get_args)
434 for col in kanban.split(","):
437 pages[int(col[0])] = int(col[1])
440 for group in model_obj.read_group(cr, uid, domain, ["id", column], groupby=column):
444 relation_id = group[column][0]
445 obj['column_id'] = relation_obj.browse(cr, uid, relation_id)
447 obj['kanban_url'] = kanban_url
448 for k, v in pages.items():
450 obj['kanban_url'] += "%s-%s" % (k, v)
453 number = model_obj.search(cr, uid, group['__domain'], count=True)
454 obj['page_count'] = int(math.ceil(float(number) / step))
455 obj['page'] = pages.get(relation_id) or 1
456 if obj['page'] > obj['page_count']:
457 obj['page'] = obj['page_count']
458 offset = (obj['page']-1) * step
459 obj['page_start'] = max(obj['page'] - int(math.floor((scope-1)/2)), 1)
460 obj['page_end'] = min(obj['page_start'] + (scope-1), obj['page_count'])
463 obj['domain'] = group['__domain']
466 obj['orderby'] = orderby
469 object_ids = model_obj.search(cr, uid, group['__domain'], limit=step, offset=offset, order=orderby)
470 obj['object_ids'] = model_obj.browse(cr, uid, object_ids)
477 'template': template,
479 return request.website._render("website.kanban_contain", values)
481 def kanban_col(self, cr, uid, ids, model, domain, page, template, step, orderby, context=None):
483 model_obj = self.pool[model]
484 domain = safe_eval(domain)
486 offset = (int(page)-1) * step
487 object_ids = model_obj.search(cr, uid, domain, limit=step, offset=offset, order=orderby)
488 object_ids = model_obj.browse(cr, uid, object_ids)
489 for object_id in object_ids:
490 html += request.website._render(template, {'object_id': object_id})
493 class website_menu(osv.osv):
494 _name = "website.menu"
495 _description = "Website Menu"
497 'name': fields.char('Menu', size=64, required=True, translate=True),
498 'url': fields.char('Url', required=True, translate=True),
499 'new_window': fields.boolean('New Window'),
500 'sequence': fields.integer('Sequence'),
501 # TODO: support multiwebsite once done for ir.ui.views
502 'website_id': fields.many2one('website', 'Website'),
503 'parent_id': fields.many2one('website.menu', 'Parent Menu', select=True, ondelete="cascade"),
504 'child_id': fields.one2many('website.menu', 'parent_id', string='Child Menus'),
505 'parent_left': fields.integer('Parent Left', select=True),
506 'parent_right': fields.integer('Parent Right', select=True),
514 _parent_order = 'sequence'
517 # would be better to take a menu_id as argument
518 def get_tree(self, cr, uid, website_id, context=None):
524 new_window=node.new_window,
525 sequence=node.sequence,
526 parent_id=node.parent_id.id,
529 for child in node.child_id:
530 menu_node['children'].append(make_tree(child))
532 menu = self.pool.get('website').browse(cr, uid, website_id, context=context).menu_id
533 return make_tree(menu)
535 def save(self, cr, uid, website_id, data, context=None):
536 def replace_id(old_id, new_id):
537 for menu in data['data']:
538 if menu['id'] == old_id:
540 if menu['parent_id'] == old_id:
541 menu['parent_id'] = new_id
542 to_delete = data['to_delete']
544 self.unlink(cr, uid, to_delete, context=context)
545 for menu in data['data']:
547 if isinstance(mid, str):
548 new_id = self.create(cr, uid, {'name': menu['name']}, context=context)
549 replace_id(mid, new_id)
550 for menu in data['data']:
551 self.write(cr, uid, [menu['id']], menu, context=context)
554 class ir_attachment(osv.osv):
555 _inherit = "ir.attachment"
556 def _website_url_get(self, cr, uid, ids, name, arg, context=None):
558 for attach in self.browse(cr, uid, ids, context=context):
559 if attach.type == 'url':
560 result[attach.id] = attach.url
562 result[attach.id] = urlplus('/website/image', {
563 'model': 'ir.attachment',
571 'website_url': fields.function(_website_url_get, string="Attachment URL", type='char')
574 class res_partner(osv.osv):
575 _inherit = "res.partner"
577 def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None):
578 partner = self.browse(cr, uid, ids[0], context=context)
580 'center': '%s, %s %s, %s' % (partner.street, partner.city, partner.zip, partner.country_id and partner.country_id.name_get()[0][1] or ''),
581 'size': "%sx%s" % (height, width),
585 return urlplus('http://maps.googleapis.com/maps/api/staticmap' , params)
587 def google_map_link(self, cr, uid, ids, zoom=8, context=None):
588 partner = self.browse(cr, uid, ids[0], context=context)
590 'q': '%s, %s %s, %s' % (partner.street, partner.city, partner.zip, partner.country_id and partner.country_id.name_get()[0][1] or ''),
592 return urlplus('https://maps.google.be/maps' , params)
594 class res_company(osv.osv):
595 _inherit = "res.company"
596 def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None):
597 partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id
598 return partner and partner.google_map_img(zoom, width, height, context=context) or None
599 def google_map_link(self, cr, uid, ids, zoom=8, context=None):
600 partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id
601 return partner and partner.google_map_link(zoom, context=context) or None
603 class base_language_install(osv.osv_memory):
604 _inherit = "base.language.install"
606 'website_ids': fields.many2many('website', string='Websites to translate'),
609 def default_get(self, cr, uid, fields, context=None):
612 defaults = super(base_language_install, self).default_get(cr, uid, fields, context)
613 website_id = context.get('params', {}).get('website_id')
615 if 'website_ids' not in defaults:
616 defaults['website_ids'] = []
617 defaults['website_ids'].append(website_id)
620 def lang_install(self, cr, uid, ids, context=None):
623 action = super(base_language_install, self).lang_install(cr, uid, ids, context)
624 language_obj = self.browse(cr, uid, ids)[0]
625 website_ids = [website.id for website in language_obj['website_ids']]
626 lang_id = self.pool['res.lang'].search(cr, uid, [('code', '=', language_obj['lang'])])
627 if website_ids and lang_id:
628 data = {'language_ids': [(4, lang_id[0])]}
629 self.pool['website'].write(cr, uid, website_ids, data)
630 params = context.get('params', {})
631 if 'url_return' in params:
633 'url': params['url_return'].replace('[lang]', language_obj['lang']),
634 'type': 'ir.actions.act_url',
639 class SeoMetadata(osv.Model):
640 _name = 'website.seo.metadata'
641 _description = 'SEO metadata'
644 'website_meta_title': fields.char("Website meta title", size=70, translate=True),
645 'website_meta_description': fields.text("Website meta description", size=160, translate=True),
646 'website_meta_keywords': fields.char("Website meta keywords", translate=True),