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, ustr, image_resize_and_sharpen, image_save_for_web
29 from openerp.tools.safe_eval import safe_eval
30 from openerp.addons.web.http import request
32 logger = logging.getLogger(__name__)
34 def url_for(path_or_uri, lang=None):
35 if isinstance(path_or_uri, unicode):
36 path_or_uri = path_or_uri.encode('utf-8')
37 current_path = request.httprequest.path
38 if isinstance(current_path, unicode):
39 current_path = current_path.encode('utf-8')
40 location = path_or_uri.strip()
41 force_lang = lang is not None
42 url = urlparse.urlparse(location)
44 if request and not url.netloc and not url.scheme and (url.path or force_lang):
45 location = urlparse.urljoin(current_path, location)
47 lang = lang or request.context.get('lang')
48 langs = [lg[0] for lg in request.website.get_languages()]
50 if (len(langs) > 1 or force_lang) and is_multilang_url(location, langs):
51 ps = location.split('/')
53 # Replace the language only if we explicitly provide a language to url_for
56 # Remove the default language unless it's explicitly provided
57 elif ps[1] == request.website.default_lang_code:
59 # Insert the context language or the provided language
60 elif lang != request.website.default_lang_code or force_lang:
62 location = '/'.join(ps)
64 return location.decode('utf-8')
66 def is_multilang_url(local_url, langs=None):
68 langs = [lg[0] for lg in request.website.get_languages()]
69 spath = local_url.split('/')
70 # if a language is already in the path, remove it
73 local_url = '/'.join(spath)
75 # Try to match an endpoint in werkzeug's routing table
76 url = local_url.split('?')
78 query_string = url[1] if len(url) > 1 else None
79 router = request.httprequest.app.get_db_router(request.db).bind('')
80 func = router.match(path, query_args=query_string)[0]
81 return func.routing.get('website', False) and func.routing.get('multilang', True)
85 def slugify(s, max_length=None):
86 """ Transform a string to a slug that can be used in a url path.
88 This method will first try to do the job with python-slugify if present.
89 Otherwise it will process string by stripping leading and ending spaces,
90 converting unicode chars to ascii, lowering all chars and replacing spaces
91 and underscore with hyphen "-".
94 :param max_length: int
99 # There are 2 different libraries only python-slugify is supported
101 return slugify_lib.slugify(s, max_length=max_length)
104 uni = unicodedata.normalize('NFKD', s).encode('ascii', 'ignore').decode('ascii')
105 slug = re.sub('[\W_]', ' ', uni).strip().lower()
106 slug = re.sub('[-\s]+', '-', slug)
108 return slug[:max_length]
111 if isinstance(value, orm.browse_record):
112 # [(id, name)] = value.name_get()
113 id, name = value.id, value.display_name
115 # assume name_search result tuple
117 slugname = slugify(name or '').strip().strip('-')
120 return "%s-%d" % (slugname, id)
123 # NOTE: as the pattern is used as it for the ModelConverter (ir_http.py), do not use any flags
124 _UNSLUG_RE = re.compile(r'(?:(\w{1,2}|\w[A-Za-z0-9-_]+?\w)-)?(-?\d+)(?=$|/)')
127 """Extract slug and id from a string.
128 Always return un 2-tuple (str|None, int|None)
130 m = _UNSLUG_RE.match(s)
133 return m.group(1), int(m.group(2))
135 def urlplus(url, params):
136 return werkzeug.Href(url)(params or None)
138 class website(osv.osv):
139 def _get_menu_website(self, cr, uid, ids, context=None):
140 # IF a menu is changed, update all websites
141 return self.search(cr, uid, [], context=context)
143 def _get_menu(self, cr, uid, ids, name, arg, context=None):
144 root_domain = [('parent_id', '=', False)]
145 menus = self.pool.get('website.menu').search(cr, uid, root_domain, order='id', context=context)
146 menu = menus and menus[0] or False
147 return dict( map(lambda x: (x, menu), ids) )
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 'partner_id': fields.related('user_id','partner_id', type='many2one', relation='res.partner', string='Public Partner'),
166 'menu_id': fields.function(_get_menu, relation='website.menu', type='many2one', string='Main Menu',
168 'website.menu': (_get_menu_website, ['sequence','parent_id','website_id'], 10)
173 'company_id': lambda self,cr,uid,c: self.pool['ir.model.data'].xmlid_to_res_id(cr, openerp.SUPERUSER_ID, 'base.public_user'),
176 # cf. Wizard hack in website_views.xml
177 def noop(self, *args, **kwargs):
180 def write(self, cr, uid, ids, vals, context=None):
181 self._get_languages.clear_cache(self)
182 return super(website, self).write(cr, uid, ids, vals, context)
184 def new_page(self, cr, uid, name, template='website.default_page', ispage=True, context=None):
185 context = context or {}
186 imd = self.pool.get('ir.model.data')
187 view = self.pool.get('ir.ui.view')
188 template_module, template_name = template.split('.')
190 # completely arbitrary max_length
191 page_name = slugify(name, max_length=50)
192 page_xmlid = "%s.%s" % (template_module, page_name)
196 imd.get_object_reference(cr, uid, template_module, page_name)
199 _, template_id = imd.get_object_reference(cr, uid, template_module, template_name)
200 page_id = view.copy(cr, uid, template_id, context=context)
201 page = view.browse(cr, uid, page_id, context=context)
203 'arch': page.arch.replace(template, page_xmlid),
207 imd.create(cr, uid, {
209 'module': template_module,
210 'model': 'ir.ui.view',
216 def page_for_name(self, cr, uid, ids, name, module='website', context=None):
218 return '%s.%s' % (module, slugify(name, max_length=50))
220 def page_exists(self, cr, uid, ids, name, module='website', context=None):
222 name = (name or "").replace("/page/website.", "").replace("/page/", "")
225 return self.pool["ir.model.data"].get_object_reference(cr, uid, module, name)
229 @openerp.tools.ormcache(skiparg=3)
230 def _get_languages(self, cr, uid, id, context=None):
231 website = self.browse(cr, uid, id)
232 return [(lg.code, lg.name) for lg in website.language_ids]
234 def get_languages(self, cr, uid, ids, context=None):
235 return self._get_languages(cr, uid, ids[0], context=context)
237 def get_alternate_languages(self, cr, uid, ids, req=None, context=None):
240 req = request.httprequest
241 default = self.get_current_website(cr, uid, context=context).default_lang_code
244 uri += '?' + req.query_string
246 for code, name in self.get_languages(cr, uid, ids, context=context):
247 lg_path = ('/' + code) if code != default else ''
251 'hreflang': ('-'.join(lg)).lower(),
253 'href': req.url_root[0:-1] + lg_path + uri,
257 if shorts.count(lang['short']) == 1:
258 lang['hreflang'] = lang['short']
261 def get_current_website(self, cr, uid, context=None):
262 # TODO: Select website, currently hard coded
263 return self.pool['website'].browse(cr, uid, 1, context=context)
265 def is_publisher(self, cr, uid, ids, context=None):
266 Access = self.pool['ir.model.access']
267 is_website_publisher = Access.check(cr, uid, 'ir.ui.view', 'write', False, context=context)
268 return is_website_publisher
270 def is_user(self, cr, uid, ids, context=None):
271 Access = self.pool['ir.model.access']
272 return Access.check(cr, uid, 'ir.ui.menu', 'read', False, context=context)
274 def get_template(self, cr, uid, ids, template, context=None):
275 if isinstance(template, (int, long)):
278 if '.' not in template:
279 template = 'website.%s' % template
280 module, xmlid = template.split('.', 1)
281 model, view_id = request.registry["ir.model.data"].get_object_reference(cr, uid, module, xmlid)
282 return self.pool["ir.ui.view"].browse(cr, uid, view_id, context=context)
284 def _render(self, cr, uid, ids, template, values=None, context=None):
285 # TODO: remove this. (just kept for backward api compatibility for saas-3)
286 return self.pool['ir.ui.view'].render(cr, uid, template, values=values, context=context)
288 def render(self, cr, uid, ids, template, values=None, status_code=None, context=None):
289 # TODO: remove this. (just kept for backward api compatibility for saas-3)
290 return request.render(template, values, uid=uid)
292 def pager(self, cr, uid, ids, url, total, page=1, step=30, scope=5, url_args=None, context=None):
294 page_count = int(math.ceil(float(total) / step))
296 page = max(1, min(int(page if str(page).isdigit() else 1), page_count))
299 pmin = max(page - int(math.floor(scope/2)), 1)
300 pmax = min(pmin + scope, page_count)
302 if pmax - pmin < scope:
303 pmin = pmax - scope if pmax - scope > 0 else 1
306 _url = "%s/page/%s" % (url, page) if page > 1 else url
308 _url = "%s?%s" % (_url, werkzeug.url_encode(url_args))
312 "page_count": page_count,
313 "offset": (page - 1) * step,
315 'url': get_url(page),
319 'url': get_url(pmin),
323 'url': get_url(max(pmin, page - 1)),
324 'num': max(pmin, page - 1)
327 'url': get_url(min(pmax, page + 1)),
328 'num': min(pmax, page + 1)
331 'url': get_url(pmax),
335 {'url': get_url(page), 'num': page}
336 for page in xrange(pmin, pmax+1)
340 def rule_is_enumerable(self, rule):
341 """ Checks that it is possible to generate sensible GET queries for
342 a given rule (if the endpoint matches its own requirements)
344 :type rule: werkzeug.routing.Rule
347 endpoint = rule.endpoint
348 methods = rule.methods or ['GET']
349 converters = rule._converters.values()
350 if not ('GET' in methods
351 and endpoint.routing['type'] == 'http'
352 and endpoint.routing['auth'] in ('none', 'public')
353 and endpoint.routing.get('website', False)
354 and all(hasattr(converter, 'generate') for converter in converters)
355 and endpoint.routing.get('website')):
358 # dont't list routes without argument having no default value or converter
359 spec = inspect.getargspec(endpoint.method.original_func)
361 # remove self and arguments having a default value
362 defaults_count = len(spec.defaults or [])
363 args = spec.args[1:(-defaults_count or None)]
365 # check that all args have a converter
366 return all( (arg in rule._converters) for arg in args)
368 def enumerate_pages(self, cr, uid, ids, query_string=None, context=None):
369 """ Available pages in the website/CMS. This is mostly used for links
370 generation and can be overridden by modules setting up new HTML
371 controllers for dynamic pages (e.g. blog).
373 By default, returns template views marked as pages.
375 :param str query_string: a (user-provided) string, fetches pages
377 :returns: a list of mappings with two keys: ``name`` is the displayable
378 name of the resource (page), ``url`` is the absolute URL
380 :rtype: list({name: str, url: str})
382 router = request.httprequest.app.get_db_router(request.db)
383 # Force enumeration to be performed as public user
385 for rule in router.iter_rules():
386 if not self.rule_is_enumerable(rule):
389 converters = rule._converters or {}
390 if query_string and not converters and (query_string not in rule.build([{}], append_unknown=False)[1]):
393 convitems = converters.items()
394 # converters with a domain are processed after the other ones
395 gd = lambda x: hasattr(x[1], 'domain') and (x[1].domain <> '[]')
396 convitems.sort(lambda x, y: cmp(gd(x), gd(y)))
397 for (i,(name, converter)) in enumerate(convitems):
400 query = i==(len(convitems)-1) and query_string
401 for v in converter.generate(request.cr, uid, query=query, args=val, context=context):
402 newval.append( val.copy() )
409 domain_part, url = rule.build(value, append_unknown=False)
411 for key,val in value.items():
412 if key.startswith('__'):
414 if url in ('/sitemap.xml',):
422 def search_pages(self, cr, uid, ids, needle=None, limit=None, context=None):
423 name = (needle or "").replace("/page/website.", "").replace("/page/", "")
425 for page in self.enumerate_pages(cr, uid, ids, query_string=name, context=context):
426 if needle in page['loc']:
428 if len(res) == limit:
432 def kanban(self, cr, uid, ids, model, domain, column, template, step=None, scope=None, orderby=None, context=None):
433 step = step and int(step) or 10
434 scope = scope and int(scope) or 5
435 orderby = orderby or "name"
437 get_args = dict(request.httprequest.args or {})
438 model_obj = self.pool[model]
439 relation = model_obj._columns.get(column)._obj
440 relation_obj = self.pool[relation]
442 get_args.setdefault('kanban', "")
443 kanban = get_args.pop('kanban')
444 kanban_url = "?%s&kanban=" % werkzeug.url_encode(get_args)
447 for col in kanban.split(","):
450 pages[int(col[0])] = int(col[1])
453 for group in model_obj.read_group(cr, uid, domain, ["id", column], groupby=column):
457 relation_id = group[column][0]
458 obj['column_id'] = relation_obj.browse(cr, uid, relation_id)
460 obj['kanban_url'] = kanban_url
461 for k, v in pages.items():
463 obj['kanban_url'] += "%s-%s" % (k, v)
466 number = model_obj.search(cr, uid, group['__domain'], count=True)
467 obj['page_count'] = int(math.ceil(float(number) / step))
468 obj['page'] = pages.get(relation_id) or 1
469 if obj['page'] > obj['page_count']:
470 obj['page'] = obj['page_count']
471 offset = (obj['page']-1) * step
472 obj['page_start'] = max(obj['page'] - int(math.floor((scope-1)/2)), 1)
473 obj['page_end'] = min(obj['page_start'] + (scope-1), obj['page_count'])
476 obj['domain'] = group['__domain']
479 obj['orderby'] = orderby
482 object_ids = model_obj.search(cr, uid, group['__domain'], limit=step, offset=offset, order=orderby)
483 obj['object_ids'] = model_obj.browse(cr, uid, object_ids)
490 'template': template,
492 return request.website._render("website.kanban_contain", values)
494 def kanban_col(self, cr, uid, ids, model, domain, page, template, step, orderby, context=None):
496 model_obj = self.pool[model]
497 domain = safe_eval(domain)
499 offset = (int(page)-1) * step
500 object_ids = model_obj.search(cr, uid, domain, limit=step, offset=offset, order=orderby)
501 object_ids = model_obj.browse(cr, uid, object_ids)
502 for object_id in object_ids:
503 html += request.website._render(template, {'object_id': object_id})
506 def _image_placeholder(self, response):
507 # file_open may return a StringIO. StringIO can be closed but are
508 # not context managers in Python 2 though that is fixed in 3
509 with contextlib.closing(openerp.tools.misc.file_open(
510 os.path.join('web', 'static', 'src', 'img', 'placeholder.png'),
512 response.data = f.read()
513 return response.make_conditional(request.httprequest)
515 def _image(self, cr, uid, model, id, field, response, max_width=maxint, max_height=maxint, cache=None, context=None):
516 """ Fetches the requested field and ensures it does not go above
517 (max_width, max_height), resizing it if necessary.
519 Resizing is bypassed if the object provides a $field_big, which will
520 be interpreted as a pre-resized version of the base field.
522 If the record is not found or does not have the requested field,
523 returns a placeholder image via :meth:`~._image_placeholder`.
525 Sets and checks conditional response parameters:
526 * :mailheader:`ETag` is always set (and checked)
527 * :mailheader:`Last-Modified is set iif the record has a concurrency
528 field (``__last_update``)
530 The requested field is assumed to be base64-encoded image data in
533 Model = self.pool[model]
536 ids = Model.search(cr, uid,
537 [('id', '=', id)], context=context)
538 if not ids and 'website_published' in Model._all_columns:
539 ids = Model.search(cr, openerp.SUPERUSER_ID,
540 [('id', '=', id), ('website_published', '=', True)], context=context)
542 return self._image_placeholder(response)
544 concurrency = '__last_update'
545 [record] = Model.read(cr, openerp.SUPERUSER_ID, [id],
546 [concurrency, field],
549 if concurrency in record:
550 server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
552 response.last_modified = datetime.datetime.strptime(
553 record[concurrency], server_format + '.%f')
555 # just in case we have a timestamp without microseconds
556 response.last_modified = datetime.datetime.strptime(
557 record[concurrency], server_format)
559 # Field does not exist on model or field set to False
560 if not record.get(field):
561 # FIXME: maybe a field which does not exist should be a 404?
562 return self._image_placeholder(response)
564 response.set_etag(hashlib.sha1(record[field]).hexdigest())
565 response.make_conditional(request.httprequest)
568 response.cache_control.max_age = cache
569 response.expires = int(time.time() + cache)
571 # conditional request match
572 if response.status_code == 304:
575 data = record[field].decode('base64')
577 if (not max_width) and (not max_height):
581 image = Image.open(cStringIO.StringIO(data))
582 response.mimetype = Image.MIME[image.format]
585 max_w = int(max_width) if max_width else maxint
586 max_h = int(max_height) if max_height else maxint
588 if w < max_w and h < max_h:
591 size = (max_w, max_h)
592 img = image_resize_and_sharpen(image, size)
593 image_save_for_web(img, response.stream, format=image.format)
594 # invalidate content-length computed by make_conditional as
595 # writing to response.stream does not do it (as of werkzeug 0.9.3)
596 del response.headers['Content-Length']
600 def image_url(self, cr, uid, record, field, size=None, context=None):
601 """Returns a local url that points to the image field of a given browse record."""
603 id = '%s_%s' % (record.id, hashlib.sha1(record.write_date).hexdigest()[0:7])
604 size = '' if size is None else '-%s' % size
605 return '/website/image/%s-%s-%s%s' % (model, id, field, size)
608 class website_menu(osv.osv):
609 _name = "website.menu"
610 _description = "Website Menu"
612 'name': fields.char('Menu', required=True, translate=True),
613 'url': fields.char('Url'),
614 'new_window': fields.boolean('New Window'),
615 'sequence': fields.integer('Sequence'),
616 # TODO: support multiwebsite once done for ir.ui.views
617 'website_id': fields.many2one('website', 'Website'),
618 'parent_id': fields.many2one('website.menu', 'Parent Menu', select=True, ondelete="cascade"),
619 'child_id': fields.one2many('website.menu', 'parent_id', string='Child Menus'),
620 'parent_left': fields.integer('Parent Left', select=True),
621 'parent_right': fields.integer('Parent Right', select=True),
624 def __defaults_sequence(self, cr, uid, context):
625 menu = self.search_read(cr, uid, [(1,"=",1)], ["sequence"], limit=1, order="sequence DESC", context=context)
626 return menu and menu[0]["sequence"] or 0
630 'sequence': __defaults_sequence,
634 _parent_order = 'sequence'
637 # would be better to take a menu_id as argument
638 def get_tree(self, cr, uid, website_id, context=None):
644 new_window=node.new_window,
645 sequence=node.sequence,
646 parent_id=node.parent_id.id,
649 for child in node.child_id:
650 menu_node['children'].append(make_tree(child))
652 menu = self.pool.get('website').browse(cr, uid, website_id, context=context).menu_id
653 return make_tree(menu)
655 def save(self, cr, uid, website_id, data, context=None):
656 def replace_id(old_id, new_id):
657 for menu in data['data']:
658 if menu['id'] == old_id:
660 if menu['parent_id'] == old_id:
661 menu['parent_id'] = new_id
662 to_delete = data['to_delete']
664 self.unlink(cr, uid, to_delete, context=context)
665 for menu in data['data']:
667 if isinstance(mid, str):
668 new_id = self.create(cr, uid, {'name': menu['name']}, context=context)
669 replace_id(mid, new_id)
670 for menu in data['data']:
671 self.write(cr, uid, [menu['id']], menu, context=context)
674 class ir_attachment(osv.osv):
675 _inherit = "ir.attachment"
676 def _website_url_get(self, cr, uid, ids, name, arg, context=None):
678 for attach in self.browse(cr, uid, ids, context=context):
680 result[attach.id] = attach.url
682 result[attach.id] = self.pool['website'].image_url(cr, uid, attach, 'datas')
684 def _datas_checksum(self, cr, uid, ids, name, arg, context=None):
686 (attach['id'], self._compute_checksum(attach))
687 for attach in self.read(
688 cr, uid, ids, ['res_model', 'res_id', 'type', 'datas'],
692 def _compute_checksum(self, attachment_dict):
693 if attachment_dict.get('res_model') == 'ir.ui.view'\
694 and not attachment_dict.get('res_id') and not attachment_dict.get('url')\
695 and attachment_dict.get('type', 'binary') == 'binary'\
696 and attachment_dict.get('datas'):
697 return hashlib.new('sha1', attachment_dict['datas']).hexdigest()
700 def _datas_big(self, cr, uid, ids, name, arg, context=None):
701 result = dict.fromkeys(ids, False)
702 if context and context.get('bin_size'):
705 for record in self.browse(cr, uid, ids, context=context):
706 if not record.datas: continue
708 result[record.id] = openerp.tools.image_resize_image_big(record.datas)
709 except IOError: # apparently the error PIL.Image.open raises
715 'datas_checksum': fields.function(_datas_checksum, size=40,
716 string="Datas checksum", type='char', store=True, select=True),
717 'website_url': fields.function(_website_url_get, string="Attachment URL", type='char'),
718 'datas_big': fields.function (_datas_big, type='binary', store=True,
719 string="Resized file content"),
720 'mimetype': fields.char('Mime Type', readonly=True),
723 def _add_mimetype_if_needed(self, values):
724 if values.get('datas_fname'):
725 values['mimetype'] = mimetypes.guess_type(values.get('datas_fname'))[0] or 'application/octet-stream'
727 def create(self, cr, uid, values, context=None):
728 chk = self._compute_checksum(values)
730 match = self.search(cr, uid, [('datas_checksum', '=', chk)], context=context)
733 self._add_mimetype_if_needed(values)
734 return super(ir_attachment, self).create(
735 cr, uid, values, context=context)
737 def write(self, cr, uid, ids, values, context=None):
738 self._add_mimetype_if_needed(values)
739 return super(ir_attachment, self).write(cr, uid, ids, values, context=context)
741 def try_remove(self, cr, uid, ids, context=None):
742 """ Removes a web-based image attachment if it is used by no view
745 Returns a dict mapping attachments which would not be removed (if any)
746 mapped to the views preventing their removal
748 Views = self.pool['ir.ui.view']
749 attachments_to_remove = []
750 # views blocking removal of the attachment
751 removal_blocked_by = {}
753 for attachment in self.browse(cr, uid, ids, context=context):
754 # in-document URLs are html-escaped, a straight search will not
756 url = escape(attachment.website_url)
757 ids = Views.search(cr, uid, ["|", ('arch', 'like', '"%s"' % url), ('arch', 'like', "'%s'" % url)], context=context)
760 removal_blocked_by[attachment.id] = Views.read(
761 cr, uid, ids, ['name'], context=context)
763 attachments_to_remove.append(attachment.id)
764 if attachments_to_remove:
765 self.unlink(cr, uid, attachments_to_remove, context=context)
766 return removal_blocked_by
768 class res_partner(osv.osv):
769 _inherit = "res.partner"
771 def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None):
772 partner = self.browse(cr, uid, ids[0], context=context)
774 '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 ''),
775 'size': "%sx%s" % (height, width),
779 return urlplus('http://maps.googleapis.com/maps/api/staticmap' , params)
781 def google_map_link(self, cr, uid, ids, zoom=8, context=None):
782 partner = self.browse(cr, uid, ids[0], context=context)
784 '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 ''),
787 return urlplus('https://maps.google.com/maps' , params)
789 class res_company(osv.osv):
790 _inherit = "res.company"
791 def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None):
792 partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id
793 return partner and partner.google_map_img(zoom, width, height, context=context) or None
794 def google_map_link(self, cr, uid, ids, zoom=8, context=None):
795 partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id
796 return partner and partner.google_map_link(zoom, context=context) or None
798 class base_language_install(osv.osv_memory):
799 _inherit = "base.language.install"
801 'website_ids': fields.many2many('website', string='Websites to translate'),
804 def default_get(self, cr, uid, fields, context=None):
807 defaults = super(base_language_install, self).default_get(cr, uid, fields, context)
808 website_id = context.get('params', {}).get('website_id')
810 if 'website_ids' not in defaults:
811 defaults['website_ids'] = []
812 defaults['website_ids'].append(website_id)
815 def lang_install(self, cr, uid, ids, context=None):
818 action = super(base_language_install, self).lang_install(cr, uid, ids, context)
819 language_obj = self.browse(cr, uid, ids)[0]
820 website_ids = [website.id for website in language_obj['website_ids']]
821 lang_id = self.pool['res.lang'].search(cr, uid, [('code', '=', language_obj['lang'])])
822 if website_ids and lang_id:
823 data = {'language_ids': [(4, lang_id[0])]}
824 self.pool['website'].write(cr, uid, website_ids, data)
825 params = context.get('params', {})
826 if 'url_return' in params:
828 'url': params['url_return'].replace('[lang]', language_obj['lang']),
829 'type': 'ir.actions.act_url',
834 class website_seo_metadata(osv.Model):
835 _name = 'website.seo.metadata'
836 _description = 'SEO metadata'
839 'website_meta_title': fields.char("Website meta title", translate=True),
840 'website_meta_description': fields.text("Website meta description", translate=True),
841 'website_meta_keywords': fields.char("Website meta keywords", translate=True),