1 # -*- coding: utf-8 -*-
17 from sys import maxint
20 # optional python-slugify import (https://github.com/un33k/python-slugify)
22 import slugify as slugify_lib
27 from openerp.osv import orm, osv, fields
28 from openerp.tools import html_escape as escape
29 from openerp.tools import ustr as ustr
30 from openerp.tools.safe_eval import safe_eval
31 from openerp.addons.web.http import request
32 from werkzeug.exceptions import NotFound
34 logger = logging.getLogger(__name__)
36 def url_for(path_or_uri, lang=None):
37 if isinstance(path_or_uri, unicode):
38 path_or_uri = path_or_uri.encode('utf-8')
39 current_path = request.httprequest.path
40 if isinstance(current_path, unicode):
41 current_path = current_path.encode('utf-8')
42 location = path_or_uri.strip()
43 force_lang = lang is not None
44 url = urlparse.urlparse(location)
46 if request and not url.netloc and not url.scheme and (url.path or force_lang):
47 location = urlparse.urljoin(current_path, location)
49 lang = lang or request.context.get('lang')
50 langs = [lg[0] for lg in request.website.get_languages()]
52 if (len(langs) > 1 or force_lang) and is_multilang_url(location, langs):
53 ps = location.split('/')
55 # Replace the language only if we explicitly provide a language to url_for
58 # Remove the default language unless it's explicitly provided
59 elif ps[1] == request.website.default_lang_code:
61 # Insert the context language or the provided language
62 elif lang != request.website.default_lang_code or force_lang:
64 location = '/'.join(ps)
66 return location.decode('utf-8')
68 def is_multilang_url(local_url, langs=None):
70 langs = [lg[0] for lg in request.website.get_languages()]
71 spath = local_url.split('/')
72 # if a language is already in the path, remove it
75 local_url = '/'.join(spath)
77 # Try to match an endpoint in werkzeug's routing table
78 url = local_url.split('?')
80 query_string = url[1] if len(url) > 1 else None
81 router = request.httprequest.app.get_db_router(request.db).bind('')
82 func = router.match(path, query_args=query_string)[0]
83 return func.routing.get('website', False) and func.routing.get('multilang', True)
87 def slugify(s, max_length=None):
88 """ Transform a string to a slug that can be used in a url path.
90 This method will first try to do the job with python-slugify if present.
91 Otherwise it will process string by stripping leading and ending spaces,
92 converting unicode chars to ascii, lowering all chars and replacing spaces
93 and underscore with hyphen "-".
96 :param max_length: int
101 # There are 2 different libraries only python-slugify is supported
103 return slugify_lib.slugify(s, max_length=max_length)
106 uni = unicodedata.normalize('NFKD', s).encode('ascii', 'ignore').decode('ascii')
107 slug = re.sub('[\W_]', ' ', uni).strip().lower()
108 slug = re.sub('[-\s]+', '-', slug)
110 return slug[:max_length]
113 if isinstance(value, orm.browse_record):
114 # [(id, name)] = value.name_get()
115 id, name = value.id, value.display_name
117 # assume name_search result tuple
119 slugname = slugify(name or '').strip().strip('-')
122 return "%s-%d" % (slugname, id)
125 # NOTE: as the pattern is used as it for the ModelConverter (ir_http.py), do not use any flags
126 _UNSLUG_RE = re.compile(r'(?:(\w{1,2}|\w[A-Za-z0-9-_]+?\w)-)?(-?\d+)(?=$|/)')
129 """Extract slug and id from a string.
130 Always return un 2-tuple (str|None, int|None)
132 m = _UNSLUG_RE.match(s)
135 return m.group(1), int(m.group(2))
137 def urlplus(url, params):
138 return werkzeug.Href(url)(params or None)
140 class website(osv.osv):
141 def _get_menu(self, cr, uid, ids, name, arg, context=None):
143 menu_obj = self.pool.get('website.menu')
145 menu_ids = menu_obj.search(cr, uid, [('parent_id', '=', False), ('website_id', '=', id)], order='id', context=context)
146 res[id] = menu_ids and menu_ids[0] or False
149 _name = "website" # Avoid website.website convention for conciseness (for new api). Got a special authorization from xmo and rco
150 _description = "Website"
152 'name': fields.char('Domain'),
153 'company_id': fields.many2one('res.company', string="Company"),
154 'language_ids': fields.many2many('res.lang', 'website_lang_rel', 'website_id', 'lang_id', 'Languages'),
155 'default_lang_id': fields.many2one('res.lang', string="Default language"),
156 'default_lang_code': fields.related('default_lang_id', 'code', type="char", string="Default language code", store=True),
157 'social_twitter': fields.char('Twitter Account'),
158 'social_facebook': fields.char('Facebook Account'),
159 'social_github': fields.char('GitHub Account'),
160 'social_linkedin': fields.char('LinkedIn Account'),
161 'social_youtube': fields.char('Youtube Account'),
162 'social_googleplus': fields.char('Google+ Account'),
163 'google_analytics_key': fields.char('Google Analytics Key'),
164 'user_id': fields.many2one('res.users', string='Public User'),
165 'compress_html': fields.boolean('Compress HTML'),
166 'partner_id': fields.related('user_id','partner_id', type='many2one', relation='res.partner', string='Public Partner'),
167 'menu_id': fields.function(_get_menu, relation='website.menu', type='many2one', string='Main Menu')
170 'user_id': lambda self,cr,uid,c: self.pool['ir.model.data'].xmlid_to_res_id(cr, openerp.SUPERUSER_ID, 'base.public_user'),
171 'company_id': lambda self,cr,uid,c: self.pool['ir.model.data'].xmlid_to_res_id(cr, openerp.SUPERUSER_ID,'base.main_company'),
172 'compress_html': False,
175 # cf. Wizard hack in website_views.xml
176 def noop(self, *args, **kwargs):
179 def write(self, cr, uid, ids, vals, context=None):
180 self._get_languages.clear_cache(self)
181 return super(website, self).write(cr, uid, ids, vals, context)
183 def new_page(self, cr, uid, name, template='website.default_page', ispage=True, context=None):
184 context = context or {}
185 imd = self.pool.get('ir.model.data')
186 view = self.pool.get('ir.ui.view')
187 template_module, template_name = template.split('.')
189 # completely arbitrary max_length
190 page_name = slugify(name, max_length=50)
191 page_xmlid = "%s.%s" % (template_module, page_name)
195 imd.get_object_reference(cr, uid, template_module, page_name)
198 _, template_id = imd.get_object_reference(cr, uid, template_module, template_name)
199 website_id = context.get('website_id')
200 key = template_module+'.'+page_name
201 page_id = view.copy(cr, uid, template_id, {'website_id': website_id, 'key': key}, context=context)
202 page = view.browse(cr, uid, page_id, context=context)
204 'arch': page.arch.replace(template, page_xmlid),
210 def page_for_name(self, cr, uid, ids, name, module='website', context=None):
212 return '%s.%s' % (module, slugify(name, max_length=50))
214 def page_exists(self, cr, uid, ids, name, module='website', context=None):
216 name = (name or "").replace("/page/website.", "").replace("/page/", "")
219 return self.pool["ir.model.data"].get_object_reference(cr, uid, module, name)
223 @openerp.tools.ormcache(skiparg=3)
224 def _get_languages(self, cr, uid, id, context=None):
225 website = self.browse(cr, uid, id)
226 return [(lg.code, lg.name) for lg in website.language_ids]
228 def get_languages(self, cr, uid, ids, context=None):
229 return self._get_languages(cr, uid, ids[0], context=context)
231 def get_alternate_languages(self, cr, uid, ids, req=None, context=None):
234 req = request.httprequest
235 default = self.get_current_website(cr, uid, context=context).default_lang_code
238 uri += '?' + req.query_string
240 for code, name in self.get_languages(cr, uid, ids, context=context):
241 lg_path = ('/' + code) if code != default else ''
245 'hreflang': ('-'.join(lg)).lower(),
247 'href': req.url_root[0:-1] + lg_path + uri,
251 if shorts.count(lang['short']) == 1:
252 lang['hreflang'] = lang['short']
255 @openerp.tools.ormcache(skiparg=4)
256 def _get_current_website_id(self, cr, uid, domain_name, context=None):
259 ids = self.search(cr, uid, [('name', '=', domain_name)], context=context)
264 def get_current_website(self, cr, uid, context=None):
265 domain_name = request.httprequest.environ.get('HTTP_HOST', '').split(':')[0]
266 website_id = self._get_current_website_id(cr, uid, domain_name, context=context)
267 return self.browse(cr, uid, website_id, context=context)
269 def is_publisher(self, cr, uid, ids, context=None):
270 Access = self.pool['ir.model.access']
271 is_website_publisher = Access.check(cr, uid, 'ir.ui.view', 'write', False, context=context)
272 return is_website_publisher
274 def is_user(self, cr, uid, ids, context=None):
275 Access = self.pool['ir.model.access']
276 return Access.check(cr, uid, 'ir.ui.menu', 'read', False, context=context)
278 def get_template(self, cr, uid, ids, template, context=None):
279 if not isinstance(template, (int, long)) and '.' not in template:
280 template = 'website.%s' % template
281 View = self.pool['ir.ui.view']
282 view_id = View.get_view_id(cr, uid, template, context=context)
285 return View.browse(cr, uid, view_id, context=context)
287 def _render(self, cr, uid, ids, template, values=None, context=None):
288 # TODO: remove this. (just kept for backward api compatibility for saas-3)
289 return self.pool['ir.ui.view'].render(cr, uid, template, values=values, context=context)
291 def render(self, cr, uid, ids, template, values=None, status_code=None, context=None):
292 # TODO: remove this. (just kept for backward api compatibility for saas-3)
293 return request.render(template, values, uid=uid)
295 def pager(self, cr, uid, ids, url, total, page=1, step=30, scope=5, url_args=None, context=None):
297 page_count = int(math.ceil(float(total) / step))
299 page = max(1, min(int(page if str(page).isdigit() else 1), page_count))
302 pmin = max(page - int(math.floor(scope/2)), 1)
303 pmax = min(pmin + scope, page_count)
305 if pmax - pmin < scope:
306 pmin = pmax - scope if pmax - scope > 0 else 1
309 _url = "%s/page/%s" % (url, page) if page > 1 else url
311 _url = "%s?%s" % (_url, werkzeug.url_encode(url_args))
315 "page_count": page_count,
316 "offset": (page - 1) * step,
318 'url': get_url(page),
322 'url': get_url(pmin),
326 'url': get_url(max(pmin, page - 1)),
327 'num': max(pmin, page - 1)
330 'url': get_url(min(pmax, page + 1)),
331 'num': min(pmax, page + 1)
334 'url': get_url(pmax),
338 {'url': get_url(page), 'num': page}
339 for page in xrange(pmin, pmax+1)
343 def rule_is_enumerable(self, rule):
344 """ Checks that it is possible to generate sensible GET queries for
345 a given rule (if the endpoint matches its own requirements)
347 :type rule: werkzeug.routing.Rule
350 endpoint = rule.endpoint
351 methods = rule.methods or ['GET']
352 converters = rule._converters.values()
353 if not ('GET' in methods
354 and endpoint.routing['type'] == 'http'
355 and endpoint.routing['auth'] in ('none', 'public')
356 and endpoint.routing.get('website', False)
357 and all(hasattr(converter, 'generate') for converter in converters)
358 and endpoint.routing.get('website')):
361 # dont't list routes without argument having no default value or converter
362 spec = inspect.getargspec(endpoint.method.original_func)
364 # remove self and arguments having a default value
365 defaults_count = len(spec.defaults or [])
366 args = spec.args[1:(-defaults_count or None)]
368 # check that all args have a converter
369 return all( (arg in rule._converters) for arg in args)
371 def enumerate_pages(self, cr, uid, ids, query_string=None, context=None):
372 """ Available pages in the website/CMS. This is mostly used for links
373 generation and can be overridden by modules setting up new HTML
374 controllers for dynamic pages (e.g. blog).
376 By default, returns template views marked as pages.
378 :param str query_string: a (user-provided) string, fetches pages
380 :returns: a list of mappings with two keys: ``name`` is the displayable
381 name of the resource (page), ``url`` is the absolute URL
383 :rtype: list({name: str, url: str})
385 router = request.httprequest.app.get_db_router(request.db)
386 # Force enumeration to be performed as public user
388 for rule in router.iter_rules():
389 if not self.rule_is_enumerable(rule):
392 converters = rule._converters or {}
393 if query_string and not converters and (query_string not in rule.build([{}], append_unknown=False)[1]):
396 convitems = converters.items()
397 # converters with a domain are processed after the other ones
398 gd = lambda x: hasattr(x[1], 'domain') and (x[1].domain <> '[]')
399 convitems.sort(lambda x, y: cmp(gd(x), gd(y)))
400 for (i,(name, converter)) in enumerate(convitems):
403 query = i==(len(convitems)-1) and query_string
404 for v in converter.generate(request.cr, uid, query=query, args=val, context=context):
405 newval.append( val.copy() )
412 domain_part, url = rule.build(value, append_unknown=False)
414 for key,val in value.items():
415 if key.startswith('__'):
417 if url in ('/sitemap.xml',):
425 def search_pages(self, cr, uid, ids, needle=None, limit=None, context=None):
426 name = (needle or "").replace("/page/website.", "").replace("/page/", "")
428 for page in self.enumerate_pages(cr, uid, ids, query_string=name, context=context):
429 if needle in page['loc']:
431 if len(res) == limit:
435 def kanban(self, cr, uid, ids, model, domain, column, template, step=None, scope=None, orderby=None, context=None):
436 step = step and int(step) or 10
437 scope = scope and int(scope) or 5
438 orderby = orderby or "name"
440 get_args = dict(request.httprequest.args or {})
441 model_obj = self.pool[model]
442 relation = model_obj._columns.get(column)._obj
443 relation_obj = self.pool[relation]
445 get_args.setdefault('kanban', "")
446 kanban = get_args.pop('kanban')
447 kanban_url = "?%s&kanban=" % werkzeug.url_encode(get_args)
450 for col in kanban.split(","):
453 pages[int(col[0])] = int(col[1])
456 for group in model_obj.read_group(cr, uid, domain, ["id", column], groupby=column):
460 relation_id = group[column][0]
461 obj['column_id'] = relation_obj.browse(cr, uid, relation_id)
463 obj['kanban_url'] = kanban_url
464 for k, v in pages.items():
466 obj['kanban_url'] += "%s-%s" % (k, v)
469 number = model_obj.search(cr, uid, group['__domain'], count=True)
470 obj['page_count'] = int(math.ceil(float(number) / step))
471 obj['page'] = pages.get(relation_id) or 1
472 if obj['page'] > obj['page_count']:
473 obj['page'] = obj['page_count']
474 offset = (obj['page']-1) * step
475 obj['page_start'] = max(obj['page'] - int(math.floor((scope-1)/2)), 1)
476 obj['page_end'] = min(obj['page_start'] + (scope-1), obj['page_count'])
479 obj['domain'] = group['__domain']
482 obj['orderby'] = orderby
485 object_ids = model_obj.search(cr, uid, group['__domain'], limit=step, offset=offset, order=orderby)
486 obj['object_ids'] = model_obj.browse(cr, uid, object_ids)
493 'template': template,
495 return request.website._render("website.kanban_contain", values)
497 def kanban_col(self, cr, uid, ids, model, domain, page, template, step, orderby, context=None):
499 model_obj = self.pool[model]
500 domain = safe_eval(domain)
502 offset = (int(page)-1) * step
503 object_ids = model_obj.search(cr, uid, domain, limit=step, offset=offset, order=orderby)
504 object_ids = model_obj.browse(cr, uid, object_ids)
505 for object_id in object_ids:
506 html += request.website._render(template, {'object_id': object_id})
509 def _image_placeholder(self, response):
510 # file_open may return a StringIO. StringIO can be closed but are
511 # not context managers in Python 2 though that is fixed in 3
512 with contextlib.closing(openerp.tools.misc.file_open(
513 os.path.join('web', 'static', 'src', 'img', 'placeholder.png'),
515 response.data = f.read()
516 return response.make_conditional(request.httprequest)
518 def _image(self, cr, uid, model, id, field, response, max_width=maxint, max_height=maxint, context=None):
519 """ Fetches the requested field and ensures it does not go above
520 (max_width, max_height), resizing it if necessary.
522 Resizing is bypassed if the object provides a $field_big, which will
523 be interpreted as a pre-resized version of the base field.
525 If the record is not found or does not have the requested field,
526 returns a placeholder image via :meth:`~._image_placeholder`.
528 Sets and checks conditional response parameters:
529 * :mailheader:`ETag` is always set (and checked)
530 * :mailheader:`Last-Modified is set iif the record has a concurrency
531 field (``__last_update``)
533 The requested field is assumed to be base64-encoded image data in
536 Model = self.pool[model]
539 ids = Model.search(cr, uid,
540 [('id', '=', id)], context=context)
541 if not ids and 'website_published' in Model._all_columns:
542 ids = Model.search(cr, openerp.SUPERUSER_ID,
543 [('id', '=', id), ('website_published', '=', True)], context=context)
545 return self._image_placeholder(response)
547 concurrency = '__last_update'
548 [record] = Model.read(cr, openerp.SUPERUSER_ID, [id],
549 [concurrency, field],
552 if concurrency in record:
553 server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
555 response.last_modified = datetime.datetime.strptime(
556 record[concurrency], server_format + '.%f')
558 # just in case we have a timestamp without microseconds
559 response.last_modified = datetime.datetime.strptime(
560 record[concurrency], server_format)
562 # Field does not exist on model or field set to False
563 if not record.get(field):
564 # FIXME: maybe a field which does not exist should be a 404?
565 return self._image_placeholder(response)
567 response.set_etag(hashlib.sha1(record[field]).hexdigest())
568 response.make_conditional(request.httprequest)
570 # conditional request match
571 if response.status_code == 304:
574 data = record[field].decode('base64')
576 if (not max_width) and (not max_height):
580 image = Image.open(cStringIO.StringIO(data))
581 response.mimetype = Image.MIME[image.format]
584 max_w = int(max_width) if max_width else maxint
585 max_h = int(max_height) if max_height else maxint
587 if w < max_w and h < max_h:
590 image.thumbnail((max_w, max_h), Image.ANTIALIAS)
591 image.save(response.stream, image.format)
592 # invalidate content-length computed by make_conditional as
593 # writing to response.stream does not do it (as of werkzeug 0.9.3)
594 del response.headers['Content-Length']
599 class website_menu(osv.osv):
600 _name = "website.menu"
601 _description = "Website Menu"
603 'name': fields.char('Menu', required=True, translate=True),
604 'url': fields.char('Url'),
605 'new_window': fields.boolean('New Window'),
606 'sequence': fields.integer('Sequence'),
607 # TODO: support multiwebsite once done for ir.ui.views
608 'website_id': fields.many2one('website', 'Website'),
609 'parent_id': fields.many2one('website.menu', 'Parent Menu', select=True, ondelete="cascade"),
610 'child_id': fields.one2many('website.menu', 'parent_id', string='Child Menus'),
611 'parent_left': fields.integer('Parent Left', select=True),
612 'parent_right': fields.integer('Parent Right', select=True),
615 def __defaults_sequence(self, cr, uid, context):
616 menu = self.search_read(cr, uid, [(1,"=",1)], ["sequence"], limit=1, order="sequence DESC", context=context)
617 return menu and menu[0]["sequence"] or 0
621 'sequence': __defaults_sequence,
625 _parent_order = 'sequence'
628 # would be better to take a menu_id as argument
629 def get_tree(self, cr, uid, website_id, context=None):
635 new_window=node.new_window,
636 sequence=node.sequence,
637 parent_id=node.parent_id.id,
640 for child in node.child_id:
641 menu_node['children'].append(make_tree(child))
643 menu = self.pool.get('website').browse(cr, uid, website_id, context=context).menu_id
644 return make_tree(menu)
646 def save(self, cr, uid, website_id, data, context=None):
647 def replace_id(old_id, new_id):
648 for menu in data['data']:
649 if menu['id'] == old_id:
651 if menu['parent_id'] == old_id:
652 menu['parent_id'] = new_id
653 to_delete = data['to_delete']
655 self.unlink(cr, uid, to_delete, context=context)
656 for menu in data['data']:
658 if isinstance(mid, str):
659 new_id = self.create(cr, uid, {'name': menu['name']}, context=context)
660 replace_id(mid, new_id)
661 for menu in data['data']:
662 self.write(cr, uid, [menu['id']], menu, context=context)
665 class ir_attachment(osv.osv):
666 _inherit = "ir.attachment"
667 def _website_url_get(self, cr, uid, ids, name, arg, context=None):
669 for attach in self.browse(cr, uid, ids, context=context):
671 result[attach.id] = attach.url
673 result[attach.id] = urlplus('/website/image', {
674 'model': 'ir.attachment',
679 def _datas_checksum(self, cr, uid, ids, name, arg, context=None):
681 (attach['id'], self._compute_checksum(attach))
682 for attach in self.read(
683 cr, uid, ids, ['res_model', 'res_id', 'type', 'datas'],
687 def _compute_checksum(self, attachment_dict):
688 if attachment_dict.get('res_model') == 'ir.ui.view'\
689 and not attachment_dict.get('res_id') and not attachment_dict.get('url')\
690 and attachment_dict.get('type', 'binary') == 'binary'\
691 and attachment_dict.get('datas'):
692 return hashlib.new('sha1', attachment_dict['datas']).hexdigest()
695 def _datas_big(self, cr, uid, ids, name, arg, context=None):
696 result = dict.fromkeys(ids, False)
697 if context and context.get('bin_size'):
700 for record in self.browse(cr, uid, ids, context=context):
701 if not record.datas: continue
703 result[record.id] = openerp.tools.image_resize_image_big(record.datas)
704 except IOError: # apparently the error PIL.Image.open raises
710 'datas_checksum': fields.function(_datas_checksum, size=40,
711 string="Datas checksum", type='char', store=True, select=True),
712 'website_url': fields.function(_website_url_get, string="Attachment URL", type='char'),
713 'datas_big': fields.function (_datas_big, type='binary', store=True,
714 string="Resized file content"),
715 'mimetype': fields.char('Mime Type', readonly=True),
718 def _add_mimetype_if_needed(self, values):
719 if values.get('datas_fname'):
720 values['mimetype'] = mimetypes.guess_type(values.get('datas_fname'))[0] or 'application/octet-stream'
722 def create(self, cr, uid, values, context=None):
723 chk = self._compute_checksum(values)
725 match = self.search(cr, uid, [('datas_checksum', '=', chk)], context=context)
728 self._add_mimetype_if_needed(values)
729 return super(ir_attachment, self).create(
730 cr, uid, values, context=context)
732 def write(self, cr, uid, ids, values, context=None):
733 self._add_mimetype_if_needed(values)
734 return super(ir_attachment, self).write(cr, uid, ids, values, context=context)
736 def try_remove(self, cr, uid, ids, context=None):
737 """ Removes a web-based image attachment if it is used by no view
740 Returns a dict mapping attachments which would not be removed (if any)
741 mapped to the views preventing their removal
743 Views = self.pool['ir.ui.view']
744 attachments_to_remove = []
745 # views blocking removal of the attachment
746 removal_blocked_by = {}
748 for attachment in self.browse(cr, uid, ids, context=context):
749 # in-document URLs are html-escaped, a straight search will not
751 url = escape(attachment.website_url)
752 ids = Views.search(cr, uid, ["|", ('arch', 'like', '"%s"' % url), ('arch', 'like', "'%s'" % url)], context=context)
755 removal_blocked_by[attachment.id] = Views.read(
756 cr, uid, ids, ['name'], context=context)
758 attachments_to_remove.append(attachment.id)
759 if attachments_to_remove:
760 self.unlink(cr, uid, attachments_to_remove, context=context)
761 return removal_blocked_by
763 class res_partner(osv.osv):
764 _inherit = "res.partner"
766 def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None):
767 partner = self.browse(cr, uid, ids[0], context=context)
769 '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 ''),
770 'size': "%sx%s" % (height, width),
774 return urlplus('http://maps.googleapis.com/maps/api/staticmap' , params)
776 def google_map_link(self, cr, uid, ids, zoom=8, context=None):
777 partner = self.browse(cr, uid, ids[0], context=context)
779 '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 ''),
782 return urlplus('https://maps.google.com/maps' , params)
784 class res_company(osv.osv):
785 _inherit = "res.company"
786 def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None):
787 partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id
788 return partner and partner.google_map_img(zoom, width, height, context=context) or None
789 def google_map_link(self, cr, uid, ids, zoom=8, context=None):
790 partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id
791 return partner and partner.google_map_link(zoom, context=context) or None
793 class base_language_install(osv.osv_memory):
794 _inherit = "base.language.install"
796 'website_ids': fields.many2many('website', string='Websites to translate'),
799 def default_get(self, cr, uid, fields, context=None):
802 defaults = super(base_language_install, self).default_get(cr, uid, fields, context)
803 website_id = context.get('params', {}).get('website_id')
805 if 'website_ids' not in defaults:
806 defaults['website_ids'] = []
807 defaults['website_ids'].append(website_id)
810 def lang_install(self, cr, uid, ids, context=None):
813 action = super(base_language_install, self).lang_install(cr, uid, ids, context)
814 language_obj = self.browse(cr, uid, ids)[0]
815 website_ids = [website.id for website in language_obj['website_ids']]
816 lang_id = self.pool['res.lang'].search(cr, uid, [('code', '=', language_obj['lang'])])
817 if website_ids and lang_id:
818 data = {'language_ids': [(4, lang_id[0])]}
819 self.pool['website'].write(cr, uid, website_ids, data)
820 params = context.get('params', {})
821 if 'url_return' in params:
823 'url': params['url_return'].replace('[lang]', language_obj['lang']),
824 'type': 'ir.actions.act_url',
829 class website_seo_metadata(osv.Model):
830 _name = 'website.seo.metadata'
831 _description = 'SEO metadata'
834 'website_meta_title': fields.char("Website meta title", translate=True),
835 'website_meta_description': fields.text("Website meta description", translate=True),
836 'website_meta_keywords': fields.char("Website meta keywords", translate=True),