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