[IMP] Sitemap
[odoo/odoo.git] / addons / website / models / website.py
1 # -*- coding: utf-8 -*-
2 import hashlib
3 import inspect
4 import itertools
5 import logging
6 import math
7 import mimetypes
8 import re
9 import urlparse
10
11 import werkzeug
12 import werkzeug.exceptions
13 import werkzeug.utils
14 import werkzeug.wrappers
15 # optional python-slugify import (https://github.com/un33k/python-slugify)
16 try:
17     import slugify as slugify_lib
18 except ImportError:
19     slugify_lib = None
20
21 import openerp
22 from openerp.osv import orm, osv, fields
23 from openerp.tools.safe_eval import safe_eval
24 from openerp.addons.web.http import request
25
26 logger = logging.getLogger(__name__)
27
28 def url_for(path_or_uri, lang=None):
29     if isinstance(path_or_uri, unicode):
30         path_or_uri = path_or_uri.encode('utf-8')
31     current_path = request.httprequest.path
32     if isinstance(current_path, unicode):
33         current_path = current_path.encode('utf-8')
34     location = path_or_uri.strip()
35     force_lang = lang is not None
36     url = urlparse.urlparse(location)
37
38     if request and not url.netloc and not url.scheme and (url.path or force_lang):
39         location = urlparse.urljoin(current_path, location)
40
41         lang = lang or request.context.get('lang')
42         langs = [lg[0] for lg in request.website.get_languages()]
43
44         if (len(langs) > 1 or force_lang) and is_multilang_url(location, langs):
45             ps = location.split('/')
46             if ps[1] in langs:
47                 # Replace the language only if we explicitly provide a language to url_for
48                 if force_lang:
49                     ps[1] = lang
50                 # Remove the default language unless it's explicitly provided
51                 elif ps[1] == request.website.default_lang_code:
52                     ps.pop(1)
53             # Insert the context language or the provided language
54             elif lang != request.website.default_lang_code or force_lang:
55                 ps.insert(1, lang)
56             location = '/'.join(ps)
57
58     return location.decode('utf-8')
59
60 def is_multilang_url(local_url, langs=None):
61     if not langs:
62         langs = [lg[0] for lg in request.website.get_languages()]
63     spath = local_url.split('/')
64     # if a language is already in the path, remove it
65     if spath[1] in langs:
66         spath.pop(1)
67         local_url = '/'.join(spath)
68     try:
69         # Try to match an endpoint in werkzeug's routing table
70         url = local_url.split('?')
71         path = url[0]
72         query_string = url[1] if len(url) > 1 else None
73         router = request.httprequest.app.get_db_router(request.db).bind('')
74         func = router.match(path, query_args=query_string)[0]
75         return func.routing.get('multilang', False)
76     except Exception:
77         return False
78
79 def slugify(s, max_length=None):
80     if slugify_lib:
81         # There are 2 different libraries only python-slugify is supported
82         try:
83             return slugify_lib.slugify(s, max_length=max_length)
84         except TypeError:
85             pass
86     spaceless = re.sub(r'\s+', '-', s)
87     specialless = re.sub(r'[^-_A-Za-z0-9]', '', spaceless)
88     return specialless[:max_length]
89
90 def slug(value):
91     if isinstance(value, orm.browse_record):
92         # [(id, name)] = value.name_get()
93         id, name = value.id, value[value._rec_name]
94     else:
95         # assume name_search result tuple
96         id, name = value
97     slugname = slugify(name or '')
98     if not slugname:
99         return str(id)
100     return "%s-%d" % (slugname, id)
101
102 def urlplus(url, params):
103     return werkzeug.Href(url)(params or None)
104
105 class website(osv.osv):
106     def _get_menu_website(self, cr, uid, ids, context=None):
107         # IF a menu is changed, update all websites
108         return self.search(cr, uid, [], context=context)
109
110     def _get_menu(self, cr, uid, ids, name, arg, context=None):
111         root_domain = [('parent_id', '=', False)]
112         menus = self.pool.get('website.menu').search(cr, uid, root_domain, order='id', context=context)
113         menu = menus and menus[0] or False
114         return dict( map(lambda x: (x, menu), ids) )
115
116     _name = "website" # Avoid website.website convention for conciseness (for new api). Got a special authorization from xmo and rco
117     _description = "Website"
118     _columns = {
119         'name': fields.char('Domain'),
120         'company_id': fields.many2one('res.company', string="Company"),
121         'language_ids': fields.many2many('res.lang', 'website_lang_rel', 'website_id', 'lang_id', 'Languages'),
122         'default_lang_id': fields.many2one('res.lang', string="Default language"),
123         'default_lang_code': fields.related('default_lang_id', 'code', type="char", string="Default language code", store=True),
124         'social_twitter': fields.char('Twitter Account'),
125         'social_facebook': fields.char('Facebook Account'),
126         'social_github': fields.char('GitHub Account'),
127         'social_linkedin': fields.char('LinkedIn Account'),
128         'social_youtube': fields.char('Youtube Account'),
129         'social_googleplus': fields.char('Google+ Account'),
130         'google_analytics_key': fields.char('Google Analytics Key'),
131         'user_id': fields.many2one('res.users', string='Public User'),
132         'partner_id': fields.related('user_id','partner_id', type='many2one', relation='res.partner', string='Public Partner'),
133         'menu_id': fields.function(_get_menu, relation='website.menu', type='many2one', string='Main Menu',
134             store= {
135                 'website.menu': (_get_menu_website, ['sequence','parent_id','website_id'], 10)
136             })
137     }
138
139     _defaults = {
140         'company_id': lambda self,cr,uid,c: self.pool['ir.model.data'].xmlid_to_res_id(cr, openerp.SUPERUSER_ID, 'base.public_user'),
141     }
142     
143     # cf. Wizard hack in website_views.xml
144     def noop(self, *args, **kwargs):
145         pass
146
147     def write(self, cr, uid, ids, vals, context=None):
148         self._get_languages.clear_cache(self)
149         return super(website, self).write(cr, uid, ids, vals, context)
150
151     def new_page(self, cr, uid, name, template='website.default_page', ispage=True, context=None):
152         context = context or {}
153         imd = self.pool.get('ir.model.data')
154         view = self.pool.get('ir.ui.view')
155         template_module, template_name = template.split('.')
156
157         # completely arbitrary max_length
158         page_name = slugify(name, max_length=50)
159         page_xmlid = "%s.%s" % (template_module, page_name)
160
161         try:
162             # existing page
163             imd.get_object_reference(cr, uid, template_module, page_name)
164         except ValueError:
165             # new page
166             _, template_id = imd.get_object_reference(cr, uid, template_module, template_name)
167             page_id = view.copy(cr, uid, template_id, context=context)
168             page = view.browse(cr, uid, page_id, context=context)
169             page.write({
170                 'arch': page.arch.replace(template, page_xmlid),
171                 'name': page_name,
172                 'page': ispage,
173             })
174             imd.create(cr, uid, {
175                 'name': page_name,
176                 'module': template_module,
177                 'model': 'ir.ui.view',
178                 'res_id': page_id,
179                 'noupdate': True
180             }, context=context)
181         return page_xmlid
182
183     def page_for_name(self, cr, uid, ids, name, module='website', context=None):
184         # whatever
185         return '%s.%s' % (module, slugify(name, max_length=50))
186
187     def page_exists(self, cr, uid, ids, name, module='website', context=None):
188         try:
189            return self.pool["ir.model.data"].get_object_reference(cr, uid, module, name)
190         except:
191             return False
192
193     @openerp.tools.ormcache(skiparg=3)
194     def _get_languages(self, cr, uid, id, context=None):
195         website = self.browse(cr, uid, id)
196         return [(lg.code, lg.name) for lg in website.language_ids]
197
198     def get_languages(self, cr, uid, ids, context=None):
199         return self._get_languages(cr, uid, ids[0])
200
201     def get_current_website(self, cr, uid, context=None):
202         # TODO: Select website, currently hard coded
203         return self.pool['website'].browse(cr, uid, 1, context=context)
204
205     def is_publisher(self, cr, uid, ids, context=None):
206         Access = self.pool['ir.model.access']
207         is_website_publisher = Access.check(cr, uid, 'ir.ui.view', 'write', False, context)
208         return is_website_publisher
209
210     def get_template(self, cr, uid, ids, template, context=None):
211         if isinstance(template, (int, long)):
212             view_id = template
213         else:
214             if '.' not in template:
215                 template = 'website.%s' % template
216             module, xmlid = template.split('.', 1)
217             model, view_id = request.registry["ir.model.data"].get_object_reference(cr, uid, module, xmlid)
218         return self.pool["ir.ui.view"].browse(cr, uid, view_id, context=context)
219
220     def _render(self, cr, uid, ids, template, values=None, context=None):
221         # TODO: remove this. (just kept for backward api compatibility for saas-3)
222         return self.pool['ir.ui.view'].render(cr, uid, template, values=values, context=context)
223
224     def render(self, cr, uid, ids, template, values=None, status_code=None, context=None):
225         # TODO: remove this. (just kept for backward api compatibility for saas-3)
226         return request.render(template, values, uid=uid)
227
228     def pager(self, cr, uid, ids, url, total, page=1, step=30, scope=5, url_args=None, context=None):
229         # Compute Pager
230         page_count = int(math.ceil(float(total) / step))
231
232         page = max(1, min(int(page), page_count))
233         scope -= 1
234
235         pmin = max(page - int(math.floor(scope/2)), 1)
236         pmax = min(pmin + scope, page_count)
237
238         if pmax - pmin < scope:
239             pmin = pmax - scope if pmax - scope > 0 else 1
240
241         def get_url(page):
242             _url = "%s/page/%s" % (url, page) if page > 1 else url
243             if url_args:
244                 _url = "%s?%s" % (_url, werkzeug.url_encode(url_args))
245             return _url
246
247         return {
248             "page_count": page_count,
249             "offset": (page - 1) * step,
250             "page": {
251                 'url': get_url(page),
252                 'num': page
253             },
254             "page_start": {
255                 'url': get_url(pmin),
256                 'num': pmin
257             },
258             "page_previous": {
259                 'url': get_url(max(pmin, page - 1)),
260                 'num': max(pmin, page - 1)
261             },
262             "page_next": {
263                 'url': get_url(min(pmax, page + 1)),
264                 'num': min(pmax, page + 1)
265             },
266             "page_end": {
267                 'url': get_url(pmax),
268                 'num': pmax
269             },
270             "pages": [
271                 {'url': get_url(page), 'num': page}
272                 for page in xrange(pmin, pmax+1)
273             ]
274         }
275
276     def rule_is_enumerable(self, rule):
277         """ Checks that it is possible to generate sensible GET queries for
278         a given rule (if the endpoint matches its own requirements)
279
280         :type rule: werkzeug.routing.Rule
281         :rtype: bool
282         """
283         endpoint = rule.endpoint
284         methods = rule.methods or ['GET']
285         converters = rule._converters.values()
286         if not ('GET' in methods
287             and endpoint.routing['type'] == 'http'
288             and endpoint.routing['auth'] in ('none', 'public')
289             and endpoint.routing.get('website', False)
290             and all(hasattr(converter, 'generate') for converter in converters)
291             and endpoint.routing.get('website')):
292             return False
293
294         # dont't list routes without argument having no default value or converter
295         spec = inspect.getargspec(endpoint.method.original_func)
296
297         # remove self and arguments having a default value
298         defaults_count = len(spec.defaults or [])
299         args = spec.args[1:(-defaults_count or None)]
300
301         # check that all args have a converter
302         return all( (arg in rule._converters) for arg in args)
303
304     def enumerate_pages(self, cr, uid, ids, query_string=None, context=None):
305         """ Available pages in the website/CMS. This is mostly used for links
306         generation and can be overridden by modules setting up new HTML
307         controllers for dynamic pages (e.g. blog).
308
309         By default, returns template views marked as pages.
310
311         :param str query_string: a (user-provided) string, fetches pages
312                                  matching the string
313         :returns: a list of mappings with two keys: ``name`` is the displayable
314                   name of the resource (page), ``url`` is the absolute URL
315                   of the same.
316         :rtype: list({name: str, url: str})
317         """
318         router = request.httprequest.app.get_db_router(request.db)
319         # Force enumeration to be performed as public user
320         uid = request.website.user_id.id
321         url_list = []
322         for rule in router.iter_rules():
323             if not self.rule_is_enumerable(rule):
324                 continue
325
326             converters = rule._converters or {}
327             values = [{}]
328             convitems = converters.items()
329             # converters with a domain are processed after the other ones
330             gd = lambda x: hasattr(x[1], 'domain') and (x[1].domain <> '[]')
331             convitems.sort(lambda x, y: cmp(gd(x), gd(y)))
332             for (name, converter) in convitems:
333                 newval = []
334                 for val in values:
335                     for v in converter.generate(request.cr, uid, query=query_string, args=val, context=context):
336                         newval.append( val.copy() )
337                         v[name] = v['loc']
338                         del v['loc']
339                         newval[-1].update(v)
340                 values = newval
341
342             for value in values:
343                 domain_part, url = rule.build(value, append_unknown=False)
344                 page = {'loc': url}
345                 for key,val in value.items():
346                     if key.startswith('__'):
347                         page[key[2:]] = val
348                 if url in ('/sitemap.xml',):
349                     continue
350                 if url in url_list:
351                     continue
352                 url_list.append(url)
353                 if query_string and not self.page_matches(cr, uid, page, query_string, context=context):
354                     continue
355                 yield page
356
357     def search_pages(self, cr, uid, ids, needle=None, limit=None, context=None):
358         return list(itertools.islice(
359             self.enumerate_pages(cr, uid, ids, query_string=needle, context=context),
360             limit))
361
362     def page_matches(self, cr, uid, page, needle, context=None):
363         """ Checks that a "page" matches a user-provide search string.
364
365         The default implementation attempts to perform a non-contiguous
366         substring match of the page's name.
367
368         :param page: {'name': str, 'url': str}
369         :param needle: str
370         :rtype: bool
371         """
372         haystack = page['name'].lower()
373
374         needle = iter(needle.lower())
375         n = next(needle)
376         end = object()
377
378         for char in haystack:
379             if char != n: continue
380
381             n = next(needle, end)
382             # found all characters of needle in haystack in order
383             if n is end:
384                 return True
385
386         return False
387
388     def kanban(self, cr, uid, ids, model, domain, column, template, step=None, scope=None, orderby=None, context=None):
389         step = step and int(step) or 10
390         scope = scope and int(scope) or 5
391         orderby = orderby or "name"
392
393         get_args = dict(request.httprequest.args or {})
394         model_obj = self.pool[model]
395         relation = model_obj._columns.get(column)._obj
396         relation_obj = self.pool[relation]
397
398         get_args.setdefault('kanban', "")
399         kanban = get_args.pop('kanban')
400         kanban_url = "?%s&kanban=" % werkzeug.url_encode(get_args)
401
402         pages = {}
403         for col in kanban.split(","):
404             if col:
405                 col = col.split("-")
406                 pages[int(col[0])] = int(col[1])
407
408         objects = []
409         for group in model_obj.read_group(cr, uid, domain, ["id", column], groupby=column):
410             obj = {}
411
412             # browse column
413             relation_id = group[column][0]
414             obj['column_id'] = relation_obj.browse(cr, uid, relation_id)
415
416             obj['kanban_url'] = kanban_url
417             for k, v in pages.items():
418                 if k != relation_id:
419                     obj['kanban_url'] += "%s-%s" % (k, v)
420
421             # pager
422             number = model_obj.search(cr, uid, group['__domain'], count=True)
423             obj['page_count'] = int(math.ceil(float(number) / step))
424             obj['page'] = pages.get(relation_id) or 1
425             if obj['page'] > obj['page_count']:
426                 obj['page'] = obj['page_count']
427             offset = (obj['page']-1) * step
428             obj['page_start'] = max(obj['page'] - int(math.floor((scope-1)/2)), 1)
429             obj['page_end'] = min(obj['page_start'] + (scope-1), obj['page_count'])
430
431             # view data
432             obj['domain'] = group['__domain']
433             obj['model'] = model
434             obj['step'] = step
435             obj['orderby'] = orderby
436
437             # browse objects
438             object_ids = model_obj.search(cr, uid, group['__domain'], limit=step, offset=offset, order=orderby)
439             obj['object_ids'] = model_obj.browse(cr, uid, object_ids)
440
441             objects.append(obj)
442
443         values = {
444             'objects': objects,
445             'range': range,
446             'template': template,
447         }
448         return request.website._render("website.kanban_contain", values)
449
450     def kanban_col(self, cr, uid, ids, model, domain, page, template, step, orderby, context=None):
451         html = ""
452         model_obj = self.pool[model]
453         domain = safe_eval(domain)
454         step = int(step)
455         offset = (int(page)-1) * step
456         object_ids = model_obj.search(cr, uid, domain, limit=step, offset=offset, order=orderby)
457         object_ids = model_obj.browse(cr, uid, object_ids)
458         for object_id in object_ids:
459             html += request.website._render(template, {'object_id': object_id})
460         return html
461
462 class website_menu(osv.osv):
463     _name = "website.menu"
464     _description = "Website Menu"
465     _columns = {
466         'name': fields.char('Menu', size=64, required=True, translate=True),
467         'url': fields.char('Url', translate=True),
468         'new_window': fields.boolean('New Window'),
469         'sequence': fields.integer('Sequence'),
470         # TODO: support multiwebsite once done for ir.ui.views
471         'website_id': fields.many2one('website', 'Website'),
472         'parent_id': fields.many2one('website.menu', 'Parent Menu', select=True, ondelete="cascade"),
473         'child_id': fields.one2many('website.menu', 'parent_id', string='Child Menus'),
474         'parent_left': fields.integer('Parent Left', select=True),
475         'parent_right': fields.integer('Parent Right', select=True),
476     }
477
478     def __defaults_sequence(self, cr, uid, context):
479         menu = self.search_read(cr, uid, [(1,"=",1)], ["sequence"], limit=1, order="sequence DESC", context=context)
480         return menu and menu[0]["sequence"] or 0
481
482     _defaults = {
483         'url': '',
484         'sequence': __defaults_sequence,
485         'new_window': False,
486     }
487     _parent_store = True
488     _parent_order = 'sequence'
489     _order = "sequence"
490
491     # would be better to take a menu_id as argument
492     def get_tree(self, cr, uid, website_id, context=None):
493         def make_tree(node):
494             menu_node = dict(
495                 id=node.id,
496                 name=node.name,
497                 url=node.url,
498                 new_window=node.new_window,
499                 sequence=node.sequence,
500                 parent_id=node.parent_id.id,
501                 children=[],
502             )
503             for child in node.child_id:
504                 menu_node['children'].append(make_tree(child))
505             return menu_node
506         menu = self.pool.get('website').browse(cr, uid, website_id, context=context).menu_id
507         return make_tree(menu)
508
509     def save(self, cr, uid, website_id, data, context=None):
510         def replace_id(old_id, new_id):
511             for menu in data['data']:
512                 if menu['id'] == old_id:
513                     menu['id'] = new_id
514                 if menu['parent_id'] == old_id:
515                     menu['parent_id'] = new_id
516         to_delete = data['to_delete']
517         if to_delete:
518             self.unlink(cr, uid, to_delete, context=context)
519         for menu in data['data']:
520             mid = menu['id']
521             if isinstance(mid, str):
522                 new_id = self.create(cr, uid, {'name': menu['name']}, context=context)
523                 replace_id(mid, new_id)
524         for menu in data['data']:
525             self.write(cr, uid, [menu['id']], menu, context=context)
526         return True
527
528 class ir_attachment(osv.osv):
529     _inherit = "ir.attachment"
530     def _website_url_get(self, cr, uid, ids, name, arg, context=None):
531         result = {}
532         for attach in self.browse(cr, uid, ids, context=context):
533             if attach.url:
534                 result[attach.id] = attach.url
535             else:
536                 result[attach.id] = urlplus('/website/image', {
537                     'model': 'ir.attachment',
538                     'field': 'datas',
539                     'id': attach.id
540                 })
541         return result
542     def _datas_checksum(self, cr, uid, ids, name, arg, context=None):
543         return dict(
544             (attach['id'], self._compute_checksum(attach))
545             for attach in self.read(
546                 cr, uid, ids, ['res_model', 'res_id', 'type', 'datas'],
547                 context=context)
548         )
549
550     def _compute_checksum(self, attachment_dict):
551         if attachment_dict.get('res_model') == 'ir.ui.view'\
552                 and not attachment_dict.get('res_id') and not attachment_dict.get('url')\
553                 and attachment_dict.get('type', 'binary') == 'binary'\
554                 and attachment_dict.get('datas'):
555             return hashlib.new('sha1', attachment_dict['datas']).hexdigest()
556         return None
557
558     def _datas_big(self, cr, uid, ids, name, arg, context=None):
559         result = dict.fromkeys(ids, False)
560         if context and context.get('bin_size'):
561             return result
562
563         for record in self.browse(cr, uid, ids, context=context):
564             if not record.datas: continue
565             try:
566                 result[record.id] = openerp.tools.image_resize_image_big(record.datas)
567             except IOError: # apparently the error PIL.Image.open raises
568                 pass
569
570         return result
571
572     _columns = {
573         'datas_checksum': fields.function(_datas_checksum, size=40,
574               string="Datas checksum", type='char', store=True, select=True),
575         'website_url': fields.function(_website_url_get, string="Attachment URL", type='char'),
576         'datas_big': fields.function (_datas_big, type='binary', store=True,
577                                       string="Resized file content"),
578         'mimetype': fields.char('Mime Type', readonly=True),
579     }
580
581     def _add_mimetype_if_needed(self, values):
582         if values.get('datas_fname'):
583             values['mimetype'] = mimetypes.guess_type(values.get('datas_fname'))[0] or 'application/octet-stream'
584
585     def create(self, cr, uid, values, context=None):
586         chk = self._compute_checksum(values)
587         if chk:
588             match = self.search(cr, uid, [('datas_checksum', '=', chk)], context=context)
589             if match:
590                 return match[0]
591         self._add_mimetype_if_needed(values)
592         return super(ir_attachment, self).create(
593             cr, uid, values, context=context)
594
595     def write(self, cr, uid, ids, values, context=None):
596         self._add_mimetype_if_needed(values)
597         return super(ir_attachment, self).write(cr, uid, ids, values, context=context)
598
599     def try_remove(self, cr, uid, ids, context=None):
600         """ Removes a web-based image attachment if it is used by no view
601         (template)
602
603         Returns a dict mapping attachments which would not be removed (if any)
604         mapped to the views preventing their removal
605         """
606         Views = self.pool['ir.ui.view']
607         attachments_to_remove = []
608         # views blocking removal of the attachment
609         removal_blocked_by = {}
610
611         for attachment in self.browse(cr, uid, ids, context=context):
612             # in-document URLs are html-escaped, a straight search will not
613             # find them
614             url = werkzeug.utils.escape(attachment.website_url)
615             ids = Views.search(cr, uid, ["|", ('arch', 'like', '"%s"' % url), ('arch', 'like', "'%s'" % url)], context=context)
616
617             if ids:
618                 removal_blocked_by[attachment.id] = Views.read(
619                     cr, uid, ids, ['name'], context=context)
620             else:
621                 attachments_to_remove.append(attachment.id)
622         if attachments_to_remove:
623             self.unlink(cr, uid, attachments_to_remove, context=context)
624         return removal_blocked_by
625
626 class res_partner(osv.osv):
627     _inherit = "res.partner"
628
629     def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None):
630         partner = self.browse(cr, uid, ids[0], context=context)
631         params = {
632             'center': '%s, %s %s, %s' % (partner.street or '', partner.city or '', partner.zip or '', partner.country_id and partner.country_id.name_get()[0][1] or ''),
633             'size': "%sx%s" % (height, width),
634             'zoom': zoom,
635             'sensor': 'false',
636         }
637         return urlplus('http://maps.googleapis.com/maps/api/staticmap' , params)
638
639     def google_map_link(self, cr, uid, ids, zoom=8, context=None):
640         partner = self.browse(cr, uid, ids[0], context=context)
641         params = {
642             'q': '%s, %s %s, %s' % (partner.street or '', partner.city  or '', partner.zip or '', partner.country_id and partner.country_id.name_get()[0][1] or ''),
643             'z': 10
644         }
645         return urlplus('https://maps.google.com/maps' , params)
646
647 class res_company(osv.osv):
648     _inherit = "res.company"
649     def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None):
650         partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id
651         return partner and partner.google_map_img(zoom, width, height, context=context) or None
652     def google_map_link(self, cr, uid, ids, zoom=8, context=None):
653         partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id
654         return partner and partner.google_map_link(zoom, context=context) or None
655
656 class base_language_install(osv.osv_memory):
657     _inherit = "base.language.install"
658     _columns = {
659         'website_ids': fields.many2many('website', string='Websites to translate'),
660     }
661
662     def default_get(self, cr, uid, fields, context=None):
663         if context is None:
664             context = {}
665         defaults = super(base_language_install, self).default_get(cr, uid, fields, context)
666         website_id = context.get('params', {}).get('website_id')
667         if website_id:
668             if 'website_ids' not in defaults:
669                 defaults['website_ids'] = []
670             defaults['website_ids'].append(website_id)
671         return defaults
672
673     def lang_install(self, cr, uid, ids, context=None):
674         if context is None:
675             context = {}
676         action = super(base_language_install, self).lang_install(cr, uid, ids, context)
677         language_obj = self.browse(cr, uid, ids)[0]
678         website_ids = [website.id for website in language_obj['website_ids']]
679         lang_id = self.pool['res.lang'].search(cr, uid, [('code', '=', language_obj['lang'])])
680         if website_ids and lang_id:
681             data = {'language_ids': [(4, lang_id[0])]}
682             self.pool['website'].write(cr, uid, website_ids, data)
683         params = context.get('params', {})
684         if 'url_return' in params:
685             return {
686                 'url': params['url_return'].replace('[lang]', language_obj['lang']),
687                 'type': 'ir.actions.act_url',
688                 'target': 'self'
689             }
690         return action
691
692 class website_seo_metadata(osv.Model):
693     _name = 'website.seo.metadata'
694     _description = 'SEO metadata'
695
696     _columns = {
697         'website_meta_title': fields.char("Website meta title", translate=True),
698         'website_meta_description': fields.text("Website meta description", translate=True),
699         'website_meta_keywords': fields.char("Website meta keywords", translate=True),
700     }
701
702 # vim:et: