fcaa184d86ecebe69b4ace3a5544082de617c0be
[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
287         return (
288             'GET' in methods
289             and endpoint.routing['type'] == 'http'
290             and endpoint.routing['auth'] in ('none', 'public')
291             and endpoint.routing.get('website', False)
292             # preclude combinatorial explosion by only allowing a single converter
293             and len(converters) <= 1
294             # ensure all converters on the rule are able to generate values for
295             # themselves
296             and all(hasattr(converter, 'generate') for converter in converters)
297         ) and self.endpoint_is_enumerable(rule)
298
299     def endpoint_is_enumerable(self, rule):
300         """ Verifies that it's possible to generate a valid url for the rule's
301         endpoint
302
303         :type rule: werkzeug.routing.Rule
304         :rtype: bool
305         """
306         spec = inspect.getargspec(rule.endpoint.method)
307
308         # if *args bail the fuck out, only dragons can live there
309         if spec.varargs:
310             return False
311
312         # remove all arguments with a default value from the list
313         defaults_count = len(spec.defaults or []) # spec.defaults can be None
314         # a[:-0] ~ a[:0] ~ [] -> replace defaults_count == 0 by None to get
315         # a[:None] ~ a
316         args = spec.args[:(-defaults_count or None)]
317
318         # params with defaults were removed, leftover allowed are:
319         # * self (technically should be first-parameter-of-instance-method but whatever)
320         # * any parameter mapping to a converter
321         return all(
322             (arg == 'self' or arg in rule._converters)
323             for arg in args)
324
325     def enumerate_pages(self, cr, uid, ids, query_string=None, context=None):
326         """ Available pages in the website/CMS. This is mostly used for links
327         generation and can be overridden by modules setting up new HTML
328         controllers for dynamic pages (e.g. blog).
329
330         By default, returns template views marked as pages.
331
332         :param str query_string: a (user-provided) string, fetches pages
333                                  matching the string
334         :returns: a list of mappings with two keys: ``name`` is the displayable
335                   name of the resource (page), ``url`` is the absolute URL
336                   of the same.
337         :rtype: list({name: str, url: str})
338         """
339         router = request.httprequest.app.get_db_router(request.db)
340         # Force enumeration to be performed as public user
341         uid = request.website.user_id.id
342         url_list = []
343         for rule in router.iter_rules():
344             if not self.rule_is_enumerable(rule):
345                 continue
346
347             converters = rule._converters
348             filtered = bool(converters)
349             if converters:
350                 # allow single converter as decided by fp, checked by
351                 # rule_is_enumerable
352                 [(name, converter)] = converters.items()
353                 converter_values = converter.generate(
354                     request.cr, uid, query=query_string, context=context)
355                 generated = ({k: v} for k, v in itertools.izip(
356                     itertools.repeat(name), converter_values))
357             else:
358                 # force single iteration for literal urls
359                 generated = [{}]
360
361             for values in generated:
362                 domain_part, url = rule.build(values, append_unknown=False)
363                 page = {'name': url, 'url': url}
364                 if url in url_list:
365                     continue
366                 url_list.append(url)
367                 if not filtered and query_string and not self.page_matches(cr, uid, page, query_string, context=context):
368                     continue
369                 yield page
370
371     def search_pages(self, cr, uid, ids, needle=None, limit=None, context=None):
372         return list(itertools.islice(
373             self.enumerate_pages(cr, uid, ids, query_string=needle, context=context),
374             limit))
375
376     def page_matches(self, cr, uid, page, needle, context=None):
377         """ Checks that a "page" matches a user-provide search string.
378
379         The default implementation attempts to perform a non-contiguous
380         substring match of the page's name.
381
382         :param page: {'name': str, 'url': str}
383         :param needle: str
384         :rtype: bool
385         """
386         haystack = page['name'].lower()
387
388         needle = iter(needle.lower())
389         n = next(needle)
390         end = object()
391
392         for char in haystack:
393             if char != n: continue
394
395             n = next(needle, end)
396             # found all characters of needle in haystack in order
397             if n is end:
398                 return True
399
400         return False
401
402     def kanban(self, cr, uid, ids, model, domain, column, template, step=None, scope=None, orderby=None, context=None):
403         step = step and int(step) or 10
404         scope = scope and int(scope) or 5
405         orderby = orderby or "name"
406
407         get_args = dict(request.httprequest.args or {})
408         model_obj = self.pool[model]
409         relation = model_obj._columns.get(column)._obj
410         relation_obj = self.pool[relation]
411
412         get_args.setdefault('kanban', "")
413         kanban = get_args.pop('kanban')
414         kanban_url = "?%s&kanban=" % werkzeug.url_encode(get_args)
415
416         pages = {}
417         for col in kanban.split(","):
418             if col:
419                 col = col.split("-")
420                 pages[int(col[0])] = int(col[1])
421
422         objects = []
423         for group in model_obj.read_group(cr, uid, domain, ["id", column], groupby=column):
424             obj = {}
425
426             # browse column
427             relation_id = group[column][0]
428             obj['column_id'] = relation_obj.browse(cr, uid, relation_id)
429
430             obj['kanban_url'] = kanban_url
431             for k, v in pages.items():
432                 if k != relation_id:
433                     obj['kanban_url'] += "%s-%s" % (k, v)
434
435             # pager
436             number = model_obj.search(cr, uid, group['__domain'], count=True)
437             obj['page_count'] = int(math.ceil(float(number) / step))
438             obj['page'] = pages.get(relation_id) or 1
439             if obj['page'] > obj['page_count']:
440                 obj['page'] = obj['page_count']
441             offset = (obj['page']-1) * step
442             obj['page_start'] = max(obj['page'] - int(math.floor((scope-1)/2)), 1)
443             obj['page_end'] = min(obj['page_start'] + (scope-1), obj['page_count'])
444
445             # view data
446             obj['domain'] = group['__domain']
447             obj['model'] = model
448             obj['step'] = step
449             obj['orderby'] = orderby
450
451             # browse objects
452             object_ids = model_obj.search(cr, uid, group['__domain'], limit=step, offset=offset, order=orderby)
453             obj['object_ids'] = model_obj.browse(cr, uid, object_ids)
454
455             objects.append(obj)
456
457         values = {
458             'objects': objects,
459             'range': range,
460             'template': template,
461         }
462         return request.website._render("website.kanban_contain", values)
463
464     def kanban_col(self, cr, uid, ids, model, domain, page, template, step, orderby, context=None):
465         html = ""
466         model_obj = self.pool[model]
467         domain = safe_eval(domain)
468         step = int(step)
469         offset = (int(page)-1) * step
470         object_ids = model_obj.search(cr, uid, domain, limit=step, offset=offset, order=orderby)
471         object_ids = model_obj.browse(cr, uid, object_ids)
472         for object_id in object_ids:
473             html += request.website._render(template, {'object_id': object_id})
474         return html
475
476 class website_menu(osv.osv):
477     _name = "website.menu"
478     _description = "Website Menu"
479     _columns = {
480         'name': fields.char('Menu', size=64, required=True, translate=True),
481         'url': fields.char('Url', translate=True),
482         'new_window': fields.boolean('New Window'),
483         'sequence': fields.integer('Sequence'),
484         # TODO: support multiwebsite once done for ir.ui.views
485         'website_id': fields.many2one('website', 'Website'),
486         'parent_id': fields.many2one('website.menu', 'Parent Menu', select=True, ondelete="cascade"),
487         'child_id': fields.one2many('website.menu', 'parent_id', string='Child Menus'),
488         'parent_left': fields.integer('Parent Left', select=True),
489         'parent_right': fields.integer('Parent Right', select=True),
490     }
491
492     def __defaults_sequence(self, cr, uid, context):
493         menu = self.search_read(cr, uid, [(1,"=",1)], ["sequence"], limit=1, order="sequence DESC", context=context)
494         return menu and menu[0]["sequence"] or 0
495
496     _defaults = {
497         'url': '',
498         'sequence': __defaults_sequence,
499         'new_window': False,
500     }
501     _parent_store = True
502     _parent_order = 'sequence'
503     _order = "sequence"
504
505     # would be better to take a menu_id as argument
506     def get_tree(self, cr, uid, website_id, context=None):
507         def make_tree(node):
508             menu_node = dict(
509                 id=node.id,
510                 name=node.name,
511                 url=node.url,
512                 new_window=node.new_window,
513                 sequence=node.sequence,
514                 parent_id=node.parent_id.id,
515                 children=[],
516             )
517             for child in node.child_id:
518                 menu_node['children'].append(make_tree(child))
519             return menu_node
520         menu = self.pool.get('website').browse(cr, uid, website_id, context=context).menu_id
521         return make_tree(menu)
522
523     def save(self, cr, uid, website_id, data, context=None):
524         def replace_id(old_id, new_id):
525             for menu in data['data']:
526                 if menu['id'] == old_id:
527                     menu['id'] = new_id
528                 if menu['parent_id'] == old_id:
529                     menu['parent_id'] = new_id
530         to_delete = data['to_delete']
531         if to_delete:
532             self.unlink(cr, uid, to_delete, context=context)
533         for menu in data['data']:
534             mid = menu['id']
535             if isinstance(mid, str):
536                 new_id = self.create(cr, uid, {'name': menu['name']}, context=context)
537                 replace_id(mid, new_id)
538         for menu in data['data']:
539             self.write(cr, uid, [menu['id']], menu, context=context)
540         return True
541
542 class ir_attachment(osv.osv):
543     _inherit = "ir.attachment"
544     def _website_url_get(self, cr, uid, ids, name, arg, context=None):
545         result = {}
546         for attach in self.browse(cr, uid, ids, context=context):
547             if attach.url:
548                 result[attach.id] = attach.url
549             else:
550                 result[attach.id] = urlplus('/website/image', {
551                     'model': 'ir.attachment',
552                     'field': 'datas',
553                     'id': attach.id
554                 })
555         return result
556     def _datas_checksum(self, cr, uid, ids, name, arg, context=None):
557         return dict(
558             (attach['id'], self._compute_checksum(attach))
559             for attach in self.read(
560                 cr, uid, ids, ['res_model', 'res_id', 'type', 'datas'],
561                 context=context)
562         )
563
564     def _compute_checksum(self, attachment_dict):
565         if attachment_dict.get('res_model') == 'ir.ui.view'\
566                 and not attachment_dict.get('res_id') and not attachment_dict.get('url')\
567                 and attachment_dict.get('type', 'binary') == 'binary'\
568                 and attachment_dict.get('datas'):
569             return hashlib.new('sha1', attachment_dict['datas']).hexdigest()
570         return None
571
572     def _datas_big(self, cr, uid, ids, name, arg, context=None):
573         result = dict.fromkeys(ids, False)
574         if context and context.get('bin_size'):
575             return result
576
577         for record in self.browse(cr, uid, ids, context=context):
578             if not record.datas: continue
579             try:
580                 result[record.id] = openerp.tools.image_resize_image_big(record.datas)
581             except IOError: # apparently the error PIL.Image.open raises
582                 pass
583
584         return result
585
586     _columns = {
587         'datas_checksum': fields.function(_datas_checksum, size=40,
588               string="Datas checksum", type='char', store=True, select=True),
589         'website_url': fields.function(_website_url_get, string="Attachment URL", type='char'),
590         'datas_big': fields.function (_datas_big, type='binary', store=True,
591                                       string="Resized file content"),
592         'mimetype': fields.char('Mime Type', readonly=True),
593     }
594
595     def _add_mimetype_if_needed(self, values):
596         if values.get('datas_fname'):
597             values['mimetype'] = mimetypes.guess_type(values.get('datas_fname'))[0] or 'application/octet-stream'
598
599     def create(self, cr, uid, values, context=None):
600         chk = self._compute_checksum(values)
601         if chk:
602             match = self.search(cr, uid, [('datas_checksum', '=', chk)], context=context)
603             if match:
604                 return match[0]
605         self._add_mimetype_if_needed(values)
606         return super(ir_attachment, self).create(
607             cr, uid, values, context=context)
608
609     def write(self, cr, uid, ids, values, context=None):
610         self._add_mimetype_if_needed(values)
611         return super(ir_attachment, self).write(cr, uid, ids, values, context=context)
612
613     def try_remove(self, cr, uid, ids, context=None):
614         """ Removes a web-based image attachment if it is used by no view
615         (template)
616
617         Returns a dict mapping attachments which would not be removed (if any)
618         mapped to the views preventing their removal
619         """
620         Views = self.pool['ir.ui.view']
621         attachments_to_remove = []
622         # views blocking removal of the attachment
623         removal_blocked_by = {}
624
625         for attachment in self.browse(cr, uid, ids, context=context):
626             # in-document URLs are html-escaped, a straight search will not
627             # find them
628             url = werkzeug.utils.escape(attachment.website_url)
629             ids = Views.search(cr, uid, ["|", ('arch', 'like', '"%s"' % url), ('arch', 'like', "'%s'" % url)], context=context)
630
631             if ids:
632                 removal_blocked_by[attachment.id] = Views.read(
633                     cr, uid, ids, ['name'], context=context)
634             else:
635                 attachments_to_remove.append(attachment.id)
636         if attachments_to_remove:
637             self.unlink(cr, uid, attachments_to_remove, context=context)
638         return removal_blocked_by
639
640 class res_partner(osv.osv):
641     _inherit = "res.partner"
642
643     def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None):
644         partner = self.browse(cr, uid, ids[0], context=context)
645         params = {
646             '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 ''),
647             'size': "%sx%s" % (height, width),
648             'zoom': zoom,
649             'sensor': 'false',
650         }
651         return urlplus('http://maps.googleapis.com/maps/api/staticmap' , params)
652
653     def google_map_link(self, cr, uid, ids, zoom=8, context=None):
654         partner = self.browse(cr, uid, ids[0], context=context)
655         params = {
656             '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 ''),
657             'z': 10
658         }
659         return urlplus('https://maps.google.com/maps' , params)
660
661 class res_company(osv.osv):
662     _inherit = "res.company"
663     def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None):
664         partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id
665         return partner and partner.google_map_img(zoom, width, height, context=context) or None
666     def google_map_link(self, cr, uid, ids, zoom=8, context=None):
667         partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id
668         return partner and partner.google_map_link(zoom, context=context) or None
669
670 class base_language_install(osv.osv_memory):
671     _inherit = "base.language.install"
672     _columns = {
673         'website_ids': fields.many2many('website', string='Websites to translate'),
674     }
675
676     def default_get(self, cr, uid, fields, context=None):
677         if context is None:
678             context = {}
679         defaults = super(base_language_install, self).default_get(cr, uid, fields, context)
680         website_id = context.get('params', {}).get('website_id')
681         if website_id:
682             if 'website_ids' not in defaults:
683                 defaults['website_ids'] = []
684             defaults['website_ids'].append(website_id)
685         return defaults
686
687     def lang_install(self, cr, uid, ids, context=None):
688         if context is None:
689             context = {}
690         action = super(base_language_install, self).lang_install(cr, uid, ids, context)
691         language_obj = self.browse(cr, uid, ids)[0]
692         website_ids = [website.id for website in language_obj['website_ids']]
693         lang_id = self.pool['res.lang'].search(cr, uid, [('code', '=', language_obj['lang'])])
694         if website_ids and lang_id:
695             data = {'language_ids': [(4, lang_id[0])]}
696             self.pool['website'].write(cr, uid, website_ids, data)
697         params = context.get('params', {})
698         if 'url_return' in params:
699             return {
700                 'url': params['url_return'].replace('[lang]', language_obj['lang']),
701                 'type': 'ir.actions.act_url',
702                 'target': 'self'
703             }
704         return action
705
706 class website_seo_metadata(osv.Model):
707     _name = 'website.seo.metadata'
708     _description = 'SEO metadata'
709
710     _columns = {
711         'website_meta_title': fields.char("Website meta title", translate=True),
712         'website_meta_description': fields.text("Website meta description", translate=True),
713         'website_meta_keywords': fields.char("Website meta keywords", translate=True),
714     }
715
716 # vim:et: